ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot] API구현 엔티티 설계 및 테스트 기록
    Backend/개발 2022. 1. 23. 13:20
    반응형

    프로젝트 에서 Spring MVC + handlebars 구조로 개발했던 웹페이지를 API용 백엔드와 React로 구현한 프론트엔드로 분리해보기로 했다.
    같은 기능이지만 리팩토링할 부분이나 조금 추가해야될 기능들을 넣고, 계층별로 유닛테스트로 테스트코드를 모두 작성해보는것이 목표이다.

    진행하면서 기록할만한 내용들


    @IdClass

    PK가 여러개인 경우 PK만을 속성으로 가지고 있는 클래스를 만들고, 엔티티 클래스에 @IdClass로 매핑해주면 된다.

    @IdClass(GameRecordParticipantKey.class)

    Dto와 Entity

    디비에서 가져온 정보인 Entity는 repository, service단에서만 두고 컨트롤러에 전해줄때나 API를 만들 땐 Dto를 생성한다.
    기능별로 필요한 정보만 DTO로 두고 재사용할 수 있고, 외부에 실제 DB구조가 노출되지 않을 수 있다.
    그리고 Json데이터형식으로 리플레이결과를 추출할때 그 형식으로 DTO를 미리 만들어두면 컨트롤러에서 받아올 때 별도의 매핑 없이 그 값을 객체형식으로 받아올 수 있다.

    @DataJpaTest

    repository계층만을 테스트 할 때 @DataJpaTest를 사용한다.

    @DataJpaTest
    @Runwith(SpringRunner.class)
    public class GameRecordRepositoryTest {
    }

    이렇게 생성했더니

    Caused by: java.lang.IllegalStateException: Failed to replace DataSource with an embedded database for tests. If you want an embedded database please put a supported one on the classpath or tune the replace attribute of @AutoConfigureTestDatabase.

    다음과같은 오류메세지가 나왔다.
    DataJpaTest에서 in memory db가 아닌 실제 db를 사용하기 위해서 별도의 세팅이 필요하다고한다.
    DataJpaTest는 기본적으로 인메모리디비를 사용하기 때문에 @AutoConfigureTestDatabase를 이용해 실제 사용하는 디비를 매핑해주면 된다.

    @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)

    나는 레파지토리 (DB) 관련 테스트는 인메모리로 하고싶어서 build.gradle에 h2 라이브러리를 추가한 후 AutoConfigureTestDatabase 추가 없이 그대로 작성하였다.

    JPA 양방향 매핑

    우리는 주기적으로 리그가 열리고, 그 리그 우승자를 저장해 회원 목록에서 별모양이나 특별한 아이콘을 지정해주기로 하였다.
    처음에는 간단하게 1등만 저장하기 위해서 @ElementCollection으로 1,2, 이런식으로 숫자를 저장해줬는데 준우승도 저장하는 기능이 추가되어야했고, 리그도 점점 많아지고 이벤트대전, 리그 종류가 많아짐에따라 좀 더 체계적인 구현이 필요해졌다.

    LeagueResult라는 객체에 게임 타입, 우승자, 등수 등을 저장하고 그걸 Member변수와 매핑시켜보기로 했다.

    RDBMS 예약어와 겹침 주의

    You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near

    order, rank같은 이름으로 속성을 만들다보면 DB의 예약어와 겹쳐서 오류가 생긴다는 메세지로 오류가 뜬다. 나도 등수를 rank로 저장하려다가 이렇게됐다.. 중요한 내용은 아니지만 에러 메세지랑 이유가 살짝 매칭이 안되서 찾으려니 쫌 애먹었다.

    OneToMany + ManyToOne

    원래 Member엔티티에 OneToMany로만 LeageResult를 넣었는데, 그렇게하면 member_league_result라는 각각의 id만을 저장하는 매핑 테이블이 생성된다.
    그렇게되면 아래와같이 조회할 때 member -> member_league_result -> league_result 이렇게 조인이 2번이나 일어난다. 흠

        from
            members memberenti0_ 
        left outer join
            members_league_results leagueresu1_ 
                on memberenti0_.account_id=leagueresu1_.member_entity_account_id 
        left outer join
            league_results leagueresu2_ 
                on leagueresu1_.league_results_league_result_id=leagueresu2_.league_result_id 
        where

    그냥 양방향으로 OneToMany, ManyToOne설정을 해줬다.
    아직도 테이블 N:M매핑 하는 법은 어려워서 일단 이렇게 하고 나중에 JPA강의를 들으면서 다시 정리해야할 것 같다.

     

    => 추가) 1:N관계에서 연관관계의 주인은 N이므로 key를 leagueResult가 가지고 있는데 Member엔티티에만 LeagueResult를 넣어줘서 이런 복잡한 구현이 일어났던 것 같다. 기본적으로 연관관계를 맺게되면 N인 엔티티에 @ManyToOne이용해 연관관계를 설정하고 필요에 따라 OneToMany를 넣는다고 생각하면 될 것 같다. 

     

    Enum default값 지정

        @Enumerated(EnumType.STRING)
        @Column(length = 30, columnDefinition = "varchar(30) default 'REGULAR'")
        private GameTypeCode gameType;

    columnDefinition으로 이렇게 지정해주면된다.

    그럼 이전에 이미 생성되어있는 값에도 REGULAR로 채워진다. gameType속성을 추가하기 전에 만들어졌던 데이터에 디폴트로 regular를 넣어두느라고 이 설정을 했고, 따로 기본값 세팅,검증 처리는 해주어야한다.

    Exception handling

    이 프로젝트에도 CustomException과 ExceptionHandler를 추가했다. 이전에 이부분에대해서 좀 깊게 고민하고 정리해두었어서 내 블로그 + 다른 블로그들 참고해서 만들었다.
    예전에는 되게 이해 안돼서 머리에 꾹꾹 담아도 새어나왔는데 이번에 만들땐 좀 느낌이 대략적으로 이해는 됐다.

    몇가지 기억해두면 좋을 점은

    ErrorResponse

    ErrorResponse 객체를 만드는 이유는 컨트롤러에서 통일된 형식으로 에러를 반환하기 위해서이다.

    message, status, errors, code 를 반환하도록 했는데 나중에 @Valid, validation으로 값을 검증할 때 올바르지 않은 필드와 이유들을 보여줄 수 있어서 블로그를 참고해서 추가해봤다.

    ErrorCode

    CustomException(ErrorCode.INTERNAL_SERVER_ERROR,""); 이런식으로 커스텀예외를 날려줄 때 그 상황에 맞는 에러코드를 계속 추가해주면서 에러 상황들을 에러코드로 관리해볼 것이다.
    status랑 에러코드 이름들을 어떻게 해야할지는 아직도 어렵다..

    서비스 계층 테스트코드 작성

    Mockito를 이용해서 서비스계층의 유닛테스트를 작성해볼것이다. 등록, 조회, 삭제, (수정) 시에 성공, 실패(실패유형별 1,2,3,...) 이렇게 하나씩 다 테스트해보려고 한다.

    테스트 코드를 작성할 때, given, when, then 순서에 따라 작성하는 규칙을 따르는 것이 좋은데 Mockito는 함수의 이름과 구조가 매칭이 잘 안되어서, 한번에 코드의 의미를 이해하기 쉽도록 BDDMockito를 사용해서 레파지토리값들을 stubbing 해 줄 것이다.

    (가짜로 수행할 객체를 넣어주는 것. repository 실제 값에 상관 없이 상황별로 가짜 객체를 만들고 서비스 계층에서 수행되어야할 테스트만을 독립적으로 진행할 수 있다. )

    일단 작성할 테스트 케이스들 정리해본다.

    • 게임 등록
      • 이미 존재하는 아이디가 있는지 확인 -> 이미 등록된 게임입니다. 실패 1
      • 올바르지 않은 값이 입력되었을 때
        • GameType 실패 2
        • Json parsing 오류 실패 3
      • 참가자 티어 크롤링 실패 4
      • 성공

     

        // Game
        DUPLICATE_GAME(409, "G001","이미 등록된 게임입니다."),
        STATS_JSON_PARSE_ERROR(400,"G002","StatsJson을 파싱하는데 실패했습니다."),
        MEMBER_TIER_CRAWLING_ERROR(400,"C003","회원 티어 갱신에 실패했습니다."),
        UNMATCHED_GAME_TYPE(400,"C004","게임 타입이 올바르지 않습니다."),
        GAME_RECORD_JSON_ERROR(400,"C005","게임 등록을 위한 Json값이 올바르지 않습니다."),

    에러코드도 만들어서 다음 에러에 대해서 테스트해주었다.

    댓글

Designed by Tistory.