티스토리 뷰

Spring

[Spring] TDD로 멤버십 전체/상세 조회 API 구현 예제 - (4/5)

망나니개발자 2021. 8. 26. 10:00
반응형

이번에는 멤버십 전체/상세 조회 관련 API를 개발해보도록 하겠습니다.

 

 

 

1. 멤버십 전체 조회 API 구현


[ 요구사항 확인 ]

  • 나의 멤버십 전체 조회 API
    • 기능: 내가 가진 모든 멤버십을 조회합니다.
    • 요청: 사용자 식별값
    • 응답: {멤버십 ID, 멤버십 이름, 포인트, 가입 일시}의 멤버십 리스트

 

[ Repository 계층 개발 ]

이전에 멤버십 등록 API를 개발할 때와 마찬가지로, 이번에도 먼저 구현할 계층은 Repository 계층이다. 또한 TDD 기반으로 개발을 하고 있으므로, 이번에도 역시 사용자 식별값으로 멤버십 정보 목록을 DB에서 조회하는 Repository에 대한 테스트 코드를 먼저 작성해보도록 하자. 이번에는 데이터가 없는 경우와 데이터가 있는 경우를 한번에 작성하도록 하자.

@Test
public void 멤버십조회_사이즈가0() {
    // given

    // when
    List<Membership> result = membershipRepository.findAllByUserId("userId");

    // then
    assertThat(result.size()).isEqualTo(0);
}

@Test
public void 멤버십조회_사이즈가2() {
    // given
    final Membership naverMembership = Membership.builder()
            .userId("userId")
            .membershipType(MembershipType.NAVER)
            .point(10000)
            .build();

    final Membership kakaoMembership = Membership.builder()
            .userId("userId")
            .membershipType(MembershipType.KAKAO)
            .point(10000)
            .build();

    membershipRepository.save(naverMembership);
    membershipRepository.save(kakaoMembership);

    // when
    List<Membership> result = membershipRepository.findAllByUserId("userId");

    // then
    assertThat(result.size()).isEqualTo(2);
}

 

 

멤버십 전체 조회를 위해서는 2가지 테스트 코드를 작성해주었다. 아무 것도 존재하지 않아 사이즈가 0인 경우, 2가지 데이터가 존재하여 사이즈가 2인 경우이다.

위와 같이 테스트 코드를 작성하면 역시 findAllByUserId()라는 메소드가 존재하지 않아서 컴파일 에러가 발생한다. 이러한 문제를 해결하기 위해 MemberRepository에 다음과 같이 메소드를 추가해주어야 한다.

public interface MembershipRepository extends JpaRepository<Membership, Long> {

    ... 생략

    List<Membership> findAllByUserId(final String userId);

}

 

 

그리고 테스트를 실행하면 테스트가 성공하게 되고, Repository 계층에 대한 개발이 마무리되었다.

확실히 처음에 멤버십 등록할 때보다 작업이 간편하게 끝났다.

 

 

 

[ Service 계층 개발 ]

이제는 서비스 계층에서 멤버십 목록 조회에 대한 테스트 코드를 작성해야 한다. 멤버십 목록 조회 시에 존재하지 않으면 빈 리스트로 반환이 되므로 실패하는 케이스가 없다. 그러므로 다음과 같은 멤버십 목록 조회 성공 테스트 코드만 작성하면 된다.

@Test
public void 멤버십목록조회() {
    // given
    doReturn(Arrays.asList(
            Membership.builder().build(),
            Membership.builder().build(),
            Membership.builder().build()
    )).when(membershipRepository).findAllByUserId(userId);

    // when
    final List<Membership> result = target.getMembershipList(userId);

    // then
    assertThat(result.size()).isEqualTo(3);
    
}

 

 

그리고 컴파일 에러를 해결하기 위해 다음과 같이 프로덕션 코드를 추가할 수 있다.

@Service
@RequiredArgsConstructor
public class MembershipService {

    private final MembershipRepository membershipRepository;

    ... 생략

    public List<Membership> getMembershipList(final String userId) {
        return membershipRepository.findAllByUserId(userId);
    }

}

 

 

그러면 테스트 코드가 통과하게 되고 서비스 계층에 대한 개발을 마무리하게 된다. 하지만 역시 엔티티 객체를 직접 반환하는 것은 좋아보이지 않으므로 DTO를 반환하도록 리팩토링을 하자.

