UserContextHolder는 일반적으로 사용자의 컨텍스트(Context)를 애플리케이션의 현재 요청 스레드에 안전하게 저장하고 관리하기 위한 클래스로 사용됩니다. 이는 마이크로서비스 아키텍처(MSA)에서 각 요청마다 고유한 사용자 정보를 유지하는 데 중요한 역할을 합니다.


1. UserContextHolder의 필요성

왜 필요한가?

  1. 요청별 사용자 정보 관리:

    • 클라이언트의 각 요청은 고유한 사용자 정보를 포함합니다(예: JWT 토큰, 세션 정보, 권한).
    • 마이크로서비스 환경에서는 각 서비스가 분리되어 있어 이러한 정보를 모든 요청에 전달하고 처리해야 합니다.
  2. 스레드 안전(Thread-Safe) 컨텍스트:

    • 요청은 서버에서 처리되는 동안 동일한 스레드에서만 안전하게 사용자 정보를 참조해야 합니다.
    • 사용자 정보를 ThreadLocal을 통해 저장하고 관리하여, 요청이 처리되는 동안 스레드 간 데이터 혼란을 방지.
  3. 로깅, 트랜잭션, 권한 검증 등:

    • 사용자 컨텍스트는 로깅, 인증/인가, 분산 추적 등에 활용됩니다.

2. UserContextHolder 구성 요소

1. UserContext

사용자의 요청 정보를 저장하기 위한 데이터 객체로, 요청별로 필요한 데이터를 담습니다.

public class UserContext {
    private String correlationId; // 요청 간 상관관계 ID
    private String authToken;     // 인증 토큰
    private String userId;        // 사용자 ID
    private String organizationId; // 조직 ID

    // Getter & Setter
    public String getCorrelationId() {
        return correlationId;
    }
    public void setCorrelationId(String correlationId) {
        this.correlationId = correlationId;
    }
    public String getAuthToken() {
        return authToken;
    }
    public void setAuthToken(String authToken) {
        this.authToken = authToken;
    }
    public String getUserId() {
        return userId;
    }
    public void setUserId(String userId) {
        this.userId = userId;
    }
    public String getOrganizationId() {
        return organizationId;
    }
    public void setOrganizationId(String organizationId) {
        this.organizationId = organizationId;
    }
}

2. UserContextHolder

ThreadLocal을 사용하여 사용자 컨텍스트를 저장하고 관리합니다.

public class UserContextHolder {
    private static final ThreadLocal<UserContext> userContext = new ThreadLocal<>();

    public static final UserContext getContext() {
        UserContext context = userContext.get();
        if (context == null) {
            context = new UserContext(); // 새로운 컨텍스트 생성
            userContext.set(context);
        }
        return context;
    }

    public static final void setContext(UserContext context) {
        userContext.set(context);
    }

    public static final void clearContext() {
        userContext.remove(); // 요청 종료 시 컨텍스트 제거
    }
}

3. UserContextHolder의 활용

1. 필터에서 사용자 정보 설정

요청(Request)에서 사용자 정보를 추출해 UserContext에 저장합니다. Spring Security나 인증 토큰을 활용해 데이터를 가져올 수 있습니다.

@Component
public class UserContextFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;

        // 사용자 컨텍스트 설정
        UserContext context = UserContextHolder.getContext();
        context.setCorrelationId(httpRequest.getHeader("X-Correlation-Id"));
        context.setAuthToken(httpRequest.getHeader("Authorization"));
        context.setUserId(httpRequest.getHeader("User-Id"));
        context.setOrganizationId(httpRequest.getHeader("Organization-Id"));

        chain.doFilter(request, response);

        // 요청 종료 시 컨텍스트 제거
        UserContextHolder.clearContext();
    }
}

2. 서비스 레이어에서 컨텍스트 활용

필터를 통해 설정된 사용자 정보를 서비스 레이어에서 간단히 가져와 사용할 수 있습니다.

@Service
public class UserService {

    public void performAction() {
        UserContext context = UserContextHolder.getContext();

        System.out.println("Correlation ID: " + context.getCorrelationId());
        System.out.println("User ID: " + context.getUserId());
        // 로직 처리
    }
}

3. 로깅 및 분산 추적

UserContextHolder를 사용해 로깅 및 분산 추적에서 중요한 상관관계 ID를 유지할 수 있습니다.

  • 예: 상관관계 ID(Correlation ID) 로깅:

    Logger logger = LoggerFactory.getLogger(UserService.class);
    
    public void logRequest() {
        String correlationId = UserContextHolder.getContext().getCorrelationId();
        logger.info("Processing request with Correlation ID: {}", correlationId);
    }
  • 예: OpenTelemetry 연동:
    상관관계 ID를 OpenTelemetry와 같은 분산 추적 도구에 연결할 수 있습니다.


4. MSA에서 UserContextHolder의 중요성

  1. 인증 및 권한 관리:

    • 요청마다 사용자 정보를 유지하므로 서비스 간 인증과 권한 검증을 쉽게 처리.
  2. 분산 추적(Distributed Tracing):

    • 상관관계 ID를 통해 여러 서비스 간의 요청 흐름을 추적.
  3. 공통 정보 관리:

    • 사용자 ID, 조직 ID 등 요청별 공통 정보를 서비스 전반에 쉽게 전달.
  4. 스레드 안정성:

    • ThreadLocal을 사용해 요청 스레드 간 데이터 충돌 방지.

5. 주의 사항

  1. ThreadLocal 사용 시 메모리 누수 방지:

    • 요청이 끝난 후 반드시 UserContextHolder.clearContext()를 호출하여 컨텍스트를 제거해야 합니다.
    • 누락 시 메모리 누수(memory leak) 위험이 있습니다.
  2. 비동기 환경에서의 한계:

    • ThreadLocal은 스레드 간 데이터를 안전하게 관리하지만, 비동기 호출(예: CompletableFuture)에서는 동일한 스레드가 사용되지 않을 수 있습니다.
    • 해결 방법:
      • Spring의 TaskDecorator를 사용해 비동기 작업에도 ThreadLocal 데이터를 전달.
      • 예:
        @Bean
        public TaskDecorator taskDecorator() {
            return runnable -> {
                UserContext context = UserContextHolder.getContext();
                return () -> {
                    try {
                        UserContextHolder.setContext(context);
                        runnable.run();
                    } finally {
                        UserContextHolder.clearContext();
                    }
                };
            };
        }
  3. 보안 데이터 관리:

    • 인증 토큰 같은 민감한 정보를 UserContext에 저장할 때는 암호화 또는 마스킹 처리를 고려.

결론

UserContextHolder는 MSA 환경에서 사용자와 요청별로 고유한 컨텍스트 정보를 안전하게 관리하기 위한 필수 도구입니다. 필터를 통해 요청마다 정보를 설정하고, 서비스 레이어에서 이를 쉽게 사용할 수 있습니다. 또한 분산 추적과 로깅에 유용하며, 스레드 안정성을 보장하지만, 비동기 작업 시 주의가 필요합니다.

+ Recent posts