Post

[Spring Boot 프로덕션] Day 5: 테스트·관측성·배포 - 변경을 안전하게 운영하기

[Spring Boot 프로덕션] Day 5: 테스트·관측성·배포 - 변경을 안전하게 운영하기

이 글은 AI(Claude)의 도움을 받아 작성하고, 작성자가 검토·편집했습니다.

서론: 프로덕션 준비는 배포 버튼 앞에서 시작되지 않는다

Day 1~4에서 요청 경계, 트랜잭션, 데이터 접근, 외부 연동을 설계했다. 마지막 단계는 변경이 그 경계를 깨뜨리지 않는지 배포 전에 검증하고, 배포 후 실제 동작을 관찰하며, 이상이 생기면 빠르게 되돌리는 것이다.

테스트·관측성·배포는 별개의 운영 항목이 아니다. 테스트에서 확인한 유스케이스가 프로덕션 메트릭의 이름이 되고, 메트릭에서 발견한 실패가 다시 회귀 테스트로 들어오는 하나의 순환이다.

1. 테스트 피라미드는 책임의 경계를 따라간다

1
2
3
4
5
6
7
8
                 [E2E] 소수
            실제 사용자 핵심 흐름
          [통합/계약 테스트]
        DB·메시지·외부 API 경계
       [슬라이스 테스트]
     MVC, Repository 등 한 계층
  [단위 테스트] 다수
도메인 규칙과 유스케이스 분기

단위 테스트는 빠르고 실패 원인이 분명하다. 통합 테스트는 실제 설정·SQL·직렬화에서 생기는 간극을 잡는다. 모든 테스트를 @SpringBootTest로 만들면 느리고 실패 범위가 넓어지며, 반대로 Mock만 쓰면 진짜 DB와 HTTP 경계의 문제를 놓친다.

2. 가장 중요한 규칙은 Spring 없이 테스트한다

도메인 객체가 프레임워크 없이도 규칙을 지킬 수 있으면 단위 테스트가 단순해진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ProductTest {

    @Test
    void 재고보다_많이_예약할_수_없다() {
        Product product = Product.withStock(2);

        assertThatThrownBy(() -> product.reserve(3))
                .isInstanceOf(OutOfStockException.class);
    }

    @Test
    void 예약한_수량만큼_재고가_줄어든다() {
        Product product = Product.withStock(5);

        product.reserve(2);

        assertThat(product.stock()).isEqualTo(3);
    }
}

이 테스트에는 ApplicationContext, DB, 네트워크가 없다. Day 1에서 비즈니스 규칙을 Domain에 둔 보상이 여기서 나타난다.

3. 슬라이스와 통합 테스트의 역할을 구분하기

1
2
3
4
5
6
7
8
9
10
11
@WebMvcTest:
  요청 바인딩, Validation, 상태 코드, 오류 응답 계약

@DataJpaTest:
  매핑, 쿼리, 제약조건, Repository 동작

@SpringBootTest:
  여러 Bean과 설정이 함께 동작하는 핵심 유스케이스

Testcontainers:
  운영과 같은 DB·브로커를 컨테이너로 실행해 실제 경계 검증
1
2
3
4
5
6
7
8
9
10
11
@DataJpaTest
class OrderRepositoryTest {

    @Test
    void 멱등성_키는_중복될_수_없다() {
        repository.saveAndFlush(order("same-key"));

        assertThatThrownBy(() -> repository.saveAndFlush(order("same-key")))
                .isInstanceOf(DataIntegrityViolationException.class);
    }
}

테스트 트랜잭션은 보통 끝에서 롤백된다. 커밋이나 flush 시점에 드러나는 제약·이벤트를 검증해야 한다면 명시적으로 flush하거나 트랜잭션 밖의 결과를 확인한다. “롤백되니 깨끗하다”가 실제 커밋 동작까지 검증했다는 뜻은 아니다.

4. 외부 API는 계약과 실패 시나리오를 테스트한다

정상 200 응답만 Mock하면 Day 4의 복원력 코드가 실제로 작동하는지 알 수 없다.

1
2
3
4
5
6
필수 시나리오:
  - 응답 지연 → 정해진 시간 안에 타임아웃
  - 429/5xx → 허용된 횟수만 재시도
  - 연속 실패 → 서킷 OPEN, 빠른 실패
  - 응답 유실 후 재요청 → 같은 멱등성 키로 중복 처리 없음
  - 스키마 변경·필드 누락 → 예측 가능한 오류 변환

Consumer-driven Contract Test를 사용하면 호출하는 쪽의 기대와 제공하는 쪽의 API 변경을 배포 전에 대조할 수 있다. 중요한 것은 도구 이름보다 “우리가 의존하는 계약이 무엇인지”를 실행 가능한 형태로 남기는 것이다.

5. 로그·메트릭·트레이스를 연결하기

1
2
3
로그:     한 사건의 상세 문맥 — 무엇이 일어났나
메트릭:   시간에 따른 수치 — 얼마나 자주, 얼마나 느린가
트레이스: 요청이 여러 컴포넌트를 지난 경로 — 어디서 느렸나
1
2
3
4
5
6
MDC.put("orderId", orderId.toString());
try {
    log.info("order.confirmed");
} finally {
    MDC.remove("orderId");
}

구조화 로그에 traceId, 업무 식별자, 결과 코드, 소요 시간을 넣으면 메트릭의 급증 지점에서 실제 실패 요청으로 이동할 수 있다. 비밀번호·토큰·주민번호·결제정보 같은 민감값은 기록하지 않는다.

