로그 추적기 만들기

bw1611·2023년 9월 1일

로그 추적기란?


애플리케이션이 커지면 모니터링과 운영이 중요해지기 때문에 어떤 부분에서 병목이 발생하고, 어떤 부분에서 예외가 발생하는지 로그를 통해 확인하는 것이 중요하다.

  • 예시
[796bccd9] OrderController.request()
[796bccd9] |-->OrderService.orderItem()
[796bccd9] | |-->OrderRepository.save()
[796bccd9] | |<--OrderRepository.save() time=1004ms
[796bccd9] |<--OrderService.orderItem() time=1014ms
[796bccd9] OrderController.request() time=1016ms

예외 발생
[b7119f27] OrderController.request()
[b7119f27] |-->OrderService.orderItem()
[b7119f27] | |-->OrderRepository.save()
[b7119f27] | |<X-OrderRepository.save() time=0ms 
ex=java.lang.IllegalStateException: 예외 발생!
[b7119f27] |<X-OrderService.orderItem() time=10ms 
ex=java.lang.IllegalStateException: 예외 발생!
[b7119f27] OrderController.request() time=11ms 
ex=java.lang.IllegalStateException: 예외 발생!

📔 V0 버전


AOP에 들어가기 앞서 로그 추적기에 대해서 배운다.
우선 V0 버전을 소개하는데 현재는 로그추적기가 적용되어 있지 않은 상태이다.

@RestController
@RequiredArgsConstructor
public class OrderControllerV0 {

    private final OrderServiceV0 orderServiceV0;

    @GetMapping("/v0/request")
    public String request(String itemId){
        orderServiceV0.orderItem(itemId);
        return "ok";
    }
}
  • @RequiredArgsConstructor
    • final이 붙거나 @NotNull 이 붙은 필드의 생성자를 자동 생성해주는 롬복 어노테이션
@Service
@RequiredArgsConstructor // 파이널이 붙은 것을 생성자로 만들어준다.
public class OrderServiceV0 {

    private final OrderRepositoryV0 orderRepositoryV0;

    public void orderItem(String itemId){
        orderRepositoryV0.save(itemId);
    }
}
  • @Component
    • 선언적인 어노테이션으로 패키지 스캔 안에 이 어노테이션이 있다면 자동으로 빈으로 등록해준다.
    • 싱글톤 클래스 빈을 생성하는 어노테이션이다. @Scope를 통해 싱글톤이 아닌 빈을 생성할 수도 있다.

@Configuration의 차이
스프링에서 Bean을 수동으로 등록하기 위해서 설정하는 어노테이션이다. 외부 라이브러리 또는 내장 클래스를 Bean으로 등록하고자 할 경우 사용한다.
@Configuration안에는 Component가 선언되어 있다.

개발자가 직접 제어 가능 : @Component
개발자가 직접 제어 불가능 : @Configuration, @Bean

@Repository
@RequiredArgsConstructor
public class OrderRepositoryV0 {

    public void save(String itemId) {
        if (itemId.equals("ex")){
            throw new IllegalStateException("예외 발생!!");
        }
        sleep(1000);
    }

