ThreadLocal

slee2·2022년 3월 4일
0

소개

쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 말한다. 쉽게 이야기해서 물건 보관 창구. 여러 사람이 같은 물건 보관 창구를 사용하더라도 직원은 사용자를 인식해서 사용자별로 확실하게 물건을 구분.

일반 변수

쓰레드 로컬

예제 코드

FieldService에서 밑줄 친 부분만 바꾸고 나머지는 메서드만 조금씩 바꾸면 된다.

테스트

A는 userA B는 userB로 각각 저장된 것을 확인할 수 있다.

쓰레드 로컬 동기화 - 개발

FieldLogTrace에서 발생했던 동시성 문제를 ThreadLocal을 사용해서 해결하려고 한다.

package hello.advanced.trace.logtrace;

import hello.advanced.trace.TraceId;
import hello.advanced.trace.TraceStatus;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ThreadLocalLogTrace implements LogTrace{

    private static final String START_PREFIX = "-->";
    private static final String COMPLETE_PREFIX = "<--";
    private static final String EX_PREFIX = "<X-";

//    private TraceId traceIdHolder; //traceId 동기화, 동시성 이슈 발생
    private ThreadLocal<TraceId> traceIdHolder = new ThreadLocal<>();

    @Override
    public TraceStatus begin(String message) {
        syncTraceId();
        TraceId traceId = traceIdHolder.get();
        Long startTimeMs = System.currentTimeMillis();
        log.info("[{}] {}{}", traceId.getId(),
                addSpace(START_PREFIX, traceId.getLevel()), message);
        return new TraceStatus(traceId, startTimeMs, message);
    }

    private void syncTraceId() {
        TraceId traceId = traceIdHolder.get();
        if (traceId == null) {
            traceIdHolder.set(new TraceId());
        } else {
            traceIdHolder.set(traceId.createNextId());
        }
    }

    @Override
    public void end(TraceStatus status) {
        complete(status, null);
    }

    @Override
    public void exception(TraceStatus status, Exception e) {
        complete(status, e);
    }

    private void complete(TraceStatus status, Exception e) {
        Long stopTimeMs = System.currentTimeMillis();
        long resultTimeMs = stopTimeMs - status.getStartTimeMs();
        TraceId traceId = status.getTraceId();
        if (e == null) {
            log.info("[{}] {}{} time={}ms", traceId.getId(),
                    addSpace(COMPLETE_PREFIX, traceId.getLevel()), status.getMessage(),
                    resultTimeMs);
        } else {
            log.info("[{}] {}{} time={}ms ex={}", traceId.getId(),
                    addSpace(EX_PREFIX, traceId.getLevel()), status.getMessage(), resultTimeMs,
                    e.toString());
        }

        releaseTraceId();
    }

    private void releaseTraceId() {
        TraceId traceId = traceIdHolder.get();
        if (traceId.isFirstLevel()) {
            traceIdHolder.remove(); //destroy
        } else {
            traceIdHolder.set(traceId.createPreviousId());
        }
    }

    private static String addSpace(String prefix, int level) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < level; i++) {
            sb.append( (i == level - 1) ? "|" + prefix : "|   ");
        }
        return sb.toString();
    }

}

ThreadLocal.remove()
쓰레드 로컬을 모두 사용하고 나면 꼭 ThreadLocal.remove()를 호출해서 쓰레드 로컬에 저장된 값을 제거해줘야 한다.

테스트 생략

쓰레드 로컬 동기화 - 적용

컨트롤러는 결국 logTrace를 불러오기 때문에 이 logTrace에 들어간 부분을 ThreadLocalLogTrace()로 바꿔주면 적용이 끝난다.
자동으로 컨트롤러에서 logTrace -> ThreadLocalLogTrace로 된다.

이렇게 연속으로 호출해도 레벨이 안꼬이고 잘 동작하는 것을 확인할 수 있다.

쓰레드 로컬 - 주의사항

쓰레드 로컬의 값을 사용 후 제거하지 않고 두면 WAS(톰캣)처럼 쓰레드 풀을 사용하는 경우에 심각한 문제가 발생할 수 있다.

사용자 A가 호출할때, 그림이다.
이후에 요청이 종료되면,

이렇게 된다.
WAS는 사용이 끝난 thread-A를 쓰레드 풀로 반환한다.
쓰레드를 생성하는 비용이 비싸기 때문에 쓰레드를 제거하지 않고, 보통 쓰레드 풀을 통해서 재사용한다.
thread-A는 풀에 아직 살아있고, 사용자A도 함께 살아있게 된다.

그 후에 B에서 조회 요청이 들어오면,
WAS는 쓰레드 풀에서 쓰레드를 하나 조회한다.
쓰레드 thread-A가 할당되었다.(다른 쓰레드가 할당될 수도 있다.)
그럼 조회하는 요청이므로 thread-A가 반환된다.
사용자A를 조회하게 된다.

그렇기 때문에 요청이 끝나면 쓰레드 로컬의 값을 ThreadLocal.remove()를 통해서 꼭 제거해야 한다.

0개의 댓글