[Spring] TDD로 멤버십 등록 API 구현 예제 -(3/5)_Repository계층

윤재열·2022년 5월 1일
0

Spring

목록 보기
56/72
post-custom-banner

1. 멤버십 등록 API 구현

요구사항

  • 나의 멤버십 등록 API
  1. 기능 : 나의 멤버십을 등록합니다.
  2. 요청 : 사용자 식별값, 멤버십 이름, 포인트
  3. 응답 : 멤버십 ID, 멤버십 이름

Repository 계층 개발

앞서 포스팅에서 살펴봤던 것처럼 Repository의 메서드 중에서 멤버십을 DB에 추가하는 테스트 코드를 먼저 작성해보고자 합니다.
MembershipRepository에 대한 테스트 클래스를 작성하면 다음과 같습니다.

public class MembershipRepositoryTests{

@Autowired
private MembershipRepository membershipRepository;
}

Repository 계층은 통합 테스트로 작성할 것이므로, @Repository 계층이 Spring 컨테이너의 빈으로 등록될 것입니다.
실제 프로덕트 코드라면 생성자 주입을 사용해야 하겠지만, 테스트 코드이므로 필드 주입은 @Autowired를 사용해줍니다.

위의 테스트 클래스에서 아직 MembershipRepository는 존재하지 않으므로,빨간 줄을 띄우며 클래스를 찾을 수 없다는 에러가 보입니다.

컴파일 에러를 해결하기 위해 MembershipRepository를 추가해주어야합니다.그리고 또 컴파일 에러를 해결하기 위해 Membership이라는 entity가 추가로 필요합니다.

public interface MembershipRepository extends JpaRepository<Membership, Long> {
}

public class Membership {

}

그러면 이제 컴파일 에러가 해결되었고, 테스트 코드를 실행 할 수 있습니다. 사실 TDD로 개발하면 이러한 클래스들은 테스트 클래스의 Inner 클래스로 만들고 프로덕션 코드로 옮기는게 정상입니다.
테스트 코드를 실행하면 MemberRepository가 Null이므로 테스트에 실패하고 빨간막대를 보게 됩니다.
이를 해결하기 위해 다음과 같은 어노테이션들을 테스트 클래스에 추가하였습니다.

@DataJpaTest	//JPA Repository들에 대한 빈들을 등록하여 단위 테스트의 작성을 용이하게 합니다.
@Display("membershipRepository는 Null이 아니다.")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class MembershipRepositoryTest{

@Autowired
private MembershipRepository membershipRepository;

@Test
public void membershiptTest(){
Assertions.asserThat(membershipRepository).isNotNull();
}
}
  • Replace 값을 NONE으로 설정해줍니다. Replace.NONE 일 경우 EmbeddedDatabase를 찾아 설정하지 않고 기존 애플리케이션에서 사용한 DataSource(저의 경우 MySQL)가 등록되게 됩니다.

Repository타입의 빈을 등록하기 위해서는 @Repository 어노테이션을 붙여주어야 합니다.
하지만 JpaRepository 하위에 @Repository가 이미 붙어있으므로 생략이 가능합니다.

또한 테스트를 위해서는 테스트 컨텍스트를 잡아주어야 할텐데, @DataJpaTest는 내부적으로 @ExtendWith(SpringExtension.class)어노테이션을 가지고 있어서, 이 어노테이션만 붙여주면 됩니다.
마찬가지로 @DataJpaTest에는 @Transacional어노테이션이 있어서, 테스트의 롤백 등을 위해 별도로 트랜잭션 어노테이션을 추가하지 않아도 됩니다.

그리고 테스트를 실행하면 이번에도 실패하고, 다음과 같은 에러가 발생합니다.

내용을 분석해보면 Membership이 JPA에 의해 관리되는 클래스가 아니라고 하는것이고,Membership을 엔티티로써 관리하기 위해 다음의 코드들을 Membership 클래스에 추가해 주어야합니다.
JPA에서 관리되는 엔티티를 위해서는 기본 생성자가 필요합니다.
그러므로 @NoArgsConstructor 어노테이션도 추가해줍니다.

