[TDD] 테스트 코드 작성

2025. 7. 18. 17:07·Concept/테스트코드

테스트 코드를 작성하면서 배운 것을 작성해봅니다. 
단순히 “코드가 잘 돌아가는지 확인하는 용도”를 넘어서 리팩토링의 자신감을 얻고, 테스트 더블을 다루며 다양한 상황을 제어하는 방법을 익혔습니다.
또한 Spy와 Mock을 직접 써보면서 둘 사이의 차이를 체감할 수 있었고, 그 차이가 단순한 동작 여부를 넘는다는 것도 알게 되었습니다.

아래는 제가 테스트 코드를 작성하면서 정리한 주요 포인트들입니다.

목차

  1. 테스트 코드로 얻는 리팩토링의 안정감
  2. 테스트 더블(Test Double)이란?
  3. Spy와 Mock을 구분하며 느낀 점
  4. Spy와 Mock, 단순히 “실제 동작 여부”만의 차이가 아니다

 

[1] 테스트 코드로 얻는 리팩토링의 안정감

처음에는 API 요구사항만 만족하게 코드를 작성했다. 
하지만 개발을 진행하며 여러 가지 사항들이 고민 되었고 리팩토링을 진행하게 되었다. 
이때 테스트코드가 있어 다음과 같은 변경을 진행한 후 요구사항들이 잘 작동되는지 확인할 수 있었다.

  1. Facade용 DTO 추가
    처음엔 엔티티를 Controller까지 전달해 Controller에서 Presentation DTO 로 변경하거나,
    여러 도메인을 조합해서 사용자에게 전달하기 위해서 Facade에서 Presentation DTO를 만들어 넘겼다. 
    그러나 그렇게 되면 계층 간 경계가 흐려지고 Controller와 Domain 사이 경계가 모호해진다. 그리하여 Facade 계층 전용 DTO를 추가했다.
  2. userId → memberId로 이름 변경
    User 엔티티에는 id와 userId가 있었고, 다른 도메인에서 User.id를 userId로 사용했다.
    같은 프로젝트 안에서 혼동돼 버그 위험이 있기 때문에 의미를 명확히 하기 위해 이름을 바꿨고, 테스트가 있었기 때문에
    코드 수정 후에도 기능 동작 여부를 쉽게 확인할 수 있었다.
  3. 포인트 생성 로직 변경
    회원가입 시 포인트를 함께 생성하도록 했다. 그러므로 포인트 생성시 값은 항상 0이어야 했다.
    기존에 new Point 시 초기값을 받던 구조를 무조건 0으로 생성하도록 변경했다.

테스트코드는 이런 리팩토링을 할 때 빠르게 문제를 감지하고 기능 작동에 대한 안전망이 되었다.
결과적으로 더 나은 설계를 선택할 수 있었다.


[2] 테스트 더블(Test Double)이란?

테스트를 작성하다 보면, 실제 의존 객체를 그대로 사용하기 어려운 상황이 있다.
예를 들어 외부 API를 호출하거나, DB에 직접 접근해야 하는 객체를 테스트하려면 테스트 환경을 복잡하게 준비해야 하고, 실행 속도도 느려진다. 이런 문제를 해결하기 위해 사용하는 것이 테스트 더블(Test Double)이다.

즉, 테스트 대상 클래스만 검증하고 나머지 의존성은 단순화하거나 가짜로 치환하는 "격리"의 의미를 지닌다.

[2-1] 테스트 더블 종류 

상황에 맞게 아래 테스트 더블을 선택한다. 

