공부내용공유

테스트 코드 주저리 주저리 본문

Spring

테스트 코드 주저리 주저리

forfun 2025. 3. 23. 15:47

서론


취직을 하고 온보딩 프로젝트를 하던데 엊그제 같은데 어느새 경력이 1년 6개월에 가까워지고 있다.

 

 

대략 1년 4개월 동안 여러 프로젝트를 하면서 꽤나 많은 테스트 코드를 작성하였는데 최근 술 블로그 글들을 읽다가 여러 글들에서 테스트 코드에 대해서 많이 다뤄서 나도 한번 테스트 코드에 대한 내 생각을 정리하려고 글을 썼다.

 

 

본론


 

이 글의 목차는

    1. 테스트 코드의 장점, 한계
    1. 어느정도로 짜야할까

으로 구성될 예정이다.

 

 

1. 테스트 코드의 장점

 

테스트 코드를 왜 짜야할까? 만약 테스트 환경이 구축이 안되어 있어서 구축을 해야한다면 어플리케이션 코드를 개발할 때 보다 오히려 시간이 더 걸리기도 한다. 업무 마감 기한도 빡빡한데 야근까지하면서 짜야하는 이유가 뭐가 있을까?

 

 

내가 느꼈던 테스트 코드의 장점을 느꼈던 경험들과 같이 서술해보겠다.

 

 

1)  코드 리뷰에 도움이 된다.

 

물론 이건 테스트 코드를 어떻게 짜는가, 팀 내 컨벤션이 있는가에 따라 달라질 수 있는 경우이다.

 

 

우리 팀에서는 테스트 코드를 짤 때 물론 기능 테스트가 주목적이기도 하지만 @DisplayName과 메서드 명등을 활용하여 해당 클래스,메서드가 어떤 역할을 하고 있고 왜 이렇게 작성했는지를 담으려고 한다.

 

 

즉, 테스트 코드에서 happy case, 코너 케이스등을 다루면서 명세를 할 때 비즈니스적인 용어로 최대한 풀어서 작성을 하여 팀원들에게 비즈니스에 대한 설명, 클래스의 책임을 좀 더 자연어로 설명할 수 있는 것이다.

 

 

예시를 간단히 들자면

@Test
@DisplayName("사용자를 삭제 실패")
void test() {
    //given
    User user = userRepository.getUser("id");

    //when & then
    assertThrownThat(userRepository.delete(user)).exception(CannotDeleUserException.class);
}


@Test
@DisplayName("amdin 사용자는 그냥 삭제할 수 없고 다른 admin 사용자 2명 이상이 삭제 허가를 눌러야 삭제가 가능하다. 그렇지 않을 경우 예외를 반환한다.")
void admin_user_() {
    //given
    User user = userRepository.getUser("id");

    //when & then
    assertThrownThat(userRepository.delete(user)).exception(CannotDeleUserException.class);
}

 

예시를 갑자기 떠올리기가 어려워서 조금 어거지긴한데 어떤 느낌인지 이해했으면 좋겠다, 테스트 내부 내용, 명세서에 내가 만든 기능을 설명할 수 있고 팀원은 이해를 더 깊게 하면서 비즈니스상 오류를 같이 찾아줄 수 있다.

 

 

2) 미래의 나한테 도움이 된다.

 

어플리케이션은의 기능은 생각보다 자주 많이 바뀐다. 그리고 바뀌는 시점은 내가 개발을 한 당시가 아닌 다른 프로젝트 1~2개를 하고 난 후인 6개월 뒤일 수도 있다.

 

 

오랜만에 해당 프로젝트에 들어가서 변경해야하는 메서드를 딱 보았다.

21 usage
public User upadteUserToAdmin() {
    ...
}

 

 

아찔하다.. 실제로 이정도 되기가 쉽지 않고 이렇게 안되게 코드를 작성하는 것도 당연히 중요하지만 이런 경우가 절대 없을거라고 자신할 수도 없다.

 

 

수정해야하는 메서드를 모두 체크하고 영향범위 파악도 당연히 해야하지만 테스트 코드는 내가 예상치 못한 예외를 잡아주기도 하고 나한테 2차적인 심리적 방어선이 되어준다.

 

 

1번과 비슷한 결로 오래전에 작성한 코드의 비즈니스 로직, 책임, 역할에 대한 복기를 빠르게 해주고 놓칠 수 있는 부분도 잘 잡아준다. (물론 잘 짜여진 테스트 코드라는 가정이 깔려있다.)

 

 

3) 코드 스멜의 지표가 된다.

 

해당 이점은 여러 기술 블로그들에서도 다룬 것인데 클래스가 여러 책임을 가지고 있고 외부 의존성에 많이 노출되어 있다면 테스트 코드도그만큼 짜기 어려워지고 중복되는 케이스, 불필요한 코드들이 많아진다.

 

네이버 d2 테스트 코드 관련 블로그 글

 

위 글에서도 볼 수 있듯이 한 클래스, 메서드의 책임은 한가지에 집중해야하고 외부 의존성들을 다형성, 의존성 역전, 의존성 주입등을 활용하여야 본질적인 부분만을 테스트할 수 있게 된다. (부수적인 코드나 중복되는 부분들이 제거된다.)

 

 

