[PROJECT] 일기장 기능 및 테스트 구현

bin1225·2024년 11월 9일
0

Project_Only

목록 보기
5/9
post-thumbnail

service 계층 테스트 필요성

Diary 생성 기능 테스트

    @Test
    @DisplayName("다이어리 생성 테스트")
    public void createDiary() throws Exception {
        //given
        Long memberId = 1L;
        Member member = new Member(memberId, "kkb");
        Diary diary = diary();
        DiaryRequest diaryRequest = new DiaryRequest("title", "content");
        MemberDiary memberDiary = new MemberDiary(member, diary);
        doReturn(member).when(memberRepository).findById(memberId);
        doReturn(diary).when(diaryRepository).save(any(Diary.class));

        //when
        diary.setMember(memberDiary);
        Diary createdDiary = diaryService.createDiary(member.getId(), diaryRequest);

        //then
        Assertions.assertThat(createdDiary.getId()).isNotNull();
        Assertions.assertThat(createdDiary.getMemberDiaries().getFirst().getId()).isNotNull();
    }

DiaryService의 crateDiary()메소드를 테스트하는 과정에서 어떻게 테스트를 작성해야할지 막막했다.

crateDiary()diaryRequestmemberId값을 받으면,
memberId에 해당하는 member 객체를 조회한 후 member와 생성하는 diary의 연관관계를 정의한 후 저장한다.

기능만을 테스트하기 위해 mock test로 단위테스트를 작성했고, repository 계층은 미리 정의한 객체를 반환하도록 설정했다.

여기서 드는 생각이 메소드가 테스트된다기 보다는 그냥 mock 라이브러리를 이용해 통과되는 테스트를 구현한다는 생각이 들었다..

실제 repository를 이용하는 통합테스트 규모라면 의미가 있겠지만, mock을 이용한 단위테스트로는 의미가 없어 보였다.

DiaryRepository의 save함수를 호출해야지 미리 지정한 객체를 반환하기 때문에, service 계층에서 repository의 메소드를 호출했음은 확인할 수 있다.

뭔가 service 계층에서 복잡한 로직이 없는 (단순히 컨트롤러와 리포지토리의 연결이 주가되는) 기능을 테스트하는 것은 테스트 작성도 복잡해지고 큰 의미도 없는 것 같다고 느껴진다.

[tdd] 상태검증과 행위검증, stub과 mock 차이

그래도 테스트가 가지는 의미를 찾아보자면 특정 메소드의 호출이 일어나는지는 확인할 수 있는 것 같다.


DataIntegrityViolationException

일기장의 일기 목록 테스트 진행 과정 중 에러가 발생했다.
spring jpa 사용시, 데이터를 영속화하는 과정에서 충돌이 발생할 때 생기는 예외인 것 같다.

diaryId값으로 pageList를 조회하는 기능을 테스트 하기 위해 page객체를 생성하면서 diary에 대한 연관관계도 함께 저장하였다.

@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "diary_id")
    private Diary diary;

page만 저장해도 diary에 영속성이 전이 되도록 casecade옵션을 CasecadeType.ALL로 설정하였다.

테스트를 위해 임의의 diary객체를 생성할 때 id값도 함께 임의로 설정해 저장했는데, 이것때문에 정상적으로 작동하지 않았다.

private Diary diary() {
        return Diary.builder()
     			.id(1L)
     			.title("abc")
                .subTitle("this is a sub title")
                .build();
    }

Id값에 GenerateValue 옵션을 설정한 상태에서 id값을 이미 가진 객체가 영속화 될 때, jpa는 id가 이미 설정된 객체는 새로운 것이 아닌 영속화 되었다가 detached된 객체로 판단한다.

따라서 이러한 객체는 merge()를 통해 영속화해야하는데, page객체에 persist()를 하며 diary객체에도 persist()가 호출 되면서 문제가 발생한 것이다.

즉 키값이 존재하는 상태에서 영속성 전이로 인해 persist가 호출되면서 오류가 발생한 것이다.

참고: https://ddorimeo.tistory.com/103


연관관계 매핑

Diary와 User엔티티의 연관관계를 구현하는 것이 어려웠다.

Diary는 최대 2명의 User가 공유할 수 있다. 따라서 다대다 관계로 보는 것이 적절하다.

다대다 관계를 다대일 + 일대다 관계로 풀어내기 위해 UserDiary객체를 추가했다.

원하는 기능은 특정 사용자가 자신의 일기장 목록을 볼 수 있도록 하는 것이었다. 따라서 userId값을 이용해 diaryList를 반환해야한다.

이를 위해서는 Diary객체를 생성하고 저장할 때 user와의 연관관계를 업데이트 해줘야 한다.

여기까지는 이론상으로 알았는데 실제로 구현해보려니까 헷갈렸다.

헷갈린 것

  • Diary를 조회하기 때문에 diaryRepository에서 수행해야하는 작업이라고 생각한다. 근데 이걸 테스트하려면 먼저 user에 대한 정보가 저장되어야 하는데, 이를 위해서는 userRepository의 함수를 호출해야하는 모순적인 상황이 생긴다.
    -> 테스트 전 기본 setup으로 DB에 임의의 더미 데이터를 넣어두는 것도 고려해봐야겠다.

  • 머리속으로 생각해보면 User가 diary list를 가지고 있는 게 합리적인데 diary를 조회하는 작업은 diaryRepository에서 일어나고, diary 생성 시 user에 대한 정보를 추가하기 때문에 뭔가 인지부조화가 일어난다.
    -> 실제로 diary를 DB에서 조회할 때 memberId를 검색 조건값으로 사용할 뿐이지 조회하는 건 diary table, 현실적인 구조와 엔티티 사이의 관계는 조금 다를 수 있다.


List 초기화에서 Builder 패턴 사용 시 NullPointerException

Diary 기능 구현하는 도중에 addPage를 하기위해 Diary의 pages를 불러오는 과정에서 NullPointException이 발생했다.

필드를 new ArrayList<>(); 로 초기화해줬음에도 문제가 발생했다.

원인

기본적으로 클래스에서 인스턴스를 생성하면 필드값을 기본값으로 초기화한다.
필드에 미리 값을 지정해둠으로써 기본값을 변경할 수 있는데, 빌더패턴을 사용하면 지정한 값이 무시되고 기본값을 사용한다.

따라서 build과정에서 값을 지정해줘야한다.

또 다른 방법으로는 @Builder.Default어노테이션으로 기본값을 사용하겠다고 명시할 수 있다.

@OneToMany(mappedBy = "diary", cascade = CascadeType.ALL)
    @Builder.Default
    private List<Page> pages = new ArrayList<>();

Controller, Service, Repository

의도적으로 MVC 패턴을 구현하기 위해 Controller, Service, Repository 3개의 계층으로 나누어 코드를 작성하고 있다.

하지만 그럼에도 불구하고 어떤 계층에 어떤 코드를 넣어야할지 모호할 때가 있다고 느껴져서 헷갈린다.

특히 테스트를 작성할 때도 특정 계층에서 어떤 데이터를 받고 반환해줘야하는지 헷갈려서 그 기준을 명확히 하고자 했다.

Controller

요청 정보에 대한 검증
Service 계층에서 요구하는 형태로 데이터 변경
Service 계층에서 반환받은 데이터를 User에게 제공할 형태로 변경

Service

데이터 연관관계 처리 등 controller, repository에서 하지 않는 모든 처리

Repository

DB 접근 관련 코드만

Reference

0개의 댓글