스프링 핵심 원리 - 고급편 - (2) 예제 만들기 - 로그 추적기
인프런 스프링 핵심원리 - 고급편을 학습하고 정리한 내용 입니다.
이전에 생성한로그 추적기를 구현해 보자.
로그 추적기 V1 - 프로토타입 개발
애플리케이션에 모든 로직에 직접 로그를 남겨도 되지만, 그것보다는 더 효율적인 개발 방법이 필요하다.
특히 트랜잭션 ID와 깊이를 표현하는 방법은 기존 정보를 이어 받아야 하기 때문에 단순히 로그만 남긴다고 해결할 수 있는 것은 아니다.
먼저 프로토타입 버전 로그 추적기를 위한 기반 데이터를 가지고 있는 TraceId, TraceStatus 클래스를 만들어보자.

해당 패키지에 만들겠다.
TraceId
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Getter
public class TraceId {
private String id;
private int level;
public TraceId() {
this.id = createId();
this.level = 0;
}
private TraceId(String traceId, int level) {
this.id = traceId;
this.level = level;
}
private String createId() {
// uuid 생성해서 앞에 8자리만 사용
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;
}
}
TraceId 클래스
로그 추적기는 트랜잭션ID와 깊이를 표현하는 방법이 필요하다.
여기서는 트랜잭션ID와 깊이를 표현하는 level을 묶어서 TraceId라는 개념을 만들었다.
TraceId는 단순히 id (트랜잭션ID)와 level정보를 함께 가지고 있다.
1
2
3
[796bccd9] OrderController.request() //트랜잭션ID:796bccd9, level:0
[796bccd9] |-->OrderService.orderItem() //트랜잭션ID:796bccd9, level:1
[796bccd9] | |-->OrderRepository.save()//트랜잭션ID:796bccd9, level:2
(최종 목표)
UUID
TraceId를 처음 생성하면 createId()를 사용해서 UUID를 만들어낸다. UUID가 너무 길어서 여기선 앞 8자리만 사용한다. 이 정도면 로그를 충분히 구분할 수 있다.
1
2
ab99e16f-3cde-4d24-8241-256108c203a2 //생성된 UUID
ab99e16f //앞 8자리만 사용
createNextId()
다음 TraceId를 만든다. 예제 로그를 잘 보면 깊이가 증가해도 트랜잭션ID는 같다. 대신에 깊이가 하나 증가한다.
createPreviousId()
createNextId()의 반대 역할을 한다. id는 기존과 같고, level은 하나 감소한다.
isFirstLevel()
첫 번째 레벨 여부를 편리하게 확인할 수 있는 메서드
TraceStatus
1
2
3
4
5
6
7
8
9
10
11
12
13
@Getter
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;
}
}
TraceStatus 클래스: 로그의 상태 정보를 나타낸다.
로그를 시작하면 끝이 있어야 한다.
1
2
[796bccd9] OrderController.request() //로그 시작
[796bccd9] OrderController.request() time=1016ms //로그 종료
TraceStatus는 로그를 시작할 때의 상태 정보를 가지고 있다. 이 상태 정보는 로그를 종료할 때 사용된다.
traceId: 내부에 트랜잭션ID와 level을 가지고 있다.startTimems: 로그 시작 시간이다. 로그 종료 시 이 시작 시간을 기준으로 시작 - 종료 까지 전체 수행 시간을 구할 수 있다.message: 시작 시 사용한 메시지 이다. 이후 로그 종료 시에도 이 메시지를 사용해서 출력한다.
TraceId, TraceStatus를 사용해서 실제 로그를 생성하고, 처리하는 기능을 개발해보자.
HelloTraceV1
hello.advanced.trace.hellotrace.HelloTraceV1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@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();
}
}
HelloTraceV1을 사용해서 실제 로그를 시작하고 종료할 수 있다. 그리고 로그를 출력하고 실행 시간도 측정할 수 있다.
@Component: 싱글톤으로 사용하기 위해 스프링 빈으로 등록한다. 컴포넌트 스캔의 대상이 된다.TraceStatus begin(String message)- 로그를 시작한다.
- 로그 메시지를 파라미터로 받아서 시작 로그를 출력한다.
- 응답 결과로 현재 로그의 상태인
TraceStatus를 반환한다.
void end(TraceStatus status)- 로그를 정상 종료한다.
- 파라미터로 시작 로그의 상태(
TraceStatus)를 전달 받는다. 이 값을 활용해서 실행 시간을 계산하고, 종료 시에도 시작할 때와 동일한 로그 메시지를 출력할 수 있다.
void exception(TraceStatus status, Exception e)- 로그를 예외 상황으로 종료한다.
TraceStatus,Exception정보를 함께 전달 받아서 실행시간, 예외 정보를 포함한 결과 로그를 출력한다.- 예외가 발생할 때 호출한다.
complete(TraceStatus status, Exception e)- 비공개 메서드
end(),exception(), 의 요청 흐름을 한 곳에서 편리하게 처리한다. 실행 시간을 측정하고 로그를 남긴다.
String addSpace(String prefix, int level): 다음과 같은 결과를 출력한다.- prefix:
-->- level 0: ``
- level 1:
|--> - level 2:
| |-->
- prefix:
<--- level 0: ``
- level 1:
|<-- - level 2:
| |<--
- prefix:
<x-- level 0: ``
- level 1:
|<X- - level 2:
| |<x-
- prefix:
테스트 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class HelloTraceV1Test {
@Test
void begin_test() {
HelloTraceV1 trace = new HelloTraceV1();
TraceStatus status = trace.begin("hello");
trace.end(status);
}
@Test
void begin_exception() {
HelloTraceV1 trace = new HelloTraceV1();
TraceStatus status = trace.begin("hello");
trace.exception(status, new IllegalStateException());
}
}

