Spring/[인프런 김영한 스프링 핵심 원리 - 고급편]
[인프런 김영한 스프링 핵심 원리 - 고급편] 예제 만들기
h2boom
2024. 12. 1. 17:36
예제 프로젝트 - V0
@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();
}
}
}
@Service
@RequiredArgsConstructor
public class OrderServiceV0 {
private final OrderRepositoryV0 orderRepository;
public void orderItem(String itemId) {
orderRepository.save(itemId);
}
}
@RestController
@RequiredArgsConstructor
public class OrderControllerV0 {
private final OrderServiceV0 orderService;
@GetMapping("/v0/request")
public String request(@RequestParam("itemId") String itemId) {
orderService.orderItem(itemId);
return "ok";
}
}
- 상품 주문 예제 코드
- Repository
- sleep(1000) - 상품 저장 시 1초의 시간이 소요될 것으로 가정함으로 지정
- itemId 값이 ex로 넘어오면 예외를 발생 시킴
- Repository
로그 추적기 - 요구사항 분석
- 요구사항
- 모든 PUBLIC 메서드의 호출과 응답 정보를 로그로 출력
- 애플리케이션의 흐름을 변경하면 안됨
- 로그를 남긴다고 해서 비즈니스 로직의 동작에 영향을 주면 안됨
- 메서드 호출에 걸린 시간
- 정상 흐름과 예외 흐름 구분
- 예외 발생시 예외 정보가 남아야 함
- 메서드 호출의 깊이 표현
- HTTP 요청을 구분
- HTTP 요청 단위로 특정 ID를 남겨서 어떤 HTTP 요청에서 시작된 것인지 명확하게 구분이 가능해야 함
- 트랜잭션 ID (DB 트랜잭션X), 여기서는 하나의 HTTP 요청이 시작해서 끝날 때 까지를 하나의 트랜잭션이
라 함
로그 출력 예시
==================================================
정상 요청
[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: 예외 발생!
로그 추적기 V1 - 프로토타입 개발
public class TraceId {
private String id; //트랜잭션 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;
}
}
- 로그 추적기를 위한 기반 데이터 클래스 예제
- createId() : 랜덤한 UUID를 생성하고 생성된 UUID가 너무 길기에 앞 8자리만 잘라서 사용하는 기능
- createNextId() : id는 같고 level이 증가하는 기능
- createPreviousId() : id는 같고 level이 감소하는 기능
- isFirstLevel() : 첫 번째 level 여부를 확인하는 기능
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 : 시작 시 사용한 메시지로 로그 종료 시에도 출력하기 위해 사용하는 필드
@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();
}
}
- 로그 출력을 위한 예제 클래스
- @Component를 사용해서 싱글톤으로 스프링 빈을 등록하고 사용
- begin() : 로그를 시작하고 메시지를 받아서 시작 로그를 출력하는 기능
- 응답 결과로 로그 상태 TraceStatus 반환
- end() : 정상 흐름에서 호출되는 메소드로 로그를 정상 종료하는 기능
- 시작 로그 상태 TraceStatus를 받아서 실행 시간과 시작 메시지와 동일한 메시지 출력
- exception() : 예외가 발생했을 때 호출되는 메소드로 로그를 예외 상황으로 종료하는 기능
- 로그 상태 TraceStatus와 Exception 정보를 받아서 시간, 예외 정보 등 결과 로그를 출력
- complete() : 실행 시간 측정하고 로그를 남길 수 있도록 요청 흐름을 한 곳에서 편리하게 처리하는 기능
- addSpace() : 로그를 형식에 맞게 (level 수준에 따라 다르게) 출력하도록 하는 기능
로그 추적기 V1 - 적용
@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("OrderController.request()");
orderService.orderItem(itemId);
trace.end(status);
return "ok";
} catch (Exception e) {
trace.exception(status, e);
throw e; //예외를 꼭 다시 던져줘야한다.
}
}
}
@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("OrderService.orderItem()");
orderRepository.save(itemId);
trace.end(status);
} catch (Exception e) {
trace.exception(status, e);
throw e;
}
}
}
@Repository
@RequiredArgsConstructor
public class OrderRepositoryV1 {
private final HelloTraceV1 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();
}
}
}
- 로그 추적기를 적용한 V1 예제
- OrderController/Service/RepositoryV1
- 로그 출력을 위해 스프링 빈으로 등록된 HelloTraceV1를 주입받는다.
- 로그 메시지에 아직까지는 수작업으로 컨트롤러 이름, 메소드 이름을 넣어줬다.
- 예외 처리를 위해 try-catch를 사용할 때 try, catch에서 모두 status 변수를 사용하기 위해서 try 이전에 status를 선언하고 null로 초기화 해줘야 한다.
- throw e : 예외를 다시 던져주지 않으면 예외를 먹어버리고 정상 흐름으로 동작하게 된다.
- 요구사항에 의해 로그는 어플리케이션 흐름에 영향을 주면 안된다.
- OrderController/Service/RepositoryV1
- 요구사항 내용 중 level에 따른 출력, HTTP 요청 구분에 대한 내용은 구현이 안된 상황
- 해당 요구사항을 만족하기 위해서 직전 로그의 level과 트랜잭션 ID를 알아야하기에 로그에 대한 문맥(Context) 정보를 넘겨줘야 한다.
로그 추적기 V2 - 파라미터로 동기화 개발
@Slf4j
@Component
public class HelloTraceV2 {
...
//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);
}
}
- HelloTraceV2 예제
- 나머지는 V1과 동일하며 beginSync()만 새로 추가
- 로그 시작할 때 사용한 id, level을 다음 로그에 넘겨주는 기능
- createNextId()로 id는 동일하고 level은 하나 증가시켜줬다.
- 나머지는 V1과 동일하며 beginSync()만 새로 추가
로그 추적기 V2 - 적용
- V2에서 V1과 차이점
- Controller->Service->Repository를 호출할 때마다 Status의 TraceId를 함께 넘겨줬다.
- Controller는 첫 로그 시작이므로 begin()을 사용하고 나머지 Service, Repository에서는 beginSync()로 로그를 시작해서 id는 그대로 두고 level을 증가시켰다.
- => 같은 HTTP 요청에 대해서 같은 트랜잭션ID가 사용되고 level에 따라 다르게 표현된다.
요구 사항 모두 만족
- 문제점
- HTTP 요청을 구분하고 level을 표현하기 위해서 TraceId 동기화가 필요
- 동기화를 위해 관련 메소드의 모든 파라미터를 수정해야한다.
- 로그 처음 시작 시 begin() 호출, 아닌 경우 beginSync()를 호출해야 한다.
- Controller -> Service -> Repository 순서로 호출하는 것이 아닌 바로 Service를 호출하는 경우 파라미터로 넘길 TraceId가 없다.
- HTTP 요청을 구분하고 level을 표현하기 위해서 TraceId 동기화가 필요
정리
- 예외가 발생하거나 던진 예외를 받았을 때 다시 던져주지 않으면 예외를 먹어버리고 정상 흐름으로 동작하게 된다.
- try-catch로 예외 처리 시 try, catch 블럭 모두에서 사용하는 변수가 있는 경우 이전에 미리 선언하고 null로 초기화해줘야 한다.
출처 : [인프런 김영한 스프링 핵심 원리 - 고급편]
스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런
김영한 | 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기
www.inflearn.com