[스프링 핵심원리 - 고급편 ] ThreadLocal (쓰레드 로컬) 동기화 개발

JEONG SUJIN·2023년 2월 13일
0

스프링부트 기본

목록 보기
15/15

FieldLogTrace 에서 발생했던 동시성 문제를 ThreadLocal로 해결해보자!
TraceId traceIdHolder 필드를 쓰레드 로컬을 사용하도록

ThreadLocal<TraceId> 

traceIdHolder로 변경하면된다.

필드 대신에 쓰레드 로컬을 사용해서 데이터를 동기화하는 ThreadLoclLogTrace를 새로 생성

ThreadLocalLogTrace.java

ThreadLocal데이터타입을 TraceId 타입으로 저장
traceIdHolder을 get()으로 가지고 오고,

package study.advanced.trace.logtrace;

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

@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();
		//꺼낸 값이 null이면, traceIdHolder 에 새로운 값을 넣어주고
		if(traceId == null) {
			traceIdHolder.set(new TraceId());
		}else {
// null아닐때는 기존아이디에 증가			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.getStartITmeMs();
		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() {
         //traceIdHolder에서 traceId 을 꺼낸다.
		 TraceId traceId = traceIdHolder.get();
		
        //traceId가 첫번째 레벨이면
		if(traceId.isFirstLevel()) {
        //
			traceIdHolder.remove(); // 쓰레드전용 보관소만 제거 
		}else {
			
        //이전아이디를 넣어준다.
        traceIdHolder.set(traceId.createPreviousId());
		}
		
	}


	private Object 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()를 호출해서 쓰레드 로컬에 저장된 값을 제거해주어야 한다.

쉽게 이야기해서 다음의 마지막 로그를 출력하고 나면 쓰레드 로컬의 값을 제거해야 한다.


테스트해보기

ThreadLocalLogTraceTest.java

테스트패키지에서 study.advanced.trace.logtrace에 ThreadLocalLogTraceTest 생성 후,

package study.advanced.trace.logtrace;

import org.junit.jupiter.api.Test;

import study.advanced.trace.TraceStatus;

public class ThreadLocalLogTraceTest {

	ThreadLocalLogTrace trace = new ThreadLocalLogTrace();
	
	@Test
	void begin_end_level2() {
		TraceStatus status1 = trace.begin("hello1");
		TraceStatus status2 = trace.begin("hello2");
		trace.end(status2);
		trace.end(status1);
	}
	
	
	@Test
	void begin_exception_level2() {
		TraceStatus status1 = trace.begin("hello1");
		TraceStatus status2 = trace.begin("hello2");
		trace.exception(status2, new IllegalStateException());
		trace.exception(status1, new IllegalStateException());
	}
}

쓰레드가 잘 동작하는걸 볼 수 있다.

profile
기록하기

0개의 댓글