TL;DR

  • Slice Test는 애플리케이션의 특정 계층만 잘라 빠르게 테스트한다.
  • @WebMvcTest, @DataJpaTest 등 전용 슬라이스 애노테이션이 핵심이다.
  • 전체 로딩보다 빠르고, 단위 테스트보다 실제 구성에 가깝다.

1. 개념

Slice Test는 Spring Boot에서 웹, 데이터, JSON 등 특정 계층만 로드하여 빠르게 검증하는 테스트 방식이다.

2. 배경

테스트 피라미드 원칙과 함께 @SpringBootTest의 느린 전체 컨텍스트 로딩이 병목이 되면서 계층별 테스트 전략이 필요해졌다.

3. 이유

테스트 속도를 높이면서도 실제 구성과 가까운 검증을 수행하기 위해 필요한 Bean만 로드하는 접근이 도입되었다.

4. 특징

@WebMvcTest, @DataJpaTest, @JsonTest와 @MockBean 활용, 계층별 자동 설정 분리, 빠른 피드백이 특징이다.

5. 상세 내용

Slice Test와 테스트 전략 - Spring Boot에서 “필요한 부분만” 테스트하기

작성일: 2026-02-26 카테고리: Backend / Testing / Spring Boot 포함 내용: Slice Test, @WebMvcTest, @DataJpaTest, @SpringBootTest, Test Pyramid, Unit Test, Integration Test, @MockBean, TestContainers, ApplicationContext, Auto-Configuration


1. 테스트가 왜 필요한가? (완전 기초부터)

1.1 테스트 = “내가 만든 코드가 제대로 동작하는지 자동으로 확인하는 코드”

┌─────────────────────────────────────────────────────────────────┐
│              테스트란 무엇인가? (완전 기초)                      │
│                                                                   │
│  비유: 자동차 공장의 출고 전 검사                                │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  자동차 공장에서는 출고 전에 반드시 검사한다:            │    │
│  │  ├── 엔진 시동이 걸리는가?                              │    │
│  │  ├── 브레이크가 제대로 작동하는가?                      │    │
│  │  ├── 에어백이 터지는가?                                 │    │
│  │  └── 조향장치가 정상인가?                               │    │
│  │                                                          │    │
│  │  수동으로 매번 확인? → 시간이 너무 오래 걸린다           │    │
│  │  자동 검사 라인?     → 빠르고 정확하게 반복 가능!        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  코드도 마찬가지:                                                │
│  ├── 코드 수정할 때마다 브라우저로 직접 확인? → 비효율          │
│  ├── 기능이 100개면 100번 클릭? → 현실적으로 불가능             │
│  └── 테스트 코드를 작성하면 → 버튼 하나로 전부 자동 검증!      │
│                                                                   │
│  테스트 코드 = 내 코드를 검사하는 또 다른 코드                   │
│  "이 함수에 3을 넣으면 6이 나와야 해" → 자동으로 확인            │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

1.2 테스트의 종류 (자동차 검사에 비유)

┌─────────────────────────────────────────────────────────────────┐
│              테스트의 종류 (비유로 이해하기)                     │
│                                                                   │
│  자동차 검사에 대응시키면:                                       │
│                                                                   │
│  1. 부품 검사 = Unit Test (단위 테스트)                          │
│     ├── 엔진만 따로 꺼내서 검사                                 │
│     ├── 다른 부품 없이 엔진 혼자 동작하는지 확인                │
│     └── 가장 빠르고, 가장 많이 함                               │
│                                                                   │
│  2. 조립 검사 = Integration Test (통합 테스트)                   │
│     ├── 엔진 + 변속기를 연결해서 검사                           │
│     ├── 부품끼리 연결했을 때 잘 동작하는지 확인                 │
│     └── 시간이 좀 더 걸림                                       │
│                                                                   │
│  3. 완성차 검사 = E2E Test (종단간 테스트)                       │
│     ├── 실제 도로에서 주행 테스트                                │
│     ├── 처음부터 끝까지 전체를 테스트                            │
│     └── 가장 느리고, 비용이 큼                                   │
│                                                                   │
│  4. ⭐ 특정 시스템 검사 = Slice Test (슬라이스 테스트)           │
│     ├── 브레이크 시스템만 전문적으로 검사                        │
│     ├── 브레이크 패드 + 유압 장치 + 센서를 연결해서 검사        │
│     ├── 엔진은 빼고, 타이어도 빼고, 브레이크만!                 │
│     └── = "한 조각(Slice)만 잘라서 테스트"                       │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

2. 테스트 피라미드 (Test Pyramid) - 테스트의 기본 원칙

2.1 테스트 피라미드란?

┌─────────────────────────────────────────────────────────────────┐
│               테스트 피라미드 (Test Pyramid)                     │
│                                                                   │
│  2009년 Mike Cohn이 "Succeeding with Agile" 책에서 제안         │
│  "피라미드 모양으로 테스트를 구성하라"                            │
│                                                                   │
│              /\                                                   │
│             /  \          ← E2E: 적게 (느리고, 비쌈)             │
│            / E2E\                                                 │
│           /──────\                                                │
│          / Slice  \       ← Slice: 계층별로 적절히               │
│         /──────────\                                              │
│        /Integration \     ← Integration: 중간                    │
│       /──────────────\                                            │
│      /  Unit  Tests   \   ← Unit: 많이 (빠르고, 쌈)             │
│     /──────────────────\                                          │
│                                                                   │
│  원칙:                                                           │
│  ├── 아래로 갈수록: 빠르고, 많이, 저렴하게                      │
│  ├── 위로 갈수록: 느리고, 적게, 비싸게                           │
│  └── 피라미드를 뒤집으면(E2E가 많으면) → 느리고 불안정           │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

2.2 각 단계 상세 설명