Spring Boot Actuator와 Micrometer는 HTTP, JVM, 커넥션 풀 같은 기본 메트릭을 수집하고 외부 모니터링 시스템으로 내보내는 출발점이 된다.

1
2
3
4
5
6
7
8
9
RED (요청 중심):
  Rate    — 초당 요청 수
  Errors  — 오류율
  Duration — p50/p95/p99 지연

자원 포화:
  Hikari active/max와 pending
  JVM heap·GC pause·thread
  메시지 consumer lag

6. 헬스체크는 살아 있음과 준비됨을 구분한다

1
2
3
4
5
6
7
Liveness:
  프로세스가 스스로 회복 불가능한가?
  실패하면 플랫폼이 재시작.

Readiness:
  지금 새 트래픽을 받을 준비가 됐는가?
  실패하면 라우팅 대상에서 제외.

Liveness가 DB 같은 외부 의존성에 매달리면 DB 장애 때 모든 애플리케이션 인스턴스가 함께 재시작되어 장애를 키울 수 있다. 외부 시스템 상태는 보통 readiness나 별도 의존성 메트릭으로 본다.

Actuator 엔드포인트는 운영망에서 필요한 것만 노출하고 인증·네트워크 정책으로 보호한다. 환경 변수, 전체 설정, 힙 덤프처럼 민감정보가 될 수 있는 진단 기능을 공개 인터넷에 열지 않는다.

7. 종료도 요청 처리의 일부다

배포 중 프로세스를 즉시 끊으면 처리 중 요청과 메시지가 유실되거나 클라이언트가 오류를 받는다.

1
2
3
4
5
6
정상 종료 순서:
  1. readiness를 내려 새 트래픽 유입 중단
  2. 진행 중 HTTP 요청 완료 대기
  3. 메시지 소비 중단, 처리 중 메시지 완료 또는 안전한 재전달
  4. DB 풀·스레드 풀·트레이서 flush
  5. 종료 제한 시간 안에 프로세스 종료

Spring Boot의 graceful shutdown 동작과 플랫폼의 종료 유예 시간이 맞는지 실제 배포 환경에서 검증한다. 애플리케이션 제한 시간이 플랫폼의 강제 종료 시간보다 길면 기다리는 도중 프로세스가 잘린다.

8. 데이터베이스 변경은 Expand-Contract로

코드와 DB 스키마는 정확히 동시에 바뀌지 않는다. 롤링 배포 중에는 구버전과 신버전이 함께 실행된다.

1
2
3
4
5
6
7
8
위험한 변경:
  컬럼 이름을 즉시 변경 → 구버전 인스턴스 쿼리 실패

Expand-Contract:
  1. 새 컬럼 추가 (기존 코드와 호환)
  2. 신버전이 구·신 컬럼을 안전하게 처리
  3. 데이터 백필 및 읽기 전환
  4. 구버전이 모두 사라진 뒤 기존 컬럼 제거

마이그레이션은 애플리케이션 릴리스와 함께 버전 관리하고, 대형 테이블의 락·실행 시간을 사전에 측정한다. “DDL 한 줄”도 프로덕션에서는 하나의 배포다.

9. 릴리스와 롤백 기준을 숫자로 정하기

1
2
3
4
5
6
7
8
9
10
배포 전:
  단위·통합·계약 테스트, 마이그레이션 리허설, 보안·설정 검증

배포 중:
  카나리 인스턴스의 오류율·p99·풀 대기·업무 성공률 비교

자동 중단/롤백 예:
  5분간 오류율 기준 초과
  p99 지연이 이전 버전보다 유의하게 악화
  주문 성공률 급락 또는 중복 처리 감지

CPU가 정상이어도 주문 성공률이 떨어질 수 있다. 기술 지표와 업무 지표를 함께 본다. 롤백 명령만 준비하지 말고, 이미 실행된 DB 마이그레이션과 외부 이벤트를 되돌릴 수 있는지도 계획한다.

10. 시리즈 종합 체크리스트

  1. Controller·Service·Domain·Infrastructure의 경계를 분명히 했다. (Day 1)
  2. 짧은 트랜잭션과 명시적 동시성·메시지 전달 정책으로 정합성을 지켰다. (Day 2)
  3. 쿼리·인덱스·페이지네이션·커넥션 풀을 측정 기반으로 최적화했다. (Day 3)
  4. 타임아웃·멱등성·재시도·격리로 외부 장애 전파를 제한했다. (Day 4)
  5. 계층별 테스트와 연결된 관측성, 정상 종료, 호환 가능한 배포로 변경을 안전하게 운영했다. (Day 5)

시리즈 마무리

Spring Boot 프로덕션 설계의 핵심은 어노테이션을 많이 아는 것이 아니다. 경계를 분명히 하고, 기다림에 상한을 두며, 재실행을 안전하게 만들고, 실제 동작을 측정하는 것이다.

요청→트랜잭션→DB→외부 시스템→운영은 서로 떨어진 주제가 아니다. Controller에서 시작한 요청은 DB 연결과 원격 호출을 지나 로그·메트릭·트레이스로 흔적을 남긴다. 어느 한 경계가 모호하면 실패가 옆 계층으로 번진다. 반대로 각 경계의 책임과 실패 정책이 명확하면 서비스는 기능이 늘고 의존성이 흔들려도 예측 가능한 방식으로 버틴다.

좋은 백엔드는 장애가 없는 시스템이 아니라, 장애의 범위를 알고 안전하게 회복하는 시스템이다.

This post is licensed under CC BY 4.0 by the author.