728x90

@Valid @Validated 차이

@Valid

@Valid는 JSR-303표준 스펙이다.

org.hibernate.validator.internal.constraintvalidators 안에 구현된

여러 Validator 구현체들로 인해 값을 검증해준다.

이의 핵심은 LocalValidatorFactoryBean 이며, 나는 스프링 부트를

사용하였기 때문에 자동으로 구성이 된다.

동작 원리

기본적으로 컨트롤러에서 @Valid가 없더라도

유효성 검증을 처리하는 로직을 지나간다.
이유는??

InvocableHandlerMethod는 적절한 파라미터 처리기를 찾으려고

HandlerMethodArgumentResolverComposite로 보낸다.

HandlerMethodArgumentResolverComposite

얘가 처리해줄 resolver를 찾는데 getArgumentResolver();

private final Map<MethodParameter, HandlerMethodArgumentResolver>argumentResolverCache = new ConcurrentHashMap<>(256);

이 인자에서 들어있는 RequestResponseBodyMethodProcessor를 통해

RequestResponseBodyMethodProcessor

validation을 진행한다.

RequestResponseBodyMethodProcessor

AbstractMessageConverterMethodArgumentResolver 를 상속받고 있는데

상속받는 이 클래스의 validateIfApplicable에서 어노테이션 for 루프를 돌면서

AbstractMessageConverterMethodArgumentResolver

@Valid가 있는지 검색한다.

있으면 DataBinder객체에 넘겨서 validate를 수행한다.

여기서 검증에 오류가 있으면 MethodArgumentNotValidException이 발생하고,

이는 스프링 ExceptionResolverDefaultHandlerExceptionResolver덕분에

400 에러를 뱉게된다.

@Validated


@Validated (전역 컨트롤러에 붙임)
위의 @Valid와 다르게 cglib 그러니까 AOP기반으로 메소드 요청을

MethodValidationInterceptor가 받아서 처리해준다.

왜 cglib이냐면 SampleController는 일반 클래스이므로

인터페이스처럼 JDK 동적 프록시가 아닌 Cglib proxy를 사용한다.

그리고선 이 프록시가 요청을 가로채서 유효성 검증을 진행해준다.

검증을 수행하고서는 Set<ConstraintViolation<Object>>result;

가 비어있는 값이 아니라면 ConstraintViolationException을 던져주는데

에러 메시지의 기본값은 javax.validation.constraints.XXX.message properties에 정의되어있다.

이는 위처럼 DefaultHandlerExceptionResolver에 등록되어 있는 객체가 아니기에

500에러와 함께 밖으로 뱉어주게 된다. 별도의 ExceptionHandler를 같이 구현해주어야 할것이다.

아래는 내가 구현한 예제 소스이다.

SampleController.java

import javax.validation.Valid;
import javax.validation.constraints.Min;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@Validated
public class SampleController {

    @PostMapping("/hello")
    public String hello(@Valid @RequestBody MessageRequest messageRequest) {
        log.info(messageRequest.getMessage());
        return "hello";
    }

    @GetMapping("/hi")
    public String hi(@Min(value = 1) int value) {
        log.info(String.valueOf(value));
        return "hi";
    }

}

MessageRequest.java

import javax.validation.constraints.NotNull;
import lombok.Getter;

@Getter
public class MessageRequest {

    @NotNull(message = "message는 null일 수 없습니다.")
    private String message;

}

SampleControllerTest.java

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.lsj8367.web.request.MessageRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@WebMvcTest(SampleController.class)
class SampleControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext ctx;

    @Autowired
    private ObjectMapper objectMapper;

    @BeforeEach
    void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(ctx)
            .alwaysDo(print())
            .build();
    }

    @Test
    @DisplayName("Post @Valid 테스트")
    void test() throws Exception {
        final String obj = objectMapper.writeValueAsString(new MessageRequest());

        mockMvc.perform(post("/hello")
                .content(obj)
                .contentType(MediaType.APPLICATION_JSON_VALUE))
            .andExpect(status().isBadRequest());
    }

    @Test
    @DisplayName("Get @Validated 테스트")
    void hiTest() throws Exception {
        mockMvc.perform(get("/hi")
                .param("value", "0")
                .contentType(MediaType.APPLICATION_JSON_VALUE)
            )
            .andExpect(status().isInternalServerError());
    }
