FireDrago

[Spring] 로그 출력기 만들기 (쓰레드 로컬 사용) 본문

카테고리 없음

[Spring] 로그 출력기 만들기 (쓰레드 로컬 사용)

화이용 2024. 5. 10. 11:54

요구사항

1. 모든 PUBLIC 메서드의 호출과 응답 정보를 로그로 출력

  - 애플리케이션의 흐름을 변경하면 안됨
  - 로그를 남긴다고 해서 비즈니스 로직의 동작에 영향을 주면 안됨
2. 메서드 호출에 걸린 시간 출력
3. 정상 흐름과 예외 흐름 구분
   - 예외 발생시 예외 정보가 남아야 함
4. 메서드 호출의 깊이 표현
5. HTTP 요청을 구분
    - HTTP 요청 단위로 특정 ID를 남겨서 어떤 HTTP 요청에서 시작된 것인지 명확하게 구분이 가능해야 함

 

 

먼저 로그정보를 담는 TraceStatusTraceId 객체를 만든다.

public class TraceId {

    private String id;
    private int level;

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

    public TraceId(String id, int level) {
        this.id = id;
        this.level = level;
    }
	
    // Http 요청별 구분을 위한 UUID 생성 (8자리)
    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);
    }
	
    // 레벨이 0일때 출력을 위한 확인 메서드
    public boolean isFirstLevel() {
        return level == 0;
    }

    public String getId() {
        return id;
    }

    public int getLevel() {
        return level;
    }
}
public class TraceStatus {
	
    // 호출레벨과, Id를 가지고 있는 TraceId를 필드로 가진다.
    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;
    }
}

 

로그 출력을위한 정보를 담는 두 객체를 완성했다.

본격적으로 로그를 출력하는 기능을 하는 ThreadLogTrace 객체를 생성하자 

향후 로직 변경을 위해 LogTrace인터페이스를 구현하는 방식으로 설계했다.

public interface LogTrace {

    TraceStatus begin(String message);

    void end(TraceStatus status);

    void exception(TraceStatus status, Exception e);
}
@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 final ThreadLocal<TraceId> traceIdHolder = new ThreadLocal<>();
	
    // TraceId를 생성하고 시작시간, 호출메시지를 이용하여, TraceStatus 객체를 생성한다.
    @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);
    }
	
    // 처음 호출되는 경우, new TraceId를 생성
    // 처음 호출 아니면, level이 증가한 TraceId를 생성
    private void syncTraceId() {
        TraceId traceId = traceIdHolder.get();
        if (traceId == null) {
            traceIdHolder.set(new TraceId());
        } else {
            traceIdHolder.set(traceId.createNextId());
        }
    }
	
    // TraceStatus 를 이용하여, 종료로직 출력을 실행한다. 
    @Override
    public void end(TraceStatus status) {
        complete(status, null);
    }
	
    // TraceStatus 를 이용하여, 종료로직 출력을 실행한다. 예외정보를 함께 전달한다.
    @Override
    public void exception(TraceStatus status, Exception e) {
        complete(status, e);
    }

    private void complete(TraceStatus status, Exception e) {
    	// 종료 시간을 측정, TraceStatus의 시작시간을 이용하여 실행시간을 구한다.
        Long stopTimeMillis = System.currentTimeMillis();
        long resultTimeMs = status.getStartTimeMs() - stopTimeMillis;
        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();
    }
	
    // 로그 출력이 완료된 경우, 레벨을 감소시키거나, TraceId를 지운다.
    // 쓰레드 로컬은 쓰레드 완료시 반드시 값을 지워줘야 한다.
    private void releaseTraceId() {
        TraceId traceId = traceIdHolder.get();
        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();
    }
}

 

이제 로그 출력 기능이 잘 작동하는지 테스트 코드를 통해 확인해보자

@Slf4j
class ThreadLocalLogTraceTest {

    ThreadLocalLogTrace trace = new ThreadLocalLogTrace();
	
    /** 콘솔 출력 결과
    * [3b331fac] hello1
    * [3b331fac] |-->hello2
    * [3b331fac] |<--hello2 time=0ms
    * [3b331fac] hello1 time=-5ms
    */
    @Test
    void begin_end_level2() {
        TraceStatus status1 = trace.begin("hello1");
        TraceStatus status2 = trace.begin("hello2");
        trace.end(status2);
        trace.end(status1);
    }
   
    /** 콘솔 출력 결과
    * [61d05c01] hello1
    * [61d05c01] |-->hello2
    * [61d05c01] |<X-hello2 time=0ms ex=java.lang.IllegalStateException
    * [61d05c01] hello time=0ms ex=java.lang.IllegalStateException
    */
    @Test
    void begin_exception_level2() {
        TraceStatus status1 = trace.begin("hello");
        TraceStatus status2 = trace.begin("hello2");
        trace.exception(status2, new IllegalStateException());
        trace.exception(status1, new IllegalStateException());
    }
}

 

로그 출력 기능을 완성했다. 이제 스프링의 여러 디자인 패턴을 활용하여 로그 출력 기능을 사용해보자