스프링의 이해 : 로그 추적기 개발(쓰레드 로컬까지 적용)

    이 포스팅은 인프런의 김영한님의 강의를 복습하며 정리한 글입니다.

     


    로그 추적기 개발 요구사항

    1. 개발의 편의를 위해서 모든 메서드마다 다음과 같은 로그를 남기는 로그 추적기의 개발 및 적용이 필요하다고 가정하자. 필요 요구사항은 다음과 같다.
    2. UUID로 요청을 분류한다.
    3. 실행되는 메서드들의 이름을 출력한다.
    4. 실행되는 메서드들이 아래로 내려가면 갈수록 -->, <-- 형태로 단계가 구분되도록 한다.
    5. 메서드가 실행되는데 필요한 시간을 출력한다.
    6. 예외가 발생할 경우 <X-- 형태로 에러가 발생했다는 것을 알려줘야한다. 

     


    V0 버전의 개발

    V0 버전은 로그 추적기가 적용 되지 않고, 핵심 기능만 구현된 날것 상태로 이해를 하면 된다. 

    OrderControllerV0 코드

    @RestController
    @Slf4j
    @RequiredArgsConstructor
    public class OrderControllerV0 {
    
        private final OrderServiceV0 orderService;
    
    
        @GetMapping("/v0/request")
        public String request(String itemId) {
            orderService.orderItem(itemId);
            return "ok";
        }
    }
    

     

    OrderServiceV0 코드

    @Service
    @RequiredArgsConstructor
    public class OrderServiceV0 {
    
        private final OrderRepositoryV0 orderRepository;
    
        public void orderItem(String itemId) {
            orderRepository.save(itemId);
    
        }
    
    
    }
    

    OrderServiceV0 코드

    @Slf4j
    @Repository
    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();
            }
        }
    }

     

    정리

    위의 기본 예제는 다음과 같은 형태로 흘러간다

    1. OrderController에서 itemId 값을 받아 OrderService로 넘겨준다.
    2. OrderService는 itemId를 OrderRepository로 넘겨준다.
    3. OrderRepository는 itemId가 ex면 예외를 발생한다. 그렇지 않으면 1초를 쉬고 반환한다. 

     


    V1 버전의 개발

    프로토 타입의 로그 추적기를 먼저 개발하고자 한다. 로그 추적기를 개발하는 부분, 개발한 로그 추적기를 적용하는 부분으로 나누어 접근할 수 있다. 

     

    로그 추적기 프로토 타입 개발 

    TraceId 클래스 코드 : UUID + 현재 깊이를 알고 있는 클래스

    @Getter
    public class TraceId {
    
        private String id;
        private int level;
    
        public TraceId() {
            this.id = createId();
            this.level = 0;
        }
    
        private String createId() {
            return UUID.randomUUID().toString().substring(0, 8);
        }
    }

     

    TraceStatus 클래스 코드 : TraceId, 어떤 위치에서 호출되었는지, 시작 시간을 기록

    @Getter
    public class TraceStatus {
    
        private TraceId traceId;
        private String message; //
        private Long startTimeMs; // 시작 시간을 가져야 한다.
    
        public TraceStatus(TraceId traceId, String message, Long startTimeMs) {
            this.traceId = traceId;
            this.message = message;
            this.startTimeMs = startTimeMs;
        }
    }

     

     

    HelloTraceV1 : 프로토 타입 로그 추적기 

    @Component
    @Slf4j
    public class HelloTraceV1 {
    
    
        private final static String START_PREFIX = "-->";
        private final static String COMPLETE_PREFIX = "<--";
        private final static String ERROR_PREFIX = "<X-";
    
        public TraceStatus begin(String message) {
    
            long startTimeMs = System.currentTimeMillis();
            TraceId traceId = new TraceId();
    
            log.info("[{}] {}{}", traceId.getId(), addSpacer(START_PREFIX, traceId.getLevel()), message);
            return new TraceStatus(traceId, message, startTimeMs);
        }
    
    
        public void complete(TraceStatus status, Exception e) {
    
            Long lastTimeMs = System.currentTimeMillis();
            Long startTimeMs = status.getStartTimeMs();
            Long resultTime = lastTimeMs - startTimeMs;
    
            TraceId traceId = status.getTraceId();
    
            if (e == null) {
                log.info("[{}] {}{} time = {}", traceId.getId(), addSpacer(COMPLETE_PREFIX, traceId.getLevel()), status.getMessage(), resultTime);
            }else{
                log.info("[{}] {}{} time = {} ex = {}", traceId.getId(), addSpacer(ERROR_PREFIX, traceId.getLevel()), status.getMessage(), resultTime, e.toString());
            }
        }
    
        public void end(TraceStatus status) {
            complete(status, null);
        }
    
        public void exception(TraceStatus status, Exception e) {
            complete(status, e);
        }
    
        private String addSpacer(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으로 시작한다.
      1. Message를 인자로 받는다. 이 Message는 어느 위치에서 실행되었는지를 표현해준다. 
      2. 로그를 찍는다. 
      3. 시작한 시점을 기준으로 LogStatus를 만들어서 Return한다. 
    • 정상적으로 끝이 나면 End로 종료한다.
      1. 마지막 실행 시점을 기록하여 소모된 시간을 계산한다. 
    • 예외가 발생하면 Exception으로 종료한다.
      1. 마지막 실행 시점을 기록하여 소모된 시간을 계산한다. 
    • 종료 + 예외는 겹치는 코드가 많기 때문에 Complete 메서드에서 조건문으로 처리하도록 작성했다. 
    • 종료 / 예외 시점에 TraceId + Message를 알고 있어야 하기 때문에 TraceStatus를 넘겨주었다.

     

    프로토 타입 로그 추적기 적용

    OrderControllerV1 코드

    public class OrderControllerV1 {
    
        private final OrderServiceV1 orderService;
        private final HelloTraceV1 trace;
    
    
    
        @GetMapping("/v1/request")
        public String request(String itemId) {
    
    
            TraceStatus status = null;
    
    
            try {
                status = trace.begin("orderController");
                orderService.orderItem(itemId);
                trace.end(status);
                return "ok";
            } catch (Exception e) {
                trace.exception(status,e);
                throw e;
            }
    
        }
    }

     

    OrderServiceV1 코드

    @Service
    @RequiredArgsConstructor
    public class OrderServiceV1 {
    
        private final HelloTraceV1 trace;
        private final OrderRepositoryV1 orderRepository;
    
        public void orderItem(String itemId) {
    
            TraceStatus status = null;
    
            try {
                status = trace.begin("OrderService");
                orderRepository.save(itemId);
                trace.end(status);
            } catch (Exception e) {
                trace.exception(status, e);
                throw e;
            }
    
        }
    
    
    }

     

    OrderRepositoryV1 코드

    @Slf4j
    @Repository
    @RequiredArgsConstructor
    public class OrderRepositoryV1 {
    
        private final HelloTraceV1 trace;
    
    
        public void save(String itemId) {
    
            TraceStatus status = null;
    
    
            try {
                status = trace.begin("OrderRepository");
    
                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();
            }
        }
    }

     

    실행 결과 

    위의 코드를 실행하면 다음과 같은 결과가 나온다. 로그를 정상적으로 찍고는 있지만, 요구사항을 모두 만족하지는 못한다. 즉, 아래의 결과에 대해서 구현할 수 있는 방법을 다시 한번 생각해봐야한다. 

    • 같은 요청이지만 동일 UUID가 나오지 않는다.
    • 같은 요청이지만 깊이에 따라서 값이 다르게 나오지 않는다. 

    위의 내용을 그림으로 살짝 정리하면 아래와 같은 상황이다.

     

    정리

    V1에 대한 내용을 정리하면 다음과 같다.

    • 로그 추적기는 싱글톤 인스턴스로 사용했다. 로그 추적기의 필드 자체가 필요한 것이 아니라 메서드가 필요한 것이기 때문이다.
    • 로그를 편히 찍기 위해 로그 추적기를 만들어 적용했지만, 실제 비즈니스 로직보다 로그를 찍기 위한 코드가 더 많아졌다. 

    먼저 요구사항을 구현하겠다. 

     


    로그 추적기 V2 버전

    로그 추적기 V1은 로그를 쉽게 찍도록 도움을 주긴 했으나, 요구사항을 모두 만족하지는 않았다. UUID와 깊이에 따른 값이 나올 수 있도록 해야한다. 이것을 가장 쉽게 구현하는 방법은 다음 메서드로 넘어갈 때, 현재의 TraceId를 넘겨주고 그 정보를 바탕으로 TraceStatus를 각각 생성하는 것이다. 

     

    로그 추적기의 구현 

    HelloTraceV2 : 파라메터를 넘겨주는 것 구현하기

    @Component
    @Slf4j
    public class HelloTraceV2 {
    
        public TraceStatus begin_sync(TraceId beforeTraceId, String message) {
    
            TraceId traceId = createNextId(beforeTraceId);
            long startTimeMs = System.currentTimeMillis();
    
            log.info("[{}] {}{}", traceId.getId(), addSpacer(START_PREFIX, traceId.getLevel()), message);
            return new TraceStatus(traceId, message, startTimeMs);
    
        }
    
        private TraceId createNextId(TraceId traceId) {
                return traceId.createNextId();
        }
    
    }
    • 기존 V1 코드에 위 코드를 추가했다. 
    • 첫번째 실행이 아닌, 두번째 메서드 실행부터 TraceId를 받고, 그 값으로 TraceStatus를 생성해서 깊이 + TraceId 정보가 유지되도록 했다.

     

    TraceId 클래스 코드 추가 : HelloLogTraceV2를 위한 추가 메서드 구현

    @Getter
    public class TraceId {
    
    	public TraceId createNextId() {
            return new TraceId(id, level + 1);
        }
    }
    • HelloTraceV2 클래스에서 createNextId() 메서드가 필요하게 되어 구현했다.
    • 현재 정보를 바탕으로 id는 유지, Level은 하나 더 높인 TraceId를 하나 만들어서 Return 해준다. 

     

    로그 추적기의 적용

    OrderControllerV2 

    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");
                orderService.orderItem(status.getTraceId(),itemId);
                trace.end(status);
                return "ok";
            } catch (Exception e) {
                trace.exception(status,e);
                throw e;
            }
    
        }
    }
    • TraceId를 넘겨줘야하기 때문에 orderItem에 itemId뿐만 아니라 traceId까지 함께 넘겨주었다. 

     

    OrderServiceV2 

    @Service
    @RequiredArgsConstructor
    public class OrderServiceV2 {
    
        private final HelloTraceV2 trace;
        private final OrderRepositoryV2 orderRepository;
    
        public void orderItem(TraceId traceId, String itemId) {
    
            TraceStatus status = null;
    
            try {
                status = trace.begin_sync(traceId,"OrderService");
                orderRepository.save(status.getTraceId() ,itemId);
                trace.end(status);
            } catch (Exception e) {
                trace.exception(status, e);
                throw e;
            }
    
        }
    
    
    }
    • 호출될 때 TraceId를 받기 때문에 기존의 메서드의 인자를 수정했다.
    • 그리고 다음 메서드를 호출할 때, 동일하게 TraceId를 넘겨주도록 수정했다. 
    • 첫번째가 아니기 때문에 begin_Sync라는 함수로 시작한다. 

     

    OrderRepositoryV2 

    @Slf4j
    @Repository
    @RequiredArgsConstructor
    public class OrderRepositoryV2 {
    
        private final HelloTraceV2 trace;
    
    
        public void save(TraceId traceId, String itemId) {
    
            TraceStatus status = null;
    
    
            try {
                status = trace.begin_sync(traceId,"OrderRepository");
    
                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를 받기 때문에 기존의 메서드의 인자를 수정했다.
    • 첫번째가 아니기 때문에 begin_Sync라는 함수로 시작한다. 

     

    수행 결과 확인하기

    • 요구 사항을 충족한 로그 추적기를 개발할 수 있었다.
    • 이전과 다르게 동일 요청에서 UUID가 유지되고, 깊이가 증가하는 것을 볼 수 있다. 

    V2를 구현하게 되면서 비로소 위 상태가 될 수 있게 되었다. 

     

    정리

    • 파라미터의 동기화를 위한 V2 로그 추적기를 개발했다.
    • 문맥을 맞추어 주기 위해서 TraceId를 넘겨주는 방식으로 개발했는데, 이로 인해서 두 가지 문제가 발생한다.
      1. 컨트롤러, 리포지토리, 서비스 메서드에서 받는 인자가 변경되게 되었다. 
      2. 리포지토리, 서비스 메서드에서 실행하는 메서드와 컨트롤러에서 실행하는 메서드가 달라지게 되었다. 

    즉, 현재의 문제점은 V2 버전의 로그 추적기를 적용하는 모든 곳에서 V2 버전의 로그 추적기와 강하게 결합 되어 있다는 점이다. 로그 추적기가 어떻게 돌아가는지 정확히 알고, 그것에 맞게 각 컨트롤러, 리포지토리, 서비스가 변경되었다는 것이 문제다. 변화에 아주 취약한 상태다. 

     


    로그 추적기 V3 개발 : 상태를 Field에 보관하는 추적기, FieldLogTrace 개발

    V3는 서비스, 리포지토리, 컨트롤러에서 다음 계층으로 넘어갈 때 직접 TraceId를 넘겨주면서 파라미터를 동기화했다. 이것 때문에 변화에 닫혀있지 못하게 되었다. 따라서 이 부분을 해결해주려면 직접 TraceId를 넘겨주지 않으면 된다. 어떻게 하면 될까?

    어플리케이션 전체에서 사용되는 클래스에 값을 등록해서 사용하면 된다. 즉, FieldLogTrace라는 클래스를 하나 만들고, 그 클래스는 내부 필드에서 현재의 TraceId를 기억하고 있도록 한다. 그리고 스프링 빈으로 등록하면, 싱글톤 빈으로 사용되기 때문에 어떤 클래스에서 쓰더라도 같은 TraceId를 가지고 있는 것이 보장된다. 따라서 Field에서 TraceId 정보를 가지고 있는 형태로 개발을 해본다. 

     

    FieldLogTrace 개발하기 

    public interface LogTrace {
    
        TraceStatus begin(String message);
        void end(TraceStatus status);
        void exception(TraceStatus status, Exception e);
    
    
    }
    • 먼저 다형성을 활용하기 위해서 LogTrace 인터페이스를 위와 같이 먼저 작성했다. 

    FieldLogTrace 클래스 코드 구현

    @Slf4j
    public class FieldLogTrace implements LogTrace{
    
        private final static String START_PREFIX = "-->";
        private final static String COMPLETE_PREFIX = "<--";
        private final static String ERROR_PREFIX = "<X-";
    
        private TraceId traceIdHolder;
    
        @Override
        public TraceStatus begin(String message) {
    
            long startTimeMs = System.currentTimeMillis();
            
            // 추가된 코드 
            syncTraceId();
            TraceId traceId = this.traceIdHolder;
    
            log.info("[{}] {}{}", traceId.getId(), addSpacer(START_PREFIX, traceId.getLevel()), message);
            return new TraceStatus(traceId, message, startTimeMs);
        }
    
    
    	// 추가된 코드 
        private void syncTraceId() {
            if (traceIdHolder == null) {
                traceIdHolder = new TraceId();
            }else{
                traceIdHolder = traceIdHolder.createNextId();
            }
        }
    
    
    
        @Override
        public void end(TraceStatus status) {
            complete(status, null);
        }
    
        @Override
        public void exception(TraceStatus status, Exception e) {
            complete(status, e);
        }
    
        public void complete(TraceStatus status, Exception e) {
    
            Long lastTimeMs = System.currentTimeMillis();
            Long startTimeMs = status.getStartTimeMs();
            Long resultTime = lastTimeMs - startTimeMs;
    
            TraceId traceId = status.getTraceId();
    
            if (e == null) {
                log.info("[{}] {}{} time = {}", traceId.getId(), addSpacer(COMPLETE_PREFIX, traceId.getLevel()), status.getMessage(), resultTime);
            }else{
                log.info("[{}] {}{} time = {} ex = {}", traceId.getId(), addSpacer(ERROR_PREFIX, traceId.getLevel()), status.getMessage(), resultTime, e.toString());
            }
    		
    		// 추가된 코드 
            releaseTraceId();
        }
    
    
    	// 추가된 코드 
        private void releaseTraceId() {
            TraceId traceId = this.traceIdHolder;
    
            if (traceId.isFirstLevel()) {
                traceIdHolder = null;
            } else {
                traceIdHolder = traceId.creatPreviousId();
            }
        }
    
    
        private String addSpacer(String prefix, int level) {
    
            StringBuilder sb = new StringBuilder();
    
            for (int i = 0; i < level; i++) {
                sb.append((i == level - 1 ? "|" + prefix : "|  "));
            }
            return sb.toString();
        }
    
    }
    • 앞서 말한 것처럼 FieldLogTrace를 스프링빈으로 생성해서 싱글톤 형태로 사용하고자 한다.
    • 싱글톤은 공유해서 사용되기 때문에 파라미터를 넘겨주고자, 해당 빈은 내부적으로 TraceId를 가지는 TraceHolderId라는 필드를 가진다. 
    • Begin 메서드에서는 다음 코드가 추가된다. (SyncTraceId)
      • 현재 TraceHolderId가 처음인지 확인한다.
        • 처음이면 새로운 TraceHoldId를 만들어준다. 
        • 처음이 아니라면, 기존 TraceHolderId를 참고해서 다음 HolderId를 만들고 Holder에 저장해준다.
    • Complet 메서드에서는 다음 코드가 추가된다. (releaseTraceId)
      • 현재 TraceHolderId가 처음인지 확인한다. 
        • 처음이면 완료된 것이기 때문에 TraceHolder를 null로 설정해준다.
        • 처음이 아니면, CreatePrevious 메서드를 이용해 Level을 하나 내려준다. 

     

    Field Log 적용하기

    OrderControllerV3 적용 

    @RestController
    @Slf4j
    @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");
                orderService.orderItem(itemId);
                trace.end(status);
                return "ok";
            } catch (Exception e) {
                trace.exception(status,e);
                throw e;
            }
    
        }
    }
    
    • 대부분 비슷한 이야기이기 때문에 OrderControllerV3에만 적용한 코드를 가져왔다.
    • V2에서는 파라미터로 TraceId를 넘겨주었는데, 이제는 넘겨주지 않아도 된다.
    • V2에서는 Begin_sync, Begin 메서드가 각 단계에 따라 다르게 실행되었는데, 지금은 Begin으로만 실행한다. 

     

    실행결과

    정상적으로 실행되는 것을 확인했다. 

     

    정리

    • V3에서는 LogTrace 인터페이스를 하나 만든 후, 구현체로 FieldLogTrace를 구현했다.
    • 해당 구현체의 핵심은 TraceHolderId를 필드로 가지고, 싱글톤 빈으로 등록한다. 등록한 후, 각 위치에서 해당 정보를 공용으로 사용해서 Level과 TraceId를 유지하는 것이었다. 

     

    그런데 동시에 여러 요청이 왔을 때, 위 문제점이 생긴다.

    1. 요청별로 UUID가 구분되지 않는다.
    2. 로그 기록도 이상하게 남는다.

    싱글톤 빈으로 필드를 공유하고 있기 때문에 동시에 여러 요청이 오면 위처럼 동시성 문제가 발생할 수 밖에 없다. 위 문제는 결국 여러 쓰레드가 하나의 자원을 공유해서 사용하기 때문에 문제가 발생하는 것이다. 이 문제를 해결하기 위해서는 쓰레드 별로 공간을 따로 할당해야한다. 

     


    로그 추적기 V3 개발 : 동시성 문제 해결을 위한 ThreadLocal 적용 추적기

    ThreadLocal은 쓰레드별로 로컬 저장소를 분배한다는 뜻이다. 싱글톤 빈처럼 등록되어서 모든 쓰레드가 동일한 곳에서 접근할 수 있는 것으로 보이지만 내부적으로 Thread 이름마다 각기 다른 저장소에 접근을 하게 된다. 여기서는 기존 TraceIdHolder를 ThreadLocal 저장소로 바꾸어 해당 문제를 해결했다. 

    https://ojt90902.tistory.com/697

     

    스프링의 이해 : 동시성 문제와 ThreadLocal 처리

    이 게시글은 인프런 김영한님 강의를 듣고 복습하며 기록한 글입니다. 동시성 문제 동시성 문제는 멀티 쓰레드 환경에서 발생하는 문제다. 위의 그림처럼 여러 쓰레드가 동시에 동일한 자원에

    ojt90902.tistory.com

     

     

    ThreadLocalLogTrace 코드 

    @Slf4j
    public class ThreadLocalLogTrace implements LogTrace{
    
        private final static String START_PREFIX = "-->";
        private final static String COMPLETE_PREFIX = "<--";
        private final static String ERROR_PREFIX = "<X-";
    
    	// 추가 및 변경된 부분 
        private ThreadLocal<TraceId> traceIdHolder = new ThreadLocal<>();
    
        @Override
        public TraceStatus begin(String message) {
    
            long startTimeMs = System.currentTimeMillis();
    
    syncTraceId();
            TraceId traceId = traceIdHolder.get();
    
            log.info("[{}] {}{}", traceId.getId(), addSpacer(START_PREFIX, traceId.getLevel()), message);
            return new TraceStatus(traceId, message, startTimeMs);
        }
    
        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);
        }
    
        public void complete(TraceStatus status, Exception e) {
    
            Long lastTimeMs = System.currentTimeMillis();
            Long startTimeMs = status.getStartTimeMs();
            Long resultTime = lastTimeMs - startTimeMs;
    
            TraceId traceId = status.getTraceId();
    
            if (e == null) {
                log.info("[{}] {}{} time = {}", traceId.getId(), addSpacer(COMPLETE_PREFIX, traceId.getLevel()), status.getMessage(), resultTime);
            }else{
                log.info("[{}] {}{} time = {} ex = {}", traceId.getId(), addSpacer(ERROR_PREFIX, traceId.getLevel()), status.getMessage(), resultTime, e.toString());
            }
    
            releaseTraceId();
        }
    
        private void releaseTraceId() {
    		//변경된 부분
            TraceId traceId = traceIdHolder.get();
    		
            //변경된 부분
            if (traceId.isFirstLevel()) {
                //변경된 부분
                traceIdHolder.remove();
            } else {
                //변경된 부분
                traceIdHolder.set(traceId.creatPreviousId());
            }
        }
    
    
        private String addSpacer(String prefix, int level) {
    
            StringBuilder sb = new StringBuilder();
    
            for (int i = 0; i < level; i++) {
                sb.append((i == level - 1 ? "|" + prefix : "|  "));
            }
            return sb.toString();
        }
    
    }
    • 기존 FieldLogTrace를 거의 그대로 가져오되, 저장소를 바꾸고 그 저장소에 맞는 문법으로 작성했다.
    • ThreadLocal은 저장소이기 때문에 set으로 값을 설정하고, get으로 값을 불러온다.

     

    ThreadLocal 컨트롤러, 리포지토리, 서비스 계층에 적용하기

    앞서 V3 버전과 바뀔 것이 없다. 왜냐하면 ThreadLocal은 FieldLogTrace와 똑같이 LogTrace 인터페이스를 구현한 구현체이기 때문이다. 다형성을 이용했기 때문에 적용을 위해 변경되어야 할 부분은 없다. 

     

    실행 결과

    실행 결과 동시에 접속이 들어와도, 쓰레드에 맞게 UUID가 설정되고 로그 역시 정상적으로 출력되는 것을 볼 수 있었다. 

     


    포스팅 요약

    • 로그 추적기 구현을 위해 각 컨트롤러들을 V0부터 V3까지 개발했다.
    • V1은 프로토 타입으로 파라메터가 동기화가 되지 않았다.
    • V2는 파라메터를 동기화했지만 TraceId를 넘겨야 했기 때문에 컨트롤러 계층의 메서드 인자가 변경되어야 했다.
    • V3(FieldLogTrace)는 웹계층에 영향을 주지 않고, TraceId를 싱글톤으로 넘겼지만 동시에 여러 스레드가 접근하면서 동시성 문제가 생겼다.
    • V3(ThreadLocal)을 사용해서 각 쓰레드별로 다른 저장소를 할당해, 싱글톤 빈을 사용해도 동시성 문제에서 자유로울 수 있도록 해결했다. 

     

    이렇게 V0 ~ V3까지 개발을 해왔는데 한 가지 문제가 있다. V0와 V3를 비교해보면, 로그 추적기를 적용하면서 지저분한 코드가 많이 들어갔다는 점이다. 즉, 비즈니스 로직보다 로그 추적기를 위한 코드가 더 많은 상황이다. 이런 상황을 해결해야한다.

    댓글

    Designed by JB FACTORY