2023.04.28 - 2023.05.31

UNCOVER

Team. Undefined

Summary

6인 팀 프로젝트 / No Copyright Music 서비스 플랫폼

저작권이 자유로는 음악들을 서비스하는 웹사이트입니다.

평소 EDM 음악을 즐겨 듣는 저의 취미에서 착안하여, 인터랙티브 웹을 강조한 기획을 진행했습니다.

Java와 Spring Boot를 활용한 첫 팀 프로젝트로 팀원들과 함께 처음 시도하는 프로젝트였기에 서로 질의응답하며

고민하는 과정이 흥미로웠고, 커뮤니케이션 능력 향상과 함께 Java, Spring Boot에 대한 익숙함도 함께 얻을 수 있었습니다.

백엔드 개발자 3명, 프론트엔드 개발자 3명으로 구성된 팀 프로젝트에서 저는 백엔드 개발자로서 아래와 같은 역할을 담당했습니다.

Team Lead

- 프로젝트 기획 및 전반적인 개발 진행에 관여

- 효율적인 협업을 위한 가이드 문서 작성

- 인터랙티브 웹을 위한 디자인 방향 제시 및 개선

- 오디오 음질 개선 및 DB 공간 확보를 위한 전략 제시

- 레퍼런스 사이트 및 디자인 탐색

- 플레이리스트 및 뮤직 배너 이미지를 GIF 형식으로 변경

- 감성적인 제목 및 본문 작성

- 아키텍처 설계 및 리팩토링

Backend Development

- 플레이리스트, 좋아요, 댓글, 태그 기능 개발

- 좋아요 및 댓글 기능에서 발생할 수 있는 동시성 문제 해결

- ngrok을 활용한 클라이언트와의 실시간 테스트 진행

- 인기 있는 노래를 기반으로 한 플레이리스트 추천 기능 개발

- 유저 선호 태그가 포함된 플레이리스트를 상단에 노출시키는 기능 개발

사용 기술 스택

Frameworks: Spring Boot(Web, Data JPA, Validation), MapStruct

Authentication: JWT

Server: AWS EC2, S3, RDS

Database: H2(in-memory), MySQL

API Documentation: Apache HttpClient

Utilities: Lombok, Gson

Testing: JUnit, Security Test

Experience

이 프로젝트에서는 그동안 학습했던 ERD 설계를 실습해 볼 수 있었습니다.

저를 포함한 3명의 백엔드 개발자와 네이버 클라우드 멘토 개발자의 도움을 받아

먼저 사용자, 음악, 플레이리스트, 태그, 좋아요 와 같은 주요 엔티티와 그 관계를 정의하며,

서비스에서 요구되는 데이터 흐름과 비즈니스 로직을 중심으로 논의를 진행했습니다.

특히, 음악, 플레이리스트, 태그 간의 다대다 관계를 설계하는 과정에서 많은 고민이 있었습니다.

초기에는 다대다 구조를 유지한 상태로 테스트를 진행했으나, 중복 데이터 생성과

관련 데이터의 반복적인 조회로 인해 성능이 크게 저하되는 문제가 발생했습니다.

이를 해결하기 위해 멘토의 피드백을 반영하여,

다대다 관계를 musicTag와 playListTag라는 중간 테이블로 분리해 일대다 관계로 재구성하고,

조인 테이블에 인덱스를 추가하여 조회 성능을 최적화했습니다.

그 결과, 데이터베이스 조회 시간이 O(n)에서 O(log n) 수준으로 개선되었고,

소규모 데이터 환경에서 약 27%의 성능 향상을 달성할 수 있었습니다.

프로젝트 테스트 과정에서, 동시에 여러 사용자가 좋아요 기능을 사용할 때 좋아요 수가 1만 증가하는 동시성 문제가 발생했습니다.

처음에는 간단히 Lock을 사용하는 방법을 고려했으나, 다중 요청이 동시에 몰리는 경우

대기 시간이 길어지고 경합 조건이나 데드락 발생 가능성이 있어 적합하지 않다고 판단했습니다.

이후 병렬 스트림을 활용하는 방안을 검토했지만, 병렬 처리 중 데이터의 순서가 유지되지 않는 문제와

동시화 및 병렬화로 인한 추가적인 오버헤드가 발생할 가능성이 있었습니다.

이를 해결하기 위해 고유한 값인 memberId를 활용하여 List에 저장하고,

해당 List의 크기를 좋아요 개수로 반환하는 방식으로 문제를 해결했습니다.

이 방식은 관리자가 작업하기 용이하며, 향후 시스템 버전과의 호환성을 유지할 가능성도 높아 실용적이라고 판단했습니다.

소스코드 보기

            
              @OneToMany(mappedBy = "playList", cascade = {CascadeType.ALL})
              private List<PlayListLike> playListLikes = new ArrayList<>();
  
              @Column(nullable = false)
              private int likeCount = this.playListLikes.size();
  
              public void addPlayListLike(PlayListLike playListLike) {
                  this.playListLikes.add(playListLike);
                  playListLike.setPlayList(this);
                  this.likeCount = this.playListLikes.size();
              }
  
              public void removePlayListLike(PlayListLike playListLike) {
                  this.playListLikes.remove(playListLike);
                  if(playListLike.getPlayList() != this) {
                      playListLike.setPlayList(this);
                  }
                  this.likeCount = this.playListLikes.size();
              }
  
              // --------------- playListLike Service -----------------
              public PlayListLike addLike(Long memberId, Long playListId){
                  Member member = memberService.findMember(memberId);
                  PlayList playList = playListService.findVerifiedPlayList(playListId);
          
                  PlayListLike like = new PlayListLike();
                  like.setMember(member);
                  like.setPlayList(playList);
          
                  playList.addPlayListLike(like);
                  member.addLikedPlayLists(like);
          
                  return playListLikeRepository.save(like);
              }
          
              public void cancelLike(Long memberId, Long playListId){
                  Member member = memberService.findMember(memberId);
                  PlayList playList = playListService.findVerifiedPlayList(playListId);
                  List<PlayListLike> likes = getAllLikesForMemberAndPlayList(memberId, playListId);
          
                  for (PlayListLike like : likes) {
                      if (member.getMemberId().equals(like.getMember().getMemberId())) {
                          playListLikeRepository.delete(like);
                          playList.removePlayListLike(like);
                          member.removeLikedPlayLists(like);
                      } else throw new BusinessLogicException(ExceptionCode.NO_PERMISSION_EDITING_COMMENT);
                  }
              }
            
          

이전 협업에서 팀원들이 적극적으로 참여하거나 아이디어를 제시하지 않는 상황을 경험한 적이 있습니다.

이러한 경험을 바탕으로 이번 프로젝트에서는 팀 리더로서 기획, 아키텍처 설계, 디자인, 개발 등

전반적인 과정에 적극적으로 참여하며, 팀원들이 능동적으로 협업에 참여할 수 있는 환경을 조성하는 데 주력했습니다.

특히 팀원들과의 커뮤니케이션을 강조하며, 자유롭게 의견을 공유할 수 있는 분위기를 형성하고자 노력했습니다.

API 명세, ERD 설계, 트러블슈팅 등 기술적인 논의 과정에서는 각자의 아이디어와 전문성을 존중하며 이를 조율해 나갔습니다.

정기적인 회의, 명확한 업무 분담, 효과적인 의사결정을 통해 팀원들과 원활하게 소통했으며,

이러한 협업은 프로젝트 일정 관리와 업무 효율성 향상에 크게 기여했습니다.