다음과 같이 트랜잭션 아이디와 시작, 종료 실행 시간이 잘 나온다 (이건 사실 테스트가 아니라 그냥 확인 용)
로그 추적기 V1 - 적용
로그를 로직에 적용하기 위해

기존에 있던 코드들을 v1 패키지에 복사 한다.
이제 컨트롤러, 리포지토리, 서비스단에 로그 추적기를 적용해 보자.
컨트롤러
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@RequiredArgsConstructor
public class OrderControllerV1 {
private final OrderServiceV1 orderService;
private final HelloTraceV1 trace;
@GetMapping("/v1/request")
public String request(@RequestParam("itemId") String itemId) {
TraceStatus status = null;
try {
status = trace.begin("OrderControllerV1.request()");
orderService.orderItem(itemId);
trace.end(status);
return "ok";
} catch (Exception e) {
trace.exception(status, e);
throw e;
}
}
}
1
status = trace.begin("OrderControllerV1.request()");
이걸로 로그를 시작한다.
1
2
//로직 정상 종료 후
trace.end(status);
로그를 종료한다.
1
2
3
4
catch (Exception e) {
trace.exception(status, e);
throw e;
}
예외가 터지면 예외 로그를 출력한다.
서비스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
@RequiredArgsConstructor
public class OrderServiceV1 {
private final OrderRepositoryV1 orderRepository;
private final HelloTraceV1 trace;
public void orderItem(String itemId) {
TraceStatus status = null;
try {
status = trace.begin("OrderServiceV1.orderItem()");
orderRepository.save(itemId);
trace.end(status);
} catch (Exception e) {
trace.exception(status, e);
throw e;
}
}
}
컨트롤러와 마찬가지로 예외를 감쌀 try~catch를 만들고, 로직 사이에 begin end, 그리고 catch에 exception을 위치 시켰다.
리포지토리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Repository
@RequiredArgsConstructor
public class OrderRepositoryV1 {
private final HelloTraceV1 trace;
public void save(String itemId) {
TraceStatus status = null;
try {
status = trace.begin("OrderRepositoryV1.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(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
컨트롤러, 서비스와 마찬가지이다.
실제 적용

정상적인 흐름일 때 다음과 같이 로그가 나온다.

예외가 터지면 다음과 같이 흐름이 진행 되고, 마지막에 컨트롤러에서도 throw e를 했기 때문에 톰캣에서도 로그를 띄운다.
남은 요구 사항
모든 PUBLIC 메서드의 호출과 응답 정보를 로그로 출력- ~~애플리케이션의 흐름을 변경하면 안됨
- ~~로그를 남긴다고 해서 비즈니스 로직의 동작에 영향을 주면 안됨
- ~~메서드 호출에 걸린 시간
- ~~정상 흐름과 예외 흐름 구분
- ~~예외 발생 시 예외 정보가 남아야 함
- 메서드 호출의 깊이 표현
- HTTP 요청을 구분
- HTTP 요청 단위로 특정 ID를 남겨서 어떤 HTTP 요청에서 시작된 것인지 명확하게 구분이 가능해야 함
- 트랜잭션 ID (DB 트랜잭션X), 여기 서는 하나의 HTTP 요청이 시작해서 끝날 때 까지 를 하나의 트랜잭션이 라함
아직 구현하지 못한 요구 사항은 메서드 호출의 깊이를 표현하고, 같은 HTTP 요청이면 같은 트랜잭션 ID를 남기는 것 이다.
이 기능은 직전 로그의 깊이와 트랜잭션 ID가 무엇인지 알아야 할 수 있는 일이다.
예를 들어서 OrderController.request()에서 로그를 남길 때 어떤 깊이와 어떤 트랜잭션 ID를 사용했는 지를 그 다음에 로그를 남기는 OrderService.orderItem()에서 알아야 한다.
결국 현재 로그의 상태 정보인 트랜잭션ID와 level이 다음으로 전달되어야 한다.
정리하면 로그에 대한 문맥(Context) 정보가 필요하다.
댓글남기기