본문 바로가기
개발&프로그래밍

[JAVA] ThreadLocal 제대로 사용하기

by 재아군 2024. 11. 17.

[JAVA] ThreadLocal 제대로 사용하기

ThreadLocal은 각 스레드가 독립적인 변수 복사본을 가질 수 있게 해주는 Java의 강력한 기능이다. 이를 통해 멀티스레드 환경에서 발생할 수 있는 동시성 문제를 해결할 수 있다.

ThreadLocal 기본 개념

ThreadLocal은 각 스레드마다 별도의 저장소를 제공하여 스레드 안전성을 보장한다.

public class UserContext {
    private static final ThreadLocal<User> userHolder = new ThreadLocal<>();

    public static void setUser(User user) {
        userHolder.set(user);
    }

    public static User getUser() {
        return userHolder.get();
    }

    public static void clear() {
        userHolder.remove();  // 중요: 메모리 누수 방지
    }
}

동시성 문제 해결

일반적인 동시성 문제

// 동시성 문제가 있는 코드
public class UserService {
    private User currentUser;  // 여러 스레드가 공유하는 변수

    public void setCurrentUser(User user) {
        this.currentUser = user;  // 동시성 문제 발생 가능
    }
}

ThreadLocal을 사용한 해결

public class UserService {
    private static final ThreadLocal<User> currentUser = new ThreadLocal<>();

    public void setCurrentUser(User user) {
        currentUser.set(user);  // 스레드별로 독립된 저장소 사용
    }

    public void processUser() {
        try {
            User user = currentUser.get();
            // 비즈니스 로직 처리
        } finally {
            currentUser.remove();  // 반드시 cleanup 수행
        }
    }
}

메모리 누수 방지

ThreadLocal 사용 시 주의해야 할 가장 중요한 점은 메모리 누수 방지다.

잘못된 사용 예

public class BadExample {
    private static ThreadLocal<HeavyObject> threadLocal = new ThreadLocal<>();

    public void process() {
        threadLocal.set(new HeavyObject());
        // 처리 로직
        // remove() 호출 누락 -> 메모리 누수 발생
    }
}

올바른 사용 예

public class GoodExample {
    private static ThreadLocal<HeavyObject> threadLocal = new ThreadLocal<>();

    public void process() {
        try {
            threadLocal.set(new HeavyObject());
            // 처리 로직
        } finally {
            threadLocal.remove();  // 반드시 cleanup
        }
    }
}

Spring에서의 활용

Spring Framework에서는 ThreadLocal을 다양한 용도로 활용한다.

트랜잭션 컨텍스트 관리

@Service
public class TransactionService {
    @Autowired
    private TransactionTemplate transactionTemplate;

    public void processInTransaction() {
        transactionTemplate.execute(status -> {
            // 트랜잭션 범위 내 작업
            // Spring이 내부적으로 ThreadLocal 사용
            return null;
        });
    }
}

사용자 인증 정보 관리

@Component
public class SecurityContextHolder {
    private static final ThreadLocal<SecurityContext> contextHolder = 
        new ThreadLocal<>();

    public static void setContext(SecurityContext context) {
        contextHolder.set(context);
    }

    public static SecurityContext getContext() {
        return contextHolder.get();
    }

    public static void clearContext() {
        contextHolder.remove();
    }
}

실무 사용 사례

1. MDC (Mapped Diagnostic Context)

public class RequestFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        try {
            MDC.put("requestId", generateRequestId());
            MDC.put("userId", getCurrentUserId());
            chain.doFilter(request, response);
        } finally {
            MDC.clear();  // ThreadLocal cleanup
        }
    }
}

2. 사용자 컨텍스트 관리

@Component
public class UserContextFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        try {
            String token = extractToken(req);
            User user = authenticateUser(token);
            UserContext.setUser(user);
            chain.doFilter(request, response);
        } finally {
            UserContext.clear();
        }
    }
}

 

성능과 메모리 고려사항

 

초기값 설정

ThreadLocal<User> userThreadLocal = ThreadLocal.withInitial(() -> new User("default"));

 

메모리 사용량 모니터링

public class ThreadLocalMonitor {
    private static final Set<ThreadLocal<?>> threadLocals = 
        Collections.newSetFromMap(new WeakHashMap<>());

    public static void register(ThreadLocal<?> threadLocal) {
        threadLocals.add(threadLocal);
    }

    public static void printStats() {
        System.out.println("Active ThreadLocals: " + threadLocals.size());
    }
}

 

모범 사례 정리

  1. 항상 try-finally 블록 사용
    • ThreadLocal 변수는 반드시 정리
    • finally 블록에서 remove() 호출
  2. 스레드 풀 사용 시 주의
    • 작업 완료 후 ThreadLocal 정리
    • 스레드 재사용 시 이전 데이터 유출 방지
  3. 초기화 표준화
    • withInitial() 메서드 활용
    • null 체크 로직 간소화

 

댓글