728x90

'Spring' 카테고리의 다른 글

분산 락  (0) 2023.04.15
@ModelAttribute, @RequestBody 커맨드 객체  (3) 2022.09.15
AOP  (0) 2022.08.10
@ExceptionHandler  (0) 2022.08.10
728x90

예제는 깃허브에 있다.

AOP (Aspect Oriented Programming)

AOP는 스프링의 핵심 구성요소중 하나이다.
관점지향 프로그래밍은 프로그램 구조에 대한 또 다른 사고방식을 제공하며
객체 지향 프로그래밍을 보완해준다.

  • OOP의 모듈화 핵심 단위
    • 클래스
  • AOP의 모듈화 단위
    • 관점 (aspect)

AOP는 횡단 관심사의 분리를 허용해주어 모듈성을 높이는 것을 목표로 하는 패러다임이다.
코드 자체를 수정하지 않고 기존 코드에 추가 동작을 추가해서 수행한다.

개념 그리고 용어

  • Aspect
    • 여러 클래스에 중복되어 있는 관심사의 모듈화
    • 대표적인 예로 트랜잭션 관리가 있다.
    • Spring AOP 에서는 @Aspect를 사용한다.
  • JoinPoint
    • 메소드 실행이나 예외 처리와 같은 프로그램 실행중인 지점
    • AOP에서의 JoinPoint는 항상 메소드 실행을 나타냄.
  • Advice
    • 언제 공통 관심 기능을 핵심 로직에 적용할지를 정의
    • around, before, after 등이 있음.
    • AOP 프레임워크는 관점을 인터셉터로 모델링하고 유지한다.
    • 타깃 오브젝트에 종속되지 않는 순수한 부가기능을 담은 오브젝트
  • PointCut
    • Advice가 이 포인트컷 표현식과 연관되고, 일치하는 모든 조인 포인트에서 실행되게 한다.
    • JoinPoint의 상세 스펙을 정의한 것이다.
    • 스프링은 기본적으로 AspectJ의 pointcut 표현식을 사용한다.
  • Advisor
    • PointCutAdvice를 하나씩 가지고 있는 오브젝트
    • 어떤 기능을 어디에 전달할 것인지를 알고있는 가장 기본이 되는 모듈
    • Spring AOP에서만 사용되는 용어

스프링 AOP의 특징

프록시 패턴 기반의 AOP 구현체, 프록시 객체를 사용하는 이유는 여러개의 부가 기능들을 추가하기 위해서 사용한다.
스프링 빈에만 AOP를 적용할 수 있다.
스프링 IoC와 연동해서 중복 코드, 프록시 패턴 구현의 번거로움, 객체간 복잡도 해결을 진행한다.
결국 프록시 패턴, 데코레이터 패턴에 대한 중복도도 제거하려고 나온것이 스프링 AOP라고 생각한다.

스프링 프록시 방식의 AOP 적용

프록시 방식의 AOP를 적용하려면 최소 아래의 네가지 빈을 등록해야 한다.

  • AutoProxyCreator
  • Advice
  • PointCut
  • Advisor

일반 스프링 프레임워크에서는 설정을 해주려면 xml에 여러가지 설정들을 해주어야 하지만,

부트에서는 build.gradle에 의존성 하나만 추가해주면 자동으로 설정이 된다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop
}

추가적으로 PointCut을 정의할 때에는 위에서 설명했던 것 처럼
AspectJ 표현식을 통해 정의해준다. 자세한건 여기를 통해서 확인할 수 있다.

AOP 어노테이션

모든 어노테이션 뒤에는 AspectJ 표현식을 사용해서 적용할 부분을 정의해준다.

  • @Pointcut
    • AspectJ를 적용할 타겟을 정의해준다.
    • 전체 컨트롤러의 함수대상, 특정 어노테이션을 설정한 함수대상, 특정 메소드 대상 등 적용하기를 원하는 범위를 정의하는 어노테이션
  • @Before
    • 조건 표현식에 정의한 메소드들이 실행되기 전에 수행
  • @AfterReturning
    • 적용된 타깃 메소드가 실행된 후에 수행
  • @Around
    • 타깃 메소드 실행 전, 후 처리 둘다 수행이 가능
    • 사용해줄 때 해당 메소드를 ProceedingJoinPoint로 받아준다.

