Ch2 - MSA 기반 물류 시스템 개발 프로젝트
배운 것
이번 프로젝트를 진행하면서 MSA 환경에서 서비스를 나눠서 개발을 진행했다. 그 중 사용자 인증과 권한 관리를 효율적으로 처리하기 위해 JWT(Json Web Token)를 활용했다.
유저 서비스에서 사용자 인증이 이루어지면, 게이트웨이 서비스에서 토큰 검증을 수행하고, 유효한 토큰에 포함된 사용자 정보를 헤더에 추가하여 각 서비스로 전달하며, 이후 각 서비스는 이 헤더 값을 활용하여 사용자 정보를 확인하고 API를 처리하도록 설계했다.
각 서비스에서는 넘어온 사용자 정보를 어떻게 저장하고 활용할지 고민을 하던 중 Spring Security를 사용하는 방법을 먼저 고려했다. 하지만 Security는 모든 요청에 대해 필터 체인을 거치고 컨텍스트를 관리해야 하는 구조라서 추가적인 부하와 복잡성을 초래할 수 있다는 단점이 있었다. 특히 간단한 사용자 정보 저장과 접근만 필요했던 상황에서는 조금 무겁다는 생각이 들었다.
대신 ThreadLocal을 활용해 사용자 정보를 저장하고 관리하는 방안을 선택했다. ThreadLocal은 각 요청 스레드에서 독립적인 저장소를 제공하므로, 사용자 정보를 쉽게 저장하고 요청 처리 흐름에서 이를 사용할 수 있었다.
문제 상황
- API 요청마다 헤더로 사용자 정보를 전달받아야 하는 상황에서 Spring Security를 도입하지 않고 사용자 정보를 관리할 방법 필요
- 각 요청마다 사용자 정보를 안전하게 관리하며, 이를 컨트롤러나 서비스 레이어에서 쉽게 사용할 수 있는 구조를 고민
해결 방법
- Filter를 통해 모든 요청에서 사용자 정보를 추출.
- 사용자 정보를 AuthContext라는 컨텍스트 객체에 저장.
- 요청 종료 후 ThreadLocal 데이터를 초기화하여 메모리 누수를 방지.
- 컨트롤러 및 서비스에서 AuthContext를 통해 간단하게 사용자 정보를 사용.
코드 구현
1. Filter를 통해 사용자 정보 추출
@Component
public class AuthHeaderFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 헤더에서 사용자 정보 추출
String userId = httpRequest.getHeader("X-USER-ID");
String role = httpRequest.getHeader("X-USER-ROLE");
if (userId != null && role != null) {
// 사용자 정보를 ThreadLocal에 저장
AuthContext.set(new AuthHeaderInfo(Long.valueOf(userId), role));
}
// 필터 체인 실행
chain.doFilter(request, response);
// 요청 종료 후 초기화
AuthContext.clear();
}
}
2. ThreadLocal을 사용해 사용자 정보 관리
public class AuthContext {
private static final ThreadLocal<AuthHeaderInfo> CONTEXT = new ThreadLocal<>();
public static void set(AuthHeaderInfo info) {
CONTEXT.set(info);
}
public static AuthHeaderInfo get() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
3. 사용자 정보 객체 정의
public record AuthHeaderInfo(
Long userId,
String role
) {
}
4. Controller에서 사용자 정보 활용
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping
public ResponseEntity<String> getOrderDetails() {
AuthHeaderInfo authInfo = AuthContext.get();
if (authInfo == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Unauthorized Access");
}
return ResponseEntity.ok("User ID: " + authInfo.userId() + ", Role: " + authInfo.role());
}
}
개선 사항
ThreadLocal을 사용해 AuthContext를 통해 사용자 정보를 관리하는 방법은 간단하고 유연하지만, 코드가 분산되면 어디서든 AuthContext를 호출할 수 있다는 단점이 있었다. 이로 인해 코드의 의존성이 높아지고, 예상치 못한 장소에서 AuthContext가 사용될 가능성이 있었다.
이 문제를 개선하고 더 일관된 방식으로 사용자 정보를 사용할 수 있도록 하기 위해, Spring Security의 @AuthenticationPrincipal처럼 컨트롤러 메서드에서 사용자 정보를 간단히 가져올 수 있는 방식을 도입했다. 이를 위해 다음과 같은 단계를 구현했다.
1. 사용자 정의 어노테이션 생성
사용자 정보를 매개변수로 간편하게 주입받기 위해 @AuthHeader라는 사용자 정의 어노테이션을 생성
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthHeader {
}
2. Argument Resolver 구현
이 어노테이션이 사용된 매개변수에 대해 AuthContext의 정보를 주입하기 위해, HandlerMethodArgumentResolver를 구현
public class AuthHeaderArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
// @AuthHeader 어노테이션이 있는 경우 지원
return parameter.hasParameterAnnotation(AuthHeader.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// ThreadLocal에서 AuthHeaderInfo 가져오기
AuthHeaderInfo authHeaderInfo = AuthContext.get();
if (authHeaderInfo == null) {
throw new throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Authentication information is missing.");
}
return authHeaderInfo;
}
}
3. Argument Resolver 등록
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthHeaderArgumentResolver());
}
}
4. Controller에서 사용
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping
public ResponseEntity<String> getOrderDetails(@AuthHeader AuthHeaderInfo authInfo) {
return ResponseEntity.ok("User ID: " + authInfo.userId() + ", Role: " + authInfo.role());
}
}
개선점
- 코드 가독성 향상: 컨트롤러 메서드에서 사용자 정보를 명시적으로 선언할 수 있어 코드의 의도가 명확해졌다.
- 의존성 관리: AuthContext를 직접 호출하지 않고, 일관된 방식으로 사용자 정보를 주입받을 수 있게 됐다.
- 확장성: 향후 인증 로직이나 사용자 정보의 저장 방식을 변경해야 할 때, Argument Resolver만 수정하면 된다.
'부트캠프 > 단기심화2기 TIL' 카테고리의 다른 글
[TIL] - MSA 프로젝트 구조 복습 및 생각 정리 (1) | 2024.11.26 |
---|---|
[TIL] - Spring Cloud Gateway 적용하기 (0) | 2024.11.25 |
[TIL] - Microservice Architecture 프로젝트 만들기 (2) | 2024.11.22 |
[TIL] - Microservice Architecture 기본 학습 (0) | 2024.11.21 |
[TIL] - AI 검증 비즈니스 프로젝트 (프로젝트 발표 및 피드백 정리) (2) | 2024.11.19 |