-
테스트가 너무 크고 복잡하면 로직을 다시 돌아볼 필요가 있다Today I Learned 2022. 11. 4. 23:55
게시글 상세 페이지 조회 REST API 요청 시 백엔드 요청을 처리할 Controller와 테스트 코드를 작성하고 있었다. 홀맨님이 도장을 돌아보는 동안 내가 작성하고 있던 소스코드를 확인하셨고, 소스코드에 문제가 있음을 확인할 수 있었다.
다음은 내가 작성하고 있던 게시물 목록을 조회하는 Controller의 요청을 처리하는 과정을 검증하는 테스트 코드이다.
테스트 코드를 작성할 때에도 이 영역이 어떤 동작을 테스트하려는 영역인지 중간중간 멈칫하는 느낌이 있었다. 일단 기능을 완성시켜야 한다는 생각에 작업을 무리해서 진행했는데, 다시 살펴보니 테스트 코드를 읽는 것만으로 어떤 테스트를 하려는 것인지 나도 이해하기 어려웠다.
다음은 테스트 코드를 통해 검증하는 Controller의 로직이다.
이 로직의 문제는 다음과 같이 정리해볼 수 있다.
1. 직관적이지 않다.
'게시물' 또는 '게시물 리스트'를 반환한다고 하면 하나의 게시물의 정보를 담는 DTO에 게시물과 관련된 모든 정보를 담아서 '하나의 게시물'을 만들어 반환하는 것을 생각할 수 있다. 그러나 지금의 구조는 게시물을 구성하는 정보들을 각각 가져온 뒤, DTO에 분리된 정보들을 모두 담아서 반환하고 있다.
소스코드를 보는 사람이 직관적으로 이해할 수 있도록 구조를 짤 수 있어야 하겠다. 관련해서 동료가 데이터를 가져오는 컨트롤러 자체를 프론트엔드 페이지의 컴포넌트 별로 분리시켜서 REST API를 호출하는 방법으로 크기를 줄이는 방법도 있다는 조언을 주었다.
2. Controller가 너무 많은 로직을 들고 있으면 안 된다
백엔드 서버에서 Controller의 역할은 UI와 같다고 볼 수 있다. 지금의 소스코드는 Controller에 너무 많은 로직이 부여되어 있는 구조였다. Controller는 가급적 요청과 응답 데이터를 프론트엔드와 Service 사이에서 전달하는 일종의 가교 역할을 수행하는 역할을 부여하고, 주요 로직은 Service에서 수행하도록 해야 하겠다.
핵심 로직을 Service로 이관하는 것을 일차적으로 시도한 구조는 다음과 같다. 추상화를 통해 동작을 최대한 나누는 것을 시도했다.
public PostsDto posts() { List<Post> posts = postRepository.findAll(); List<PostDto> postDtos = posts.stream() .map(this::createPostDto) .toList(); return new PostsDto(postDtos); } public PostDto post(Long postId) { Post post = postRepository.findById(postId) .orElseThrow(PostNotFound::new); return createPostDto(post); } public PostDto createPostDto(Post post) { Game game = gameRepository.findByPostId(post.id()) .orElseThrow(GameNotFound::new); List<Team> teams = teamRepository.findByGameId(game.id()); List<Role> roles = roleRepository.findByGameId(game.id()); List<Member> members = memberRepository.findByGameId(game.id()); List<MemberDto> memberDtos = members.stream() .map(this::createMemberDto) .toList(); List<RoleDto> roleDtos = roles.stream() .map(role -> createRoleDto(memberDtos, role)) .toList(); List<TeamDto> teamDtos = teams.stream() .map(team -> createTeamDto(roleDtos, team)) .toList(); Place place = placeRepository.findByGameId(game.id()); GameDto gameDto = game.toDto(place.name(), teamDtos); User author = userRepository.findById(post.userId()) .orElseThrow(UserNotFound::new); List<ImageDto> imageDtos = imageRepository.findAllByPostId(post.id()) .stream() .map(Image::toDto) .toList(); return post.toPostDto(author.name(), imageDtos, gameDto); } public MemberDto createMemberDto(Member member) { User user = userRepository.findById(member.userId()) .orElseThrow(UserNotFound::new); return member.toMemberDto(user.name(), user.mannerScore()); } public RoleDto createRoleDto(List<MemberDto> memberDtos, Role role) { List<MemberDto> memberDtosInRole = memberDtos.stream() .filter(memberDto -> memberDto.getPositionId().equals(role.id())) .toList(); return role.toRoleDto(memberDtosInRole.size(), memberDtosInRole); } public TeamDto createTeamDto(List<RoleDto> roleDtos, Team team) { List<RoleDto> roleDtosInTeam = roleDtos.stream() .filter(roleDto -> roleDto.getTeamId().equals(team.id())) .toList(); Integer membersCount = roleDtosInTeam.stream() .map(RoleDto::getCurrentParticipants) .reduce(0, Integer::sum); return team.toTeamDto(membersCount, roleDtosInTeam); }
추상화로 작업을 분리했을 때, 하나의 작업의 테스트 코드의 크기가 확인 가능한 적정 수준으로 줄어드는 것을 확인할 수 있었다.
@Test void createMemberDto() { Long userId = 1L; User user = new User(userId, "김용기", 10.0); given(userRepository.findById(1L)).willReturn(Optional.of(user)); Long memberId = 1L; Long gameId = 1L; Long teamId = 1L; Long roleId = 1L; Member member = new Member(memberId, gameId, teamId, roleId, userId); MemberDto memberDto = postService.createMemberDto(member); assertThat(memberDto.getName()).isEqualTo("김용기"); }
'Today I Learned' 카테고리의 다른 글
하나의 메서드를 두 개 이상의 기능이 공유하면 발생할 수 있는 문제 (0) 2022.11.06 풀리지 않는 문제가 있다면 문제의 범위를 좁혀보자 (0) 2022.11.05 스프린트 주간 2주차 중간점검 (0) 2022.11.03 양방향 연관관계를 갖는 객체를 테스트하는 어려움 (0) 2022.11.02 POST 요청으로 연관관계를 갖는 객체들의 데이터 생성하기 (0) 2022.11.01