프록시 팩토리의 기술 선택 방법

  • 대상에 인터페이스가 있으면
    • JDK 동적 프록시, 인터페이스 기반 프록시
  • 대상에 인터페이스가 없으면
    • cglib, 구체 클래스 기반 프록시
  • ProxyFactory의 setProxyTargetClass(true);
    • cglib, 구체클래스 기반 프록시, 인터페이스 여부 상관없음
728x90

'Spring' 카테고리의 다른 글

@ModelAttribute, @RequestBody 커맨드 객체  (3) 2022.09.15
@Valid, @Validated 차이  (0) 2022.08.10
@ExceptionHandler  (0) 2022.08.10
Spring Rest Docs 테스트로 문서화를 해보자!  (0) 2022.08.10
728x90

@ExceptionHandler

예외 처리기가 어떻게 동작하는지에 대해서 궁금했어서
업무중에 돌려보게 되었다. (예제코드는 다시 작성할 예정)
일단 동작과정은 DB에서 해당 id를 찾아 검색했을 때 없을 경우 예외를 던져주게 하는

예시

간단하게 보면

public class UserService {
    private final UserRepository userRepository;

    public User findById(final int id) {
        return userRepository.findById(id)
                             .orElseThrow(() -> new NotFoundException("해당 유저를 찾을 수 없습니다"));
    }
}

라고 로직을 구성했을 때 이 로직의 예외에 대한 핸들러 동작을 파보게 되었다.
일단 get 메소드로 조회 로직을 수행하고

그 요청을 RequestMappingHandlerAdapter로 위임해서 처리를 요청한다.

그다음 주어진 값들로 컨트롤러에 대한 로직을 처리하는데

InvocableHandlerMethod

다음 그림이 InvocableHandlerMethod 클래스이다.

스크린샷 2022-02-09 오전 11 14 03

이 때, getBridgedMethod().invoke(getBean(), args); 부분이

cglibAopProxy쪽으로 조회 로직을 맡기게 되고 그 곳에서 repository에 대한 동작을 수행하다가
findById에서 못찾았을 경우에 에러를 던지게 된다.
다시 cglibAopProxy에서 요청결과에 대한 에러를 잡아서 다시 처리되는데

이것이 InvocableHandlerMethodcatch로 넘어온다.

catch (InvocationTargetException ex) 이부분에서

RuntimeException의 인자인지를 확인한다.

NotFoundException

이 부분에서 내가 구현한 NotFoundException 의 상속도는

NotFoundException -> NoSuchElementException -> RuntimeException -> Exception 순서기 때문에

instanceof RuntimeException 으로 처리가 되게 된다.

그렇게해서 exception 객체를 담아서 DispatcherServlet 으로 넘겨주게 된다.

DispatcherServlet

이젠 디스패쳐 서블릿이 받은 에러를 처리해줄 누군가를 찾기 시작하는데

스크린샷 2022-02-09 오전 11 18 55

HandlerExceptionResolverComposite

스크린샷 2022-02-09 오전 11 19 56

이 두 개중 HandlerExceptionResolverComposite 로 처리를 진행해준다.

그렇게 해서 resolveException 메소드를 실행해주는데

다음 이미지를 보게되면 resolvers에 3개가 들어있게 된다.

스크린샷 2022-02-09 오전 11 20 44

그 리졸버들이 바로 HandlerExceptionResolver 들을 구현한것들

스크린샷 2022-02-09 오전 11 22 39

그중에서도 나는 ResponseStatusException을 날려준게 아니기 때문에

그중에서 최종적으로 HandlerExceptionResolver 인터페이스 구현체인

ExceptionHandlerExceptionResolver, ResponseStatusExceptionResolver, DefaultHandlerExceptionResolver

세개를 가지고 for문을 돌게 된다

InvocableTargetException 이라는 객체의 target

즉, 대상이 어떤 Exception이냐에 따라서 Exception 에맞는 @ExceptionHandler로 파싱되어 커스텀된 응답으로 나가게 된다.

마무리

똑같은 로직 이라고 가정했을때 미묘하게 최적화를 하고싶다? 한다면

