인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

김선도님의 프로필 이미지
김선도

작성한 질문수

토비의 스프링 부트 - 이해와 원리

Service 계층에서 테스트 관련해서 질문이 있습니다.

작성

·

1.2K

·

수정됨

5

안녕하세요 토비님

제가 프로젝트를 진행하면서 도저히 모르는 부분이 있습니다.

강의와 관련이 없는 질문이지만, 간절한 마음으로 질문해봅니다

 

Service 계층은 상태검증과 행위 검증에 대한 고민이 있습니다.

특히, 객체의 책임과 테스트 범위에 대한 관점에 대한 차이때문에 고민이 있습니다.

우선, 코드를 보여드리겠습니다.

CommunityCommandService.updateCommunity는 커뮤니티의 소개란과 해시태그를 업데이트하는 부분입니다.

//CommunityCommandService.java
public void updateCommunity(Long userId, Long communityId, String description, List<String> newTags) {  
    Community community = communityRepository.findCommunityById(communityId);  
    memberQueryService.getManager(userId, communityId);  
  
    community.updateCommunity(description, newTags);  
}

해당 코드는 communityId로 community를 가져오고, userId / communityId로 요청한 유저가 메니저인지 확인합니다.

그 후, community 객체에게 update를 위임합니다. 그러면 community 객체는 내부 상태값을 변경합니다.

 

여기서, 상태 검증인지 행위 검증인지에 따라 테스트가 달라집니다.

public class CommunityCommandServiceTest {
    @Test
    void 상태검증_테스트() {
        Community community = new Community("dummy Intro", List.of("dummy tag"));
        given(communityRepository.findById(any)).willReturn(community);

        communityCommandService.updateCommunity(1L, "new intro", List.of("new tag"));

        assertThat(community.getIntroduce).isEqualTo("new intro");
        assertThat(community.getTags).containsExactly("new tag");
    }


    @Test
    void 행위검증_테스트() {
        Community community = mock(Community.class);

        given(communityRepository.findById(any)).willReturn(community);

        communityCommandService.updateCommunity(1L, "new intro", List.of("new tag"));
        then(community).should(times(1)).update("new intro", List.of("new tag"))
    }
}

상태검증_테스트의 검증 부분을 보면, 위임한 결과에 대해서 테스트를 진행하고 있습니다. Community 클래스의 update를 또 테스트하는 것 같은 느낌이 있습니다. 즉, 서비스 계층의 테스트 영역을 넘어서는 것인지 의문입니다.

반면, 행위검증_테스트는 community.update가 호출하면서 위임했는지에 대해서만 테스트합니다. 하지만, 내부 로직에 하드코딩 되어있는 듯 합니다.

사실 저는 상태검증을 더 선호합니다. 하지만 상태 검증이 객체지향스러운지 잘 모르겠습니다. 어느정도 감수해야하는 것 일까요?

 

정리하자면,

  • 상태검증

    • CommunityCommandService.updateCommunity로 변경된 상태를 테스트

    • 개인적으로 선호하는 방식. 하지만, 상태를 테스트하기 때문에 객체지향의 관점에서 맞는지 확신이 없다. 테스트코드는 이 부분을 감수하는 것인지?

  • 행위검증

    • 협력한 객체의 행위에 대한 테스트

    • 객체지향의 관점에서 위임이 잘 이루어졌는지 테스트하는게 자연스럽다고 생각

 

질문하자면,

서비스계층에서 Community.update()로 커뮤니티 내부 값에 대한 변경을 요청하였습니다.

객체지향에서는 객체들이 서로 책임을 위임하며 상호작용하는 것이기 때문에

Service 계층에서는 위임이 되었는지 호출 여부만 판단하는게 적절한지,

 

아니면, Service 계층에서 위임한 그 결과 Community의 내부값을 바꾼게 적절한 테스트인지..

만약 이 방법이 맞다면 객체지향스럽다고 말할 수 있는지..?

어떤 방식이 적절한지 잘 모르겠습니다.

 

이론적인 부분과 실제 테스트에 대한 괴리때문에 발생하는 문제 같습니다.

 

긴 질문 읽어주셔서 감사드립니다!

 

답변 2

9

토비님의 프로필 이미지
토비
지식공유자

말씀하신 내용은 잘 이해하겠습니다. 최신 테스트 책이나 글을 읽어보면 이런 접근 방법에 대하 다양한 고민들이 보이기도 합니다.

이건 단위 테스트의 단위를 어떻게 잡을 것인가의 문제이기도 합니다. 그리고 의존 오브젝트, 협력 오브젝트로 복잡하게 얽혀있는 구조에서 테스트는 어떻게 하는게 좋은가의 문제이기도 하죠.

저는 이건 상태 검증, 행위 검증 같은 식으로 구분하고 접근하는 걸 좋아하지 않습니다. 무슨 의미인지는 알겠으나 그런 접근방법이 뭔가 기계적인 틀에다가 유연하게 성장하고 발전하는 코드를 억지로 맞추는 듯한 느낌이 들어서 그렇습니다. 물론 목 테스트 등의 주제를 한번쯤 고민해볼때 설명하기 좋은 방식이긴합니다.

저라면 내가 작성하는 코드를 잘 검증하고 있나, 뭘 검증할 것인가, 어떻게 검증할 것인가, 얼마나 빠르게 검증할 것인가 등의 관점으로 이번엔 테스트를 어떻게 작성할까를 결정합니다. 테스트가 너무 복잡해지거나, 수행이 느리거나, 리팩토링할 때마다 테스트도 많이 고쳐야 하거나, 테스트에 버그가 잘 들어가게 되거나 하는 상황을 잘 살펴보면서 주의하고 방향을 조금씩 수정합니다.

