kdmstj

[항해 플러스] 1주차 회고: 테스트에 대한 생각 정리 본문

회고 및 후기/항해 플러스 백엔드

[항해 플러스] 1주차 회고: 테스트에 대한 생각 정리

kdmstj 2025. 8. 18. 17:01

이번주에는 TDD 와 테스트 작성에 대해 고민해보는 시간을 가졌습니다. 아래와 같은 질문 목록을 정리하며, 테스트에 대한 내 생각 정리해보기라는 목표를 세웠습니다.

목표

  • 어떤 계층에서 무엇을 검증할 것인가?
  • 단위 테스트 vs 통합 테스트 기준 정립
  • 테스트 대역(Test Double) 사용 기준 정립
  • 가독성 있는 테스트 코드 작성법에 관리하여 
    • Test Fixture 에 대하여 정리
    • @Nested 로 논리적으로 분리
    • @ParmaterizedTest 와 Source

 

이번 주에 고민한것들

1. 어떤 계층에서 무엇을 검증할 것인가?

개발을 하다 보면, 특정 로직이 어디에 위치해야 하는지 명확하지 않아 고민이 될 때가 있습니다.

 

이번에 포인트 충전/차감 로직과 유효성 검증을 구현하면서, 특히 서비스(Service) 계층과 도메인(Domain) 계층의 역할을 명확히 구분하지 못해 코드가 불명확하고 테스트하기 어려운 상황이 발생했습니다.

 

처음에는 서비스 계층에 충전/차감 로직을 구현하고, 도메인 계층에는 단순히 유효성 검증만 맡기는 방식으로 작성했습니다. 하지만 도메인 주도 설계(DDD) 관점에서 다시 고민해 본 결과, 1) 핵심 비즈니스 규칙(포인트 충전/차감)은 도메인 모델이 책임져야 하고 2) 서비스 계층은 트랜잭션 관리와 repository 호출을 통해 도메인 객체 간 협력을 조율하는 역할만 하도록 분리하는 것이 적절하다는 결론에 도달했습니다.

 

이렇게 개선하자 각 계층의 책임이 명확해졌고, 도메인 객체만으로도 핵심 로직을 테스트할 수 있게 되어 테스트 작성이 훨씬 수월해졌습니다. 계층별 역할과 검증 범위를 명확히 정의하는 것이 유지보수성과 테스트 품질을 높이는 데 큰 도움이 된다는 점도 깨달았습니다.

계층별 검증 범위 정리

  • Domain 계층: 핵심 비즈니스 규칙과 로직 검증 (예: 포인트 충전/차감 규칙)
  • Application 계층: 여러 도메인 객체 간의 협력, 트랜잭션 처리, 외부 시스템 연계
  • Presentation 계층: HTTP 요청/응답 포맷, 입력값 유효성 검증, 상태 코드
  • Repository 계층: 쿼리가 의도한 대로 동작하는지 검증

 

2.  단위 테스트 vs 통합 테스트 기준 정립

개발 초기에는 단위 테스트와 통합 테스트를 언제 사용해야 하는지 명확히 구분하지 못했고, 어떤 시나리오를 어느 테스트에 넣어야 할지 많은 고민이 있었습니다.

 

우선, 좋은 테스트의 원칙인 FIRST 중 첫 번째인 Fast를 만족하기 위해서는, 모든 빈을 띄우느라 속도가 느린 통합 테스트보다는 단위 테스트가 더 적합하다고 판단했습니다. 그래서 도메인, 서비스, 컨트롤러 계층에 대해 우선적으로 단위 테스트를 작성했습니다. 특히 컨트롤러와 서비스 계층의 경우 외부 의존성을 분리하기 위해 Mockito를 활용해 mock 처리했습니다.

 

하지만 DB, 트랜잭션, 실제 저장소와 연관된 동시성 테스트나, 여러 계층이 함께 동작하는 복잡한 시나리오를 검증하기에는 단위 테스트만으로는 한계가 있었습니다. 이런 “큰 시나리오”를 통합 테스트로 검증하는 것이 좋은 이유는, 각 계층과 실제 인프라가 함께 동작하는 환경에서만 발생할 수 있는 문제(예: 트랜잭션 롤백, 레이스 컨디션 등)를 사전에 확인할 수 있기 때문입니다.

 

결국, 단위 테스트는 빠르고 명확한 피드백을 받을 수 있다는 장점이 있고, 통합 테스트는 실제 환경과 유사한 상황에서의 동작을 보장할 수 있다는 장점이 있다는 것을 깨달았습니다. 그래서 저는 다음과 같은 기준을 세웠습니다. 단위 테스트를 기본으로 작성하되, 실제 환경에서 발생 가능한 문제나 흐름을 문서처럼 설명하고 싶을 때 통합 테스트를 작성하자.

 

 

3. 테스트 대역(Test Double) 사용 기준 정립

Stub 과 Mock 요약

