본문 바로가기
Spring

로그인 체크 기능 간결하고 낮은 결합도로 구현해보기

by PROMISE_YOO 2022. 1. 28.

문제점

- 프로젝트를 진행하다 보니 로그인한 사용자만 접근할 수 있는 기능(주문, 결제 등)이 생겼습니다.

 

- 이러한 상황은 인증이 필요한 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를 적용하려면 부가 작업들이 생길 수 있습니다. 그러나 메소드별 설정이 가능합니다.

참고 자료