    private void sleep(int millis){
        try {
            Thread.sleep(millis);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}
  • itemId가 ex면 예외를 발생한다. 그렇지 않으면 sleep을 통해 1초를 쉬고 반환한다.

📔 V1 버전


public class TraceId {

    private String id;
    private int level;

    public TraceId() {
        this.id = createId();
        this.level = 0;
    }

    private TraceId(String id, int level){
        this.id = id;
        this.level = level;
    }

    private String createId() {
        return UUID.randomUUID().toString().substring(0, 8);
    }

    public TraceId createNextId() {
        return new TraceId(id, level + 1);
    }

    public TraceId createPreviousId() {
        return new TraceId(id, level - 1);
    }

    public boolean isFirstLevel() {
        return level == 0;
    }

    public String getId() {
        return id;
    }

    public int getLevel() {
        return level;
    }
}
  • UUID ( Universally Unique Identifier )
    • 범용 고유 식별자를 의미하며 중복이 되지 않는 유일한 값을 구성하고자 할때 주로 사용이 됩니다.
public class TraceStatus {

    private TraceId traceId;
    private Long startTimeMs;
    private String message;

    public TraceStatus(TraceId traceId, Long startTimeMs, String message) {
        this.traceId = traceId;
        this.startTimeMs = startTimeMs;
        this.message = message;
    }

    public TraceId getTraceId() {
        return traceId;
    }

    public Long getStartTimeMs() {
        return startTimeMs;
    }

    public String getMessage() {
        return message;
    }
}
  • 로그를 시작할 때의 상태 정보를 가지고 있으며, 상태 정보는 로그를 종료할 때 사용된다.
  • message는 시작, 종료시에 메시지를 사용하여 출력
  • startTimeMs는 로그 시작시간이다. 시작시간 기준 - 종료로 전체 수행 시간을 구한다.
@Slf4j
@Component
public class HelloTraceV1 {

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

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

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

    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());
        }
    }
    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();
    }
}
  • Begin으로 시작하여 message로 인자를 받고 어느 위치에서 시작됐는지 표현해준다.
  • 시작한 시점을 기준으로 LogStatus를 만들어서 return
  • 정식적으로 끝나면 End로 종료하며 소모된 시간을 계산한다.
  • 예외가 발생하면 Exception으로 종료
  • 종료, 예외는 겹치는 코드가 많기 때문에 Complete 메서드에서 조건문으로 처리
@RestController
@RequiredArgsConstructor
public class OrderControllerV1 {

    private final OrderServiceV1 orderServiceV1;
    private final HelloTraceV1 traceV1; // 컴포넌트가 있기 때문에 자동 스프링 빈으로 등록이 된다.

    @GetMapping("/v1/request")
    public String request(String itemId) {

        TraceStatus status = null;
        try {
            status = traceV1.begin("OrderController.request()");
            orderServiceV1.orderItem(itemId);
            traceV1.end(status);
            return "ok";
        } catch (Exception e) {
            traceV1.exception(status, e);
            throw e;
        }
    }
}
  • 예외가 터졌을 때를 가정하기 위해서 try, catch를 넣어준다.
@Service
@RequiredArgsConstructor // 파이널이 붙은 것을 생성자로 만들어준다.
public class OrderServiceV1 {

    private final OrderRepositoryV1 orderRepositoryV1;
    private final HelloTraceV1 traceV1;

    public void orderItem(String itemId){

        TraceStatus status = null;
        try {
            status = traceV1.begin("OrderServiceV1.request()");
            orderRepositoryV1.save(itemId);
            traceV1.end(status);
        } catch (Exception e) {
            traceV1.exception(status, e);
            throw e;
        }
    }
}
@Repository
@RequiredArgsConstructor
public class OrderRepositoryV1 {

    private final HelloTraceV1 traceV1;

    public void save(String itemId) {

        TraceStatus status = null;
        try {
            status = traceV1.begin("OrderRepositoryV1.request()");

            // 저장 로직
            if (itemId.equals("ex")){
                throw new IllegalStateException("예외 발생!!");
            }
            sleep(1000);
            traceV1.end(status);
        } catch (Exception e) {
            traceV1.exception(status, e);
            throw e;
        }
    }

    private void sleep(int millis){
        try {
            Thread.sleep(millis);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}
  • 실행 로그

📔 V2 버전


@Slf4j
@Component
public class HelloTraceV2 {
    private static final String START_PREFIX = "-->";
    private static final String COMPLETE_PREFIX = "<--";
    private static final String EX_PREFIX = "<X-";

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

    //V2에서 추가
    public TraceStatus beginSync(TraceId beforeTraceId, String message) {
        TraceId nextId = beforeTraceId.createNextId();
        Long startTimeMs = System.currentTimeMillis();
        log.info("[" + nextId.getId() + "] " + addSpace(START_PREFIX, nextId.getLevel()) + message);
        return new TraceStatus(nextId, startTimeMs, message);
    }

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

    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("[" + traceId.getId() + "] " + addSpace(COMPLETE_PREFIX, traceId.getLevel()) + status.getMessage() + " time=" + resultTimeMs + "ms");
        } else {
            log.info("[" + traceId.getId() + "] " + addSpace(EX_PREFIX, traceId.getLevel()) + status.getMessage() + " time=" + resultTimeMs + "ms" + " ex=" + e);
        }
    }
    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();
    }
}
  • beginSync 메서드를 추가한다.
    • 기존 TraceId에서 createNextId()를 통해 ID를 구한다.
    • 트랜잭션 ID는 기존과 같이 유지, level을 1 증가
    • beginSync를 호출할 때 직전 로그의 TraceId를 넘겨주어야 한다.
