[Spring] AOP 구현 + 인터셉터 등록

merci·2023년 3월 13일
0

Spring

목록 보기
13/21
post-thumbnail

AOP + Interceptor

이번에는 AOP 를 이용해서 세션의 값을 가져오기 편하게 만들어보자

이전에는 세션을 가져오기 위해서 가장 기초적인 방법을 사용했었다.

User user = (user)session.getAttribute("user");

이러한 방법의 단점은 세션이 필요할때마다 항상 위와 같은 코드를 넣고 if 조건으로 없을경우 다른로직을 처리했는데 매번 이러기란 번거롭다.

이번에는 인터셉터와 aop를 이용해서 이러한 과정을 없애 중복되는 코드를 줄여보려고 한다.

HandlerInterceptor - 인터셉터 생성

먼저 인터셉터를 하나 만든다.

public class LoginInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
    		Object handler) throws Exception {
            User principal = (User) request.getSession().getAttribute("principal");
            if (principal == null) {
                response.setContentType("text/html; charset=utf-8");
                response.getWriter().println("잘못된 접근입니다.");
                return false;
            }else{
                return true;
            }
    }
}

세션이 존재 하지 않을경우 매핑을 중지시키고 단순한 텍스트만 화면에 보여주게 했다.

WebMvcConfigurer - 인터셉터 등록