즉, 이런 테스트 코드가 짜지지 않는다면 코드 스멜이라 생각하고 어떻게 개선할 수 있는지 고민해야 한다. 이를 통한 개선은 단순히 테스트 코드를 잘 짤수 있어질 뿐만 아니라 보다 더 SOLID한 코드로 유지보수도 좋아진다.

 

 

온보딩 때 팀장님께 이와 유사한 결로 클래스를 조금 분리하여 테스트 코드를 더 세분화하여 짜고 책임을 잘 분히라면 좋을 것 같다는 리뷰를 받은적이 있었는데 실제로 나중에 기획 사항이 변경되면서 코드들을 수정할 때 더 편하게 수정한 경험이 있다.

 

 

이 외에도 여러가지가 있지만 내가 직접 경험하고 느낀 큰 부분들은 이정도이다 그러면 한계점은 뭐가 있을까?

 

 

1) 실제 prod 환경과 100% 맞추기 쉽지 않다.

 

최근에 겪은 경험인데 난 특정 기능을 구현 후 테스트를 다 짜고 잘 돌아가네 ㅎㅎ 하면서 개발 서버에 반영한 적이 있다.

 

 

그런데 개발 서버에서 해당 api를 사용하던 프론트 팀원 분이 간헐적으로 api가 실패한다고 얘기를 해주셨고 삽질을 하다보니 api에서 사용하던 db에 proxy sql이 적용되어 있었고 같은 api의 read, write가 어떨 땐 동기화가 되어 잘 읽어오고 어떨 땐 정합성이 깨져서 발생하던 문제였다.

 

 

요즘에 test container와 같이 최대한 실서버와 유사한 환경을 구축할 수 있긴 하지만 분명 한계들이 있을 것이기에 이런 부분은 테스트 코드로는 다 커버하기에 무리가 있다.

 

 

2) '잘' 짜야한다.

 

jacoco와 같은 테스트 커버리지를 사용하여 테스트 코드가 어플리케이션 코드의 몇 %를 커버하는지 수치화 할 수 있다. 수치적인 커버리지도 당연히 무시할 수 없지만, 커버리지만 신경 쓴 코드는 좋은 테스트 코드라 할 수 없다.

 

 

또 그냥 팀 컨벤션이니까 오늘은 좀 바쁘니까 하면서 mock으로 떡칠한 테스트 코드도 사실상 안짜는게 더 나을 것이다.

 

 

정답은 없겠지만 각자 생각하는 장점을 잘 살린 테스트 코드를 짜야한다. 내가 생각했을 때는 비즈니스 명세를 잘 하고, 코너 케이스들을 잘 작성하고, 메인 시나리오 케이스도 잘 짜서 기능이 변경되면 테스트가 깨지게. 그래서 영향 범위를 쉽게 파악할 수 있게 짜야 한다고 생각한다.

 

 

2. 어느정도로 짜야할까?

 

보통들 코드, 테스트 코드를 작성할 때 presentation layer, domain layer, persistence layer, infra layer 등등으로 나눌 것이다.

1) presentation

presentation의 경우에는 프론트 팀에게 전달을 해야하기에 rest docs + swagger 등을 많이 혼합해서 웬만하면 다 코드를 작성하지 않을까 싶다. 근데 여기서 나뉠 수 있는 점이라면 유닛 테스트로 짜나 아니면 통합 테스트로 짜냐이다.

 

 

프로젝트에 따라 달라질 것 같은데 요즘 우리 팀내 프로젝트는 보통 클린 아키텍쳐를 지향하기에 presentation 계층은 굳이 통합 테스트까지 끌고올 필요 없이 유닛 테스트로 진행해도 충분하지 않나 생각한다. presentation 계층은 엔드포인트 와 request 맵핑에 집중하고 라이브러리 힘을 빌려서 실제 api와 문서 상의 정합성을 맞추는데 집중해야 하기 때문이다.

 

2) domain

 

domain의 경우에도 클래스의 책임에 따라 다를 것이다. utility class의 경우에는 유닛 테스트로 충분할 거지만 그렇지 않은 클래스들은 통합 테스트를 작성하여 서로의 상호작용과 스프링의 기능들이 잘 사용되고 있는지 확인을 해야 할 것이다.

 

 

도메인 테스트의 경우에는 아까 위에서 말한 것처럼 테스트의 종류, 기능도 중요하지만 비즈니스 적인 명세 좀 더 집중을 하는게 좋지 않을까 생각한다.

 

3) persistence, infra

 

persistence나 infra의 경우에는 통합까지는 아니더라도 보통 spring 진영의 기술을 많이 사용할거기에 @SpringDataTest와 같은 어노테이션이나 각 infra에 사용되는 환경설정 어노테이션으로 필요한 최소한의 환경을 만들고 각 기능이 잘 작동하는지 테스트를 해야한다.

 

 

조금 tmi지만 사실 난 persistence 테스트를 짤 때 querydsl이나 native query를 사용하는 경우에는 테스트 코드를 작성하고 간단한 jpa나 쿼리 네이밍 메서드 같은 경우에는 테스트 코드를 작성하지 않는다. (물론 쿼리가 조금 복잡해진다 싶으면 querydsl이나 native query등을 사용한다.)

 

 

결론


대략 이 정도로 테스트 코드에 대한 내 생각을 정리하였다, 다음 글에서는 지금까지 테스트 코드를 작성하면서 알게된 꿀팁들에 대해 간단히 정리할 예정이다.