self-invocation 때문에 서비스 검증(@Validated)이 안 걸리는 문제 코드와, 실무에서 제일 흔한 해결책인 검증이 필요한 메서드를 다른 빈으로 분리 방법

2026. 2. 26. 10:46·개발/spring boot
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) 이 방식이 실무에서 선호되는 이유

  1. self-invocation 문제를 구조적으로 제거합니다. (꼼수 없음)
  2. 검증이 “한 덩어리”로 모여서 가독성/테스트가 좋아집니다.
  3. Service는 “무엇을 할지(유스케이스)” 중심, Validator는 “요청이 유효한지” 중심으로 역할이 선명합니다.
  4. 나중에 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
'개발/spring boot' 카테고리의 다른 글
  • 멀티모듈 Gradle 프로젝트에서 IntelliJ 빌드하는 방법
  • 헥사고날 아키텍처 — 싱글 모듈에서 멀티 모듈로
  • 헥사고날 아키텍처 구조 설명
  • (Spring) sso 외부 인증 토큰을 검증하고 세션에 사용자 정보를 저장하는 흐름
nix-be
nix-be
  • nix-be
    NiX
    nix-be
  • 전체
    오늘
    어제
    • 홈
      • 책
        • 오브젝트
      • 성장
        • jpa Querydsl 정리
        • 코딩테스트
        • 스프링 핵심 원리 - 기본편
      • 인프라
        • linux
        • vmware
        • CI&CD
        • 네트워크
        • docker
      • 개발
        • spring boot
        • spring security
        • JPA
        • java
        • thymeleaf
        • 이슈
        • jquery
        • javascript
        • 안드로이드
        • MyBatis
        • git
        • 과제
      • DB
        • postgreSql
        • mysql
        • redis
      • 잡다한것
        • 프로그램
        • 일상 관련
        • 프로젝트-주차관리
      • 회사
        • 티
        • 피
  • 블로그 메뉴

    • 홈
    • 개발
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
nix-be
self-invocation 때문에 서비스 검증(@Validated)이 안 걸리는 문제 코드와, 실무에서 제일 흔한 해결책인 검증이 필요한 메서드를 다른 빈으로 분리 방법
상단으로

티스토리툴바