package org.codeJ.membership.entity;

import lombok.NoArgsConstructor;

import javax.persistence.Entity;

@Entity
@NoArgsConstructor
public class Membership {

	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
}

그리고 테스트를 실행하면 드디어 첫 초록막대를 볼 수 있습니다.

첫 테스트를 성공하기까지 여러번 테스트를 실행하고, 수정하는 작업을 반복하였습니다. 이러한 작업 흐름이 TDD의 흐름이며 , 이러한 흐름을 유지한 채로, 멤버십을 등록하는 Repository 개발을 이어가보도록 합니다.

MembershipRepository가 Null인지 검사하는 테스트는 이제 불필요하므로 제거합니다.

그리고 다음과 같은 멤버십 등록 테스트를 작성합니다.
테스트의 결과 검증은 응답인 멤버십 ID와 멤버십 이름이 Null이 아님으로 판단합니다.

@Test
@DisplayName("멤버십 등록")
public void registerMembership(){

//given
final Membership membership = Membership.builder()
							.userId("userId")
                            .membershipName("네이버")
                            .point(10000)
                            .build();
                            
//when
final Membership result = membershipRepository.save(membership);

//then
assertThat(result.getId()).isNotNull();
assertThat(result.getUserId()).isEqualTo("userId"); 
assertThat(result.getMembershipName()).isEqualTo("네이버"); 
assertThat(result.getPoint()).isEqualTo(10000);
}

역시 테스트 코드에서 컴파일 에러들이 발생하고 있고 , 이를 해결하기 위해 다음과 같은 프로덕션 코드들을 추가할 수 있습니다.

package org.codeJ.membership.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class Membership {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, length=20)
    private String membershipName;
    
    @Column(nullable = false)
    @ColumnDefault("0")
    private Integer point;
    
    @Column(nullable = false)
    private String userId;

    @CreationTimestamp
    @Column(nullable = false,length = 20, updatable = false)
    private LocalDateTime createdAt;
    
    @Column(length = 20)
    @UpdateTimestamp
    private LocalDateTime updatedAt;
}
  • Entity 객체에서 @Column을 붙여주지 않은 지역 변수도 컬럼에 추가됩니다.
    하지만 id외에는 모두 속성이 있어서 일관성을위해 붙여 주었습니다.

테스트를 실행하면 다음과 같이 테스트를 성공하고 초록 막대를 보게 됩니다.

테스트가 성공하고 초록 막대를 보았으니 이제는 리팩토링의 단계를 진행해야 합니다. 요구사항으로는 네이버,카카오,라인 3개의 멤버십을 관리할 수 있어야 하는데, 이부분을 Enum으로 관리하면 좋습니다.
그러므로 멤버십 이름을 멤버십 타입으로 관리하도록 합니다.
여기서 다시 TDD기반으로 개발하고 있음을 잊어서는 안됩니다.
테스트 코드에 String으로 되어 있는 membershipName을 Enum 기반의 MembershipType으로 먼저 변경하고, 프로덕트 코드로 변경해줍니다.

  @Test
    @DisplayName("멤버십 등록")
    public void registerMembership(){
        //given

        final Membership membership= Membership.builder()
                .userId("userId")
                .membershipName(MembershipType.NAVER)
                .point(10000)
                .build();

        //when
        final  Membership result = membershipRepository.save(membership);

        //then
        assertThat(result.getId()).isNotNull();
        assertThat(result.getUserId()).isEqualTo("userId");
        assertThat(result.getMembershipType()).isEqualTo(MembershipType.NAVER);
        assertThat(result.getPoint()).isEqualTo(10000);

    }

컴파일 에러를 수정하기 위해 MembershipType이라는 Enum 이라는 클래스를 만들고 Membership 엔티티를 수정해줍니다.

@Getter
@RequiredArgsConstructor
public enum MembershipType {

   

    NAVER("네이버"),
    LINE("라인"),
    KAKAO("카카오");

