카테고리 없음

[스프링 핵심원리 고급편] 예제 만들기 & ThreadLocal

## 요구사항

0. 다음 흐름의 api를 구현한다.

client 요청 -> controller -> service -> repository -> service -> controller -> 응답

1. 각 계층에서 로그를 찍는다.

2. 각 계층에서 로직을 마칠 때 찍는 로그에는 소요 시간도 추가한다.

3. 어느 뎁스인지 UI로 표시한다.

4. 각 요청의 ID를 부과하여 같이 출력한다.

[fbda3e10] OrderController.request()
[fbda3e10] |--->OrderService.request()
[fbda3e10] |    |--->OrderRepository.request()
[fbda3e10] |    |<---OrderRepository.request() time=10001ms
[fbda3e10] |<---OrderService.request() time=10005ms

 

## [v0] 요구사항 0번 만족

단순 api 구현

 

## [v1] 요구사항 0, 1, 2번 만족

- 각 비즈니스 메서드 시작과 끝 지점에 trace.begin()/trace.end() 메서드를 추가하여 로그 출력

- begin()에서 currentTimeMillis()를 저장하고 end()에서 현재 시각과 차이를 계산하여 소요 시간 출력

 

## [v2] 요구사항 0, 1, 2, 3, 4번 만족

traceId를 파라미터로 넘겨 ID와 뎁스 정보를 유지함

 

ex) Service 비즈니스 메서드

public void orderItem(TraceId traceId, String itemId) { //파라미터 추가로 받음
    TraceStatus status = null;
    try {
        status = trace.beginSync(traceId, "OrderService.request()"); //begin이 아닌 뎁스+1 로직이 포함된 beginSync() 사용
        orderRepository.save(status.getTraceId(), itemId);  //respository에 파라미터 넘김
        trace.end(status);
    } catch (Exception e){
        trace.exception(status, e);
        throw e; //로그는 예외에 영향 미치면 안 됨. 예외를 꼭 다시 던져줘야 함.
    }
}

 

### 장점

- 정보를 넘겨줌으로 3, 4번 조건을 만족함

### 단점

- 파라미터로 넘기고 있어, 로그 때문에 비즈니스 메서드를 수정해주어야 함 (만약 인터페이스 구현체라면 인터페이스까지 수정해야 함)

- 시작점인 controller에서는 0뎁스에서 시작하고, 이후 계층에서는 뎁스 정보를 받아 +1해주는 로직이 포함되어, 단위 테스트 시 깔끔하지 않음

 

## [v3] 파라미터 방식 대신 Log 객체를 Spring Bean으로 등록

### 방법 1. 싱글톤 객체를 사용하여 TraceId 공유

@Slf4j
@Component
public class FieldLogTrace implements LogTrace {
    ...
    
    private TraceId traceIdHolder;  //traceId 동기화, 동시성 이슈 발생
    
    ...
 }

#### 장점

- 파라미터 방식을 사용하여 발생하는 위 단점 모두 해결

#### 단점

- 싱글톤 객체로서 많은 요청 유입 시 데이터가 꼬이는 동시성 이슈 발생

 

### 방법 2. ThreadLocal을 사용하여 스레드별 데이터 구분

스레드 로컬이란?
해당 스레드만 접근할 수 있는 특별한 저장소를 뜻함.
멀티스래딩의 패턴으로 가지고 있는 개념이고 자바에서는 이를 지원하기 위해 java.lang.ThreadLocal클래스 제공

@Slf4j
@Component
public class ThreadLocalLogTrace implements LogTrace {
	...
    private ThreadLocal<TraceId> traceIdHolder = new ThreadLocal<>();
    ...
}

#### 장점

- 스레드별 동시성 이슈 해결

#### 주의점

- 데이터 사용을 마치고 나면 반드시 ThreadLocal.remove()를 호출해 값을 제거해야 함.

- Tomcat은 스레드풀을 사용하고 있기 때문에, 다음 요청에서 이전 요청의 데이터를 사용하면 데이터 유출 이슈로 이어질 수 있음

 

 

 

 

## 번외

스레드 테스트를 진행하며 "테스트하고 있는 스레드가 종료되기 전에 메인 스레드가 종료되어 로그를 찍을 수 없는 문제"를 마주했다.

강의에서 sleep을 걸어서 메인 스레드가 종료되는 시점을 늦추는 방법을 사용했지만, 다른 좋은 방법이 있다고 언급만 해주셨다.

 

바로 CatchDownLatch이다.

정리하기엔 너무 길어서 다른 글 가져왔다.

https://codechacha.com/ko/java-countdownlatch/

 

Java - CountDownLatch 사용 방법

CountDownLatch는 어떤 쓰레드가 다른 쓰레드에서 작업이 완료될 때 까지 기다릴 수 있도록 해주는 클래스입니다. countDown()이 호출되면 Latch의 수가 1개씩 감소하며, await()은 Latch의 숫자가 0이 될 때

codechacha.com