[Java] ThreadLocal

애이용·2021년 5월 10일
0

Java

목록 보기
7/8
post-thumbnail

프로젝트를 진행하던 중, AOP를 이용해 Access Token을 검증하는 로직을 구현했다. (스프링 시큐리티없이)

AuthAspect 클래스 (Around Advice)

    @Around("@annotation(Auth)")
    public Object around(final ProceedingJoinPoint pjp) throws Throwable {
        try {
            String token = httpServletRequest.getHeader(AUTHORIZATION);

            JwtPayload payload = jwtService.getPayload(token);

            Optional<UserDto> user = userMapper.findByUserId(payload.getId());

            if (user.isEmpty()) {
                throw new UserNotFoundException("존재하지 않는 유저입니다.");
            }

            return pjp.proceed(new Object[] { user.get().getId() });
        } catch (SignatureException | ExpiredJwtException | MalformedJwtException | UnsupportedJwtException e) {
            throw new IllegalArgumentException(e.getMessage());
        }
    }

초기에 이렇게 구현해보았다. (쌩 초반 코드)
하지만 Controller에서 파라미터가 많아지면 각 메서드마다 해결해야해서 좋은 방법이 없을까 고민했다.
그때 팀원분이 ThreadLocal을 이용한 방법을 알려주셨다!
코드를 보니까 Spring Security를 이용했을 때 SecurityContextHolder가 떠올랐다. (이건 맨 밑 참고)

소스 전체에서 사용할 수 있는 전역 변수처럼 데이터를 할당하고 싶을 때 쓰는 클래스라 할 수 있다.
ThreadLocal 클래스는 하나의 쓰레드에 의해 읽고 쓸 수 있는 변수를 생성한다.
2개의 쓰레드가 같은 코드를 실행할 때 각각의 쓰레드는 서로의 ThreadLocal 변수를 확인할 수 없다.
👉 Thread 단위 Local 변수를 할당하는 기능이다.

ThreadLocal

한 스레드에서 실행되는 코드가 동일한 객체를 사용할 수 있도록 해주기 때문에 관련된 코드에서 파라미터를 사용하지 않고 객체를 각자가 가져다 쓸 때 사용된다.
(출처 : 최범균님 블로그)

주요 용도

  • 사용자 인증정보 전파 - Spring Security에서는 ThreadLocal을 이용해서 사용자 인증 정보를 전파한다.
  • 트랜잭션 컨텍스트 전파 - 트랜잭션 매니저는 트랜잭션 컨텍스트를 전파하는 데 ThreadLocal을 사용한다.
  • 쓰레드에 안전해야 하는 데이터 보관

ThreadLocal은 메모리 누수의 주범이 될 수 있어 주의해서 사용해야한다.

사용법(과정)

  1. ThreadLocal 객체를 생성한다.
  2. ThreadLocal.set() 메서드를 이용해서 현재 쓰레드의 로컬 변수에 값을 저장한다.
  3. ThreadLocal.get() 메서드를 이용해서 현재 쓰레드의 로컬 변수 값을 읽어온다.
  4. ThreadLocal.remove() 메서드를 이용해서 현재 쓰레드의 로컬 변수 값을 삭제한다.

기본적인 사용 방법

// 현재 쓰레드와 관련된 로컬 변수를 하나 생성한다.
ThreadLocal<UserInfo> local = new ThreadLocal<UserInfo>();
// 로컬 변수에 값 할당
local.set(currentUser);
// 이후 실행되는 코드는 쓰레드 로컬 변수 값을 사용
UserInfo userInfo = local.get();

그림으로 이해

(출처 : 최범균님 블로그)

  • 2 - A.a() 메서드에서 현재 쓰레드의 로컬 변수에 Date 객체를 저장한다.
  • 4 - B.b() 메서드에서 현재 쓰레드의 로컬 변수에 저장된 Date 객체를 읽어와 사용한다.
  • 6 - C.c() 메서드에서 현재 쓰레드의 로컬 변수에 저장된 Date 객체를 읽어와 사용한다.
  • 9 - A.a() 메서드에서 현재 쓰레드의 로컬 변수를 삭제한다.

쓰레드 풀 환경에서 ThreadLocal을 사용하는 경우 ThreadLocal 변수에 보관된 데이터의 사용이 끝나면 반드시 해당 데이터를 삭제해 주어야 한다. 그렇지 않을 경우 재사용되는 쓰레드가 올바르지 않은 데이터를 참조할 수 있다고 한다!

활용

public class UserContext {

    public static final ThreadLocal<JwtPayload> USER_CONTEXT = new ThreadLocal<>();

    public static Long getCurrentUserId() {
        if (UserContext.USER_CONTEXT.get() != null) {
            return UserContext.USER_CONTEXT.get().getId();
        }

        throw new Exception("null");
    }
}

set
토큰을 통해 사용자 인증 과정을 거친 후,
UserContext에 현재 user의 id를 저장

UserContext.USER_CONTEXT.set(new JwtPayload(user.getId()));

get

Long userId = UserContext.getCurrentUserId();

이상 ThreadLocal을 이용한 인증된 사용자 정보 가져오기였다!


Spring Security ThreadLocal

cf) Spring Security에서는 ThreadLocal을 이용해서 사용자 인증 정보를 전파한다.
👉 어떻게 구현된 건지 궁금해서 스프링 시큐리티 관련해서 찾아보았다.

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User)authentication.getPrincipal();

이렇게 인증된 사용자를 가져오니
SecurityContextHolder 클래스를 찾아봤다.

해당 클래스에서 초기화 메서드를 호출할 때

    if (!StringUtils.hasText(strategyName)) {
    // Set default
    strategyName = MODE_THREADLOCAL;
    }
    if (strategyName.equals(MODE_THREADLOCAL)) {
    strategy = new ThreadLocalSecurityContextHolderStrategy();
    }
    else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
    strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
    }
    else if (strategyName.equals(MODE_GLOBAL)) {
    strategy = new GlobalSecurityContextHolderStrategy();
    }
    ...

이런 식으로 ThreadLocal 객체를 생성했다.

ThreadLocalSecurityContextHolderStrategy 클래스

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

	private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

	@Override
	public void clearContext() {
		contextHolder.remove();
	}

	@Override
	public SecurityContext getContext() {
		SecurityContext ctx = contextHolder.get();
		if (ctx == null) {
			ctx = createEmptyContext();
			contextHolder.set(ctx);
		}
		return ctx;
	}

	@Override
	public void setContext(SecurityContext context) {
		Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
		contextHolder.set(context);
	}

	@Override
	public SecurityContext createEmptyContext() {
		return new SecurityContextImpl();
	}

}

위에서 작성한 UserContext 클래스와 비슷한 게 보인다. 매우 신기
스프링 시큐리티없이 구현하니까 스프링 시큐리티를 알아가는 느낌

참고 링크

profile
로그를 남기자 〰️

0개의 댓글