728x90
반응형
1) 문제가 발생하는 코드 (self-invocation으로 프록시를 안 탐)
상황
- UserAccountService에 @Validated를 붙였고
- createUser() 내부에서 같은 클래스의 validateCreate()를 호출함
- 그런데 validateCreate()의 파라미터 검증(@Valid, @NotBlank 등)이 실행되지 않음
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@Service
@Validated
@RequiredArgsConstructor
public class UserAccountService {
// 예시 의존성
private final UserRepository userRepository;
public Long createUser(@Valid CreateUserCommand cmd) {
// ✅ self-invocation: 같은 클래스 내부 메서드 호출
// => 스프링 프록시를 거치지 않음
// => 아래 validateCreate()의 메서드 검증이 안 걸릴 수 있음
validateCreate(cmd);
// ... 저장 로직
User user = new User(cmd.email(), cmd.password());
return userRepository.save(user).getId();
}
// "검증이 필요한 메서드" 라고 가정
public void validateCreate(@Valid CreateUserCommand cmd) {
// 여기엔 비즈니스 규칙(DB 조회 등)도 있을 수 있음
if (userRepository.existsByEmail(cmd.email())) {
throw new IllegalStateException("이미 가입된 이메일");
}
}
public record CreateUserCommand(
@NotBlank @Email String email,
@NotBlank String password
) {}
}
왜 안 걸리나?
스프링의 메서드 검증은 AOP 프록시가 “메서드 호출을 가로채서” 동작합니다.
그런데 createUser() → validateCreate() 호출은 **this.validateCreate()**와 동일한 “내부 호출”이라 프록시가 개입할 기회가 없습니다.
2) 해결 코드 (검증 메서드를 “다른 빈”으로 분리)
핵심은 간단합니다.
- UserAccountService는 흐름/트랜잭션/오케스트레이션
- UserAccountCreateValidator(새 빈)는 검증 전담
- 이제 호출이 UserAccountService (빈) → Validator (다른 빈) 이라서 프록시를 타고 검증이 정상 동작
2-1) 검증 전담 빈 만들기
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
@Component
@Validated
@RequiredArgsConstructor
public class UserAccountCreateValidator {
private final UserRepository userRepository;
// ✅ 이제 이 메서드는 "빈 외부에서 호출"되므로 프록시를 타고 검증이 걸림
public void validate(@Valid UserAccountService.CreateUserCommand cmd) {
if (userRepository.existsByEmail(cmd.email())) {
throw new IllegalStateException("이미 가입된 이메일");
}
}
}
DTO 검증(@NotBlank, @Email)은 @Valid로 자동 수행되고,
DB 조회 같은 업무 규칙은 이 안에서 추가로 검사합니다.
2-2) 서비스에서 validator 빈을 호출하도록 변경
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@Service
@Validated
@RequiredArgsConstructor
public class UserAccountService {
private final UserRepository userRepository;
private final UserAccountCreateValidator createValidator;
public Long createUser(@Valid CreateUserCommand cmd) {
// ✅ 빈 -> 빈 호출: 프록시를 타므로 메서드 검증이 정상 동작
createValidator.validate(cmd);
User user = new User(cmd.email(), cmd.password());
return userRepository.save(user).getId();
}
public record CreateUserCommand(
jakarta.validation.constraints.NotBlank
jakarta.validation.constraints.Email
String email,
jakarta.validation.constraints.NotBlank
String password
) {}
}
3) 이 방식이 실무에서 선호되는 이유
- self-invocation 문제를 구조적으로 제거합니다. (꼼수 없음)
- 검증이 “한 덩어리”로 모여서 가독성/테스트가 좋아집니다.
- Service는 “무엇을 할지(유스케이스)” 중심, Validator는 “요청이 유효한지” 중심으로 역할이 선명합니다.
- 나중에 create, update, delete 등 유스케이스가 늘어도
CreateValidator, UpdateValidator처럼 확장하기 쉽습니다.
4) 주의할 점 (중복 방지)
- DTO의 형식 규칙은 DTO에만 둡니다. (@NotBlank, @Size, @Email…)
- Validator/Service에는 DB 필요 규칙, 상태 기반 규칙 위주로 둡니다.
- “형식 규칙을 Validator에서 또 if로 검사” 같은 중복은 피하는 게 깔끔합니다.
728x90
반응형
'개발 > spring boot' 카테고리의 다른 글
| 멀티모듈 Gradle 프로젝트에서 IntelliJ 빌드하는 방법 (0) | 2026.03.06 |
|---|---|
| 헥사고날 아키텍처 — 싱글 모듈에서 멀티 모듈로 (0) | 2026.03.05 |
| 헥사고날 아키텍처 구조 설명 (0) | 2026.02.12 |
| (Spring) sso 외부 인증 토큰을 검증하고 세션에 사용자 정보를 저장하는 흐름 (0) | 2026.01.07 |
| resolveLocale(req)가 하는 일 - 이 요청의 언어/국가(locale)를 어떻게 정할까? (0) | 2025.10.15 |