문제점
- 프로젝트를 진행하다 보니 로그인한 사용자만 접근할 수 있는 기능(주문, 결제 등)이 생겼습니다.
- 이러한 상황은 인증이 필요한 API마다 로그인 체크 코드를 작성해야 하는 문제로 연결됐습니다.
- 핵심 기능 로직과 로그인 체크 코드를 분리시켜서 결합도 낮추는 방법을 고민하였습니다.
해결 방법
1) Class로 분리하여 DI하기
- 처음에 로그인 체크 기능을 메서드나 클래스로 분리하여 DI하는 방법을 사용했습니다.
- LoginCheck클래스를 생성하고 로그인 체크 코드를 분리했습니다. HttpSession을 @RequiredArgsConstructor어노테이션을 사용하여 주입받고 session 속성을 체크하여 인증 여부를 확인하는 로직입니다.
package com.flab.doorrush.global.loginCheck;
import com.flab.doorrush.domain.authentication.exception.AuthenticationCredentialsNotFoundException;
import com.flab.doorrush.domain.authentication.service.AuthenticationService;
import javax.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class LoginCheck {
private final HttpSession httpSession;
public void checkLogin(){
String currentId = (String) httpSession.getAttribute(AuthenticationService.LOGIN_SESSION);
if (currentId == null) {
throw new AuthenticationCredentialsNotFoundException("로그인이 필요합니다.");
}
}
}
- 식당 정보를 추가하는 API에 LoginCheck 클래스를 주입하고 사용해봤습니다.
package com.flab.doorrush.domain.restaurant.api;
import com.flab.doorrush.domain.restaurant.dto.request.AddRestaurantRequest;
import com.flab.doorrush.domain.restaurant.service.RestaurantService;
import com.flab.doorrush.global.loginCheck.LoginCheck;
import com.flab.doorrush.global.Response.BasicResponse;
```생략 ```
@Controller
@RequestMapping("/restaurants")
@RequiredArgsConstructor
public class RestaurantController {
private final RestaurantService restaurantService;
private final LoginCheck loginCheck;
@PostMapping("/{ownerSeq}/addRestaurant")
public ResponseEntity<BasicResponse<Void>> addRestaurant(@PathVariable Long ownerSeq,
@Valid @RequestBody AddRestaurantRequest addRestaurantRequest) {
loginCheck.checkLogin();
addRestaurantRequest.setOwnerSeq(ownerSeq);
restaurantService.addRestaurant(addRestaurantRequest);
return ResponseEntity.status(HttpStatus.OK).build();
}
}
- 주입받은 LoginCheck클래스의 checkLogin메서드 호출 부분이 addRestaurant메서드 내에 있다는 것이 마음을 불편하게 합니다. checkLogin의 메서드 명이 수정되거나 파라미터가 생긴다면 addRestaurant메서드도 수정해야 합니다.
loginCheck.checkLogin();
- API 메서드 안에 LoginCheck클래스의 checkLogin메서드 호출 코드가 남지 않고 더 완벽하게 분리해내기 위한 방법을 찾아봤습니다. Filter, Interceptor, AOP 세 가지 방법을 많이 사용하는 것을 알게 됐습니다.
Filter, Interceptor, AOP를 간단하게 살펴보고 로그인 체크 기능에 적합할지 정리해보겠습니다.
2) Filter
- Filter(필터)는 요청과 응답을 정제하는 역할을 합니다. 자원의 처리가 끝난 후 응답 내용에 대해서도 변경하는 처리를 할 수 있습니다.
- 스프링 컨테이너가 아닌 톰캣과 같은 웹 컨테이너에 의해 관리가 되므로 DispatcherServlet이전에 실행되며 스프링과 무관한 자원에 대해 동작합니다. (빈에 접근할 수 없습니다.)
- 보통 web.xml에 등록하고, 일반적으로 인코딩 변환 처리, XSS 방어 등의 요청에 대한 처리로 사용됩니다.
- 메서드
init() : 필터 인스턴스 초기화
doFilter() : 전/후 처리
destroy() : 필터 인스턴스 종료
2-1) 로그인 체크와 Filter에 관한 의견
Filter의 경우 스프링 컨텍스트 외부에 존재하여 빈 객체에 접근할 수 없습니다. 현재는 session정보만으로 로그인 체크를 하겠지만 후에 권한 체크하기 위해 Service Layer에 접근해야 하는 등의 상황을 고려하였을 때 좋은 방법이 아니라 판단했습니다.
Spring 공식문서에 의하면 Filter는 다중 파트 양식 및 GZIP 압축과 같은 요청 콘텐츠 및 보기 콘텐츠 처리에 적합하며 일반적으로 특정 콘텐츠 유형(예: 이미지)을 요청에 매핑해야 하는 경우 Filter사용을 권장하고 있습니다. 왜 Filter를 요청이나 응답 데이터 자체를 인코딩하는 등의 가공에 사용하는지 이해할 수 있었습니다.
3) Interceptor
- Spring이 제공하는 기술로써, Dispatcher Servlet이 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있는 기능을 제공합니다.
- 웹 컨테이너에서 동작하는 필터와 달리 인터셉터는 스프링 컨텍스트에서 동작을 하기 때문에 스프링의 모든 빈 객체에 접근할 수 있습니다.
- Dispatcher Servlet은 핸들러 매핑을 통해 적절한 컨트롤러를 찾도록 요청하는데, 그 결과로 실행 체인(HandlerExecutionChain)을 돌려줍니다. 그래서 이 실행 체인은 1개 이상의 인터셉터가 등록되어 있다면 순차적으로 인터셉터들을 거쳐 컨트롤러가 실행되도록 하고, 인터셉터가 없다면 바로 컨트롤러를 실행합니다.
- 메서드
preHandler() : HandlerMapping이 적절한 핸들러 객체를 결정한 후 HandlerAdapter가 핸들러를 호출하기 전에 호출됩니다. (컨트롤러 메서드가 실행되기 전)
postHanler() : HandlerAdapter가 실제로 핸들러를 호출한 후 DispatcherServlet이 뷰를 렌더링 하기 전에 호출됩니다.
afterCompletion() : view 페이지가 렌더링 되고 난 후
3-1) 로그인 체크와 Interceptor에 관한 의견
Spring 공식문서는 핸들러 관련 전처리 작업과 핸들러에 공통적인 코드 추가 및 권한 검사 등에 Interceptor 사용을 권장합니다. 그리고 Interceptor는 Request와 Response를 파라미터로 받기 때문에 인증을 확인하거나 권한을 체크하기에 편리합니다.
현재 진행 중인 프로젝트는 여러 API가 이미 개발된 상태입니다.
예를 들면, RequestMapping 되는 URL이 "/restaurant"로 시작하는 API가 있습니다. 해당 API 내에 restaurant을 등록, 조회, 수정하는 기능을 가지고 있습니다. 이때 restaurant 조회하는 기능은 로그인 체크를 하지 않고도 사용할 수 있도록 해야 합니다.
아래 코드는 LoginInterceptor를 등록하는 InterceptorConfig 코드입니다.
package com.flab.doorrush.global.config;
import com.flab.doorrush.domain.authentication.api.LoginInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor())
.addPathPatterns("/restaurant");
}
@Bean
public AutoLoginInterceptor loginInterceptor() {
return new LoginInterceptor();
}
}
Interceptor를 등록할 때 addPathPatterns메서드를 통해 요청 URL과 Interceptor를 매핑하여 실행시킵니다.
RequestMapping되는 URL이 "/restaurant"로 시작하는 API의 각 메서드에 로그인 체크 기능 적용 여부를 다르게 설정하기 어렵다는 것을 확인할 수 있습니다.
4) Spring AOP
- AOP는 OOP를 보완하기 위해 나온 개념으로 객체 지향의 프로그래밍을 했을 때 중복을 줄일 수 없는 부분을 줄이기 위해 Aspect(관점)에서 바라보고 처리합니다.
- 주로 ‘로깅’, ‘트랜잭션’, ‘에러 처리’ 등 비즈니스단의 메서드에서 조금 더 세밀하게 조정할 때 사용합니다.
- Filter와 Interceptor와 달리 메소드 전후 지점에서 자유롭게 설정이 가능합니다. AOP는 주소, 파라미터, 어노테이션 등 다양한 방법으로 대상을 지정할 수 있습니다.
- AOP에 관해 정리한 TIL 링크
https://github.com/ypr821/TIL/blob/main/2021_10/AOP.md
4-1) 로그인 체크와 Spring AOP에 관한 의견
AOP를 사용할때 Spring의 컨트롤러는 타입과 실행 메서드가 모두 제 각각이라 포인트컷(적용할 메소드 선별)의 작성이 어려울 수 있습니다. 그리고 Spring의 컨트롤러는 파라미터나 리턴 타입이 일정하지 않고 호출 패턴도 정해져 있지 않기 때문에 컨트롤러에 AOP를 적용하려면 부가 작업들이 생길 수 있습니다.
하지만 AOP를 사용하여 URL이 "/restaurant"로 시작하는 API의 각 메서드에 로그인 체크 기능 적용을 다르게 설정할 수 있습니다. 즉, 같은 API안에서 메소드별로 다르게 설정할 수 있습니다.
설정의 어려움을 해결할 방법으로 어노테이션을 사용하였습니다. 그리고 일부 클래스 내의 모든 메서드에 적용해야 할 때는 excution포인트 컷 설정과 조합하여 사용하였습니다.
@Before("@annotation(com.flab.doorrush.domain.authentication.annotation.CheckLogin)
|| execution(* com.flab.doorrush.domain.적용할 컨트롤러 클래스명.*(..))")
아래 코드는 Aspect를 구현한 코드입니다. 로그인 체크 로직이 들어가는 부분입니다.
package com.flab.doorrush.domain.authentication.aspect;
import com.flab.doorrush.domain.authentication.exception.AuthenticationCredentialsNotFoundException;
import com.flab.doorrush.domain.authentication.service.AuthenticationService;
import javax.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
@RequiredArgsConstructor
public class CheckLoginAspect {
private final HttpSession httpSession;
@Before("@annotation(com.flab.doorrush.domain.authentication.annotation.CheckLogin) || execution(* com.flab.doorrush.domain.restaurant.api.RestaurantController.*(..))")
public void checkLogin() throws AuthenticationCredentialsNotFoundException {
String currentId = (String) httpSession.getAttribute(AuthenticationService.LOGIN_SESSION);
if (currentId == null) {
throw new AuthenticationCredentialsNotFoundException("로그인이 필요합니다.");
}
}
}
현재 진행중인 프로젝트의 경우 AOP를 사용하여 로그인 체크 기능을 구현하였습니다.
결론
Filter
요청 콘텐츠 및 보기 콘텐츠 처리에 적합하며 일반적으로 특정 콘텐츠 유형(예: 이미지)을 요청에 매핑할때 사용하길 권장하고 있습니다. 즉, 요청과 응답 자체를 가공할때 사용합니다.
Interceptor
Spring 공식문서에서 핸들러 관련 전처리 작업과 핸들러에 공통적인 코드 추가 및 권한 검사 등에 Interceptor 사용을 권장합니다. 그리고 Interceptor는 Request와 Response를 파라미터로 받기 때문에 인증을 확인하거나 권한을 체크 하기에 편리합니다. 그러나 메소드 별 설정이 어렵습니다.
Spring AOP
Contoller의 메서드의 파라미터나 리턴 타입이 일정하지 않고 호출 패턴도 정해져 있지 않기 때문에 컨트롤러에 AOP를 적용하려면 부가 작업들이 생길 수 있습니다. 그러나 메소드별 설정이 가능합니다.
참고 자료
'Spring' 카테고리의 다른 글
Spring Kotlin JPA에서의 @Transactional 활용 및 트랜잭션 전파 전략 (0) | 2025.03.14 |
---|---|
@Async 비동기 메서드와 No thread-bound request found 오류 (0) | 2023.05.30 |
스프링 부트 AutoConfiguration 원리 (0) | 2022.01.15 |
SpringBoot의 의존성 버전 관리와 원리 (0) | 2022.01.04 |
logging @SLF4J (0) | 2022.01.04 |