개발을 하다 보면 컨트롤러에서 핵심 비즈니스 로직 외에 여러 부가기능을 동시에 처리하는 경우가 존재한다.
ex) 로그인 확인, 사용자 정보 확인
이럴 경우 하나의 객체 안에 핵심 비즈니스 로직과 부가 기능 로직이 섞여 들어가게 된다. 또한 부가 기능을 여러 곳에 적용하려면 상당히 귀찮다. 예를 들어 부가 기능이 필요한 클래스가 수십 개면 수십 개 모두 동일한 코드를 반복해서 추가해야 한다.
위와 같은 문제를 AOP
, Interceptor
, Filter
등을 통해 공통적으로 처리할 수 있다.
그중 Interceptor
와 HandlerMethodArgumentResolver
를 이용해 해결해 보자.
애플리케이션을 이용하다 보면 로그인을 해야 접근 가능한 페이지가 있다.
예를 들어 마이페이지, 회원 전용 페이지, 비밀번호 변경 등이 있다.
Interceptor
를 적용하기 전 로그인 확인 과정Interceptor
를 적용하지 않았을 때는 아래와 같은 과정이 필요하다.
session에서 현재 로그인된 사용자의 정보를 꺼내온다.
만약 session에서 꺼낸 정보가 없다면 (null이라면),
해당 사용자는 로그인을 하지 않은 상태이므로 401 UNAUTHORIZED
에러를 반환한다.
만약 정상적으로 session에서 로그인 정보를 꺼낼 수 있다면 200 OK
를 반환한다.
@GetMapping("/myInfo")
public ResponseEntity<UserInfoDto> myPage() {
String currentUser = loginService.getLoginUser();
UserInfoDto userInfoDto = userService.getUserInfo(currentUser);
return ResponseEntity.ok(loginUser);
}
public String getLoginUser() {
String userId = session.getAttribute(USER_ID);
if(userId == null) {
throw new UnauthenticatedUserException();
}
return userId;
}
위 코드처럼 로그인 된 상태인지 확인하는 로직이 사용되고 있다.
Interceptor
를 적용해서 해당 메서드가 핵심 비즈니스 로직에만 집중해서 처리할 수 있도록 만들어보자.
@Retention(RUNTIME)
@Target(METHOD)
public @interface LoginCheck {}
@LoginCheck
: 현재 사용자가 로그인 한 사용자인지 확인@Retention
: 어느 시점까지 어노테이션의 메모리를 가져갈지 설정@Target
: 어노테이션이 사용될 위치 지정@Component
@RequiredArgsConstructor
public class LoginCheckInterceptor implements HandlerInterceptor {
private final LoginService loginService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
LoginCheck loginCheck = handlerMethod.getMethodAnnotation(LoginCheck.class);
if (loginCheck == null) {
return true;
}
if (loginService.getLoginUser() == null) {
throw new UnauthenticatedUserException("로그인 후 이용 가능합니다.");
}
return true;
}
}
Interceptor
의 실행 메서드는 크게 preHandler(), postHandler(), afterCompletion()로 구성되어 있다.
컨트롤러의 메서드 실행 직후에 해당 요청을 가로채서 로그인 여부를 판단하기 위해 preHandler를 사용했다. 판단하는 과정은 아래와 같다.
HandlerMethod
: 실행될 컨트롤러의 메서드 (핸들러)
LoginCheck
: LoginCheck 어노테이션이 존재하는지 확인
loginCheck
가 null이면 로그인 없이 접근 가능한 페이지 👉 true 리턴
loginCheck
가 null이 아니면 session에서 로그인 정보(email)를 꺼내서 null 여부를 판단
하고,
null이면 Exception을 던진다.
모든 조건을 통과하면 로그인 완료 상태로 true 리턴
만든 Interceptor
를 등록해 보자.
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor);
}
}
webMvcConfigurer
를 구현할 경우 스프링 부트가 기본으로 설정한 MVC 설정에 기능을 추가적으로 커스터마이징 할 수 있다.
@LoginCheck
@GetMapping("/myInfo")
public ResponseEntity<UserInfoDto> myPage() {
String currentUser = loginService.getLoginUser();
UserInfoDto loginUser = userService.getUserInfo(currentUser);
return ResponseEntity.ok(loginUser);
}
public String getLoginUser() {
String userId = session.getAttribute(USER_ID);
return userId;
}
Service
에서 불필요한 예외 처리를 하지 않아도 된다.
Interceptor
를 적용하고 나서 LoginCheck
어노테이션을 통해 로그인 확인을 할 수 있다.
String currentUser = loginService.getLoginUser();
하지만 로그인된 사용자 정보를 확인하는 과정은 코드로 남아있다.
HandlerMethodArgumentResolver
인터페이스를 추가해서 마저 해결해 보자.
✅ HandlerMethodArgumentResolver
은 컨트롤러 메서드에서 특정 조건에 맞는 파라미터가 있을 때 원하는 값을 바인딩 해주는 인터페이스다.
스프링에서는 Controller에서 @RequestBody
어노테이션을 사용해 Request의 Body 값을 받아올 때, @PathVariable
어노테이션을 사용해 Request의 Path Parameter 값을 받아올 때 HandlerMethodArgumentResolver
를 사용해서 값을 받아온다.
@Retention(RUNTIME)
@Target(PARAMETER)
public @interface CurrentUser {}
@CurrentUser
: 현재 로그인된 USER의 ID(email)를 가져온다.@Component
@RequiredArgsConstructor
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
private final LoginService loginService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return loginService.getLoginUser();
}
}
supportsParameter()
현재 parameter를 resolver가 지원할지 true/false
로 반환
즉, 해당 메서드가 참이라면 resolveArgument()
를 반환한다.
해당 코드에서는 hasParameterAnnotation
메서드를 사용하여, 해당 메서드에 CurrentUser 어노테이션이 존재하는지 확인한다.
resolveArgument()
실제 바인딩 할 객체 반환한다.
해당 코드에서는 현재 로그인된 사용자 ID 반환한다.
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final LoginCheckInterceptor loginCheckInterceptor;
private final LoginUserArgumentResolver loginUserArgumentResolver;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor);
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginUserArgumentResolver);
}
}
@LoginCheck
@GetMapping("/myInfo")
public ResponseEntity<UserInfoDto> myPage(@CurrentUser String email) {
UserInfoDto loginUser = userService.getUserInfo(email);
return ResponseEntity.ok(loginUser);
}