아래는 **9장. 테스트 피라미드와 실무 적용**에 대한 글입니다. 이 장은 테스트 피라미드라는 이론적 프레임워크를 기반으로 스프링 부트 프로젝트에서 실무적으로 테스트를 적용하는 방법을 다룹니다. 실습 예제와 실무 팁을 포함해 실질적인 가치를 제공하도록 작성했습니다.

---

## 9장. 테스트 피라미드와 실무 적용

테스트는 단순히 코드를 검증하는 도구를 넘어, 프로젝트의 품질과 유지보수성을 높이는 전략입니다. 테스트 피라미드는 이를 체계적으로 설계하는 데 유용한 개념으로, 스프링 부트 프로젝트에서 어떻게 적용할 수 있는지 이 장에서 다룹니다. 실무에서의 전략, 흔한 실수, 그리고 성공적인 테스트 문화를 구축하는 방법까지 살펴보겠습니다.

### 9.1 테스트 피라미드 이해

테스트 피라미드는 테스트의 종류와 비율을 계층적으로 표현한 모델입니다. 아래에서 위로 갈수록 테스트 범위는 넓어지지만, 수와 실행 비용은 줄어듭니다. 주요 계층은 다음과 같습니다:

- **단위 테스트(Unit Tests)**: 가장 아래층으로, 개별 메서드나 클래스를 독립적으로 검증. 빠르고 수가 많음.
- **통합 테스트(Integration Tests)**: 중간층으로, 여러 모듈이나 계층 간 상호작용을 테스트. 단위 테스트보다 느림.
- **엔드투엔드 테스트(E2E Tests)**: 최상층으로, 사용자 관점에서 전체 시스템을 검증. 가장 느리고 수가 적음.

스프링 부트에서:
- 단위 테스트: 서비스 로직, 유틸리티 메서드 (예: `UserService.getUserName()`).
- 통합 테스트: 컨트롤러와 리포지토리 연동 (예: REST API 호출).
- E2E 테스트: 브라우저나 클라이언트를 통한 전체 워크플로우.

피라미드의 핵심은 **단위 테스트를 기반으로 하되, 필요한 만큼 상위 테스트를 보완**하는 것입니다. 단위 테스트가 70-80%, 통합 테스트가 15-25%, E2E 테스트가 5-10% 정도가 일반적인 비율입니다.

### 9.2 실무에서의 테스트 전략 수립

스프링 부트 프로젝트에서 테스트 전략을 수립하려면 다음 단계를 따릅니다:

1. **프로젝트 요구사항 분석**:
   - 빠른 피드백이 중요한가? → 단위 테스트 강화.
   - 외부 시스템 연동이 많나? → 통합 테스트 필요.
   - UI 중심인가? → E2E 테스트 고려.

2. **테스트 범위 정의**:
   - 핵심 비즈니스 로직은 단위 테스트로 100% 커버.
   - API 엔드포인트는 통합 테스트로 검증.
   - 주요 사용자 시나리오는 E2E로 확인.

3. **도구 선택**:
   - 단위: JUnit, Mockito, AssertJ.
   - 통합: `@SpringBootTest`, Testcontainers.
   - E2E: Selenium, Cypress (웹 애플리케이션용).

#### 예제 전략
간단한 사용자 관리 API 프로젝트:
- **단위 테스트**: `UserService`의 `createUser()`, `getUserName()` 메서드.
- **통합 테스트**: `/users` 엔드포인트 호출 및 DB 저장 확인.
- **E2E 테스트**: 사용자 등록 후 이름 조회 시나리오.

```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserApiIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void createAndGetUser() {
        // 통합 테스트: 사용자 생성
        ResponseEntity<String> createResponse = restTemplate.postForEntity("/users", new User(null, "Alice"), String.class);
        assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);

        // 통합 테스트: 사용자 조회
        ResponseEntity<String> getResponse = restTemplate.getForEntity("/users/1/name", String.class);
        assertThat(getResponse.getBody()).isEqualTo("Alice");
    }
}
```

### 9.3 흔한 테스트 실수와 해결 방법

실무에서 자주 발생하는 실수와 대처법을 정리했습니다:

- **실수 1: 모든 것을 통합 테스트로 작성**
  - 문제: 느리고 유지보수가 어려움.
  - 해결: 단위 테스트로 분리 가능한 로직을 옮기고, 통합 테스트는 핵심 흐름에 집중.
  
- **실수 2: 모킹 과다 사용**
  - 문제: 실제 동작과 달라 신뢰성 하락.
  - 해결: 모킹은 외부 의존성에만 사용하고, 내부 로직은 실제 구현으로 테스트.

- **실수 3: 테스트 데이터 관리 부족**
  - 문제: 테스트 간 간섭으로 실패.
  - 해결: `@Transactional` 사용하거나, 각 테스트마다 고유 데이터 생성.

- **실수 4: 커버리지에 집착**
  - 문제: 의미 없는 테스트 증가.
  - 해결: 핵심 로직의 품질에 집중하고, 커버리지는 참고 지표로만 활용.

#### 예제: 실수 수정
```java
// 잘못된 예: 모든 것을 통합 테스트로
@SpringBootTest
class BadTest {
    @Test
    void testEverything() {
        // 너무 많은 검증
    }
}

// 개선된 예: 단위 테스트로 분리
class UserServiceTest {
    @Mock
    private UserRepository repository;

    @InjectMocks
    private UserService service;

    @Test
    void getUserName() {
        when(repository.findById(1L)).thenReturn(new User(1L, "Alice"));
        assertThat(service.getUserName(1L)).isEqualTo("Alice");
    }
}
```

### 9.4 성공적인 테스트 문화 구축

테스트를 프로젝트의 핵심 요소로 만들려면 팀 차원의 노력이 필요합니다:
- **교육과 공유**: 신입 개발자에게 테스트 작성법을 가르치고, 코드 리뷰에서 테스트를 점검.
- **자동화**: CI/CD에 테스트를 통합해 빠른 피드백 제공 (7장 참고).
- **책임 분담**: 모든 팀원이 테스트 작성에 참여하도록 장려.
- **측정과 개선**: 커버리지와 실패율을 모니터링하며 지속적으로 개선.

#### 실무 팁
- **테스트 주도 개발(TDD)**: 요구사항을 테스트로 먼저 작성 후 구현.
- **페어 프로그래밍**: 테스트 코드를 함께 작성하며 품질 향상.
- **리팩토링 시간 확보**: 테스트가 없으면 리팩토링이 두려워지므로, 초기에 투자.

---

### 마무리

테스트 피라미드는 스프링 부트 프로젝트에서 테스트를 체계적으로 설계하는 가이드입니다. 단위 테스트로 기반을 다지고, 통합 테스트로 상호작용을 검증하며, E2E 테스트로 사용자 경험을 보장합니다. 실무에서 흔한 실수를 피하고, 팀 전체가 테스트 문화를 받아들이면 안정적이고 확장 가능한 애플리케이션을 만들 수 있습니다. 다음 장에서는 실제 사례를 통해 배운 교훈을 공유하며 이 책을 마무리하겠습니다!

---

이 글은 테스트 피라미드의 이론과 실무 적용을 균형 있게 다루며, 실습과 팁으로 실질적인 도움을 주고자 했습니다. 추가 요청이나 수정 사항이 있다면 말씀해 주세요!