사이드 프로젝트를 하다보면, 거의 필수로 들어가는 기능이 로그인 기능이라고 생각합니다.
편의를 위해 소셜로그인(ex. 카카오, 구글, 네이버 로그인)을 적용시키는 경우도 있지만, 로그인과 사용자 인증에 대한 깊은 고민을 해볼 필요가 있어서 한 번 쯤 직접 A부터 Z까지 개발해보는 것도 좋은 것 같습니다.
본 포스팅은 제가 사이드 프로젝트를 진행하면서 로그인과 사용자 인증 처리를 Spring 의 ArgumentResolver로 적용한 과정을 적어보자 합니다.
1. 로그인 기능
로그인 기능 자체는 어렵지 않다. 단순하게 로그인 아이디와 비밀번호를 받아 조회되는 사용자가 있으면 성공, 없으면 실패 정도의 로직입니다.
보통 로그인 기능을 구현할 때는 세션을 사용하지만, 공부를 해볼 겸 JWT 를 사용해봤습니다.
(두 방식의 차이점은 나중에 한 번 포스팅해보겠습니다.)
2. 로그인한 사용자 인증 기능
제가 개발하고 있던 쇼핑몰에는 사용자를 위한 페이지와 판매자를 위한 페이지가 분리되어 있는데요.
오늘 적용해볼 곳은 판매자를 위한 페이지에서의 동작을 통해 로그인 기능을 확인해보려 합니다.
JWT 의 경우, 요청 헤더 정보 중 Authorization 항목에 "Bearer 토큰"의 형식으로 토큰을 같이 보내고, 해당 토큰을 서버에서 파싱하여
판매자 정보를 조회합니다.
@PostMapping("/products")
public ResponseEntity<ProductResponse> createProduct(HttpServletRequest request,
@RequestBody ProductRequest productRequest) {
// JWT token parsing
if(request == null) {
throw new IllegalStateException("네트워크에 문제가 발생했습니다. 잠시 후, 다시 시도해주세요.");
}
String token = parsingToken(request); // Request Header 에서 토큰 추출
Long partnerId = jwtTokenProvider.getUserId(token); // 추출한 토큰에서 판매자 ID 조회
// Business Logic
ProductDto productDto = ProductRequest.toDto(productRequest, partnerId);
ProductResponse productResponse = productService.saveProduct(partnerId, productDto);
return ResponseEntity.created(URI.create("/products/"+productResponse.getId())).body(productResponse);
}
위 처럼 판매자 정보의 인증이 필요한 로직을 실행하는 경우, 필연적으로 아래 내용이 모든 Controller 메서드에 포함되어야 합니다.
- 파라미터에 HttpServletRequest 포함
- JWT 를 파싱해서 판매자 정보를 조회
현재는 판매자가 상품을 등록하는 하나의 기능만을 예시로 들어서 와닿지 않을 수 있지만, 판매자가 수행할 수 있는 기능이 많아질 수 록
단 4줄의 공통된 로직의 반복은 좋은 방법이 아닐 것입니다.
(만약, 토큰에서 추출해야하는 정보가 추가되거나, 기타 다른 수정사항이 생긴다면 공통적으로 작성했던 모든 코드를 다 변경해야겠죠?)
위 같은 중복 로직을 한 곳에서 처리하게 하기 위해 SprinMVC 의 구성요소 중 하나인 ArgumentResolver 를 사용해서 위 문제를 해결해 보도록 하겠습니다.
3. 중복 로직을 Annotation 과 ArgumentResolver 로 변경하기
Spring MVC 사이클 내에 ArgumentResolver 를 커스텀하면, 사용자 검증 로직의 중복을 최소화 할 수 있습니다.
Controller 에서 사용할 Annotation 과 ArgumentResolver 역할에 필요한 class 들을 생성해줍니다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginPartner {
}
public class AuthorizedPartner {
private Long id;
public AuthorizedPartner() {
}
public AuthorizedPartner(Long id) {
this.id = id;
}
}
@Component
public class LoginPartnerArgumentResolver implements HandlerMethodArgumentResolver {
private final JwtTokenProvider jwtTokenProvider;
public LoginPartnerArgumentResolver(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(LoginPartner.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if(request == null) {
throw new IllegalStateException("네트워크에 문제가 발생했습니다. 잠시 후, 다시 시도해주세요.");
}
String token = parsingToken(request);
Long partnerId = jwtTokenProvider.getUserId(token);
return new AuthorizedPartner(partnerId);
}
}
LoginPartnerArgumentResolver 클래스는 ArgumentResolver를 커스텀하기 위해 HandlerMethodArgumentResolver 인터페이스를 상속받아 줍니다.
그러면, 2개의 메서드를 override 해야 합니다.
- boolean supportParameter(MethodParameter parameter);
- Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throw Exception;
위 내용을 설명하려면 먼저 ArgumentResolver 의 역할을 간략하게 알아볼 필요가 있습니다.
먼저, 스프링 웹 애플리케이션은 요청을 받으면 그 요청에 맞는 컨트롤러의 메서드를 호출하도록 설계되어 있습니다.
이 과정에서 @GetMapping 혹은 @PostMapping 에 매핑된 URL 정보 중에 RequestURI 가 일치하는 메서드를 호출하게 되는데,
해당 메서드에 지정된 파라미터와 Request 의 파라미터 정보가 일치하는지 확인하는 과정을 가지게 되고, 일치하면 메서드의 파라미터인 객체들에 자동으로 데이터를 바인딩하게 됩니다.
Controller 메서드의 파라미터를 보게되면, @RequestBody 와 같은 Annotation 을 사용하게 되는데,
이 또한 ArgumentResolver 에 의해 확인되어 데이터를 매핑하게 됩니다.
그래서 @RequestBody 와 같이 데이터를 바인딩 하기 위한 Annotation 을 추가로 생성해주고, (@LoginPartner)
해당 Annotation이 매핑할지 말지 판단할 ArgumentResolver를 생성해 줍니다. (LoginPartnerArgumentResolver)
4. Custom 한 ArgumentResolver 를 Spring 에 등록하기
나만의 ArgumentResolver 를 새로 만들었다면, 이제 이 ArgumentResolver 가 Spring 에서 동작 할 수 있게 설정해주어야 합니다.
단순히 Bean을 등록하는 것과는 조금 다른 절차가 필요합니다.
먼저 @Configuration class 를 하나 생성해줍니다.
@Configuration
public class PartnersConfiguration {
}
일반적으로 Bean 등록을 위해서는 @Bean Annotation을 쓰고 메서드를 만들면 되지만, ArgumentResolver 를 등록하기 위해서는
SpringMVC 가 해당 설정을 가져갈 수 있게 WebMvcConfigurer 인터페이스를 implement 해줍니다.
@Configuration
public class PartnersConfiguration implements WebMvcConfigurer {
}
그리고, intelliJ Mac 버전 기준 단축키 cmd+n 을 입력해주면, 몇 가지 메뉴가 나오는데 그 중 "implement method..."를 선택해줍니다.
그러면 수 많은 메서드를 implement 할 수 있는데, 그 중 "addArgumentResolver" 를 선택해줍니다.
해당 메서드를 선택하면, 자동완성으로 아래와 같이 코드가 나타납니다.
@Configuration
public class PartnersConfiguration implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
WebMvcConfigurer.super.addArgumentResolvers(resolvers);
}
}
기존에 작성되어 있는 문장은 지워주셔도 무방하고, 파라미터로 생성된 resolvers에 새로 만든 ArgumentResolver 객체를 추가해주면 됩니다. 새로 만든 ArgumentResolver 가 필요하기 때문에, 의존관계 주입을 받아주도록 합니다.
@Configuration
public class PartnersConfiguration implements WebMvcConfigurer {
private final LoginPartnerArgumentResolver loginPartnerArgumentResolver;
public PartnersConfiguration(LoginPartnerArgumentResolver loginPartnerArgumentResolver) {
this.loginPartnerArgumentResolver = loginPartnerArgumentResolver;
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
WebMvcConfigurer.super.addArgumentResolvers(resolvers);
}
}
@Configuration도 Application 로딩 시점에 spring 에서 의존관계 주입을 해주니 생성자 주입을 사용하도록 합니다.
그럼 새로 만든 ArgumentResolver 도 준비가 되었으니, spring 이 사용할 수 있도록 추가해보도록 하겠습니다.
오버라이딩한 메서드의 내용을 아래처럼 수정합니다.
@Configuration
public class PartnersConfiguration implements WebMvcConfigurer {
private final LoginPartnerArgumentResolver loginPartnerArgumentResolver;
public PartnersConfiguration(LoginPartnerArgumentResolver loginPartnerArgumentResolver) {
this.loginPartnerArgumentResolver = loginPartnerArgumentResolver;
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
// 기존에 자동으로 있던 line 은 삭제해도 무방합니다.
// WebMvcConfigurer.super.addArgumentResolvers(resolvers);
resolvers.add(loginPartnerArgumentResolver);
}
}
파라미터에 생긴 resolvers list 에 add 를 해주는 것으로 spring 이 사용하는 ArgumentResolver 들에 새로 만든 LoginPartnerArgumentResolver도 포함해주게 됩니다.
5. ArgumentResolver 를 적용한 Controller 수정
ArgumentResolver 를 생성했으니, 기존에 JWT token 을 parsing 하고 판매자 정보를 추출하는건 이제 ArgumentResolver 의 역할이 되었습니다. 기존에 Controller 에 있던 로직을 제거하고, ArgumentResolver 가 동작할 수 있도록 파라미터를 수정해줍니다.
@PostMapping("/products")
public ResponseEntity<ProductResponse> createProduct(@LoginPartner AuthorizedPartner partner,
@RequestBody ProductRequest productRequest) {
ProductDto productDto = ProductRequest.toDto(productRequest, partner.getId());
ProductResponse productResponse = productService.saveProduct(partner.getId(), productDto);
return ResponseEntity.created(URI.create("/products/"+productResponse.getId())).body(productResponse);
}
이렇게 하면 앞으로 판매자 인증이 필요할 때, 별다른 로직의 추가 없이 파라미터에 @LoginPartner AuthorizedPartner partner 파라미터만 추가해주면 되게 되었습니다.
'Spring' 카테고리의 다른 글
[Spring] Spring AOP - 관심사 분리(부가기능과 핵심기능) (0) | 2024.02.26 |
---|---|
[Design Pattern] 프록시 패턴 & 데코레이터 패턴 (0) | 2024.02.15 |
[Design Pattern] 템플릿/콜백 패턴(Template/Callback Pattern) (0) | 2023.12.08 |
[Spring Rest Docs] 테스트 코드를 통한 API 문서 만들기 (0) | 2023.10.20 |
스프링의 객체지향 (1) | 2023.03.06 |
댓글