┌─────────────────────────────────────────────────────────────────┐
│              Unit Test (단위 테스트)                             │
│                                                                   │
│  정의: 함수나 메서드 "하나"를 고립시켜서 테스트                  │
│                                                                   │
│  특징:                                                           │
│  ├── 외부 의존성 없음 (DB 없음, 네트워크 없음)                  │
│  ├── 매우 빠름 (밀리초 단위, 1000분의 1초)                      │
│  ├── JUnit 5, Kotest 등의 프레임워크 사용                       │
│  └── Spring을 아예 띄우지 않음 → 순수 Java/Kotlin만 실행        │
│                                                                   │
│  비유: 전구 하나를 소켓에 끼워서 불 켜지는지만 확인              │
│        다른 전기 시스템과 연결하지 않고 전구만 테스트            │
│                                                                   │
│  예시:                                                           │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  // "가격 계산" 함수만 단독으로 테스트                    │    │
│  │  @Test                                                    │    │
│  │  fun `할인율 10%면 1000원이 900원이 된다`() {             │    │
│  │      val result = calculateDiscount(1000, 10)             │    │
│  │      assertThat(result).isEqualTo(900)                    │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│           Integration Test (통합 테스트)                         │
│                                                                   │
│  정의: 여러 컴포넌트(부품)를 연결해서 함께 테스트               │
│                                                                   │
│  컴포넌트(Component)란?                                          │
│  = 프로그램을 구성하는 독립적인 부품                             │
│  = Controller, Service, Repository 등 각각이 하나의 컴포넌트    │
│                                                                   │
│  특징:                                                           │
│  ├── 실제 DB, 실제 네트워크 사용 가능                           │
│  ├── 느림 (초~분 단위)                                          │
│  ├── @SpringBootTest 사용 → Spring 전체를 띄움                  │
│  └── 실제 환경과 가장 비슷하게 테스트                           │
│                                                                   │
│  비유: 조명 시스템 전체를 켜서                                   │
│        스위치 → 배선 → 전구 순서로 전류가 흐르는지 확인         │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│           E2E Test (End-to-End, 종단간 테스트)                  │
│                                                                   │
│  정의: 사용자 관점에서 전체 시스템을 처음부터 끝까지 테스트     │
│                                                                   │
│  End-to-End = "끝에서 끝까지"                                    │
│  = 사용자가 브라우저에서 버튼을 누르면                           │
│    → 서버에 요청이 가고                                          │
│    → DB에서 데이터를 가져오고                                    │
│    → 화면에 결과가 나타나는 것까지 전부 테스트                   │
│                                                                   │
│  특징:                                                           │
│  ├── 브라우저 자동화 도구 사용 (Selenium, Playwright)            │
│  ├── 매우 느리고, 깨지기 쉬움                                   │
│  ├── 실제 서버 + 실제 DB + 실제 브라우저 필요                   │
│  └── 핵심 시나리오만 최소한으로 작성                             │
│                                                                   │
│  비유: 실제로 집에 들어가서 전등 스위치를 눌러보는 것            │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│     ⭐ Slice Test (슬라이스 테스트) = 이 문서의 핵심!           │
│                                                                   │
│  정의: "애플리케이션의 특정 계층(Slice)만 잘라서 테스트"         │
│                                                                   │
│  Slice = 조각, 한 겹                                             │
│  애플리케이션을 케이크처럼 여러 층으로 보고,                     │
│  그 중 한 층만 잘라서(Slice) 테스트한다!                         │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │           애플리케이션의 계층 구조                        │    │
│  │                                                          │    │
│  │  ┌────────────────────────────────┐                      │    │
│  │  │   Controller (웹 계층)        │ ← @WebMvcTest        │    │
│  │  ├────────────────────────────────┤                      │    │
│  │  │   Service (비즈니스 로직)     │ ← Unit Test          │    │
│  │  ├────────────────────────────────┤                      │    │
│  │  │   Repository (DB 접근 계층)   │ ← @DataJpaTest       │    │
│  │  └────────────────────────────────┘                      │    │
│  │                                                          │    │
│  │  각 계층을 "슬라이스"해서 독립적으로 테스트!             │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  특징:                                                           │
│  ├── Unit과 Integration 사이의 "딱 맞는" 테스트                 │
│  ├── 필요한 Bean만 로드 → 빠르면서도 실제적                     │
│  ├── Spring Boot 1.4 (2016년)에서 처음 도입                     │
│  └── @WebMvcTest, @DataJpaTest 등의 어노테이션 사용             │
│                                                                   │
│  Bean이란?                                                       │
│  = Spring이 생성하고 관리하는 객체 (나중에 자세히 설명)          │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

3. Slice Test가 나오게 된 배경

3.1 핵심 용어부터 정리

┌─────────────────────────────────────────────────────────────────┐
│         이해를 위해 꼭 알아야 할 용어 정리                      │
│                                                                   │
│  1. Spring Framework                                             │
│     = Java/Kotlin으로 웹 서버를 만들 때 쓰는 도구 모음          │
│     = 비유: 요리할 때 쓰는 주방 도구 세트                        │
│                                                                   │
│  2. Spring Boot                                                  │
│     = Spring Framework를 더 쉽게 쓸 수 있게 만든 것             │
│     = 비유: 주방 도구 세트 + 자동 세팅 (오븐 예열 자동 등)      │
│                                                                   │
│  3. Bean (빈)                                                    │
│     = Spring이 생성하고 관리하는 객체(Object)                    │
│     = Controller, Service, Repository 등이 각각 Bean             │
│     = 비유: Spring이라는 공장에서 만들어 관리하는 부품들         │
│                                                                   │
│  4. ApplicationContext (애플리케이션 컨텍스트)                    │
│     = Spring이 관리하는 "Bean들의 컨테이너(보관함)"             │
│     = 모든 Bean이 여기에 등록되고, 필요할 때 꺼내 씀            │
│     = 비유: 공구함. 각종 공구(Bean)가 들어있고                   │
│             필요할 때 "드라이버 줘" 하면 꺼내줌                  │
│                                                                   │
│  5. Auto-Configuration (자동 설정)                               │
│     = Spring Boot가 자동으로 필요한 설정을 해주는 것            │
│     = 비유: 호텔 체크인하면 방에 이미 수건, 비누,                │
│             침대가 준비되어 있는 것                               │
│     = 개발자가 일일이 설정하지 않아도 자동으로 세팅!             │
│                                                                   │
│  6. 어노테이션 (Annotation, @로 시작하는 것)                     │
│     = 코드에 붙이는 "표시" 또는 "라벨"                           │
│     = @Controller → "이 클래스는 컨트롤러야" 라고 표시           │
│     = @Test → "이 메서드는 테스트야" 라고 표시                   │
│     = 비유: 택배 상자에 "취급주의" 스티커 붙이는 것              │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

3.2 @SpringBootTest의 등장과 한계

┌─────────────────────────────────────────────────────────────────┐
│           @SpringBootTest의 편리함과 한계                        │
│                                                                   │
│  Spring Boot 이전 (고통의 시대):                                 │
│  ├── XML 설정 파일을 수동으로 작성                               │
│  ├── Bean을 하나하나 수동 등록                                   │
│  ├── 테스트용 설정을 따로 관리                                   │
│  └── 매우 번거롭고 느림                                         │
│                                                                   │
│  Spring Boot가 @SpringBootTest를 도입:                           │
│  ├── 한 줄이면 전체 ApplicationContext 로드!                     │
│  ├── 설정 자동 → 매우 편리!                                     │
│  └── 하지만... 전체를 다 로드하니까 느림!                        │
│                                                                   │
│  비유로 이해하기:                                                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  상황: 엔진만 테스트하고 싶다                             │    │
│  │                                                          │    │
│  │  @SpringBootTest 방식:                                    │    │
│  │  "엔진 테스트하려면 자동차 전체를 조립해야 합니다"        │    │
│  │  → 에어백, 오디오, 네비, 시트 히터까지 전부 장착          │    │
│  │  → 그제서야 엔진 테스트 시작                              │    │
│  │  → 시간 낭비!                                             │    │
│  │                                                          │    │
│  │  실제 수치:                                               │    │
│  │  ├── Bean이 200개인 프로젝트                              │    │
│  │  ├── Controller 하나 테스트하는데 30초 이상               │    │
│  │  ├── 테스트가 50개면 → 30초 x 50 = 25분!                 │    │
│  │  └── 개발자: 커피 마시러 감...                            │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

3.3 Slice Test의 탄생 (Spring Boot 1.4, 2016)

┌─────────────────────────────────────────────────────────────────┐
│              Slice Test의 탄생 배경                              │
│                                                                   │
│  Spring Boot 팀 (Phil Webb, Andy Wilkinson 등)의 해결책:        │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  "전체 다 로드하지 말고, 필요한 계층만 로드하자!"        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  핵심 아이디어: Auto-Configuration을 계층별로 분리               │
│                                                                   │
│  @SpringBootTest 방식:                                           │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  ApplicationContext에 모든 Bean을 다 넣음                 │    │
│  │  [Controller][Service][Repository][Security][Cache]...    │    │
│  │  → 공구함에 모든 공구를 전부 넣음 → 무겁고 느림          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  @WebMvcTest 방식 (Slice Test):                                  │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  ApplicationContext에 웹 관련 Bean만 넣음                 │    │
│  │  [Controller][Filter][WebConfig]                          │    │
│  │  → 공구함에 웹 관련 공구만 넣음 → 가볍고 빠름!           │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  속도 비교:                                                      │
│  ├── @SpringBootTest → 5~30초 (전체 로드)                       │
│  ├── @WebMvcTest     → 1~3초 (웹 계층만)                        │
│  └── @DataJpaTest    → 2~5초 (DB 계층만)                        │
│                                                                   │
│  2016년 Spring Boot 1.4에서 공식 도입                            │
│  이후 테스트 작성의 표준이 됨                                    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

4. Spring Boot Slice Test 종류 - 하나하나 상세 설명

4.1 @WebMvcTest - 웹 계층(Controller) 테스트

┌─────────────────────────────────────────────────────────────────┐
│        @WebMvcTest - 웹 계층(Controller)만 테스트               │
│                                                                   │
│  이게 뭔가?                                                      │
│  = Controller만 테스트한다.                                      │
│  = Service, Repository는 아예 로드하지 않는다.                   │
│  = 실제 HTTP 서버를 띄우지 않고, 요청/응답을 시뮬레이션한다.    │
│                                                                   │
│  Controller란?                                                   │
│  = 사용자의 HTTP 요청을 받아서 처리하는 클래스                   │
│  = "GET /users/1 요청이 오면 1번 사용자 정보를 반환해라"         │
│  = 비유: 식당의 웨이터. 손님 주문을 받아서 주방에 전달           │
│                                                                   │
│  로드되는 것:                                                    │
│  ├── @Controller, @RestController (요청 처리기)                  │
│  ├── @ControllerAdvice (에러 처리기)                             │
│  ├── @JsonComponent (JSON 변환기)                                │
│  ├── Filter (요청 필터)                                          │
│  └── WebMvcConfigurer (웹 설정)                                  │
│                                                                   │
│  로드 안 되는 것:                                                │
│  ├── @Service (비즈니스 로직)                                    │
│  ├── @Repository (DB 접근)                                       │
│  └── @Component (기타 Bean)                                      │
│                                                                   │
│  왜 쓰나?                                                        │
│  ├── "GET /users 요청 시 200 OK를 반환하는가?"                   │
│  ├── "잘못된 요청이 오면 400 Bad Request를 반환하는가?"          │
│  ├── "인증 없이 접근하면 401을 반환하는가?"                      │
│  └── "응답 JSON 형식이 올바른가?"                                │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Kotlin 코드 예시

┌─────────────────────────────────────────────────────────────────┐
│           @WebMvcTest 코드 예시 (Kotlin)                        │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  @WebMvcTest(UserController::class)                       │    │
│  │  class UserControllerTest {                               │    │
│  │                                                          │    │
│  │      @Autowired                                           │    │
│  │      lateinit var mockMvc: MockMvc                        │    │
│  │                                                          │    │
│  │      @MockBean                                            │    │
│  │      lateinit var userService: UserService                │    │
│  │                                                          │    │
│  │      @Test                                                │    │
│  │      fun `사용자 조회 성공`() {                           │    │
│  │          // Given                                         │    │
│  │          given(userService.getUser(1L))                   │    │
│  │              .willReturn(                                 │    │
│  │                  UserDto(1L, "Kim", "kim@test.com")       │    │
│  │              )                                            │    │
│  │                                                          │    │
│  │          // When & Then                                   │    │
│  │          mockMvc.perform(get("/users/1"))                 │    │
│  │              .andExpect(status().isOk)                    │    │
│  │              .andExpect(                                  │    │
│  │                  jsonPath("$.name").value("Kim")          │    │
│  │              )                                            │    │
│  │      }                                                    │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  코드 한 줄 한 줄 설명:                                         │
│                                                                   │
│  @WebMvcTest(UserController::class)                              │
│  └── "UserController만 로드해라"                                 │
│      → Service, Repository는 로드하지 않음                       │
│                                                                   │
│  @Autowired lateinit var mockMvc: MockMvc                        │
│  ├── @Autowired = "Spring이 자동으로 주입해줘"                   │
│  ├── MockMvc = 실제 HTTP 서버 없이 요청을 시뮬레이션하는 도구   │
│  └── 비유: 진짜 손님 없이 주문서를 직접 작성해서 테스트         │
│                                                                   │
│  @MockBean lateinit var userService: UserService                 │
│  └── "진짜 UserService 대신 가짜(Mock)를 넣어라"                │
│      → 이유: 바로 아래에서 상세 설명                             │
│                                                                   │
│  given(userService.getUser(1L)).willReturn(...)                  │
│  └── "getUser(1)이 호출되면 이 값을 반환하라"고 설정             │
│      → 가짜 Service에게 "이렇게 행동해" 라고 지시                │
│                                                                   │
│  mockMvc.perform(get("/users/1"))                                │
│  └── "GET /users/1 요청을 보내라" (시뮬레이션)                   │
│                                                                   │
│  .andExpect(status().isOk)                                       │
│  └── "응답 상태가 200 OK인지 확인해라"                           │
│                                                                   │
│  .andExpect(jsonPath("$.name").value("Kim"))                     │
│  └── "응답 JSON의 name 필드가 Kim인지 확인해라"                  │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

@MockBean 상세 설명

┌─────────────────────────────────────────────────────────────────┐
│             @MockBean 상세 설명 (가짜 객체)                     │
│                                                                   │
│  Mock = 가짜, 모형                                               │
│  @MockBean = "진짜 Bean 대신 가짜 Bean을 Spring에 등록해라"      │
│                                                                   │
│  왜 가짜를 쓰나?                                                 │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  Controller를 테스트하는데 진짜 Service를 쓰면...        │    │
│  │                                                          │    │
│  │  Controller → Service(진짜) → Repository(진짜) → DB      │    │
│  │                                                          │    │
│  │  → Service가 DB를 호출함                                 │    │
│  │  → DB도 준비해야 함                                      │    │
│  │  → 점점 무거워짐                                         │    │
│  │  → "컨트롤러만 테스트하고 싶은데...!"                    │    │
│  │                                                          │    │
│  │  가짜 Service를 쓰면:                                    │    │
│  │  Controller → Service(가짜) → (끝! DB 불필요!)           │    │
│  │                                                          │    │
│  │  "getUser(1) 호출되면 이 값을 줘" 하고 미리 설정         │    │
│  │  → DB 없이 빠르게 테스트 가능!                           │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  비유:                                                           │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  영화 촬영에서:                                          │    │
│  │  ├── 진짜 폭발 → 위험하고 비쌈                          │    │
│  │  ├── CG(컴퓨터 그래픽) → 안전하고 저렴                  │    │
│  │  └── CG = Mock (가짜지만 목적에 충분)                    │    │
│  │                                                          │    │
│  │  테스트에서:                                              │    │
│  │  ├── 진짜 Service → DB도 필요, 느림                      │    │
│  │  ├── Mock Service → DB 불필요, 빠름                      │    │
│  │  └── Mock = CG (가짜지만 Controller 테스트에 충분)       │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  @MockBean vs @Mock 차이:                                        │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  @Mock (Mockito 라이브러리):                              │    │
│  │  ├── 순수 가짜 객체 생성                                 │    │
│  │  ├── Spring과 무관                                       │    │
│  │  └── Unit Test에서 주로 사용                              │    │
│  │                                                          │    │
│  │  @MockBean (Spring Boot):                                 │    │
│  │  ├── 가짜 객체를 생성 + Spring ApplicationContext에 등록  │    │
│  │  ├── 진짜 Bean을 대체함                                  │    │
│  │  └── Slice Test에서 주로 사용                             │    │
│  │                                                          │    │
│  │  핵심 차이: @MockBean은 Spring이 관리하는 가짜            │    │
│  │            @Mock은 개발자가 직접 관리하는 가짜            │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

@WebMvcTest 실전: 에러 처리 테스트

┌─────────────────────────────────────────────────────────────────┐
│          @WebMvcTest로 에러 상황 테스트하기                      │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  @Test                                                    │    │
│  │  fun `존재하지 않는 사용자 조회 시 404`() {              │    │
│  │      // Given: 예외를 던지도록 설정                       │    │
│  │      given(userService.getUser(999L))                    │    │
│  │          .willThrow(UserNotFoundException(999L))          │    │
│  │                                                          │    │
│  │      // When & Then: 404 Not Found 확인                  │    │
│  │      mockMvc.perform(get("/users/999"))                   │    │
│  │          .andExpect(status().isNotFound)                  │    │
│  │          .andExpect(                                      │    │
│  │              jsonPath("$.message")                        │    │
│  │                  .value("User 999 not found")            │    │
│  │          )                                                │    │
│  │  }                                                        │    │
│  │                                                          │    │
│  │  @Test                                                    │    │
│  │  fun `잘못된 요청 시 400`() {                            │    │
│  │      // When & Then: 이메일 없이 요청 → 400              │    │
│  │      mockMvc.perform(                                     │    │
│  │          post("/users")                                   │    │
│  │              .contentType(MediaType.APPLICATION_JSON)     │    │
│  │              .content("""{"name": ""}""")                 │    │
│  │      )                                                    │    │
│  │          .andExpect(status().isBadRequest)                │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  ❌ 이런 것은 @WebMvcTest로 테스트하면 안 됨:                    │
│  ├── DB에 데이터가 실제로 저장되는지                             │
│  ├── Service 로직이 올바른지                                     │
│  └── 여러 API를 연속으로 호출하는 시나리오                       │
│                                                                   │
│  ✅ 이런 것이 @WebMvcTest에 적합:                                │
│  ├── 요청 URL/메서드가 올바르게 매핑되는지                       │
│  ├── 요청 검증(Validation)이 작동하는지                          │
│  ├── 응답 JSON 형식이 올바른지                                   │
│  └── 에러 시 적절한 HTTP 상태 코드를 반환하는지                  │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

4.2 @DataJpaTest - JPA(데이터베이스) 계층 테스트

┌─────────────────────────────────────────────────────────────────┐
│       @DataJpaTest - JPA(데이터베이스) 계층만 테스트            │
│                                                                   │
│  먼저 용어부터:                                                  │
│                                                                   │
│  JPA (Java Persistence API)란?                                   │
│  = Java/Kotlin에서 DB에 데이터를 저장/조회하는 표준 방법         │
│  = SQL을 직접 쓰지 않고, 객체를 DB에 저장할 수 있게 해줌        │
│  = 비유: 통역사. 한국어(Java 객체) ↔ 영어(SQL) 번역             │
│                                                                   │
│  Repository란?                                                   │
│  = DB에 접근하는 코드를 모아놓은 클래스                          │
│  = "사용자 저장", "사용자 검색", "사용자 삭제" 등                │
│  = 비유: 도서관 사서. "이 책 찾아줘", "이 책 넣어줘"            │
│                                                                   │
│  @Entity란?                                                      │
│  = "이 클래스는 DB 테이블에 대응하는 클래스야" 라는 표시         │
│  = User 클래스에 @Entity → users 테이블과 연결                   │
│                                                                   │
│  @DataJpaTest가 하는 일:                                         │
│  ├── @Entity 클래스들을 로드                                     │
│  ├── @Repository 클래스들을 로드                                 │
│  ├── JPA 관련 설정만 로드                                        │
│  ├── 내장 DB(H2)를 자동으로 사용 (설치 불필요!)                  │
│  ├── 각 테스트 후 자동으로 롤백 (데이터 원상복구)                │
│  └── @Controller, @Service는 로드하지 않음                       │
│                                                                   │
│  내장 DB(H2)란?                                                  │
│  = 별도 설치 없이 메모리에서 실행되는 가벼운 DB                  │
│  = 테스트 끝나면 자동으로 사라짐                                 │
│  = 비유: 연습장. 아무거나 써도 되고 다 쓰면 버리면 됨            │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Kotlin 코드 예시

┌─────────────────────────────────────────────────────────────────┐
│          @DataJpaTest 코드 예시 (Kotlin)                        │
│                                                                   │
│  먼저 Entity 클래스:                                             │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  @Entity                                                  │    │
│  │  @Table(name = "users")                                   │    │
│  │  class User(                                              │    │
│  │      @Id @GeneratedValue(strategy = IDENTITY)             │    │
│  │      val id: Long = 0,                                    │    │
│  │      val name: String,                                    │    │
│  │      val email: String                                    │    │
│  │  )                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  Repository 인터페이스:                                          │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  interface UserRepository : JpaRepository<User, Long> {  │    │
│  │      fun findByName(name: String): User?                  │    │
│  │      fun findByEmailContaining(keyword: String):          │    │
│  │          List<User>                                       │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  테스트 코드:                                                    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  @DataJpaTest                                             │    │
│  │  class UserRepositoryTest {                               │    │
│  │                                                          │    │
│  │      @Autowired                                           │    │
│  │      lateinit var userRepository: UserRepository          │    │
│  │                                                          │    │
│  │      @Autowired                                           │    │
│  │      lateinit var entityManager: TestEntityManager        │    │
│  │                                                          │    │
│  │      @Test                                                │    │
│  │      fun `이름으로 사용자 검색`() {                       │    │
│  │          // Given: 테스트 데이터 저장                     │    │
│  │          entityManager.persistAndFlush(                   │    │
│  │              User(name = "Kim", email = "kim@test.com")   │    │
│  │          )                                                │    │
│  │                                                          │    │
│  │          // When: 이름으로 검색                           │    │
│  │          val found = userRepository.findByName("Kim")     │    │
│  │                                                          │    │
│  │          // Then: 결과 확인                               │    │
│  │          assertThat(found).isNotNull                      │    │
│  │          assertThat(found!!.email)                        │    │
│  │              .isEqualTo("kim@test.com")                   │    │
│  │      }                                                    │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  코드 설명:                                                      │
│                                                                   │
│  @DataJpaTest                                                    │
│  └── "JPA 관련 Bean만 로드해라. 내장 DB를 사용해라."             │
│                                                                   │
│  TestEntityManager                                               │
│  └── 테스트 전용 DB 접근 도구. 데이터 직접 저장/조회 가능        │
│                                                                   │
│  persistAndFlush()                                               │
│  ├── persist = DB에 저장                                         │
│  └── flush = 저장 명령을 즉시 실행 (나중에 말고 지금 당장!)      │
│                                                                   │
│  각 테스트 후 자동 롤백:                                         │
│  ├── @DataJpaTest는 @Transactional이 자동 적용                   │
│  ├── 테스트에서 저장한 데이터는 테스트 끝나면 사라짐             │
│  └── 다음 테스트에 영향 없음 = 테스트 격리                       │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

@AutoConfigureTestDatabase 설명

┌─────────────────────────────────────────────────────────────────┐
│          @AutoConfigureTestDatabase 설명                        │
│                                                                   │
│  @DataJpaTest는 기본적으로 내장 DB(H2)를 사용한다.              │
│  하지만 실제 DB(PostgreSQL, MySQL)로 테스트하고 싶다면?         │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  // 내장 DB 대신 실제 DB 사용하기                         │    │
│  │  @DataJpaTest                                             │    │
│  │  @AutoConfigureTestDatabase(replace = Replace.NONE)       │    │
│  │  class UserRepositoryRealDbTest {                         │    │
│  │      // application.yml에 설정된 실제 DB를 사용           │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  Replace.NONE = "내장 DB로 교체하지 마. 원래 설정 그대로 써"     │
│                                                                   │
│  ❌ 내장 DB의 한계:                                              │
│  ├── H2는 PostgreSQL/MySQL과 SQL 문법이 다를 수 있음            │
│  ├── 특정 DB 전용 기능 (JSON 타입, 배열 등) 테스트 불가         │
│  └── "H2에서는 통과, 실제 DB에서는 실패" 문제 발생 가능         │
│                                                                   │
│  ✅ 이 문제를 해결하는 것이 → TestContainers (6장에서 설명)      │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

4.3 @WebFluxTest - WebFlux(리액티브 웹) 계층 테스트

┌─────────────────────────────────────────────────────────────────┐
│       @WebFluxTest - 리액티브 웹 컨트롤러 테스트                │
│                                                                   │
│  WebFlux란?                                                      │
│  = Spring의 비동기/논블로킹 웹 프레임워크                        │
│  = 기존 Spring MVC가 "한 요청당 한 스레드"라면                   │
│    WebFlux는 "적은 스레드로 많은 요청 처리"                      │
│  = 비유: MVC = 카운터 직원이 한 명씩 처리                        │
│          WebFlux = 키오스크가 여러 명 동시 처리                   │
│                                                                   │
│  @WebFluxTest:                                                   │
│  ├── WebFlux Controller만 로드                                   │
│  ├── WebTestClient를 사용 (MockMvc 대신)                         │
│  └── 나머지는 @WebMvcTest와 같은 원리                            │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  @WebFluxTest(UserController::class)                      │    │
│  │  class UserControllerTest {                               │    │
│  │                                                          │    │
│  │      @Autowired                                           │    │
│  │      lateinit var webTestClient: WebTestClient            │    │
│  │                                                          │    │
│  │      @MockBean                                            │    │
│  │      lateinit var userService: UserService                │    │
│  │                                                          │    │
│  │      @Test                                                │    │
│  │      fun `사용자 조회`() {                                │    │
│  │          given(userService.getUser(1L))                   │    │
│  │              .willReturn(Mono.just(UserDto(1L, "Kim")))   │    │
│  │                                                          │    │
│  │          webTestClient.get()                              │    │
│  │              .uri("/users/1")                             │    │
│  │              .exchange()                                  │    │
│  │              .expectStatus().isOk                         │    │
│  │              .expectBody()                                │    │
│  │              .jsonPath("$.name").isEqualTo("Kim")         │    │
│  │      }                                                    │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

4.4 @JsonTest - JSON 직렬화/역직렬화 테스트

┌─────────────────────────────────────────────────────────────────┐
│         @JsonTest - JSON 변환 테스트                             │
│                                                                   │
│  직렬화(Serialization)란?                                        │
│  = 객체 → JSON 문자열로 변환                                     │
│  = User(name="Kim") → {"name": "Kim"}                            │
│  = 비유: 택배 보내기. 물건을 박스에 포장하는 것                   │
│                                                                   │
│  역직렬화(Deserialization)란?                                    │
│  = JSON 문자열 → 객체로 변환                                     │
│  = {"name": "Kim"} → User(name="Kim")                            │
│  = 비유: 택배 받기. 박스를 뜯어서 물건을 꺼내는 것               │
│                                                                   │
│  왜 이것을 테스트하나?                                           │
│  ├── 날짜 형식이 "2026-02-26"으로 나오는지                      │
│  ├── null인 필드가 JSON에서 빠지는지                             │
│  ├── 금액이 1000이 아니라 "1,000원"으로 나오는지                │
│  └── 커스텀 변환 로직이 올바른지                                 │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  @JsonTest                                                │    │
│  │  class UserDtoJsonTest {                                  │    │
│  │                                                          │    │
│  │      @Autowired                                           │    │
│  │      lateinit var json: JacksonTester<UserDto>            │    │
│  │                                                          │    │
│  │      @Test                                                │    │
│  │      fun `직렬화 테스트`() {                              │    │
│  │          val user = UserDto(1L, "Kim", "kim@test.com")    │    │
│  │          val result = json.write(user)                    │    │
│  │                                                          │    │
│  │          assertThat(result)                               │    │
│  │              .extractingJsonPathStringValue("$.name")     │    │
│  │              .isEqualTo("Kim")                            │    │
│  │      }                                                    │    │
│  │                                                          │    │
│  │      @Test                                                │    │
│  │      fun `역직렬화 테스트`() {                            │    │
│  │          val content = """                                │    │
│  │              {"id":1,"name":"Kim","email":"kim@test.com"} │    │
│  │          """.trimIndent()                                 │    │
│  │                                                          │    │
│  │          val result = json.parse(content)                 │    │
│  │          assertThat(result.object.name)                   │    │
│  │              .isEqualTo("Kim")                            │    │
│  │      }                                                    │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  JacksonTester = JSON 변환 테스트 전용 도구                      │
│  Jackson = Java/Kotlin에서 가장 많이 쓰는 JSON 라이브러리        │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

4.5 @RestClientTest - REST 클라이언트 테스트

┌─────────────────────────────────────────────────────────────────┐
│       @RestClientTest - 외부 API 호출 테스트                    │
│                                                                   │
│  REST 클라이언트란?                                              │
│  = 우리 서버에서 다른 서버의 API를 호출하는 코드                 │
│  = 예: 우리 서버 → 카카오 API로 결제 요청                        │
│  = 비유: 다른 가게에 전화해서 배달 주문하는 것                   │
│                                                                   │
│  문제: 테스트할 때 진짜 카카오 서버를 호출할 수 없음             │
│  ├── 돈이 진짜 빠져나감                                         │
│  ├── 카카오 서버가 느릴 수 있음                                  │
│  └── 네트워크 끊기면 테스트 실패                                 │
│                                                                   │
│  해결: MockRestServiceServer로 외부 API를 가짜로 만듦            │
│  = "카카오 서버인 척하는 가짜 서버"                               │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  @RestClientTest(PaymentClient::class)                    │    │
│  │  class PaymentClientTest {                                │    │
│  │                                                          │    │
│  │      @Autowired                                           │    │
│  │      lateinit var client: PaymentClient                   │    │
│  │                                                          │    │
│  │      @Autowired                                           │    │
│  │      lateinit var server: MockRestServiceServer           │    │
│  │                                                          │    │
│  │      @Test                                                │    │
│  │      fun `결제 요청 성공`() {                             │    │
│  │          // 가짜 서버가 이 응답을 반환하도록 설정         │    │
│  │          server.expect(requestTo("/pay"))                 │    │
│  │              .andRespond(withSuccess(                     │    │
│  │                  """{"status":"OK"}""",                   │    │
│  │                  MediaType.APPLICATION_JSON               │    │
│  │              ))                                           │    │
│  │                                                          │    │
│  │          val result = client.pay(1000)                    │    │
│  │          assertThat(result.status).isEqualTo("OK")       │    │
│  │      }                                                    │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

4.6 기타 Slice Test 어노테이션

┌─────────────────────────────────────────────────────────────────┐
│             기타 Slice Test 어노테이션들                         │
│                                                                   │
│  @DataMongoTest                                                  │
│  ├── MongoDB(문서형 DB) 관련 Bean만 로드                         │
│  ├── 내장 MongoDB 사용 가능                                      │
│  └── MongoDB Repository 테스트에 사용                            │
│                                                                   │
│  @DataRedisTest                                                  │
│  ├── Redis(인메모리 캐시 DB) 관련 Bean만 로드                    │
│  └── Redis 연동 코드 테스트에 사용                               │
│                                                                   │
│  @DataR2dbcTest                                                  │
│  ├── R2DBC(리액티브 DB 접근) 관련 Bean만 로드                    │
│  └── 비동기 DB 접근 테스트에 사용                                │
│                                                                   │
│  @JdbcTest                                                       │
│  ├── JDBC(저수준 DB 접근) 관련 Bean만 로드                       │
│  └── JdbcTemplate 사용 코드 테스트에 사용                        │
│                                                                   │
│  @JooqTest                                                       │
│  ├── jOOQ(SQL 빌더 라이브러리) 관련 Bean만 로드                  │
│  └── jOOQ 기반 쿼리 테스트에 사용                                │
│                                                                   │
│  공통점: 모두 "특정 기술 계층"만 잘라서 테스트한다               │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

5. @SpringBootTest vs Slice Test - 언제 무엇을 쓰나?

5.1 상세 비교

┌─────────────────────────────────────────────────────────────────┐
│         @SpringBootTest vs Slice Test 비교                      │
│                                                                   │
│  ┌───────────┬──────────────────┬─────────────────────┐         │
│  │   항목    │ @SpringBootTest  │    Slice Test        │         │
│  ├───────────┼──────────────────┼─────────────────────┤         │
│  │ 로드 범위 │ 전체 Bean        │ 특정 계층 Bean만     │         │
│  │ 속도     │ 느림 (5~30초)    │ 빠름 (1~5초)         │         │
│  │ 사용시점 │ 전체 통합 검증    │ 계층별 기능 검증     │         │
│  │ 설정     │ 간단 (다 로드)   │ Mock 설정 필요       │         │
│  │ Bean 수  │ 전체 (수백 개)   │ 최소 필요분 (수십)   │         │
│  │ DB       │ 필요할 수 있음    │ 내장 DB / 불필요     │         │
│  │ 현실성   │ 높음 (실제와 유사)│ 중간 (계층만 검증)   │         │
│  └───────────┴──────────────────┴─────────────────────┘         │
│                                                                   │
│  비유:                                                           │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  @SpringBootTest = 종합 건강검진                          │    │
│  │  ├── 전신을 다 검사                                      │    │
│  │  ├── 시간 오래 걸림 (반나절)                             │    │
│  │  └── 문제를 빠짐없이 발견                                │    │
│  │                                                          │    │
│  │  Slice Test = 전문과 진료                                 │    │
│  │  ├── 눈만 검사, 치아만 검사                              │    │
│  │  ├── 빠름 (30분)                                         │    │
│  │  └── 해당 부위 문제를 정밀하게 발견                      │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

5.2 실전 선택 가이드

┌─────────────────────────────────────────────────────────────────┐
│              실전 선택 가이드                                    │
│                                                                   │
│  "어떤 테스트를 써야 하지?" 결정 플로우:                         │
│                                                                   │
│  ┌──────────────────────────────────────────┐                    │
│  │ 무엇을 테스트하고 싶은가?               │                    │
│  └──────────────┬───────────────────────────┘                    │
│                 │                                                 │
│    ┌────────────┼────────────┬──────────────┐                    │
│    ▼            ▼            ▼              ▼                    │
│  Controller  Repository  JSON 변환    전체 흐름                  │
│    │            │            │              │                    │
│    ▼            ▼            ▼              ▼                    │
│  @WebMvcTest @DataJpaTest @JsonTest  @SpringBootTest             │
│                                                                   │
│  구체적인 상황별 가이드:                                         │
│                                                                   │
│  ✅ @WebMvcTest 쓸 때:                                           │
│  ├── Controller의 URL 매핑이 올바른지                            │
│  ├── 요청 파라미터 검증이 작동하는지                             │
│  ├── 응답 HTTP 상태 코드가 올바른지                              │
│  └── 응답 JSON 구조가 올바른지                                   │
│                                                                   │
│  ✅ @DataJpaTest 쓸 때:                                          │
│  ├── 커스텀 쿼리가 올바른 결과를 반환하는지                     │
│  ├── 엔티티 간 관계(1:N, M:N)가 올바른지                       │
│  ├── DB 제약조건(UNIQUE, NOT NULL)이 작동하는지                  │
│  └── 페이징/정렬이 올바른지                                      │
│                                                                   │
│  ✅ @JsonTest 쓸 때:                                             │
│  ├── 날짜 형식이 올바른지                                        │
│  ├── 커스텀 직렬화/역직렬화가 올바른지                          │
│  └── null 처리가 올바른지                                        │
│                                                                   │
│  ✅ @SpringBootTest 쓸 때:                                       │
│  ├── Controller → Service → Repository 전체 흐름                │
│  ├── 트랜잭션이 여러 서비스에 걸쳐 올바른지                     │
│  ├── 보안 설정이 전체적으로 올바른지                             │
│  └── 여러 API를 연속 호출하는 시나리오                           │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

6. 실전에서 필수인 관련 개념들

6.1 TestContainers - 진짜 DB로 테스트하기

┌─────────────────────────────────────────────────────────────────┐
│         TestContainers - Docker로 진짜 DB 띄워서 테스트         │
│                                                                   │
│  문제 상황:                                                      │
│  ├── @DataJpaTest는 기본으로 내장 DB(H2)를 사용                 │
│  ├── 그런데 H2와 실제 DB(PostgreSQL, MySQL)는 다름!             │
│  ├── H2에서는 통과하는데 실제 DB에서는 실패하는 경우 발생       │
│  └── 특히: JSON 타입, 배열, 윈도우 함수 등                      │
│                                                                   │
│  TestContainers란?                                               │
│  = Docker 컨테이너로 진짜 DB를 띄워서 테스트하는 도구           │
│                                                                   │
│  Docker란? (간단히)                                              │
│  = 프로그램을 "컨테이너"라는 격리된 환경에서 실행하는 기술      │
│  = 비유: 일회용 가상 컴퓨터. 필요할 때 만들고, 쓰고, 버림       │
│                                                                   │
│  TestContainers의 동작:                                          │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  1. 테스트 시작 → Docker로 PostgreSQL 컨테이너 생성      │    │
│  │  2. 진짜 PostgreSQL에서 테스트 실행                       │    │
│  │  3. 테스트 끝 → 컨테이너 자동 삭제                        │    │
│  │                                                          │    │
│  │  → 실제 환경과 100% 동일한 DB로 테스트!                   │    │
│  │  → H2와의 차이로 인한 버그를 사전에 발견!                 │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  Kotlin 예시:                                                    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  @DataJpaTest                                             │    │
│  │  @AutoConfigureTestDatabase(replace = Replace.NONE)       │    │
│  │  @Testcontainers                                          │    │
│  │  class UserRepositoryTest {                               │    │
│  │                                                          │    │
│  │      companion object {                                   │    │
│  │          @Container                                       │    │
│  │          val postgres = PostgreSQLContainer(              │    │
│  │              "postgres:15-alpine"                         │    │
│  │          )                                                │    │
│  │                                                          │    │
│  │          @JvmStatic                                       │    │
│  │          @DynamicPropertySource                           │    │
│  │          fun properties(registry:                         │    │
│  │                  DynamicPropertyRegistry) {               │    │
│  │              registry.add("spring.datasource.url",       │    │
│  │                  postgres::getJdbcUrl)                    │    │
│  │              registry.add("spring.datasource.username",  │    │
│  │                  postgres::getUsername)                    │    │
│  │              registry.add("spring.datasource.password",  │    │
│  │                  postgres::getPassword)                    │    │
│  │          }                                                │    │
│  │      }                                                    │    │
│  │                                                          │    │
│  │      @Autowired                                           │    │
│  │      lateinit var userRepository: UserRepository          │    │
│  │                                                          │    │
│  │      @Test                                                │    │
│  │      fun `PostgreSQL에서 사용자 검색`() {                │    │
│  │          userRepository.save(                             │    │
│  │              User(name = "Kim", email = "kim@test.com")  │    │
│  │          )                                                │    │
│  │          val found = userRepository.findByName("Kim")    │    │
│  │          assertThat(found).isNotNull                     │    │
│  │      }                                                    │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  코드 설명:                                                      │
│  ├── @Testcontainers: TestContainers 활성화                     │
│  ├── @Container: Docker 컨테이너 정의                            │
│  ├── PostgreSQLContainer: 진짜 PostgreSQL을 Docker로 실행        │
│  ├── @DynamicPropertySource: DB 접속 정보를 동적으로 설정        │
│  └── Replace.NONE: 내장 DB 대신 진짜 DB 사용                    │
│                                                                   │
│  장점:                                                           │
│  ├── 실제 DB와 동일한 환경에서 테스트                            │
│  ├── CI/CD에서도 동일하게 실행 가능                              │
│  └── "내 PC에서는 되는데..." 문제 방지                           │
│                                                                   │
│  단점:                                                           │
│  ├── Docker 설치 필요                                            │
│  ├── 내장 DB보다 느림 (컨테이너 시작 시간)                      │
│  └── CI 환경에 Docker가 있어야 함                                │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

6.2 @MockBean vs @SpyBean

┌─────────────────────────────────────────────────────────────────┐
│            @MockBean vs @SpyBean 차이                           │
│                                                                   │
│  @MockBean = 완전한 가짜 (전부 연기하는 대역 배우)              │
│  ├── 모든 메서드가 null 또는 기본값을 반환                      │
│  ├── "이 메서드 호출되면 이거 반환해" 라고 설정 필수            │
│  └── 설정하지 않은 메서드 호출 시 → null/0/false 반환           │
│                                                                   │
│  @SpyBean = 진짜를 감싼 가짜 (본인이 연기하되 일부만 대역)     │
│  ├── 기본적으로 진짜 로직이 실행됨                              │
│  ├── 특정 메서드만 "이 메서드는 이거 반환해" 라고 덮어쓸 수 있음│
│  └── 나머지는 원래 코드 그대로 실행                             │
│                                                                   │
│  비유:                                                           │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  영화 촬영에 비유:                                        │    │
│  │                                                          │    │
│  │  @MockBean = 대역 배우 (Stunt Double)                    │    │
│  │  ├── 모든 장면을 대역이 연기                             │    │
│  │  ├── 감독: "이 장면에서 이렇게 연기해"                   │    │
│  │  └── 지시하지 않은 장면 → 가만히 서 있음                 │    │
│  │                                                          │    │
│  │  @SpyBean = 본인 + 일부 장면만 대역                      │    │
│  │  ├── 대부분 본인이 직접 연기                             │    │
│  │  ├── 위험한 액션 장면만 대역                             │    │
│  │  └── 나머지는 본인의 진짜 연기                           │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  코드 예시:                                                      │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  // @MockBean: 완전한 가짜                                │    │
│  │  @MockBean                                                │    │
│  │  lateinit var userService: UserService                    │    │
│  │  // userService.getUser(1) → null (설정 안 하면)          │    │
│  │  // given(userService.getUser(1)).willReturn(...) 필수!   │    │
│  │                                                          │    │
│  │  // @SpyBean: 진짜를 감싼 가짜                            │    │
│  │  @SpyBean                                                 │    │
│  │  lateinit var userService: UserService                    │    │
│  │  // userService.getUser(1) → 진짜 로직 실행!             │    │
│  │  // 특정 메서드만 덮어쓰기 가능:                          │    │
│  │  // doReturn(...).whenever(userService).sendEmail(any())  │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  언제 무엇을 쓰나?                                               │
│  ├── @MockBean: 의존성을 완전히 제거하고 싶을 때                │
│  └── @SpyBean: 대부분 진짜로 돌리되 일부만 가짜로 할 때        │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

6.3 @Sql, @DirtiesContext

┌─────────────────────────────────────────────────────────────────┐
│            @Sql, @DirtiesContext 설명                            │
│                                                                   │
│  @Sql:                                                           │
│  = "테스트 전에 이 SQL 파일을 실행해라"                          │
│  = 테스트에 필요한 데이터를 미리 준비                            │
│  = 비유: 시험 전에 교실 책상 배치하는 것                         │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  @Test                                                    │    │
│  │  @Sql("/test-data/users.sql")  // 이 SQL을 먼저 실행     │    │
│  │  fun `사용자 목록 조회`() {                               │    │
│  │      val users = userRepository.findAll()                │    │
│  │      assertThat(users).hasSize(3)                        │    │
│  │  }                                                        │    │
│  │                                                          │    │
│  │  -- test-data/users.sql                                   │    │
│  │  INSERT INTO users (name, email)                         │    │
│  │  VALUES ('Kim', 'kim@test.com'),                         │    │
│  │         ('Lee', 'lee@test.com'),                         │    │
│  │         ('Park', 'park@test.com');                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  @DirtiesContext:                                                │
│  = "이 테스트가 끝나면 ApplicationContext를 버리고 새로 만들어"  │
│  = 테스트가 Spring 설정을 변경했을 때 사용                      │
│  = 다음 테스트에 영향을 주지 않기 위함                          │
│  = 비유: "내가 교실을 엉망으로 만들었으니, 다음 시간 전에       │
│          교실을 완전히 새로 세팅해줘"                            │
│                                                                   │
│  ⚠️ 주의: @DirtiesContext는 느림!                                │
│  ├── ApplicationContext를 재생성하므로 시간이 오래 걸림          │
│  └── 꼭 필요한 경우에만 사용                                     │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

6.4 테스트 격리 (Test Isolation)

┌─────────────────────────────────────────────────────────────────┐
│            테스트 격리 (Test Isolation)                          │
│                                                                   │
│  격리(Isolation)란?                                              │
│  = 각 테스트가 서로 영향을 주지 않는 것                          │
│  = 테스트 A의 결과가 테스트 B에 영향을 주면 안 됨               │
│                                                                   │
│  ❌ 격리가 안 된 경우:                                           │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  테스트 A: DB에 User("Kim") 저장                         │    │
│  │  테스트 B: DB에서 findAll() → 기대: 0개                  │    │
│  │                                                          │    │
│  │  결과: 테스트 B에서 1개 나옴! (A가 저장한 Kim)           │    │
│  │  → 테스트 순서에 따라 결과가 달라짐 = 불안정!            │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  ✅ 격리가 된 경우:                                              │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  테스트 A: DB에 User("Kim") 저장 → 테스트 끝 → 롤백!    │    │
│  │  테스트 B: DB에서 findAll() → 기대: 0개                  │    │
│  │                                                          │    │
│  │  결과: 테스트 B에서 0개 나옴! (A의 데이터가 사라짐)      │    │
│  │  → 테스트 순서와 무관하게 항상 같은 결과 = 안정적!       │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  Spring Boot에서 격리하는 방법:                                  │
│  ├── @DataJpaTest → 자동 롤백 (@Transactional 적용)             │
│  ├── @Sql → 테스트 전 데이터 초기화                              │
│  ├── @DirtiesContext → ApplicationContext 재생성                  │
│  └── @BeforeEach에서 직접 데이터 정리                            │
│                                                                   │
│  비유: 수학 시험                                                 │
│  ├── 격리 O: 매 문제마다 새 계산 용지 제공                      │
│  └── 격리 X: 이전 학생이 쓴 낙서가 남아있는 종이                │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

6.5 BDD 스타일 테스트

┌─────────────────────────────────────────────────────────────────┐
│          BDD 스타일 테스트 (Given-When-Then)                    │
│                                                                   │
│  BDD = Behavior-Driven Development (행위 주도 개발)              │
│  = "이 코드가 어떻게 행동하는지"를 중심으로 테스트               │
│                                                                   │
│  Given-When-Then 패턴:                                           │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  Given (주어진 상황):                                     │    │
│  │  = 테스트의 사전 조건. "이런 상황이 주어졌을 때"          │    │
│  │  = 비유: "고객이 장바구니에 상품 3개를 담았을 때"         │    │
│  │                                                          │    │
│  │  When (행동):                                             │    │
│  │  = 테스트할 행위. "이 행동을 하면"                        │    │
│  │  = 비유: "결제 버튼을 누르면"                             │    │
│  │                                                          │    │
│  │  Then (결과):                                             │    │
│  │  = 기대하는 결과. "이런 결과가 나와야 한다"               │    │
│  │  = 비유: "주문이 생성되고, 재고가 3개 줄어야 한다"        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  BDDMockito 사용 예시:                                           │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  import org.mockito.BDDMockito.given                      │    │
│  │  import org.mockito.BDDMockito.then                       │    │
│  │                                                          │    │
│  │  @Test                                                    │    │
│  │  fun `사용자 생성 시 이메일 발송`() {                    │    │
│  │      // Given: 이런 상황이 주어졌을 때                    │    │
│  │      val user = UserDto("Kim", "kim@test.com")           │    │
│  │      given(userRepository.save(any()))                    │    │
│  │          .willReturn(User(1L, "Kim", "kim@test.com"))    │    │
│  │                                                          │    │
│  │      // When: 이 행동을 하면                              │    │
│  │      userService.createUser(user)                         │    │
│  │                                                          │    │
│  │      // Then: 이런 결과가 나와야 한다                     │    │
│  │      then(emailService).should()                          │    │
│  │          .sendWelcomeEmail("kim@test.com")               │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  given() = "이 상황에서"  (BDDMockito)                           │
│  when() = "이 행동을 하면" (Kotlin에서는 when이 예약어라         │
│            메서드 호출로 대체)                                    │
│  then() = "이런 결과여야 한다"                                   │
│                                                                   │
│  왜 BDD 스타일을 쓰나?                                           │
│  ├── 테스트가 읽기 쉬워짐 (자연어에 가까움)                     │
│  ├── 비개발자도 테스트 의도를 이해할 수 있음                    │
│  └── "뭘 테스트하는지"가 명확함                                  │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

7. 실전 프로젝트 테스트 구성 예시

7.1 전체 테스트 구조

┌─────────────────────────────────────────────────────────────────┐
│            실전 프로젝트의 테스트 디렉토리 구조                  │
│                                                                   │
│  src/test/kotlin/com/example/myapp/                              │
│  │                                                               │
│  ├── unit/                    ← 순수 단위 테스트                 │
│  │   ├── service/             (Spring 없이 순수 Kotlin)          │
│  │   │   ├── UserServiceTest.kt                                  │
│  │   │   └── OrderServiceTest.kt                                 │
│  │   └── domain/                                                 │
│  │       ├── MoneyTest.kt                                        │
│  │       └── OrderTest.kt                                        │
│  │                                                               │
│  ├── slice/                   ← Slice 테스트                     │
│  │   ├── web/                 (@WebMvcTest 사용)                 │
│  │   │   ├── UserControllerTest.kt                               │
│  │   │   └── OrderControllerTest.kt                              │
│  │   ├── jpa/                 (@DataJpaTest 사용)                │
│  │   │   ├── UserRepositoryTest.kt                               │
│  │   │   └── OrderRepositoryTest.kt                              │
│  │   └── json/                (@JsonTest 사용)                   │
│  │       └── UserDtoJsonTest.kt                                  │
│  │                                                               │
│  └── integration/             ← 통합 테스트                      │
│      ├── UserApiIntegrationTest.kt  (@SpringBootTest)            │
│      └── OrderFlowTest.kt          (@SpringBootTest)            │
│                                                                   │
│  테스트 지원 파일:                                               │
│  src/test/resources/                                             │
│  ├── application-test.yml     ← 테스트 전용 설정                │
│  └── test-data/               ← @Sql용 SQL 파일                 │
│      ├── users.sql                                               │
│      └── orders.sql                                              │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

7.2 테스트 비율 가이드

┌─────────────────────────────────────────────────────────────────┐
│              테스트 비율 가이드 (권장)                           │
│                                                                   │
│  ┌────────────────────────────────────────────────────┐         │
│  │                                                      │         │
│  │  Unit Test:         70%  ████████████████████████    │         │
│  │  (빠르고 많이)           (빠름: ms 단위)             │         │
│  │                                                      │         │
│  │  Slice Test:        20%  ███████                     │         │
│  │  (계층별 검증)           (보통: 1~5초)               │         │
│  │                                                      │         │
│  │  Integration Test:   8%  ███                         │         │
│  │  (전체 흐름)             (느림: 5~30초)              │         │
│  │                                                      │         │
│  │  E2E Test:           2%  █                           │         │
│  │  (핵심 시나리오만)       (매우 느림: 분 단위)        │         │
│  │                                                      │         │
│  └────────────────────────────────────────────────────┘         │
│                                                                   │
│  예시: 테스트가 총 100개라면                                     │
│  ├── Unit Test: 70개 (Service 로직, 도메인 객체 등)             │
│  ├── Slice Test: 20개 (Controller, Repository 등)               │
│  ├── Integration Test: 8개 (주요 API 흐름)                      │
│  └── E2E Test: 2개 (로그인→주문→결제 같은 핵심 시나리오)       │
│                                                                   │
│  왜 이 비율인가?                                                 │
│  ├── Unit Test가 많으면: 빠르게 피드백, 빠르게 버그 발견         │
│  ├── E2E가 많으면: 느리고, 불안정하고, 유지보수 힘듦             │
│  └── Slice Test로 "실제적이면서 빠른" 균형을 잡음               │
│                                                                   │
│  ⚠️ 절대적인 규칙은 아님!                                       │
│  ├── 프로젝트 특성에 따라 비율 조정                              │
│  ├── CRUD 위주 → Slice Test 비율 높일 수 있음                   │
│  └── 복잡한 비즈니스 로직 → Unit Test 비율 높일 수 있음         │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

7.3 실전 팁: ApplicationContext 캐싱

┌─────────────────────────────────────────────────────────────────┐
│        실전 팁: ApplicationContext 캐싱으로 속도 향상            │
│                                                                   │
│  Spring Test는 ApplicationContext를 캐싱한다!                    │
│  = 같은 설정의 테스트들은 Context를 공유                         │
│  = 첫 번째 테스트만 느리고, 이후 테스트는 빠름                   │
│                                                                   │
│  ✅ Context가 캐싱되는 경우 (같은 Context 재사용):               │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  @WebMvcTest(UserController::class)                       │    │
│  │  class UserControllerTest1 { ... }                        │    │
│  │                                                          │    │
│  │  @WebMvcTest(UserController::class)   // 같은 설정!      │    │
│  │  class UserControllerTest2 { ... }                        │    │
│  │                                                          │    │
│  │  → 두 번째 테스트는 Context를 새로 만들지 않고 재사용    │    │
│  │  → 매우 빠름!                                            │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  ❌ Context 캐싱을 깨뜨리는 것들:                                │
│  ├── @MockBean이 다른 경우 (다른 Bean을 Mock함)                 │
│  ├── @DirtiesContext 사용 (Context 강제 재생성)                  │
│  ├── 프로파일(@ActiveProfiles)이 다른 경우                       │
│  └── 속성(@TestPropertySource)이 다른 경우                       │
│                                                                   │
│  팁: @MockBean을 가능한 통일하면 Context 캐싱 효율이 높아짐     │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

8. 자주 하는 실수와 해결법

┌─────────────────────────────────────────────────────────────────┐
│              자주 하는 실수 Top 5                                │
│                                                                   │
│  실수 1: 모든 테스트에 @SpringBootTest 사용                      │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  ❌ 잘못된 예:                                           │    │
│  │  @SpringBootTest   // 전체 로드 → 느림!                  │    │
│  │  class UserControllerTest { ... }                        │    │
│  │                                                          │    │
│  │  ✅ 올바른 예:                                           │    │
│  │  @WebMvcTest(UserController::class)  // 필요한 것만!     │    │
│  │  class UserControllerTest { ... }                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  실수 2: @MockBean 남발로 Context 캐싱 파괴                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  ❌ 테스트마다 다른 @MockBean 조합                       │    │
│  │  → 매번 새 Context 생성 → 느림                           │    │
│  │                                                          │    │
│  │  ✅ 공통 상위 클래스에 @MockBean 통일                    │    │
│  │  abstract class BaseControllerTest {                     │    │
│  │      @MockBean lateinit var userService: UserService     │    │
│  │      @MockBean lateinit var orderService: OrderService   │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  실수 3: 테스트 간 데이터 격리 안 함                             │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  ❌ 테스트 A가 저장한 데이터가 테스트 B에 영향           │    │
│  │  → 실행 순서에 따라 결과가 달라짐                        │    │
│  │                                                          │    │
│  │  ✅ @DataJpaTest 사용 (자동 롤백)                        │    │
│  │  ✅ @BeforeEach에서 데이터 정리                          │    │
│  │  ✅ @Sql로 데이터 초기화                                 │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  실수 4: 테스트에서 실제 외부 API 호출                           │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  ❌ 테스트에서 진짜 카카오 API 호출                      │    │
│  │  → 네트워크 끊기면 테스트 실패                           │    │
│  │  → 비용 발생                                             │    │
│  │                                                          │    │
│  │  ✅ @RestClientTest + MockRestServiceServer 사용         │    │
│  │  ✅ WireMock으로 외부 API 모킹                           │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  실수 5: 테스트 이름이 모호함                                    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  ❌ fun testGetUser() { ... }                            │    │
│  │     → 무엇을 검증하는지 모름                             │    │
│  │                                                          │    │
│  │  ✅ fun `존재하지 않는 사용자 조회 시 404 반환`() { ... } │    │
│  │     → 한글로 명확하게! (Kotlin 백틱 활용)                │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

9. Stale Test (스테일 테스트) - 썩은 테스트

9.1 Stale Test란?

┌─────────────────────────────────────────────────────────────────┐
│                    Stale Test (스테일 테스트)                     │
│                                                                   │
│  Stale = "신선하지 않은", "오래되어 썩은"                         │
│  (빵이 stale 해졌다 = 빵이 딱딱해지고 상했다)                    │
│                                                                   │
│  Stale Test = "코드는 변했는데 테스트는 안 따라간 테스트"        │
│                                                                   │
│  비유:                                                           │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  건강검진 비유:                                           │    │
│  │                                                          │    │
│  │  10년 전 검진표: "시력 2.0, 건강함"                      │    │
│  │  현재 실제 상태: 시력 0.3, 안경 필요                     │    │
│  │                                                          │    │
│  │  옛날 검진표를 보고 "나는 건강해!" 라고 믿으면?           │    │
│  │  → 위험! 현실을 반영하지 못하는 검사 = Stale Test        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  핵심 정의:                                                      │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  Stale Test = 코드가 변했지만 테스트는 업데이트되지 않아  │    │
│  │  더 이상 의미 있는 검증을 하지 못하는 테스트              │    │
│  │                                                          │    │
│  │  ● 통과는 하지만 실제로 아무것도 검증하지 않음            │    │
│  │  ● 실패해야 하는데 통과하여 거짓된 안전감을 줌            │    │
│  │  ● 코드 변경과 동기화되지 않은 "죽은" 테스트             │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

9.2 Stale Test가 생기는 과정

┌─────────────────────────────────────────────────────────────────┐
│              Stale Test가 만들어지는 과정                         │
│                                                                   │
│  처음:                                                           │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  // 할인 로직: 10% 고정 할인                             │    │
│  │  fun calculateDiscount(price: Long): Long {              │    │
│  │      return price * 10 / 100                              │    │
│  │  }                                                        │    │
│  │                                                          │    │
│  │  // 테스트: 10% 할인 검증                                │    │
│  │  @Test                                                    │    │
│  │  fun `10000원의 10% 할인은 1000원`() {                   │    │
│  │      assertEquals(1000, calculateDiscount(10000))         │    │
│  │  }                                                        │    │
│  │  → ✅ 코드와 테스트가 일치. 좋은 테스트!                │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  6개월 후: 요구사항이 바뀜                                       │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  // 할인 로직 변경: VIP 등급별 차등 할인                 │    │
│  │  fun calculateDiscount(price: Long, grade: Grade): Long {│    │
│  │      return when (grade) {                                │    │
│  │          Grade.VIP    -> price * 20 / 100                 │    │
│  │          Grade.GOLD   -> price * 15 / 100                 │    │
│  │          Grade.NORMAL -> price * 10 / 100                 │    │
│  │      }                                                    │    │
│  │  }                                                        │    │
│  │                                                          │    │
│  │  // 테스트는 그대로... (아무도 안 고침)                  │    │
│  │  @Test                                                    │    │
│  │  fun `10000원의 10% 할인은 1000원`() {                   │    │
│  │      assertEquals(1000,                                   │    │
│  │          calculateDiscount(10000, Grade.NORMAL))          │    │
│  │  }                                                        │    │
│  │  → ⚠️ 통과는 하지만 VIP/GOLD 할인은 검증 안 함!        │    │
│  │  → 이것이 Stale Test!                                    │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  문제가 터지는 순간:                                             │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  VIP 할인 계산에 버그가 있어도                            │    │
│  │  테스트는 여전히 ✅ 통과                                  │    │
│  │                                                          │    │
│  │  개발자: "테스트 다 통과하니까 배포해도 되겠지!"          │    │
│  │  프로덕션: VIP 고객 할인이 안 됨 → 클레임 폭주           │    │
│  │                                                          │    │
│  │  원인: 테스트가 현재 코드를 제대로 검증하지 않았기 때문   │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

9.3 Stale Test의 유형

┌─────────────────────────────────────────────────────────────────┐
│                  Stale Test의 5가지 유형                          │
│                                                                   │
│  유형 1: Orphaned Test (고아 테스트)                              │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  테스트 대상 코드가 삭제/이동되었는데                     │    │
│  │  테스트만 남아있는 경우                                   │    │
│  │                                                          │    │
│  │  예: UserService.findByEmail() 삭제됨                    │    │
│  │      → 테스트는 Mock만 검증하므로 여전히 통과             │    │
│  │      → 실제로는 아무것도 테스트하지 않음                  │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  유형 2: Partial Coverage Test (부분 검증 테스트)                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  코드에 새 기능/분기가 추가되었는데                       │    │
│  │  테스트가 업데이트되지 않은 경우                          │    │
│  │                                                          │    │
│  │  예: if-else 3개 → 5개로 늘었는데                        │    │
│  │      테스트는 여전히 3개 분기만 검증                      │    │
│  │      → 새 분기의 버그를 놓침                              │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  유형 3: Tautological Test (동어반복 테스트)                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  Mock이 반환하는 값을 그대로 검증하는 테스트              │    │
│  │  = 자기가 정한 답을 자기가 맞추는 시험                    │    │
│  │                                                          │    │
│  │  ❌ 나쁜 예:                                             │    │
│  │  given(userService.findById(1))                           │    │
│  │      .willReturn(User("Kim"))                             │    │
│  │  val result = userService.findById(1)                     │    │
│  │  assertEquals("Kim", result.name)  // Mock 값 = 검증 값  │    │
│  │  → 항상 통과. 실제 로직 검증 안 함                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  유형 4: Flaky-Stale Test (불안정한 스테일 테스트)               │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  때때로 실패하는 테스트를 @Disabled 처리하고 방치한 경우  │    │
│  │                                                          │    │
│  │  @Disabled("가끔 실패해서 임시 비활성화")                │    │
│  │  @Test                                                    │    │
│  │  fun `동시성 테스트`() { ... }                            │    │
│  │                                                          │    │
│  │  → "임시"라고 했지만 6개월째 비활성화                     │    │
│  │  → 그 사이 관련 코드는 계속 변경됨                        │    │
│  │  → 다시 활성화하면 컴파일조차 안 될 수 있음               │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  유형 5: Copy-Paste Stale Test (복붙 스테일 테스트)              │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  기존 테스트를 복사해서 새 테스트를 만들었는데             │    │
│  │  assertion만 바꾸고 setup은 안 바꾼 경우                  │    │
│  │                                                          │    │
│  │  ❌ 테스트 이름: `관리자 권한으로 삭제 시 성공`          │    │
│  │     실제 setup: 일반 사용자로 로그인                      │    │
│  │     → 관리자 권한 테스트인 척 하지만 일반 사용자 테스트   │    │
│  │     → 테스트 이름과 내용 불일치                           │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

9.4 Stale Test가 위험한 이유

┌─────────────────────────────────────────────────────────────────┐
│                  Stale Test의 실제 피해                           │
│                                                                   │
│  1. 거짓된 안전감 (False Confidence)                              │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  "테스트 100개 모두 통과! 배포 OK!"                      │    │
│  │                                                          │    │
│  │  실제 상황:                                               │    │
│  │  ├── 유효한 테스트: 60개 (실제로 뭔가를 검증)             │    │
│  │  ├── Stale Test: 30개 (항상 통과, 검증 안 함)             │    │
│  │  └── Disabled: 10개 (실행조차 안 됨)                      │    │
│  │                                                          │    │
│  │  → 실제 테스트 커버리지: 60%인데 100%로 착각              │    │
│  │  → "테스트가 있으니까 안전해" 라는 착각이 가장 위험       │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  2. 코드 변경 공포 (Fear of Change)                              │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  Stale Test가 많아지면:                                   │    │
│  │  ├── 코드 수정 → 어떤 테스트가 깨질지 예측 불가          │    │
│  │  ├── 테스트 수정 → 원래 의도를 모르니까 건드리기 무서움   │    │
│  │  └── 결과 → 아무도 리팩토링을 안 하게 됨                 │    │
│  │                                                          │    │
│  │  "테스트가 이상한데... 건드렸다가 더 망하면 어쩌지?"      │    │
│  │  → 레거시 코드가 점점 더 레거시가 되는 악순환             │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  3. CI/CD 파이프라인 낭비                                        │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  Stale Test도 CI에서 실행됨:                              │    │
│  │  ├── 실행 시간: 아무것도 검증 안 하면서 시간만 소비       │    │
│  │  ├── 리소스 낭비: 불필요한 Context 로딩, DB 연결          │    │
│  │  └── 비용 증가: 클라우드 CI는 실행 시간 = 비용            │    │
│  │                                                          │    │
│  │  100개 Stale Test × 2초 = 매 빌드마다 200초 낭비          │    │
│  │  하루 50번 빌드 × 200초 = 하루 약 3시간 낭비              │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

9.5 Stale Test가 나오게 된 배경

┌─────────────────────────────────────────────────────────────────┐
│              Stale Test 문제의 역사적 배경                        │
│                                                                   │
│  2000년대 초반: TDD와 테스트 문화의 확산                          │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  Kent Beck의 "Test-Driven Development" (2002) 출간       │    │
│  │  → "테스트를 먼저 작성하라!" 라는 문화가 퍼짐            │    │
│  │  → 테스트 커버리지를 높이는 것이 좋다는 인식              │    │
│  │  → 회사들이 "커버리지 80% 이상" 같은 규칙을 만듦         │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  2010년대: 테스트 부채의 발견                                    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  프로젝트가 3-5년 지나면서:                               │    │
│  │  ├── 테스트가 수천 개로 늘어남                            │    │
│  │  ├── 원래 작성자는 퇴사함                                 │    │
│  │  ├── 비즈니스 로직은 수십 번 바뀜                         │    │
│  │  └── 테스트는 처음 작성된 그대로                          │    │
│  │                                                          │    │
│  │  → "테스트 부채(Test Debt)" 개념 등장                    │    │
│  │  → 코드 부채(Technical Debt)처럼 테스트도 부채가 쌓임     │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  핵심 원인:                                                      │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  1. 커버리지 숫자에 집착                                  │    │
│  │     → "커버리지 높이기" 위해 의미 없는 테스트 작성        │    │
│  │                                                          │    │
│  │  2. 테스트를 "한 번 쓰고 끝"으로 인식                    │    │
│  │     → 코드 리뷰 때 테스트 변경은 검토 안 함              │    │
│  │                                                          │    │
│  │  3. 코드 변경과 테스트 변경의 분리                        │    │
│  │     → 코드만 고치고 PR 올림 → 테스트 업데이트 누락       │    │
│  │                                                          │    │
│  │  4. @Disabled 남용                                        │    │
│  │     → 실패하면 원인 분석 대신 비활성화                    │    │
│  │     → "나중에 고치겠다"는 나중은 오지 않음                │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

9.6 Stale Test 탐지 방법

┌─────────────────────────────────────────────────────────────────┐
│                  Stale Test 찾는 방법                             │
│                                                                   │
│  방법 1: Mutation Testing (돌연변이 테스트)                       │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  원리: 코드를 일부러 망가뜨렸을 때                        │    │
│  │       테스트가 실패하는지 확인                             │    │
│  │                                                          │    │
│  │  예: price * 10 / 100 → price * 10 * 100 (일부러 변경)   │    │
│  │  좋은 테스트: 즉시 실패 ✅ (변이를 "죽임")               │    │
│  │  Stale 테스트: 여전히 통과 ❌ (변이가 "살아남음")        │    │
│  │                                                          │    │
│  │  도구: PIT (Java/Kotlin), Stryker (JS/TS)                │    │
│  │                                                          │    │
│  │  // build.gradle.kts                                      │    │
│  │  plugins {                                                │    │
│  │      id("info.solidsoft.pitest") version "1.15.0"        │    │
│  │  }                                                        │    │
│  │  pitest {                                                 │    │
│  │      targetClasses.set(listOf("com.example.*"))           │    │
│  │      mutationThreshold.set(80)  // 80% 변이 죽여야 통과  │    │
│  │  }                                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  방법 2: 테스트 영향도 분석 (Test Impact Analysis)               │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  코드 변경 시 실패하지 않는 테스트 추적                   │    │
│  │                                                          │    │
│  │  관련 코드를 10번 변경했는데                              │    │
│  │  한 번도 실패하지 않은 테스트 = Stale 의심               │    │
│  │                                                          │    │
│  │  도구: IntelliJ 커버리지 도구, JaCoCo 리포트              │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  방법 3: 코드 리뷰 체크리스트                                    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  PR 리뷰 때 확인할 것:                                    │    │
│  │  ☐ 비즈니스 로직 변경 시 관련 테스트도 변경했는가?        │    │
│  │  ☐ 새 분기/조건이 추가되었다면 테스트도 추가했는가?       │    │
│  │  ☐ @Disabled 테스트가 있다면 이유와 기한이 있는가?        │    │
│  │  ☐ Mock 설정이 실제 구현과 일치하는가?                    │    │
│  │  ☐ 테스트 이름이 실제 검증 내용과 일치하는가?             │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  방법 4: 정기적 테스트 건강 점검                                 │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  분기별 "테스트 스프린트" 운영:                            │    │
│  │                                                          │    │
│  │  1단계: @Disabled 테스트 목록 추출                        │    │
│  │  2단계: 6개월 이상 변경 없는 테스트 파일 추출              │    │
│  │  3단계: Mutation Testing 실행                             │    │
│  │  4단계: 살아남은 변이(Stale 후보) 목록 작성               │    │
│  │  5단계: 팀 리뷰 → 수정 or 삭제 결정                      │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

9.7 Stale Test 예방과 해결

┌─────────────────────────────────────────────────────────────────┐
│                  Stale Test 예방 전략                             │
│                                                                   │
│  예방 1: 코드와 테스트를 항상 같이 변경                          │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  규칙: "프로덕션 코드 변경 = 테스트 코드도 변경"         │    │
│  │                                                          │    │
│  │  Git commit 메시지 예:                                    │    │
│  │  ❌ "할인 로직 변경"                                     │    │
│  │  ✅ "할인 로직을 등급별 차등 적용으로 변경 및 테스트 추가"│    │
│  │                                                          │    │
│  │  PR 체크리스트에 추가:                                    │    │
│  │  ☐ 프로덕션 코드 변경에 대응하는 테스트 변경 포함         │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  예방 2: @Disabled에 만료일 달기                                 │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  // 만료일이 있는 @Disabled                               │    │
│  │  @Disabled("동시성 이슈 조사 중 - 2024-03-31까지")       │    │
│  │  @Test                                                    │    │
│  │  fun `동시 주문 처리 테스트`() { ... }                    │    │
│  │                                                          │    │
│  │  // CI에서 만료된 @Disabled를 찾는 스크립트               │    │
│  │  // expired-tests.sh                                      │    │
│  │  grep -rn "@Disabled" src/test/ |                         │    │
│  │    grep -oP "\d{4}-\d{2}-\d{2}" |                        │    │
│  │    while read date; do                                    │    │
│  │      if [[ "$date" < "$(date +%Y-%m-%d)" ]]; then        │    │
│  │        echo "⚠️ 만료된 @Disabled 발견: $date"            │    │
│  │      fi                                                   │    │
│  │    done                                                   │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  예방 3: Assertion 품질 높이기                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  ❌ 약한 검증 (Stale 되기 쉬움):                         │    │
│  │  assertNotNull(result)       // null 아니면 뭐든 통과     │    │
│  │  assertTrue(result.size > 0) // 1개든 100개든 통과        │    │
│  │                                                          │    │
│  │  ✅ 강한 검증 (Stale 방지):                              │    │
│  │  assertEquals(3, result.size)                             │    │
│  │  assertEquals("Kim", result[0].name)                      │    │
│  │  assertThat(result).containsExactly(                      │    │
│  │      User("Kim"), User("Lee"), User("Park")               │    │
│  │  )                                                        │    │
│  │                                                          │    │
│  │  → 구체적으로 검증할수록 코드 변경 시 테스트가 실패      │    │
│  │  → 실패 = 좋은 것! 변경을 감지한다는 의미                │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  예방 4: Mutation Testing을 CI에 포함                            │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  // GitHub Actions 예시                                   │    │
│  │  // .github/workflows/mutation-test.yml                   │    │
│  │  name: Mutation Testing                                   │    │
│  │  on:                                                      │    │
│  │    schedule:                                              │    │
│  │      - cron: '0 2 * * 1'  # 매주 월요일 새벽 2시         │    │
│  │    workflow_dispatch:                                      │    │
│  │  jobs:                                                    │    │
│  │    pitest:                                                │    │
│  │      runs-on: ubuntu-latest                               │    │
│  │      steps:                                               │    │
│  │        - uses: actions/checkout@v4                        │    │
│  │        - run: ./gradlew pitest                            │    │
│  │        - run: |                                           │    │
│  │            SURVIVED=$(cat build/reports/pitest/index.html │    │
│  │              | grep -oP 'survived: \K\d+')                │    │
│  │            if [ "$SURVIVED" -gt 10 ]; then                │    │
│  │              echo "⚠️ $SURVIVED mutations survived!"     │    │
│  │              exit 1                                        │    │
│  │            fi                                              │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

9.8 Stale Test vs 관련 개념 비교

┌─────────────────────────────────────────────────────────────────┐
│              Stale Test와 헷갈리는 개념들                         │
│                                                                   │
│  ┌──────────────┬────────────────────────────────────────┐     │
│  │    용어       │              설명                       │     │
│  ├──────────────┼────────────────────────────────────────┤     │
│  │ Stale Test   │ 코드와 동기화되지 않아 의미 없는 테스트 │     │
│  │              │ → 통과하지만 검증 안 함                  │     │
│  ├──────────────┼────────────────────────────────────────┤     │
│  │ Flaky Test   │ 같은 코드인데 실행할 때마다 결과 다름   │     │
│  │              │ → 때로 통과, 때로 실패 (비결정적)        │     │
│  │              │ 원인: 타이밍, 순서 의존, 외부 자원       │     │
│  ├──────────────┼────────────────────────────────────────┤     │
│  │ Fragile Test │ 관련 없는 코드 변경에도 깨지는 테스트   │     │
│  │ (깨지기 쉬운)│ → 구현 세부사항에 너무 강하게 결합      │     │
│  │              │ 원인: 내부 구현 검증, 순서 의존          │     │
│  ├──────────────┼────────────────────────────────────────┤     │
│  │ Dead Test    │ 아예 실행되지 않는 테스트               │     │
│  │ (죽은 테스트)│ → @Disabled, @Ignore, 주석 처리         │     │
│  │              │ Stale Test의 극단적 형태                 │     │
│  └──────────────┴────────────────────────────────────────┘     │
│                                                                   │
│  관계도:                                                         │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                          │    │
│  │     Stale Test (썩은)                                    │    │
│  │     ├── 심화 → Dead Test (죽은, @Disabled)               │    │
│  │     └── 원인 → 코드 변경 미반영                          │    │
│  │                                                          │    │
│  │     Flaky Test (불안정)                                   │    │
│  │     ├── 방치 → Stale Test로 전이 (@Disabled 처리 시)     │    │
│  │     └── 원인 → 비결정적 요소 (시간, 네트워크, 순서)      │    │
│  │                                                          │    │
│  │     Fragile Test (깨지기 쉬운)                           │    │
│  │     ├── 반대 → Stale과 정반대 문제                       │    │
│  │     │          (Stale: 안 깨짐 / Fragile: 너무 잘 깨짐)  │    │
│  │     └── 원인 → 구현에 과도하게 결합                      │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

10. 정리

┌─────────────────────────────────────────────────────────────────┐
│                     전체 핵심 정리                               │
│                                                                   │
│  테스트란?                                                       │
│  = 내 코드가 올바르게 동작하는지 자동으로 확인하는 코드          │
│  = 수동 확인 대신 자동 검사 → 빠르고 정확하게 반복 가능         │
│                                                                   │
│  테스트 피라미드:                                                │
│  ├── Unit Test (70%): 함수 하나를 고립 테스트. 가장 빠름        │
│  ├── Slice Test (20%): 특정 계층만 잘라서 테스트. 빠르면서 실제적│
│  ├── Integration Test (8%): 전체 연결해서 테스트. 느리지만 현실적│
│  └── E2E Test (2%): 사용자 관점 전체 테스트. 가장 느림           │
│                                                                   │
│  Slice Test = "애플리케이션의 특정 계층만 잘라서 테스트"         │
│  ├── 2016년 Spring Boot 1.4에서 도입                             │
│  ├── @SpringBootTest의 "전부 로드 → 느림" 문제를 해결            │
│  └── 필요한 Bean만 로드 → 빠르면서도 실제적인 테스트             │
│                                                                   │
│  주요 Slice Test 어노테이션:                                     │
│  ├── @WebMvcTest: Controller 테스트 (MockMvc 사용)               │
│  ├── @DataJpaTest: Repository 테스트 (내장 DB 사용)              │
│  ├── @JsonTest: JSON 직렬화/역직렬화 테스트                      │
│  ├── @WebFluxTest: WebFlux Controller 테스트                     │
│  └── @RestClientTest: 외부 API 클라이언트 테스트                 │
│                                                                   │
│  핵심 도구:                                                      │
│  ├── @MockBean: 가짜 객체로 의존성 대체                          │
│  ├── @SpyBean: 진짜 객체를 감싸서 일부만 가짜로                  │
│  ├── TestContainers: Docker로 진짜 DB 띄워서 테스트              │
│  └── BDDMockito: Given-When-Then 스타일 테스트                   │
│                                                                   │
│  실전 원칙:                                                      │
│  ├── 테스트 격리: 각 테스트는 독립적이어야 함                    │
│  ├── 적절한 레벨 선택: Controller → @WebMvcTest                  │
│  │                     Repository → @DataJpaTest                 │
│  │                     전체 흐름 → @SpringBootTest               │
│  ├── Context 캐싱 활용: @MockBean을 통일해서 속도 향상           │
│  └── 피라미드 지키기: Unit 많이, E2E 적게                        │
│                                                                   │
│  Stale Test (썩은 테스트):                                       │
│  = 코드는 변했는데 테스트가 안 따라간 "유통기한 지난 테스트"     │
│  ├── 거짓된 안전감: 통과하지만 실제로 검증 안 함                │
│  ├── 예방: 코드 변경 = 테스트 변경, 강한 assertion 작성          │
│  └── 탐지: Mutation Testing (PIT), 정기 테스트 건강 점검         │
│                                                                   │
│  한 줄 요약:                                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  "전부 다 테스트하지 말고, 필요한 부분만 잘라서(Slice)    │    │
│  │   빠르고 정확하게 테스트하자!"                            │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

관련 키워드

Slice Test, @WebMvcTest, @DataJpaTest, @JsonTest, @WebFluxTest, @RestClientTest, @SpringBootTest, Test Pyramid, Unit Test, Integration Test, E2E Test, @MockBean, @SpyBean, MockMvc, TestContainers, ApplicationContext, Auto-Configuration, Bean, BDD, Given-When-Then, Test Isolation, Stale Test, Mutation Testing, Flaky Test, Fragile Test, Dead Test, Test Debt, PIT, False Confidence, Test Impact Analysis