구분 Stub Mock
목적 고정된 동작/값 반환 호출 여부/횟수/순서 검증
관점 상태 기반 테스트 (무엇을 반환했는가) 행동 기반 테스트
사용 시점 응답에 따라 대상 로직이 달라질 때 협력 객체와의 상호 작용 검증할 때

 

Stub 예시

when(memberRepository.existsByEmail(email)).thenReturn(true);

 

Mock 예시

verify(weatherService, times(1)).getWeather();

 

너무 많은 Mock 객체를 쓰는 경우 문제가 발생한다.

1. 테스트 코드 관리 비용 증가

  • given 메서드로 협력 객체 행위를 설정할 때 테스트 대상 객체와 협력 객체가 어떤 식으로 협력할지 이미 알고 작성을 해야 합니다.
  • 테스트 코드에 협력 객체와 어떤 식으로 대화할 것인지가 드러나 버리게 되어 (테스트 코드와 구현 코드의 강결합 발생) 그래서 테스트 대상 객체를 리팩토링할 때, 협력 객체와 협력 방식에 조금이라도 변경이 생기면 관련 테스트 코드가 모두 깨지는 상황이 펼쳐지게 됩니다.

2. 신뢰도가 떨어지는 테스트 코드

  • Mock 객체에서 given 메서드로 행위를 설정할 때, production 에 사용할 협력 객체의 실제 구현체가 그렇게 동작하기를 기대하면서 작성을 하게 되지만,실제 구현체는 의도한대로 동작하지 않을 수 있습니다.

그래서 저는 Mock 객체 사용을 최소화하고, @MockBean 은 Presentation Layer에서 직렬화/역직렬화 테스트 등, 꼭 필요한 경우에만 사용했습니다.

 

4. 가독성 있는 테스트 코드 개선

1. TestFixture 도입

테스트마다 유사한 객체 생성 코드가 반복되면서 중복이 많고, 가독성이 떨어지는 문제가 있었습니다.

 

처음에는 @BeforeEach 어노테이션을 사용해 매번 동일한 파라미터로 객체를 생성하는 방식을 고려했지만, 이는 좋은 테스트의 원칙(FIRST) 중 하나인 Independent(독립성) 을 지키지 못한다고 판단했습니다. 또한, 테스트 코드만 읽었을 때 각 테스트의 맥락과 의도를 바로 이해하기 어렵고, 테스트마다 다른 파라미터를 설정하기 힘들다는 한계도 있었습니다.

 

이에 TestFixture를 도입해 필수 파라미터만 전달하고, 나머지 값들은 기본값을 사용하도록 개선했습니다.

public static User createUserWithPoint(long point) {
    return createDefaultUser().toBuilder().point(point).build();
}

이에 테스트의 의도를 명확히 드러내고 코드 중복을 줄일 수 있었으며, 가독성도 높아졌습니다. 특히 실무에서 객체 필드가 추가될 때마다 테스트 코드의 여러 곳을 일일이 수정하느라 어려움을 겪었던 경험이 있었는데, TestFixture만 수정하면 되어 유지보수성도 크게 향상되었습니다.

 

2. @Nested로 논리적 그룹화

@Nested
@DisplayName("포인트 차감 테스트")
class PointUseTest {
    @Test
    void 포인트가_부족하면_예외가_발생한다() {
        ...
    }
}

테스트 메서드들을 관련된 기능별로 논리적으로 그룹화하기 위해 JUnit의 @Nested를 적용했습니다. 이를 통해 테스트의 구조가 명확해지고, 테스트의 의도를 더 쉽게 파악할 수 있도록 개선했습니다.

 

3. @ParameterizedTest를 사용한 경계값 테스트

@ParameterizedTest
@CsvSource(
	{"1000, 2000",
	"0, 100"}
)
void 포인트가_부족하면_예외(long origin, long use) {
    ...
}


다양한 입력값을 체계적으로 검증하고, 테스트 시나리오를 한눈에 파악할 수 있도록 @ParameterizedTest를 활용했습니다. 특히 @CsvSource를 사용해 여러 인자를 조합한 경계값 테스트를 작성해 코드의 신뢰성과 커버리지를 높였습니다.

 

얻은 인사이트

  • 익숙하지 않은 TDD를 적용하면서 개발 속도는 느렸지만, "이 로직은 어디에서 테스트해야 하지?"라는 질문을 시작으로 역할과 책임을 고려한 구조 설계를 하게 되었습니다.
  • 단위 테스트는 빠르고 신뢰성 있는 피드백, 통합 테스트는 실제 상황을 시뮬레이션할 수 있는 장점이 있다는 것을 체감했습니다.
  • 처음에는 통합 테스트에 대부분의 로직을 넣었지만, 이제는 단위 테스트로 핵심 로직을 검증하고 통합 테스트는 흐름만 확인하는 방향으로 확신이 생겼습니다.
  • Mock 객체 사용은 꼭 필요한 곳에서만, 신중하게 선택해야 한다는 점도 다시 한번 깨달았습니다.