 private final String companyName;
}
package org.codeJ.membership.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class Membership {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false)
    private Long id;

    @Enumerated(EnumType.STRING)
    private MembershipType membershipType;

    @Column(nullable = false)
    @ColumnDefault("0")
    private Integer point;

    @Column(nullable = false)
    private String userId;

    @CreationTimestamp
    @Column(nullable = false,length = 20, updatable = false)
    private LocalDateTime createdAt;

    @Column(length = 20)
    @UpdateTimestamp
    private LocalDateTime updatedAt;
}
  • 여기서 약간의 팁이지만, Enum에서는.;문법이 허용됩니다.
  • 이렇게 Enum 클래스를 작성하면 불필요하게 세미콜론을 관리할 필요가 없으므로 새로운 Type을 추가하기에 편리합니다.
  • MembershipType 같은 Nnum 변수에 @Enumerated(EnumType.String)어노테이션을 추가해준것을 알 수 있습니다.
  • 이것은 DB에 저장될 때 Enum의 코드값(Naver or KAKAO 등)으로 저장을 하도록 하는데, 이 값이 없으면 Enum의 ORDINAL(1,2,3과 같은 Integer 값)으로 저장이 됩니다.
    ( 이 부분이 중요한 이유는 현재 LINE은 두번째에 있어 ORDINAL 값이 2인데, LINE 앞에 다른 KIA와 같은 값이 추가되면 ORDINAL 순서가 흐뜨려저 데이터의 정합성에 문제가 생기기 때문입니다.)
  • 이렇게 테스트 코드와 프로덕션 코드를 수정하고 실행하면 다음과 같이 테스트를 성공하고 초록 막대를 보게 됩니다. 그러면 이제 멤버십을 등록하는 Repository 테스트를 작성한 것입니다.

하지만 아직 하나 더 추가해야 하는 Repository 테스트가 있습니다. 그것은 바로 어떤 사용자가 이미 멤버십 타입을 등록했으면 중복 검사를 하여 등록되지 않도록 해야 하는 것입니다.

해당 로직을 구현하기 위해서는 사용자의 아이디와 멤버십 테스트로 멤버십을 조회할 수 있어야 합니다.
이에 대한 테스트 코드를 다음과 같이 작성할 수 있습니다.

 @Test
    @DisplayName("멤버십이 존재하는지에 대한 테스트")
    void alreadyMembership(){
        //given
        final Membership membership = Membership.builder()
                .userId("UserId")
                .membershipType(MembershipType.NAVER)
                .point(10000)
                .build();

        //when
        membershipRepository.save(membership);
        membershipRepository.findByUserIdAndMembershipType("userId",MembershipType.NAVER);


        //then
        assertThat(findResult).isNotNull();
        assertThat(findResult.getId()).isNotNull();
        assertThat(findResult.getUserId()).isEqualTo("userId"); 
        assertThat(findResult.getMembershipType()).isEqualTo(MembershipType.NAVER);
        assertThat(findResult.getPoint()).isEqualTo(10000);

    }
  • 그러면 MembershipRepository의 findByUserIdAndMembershipType이 존재하지 않아서 컴파일 에러가 발생합니다. 그리고 이를 해결하기 위해 MembershipRepository에 다음과 같이 메서드를 추가해줍니다.
package org.codeJ.membership.repository;

import org.codeJ.membership.entity.Membership;
import org.codeJ.membership.entity.MembershipType;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MembershipRepository extends JpaRepository<Membership,Long> {
    
    Membership findByUserIdAndMembershipType(final String userId, final MembershipType membershipType);
}

그리고 테스트를 실행하면 다음과 같이 초록 막대를 볼 수 있습니다.

이렇게 되면 멤버십을 등록하기 위한 API에 필요한 Repositroy의 로직들은 구현이 완료되었습니다.
이제 다음 단계로 Service 계층을 개발해봅니다.

profile
블로그 이전합니다! https://jyyoun1022.tistory.com/
post-custom-banner

0개의 댓글