@RestController
@RequiredArgsConstructor
public class OrderControllerV2 {
    private final OrderServiceV2 orderService;
    private final HelloTraceV2 trace;

    @GetMapping("/v2/request")
    public String request(String itemId) {
        TraceStatus status = null;
        try {
            status = trace.begin("OrderController.request()");
            orderService.orderItem(status.getTraceId(), itemId);
            trace.end(status);
            return "ok";
        } catch (Exception e) {
            trace.exception(status, e);
            throw e;
        }
    }
}
@Service
@RequiredArgsConstructor // 파이널이 붙은 것을 생성자로 만들어준다.
public class OrderServiceV2 {
    private final OrderRepositoryV2 orderRepository;
    private final HelloTraceV2 trace;

    public void orderItem(TraceId traceId, String itemId) {
        TraceStatus status = null;
        try {
            status = trace.beginSync(traceId, "OrderService.orderItem()");
            orderRepository.save(status.getTraceId(), itemId);
            trace.end(status);
        } catch (Exception e) {
            trace.exception(status, e);
            throw e;
        }
    }
}
@Repository
@RequiredArgsConstructor
public class OrderRepositoryV2 {
    private final HelloTraceV2 trace;
    public void save(TraceId traceId, String itemId) {
        TraceStatus status = null;
        try {
            status = trace.beginSync(traceId, "OrderRepository.save()");
            //저장 로직
            if (itemId.equals("ex")) {
                throw new IllegalStateException("예외 발생!");
            }
            sleep(1000);
            trace.end(status);
        } catch (Exception e) {
            trace.exception(status, e);
            throw e;
        }
    }
    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • TraceId를 넘겨줌으로써 콘솔 log가 바뀐것을 확인할 수 있다.
  • level을 증가시켜 줬기 때문에 로그에 레벨이 나오는 것을 확인할 수 있다.

📔 V3 버전


public interface LogTrace {

    TraceStatus begin(String message);

    void end(TraceStatus status);

    void exception(TraceStatus status, Exception e);
}
  • 먼저 다형성을 활용하기 위해서 LogTrace 인터페이스 생성
  • 로그 추적기를 위한 최소한의 기능 begin, end, exception만 정의
@Slf4j
public class FieldLogTrace 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 동기화, 동시성 이슈 발생

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

    @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 syncTraceId() {
        if (traceIdHolder == null) {
            traceIdHolder = new TraceId();
        } else {
            traceIdHolder = traceIdHolder.createNextId();
        }
    }

    private void releaseTraceId() {
        if (traceIdHolder.isFirstLevel()) {
            traceIdHolder = null; //destroy
        } else {
            traceIdHolder = traceIdHolder.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();
    }
}
  • beginSync가 사라지고 TraceIdHolder를 선언한다.

    • 하지만 동시성 이슈가 발생하는 문제가 있음
  • sycnTraceId()를 만들어 traceIdHolder가 null인지 아닌지를 체크해준다.

    @Configuration
    public class LogTraceConfig {
    