먼저 기존의 테스트 코드를 수정해야 한다. 그런데 기존에 만들었던 MemebershipResponse와 다르게 별도의 Response DTO가 필요해 보인다. 그런데 기존의 MembershipResponse가 이름에 걸리는데 MembershipAddResponse로 수정하고, 멤버십을 조회한 결과를 반환할 DTO 클래스를 MemebershipDetailResponse로 하고 테스트 코드를 수정하도록 하자. (MembershipRequest는 포인트 적립 시에도 쓰일 것 같으므로 놔두도록 하자.)

@Test
public void 멤버십목록조회() {
    // given
    doReturn(Arrays.asList(
            Membership.builder().build(),
            Membership.builder().build(),
            Membership.builder().build()
    )).when(membershipRepository).findAllByUserId(userId);

    // when
    final List<MemebershipDetailResponse> result = target.getMembershipList(userId);

    // then
    assertThat(result.size()).isEqualTo(3);
}

 

 

그리고 서비스 계층의 코드와 기존의 MembershipResponse를 다음과 같이 수정해야 한다.

@Getter
@Builder
@RequiredArgsConstructor
public class MembershipAddResponse {

    private final Long id;
    private final MembershipType membershipType;

}

@Service
@RequiredArgsConstructor
public class MembershipService {

    private final MembershipRepository membershipRepository;

    ... 생략

    public List<MembershipDetailResponse> getMembershipList(final String userId) {
    
    final List<Membership> membershipList = membershipRepository.findAllByUserId(userId);
    
    return membershipList.stream()
            .map(v -> MembershipDetailResponse.builder()
                    .id(v.getId())
                    .membershipType(v.getMembershipType())
                    .point(v.getPoint())
                    .createdAt(v.getCreatedAt())
                    .build())
            .collect(Collectors.toList());
    }

}

 

 

위의 코드에서는 Entity를 DTO로 직접 변환해주고 있는데, 이를 일일이 처리하려면 귀찮음을 느낄 수 있다. 이를 도와주는 많은 라이브러리들이 있는데, ModelMapper와 같은 도구들을 이용하면 Entity를 DTO로 직접 변환을 편리하게 처리할 수도 있다. (하지만 지금은 이렇게 변환해주어야 하는 경우가 많지 않으므로 넘어가도록 하자)

 

 

 

[ Controller 계층 개발 ]

이제는 멤버십 목록을 조회하는 API를 개발해야 한다. 이번에도 테스트 코드를 먼저 작성해고자 하는데, 사용자 식별값이 없으면 실패하는 경우가 있다. 그러므로 사용자 식별값이 없어 실패하는 경우부터 성공하는 겨우까지 작성해보도록 하자.

@Test
public void 멤버십목록조회실패_사용자식별값이헤더에없음() throws Exception {
    // given
    final String url = "/api/v1/memberships";

    // when
    final ResultActions resultActions = mockMvc.perform(
            MockMvcRequestBuilders.get(url)
    );

    // then
    resultActions.andExpect(status().isBadRequest());
}

@Test
public void 멤버십목록조회성공() throws Exception {
    // given
    final String url = "/api/v1/memberships";
    doReturn(Arrays.asList(
            MembershipDetailResponse.builder().build(),
            MembershipDetailResponse.builder().build(),
            MembershipDetailResponse.builder().build()
    )).when(membershipService).getMembershipList("12345");

    // when
    final ResultActions resultActions = mockMvc.perform(
            MockMvcRequestBuilders.get(url)
                    .header(USER_ID_HEADER, "12345")
    );

    // then
    resultActions.andExpect(status().isOk());
}

 

 

위와 같이 테스트 코드를 작성하고 실행하면 이전에도 겪었던 것 처럼 404 Not Found 에러가 발생하게 된다. 역시 개발된 API가 없기 때문인데, 이를 해결하기 위해 다음과 같이 컨트롤러 클래스에 API를 작성해주도록 하자.

@GetMapping("/api/v1/memberships")
public ResponseEntity<List<MembershipDetailResponse>> getMembershipList(
        @RequestHeader(USER_ID_HEADER) final String userId) {
    
    return ResponseEntity.ok(membershipService.getMembershipList(userId));
}

 

 

그리고 테스트를 실행하면 테스트가 통과하게 된다. 그러면 멤버십 목록 조회 API 구현이 마무리 된 것이다.