HandlerExceptionResolver 리스트의 맨앞인 ExceptionHandlerExceptionResolver를 사용 그러니까

커스텀으로 @ExceptionHandler 를 만들어 쓰는것이 ResponseStatusException 을 던지거나, @ResponseStatus 을 사용하는것보다

빠를 수 있겠다.

728x90

'Spring' 카테고리의 다른 글

@Valid, @Validated 차이  (0) 2022.08.10
AOP  (0) 2022.08.10
Spring Rest Docs 테스트로 문서화를 해보자!  (0) 2022.08.10
Service Layer에 대한 생각  (0) 2022.08.10
728x90

728x90

'Spring' 카테고리의 다른 글

AOP  (0) 2022.08.10
@ExceptionHandler  (0) 2022.08.10
Service Layer에 대한 생각  (0) 2022.08.10
Validaion  (0) 2022.08.09
728x90

Service Layer

이직하고 프로젝트에 대해 구조 파악을 하면서 리팩토링을 진행중에 좀 생각하게 된게 꽤많았다.
사실 서비스 레이어라고 해서 비즈니스 로직을 다 넣는건가?
또는 비즈니스 로직이 다 들어가 있는 것이다. 라는 얘기들이 많았다.
사실 나도 그렇게 생각했었던 사람중 1명이었다.
이게 근데 잘못된 생각이었다.

결국엔 사실 소스 코드를 다 분리하고 봐도 하나로 합쳐져서 동작하게 되는건 사실 맞다고 본다.

그러니까 다시 말하면 클래스 하나의 메소드에서 엄청나게 많은 줄을 가지고 개발을 할 수도 있다는 것이다.
근데 이거는 객체 지향 설계 관점에선 너무 안좋은 것이고
각자의 책임이 있을 것인데 그걸 분리한게 객체 지향인거다.

Service Layer에 대한 오해

일단 이 서비스 레이어에서는 비즈니스 로직이 넘쳐나게 될 것이 아니라,

적어도 뭔가의 조건을 통해 돌려주고 수정하고 하는 로직들은 도메인 객체가 해야될 일이라는 것이다.

보통의 서비스 레이어 특징

  • @Service에 사용되는 서비스
  • 일반적으로는 @Controller, @Repository에 사용된다.
  • @Transactional 이 사용되는 영역

도메인 특징

  • 도메인이라고 불리는 대상이 뭘 하는 객체인지 모든 사람들이 알 수 있고 공유하게 만든 모델
    • 주문, 상품 등등.. 이 도메인이 될 수 있다.
  • JPA를 사용한다면 @Entity 모델이 될 수도 있다.
    • 그렇지만, DB의 테이블과 동일해야 한다? 에서는 NO라고 할 수 있다.

모아서 보니까 결국엔 비즈니스 로직은 도메인이 가져야 한다.

그렇다면 서비스 레이어는???

트랜잭션 관리, 도메인의 순서 대로 객체에게 할당을 하여 식만 조합해주는 느낌으로 가야한다.
그래서 나는 예시 소스로 간단하게 만들어보자면

public class ExampleService {
    private final ExampleRepository1 exampleRepository1;
    private final ExampleRepository2 exampleRepository2;
    private final ExampleRepository3 exampleRepository3;

    public ExampleService(ExampleRepository1 exampleRepository1,
     ExampleRepository2 exampleRepository2, ExampleRepository3 exampleRepository3) {
        this.exampleRepository1 = exampleRepository1;
        this.exampleRepository2 = exampleRepository2;
        this.exampleRepository3 = exampleRepository3;
    }
}

만약 이런 클래스가 있어서 각 repository별로 사용하는 트랜잭션이 다르다면 트랜잭션 처리가 애매했다.
위 구조를 아래와 같이 변경했다.

public class ExampleFacade {
    private final ExampleService1 exampleService1;
    private final ExampleService2 exampleService2;
    private final ExampleService3 exampleService3;

    public ExampleService(ExampleService1 exampleService1,
     ExampleService2 exampleService2, ExampleService3 exampleService3) {
        this.exampleService1 = exampleService1;
        this.exampleService2 = exampleService2;
        this.exampleService3 = exampleService3;
    }

}

@Transactional
public class ExampleService1 {
    private final ExampleRepository1 exampleRepository1;

