2023.04.28 ~ 2023.05.31

UNCOVER

Team. Undefined

Summary

Team Lead / Back-End Development

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

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

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

Java와 Spring Boot를 활용한 프로젝트로, 팀 내에서의 첫 번째 경험이었습니다.

팀원들과 함께 처음 시도하는 프로젝트였기에 서로 질의응답하며 고민하는 과정이 흥미로웠고, 이를 통해 많이 배울 수 있었습니다.

커뮤니케이션 능력 향상과 함께 Java, Spring Boot에 대한 익숙함도 함께 얻을 수 있었습니다.

이 프로젝트는 이전까지의 공부와는 다르게 팀 프로젝트를 성공적으로 마무리한 경험이었습니다.

이를 통해 자발적인 학습과 실전 경험의 중요성을 깨달았으며, 이전과는 다른 수준의 자신감을 얻을 수 있었습니다.

Experience

이 프로젝트에서 저는 팀 리더로서 기획부터 아키텍처 설계, 가이드 문서 작성, 디자인 아이디어 제시를 주도했습니다.

기술적인 측면에서는 주로 플레이리스트와 관련된 부분에 집중했습니다.

구체적으로는 플레이리스트, 플레이리스트 댓글, 좋아요 기능, 그리고 태그 및 플레이리스트 태그를 구현하는 등의 작업을 수행했습니다.

플레이리스트는 멤버, 음악, 태그, 댓글, 좋아요 등 모든 부분과 연결되어 있어 예상보다 복잡한 작업이었습니다.

미숙함으로 인해 다른 테이블과의 연관관계 매핑이 어려웠고, 이에 따른 작업이 상당한 시간을 필요로 했습니다.

초기에는 태그 테이블이 모든 테이블에 연결되어 있는 다대다 구조였습니다.

구현 단계에서 데이터 중복, 쿼리의 복잡성 등의 문제가 발생했고,

하이버네이트에 의해 생성된 중간 테이블은 비즈니스 로직상 필요한 정보들이 담기지 않았습니다.

이에 따라 ERD를 수정하여 뮤직 태그와 플레이리스트 태그 테이블을 따로 만들고 일대다 관계로 변경하였습니다.

소스코드 보기

            
              @ElementCollection(fetch = FetchType.LAZY)
              private List<String> tags = new ArrayList<>();
    
              @OneToMany(mappedBy = "playList", cascade = {CascadeType.ALL})
              private List<PlayListTag> playListTags = new ArrayList<>();
    
              @OneToMany(mappedBy = "playList", cascade = {CascadeType.ALL})
              private List<PlayListLike> playListLikes = new ArrayList<>();
    
              @OneToMany(mappedBy = "playList", cascade = {CascadeType.ALL})
              private List<PlayListComment> playListComments = new ArrayList<>();
    
              @Column(nullable = false)
              private int likeCount = this.playListLikes.size();
    
              @OneToMany(mappedBy = "playList", cascade = {CascadeType.ALL})
              private List<PlayListMusic> playlistMusics = new ArrayList<>();
            
          

또한 프로젝트가 진행되면서, 사용자의 역할(role)에 따른 권한 설정에 대한 이슈도 함께 다뤘습니다.

이는 관리자와 사용자 간의 권한을 나누어 각각 다른 기능을 제공하도록 하였습니다.

이 부분은 프로젝트 시작 전부터 공부하고 싶었던 주제였으며,

실제로 구현해보니 이론에서 얻지 못했던 깊은 이해와 새로운 인사이트를 얻을 수 있었습니다.

관리자 계정은 노래(신곡) 추가, 추천 플레이리스트 생성 외에도 댓글 관리, 프로필 사진 관리, 플레이리스트 사진 관리, 계정 정지 등

role 시스템을 도입하여 부적절한 요소를 제거하고 페이지를 관리하도록 하여 쾌적한 유저 환경을 조성하고자 했습니다.