종류 사용하는 상황 특징 코드 예시 
Fake 실제 구현 대신 단순한 대체 구현으로 테스트하고 싶을 때 실제처럼 동작하지만 간소화된 버전. 주로 메모리 기반으로 구현 InMemoryRepository를 구현해 DB 대신 사용
Stub 특정 메서드가 호출될 때 정해진 값을 반환하도록 하고 싶을 때 고정된 응답을 준비해 놓은 객체.
외부 시스템에 의존하지 않음
when(repo.findById(1L)).thenReturn(mockUser)
Spy 실제 객체처럼 동작하면서 호출 내역을 추적하고 싶을 때 실제 메서드 호출 후
호출 횟수/인자 등을 검증 가능
verify(service, times(1)).sendEmail()
Mock 외부 의존성을 완전히 모킹하고, 특정 동작이 있었는지/없었는지를 검증하고 싶을 때 기대한 동작을 사전에 정의하고, 테스트 종료 후 호출 여부를 검증 verify(repo).save(any())

 

[3] Spy와 Mock을 구분하며 느낀 점

Mock과 Spy는 처음에는 비슷해서 구분하기 어려운데, 여러 요구사항을 테스트하다보면 차이가 명확해진다.
특히 Spy는 “실제 동작”과 “stubbed 동작”을 한 테스트 클래스 안에서 혼합할 수 있다는 점이 좋았다.

예를 들어 같은 테스트 클래스에서:

  • 기본적으로는 실제 로직을 수행하게 두고,
  • 특정 메서드만 doReturn()이나 when()으로 원하는 값으로 덮어쓸 수 있다.

덕분에 “이 부분은 실제 DB처럼 동작하길 원하지만, 저 부분은 값만 고정해서 빠르게 테스트하고 싶다” 같은 상황을 한 번에 처리할 수 있었다.

Spy를 코드로 보면, 

@MockitoSpyBean
private UserJpaRepository userJpaRepository; // 실제 빈을 Spy로 감싼다.

@Test
void spyExample() {
    // 기본적으로는 실제 save 동작
    User saved = userJpaRepository.save(new User("id1", "test@test.com"));

    // 하지만 특정 메서드만 원하는 동작으로 덮어쓰기
    doReturn(Optional.of(saved))
        .when(userJpaRepository)
        .findById(saved.getId());

    // 이후 테스트에서 findById는 doReturn 값 반환
    Optional<User> found = userJpaRepository.findById(saved.getId());
    assertThat(found).isPresent();
}

 

비교해서 Mock을 코드로 보면, 

@MockitoBean
private UserJpaRepository userJpaRepository; // 가짜 객체

@Test
void mockExample() {
    User user = new User("id1", "test@test.com", "2000-01-01", Gender.MALE);

    // Mock은 기본적으로 아무 동작도 하지 않으므로,
    // save() 호출 시 어떤 값을 반환할지도 명시적으로 정의해야 한다.
    when(userJpaRepository.save(any(User.class))).thenReturn(user);

    // findById도 마찬가지로 직접 정의
    when(userJpaRepository.findById(user.getId())).thenReturn(Optional.of(user));

    // 이제야 원하는 동작 수행 가능
    User saved = userJpaRepository.save(user);
    Optional<User> found = userJpaRepository.findById(saved.getId());

    assertThat(found).isPresent();
}

 

 

[4] Spy와 Mock, 단순히 “실제 동작 여부”만의 차이가 아니다

테스트를 작성하면서 가장 많이 혼동하는 두 가지가 Spy와 Mock이다.
둘 다 테스트 더블의 한 종류이지만 용도와 철학이 다르다.

[4-1] 기본 동작 여부만이 전부는 아니다

  • Mock:
    기본적으로는 아무 동작도 하지 않는다.
    “이 메서드가 이렇게 호출될 것이다”라는 기대(Expectation) 를 먼저 세팅하고,
    테스트 후에 “정말 그렇게 호출됐는지”를 검증(Verification) 한다.
  • Spy:
    실제 객체를 감싸서 실제 메서드를 실행한다.
    다만 특정 메서드만 doReturn() 등으로 스텁할 수 있다.
    Spy의 주된 목적은 호출 내역을 감시(Observation) 하는 데 있다.