    public ExampleService1(ExampleRepository1 exampleRepository1) {
        this.exampleRepository1 = exampleRepository1;
    }
}

@Transactional
public class ExampleService2 {
    private final ExampleRepository2 exampleRepository2;

    public ExampleService1(ExampleRepository2 exampleRepository2) {
        this.exampleRepository2 = exampleRepository2;
    }
}

@Transactional
public class ExampleService3 {
    private final ExampleRepository3 exampleRepository3;

    public ExampleService1(ExampleRepository3 exampleRepository3) {
        this.exampleRepository3 = exampleRepository3;
    }
}

이렇게 분리를 해서 트랜잭션 관리를 해줬었다.
이게 이제 이전에 내가 퍼사드 패턴 적용기 라고 해서 포스팅을 했었던 구조인데
거기에 +@ 로 덧붙인다면 각 도메인에 대한 로직 수행을 서비스 -> 도메인 이 과정을
넣어주어야 완성인 것 같다.
객체 설계에 대해 점차 이해가 쏙쏙 되는 중이다.

728x90

'Spring' 카테고리의 다른 글

@ExceptionHandler  (0) 2022.08.10
Spring Rest Docs 테스트로 문서화를 해보자!  (0) 2022.08.10
Validaion  (0) 2022.08.09
Filter, Interceptor 정리  (0) 2022.08.07
728x90

Spring Security 예제

우선 코드는 깃허브에 있다.

일단 문서로 정리한 것을 토대로 로그인과 회원가입 어떤 순서로 동작하는지 알아보려고 했다.

로그인

일단 시큐리티에 대한 설정은 SecurityConfig 라는 클래스 설정파일을 만들어서 진행했다.

WebSecurityConfigurerAdapter

WebSecurityConfigurerAdapterWebSecurityConfigurer 라는 인터페이스를

조금 더 쉽게 생성하기 위해 존재하는 클래스이다. 이 클래스의 구현을

그러니까 기본으로 적용되어 있는것 외에 재정의 하여 사용할 수 있다.

아래는 해당 추상클래스에 대한 설명을 가져와봤다.

스크린샷 2021-12-05 오후 8 51 54

우리는 이 추상 클래스에서 구현되어있는 configure(HttpSecurity http) 메소드를 재정의 하여 설정을 진행한다.

스크린샷 2021-12-05 오후 8 55 19

SecurityConfig

package io.github.lsj8367.security.configuration;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/", "/h2-console/**"); // "/" 와 "/h2-console"은 접근 가능
        //여기서 무시하는 과정이 아래의 configure()보다 우선순위가 높다.
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .anyRequest().authenticated() //모든 요청은 인증이 필요함 위의 ignoring()에 포함된 애들은 제외시킨다. 추가적으로 permitAll()로 해제해준 url은 접근 가능
            .and()
            .formLogin() //formLogin 설정 default /login
            .and()
            .logout().permitAll(); //logout에 대한 모든권한 적용 default /logout
    }

}

여기 설정에서 formLogin()은 uri를 잡아주지 않았기에 기본값인 /login으로 로그인을 처리한다.

UsernamePasswordAuthenticationFilter

이때 동작하는 필터가 UsernamePasswordAuthenticationFilter인데 이 클래스는

AbstractAuthenticationProcessingFilter를 상속받아 처리해준다.

UsernamePasswordAuthenticationFilter 클래스는 기본값인 /login에 대해 응답한다고

설명에 명시되어 있다. 여기서 사용되는 username, passwordconfigure()에서

usernameParameter()passwordParameter()로 변경해서 매칭해줄 수 있다.

인증에 대한 검증을 수행하게 되는데

스크린샷 2021-12-05 오후 9 07 52

HTTP 메소드가 POST인지를 먼저 판단한다.

그 후에 obtainUsername, obtainPassword로 파라미터 설정한것을 가져온다.

나는 기본값으로 두었기에 form안에 input id가 username, password 이다.

그다음에 null인지, 빈문자열인지 검증을 한 뒤에

UsernamePasswordAuthenticationToken을 발급해준다.

이렇게 로그인 인증이 일단락 되었다.

728x90

'Spring > Security' 카테고리의 다른 글

Authentication 인증  (0) 2022.08.09
개요  (0) 2022.08.09

+ Recent posts