Spring 요청 데이터의 예외처리 (@Valid, AOP)

winluck·2024년 9월 7일
0

Springboot

목록 보기
18/18

1. Controller에서의 예외처리 (@Valid 기반)

  • RequestDto에 여러 어노테이션 기반으로 제약을 걸고, 이를 Controller 단의 RequestBody에 @Valid 어노테이션을 추가하는 방안입니다.
  • Controller 진입 전 Dto 내부 필드의 유효성을 간단하게 검사할 때 주로 사용하며, 유효성 검사 실패 시 GlobalExceptionHandler와 연계하여 관련 예외를 손쉽게 처리할 수 있다는 특징이 있습니다.
  • 다만 필드에 대한 더 복잡한 유효성 검사의 경우 온전하게 수행하기 어려우며, 이를 위해 Service 내부에서 Validator 객체를 활용하거나 AOP 기반으로 추가적인 유효성 검사를 추가하기도 합니다.
  • 또한 @Valid는 기본적으로 Controller 계층에서만 동작하며 다른 계층에서는 검증이 되지 않습니다. 다른 계층에서 파라미터를 검증하기 위해서는 @Validated와 결합되어야 합니다.

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-validation'

JoinRequestDto

@Getter
@NoArgsConstructor
public class JoinRequestDto {

    @NotNull(message = "이름은 필수 입력 값입니다.")
    @NotBlank(message = "이름은 공백일 수 없습니다.")
    private String name;

    @Email(message = "이메일 형식이 아닙니다.")
    private String email;

    @NotNull(message = "비밀번호는 필수 입력 값입니다.")
    @NotBlank(message = "비밀번호는 공백일 수 없습니다.")
    private String password;

    @Range(min = 1, max = 100, message = "1~100 사이의 값을 입력해주세요.")
    private int size;
}
  • RequestDto 내부 필드에 대한 여러 제약을 어노테이션으로 추가합니다.
    • @NotNull: null인 경우를 허용하지 않음
    • @NotEmpty:null 과 "" 둘 다 허용하지 않음
    • @NotBlank: null 과 "" 과 " " 모두 허용하지 않음
    • @DecimalMin(value = ??): value 미만인 경우를 허용하지 않음
    • @DecimalMax(value = ??): value 초과인 경우를 허용하지 않음
    • @Email: 이메일 형식에 부합해야 함
    • @Range(min = 1, max = 5): 값이 1 이상 5 이하여야 함

AuthController

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/user")
public class AuthController {

    private final AuthService authService;

    // 회원가입
    @PostMapping("/join")
    public ResponseEntity<Void> join(@RequestBody @Valid ****JoinRequestDto dto) {
        authService.join(dto);
        return ResponseEntity.ok().build();
    }
  • 이렇게 @Valid를 통해 Dto 객체 내부 필드에 대한 유효성 검사를 진행할 수 있습니다.

GlobalExceptionHandler

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = {MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, String> handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {
        Map<String, String> errors = new HashMap<>();
        exception.getBindingResult().getFieldErrors().forEach(error ->
                errors.put(error.getField(), error.getDefaultMessage())
        );
        return errors;
    }
}
  • Valid로 인한 필드 유효성 검사가 실패할 경우 Spring에서는 MethodArgumentNotValidException을 발생시킵니다.
  • 이는 GlobalExceptionHandler에서 관리하여, 어떤 필드가 왜 유효성 검사에 실패하는지에 대한 메시지를 반환하도록 설정할 수 있습니다.

image.png

image.png

  • 실제로 @Valid 어노테이션이 없다면 500이 뜨지만, @Valid 어노테이션이 존재할 경우 의도한 대로 <필드, 에러메시지>의 형태로 응답이 반환되는 것을 확인할 수 있습니다.

2. Service에서의 예외처리 (AOP 기반)

