Java/Spring 백엔드 구현
Spring Boot 백엔드 코드를 새로 쓰거나 구조를 바꿀 때의 판단 절차다. 프레임워크 사용법 설명이 아니라, 어디에 무엇을 둘지 결정하는 기준이다.
절차
- 변경 단위를 먼저 정한다. 요청 하나가 끝나는 지점(트랜잭션 1건, 외부 호출 1건)을 식별한다. 이 경계가 정해지지 않으면 계층 배치도 정할 수 없다.
- 계층에 책임을 배정한다. 아래 기준을 벗어나면 벗어나는 이유를 코드가 아니라 말로 설명할 수 있어야 한다.
- 트랜잭션 경계를 명시한다.
- 경계에서 타입을 바꾼다. 외부로 나가는 타입과 내부 타입을 분리한다.
- 실패 경로를 먼저 정한다. 정상 경로보다 예외 응답 규약을 먼저 확정한다.
계층 배치 기준
| 계층 | 두는 것 | 두지 않는 것 |
|---|---|---|
| Controller | 요청/응답 매핑, 입력 검증(형식), 인증 주체 추출 | 비즈니스 분기, 트랜잭션, 엔티티 직접 조작 |
| Service | 유스케이스 흐름, 트랜잭션 경계, 도메인 규칙 조합 | HTTP 개념(status, header), 쿼리 최적화 세부 |
| Domain/Entity | 불변식, 상태 전이 규칙 | 영속성 외 인프라 의존, 프레젠테이션 포맷 |
| Repository | 조회/저장, 쿼리 | 비즈니스 분기, 트랜잭션 시작 |
계층을 늘리기 전에 먼저 묻는다: 이 계층이 없으면 무엇이 깨지는가. 답이 없으면 만들지 않는다.
트랜잭션 경계
@Transactional은 Service의 유스케이스 메서드에 둔다. Controller와 Repository에 두지 않는다.- 조회 전용 경로는
readOnly = true를 명시한다. - 외부 호출(HTTP, 메시지 발행, 결제, 메일)을 트랜잭션 안에 넣지 않는다. 커밋 이후로 밀거나 (이벤트/아웃박스), 트랜잭션을 외부 호출 앞뒤로 쪼갠다. 이 규칙을 어기면 DB 락 유지 시간이 외부 서비스 지연에 묶인다.
- 같은 클래스 내부 호출은 프록시를 타지 않아 트랜잭션이 걸리지 않는다. 자기 호출 구조가 보이면 경계 설계가 잘못된 신호다.
- 예외에 의한 롤백 범위를 확인한다. checked exception은 기본적으로 롤백되지 않는다.
DTO와 entity 경계
- entity를 controller 응답으로 그대로 반환하지 않는다. 지연 로딩, 순환 참조, 내부 필드 노출이 한꺼번에 따라온다.
- 요청 DTO → 도메인 변환은 Service 진입 지점에서 끝낸다.
- 조회 전용 화면은 entity를 거치지 않고 projection으로 바로 뽑는 쪽을 먼저 검토한다.
예외와 응답 규약
- 도메인 예외는 도메인 언어로 던지고, HTTP 상태 매핑은 한 곳(
@RestControllerAdvice)에 모은다. - 에러 응답 body 형식을 프로젝트 전체에서 하나로 고정한다. 형식이 이미 정해져 있으면 그것을 따른다.
- 예외를 삼키지 않는다. 로그만 남기고 정상 응답을 반환하는 코드는 장애를 은폐한다.
설정
- 환경에 따라 달라지는 값은 코드가 아니라 property로 뺀다.
- property는
@ConfigurationProperties로 타입 있는 객체에 바인딩한다. 문자열 키를 코드 곳곳에 흩지 않는다. - 비밀값을 저장소에 커밋하지 않는다. 주입 경로(환경변수, 시크릿 매니저)를 먼저 정한다.
마무리 확인
- 새로 만든 public 메서드 각각에 대해: 트랜잭션 경계 안인가 밖인가 답할 수 있는가
- 외부 호출이 트랜잭션 안에 들어가 있지 않은가
- entity가 controller 밖으로 새어 나가지 않는가
- 실패했을 때 호출자가 무엇을 받는지 정의되어 있는가
프로젝트 고유의 계층 규칙·패키지 구조·네이밍이 있으면 그 프로젝트의 CLAUDE.md가 우선한다.