확실히 첫 API를 개발했을 때 보다 빠르게 작업이 끝난 것 같다. 바로 멤버십 상세 조회 API를 구현해보도록 하자.

 

 

 

 

 

2. 멤버십 상세 조회 API 구현


[ 요구사항 확인 ]

  • 나의 멤버십 상세 조회 API
    • 기능: 나의 1개 멤버십을 상세 조회합니다.
    • 요청: 사용자 식별값, 멤버십 ID
    • 응답: 멤버십 ID, 멤버십 이름, 포인트, 가입일시

 

[ Repository 계층 개발 ]

멤버십 상세 조회를 위해서 레포지토리 계층에 추가할 코드는 없다. 왜냐하면 멤버십 등록 API를 구현하면서 중복 검사를 위해 이미 조회하는 코드를 개발하였기 때문이다.

그러므로 리포지토리 계층은 넘어가주도록 하자.

 

[ Service 계층 개발 ]

이번에는 멤버십 상세 조회를 위한 서비스 계층을 개발해보도록 하자. 서비스 계층에서는 멤버십이 존재하지 않아서 조회에 실패하는 경우, 본인의 멤버십이 아닌 경우, 멤버십이 존재하여 조회에 성공하는 경우 3가지가 있을 것이다.

두 가지 경우에 대한 테스트 코드를 작성하면 다음과 같다. 원래라면 실패 케이스를 먼저 작성하고, 통과한 후에 성공 케이스를 작성하는게 정석이지만, TDD에 어느정도 익숙해졌다는 가정 하에 3가지를 한번에 작성하도록 하자.

@Test
public void 멤버십상세조회실패_존재하지않음() {
    // given
    doReturn(Optional.empty()).when(membershipRepository).findById(membershipId);

    // when
    final MembershipException result = assertThrows(MembershipException.class, () -> target.getMembership(membershipId, userId));

    // then
    assertThat(result.getErrorResult()).isEqualTo(MembershipErrorResult.MEMBERSHIP_NOT_FOUND);
}

@Test
public void 멤버십상세조회실패_본인이아님() {
    // given
    doReturn(Optional.empty()).when(membershipRepository).findById(membershipId);

    // when
    final MembershipException result = assertThrows(MembershipException.class, () -> target.getMembership(membershipId, "notowner"));

    // then
    assertThat(result.getErrorResult()).isEqualTo(MembershipErrorResult.MEMBERSHIP_NOT_FOUND);
}

@Test
public void 멤버십상세조회성공() {
    // given
    doReturn(Optional.of(membership())).when(membershipRepository).findById(membershipId);

    // when
    final MembershipDetailResponse result = target.getMembership(membershipId, userId);

    // then
    assertThat(result.getMembershipType()).isEqualTo(MembershipType.NAVER);
    assertThat(result.getPoint()).isEqualTo(point);
}

 

 

이번에도 컴파일 에러를 위해 다음과 같은 프로덕션 코드들을 추가해주어야 할 것이다.

@Getter
@RequiredArgsConstructor
public enum MembershipErrorResult {

    MEMBERSHIP_NOT_FOUND(HttpStatus.NOT_FOUND, "Membership Not found"),

    ... 생략

    ;

    private final HttpStatus httpStatus;
    private final String message;

}

@Service
@RequiredArgsConstructor
public class MembershipService {

    private final MembershipRepository membershipRepository;

    ... 생략

    public MembershipDetailResponse getMembership(final Long membershipId, final String userId) {
        return null;
    }
}

 

 

그리고 테스트를 실행하면 실패하게 된다. 왜냐하면 getMembership에 대한 구현이 완료되지 않았기 때문이다.

그러므로 다음과 같이 getMembership에 대한 구현을 하고 테스트를 실행하면 통과를 하게 된다.

public MembershipDetailResponse getMembership(final Long membershipId, final String userId) {
    final Optional<Membership> optionalMembership = membershipRepository.findById(membershipId);
    final Membership membership = optionalMembership.orElseThrow(() -> new MembershipException(MembershipErrorResult.MEMBERSHIP_NOT_FOUND));
    if (!membership.getUserId().equals(userId)) {
        throw new MembershipException(MembershipErrorResult.NOT_MEMBERSHIP_OWNER);
    }

    return MembershipDetailResponse.builder()
            .id(membership.getId())
            .membershipType(membership.getMembershipType())
            .point(membership.getPoint())
            .createdAt(membership.getCreatedAt())
            .build();
}

 

 

