1. Controller 계층
목적: 사용자 경험 보호, 빠른 피드백
검증 범위:
- 필수 필드 존재 여부(null, empty)
- 형식검증(이메일 형식, 날짜형식, 전화번호 패턴 ..)
- 길이/범위 제함(글자수, 숫자 점위)
원칙: 이 계층의 검증은 신뢰할 수 없다. 클라이언트 검증은 언제든 우회 가능하므로, 보안을 위한 게이트로 삼으면 안된다.
// Controller: 형식과 필수값만 검증
@PostMapping("/users")
public ResponseEntity<?> createUser(@Valid @RequestBody CreateUserRequest request) {
// @Valid: @NotNull, @Email, @Size 등 포맷 검증만 수행
return userService.createUser(request);
}
@Valid: @NotNull, @Email, @Size등 포맷 검증만 수행한다.
2. Service 계층
목적: 비즈니스 규칙 수호, 도메인 불변식 보장
검증범위:
- 비즈니스 규칙(ex: 만 19세 이상만 가입, 잔액 초과 출금 불가 ..)
- *도메인 불변식(주문 상태 전이 유효성)
- *교차 엔티티 일관성(A와 B가 동시에 존재해야하는 관계)
- *권한/컨텍스트 기반 규칙
*도메인 불변식 : "불변식"이란 객체가 살아있는 동안 항상 참이어야 하는 규칙.
주문 상태 전이를 예로 들면: 주문생성 -> 결제완료 -> 배송중 -> 배송완료
-> 취소 가능/ 취소 불가
이 흐름을 절대 어겨서는 안된다. 배송완료된 주문을 갑자기 결제완료로 되돌리거나, 주문 생성에서 바로 배송완료로 건너뛰는건 불가능해야한다.
*교차 엔티티 일관성 :
예) 팀 - 팀장의 관계
팀이 존재하면 -> 팀장 한명이 있어야한다.
팀장이 퇴사하면 -> 팀을 없애거나 팀장을 교체해야한다.
예2) Order - OrderItem 관계
주문은 반드시 1개 이상의 상품을 가져야한다.
마지막 상품을 지우면 주문 자체도 취소. 빈 주문은 존재할 수 없다.
-> 단순한 null 체크나 비즈니스 규칙이 아니라 "두 엔티티의 관계 자체가 깨지면 안된다"
*권한/컨텍스트 기반 규칙 : 같은 행위라도 누가 하느냐에 따라 달라짐.
동일한 요청이라도 호출자의 역할이나 상황에 따라 허용 여부가 달라지는 규칙.
예)
권한기반: 본인 글이거나 관리자만 삭제 가능.
컨텍스트 기반: 관리자도 공지상항은 슈퍼관리자만 삭제 가능.
예2) 같은 API라도 시간, 상태에 따라 달라지는 컨텍스트 규칙.
경매가 진행 중일 때만 입찰가능.
본인이 만든 경매에는 입찰 불가.
// Service: 비즈니스 규칙 검증
public void withdraw(Long accountId, Money amount) {
Account account = accountRepository.findById(accountId);
// 비즈니스 규칙: 잔액 부족, 계좌 상태, 일일 한도
if (account.isLocked()) throw new AccountLockedException();
if (account.getBalance().isLessThan(amount)) throw new InsufficientFundsException();
if (account.getDailyWithdrawn().plus(amount).exceeds(DAILY_LIMIT))
throw new DailyLimitExceededException();
account.withdraw(amount);
}
3. Repository 계층
목적: 데이터 무결성 최후 방어선
검증범위:
- DB제약조건(UNIQUE, NOT NULL, FK, CHECK)
- *트랜잭션 격리 수준에 따른 동시성 보호
- *외부 시스템 연동시 응답값 검증
-- DB는 최후의 안전망으로서 제약을 유지
ALTER TABLE users
ADD CONSTRAINT uq_email UNIQUE (email),
ADD CONSTRAINT chk_age CHECK (age >= 19);
*트랜잭션 격리 수준에 따른 동시성 보호
"동시에 여러 요청이 들어왔을때 데이터가 꼬이지 않게 보호하는 것"
1. 비관적 락(Pessimistic Lock)으로 해결: 내가 읽는 순간 다른 트랜잭션은 이 행에 손대지 마.
2. 낙관적 락(Optimistic Lock)으로 해결: 충돌이 드물거라 가정하고, 저장할때 버전 확인
*외부 시스템 연동시 응답값 검증
외부 API는 내코드가 아니므로, 응답을 믿지 말고 반드시 검증해야한다.
중복검증의 트레이드 오프
중복 검증을 완전히 없애는것이 항상 옳지는 않다. 의도적 중복과 우발적 중복을 구분해야한다.
| 구분 | 의도적 중복 | 우발적 중복 |
| 목적 | 계층별 방어, 보안 강화 | 코드 복붙, 설계 부재 |
| 예시 | 이메일 형식을 Controller·DB 모두 검증 | 동일한 비즈니스 규칙을 Controller·Service 양쪽에 작성 |
| 유지보수 | 각 계층이 독립적으로 역할 수행 | 규칙 변경 시 여러 곳 수정 필요 |
원칙1. 비즈니스 규칙은 Service에만.
비즈니스 규칙이 Controller에 있으면 API가 바뀔 때마다 규칙도 흩어진다.
원칙2. DB제약은 절대 제거 X
원칙3. 검증 로직을 도메인 객체로 응집.
public class Email {
private final String value;
public Email(String value) {
if (value == null || !value.matches("^[\\w.]+@[\\w.]+\\.[a-z]{2,}$"))
throw new InvalidEmailException(value);
this.value = value;
}
}
// Service, Controller 어디서 생성해도 동일한 규칙 적용
Email email = new Email(request.getEmail());
'코드잇 스프린트 > Spring 이론' 카테고리의 다른 글
| Spring Security에서 왜 Member 대신 UserDetails를 사용하나? (0) | 2026.05.12 |
|---|---|
| 트랜잭션 ACID중 격리성(Isolation)이 보장되지 않을때 문제 (0) | 2026.04.01 |
| JPA에서 발생하는 N+1 문제의 발생 원인과 해결 방안(EAGER 는 JOIN이 아니다.) (1) | 2026.03.26 |
| 웹 API가 SOAP에서 REST로 전환된 이유 (0) | 2026.02.27 |
| Spring Bean이란? (2) | 2026.01.26 |