지금 봐서 Community는 다른 의존 관계가 없는 단순한 도메인 오브젝트가 아닌가 싶습니다. 이건 작성하면서 빠르게 테스트를 만들 수 있고, 충분히 검증할 수 있고, 빠르게 테스트 수행도 가능하죠. 그러니까 테스트를 잘 만들어야하고 잘 만들기 쉽습니다.

그런데 이 Community 오브젝트에는 자신의 상태를 가지고 로직을 수행하는 기능이 잘 들어가겠죠. 그리고 이 기능은 다른 여러 서비스 객체 등에서 자유롭게 사용할 수 있습니다. 그런데 이것도 기능을 수행하는 것이고, 그래서 CommunityCommandService.updateCommunity() 에 대한 테스트가 수행되는 동안에 Community 기능도 같이 동작을 해버리니 순수하게 updateCommunity()에 대한 상태 검증만 안 하는게 아닌가 싶을 수도 있겠죠. 그래서 이걸 또 목으로 만들고 Community의 어떤 기능을 사용하는지를 또 테스트에서 체크하고...

이런식으로 개발하다보면 테스트 작성하다가 지쳐서 전체 개발이 느려지고 테스트는 점점 복잡해지고, 애플리케이션 코드의 발전을 테스트 코드가 발목을 잡게 될 수도 있습니다. 그냥 상태 테스트라고 생각하고 Community.update()까지 수행하는 코드를 테스트 하세요. Community는 별도의 단위테스트로 잘 검증을 했다면 문제없을 것이라고 가정을 해도 됩니다. 굳이 mock까지 만들 필요가 없겠죠. mock은 정말 테스트 하기 어렵거나, 당장에 개발이 안 돼서 테스트 대상이 수행되는 걸 어렵게 만드는 외부 의존 기능과의 관계를 포함한 기능을 테스트할 때 정도 사용하면 됩니다. 딱히 행위를 검증하겠다라기 보다는 테스트 대상의 기능을 충분히 검증할만한가 보고, 필요에 따라 추가하는 것이지요.

문제는 위와 같은 Command 류의 코드는 그 안에서 발생하는 부수효과를 확인하기가 매우 어렵다는 점입니다. 이게 결국 DB에서 가져와서 업데이트만 하고, 결과는 리턴도 안 해주는 스타일이라서 그렇습니다. 제가 매우 안 좋아하는 방식이죠.

저라면 업데이트된 Community를 리턴하게 만들어서 그걸 검증하고, 필요에 따라 호출한 쪽에서 사용하게 만들겠습니다. 커맨드는 리턴하면 안 된다라고 하지만 꼭 그래야만 하는지 모르겠습니다. 그렇지 않으면 이 경우 테스트는 지금 만드신 방법대로 외부에서 가져오는 코드부터 mock을 사용해서 그 오브젝트에 어떤 변화가 일어나는지 등을 체크하게 만들거나, 아니면 DB까지 참여하는 통합 테스트로 만들어서 update()를 수행하고 DB에 다시 질의를 해서 최종 결과를 확인하거나, 아니면 변경에 대한 이벤트를 던지게 해서 그 이벤트를 받아서 확인하는 등의 복잡한 간접적인 수단을 써야 하는데 그게 꼭 필요한 상황이 아니라면, 테스트를 복잡하게 만들고 느리게 수행하게 만들고, 코드를 검증하기 힘들어서 테스트를 잘 안 만들게 만들죠.

저라면 그냥 Community를 리턴하겠습니다. 설령 애플리케이션 코드에서 그걸 받아서 활용할게 없다고 하더라도 말이지요. 리턴 값은 죽어도 쓰기 싫다면 Supplier 콜백을 하나 넣어서 거기다가 집어 넣게 해서 확인하든지요. 이벤트 퍼블리셔 같은 건데.. 콜백을 통해서 받으나 리턴으로 받으나 그게 그거 아닌가 싶습니다.

어떤 틀과 룰에 모든 걸 맞춰서 개발하려고 하는 것보다는, 그런 원칙, 가이드 등이 추구하고자하는 게 뭔지, 어떤 걸 이롭게 만들어서 개발을 더 돕는 것인지 등을 잘 생각해보면서 그때그때 유연한 방식으로 접근하시면 좋지 않을까 싶습니다.

 

김선도님의 프로필 이미지
김선도
질문자

답변 정말정말 감사드립니다!!ㅠㅠㅠㅠ

설명해주신 내용들이 부분부분만 이해가 되네요 ㅠㅠ

결론은 형식에 억매여서 테스트 하지 말자는 거 같고...

update 동작에 대한 테스트로는 반환값을 주도록 해서 반환이 잘 되는지 테스트 하는 것을 추천....!?

4

토비님의 프로필 이미지
토비
지식공유자

그리고 이 코드가 테스트하기 복잡해지는 이유는 repository라는 외부 오브젝트에 직접 의존하고 있다는 점이거든요. 이걸 단순한 형태의 조회용 인터페이스를 받도록 하고 호출하는 쪽에서 리포지토리를 사용하는 함수를 넘기도록 만들면 또 테스트가 수월해집니다. 굳이 mock을 쓰지 않고 간단한 람다식을 넣어서 테스트 할 수 있죠. 가끔 사용하는 방식입니다.

그 외에 권한 체크하는 위치나 방법은 좀 더 나은 방법을 생각해보는 것도 좋겠습니다.

김선도님의 프로필 이미지
김선도

작성한 질문수

질문하기