그럼 이제 멤버십 상세 조회에 대한 서비스 계층이 완료되었고, 이제 컨트롤러 개발로 넘어갈 차례이다.

 

 

 

 

[ Controller 계층 개발 ]

이제는 익숙하게 컨트롤러에 대한 테스트 코드를 작성해보도록 하자. 이번의 테스트 케이스는 총 3가지인데, 사용자 식별 헤더가 없어서 실패하는 경우(400 에러), 등록된 멤버십이 존재하지 않는 경우(404), 그리고 멤버십 조회에 성공하는 경우가 있다. 테스트 케이스들 중에서 멤버십이 존재하지 않는 경우에는 404 Not Found가 발생해야 하므로 이를 참고하여 작성하도록 하자. 

이 테스트들도 원래 하나씩 작성하여 통과하면 다음 테스트를 작성하는게 맞지만, 한번에 작성된 테스트들을 보여주도록 하겠다.

@Test
public void 멤버십상세조회실패_사용자식별값이헤더에없음() throws Exception {
    // given
    final String url = "/api/v1/memberships";

    // when
    final ResultActions resultActions = mockMvc.perform(
            MockMvcRequestBuilders.get(url)
    );

    // then
    resultActions.andExpect(status().isBadRequest());
}

@Test
public void 멤버십상세조회실패_멤버십이존재하지않음() throws Exception {
    // given
    final String url = "/api/v1/memberships/-1";
    doThrow(new MembershipException(MembershipErrorResult.MEMBERSHIP_NOT_FOUND))
            .when(membershipService)
            .getMembership(-1L, "12345");

    // when
    final ResultActions resultActions = mockMvc.perform(
            MockMvcRequestBuilders.get(url)
                    .header(USER_ID_HEADER, "12345")
    );

    // then
    resultActions.andExpect(status().isNotFound());
}

@Test
public void 멤버십상세조회성공() throws Exception {
    // given
    final String url = "/api/v1/memberships/-1";
    doReturn(
            MembershipDetailResponse.builder().build()
    ).when(membershipService).getMembership(-1L, "12345");

    // when
    final ResultActions resultActions = mockMvc.perform(
            MockMvcRequestBuilders.get(url)
                    .header(USER_ID_HEADER, "12345")
                    .param("membershipType", MembershipType.NAVER.name())
    );

    // then
    resultActions.andExpect(status().isOk());
}

 

 

그리고 테스트를 실행하면 아직 API를 개발하지 않았으므로 404 에러가 발생하게 된다. 컨트롤러에 다음과 같이 API를 추가해주도록 하자.

@GetMapping("/api/v1/memberships/{id}")
public ResponseEntity<MembershipDetailResponse> getMembership(
        @RequestHeader(USER_ID_HEADER) final String userId,
        @PathVariable final Long id) {

    return ResponseEntity.ok(membershipService.getMembership(id, userId));
}

 

 

그러면 모든 테스트가 성공하고, 초록 막대를 볼 수 있다.

이제 멤버십 조회와 관련된 2개의 API 개발을 마무리하였다. 확실히 이전 멤버십 등록과 중복되는 작업이 있어서 비교적 간단하게 구현을 할 수 있었다. 다음에는 멤버십 삭제 및 포인트 적립 API에 대해 개발해보도록 하자.

 

 

 

위에서 작성한 내용들은 클린코드나 이펙티브 자바, 테스트 주도 개발 By Example 등의 책과 실무 경험 등을 종합하여 개인적인 생각을 정리한 글입니다. 옳고 그른 얘기들 및 부족한 점 충분히 있을 수 있는데, 피드백이나 커멘트 등은 언제나 환영합니다:)

코드는 깃허브에 공개되어 있습니다! 코드를 확인하시려면 여기를 참고해주세요:)

 

 

관련 포스팅

  1. 단위 테스트와 TDD(테스트 주도 개발) 프로그래밍 방법 소개 - (1/5)
  2. TDD 연습문제 소개와 요구사항 분석 및 SpringBoot 프로젝트 설정 - (2/5)
  3. TDD로 멤버십 등록 API 구현 예제 - (3/5)
  4. TDD로 멤버십 전체/상세 조회 API 구현 예제 - (4/5)
  5. TDD로 멤버십 삭제 및 포인트 적립 API 구현 예제 - (5/5)

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함