WebMvcConfigurer 는 Spring MVC의 구성을 커스터마이즈하기 위한 인터페이스다.
WebMvcConfigurer 를 구현한 설정 클래스에 위에서 만든 인터셉터를 등록한다

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/auth/**");
    }
 }

여기서 모든 요청이 인터셉터를 거치게 만든다면 아래처럼 하고

	.addPathPatterns("/**")

인터셉터에서 제외시키고 싶은 uri는 아래처럼 만들면 된다.

    .excludePathPatterns("/", "/userjoin", "/user/emailCheck",
    "/userlogin", "/compjoin", "/comp/emailCheck",
    );

이제 /auth/ 로 시작하는 모든 요청은 인터셉터를 거치게 된다.

어노테이션 생성

다음으로 @LoginUser 어노테이션을 하나 만든다

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
    
}

그리고 컨트롤러에서 해당 어노테이션을 이용한다.

@RestController
@RequiredArgsConstructor    // final 이 붙은 생성자를 만들어 준다.
public class UserController {

    private final HttpSession session;  // 리플렉션이 자동으로 받아준다.

    @GetMapping("/login")
    public String login(){ 
        User user = User.builder()
                    .username("ssar")
                    .password("1234")
                    .tel("0102222")
                    .build();
        session.setAttribute("principal", user);
        return "login ok";
    }

    @GetMapping("/user/1")
    public String userInfo(){ // 인증 필요 없음
        return "user ok";
    }

    @GetMapping("/auth/1")
    public String authInfo(@LoginUser User principal){ // 인증 필요함 
        System.out.println("로그인한 사용자: " + principal.getUsername());
        return "auth ok";
    }
}

인터셉터에 의해서 세션이 존재 하지 않는다면 애초에 매핑이 되지 않아 화면에는 잘못된 접근입니다. 만 나올것이다.
세션이 존재한다면 @LoginUser 어노테이션이 붙은 객체에 세션을 넣어주도록 AOP를 구현한다.

Aspect - AOP 구현

AOP의 Aspect(관점)은 관심사를 캡슐화하는데 사용된다.
스프링은 서버 실행시 자동으로 @Aspect + @Component 를 감지해서 등록하게 된다.
이때 스프링은 자동으로 AOP Proxy를 만들게 되고 이때 리플렉션이 등록된 @PointCut이나 @Around를 찾는다.
생성된 AOP Proxy는 비즈니스 로직을 감싸게 되고 HTTP 요청을 Proxy가 먼저 처리하게 된다.

AOP를 구현하는 코드는 아래처럼 작성한다.
로그인된 사용자를 처리하는 방법으로 Aspect를 정의해서 분리시킨다.

@Aspect
@Component
public class LoginAdvice {

    @Around("execution(* shop.mtcoding.aopstudy.controller..*.*(..))") 
    public Object loginUserAdvice(ProceedingJoinPoint jp) throws Throwable {
        Object result = jp.proceed();
        Object[] args = jp.getArgs();
        Object[] param = new Object[1]; // 배열 생성
        
        for (Object arg : args) {
            if (arg instanceof User) {
                HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder
                			.getRequestAttributes()).getRequest();
                HttpSession session = req.getSession();
                
                User principal = (User) session.getAttribute("principal");
                param[0] = principal;
                result = jp.proceed(param);
            }
        }
        return result;
    } 
}

메소드위가 아닌 파라미터에 어노테이션을 사용할경우에는 아래와 같은 코드를 사용하지 않고

    @Pointcut("@annotation(shop.mtcoding.aopstudy.handler.aop.LoginUser)")
    public void loginUser() {}

아래의 코드를 사용한다.

	@Around("execution(* shop.mtcoding.aopstudy.controller..*.*(..))")

파라미터에 어노테이션을 넣는다면 첫번째 코드로는 접근하지 못하기 때문에 두번째 코드를 이용한다.

ProceedingJoinPoint

ProceedingJoinPoint는 AOP에서 사용하는 인터페이스로 proceed()를 통해 메소드를 제어할 수 있다.
Object result = jp.proceed(); 를 통해 원래 메소드의 반환결과를 가져오고
Object[] args = jp.getArgs(); 를 통해서 메소드의 입력 파라미터들을 가져온다.

인자들을 순회해서 타입이 User라면 현재 세션에서 User정보를 추출해서 리턴해주게 된다.
따라서 세션의 User 객체를 원래 메소드의 인자로 넣어주게 되어 실제 코드에서는 로그인이 되어 있다면 User객체에 접근해서 바로 가져올수 있게 된다.

AOP를 이용하지 않았을때는
@RequiredArgsConstructor + private final HttpSession session; 을 이용해서 번거롭게 꺼냈었다. !!
더이상 중복되는 코드를 작성할 일이 없어졌다. 아주 좋은 일이다.


정리 하자면 위와 같은 방법으로 번거롭게 세션에서 User 정보를 꺼내는 코드를 줄이고 유효성 검사도 줄일 수 있게 된다.

또 다른 방법으로는 HandlerMethodArgumentResolver 를 이용해서 AOP 를 핸들링 하는 방법이 있다.



HandlerMethodArgumentResolver

이 방법은 HandlerMethodArgumentResolver 을 구현해서 어노테이션을 핸들링한다.
Aspect와 다르게 Http요청의 일부를 해석하고 바인딩 하는 방법을 정의한다.
지금 하고자 하는 목적이 세션에서 User 오브젝트를 바인딩 하는것이므로 리졸버를 사용해도 유사한 결과가 나온다.

@RequiredArgsConstructor
@Configuration
public class LoginResolver implements HandlerMethodArgumentResolver {

    private final HttpSession session;

	// supportsParameter - 특정 타입 인자를 처리할 수 있는지 체크
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
    	// SessionUser 어노테이션 있어 ?
        boolean check1 = parameter.getParameterAnnotation(SessionUser.class) != null;
        // 메소드가 User를 반환해 ?
        boolean check2 = User.class.equals(parameter.getParameterType());  
        return check1 && check2;
    }

	// resolveArgument - 실제 인자 반환 ( supportsParameter 통과 )
    @Override
    @Nullable
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        // 요청 파라미터를 메소드 인자로 바인딩
        return session.getAttribute("principal");
    }
}

SessionUser.class는 다음과 같다.

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface SessionUser {
    
}

구현한 리졸버를 등록한다.

@RequiredArgsConstructor
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final LoginResolver loginResolver;
    private final LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/auth/**");
    }
	// 리졸버 등록
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(loginResolver);
    }
}

@SessionUser@LoginUser 와 마찬가지로 파라미터에 등록되는 어노테이션인데 위 코드에서는 리플렉션이 해당 어노테이션이 존재하는 메소드와 해당 메소드의 파라미터가 User 타입인지 확인해서 참이라면 세션에서 User 정보를 가져와서 메소드의 인자로 넣어주는 코드다.

이러한 방법을 이용해서도 세션이 필요한 유효성 검사를 간단하게 할 수 있다

profile
작은것부터

0개의 댓글