실전에서 버티는 Spring Boot REST API 설계
그래서 좋은 Spring Boot API는 단순히 응답이 맞는 API가 아니라, 몇 달이 지나도 경계가 설명 가능한 API여야 합니다.
시작점은 annotation이 아니라 계약 소유권이다
첫 질문은 어떤 annotation을 쓸지가 아닙니다. API 계약을 누가 소유하고, 그 계약을 얼마나 안정적으로 유지해야 하는지가 먼저입니다.
건강한 팀은 DTO를 외부 계약으로 분명히 다룹니다.
- request는 클라이언트가 보낼 수 있는 입력을 정의하고
- response는 API가 약속하는 출력 형식을 정의하며
- 경계 validation은 내부 코어를 보호하고
- entity는 내부 구현 세부사항으로 남습니다
entity가 API surface로 바로 노출되는 순간부터 persistence detail이 클라이언트 행태를 결정하기 시작합니다. 그게 결국 “간단한 CRUD”가 장기 결합으로 바뀌는 지점입니다.
controller는 HTTP를 번역해야지 use case를 소유하면 안 된다
controller가 맡아야 할 일은 보통 아래에 그칩니다.
- HTTP 입력 파싱
- 상태 코드와 헤더 결정
- 적절한 application use case 호출
- 결과를 response DTO로 변환
반대로 아래가 controller에 쌓이기 시작하면 금방 위험해집니다.
- 비즈니스 정책
- transaction 규칙
- 복잡한 권한 분기
- repository를 넘나드는 orchestration
controller가 똑똑해질수록 테스트가 어려워지고, edge behavior를 깨지 않고 구조를 바꾸는 일도 어려워집니다.
레이어 구조는 폴더가 아니라 책임이 있어야 의미가 있다
익숙한 구조는 여전히 유효합니다.
src/main/java/com/example/
├─ controller/
├─ service/
├─ repository/
├─ domain/
├─ dto/
└─ config/
하지만 진짜 아키텍처는 폴더명이 아니라 책임 경계입니다.
- controller는 transport concern을 처리하고
- service는 use case와 transaction coordination을 소유하며
- repository는 persistence access를 맡고
- domain object는 비즈니스 의미를 지켜야 합니다
폴더만 나뉘고 책임은 뒤섞여 있으면, 구조는 ceremony만 늘리고 clarity는 주지 못합니다.
예시: 얇은 controller 경계
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping
public ResponseEntity<UserResponse> create(@Valid @RequestBody CreateUserRequest request) {
UserResponse response = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
핵심은 controller가 짧다는 사실보다, HTTP 번역 이상의 비즈니스 의미를 소유하지 않는다는 점입니다.
DTO 분리는 API를 persistence drift로부터 보호한다
전용 DTO를 쓰는 이유는 형식주의가 아니라 계약 보호입니다.
public record CreateUserRequest(
@NotBlank String email,
@NotBlank String name
) {}
public record UserResponse(
Long id,
String email,
String name,
LocalDateTime createdAt
) {}
이 분리는 여러 면에서 이득이 큽니다.
- validation 규칙이 persistence 필드와 독립적으로 진화하고
- 내부 컬럼 변경이 클라이언트 파손으로 이어질 가능성이 줄며
- read model과 write model을 다르게 설계하기 쉽고
- 민감 필드가 실수로 새어 나갈 위험도 줄어듭니다
production API에서 DTO 분리는 취향 문제가 아니라 안정성 기능입니다.
service는 transaction과 use case를 소유해야 한다
service layer는 비즈니스 작업이 명시적으로 드러나는 곳이어야 합니다.
여기에는 보통 아래가 들어갑니다.
- transaction boundary
- 여러 repository나 외부 시스템을 넘는 orchestration
- request validation을 넘어선 domain validation
- idempotency, retry, side effect 순서 결정
transaction 경계가 controller, repository, event publisher에 흩어지면 테스트는 통과해도 장애 상황에서 거동이 예측 불가능해집니다.
validation과 에러 응답은 초기에 표준화해야 한다
많은 팀이 에러 포맷 표준화를 cosmetic issue처럼 미룹니다. 하지만 운영 단계에서는 전혀 cosmetic이 아닙니다.
건강한 API는 최소한 아래를 갖춰야 합니다.
- 일관된 error envelope
- machine-readable error code
- 필드 단위 validation 응답
- correlation/request ID
프런트엔드, 모바일, 외부 연동 주체가 같은 API를 함께 보는 순간, 이 일관성은 지원 비용을 크게 줄여 줍니다.
읽기와 쓰기를 같은 방식으로 다루면 금방 비싸진다
production API에서 가장 유용한 구분 중 하나는 read path와 write path를 대칭적으로 보지 않는 것입니다.
write endpoint는:
- rule enforcement
- transaction correctness
- side effect ordering
을 우선합니다.
read endpoint는:
- response shape
- projection 효율
- pagination 안정성
- caching 전략
을 더 우선할 수 있습니다.
이 둘을 같은 구조로만 밀어붙이면, 맞긴 하지만 비싸거나, 빠르긴 하지만 경계가 약한 API가 되기 쉽습니다.
운영 기준으로 꼭 답할 수 있어야 하는 질문
Spring Boot API를 production-ready라고 부르려면 최소한 아래는 설명할 수 있어야 합니다.
- request ID는 어떻게 전파되는가
- 느린 엔드포인트는 어떻게 감지하는가
- 어떤 예외가 어떤 client-facing code로 매핑되는가
- transaction 시간과 lock risk는 어디에 집중되는가
- retry와 side effect는 어떤 관계를 가지는가
이 질문들은 사실 운영 질문처럼 보이지만, 실은 모두 아키텍처 질문입니다.
자주 보는 안티패턴
- entity를 controller에서 바로 내보내는 경우
- controller에 비즈니스 규칙이 쌓이는 경우
- input validation과 domain validation을 섞어 버리는 경우
- 엔드포인트마다 다른 에러 응답 형식을 쓰는 경우
- 읽기/쓰기를 같은 transaction 스타일로 밀어붙이는 경우
이 문제들은 대개 API가 커지는데도 경계 모델은 고정하지 않은 채 기능만 덧붙였을 때 생깁니다.
리뷰 체크리스트
- controller가 HTTP concern만 처리하는가
- DTO가 내부 변경으로부터 외부 계약을 보호하는가
- transaction boundary가 service에 분명히 보이는가
- 에러 모델이 일관되고 추적 가능한가
- read path와 write path가 다른 목적을 의식하고 설계됐는가
마무리 판단
Spring Boot REST API의 진짜 시험은 빨리 시작하느냐가 아니라, 트래픽과 팀 규모, 제품 범위가 커져도 경계가 여전히 이해 가능하냐입니다. 얇은 controller, 안정적인 계약, 명시적인 transaction ownership, 운영 친화적인 에러 모델이 결국 오래 버티는 API를 만듭니다.
Continue Reading
다음으로 읽기 좋은 글
WebSocket 실시간 통신 설계 가이드
Spring Boot 기반 WebSocket과 STOMP를 도입할 때 필요한 연결 수명주기, 메시지 모델, 인증, 전달 보장, 운영 함정을 실무 관점에서 정리합니다.
⚙️ BackendPython FastAPI로 REST API 빠르게 만들기
FastAPI 데모를 넘어서 라우팅, Pydantic 스키마, 의존성 주입, 인증 경계, 예외 처리, 운영 체크포인트까지 실무형 REST API 기준으로 정리합니다.
🧪 TestSpring Boot 테스트 슬라이스: @WebMvcTest, @DataJpaTest
Spring Boot 테스트 슬라이스를 단순 어노테이션 모음이 아니라 테스트 피라미드와 실행 비용 관점에서 정리합니다. @WebMvcTest, @DataJpaTest, @JsonTest, @RestClientTest를 언제 쓰고 언제 @SpringBootTest가 더 맞는지 설명합니다.
🧪 TestREST Assured API 테스트 전략 가이드
REST Assured를 사용해 Java 기반 API를 어떻게 검증할지 정리합니다. 요청/응답 예제보다 계약 검증, 인증 흐름, 테스트 데이터, 통합 테스트 경계에 초점을 맞춘 실무형 가이드입니다.
다음 탐색