  • 비즈니스 로직에서 DB에 접근하는 것을 통해 유효성 검사가 진행되는 경우도 있습니다.
    • 예: 이메일 중복 여부 검증, 비밀번호 정/오 판정, PK로 Entity 존재 여부 판정 등
  • 이렇게 Service Layer에서 예외처리를 진행할 경우 AOP 기반으로 수행할 수 있습니다.

AuthService

@Transactional
public void join(JoinRequestDto dto) {
    String name = dto.getName();
    String email = dto.getEmail();
    String password = dto.getPassword();
  
    if(memberRepository.existsByEmail(email)) {
       throw new IllegalArgumentException("이미 존재하는 회원입니다.");
    }
    
    memberRepository.save(Member.of(name, passwordEncoder.encode(password), email));
}
  • 이 메서드는 DB에 접근하여 이메일로 유저의 중복 여부를 판정하는 유효성 검사가 존재합니다.
    • 반환 객체가 없기에 별도로 분리해도 무방합니다.
  • private 메서드로 분리하는 것이 가장 간단한 방법이지만, 다른 Service에서도 유효성 검사 내용이 반복된다면 Spring AOP 기반으로 분리할 수 있습니다.

CheckEmailDuplicate

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckEmailDuplicate {
}

AuthValidationAspect

@Aspect
@Component
@RequiredArgsConstructor
public class AuthValidationAspect {

    private final MemberRepository memberRepository;

		// @CheckEmailDuplicate이 붙은 메서드 시작 전 실행하며 매개변수로 dto 사용
    @Before("@annotation(csw.practice.security.annotation.CheckEmailDuplicate) && args(dto)")
    public void checkEmailDuplicate(JoinRequestDto dto) {
        if (memberRepository.existsByEmail(dto.getEmail())) {
            throw new IllegalArgumentException("이미 존재하는 회원입니다.");
        }
    }
}
  • 특정 어노테이션을 조건으로 하여 메서드를 실행하여, @Before, @After, @Around 등으로 구체적인 실행 시점을 지정할 수 있습니다.

AuthService

@CheckEmailDuplicate
@Transactional
public void join(JoinRequestDto dto) {
    String name = dto.getName();
    String email = dto.getEmail();
    String password = dto.getPassword();
    memberRepository.save(Member.of(name, passwordEncoder.encode(password), email));
}
  • 유효성 검사 과정을 AOP 메서드로 분리하였습니다.

  • 이렇게 이메일 유효성 검사가 AOP 메서드에서 실행되며 Validation이 정상적으로 동작하는 것을 확인할 수 있습니다.

부록: AOP를 활용한 로깅

  • AOP 기반으로 특정 메서드의 실행 시간을 측정할 수도 있습니다.

MeasureTime

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MeasureTime {
}

@MeasureTime // 어노테이션을 특정 메서드에 붙인다.
@CheckEmailDuplicate
@Transactional
public void join(JoinRequestDto dto) {
    String name = dto.getName();
    String email = dto.getEmail();
    String password = dto.getPassword();
    memberRepository.save(Member.of(name, passwordEncoder.encode(password), email));
}

LogAspect

@Slf4j
@Aspect
@Component
public class LogAspect {

    // 조인포인트를 어노테이션으로 설정
    @Pointcut("@annotation(csw.practice.security.annotation.MeasureTime)")
    private void timer(){}

    // 메서드 실행 전,후로 시간을 공유
    @Around("timer()")
    public void loggingExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {

        StopWatch stopWatch = new StopWatch();

        stopWatch.start();
        joinPoint.proceed(); // 조인포인트의 메서드 실행
        stopWatch.stop();

        long totalTimeMillis = stopWatch.getTotalTimeMillis();

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String methodName = signature.getMethod().getName();

        log.info("실행 메서드: {}, 실행시간 = {}ms", methodName, totalTimeMillis);
    }
}
  • @Pointcut을 통해 어디에서 AOP가 적용될지를 지정합니다.
  • @Around를 통해 메서드 실행 전후에 특정 로직을 실행할 수 있습니다. 위 로직에서는 메서드가 실행되기 전과 후에 시간을 측정하고 그 결과를 로그로 남기는 역할을 합니다.
  • ProceedingJoinPoint는 현재 JointPoint, 즉 AOP가 적용된 메서드에 대한 정보를 담고 있는 객체입니다. 이 객체를 통해 대상 메서드를 실행하거나 메서드에 대한 다양한 정보를 가져을 수 있습니다.
profile
Discover Tomorrow

0개의 댓글