-
하나의 메서드를 두 개 이상의 기능이 공유하면 발생할 수 있는 문제Today I Learned 2022. 11. 6. 23:00
하나의 운동 모집 게시글에 대해 작성자, 게시글에 참가를 신청하지 않은 사용자, 참가를 신청한 사용자에 대해 각기 다른 포지션 신청 UI를 보여주는 기능을 구현하고 있다.
우선 참가를 신청하지 않은 사람은 사진 왼쪽처럼 빈 자리가 있는 포지션에 신청하기 버튼이 출력되고, 참가를 신청한 사람은 오른쪽처럼 신청한 포지션에 신청취소 버튼이 출력되고, 다른 포지션의 신청하기 버튼은 출력되지 않도록 하는 것이 목표이다.
게시물의 상세 정보를 보여주는 기능을 수행하기 위한 모델들 간의 관계는 다음과 같다. 두 개 이상 존재하는 모델들은 상위 모델 하나 당 여러 개가 존재할 수 있다.
REST API 요청을 받아 게시글 정보를 조합해 반환하는 Service 로직은 다음과 같다.
public PostDto post(Long postId) { Post post = postRepository.findById(postId) .orElseThrow(PostNotFound::new); return createPostDto(post); }
createPostDto 메서드는 다음과 같이 게임, 팀, 포지션, 멤버 정보들을 Repository에서 찾고, 찾은 정보들을 조합해 Dto로 변환한 뒤 반환한다. 이때 createPostDto는 게시글 상세정보 조회뿐만 아니라 게시글 목록 조회를 수행하는 메서드인 posts에서도 사용되고 있다.
public PostDto createPostDto(Post post) { Game game = gameRepository.findByPostId(post.id()) .orElseThrow(GameNotFound::new); List<Team> teams = teamRepository.findAllByGameId(game.id()); List<Role> roles = roleRepository.findAllByGameId(game.id()); List<Member> members = memberRepository.findAllByGameId(game.id()); List<MemberDto> memberDtos = createMemberDtos(members); List<RoleDto> roleDtos = createRoleDtos(roles, memberDtos); List<TeamDto> teamDtos = createTeamDtos(teams, roleDtos); Place place = placeRepository.findByGameId(game.id()); GameDto gameDto = game.toDto(place.name(), teamDtos); User author = userRepository.findById(post.id()) .orElseThrow(UserNotFound::new); List<ImageDto> imageDtos = imageRepository.findAllByPostId(post.id()) .stream() .map(Image::toDto) .toList(); return post.toPostDto(author.name(), imageDtos, gameDto); }
public PostsDto posts() { List<Post> posts = postRepository.findAll(); List<PostDto> postDtos = posts.stream() .map(post -> createPostDto(post)) .toList(); return new PostsDto(postDtos); }
클라이언트에 접속한 유저 자신이 누구인지를 어떻게 알게 할 수 있을까? 생각해본 방법은 다음과 같다.
일단 LocalStorage에 접속한 사용자의 User Id 값을 JWT로 인코딩한 Token을 들고 있게 한다. 서버에 요청 시 Token을 헤더에 Authorization으로 전달하면 Interceptor에서 Decode해 접속한 사용자의 User Id가 무엇인지 알아낸다.
이 User Id를 이용해 사용자가 같이 전달된 게시글 Id의 경기에 참가하는지 여부를 판별한 값과 Decode한 사용자의 User Id를 Game Dto에 추가로 담아 반환하면, 클라이언트에서는 User Id와 경기 참가 여부 판별 값을 이용해 어떤 버튼이 화면에 출력되게 할 것인지 결정한다.
구상한 대로 구현하기 위해서는 createPostDto에서 Game 객체를 찾고 나서, 찾은 Game Id를 갖고 있는 Member들 중 User Id가 헤더로 전달받은 User Id와 일치하는 유저가 있는지 판별하는 과정이 필요했다.
문제는 createPostDto가 게시글 상세정보 조회뿐만 아니라 게시글 목록 조회에서도 같이 사용되는 메서드라는 점에 있었다. post 메서드에서 먼저 동작을 수행해서 상태를 받아온 뒤, createPostDto에 결과를 인자로 넘겨주기 위해 시그니처를 변경한 순간 posts 메서드에 영향이 가기 시작했고, posts 메서드도 같은 시그니처를 사용하게 하기 위해 멀쩡히 잘 작동하고 있던 메서드를 굳이 내용을 수정해야 하는 상황이 발생했다.
public PostDto post(Long postId, Long accessedUserId) { Post post = postRepository.findById(postId) .orElseThrow(PostNotFound::new); String userStatus = ""; Long roleIdOfAccessedUser = 0L; Game game = gameRepository.findByPostId(postId) .orElseThrow(GameNotFound::new); if (post.authorId().equals(accessedUserId)) { userStatus = IS_AUTHOR; } List<Member> members = memberRepository.findAllByGameId(game.id()); // ... return createPostDto(post, game); }
public PostsDto posts() { List<Post> posts = postRepository.findAll(); List<List<Object>> postAndGames = posts.stream() .map(post -> { Game game = gameRepository.findByPostId(post.id()) .orElseThrow(GameNotFound::new); return List.of(post, game); }) .toList(); List<PostDto> postDtos = postAndGames.stream() .map(postAndGame -> { Post post = (Post) postAndGame.get(0); Game game = (Game) postAndGame.get(1); return createPostDto(post, game); }) .toList(); return new PostsDto(postDtos); }
고통스러운 부분을 정리했다.
- 하나의 메서드에서 처리해야 하는 내용의 로직이 너무 큰데, 거기에 기능이 추가되어야 한다. 심지어 큰 메서드를 서로 다른 두 기능이 공유하고 있다. 어떻게든 동작하게는 만들 수는 있겠지만, 올바른 구현이라는 확신이 들지 않아 미리 설계한 내용을 보면서 구현하는 데도 멈칫하게 된다.
- 테스트 코드 동작에 필요한 픽스쳐를 정의하는 영역이 너무 커져서 감당할 수 없을 정도가 되어 테스트 코드 작성을 중단했다. 따라서 새로 추가되는 기능의 동작을 검증할 수 없다.
- 스프린트 계획보다 구현이 뒤쳐지고 있다 보니 올바른 구조를 갖춰 기능을 만들어야 한다는 마음보다는 얼른 끝내고 기능을 하나라도 더 추가해야 하지 않나 하는 생각에 쫓기고 있다. 그러나 둘 중 어느 한 방향도 제대로 잡지 못하고 있는 것 같다.
'Today I Learned' 카테고리의 다른 글
돌돌설 (돌고 돌아 설계 문서 작성부터 다시) (0) 2022.11.08 프로젝트 설계 문서 공개 리팩터링 (1) 2022.11.07 풀리지 않는 문제가 있다면 문제의 범위를 좁혀보자 (0) 2022.11.05 테스트가 너무 크고 복잡하면 로직을 다시 돌아볼 필요가 있다 (0) 2022.11.04 스프린트 주간 2주차 중간점검 (0) 2022.11.03 - 하나의 메서드에서 처리해야 하는 내용의 로직이 너무 큰데, 거기에 기능이 추가되어야 한다. 심지어 큰 메서드를 서로 다른 두 기능이 공유하고 있다. 어떻게든 동작하게는 만들 수는 있겠지만, 올바른 구현이라는 확신이 들지 않아 미리 설계한 내용을 보면서 구현하는 데도 멈칫하게 된다.