배경
팀원들과 기능별로 브랜치를 따로 파서 작업을 시작하기로 했다.
그리고 어느 정도 기능구현이 완료되었을 때 dev브랜치로 병합 한번 해보기로 결정했다.
처음 팀원들과 협업하는 프로젝트이다 보니 브랜치를 따로 설정해서 작업하는 것도 처음이고 merge 하는 것도 처음이라 merge 할 때 충돌이 일어날까 두려웠다.
팀원들과 회의 끝에 돌이킬 수 없는 문제가 발생할 수도 있으니 튜터님 앞에서 merge를 하기로 결정이 되었다.
원인
아니나 다를까 merge 버튼을 누르자 아래 사진예시와 같이 충돌이 일어났다.
충돌이 일어난 부분을 더블클릭하게 되면 아래 사진과 같은 창이 뜬다.
같은 줄에 서로 다른 코드가 있기 때문에 나타나는 문제이다.
dev브랜치라고 되어 있는 부분이 합치기 전 dev 브랜치에 있는 코드들이고, 맨 오른쪽 기능 브랜치라고 되어 있는 부분이 합치고 싶은 코드들이 있는 곳이다.
그리고 가운데가 최종적으로 합쳐져서 만들어지는 dev 브랜치가 된다.
만약, 충돌이 일어난 코드가 있으면 사진과 같이 추가할지 삭제할지에 대한 선택지가 나온다.
위 사진에서와 같이 이전 dev브랜치에서 살리고 싶은 코드가 있으면 >> 버튼을 누르고,
만약 필요가 없어 삭제하려면 X를 누르면 된다.
이 방법은 인텔리제이에서 제공하는 merge 기능으로 너무나도 간편하게 merge를 할 수 있게 해 준다.
그러나 이렇게 merge를 하려면 모든 팀원들이 모여있는 상태에서 서로 이야기를 하면서 해야 수월하게 merge가 된다는 단점이 있다.
만약, 팀원이 없는 상태에서 나 혼자서만 merge를 하려고 할 때 다른 사람의 코드 중 한 부분을 필요 없다 느껴 추가하지 않게 되는 불상사가 발생할 수도 있기 때문이다.
이런 단점을 보완하고, 더 효율적인 작업을 할 수 있게 하는 방법이 git에 PR(Pull & Request)를 보내는 것이다.
변경한 내용을 git에 push 하게 되면 위 사진과 같이 Compare & pull request라는 문구가 뜨는 걸 볼 수 있다.
이것을 누르고 들어가면 아래 사진과 같이 나온다.
여기에 어떠한 내용들을 변경해서 push를 했는지 팀원들이 확인해 줬으면 좋겠는지 이러한 내용을 적고 Create pull request 버튼을 클릭한다.
이후부터는 팀원들이 내가 pr을 요청한 코드에 대해 리뷰하면서 merge를 승인할지, 수정이 필요하면 comment를 남겨 보완할 수 있도록 한다.
이런 review 과정을 거치고 merge에 대한 승인이 결정되면 merge가 완료된다.
협업 시 github를 단순히 원격 저장소의 역할로만 활용하는 게 아닌 코드리뷰를 할 수 있는 소통의 창구로 활용할 수 있다.
배경
서로 각자 맡은 기능들을 개발하다가 한번 merge 한 후에 다시 작업을 하기로 결정했다.
merge 할 때 충돌이 많이 일어날 것 같아서 많이 두려웠지만, 생각보다 어려운 충돌은 일어나지 않았다.
그 이유는 각자 기능에 맞는 브랜치명을 따로 파서 작업했기 때문이다.
이후 실행이 잘 되는지 요청을 보내면서 하나하나 확인을 하는 작업을 거쳤다.
이때, 댓글 생성하는 기능에서 에러가 발생했다.
원인
ClassCastException이란 에러가 발생했고, 이를 찾아보니 형변환하는 과정에서 잘못되었다는 것이다.
로그인 확인을 하려고 session에 저장된 객체에서 userId를 가져오는 코드이다.
왜 여기에서 오류가 발생했지? 고민하다가 원인을 찾았다.
원인은 AuthController의 로그인 메서드에서 발생하였다.
session의 key는 userId로 설정해 놓고, 정작 값은 Long 타입의 id 값이 아닌 LoginResponseDto 형태의 객체를 저장을 했다.
처음에 팀원분과 상의할 때 sesssion key를 맞추자고 이야기가 나왔고, key는 userId로 했었다.
그러고 나서 '키에 해당하는 값에 어떤 것을 넣어야 할까?, User 객체를 넣어야 하나?, responseDto를 넣어야 하나?, 이 session을 나는 사용을 안 하는데?' 고민을 많이 했다.
결국, Dto를 저장하고 필요한 값을 Dto에서 꺼내와 사용하는 방법을 택했다.
하지만, 다른 기능을 개발하는 팀원은 위 객체에서 꺼내와 사용하는 방법이 아닌 userId 값이 저장되어 있다고 생각하고 기능을 구현한 것이다.
해결
session의 키 이름이 userId여서 값으로 저장되는 것은 id가 저장되어야 하는 게 맞았다.
팀원이 키 이름을 useId로 설정하자고 했을 때 또는 어떤 값을 넣어야 할지 고민했을 때 혼자서만 고민하지 말고 팀원과 소통했으면 일어날 문제가 아니었던 것이다.
협업을 할 때는 팀원과 상시로 소통을 하는 부분이 매우 중요하다고 깨달았다.
배경
프로젝트 요구사항에 비밀번호는 대소문자를 포함한 영문, 숫자, 특수문자 중 1개 이상을 포함해야 하며, 8글자 이상이어야 한다는 조건이 있었다.
이 조건에 부합하는 검사를 구현하고 팀원들이 구현한 기능들과 merge 하다 보니 에러는 아니지만, 고민되는 부분이 생겼다.
전개
회원가입기능을 구현할 때 비밀번호를 입력받는 부분에서 유효성 검사를 하고, 비밀번호를 수정하는 부분에서도 적합한 비밀번호 형식인지 유효성 검사를 실시한다.
하지만, 회원가입과 비밀번호 수정 기능을 서로 다른 팀원이 개발하다 보니 각자 다른 방법으로 유효성 검사를 구현한 것이다.
한 명은 RequestDto에 @Pattern을 사용하여 정규표현식으로 비밀번호 형식을 적용하고, RequestDto형태로 넘겨주기 위해 @RequestBody로 입력받을 때 정규표현식에 일치하는지 검사를 한다.
즉, Controller에서 유효성 검사를 @Vaild로 했다.
다른 한 명은 Service에서 정규표현식을 사용하여 비밀번호 형식을 검사하는 메서드를 만들었고, 이를 조건문으로 처리하였다.
동일한 검사를 서로 다른 방법으로 구현했다 보니 통일되지 않는다는 문제가 생겼다.
이를 해결하기 위해 계속해서 회의한 결과 답이 나오지 않았다.
그 이유는 통일성을 해결하고자 하나의 방법으로만 구현하려면 각자의 방법에서 또 다른 통일성이 깨지는 것 같은 문제가 발생했다.
@Valid를 사용하는 방법과 메서드를 사용하여 Service에서 구현하는 방법 중 어느 방법이 더 좋거나 나쁜 방법이라고 할 수는 없다. 두 방법 중에서 선택은 자유지만 사용하려면 하나의 방법만을 사용해서 통일성을 지켜주는 게 좋다.
여기서 또 한 번 협업 시 소통의 중요함을 느낀다.
배경
기존에는 @Manytoone 어노테이션을 사용해서 단방향 연관관계를 설정해서 CRUD를 구현했었다.
하지만, 이번에는 친구 요청 보낸 user와 친구 요청을 반는 user가 따로 존재해야 해서 양방향 연관관계를 설정해야 하는 상황이 발생했다.
양방향 연관관계는 사용해 보지 않아 일단 단방향 관계로 설정하고 기능들을 구현했다.
이렇게 단방향이다 보니 친구요청 보내기, 친구 요청 목록 조회, 요청 수락, 요청 거절, 요청받은 목록 조회는 구현이 가능했다.
하지만, 요청을 수락한 친구들 목록을 조회하고, 거기서 삭제하는 기능을 어떻게 구현해야 할지.... 많은 고민이 들었다.
원인
먼저, 응답받는 부분을 Dto 형태로 가져오고 싶었지만, Dto 형태로 작업하다 보니 난생처음 보는 Exception을 만났다.
처음에는 인텔리제이가 문제없이 작동되길래 기쁜 마음으로 postman을 활용해서 요청을 보내 보았다.
친구 요청이 1개가 왔을 땐 수락하고 나서 친구 목록을 조회하면 정상적으로 작동하길래 좋았었다.
하지만, 문제는 요청이 2개 이상이 될 때부터였다.
NonUniqueResultException: Query did not return a unique result: 3 results were returned 이란 에러가 뜨더니 목록이 조회가 안 되는 것이다!!!
NonUniqueResultException을 검색해 보니까 JPA에서 단일 결과를 반환해야 하는 쿼리가 여러 결과를 반환했을 때 발생하는 에러이다.
즉, 쿼리 조건에 맞는 데이터가 3개 이상 존재하며, 이를 단일 객체로 처리할 수 없어서 예외가 발생한 것이다
해결
이 방법 저 방법 사용하다 보니 결국 요청을 수락받은 친구들 목록을 조회하는 기능은 아래 사진과 같이 구현했다.
반환을 Dto 형태가 아닌 String으로 바꾸고 Service 로직에서 변경된 사항들이 있다.
아래 순서로 로직을 구현하였다.
User userByNicknameOrElseThrow = userRepository.findUserByNicknameOrElseThrow(nickname);
List<FriendResponseDto> list =
friendRepository.findByUserrequest(userByNicknameOrElseThrow)
.stream()
.filter(friend -> friend.getStatus().equals(ACCEPTED)) // 친구 요청을 수락했는지 확인
.map(friend -> new FriendResponseDto(friend))
.toList();
List<String> nicknames = list.stream()
.map(friendResponseDto -> friendResponseDto.getReceiverNickname()) // FriendResponseDto에서 nickname 가져오기
.toList();
return nicknames;
또 다른 문제
일단 구현하기 위해 이런 형식으로 하다 보니 친구 삭제와 목록에서 또 문제가 발생했다.
예를 들어 친구 1이 친구 2에게 친구신청을 요청을 보내고 친구 2가 수락을 받게 되어 친구관계가 된다면
친구 1의 친구목록을 조회하면 친구 2가 잘 조회된다.
하지만!
친구 2에게는 친구 1이 친구목록에서 조회되지 않는다....
그리고 위 사진과 같이 JPA에서 제공해 주는 delete 메서드는 Friend Entity 객체 타입을 파라미터로 받는다.
하지만, 여기서 친구목록을 Friend 객체가 아닌 String형의 List로 설정하다 보니 이렇게 문제가 생겼다....
해결
양방향 연관관계를 설정해 줌으로써 해결할 수 있었다.
이전에 없던 User Entity에 @oneToMany라는 어노테이션을 사용하고, userrequest 또는 userreceiver를 매핑하도록 하여 양방향으로 연관관계를 설정하였다.
LoginUser 인터페이스를 만들어
@LoginUser 어노테이션을 사용해 로그인된 유저가 본인인지 확인하는 기능을 구현했다.
그리고 위 사진과 같이 user1(로그인된 본인)이 user2와 친구관계인지 확인하는 로직과 user2가 user1과 친구관계인지 확인하는 로직을 구현하여 삭제하도록 구현했다.
그리고 delete 하는 객체도 Friend로 받아오도록 구현했다.
이렇게 구현하니 친구삭제 요청도 성공적으로 작동하고, 친구목록에서도 삭제가 된다.
양쪽에서 관계를 탐색하고 관련 문제를 신중하게 관리해야 할 필요가 있는 경우에는 양방향으로 연관관계를 설정하여 매핑할 수 있도록 해야 한다.
이번 팀프로젝트를 진행하면서 가장 많이 느꼈던 점은 초기 설정을 할 시에는 꼭 변수선언 및 기본 방향을 잘 설정해야겠다는 생각이 많이 들었다.
또한, 세션, JWT, 인증/인가, 로그인로직, AOT, 시큐리티 등등 공부해야할 것들이 많다는 생각을 했다. 이번 프로젝트로 내가 공부를 해야하는 것들을 잘 정리할 수 있어서 좋았다!