소스코드 보기

            
              public Member createMember(Member member) {
                  verifyExistEmail(member.getEmail());
                  member.setName(verifyExistName(member.getName()));
          
                  // (3) 추가: Password 암호화
                  String encryptedPassword = passwordEncoder.encode(member.getPassword());
                  member.setPassword(encryptedPassword);
          
                  // (4) 추가: DB에 User Role 저장
                  List<String> roles = authorityUtils.createRoles(member.getEmail());
                  member.setRoles(roles);
          
                  Member savedMember = memberRepository.save(member);
          
                  return savedMember;
              }

              public Member createMemberOAuth2(Member member) {
                  List<String> roles = authorityUtils.createRoles(member.getEmail());
                  member.setRoles(roles);
                  String newName = verifyExistName(member.getName());
                  member.setName(newName);
          
                  return memberRepository.save(member);
              }

              public void deletePlayList(long playListId, long memberId){
                  PlayList playList = findVerifiedPlayList(playListId);
                  Member member = memberService.findMember(memberId);
          
                  if (!member.getRoles().contains("ADMIN")) {
                      if (!member.getMemberId().equals(playList.getMember().getMemberId())){
                          throw new BusinessLogicException(ExceptionCode.NO_PERMISSION_DELETING_POST);
                      }
                  }
                  member.removePlayList(playList);
                  playListRepository.delete(playList);
              }

              public PlayListComment updateComment(long memberId, long commentId, String content){
                  PlayListComment comment = getComment(commentId);
                  Member member = memberService.findMember(memberId);
          
                  if (!member.getRoles().contains("ADMIN")) {
                      if (!member.getMemberId().equals(comment.getMember().getMemberId())){
                          throw new BusinessLogicException(ExceptionCode.NO_PERMISSION_DELETING_POST);
                      }
                  }
          
                  comment.setContent(content);
                  return commentRepository.save(comment);
              }

              public void deleteComment(long memberId, long commentId){
                  PlayListComment comment = getComment(commentId);
                  Member member = memberService.findMember(memberId);
          
                  if (!member.getRoles().contains("ADMIN")) {
                      if (!member.getMemberId().equals(comment.getMember().getMemberId())){
                          throw new BusinessLogicException(ExceptionCode.NO_PERMISSION_DELETING_POST);
                      }
                  }
                  commentRepository.delete(comment);
              }
            
          

마지막으로 프로젝트 중에 발생한 동시성 문제에 대한 해결책 모색과 코드 리팩토링을 통해 성능 개선을 이루어냈습니다.

동시에 여러 사용자가 좋아요 기능을 사용할 때, 좋아요 수는 1만 증가하는 이 동시성 문제는 이번 프로젝트 중 가장 깊게 고민한 문제입니다.

처음에는 간단한 Lock 사용을 고려했지만, 여러 곳에서 동시에 요청이 몰리면

대기 시간이 길어져 경합 조건이나 데드락 등의 문제를 야기할 수 있다고 판단했습니다.

다음으로 병렬 스트림을 고려했으나, 연산을 병렬로 처리함으로써 원본 데이터의 순서가 유지되지 않으며,

동기화와 병렬화 오버헤드가 발생할 수 있었습니다.

고민 끝에 관리자가 작업하기 편리하고 새로운 버전과의 호환성을 유지할 가능성이 높도록

unique 값인 memberId를 사용해 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);
                  }
              }
            
          

동시성 문제 해결법 (블로그) >

이러한 어려움에 직면하면서도 팀원들과의 원활한 협업을 통해 공동의 목표를 달성하기 위해 노력했습니다.

사용자 피드백을 수렴하고, 이를 바탕으로 지속적인 업데이트 및 개선 작업을 진행하였습니다.

노래를 좋아요 순으로 정렬하고 추천 리스트에 표시하는 기능을 업데이트하였으며,

사용자가 많이 들은 노래의 태그에 해당하는 노래별로 카운트하여 비슷한 노래들을 추천하는 알고리즘도 개발 중에 있습니다.

앞으로는 이러한 경험을 기반으로 더욱 복잡하고 대규모의 프로젝트에서도 효과적으로

리더십을 발휘하고, 기술적인 도전에 더욱 안정적으로 대처해 나가고자 합니다.

Retrospective

프로젝트 동안 팀원들과의 원활한 커뮤니케이션은 프로젝트의 핵심이었습니다.

팀 프로젝트 이전에는 협업에서 내가 잘하고 내가 다 커버하면 된다는 마인드를 가지고 있었지만,

시간이 지날수록 사람들은 저에게 의존하면서 스스로 노력하지 않고,

적극적인 참여와 아이디어 제시를 하지 않는 상황이 되었습니다.

그래서 저의 행동을 반성하고 제 마인드의 방향을 바꾸어,

이 프로젝트에서는 팀원들에게 커뮤니케이션을 강조하고 자유롭게 의견을 공유할 수 있는 환경을 만들고자 하였습니다.

특히 API 명세, ERD 등 아키텍처나 트러블 슈팅에 관해 의견을 공유하고 조율하는 과정에서

서로의 아이디어와 전문성을 존중하고 융합할 수 있는 능력을 키울 수 있었습니다.

특히 정기적인 회의와 업무 분담, 의사 결정 과정에서의 원활한 소통은

프로젝트 일정을 효율적으로 관리하고 업무의 효율성을 높이는 데 기여했습니다.

프로젝트 마무리 단계에서 다른 팀의 서비스를 사용하면서 우리 팀과의 기술적인 차이를 경험했습니다.

이미지 업로드 기술, 멤버 페이지 디자인, OAuth 2 로그인 기능 등을 참고하여

기술적인 개선점을 도출하여 우리 팀의 프로젝트에 적용할 수 있는 아이디어를 얻을 수 있었습니다.

이를 향후 프로젝트에 지속적인 서비스로 적용하고 개선해 나가 보다 나은 결과물을 만들어 나갈 수 있도록 하고 싶습니다.