       @Bean
       public LogTrace logTrace() {
           return new ThreadLocalLogTrace();
       }
    }

- @Configuration을 통해 @Bean이 붙어있는 것을 수동으로 등록해준다.

```java
@RestController
@RequiredArgsConstructor
public class OrderControllerV3 {
    private final OrderServiceV3 orderService;
    private final LogTrace trace;

    @GetMapping("/v3/request")
    public String request(String itemId) {
        TraceStatus status = null;
        try {
            status = trace.begin("OrderController.request()");
            orderService.orderItem(itemId);
            trace.end(status);
            return "ok";
        } catch (Exception e) {
            trace.exception(status, e);
            throw e;
        }
    }
}
@Service
@RequiredArgsConstructor // 파이널이 붙은 것을 생성자로 만들어준다.
public class OrderServiceV3 {
    private final OrderRepositoryV3 orderRepository;
    private final LogTrace trace;

    public void orderItem(String itemId) {
        TraceStatus status = null;
        try {
            status = trace.begin( "OrderService.orderItem()");
            orderRepository.save(itemId);
            trace.end(status);
        } catch (Exception e) {
            trace.exception(status, e);
            throw e;
        }
    }
}
@Repository
@RequiredArgsConstructor
public class OrderRepositoryV3 {
    private final LogTrace trace;
    public void save(String itemId) {
        TraceStatus status = null;
        try {
            status = trace.begin("OrderRepository.save()");
            //저장 로직
            if (itemId.equals("ex")) {
                throw new IllegalStateException("예외 발생!");
            }
            sleep(1000);
            trace.end(status);
        } catch (Exception e) {
            trace.exception(status, e);
            throw e;
        }
    }
    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • TraceId가 필요없어졌기 때문에 파라미터로 넘긴 것을 다 지워준다.
  • 하지만 현재 V3에는 큰 동시성 문제라는 큰 오류가 하나 있다. 동시성 문제는 무엇일까?
[nio-8080-exec-3] [f9ae6789] OrderController.request()
[nio-8080-exec-3] [f9ae6789] |-->OrderService.orderItem()
[nio-8080-exec-3] [f9ae6789] | |-->OrderRepository.save()
[nio-8080-exec-4] [f9ae6789] | | |-->OrderController.request()
[nio-8080-exec-4] [f9ae6789] | | | |-->OrderService.orderItem()
[nio-8080-exec-4] [f9ae6789] | | | | |-->OrderRepository.save()
[nio-8080-exec-3] [f9ae6789] | |<--OrderRepository.save() time=1005ms
[nio-8080-exec-3] [f9ae6789] |<--OrderService.orderItem() time=1005ms
[nio-8080-exec-3] [f9ae6789] OrderController.request() time=1005ms
[nio-8080-exec-4] [f9ae6789] | | | | |<--OrderRepository.save() 
time=1005ms
[nio-8080-exec-4] [f9ae6789] | | | |<--OrderService.orderItem() 
time=1005ms
[nio-8080-exec-4] [f9ae6789] | | |<--OrderController.request() time=1005ms

위와 같이 동시성 문제가 발생한다.
3번 쓰레드가 들어오고 바로 4번 쓰레드가 들어와 동시성 문제가 발생하게 되서 쓰레드를 구별할 수 없게 된다.

  • 문제가 발생하게 된 원인
    • FieldLogTrace는 싱글톤으로 등록된 스프링 빈이다. 이 객체에서는 애플리케이션이 딱 1개 존재한다는 뜻인데 하나만 있는 인스턴스에 여러 쓰레드가 동시에 접근하기 때문에 문제가 발생한다.

ㅇ 🎄 그렇다면 V3에서 동시성문제를 어떻게 해결해야할까?

  • ThreadLocal
@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 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);
    }

    @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 syncTraceId() {
        TraceId traceId = traceIdHolder.get();
        if (traceId == null) {
            traceIdHolder.set(new TraceId());
        } else {
            traceIdHolder.set(traceId.createNextId());
        }
    }

    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();
    }
}
  • 원래 private TraceId traceIdHolder; 이 부분을 지우고 ThreadLocal을 사용하기 위해서 private ThreadLocal<TraceId> traceIdHolder = new ThreadLocal<>(); 로 바꿔준다.
@Configuration
public class LogTraceConfig {

    @Bean
    public LogTrace logTrace() {
        return new ThreadLocalLogTrace();
    }
}
  • FieldLogTrace로 되어있던 부분을 ThreadLocalLogTrace()로 수정해주기만 하면 동시성 문제가 해결이 된다.

📕 동시성 문제는 왜 발생하는 걸까?


ㅇ 여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제가 동시성 문제라 한다. 이런 동시성 문제는 여러 쓰레드가 같은 인스턴스의 필드에 접근해야 하기 때문에 트래픽이 적을 경우 잘 나타나지 않지만 트래픽이 점점 많아질 수 록 자주 발생한다. 스프링 빈 처럼 싱글톤 객체의 필드를 변경하며 사용할 때 이러한 동시성 문제를 조심해야 한다.
하지만 이런 동시성 문제는 지역 변수에서는 발생하지 않는다. 지역 변수는 쓰레드마다 각각 다른 메모리 영역이 할당되기 때문이다. 또한 동시성 문제는 값을 읽을 때는 발생하지 않고 값을 변경할 때 발생한다.

ㅇ 동시성 이슈의 문제점

  • 공유 데이터에 대해 예상 결과가 다르지만 오류 발생하지 않음
  • 오류 부분을 찾기가 매우 힘듬
  • 비정형적으로 발생하기 때문에 디버깅이 힘듬

ㅇ 동시성 문제를 해결하기 위한 다른 방안

  • 낙관전 락과 비관적 락
    • 낙관적 락(Optimistic Lock) : 특정 자원에 대한 경쟁을 낙관적으로 바라보는 락 방식, 여러 트랜잭션이 데이터를 동시에 수정하지 않는다는 가정하에 트랜잭션 충돌을 방지하는 기법 ( 자원에 락을 걸어서 선점하지말고 커밋할 때 동시성 문제가 발생하면 그때 처리하자는 방법론)
    • 비관전 락(Pessimistic Lock) : 어떤 자원 경쟁을 비관적으로 바로보는 락 방식, 여러 트랜잭션이 데이터를 동시에 수정할 것이라고 가정하는 방식, 하나의 트랜잭션이 데이터를 읽는 시점에서 락을 걸고, 조회 또는 업데이트 처리가 완료될 때까지 유지 (트랜잭션의 충돌이 발생한다고 가정하고 우선 락을 거는 방법론)

📘ThreadLocal


ㅇ 해당 쓰레드만 접근할 수 있는 특별한 저장소

  • 예를 들어 물건 보관 창구를 떠올리면 된다. 여러 사람이 같은 물건 보관 창구를 사용하더라도 창구 직원은 사용자를 인식해서 사용자별로 확실하게 물건을 주는 개념이다.

ㅇ ThreadLocal을 사용할 때 지켜야할 것!

  • ThreadLocal은 Thread의 정보를 key로 하여 Map의 형식으로 데이터를 저장하는 자료구조를 가지고 있다. 따라서 만약 ThreadPool을 사용하여 thread를 재활용한다면 동일한 이전에 세팅했던 ThreadLocal의 정보가 남아있어 원치않게 동작할 수 있다. 따라서 ThreadPool을 사용하는 경우에는 반드시 모두 사용 후 ThreadLocal의 값을 remove 메서드를 사용하여 값을 제거해줘야 한다.

ㅇ ThreadLocal의 기본 사용법
1, ThreadLocal 객체 생성
2, ThreadLocal.set() 이용하여 현재 쓰레드의 로컬 변수에 값 저장
3, ThreadLocal.get() 이용해 현재 쓰레드의 로컬 변수 값 읽기
4, ThraedLocal.remove() 이용하여 현재 쓰레드의 로컬 변수 값 제거

ㅇ ThreadLocal 동작 방식

  • 스레드마다의 고유한 변수를 사용하도록 해당 변수를 저장하는 Thread.set()
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

Thread에서 ThreadLocalMap이란 것을 꺼내 그 안에서 저장해놓은 변수를 꺼내는 식으로 처리. ThreadLocal에서 지금 현재 스레드의 table에서 현재 ThreadLocal 클래스를 키 값으로 map에 값을 저장

  • 값을 꺼내 사용하는 Thread.get()
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

해당 스레드의 ThreadLocalMap에서 가져온다. 그리고 map에서 값을 꺼내 그 값을 반환해야하는 클래스로 형변환하여 반환

1, set()을 통해 변수 값을 해당 스레드가 가지고 있는 Map에 생성한 ThreadLocal 클래스 키를 값으로 저장
2, get()을 통해 변수 값을 해당 스레드가 가지고 있는 Map에서 현재 ThreadLocal 클래스 키 값으로 가지고 옴

profile
Java BackEnd Developer

0개의 댓글