[4-2] 철학과 사용 의도

구분 Mock Spy
철학 행동 기반(Behavior-Based) 테스트에 초점.
“이렇게 호출되었는가?”
상태 기반(State-Based)와 행동 기반을 혼합.
실제 동작을 살리면서 호출 내역을 검증
주요 목적 특정 상호작용이 있었는지 검증 실제 로직을 유지하면서 일부만 스텁하거나
호출 기록을 확인
어떤 상황에서 외부 시스템이나 복잡한 의존성을 전부 가짜로 두고 싶을 때 실제 동작을 믿되, 특정 부분만 제어하고 싶을 때
 

[4-3] 검증 방식 차이

Mock은 기대-검증 패턴을 강하게 따른다.
예를 들어 verify(repo).save()처럼 “save가 정확히 한 번 호출됐는가?”를 검증하는 식이다.
테스트가 이 기대와 다르면 바로 실패한다.

Spy는 실제 동작을 살리기 때문에,
“실제 로직 결과가 어떤지”를 상태 기반으로도 검증할 수 있고,
필요하면 verify()로 호출 내역을 확인할 수도 있다.

[4-4] 테스트 설계에 미치는 영향

  • Mock을 많이 쓰면?
    테스트가 내부 구현(메서드 호출 횟수나 순서)에 강하게 결합된다.
    리팩토링 시 내부 로직을 조금만 바꿔도 테스트가 깨질 수 있다.
    즉, 행동에 대한 강한 검증이 필요한 시점에만 쓰는 것이 좋다.
  • Spy를 적절히 쓰면?
    실제 로직을 기반으로 하므로 리팩토링 내성이 더 높다.
    다만 실제 동작을 하다 보니, 외부 리소스를 건드리는 객체를 Spy로 감쌀 경우 원치 않는 동작이 발생할 수 있어 주의해야 한다.
 

 

 

저작자표시 비영리 변경금지 (새창열림)

'Concept > 테스트코드' 카테고리의 다른 글

id가 없는 상태의 객체 생성을 위한 편의 생성자  (0) 2025.04.25
내부 구조에 기대지 않고 외부에 드러나는 동작을 확인하자  (0) 2025.04.24
테스트주도개발(TDD) 시작하기  (3) 2024.12.22
'Concept/테스트코드' 카테고리의 다른 글
  • id가 없는 상태의 객체 생성을 위한 편의 생성자
  • 내부 구조에 기대지 않고 외부에 드러나는 동작을 확인하자
  • 테스트주도개발(TDD) 시작하기
devstep
devstep
웹 백엔드 개발자
  • devstep
    개발 여정
    devstep
  • 전체
    오늘
    어제
    • 분류 전체보기 (91)
      • Java (24)
      • Spring Framework (17)
        • Spring (14)
        • JPA (3)
      • Database (8)
        • RDBMS공통 (1)
        • MySQL (6)
        • Redis (0)
        • Oracle (1)
      • Concept (13)
        • 테스트코드 (4)
        • 클린코드 (2)
        • 성능테스트 (4)
        • 설계 (1)
        • 인증 (1)
        • REST API (1)
      • git (2)
      • Intellij (4)
      • Computer Science (3)
        • 네트워크 (1)
        • 자료구조 (1)
        • 보안 (1)
      • Essay (18)
        • Learning Essay (10)
        • WIL (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    인텔리제이실행에러
    DDD
    springsecurity
    JMeter
    seed
    storageEngine
    블록암호화
    비대칭암호화
    대칭암호화
    테스트코드
    tdd
    클린코드
    부하테스트도구
    bean
    JavaMemoryModel
    aggregate
    component
    자바메모리모델
    보안
    부하테스트
    innodb
    linux
    성능테스트
    단위테스트
    JVM
    applicationcontext
    nofile
    nginx
    ClusteredIndex
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
devstep
[TDD] 테스트 코드 작성
상단으로

티스토리툴바