본문 바로가기
Back/Spring

[Spring] 예제 프로젝트 만들기 - 상품 주문 프로세스

by 오엥?은 2023. 5. 22.
반응형

예제 프로젝트 만들기

: 상품을 주문하는 프로세스  /  Controller → Service → Repository 

 

◽ advanced > app > v0 > OrderRepositoryV0

package hello.advanced.app.v0;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class OrderRepositoryV0 {

    public void save(String itemId) {
        // 저장 로직
        if (itemId.equals("ex")) { // 상품의 아이디가 "ex" 로 넘어오면 
            throw new IllegalStateException("예외 발생!");  // 문제가 발생한 상황으로 예외가 터진다.
        }
        sleep(1000); // 상품을 저장하는데 1초가 걸린다.
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

◽ advanced > app > v0 > OrderServiceV0

package hello.advanced.app.v0;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class OrderServiceV0 {

    private final OrderRepositoryV0 orderRepository;

    public void orderItem(String itemId) {
        orderRepository.save(itemId);
    }
}

➕ @RequiredArgsConstructor

 Lombok에서 자동으로 생성자 주입에 대한 코드를 생성. 

 

◽ advanced > app > v0 > OrderControllerV0

package hello.advanced.app.v0;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController  // @RestController = @Controller + @ResponseBody
@RequiredArgsConstructor
public class OrderControllerV0 {

    private final OrderServiceV0 orderService;

    @GetMapping("/v0/request")
    // 상품의 이름을 넘겨주기 (itemId)
    public String request(String itemId) {
        orderService.orderItem(itemId);
        return "ok";
    }
}

 

이렇게 작성하고, localhost:8080/v0/request 에 들어가보면

500 뜬다.

itemId 를 넘겨줘야 하는데 null 이라서 그렇다.

 

다시 정신차리고 http://localhost:8080/v0/request?itemId=hello 에 들어가보면 

이렇게 뜬다. 

 

 로그 추적기 - 요구사항 분석

: 어떤 부분에서 병목이 발생하는지, 예외가 발생하는지를 로그를 통해 확인하는 것이 점점 중요해지고 있다.

 

< 요구사항 >

• 모든 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: 예외 발생!

[] 안에 들어있는 것은 HTTP 요청 단위의 특정 ID

 

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

◽ advanced > trace > TraceId

package hello.advanced.trace;

import java.util.UUID;

public class TraceId {

    private String id;
    private int level;

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

    private TraceId(String id, int level) {
        this.id = id;
        this.level = level;
    }

    // 만든 로그 추적 아이디 앞 8글자만 자르기
    private String createId() {
        return UUID.randomUUID().toString().substring(0, 8);
    }

    // 다음 아이디를 만드는 것 (아이디는 똑같고, 레벨은 하나 올라감)
    // 깊이가 증가해도 트랜잭션 ID는 같다. 대신에 깊이가 하나 증가한다.
    public TraceId createNextId() {
        return new TraceId(id, level + 1);
    }

    // 이전 아이디
    public TraceId createPreviousId() {
        return new TraceId(id, level - 1);
    }

    public boolean isFirstLevel() {
        return level == 0;
    }

    // id 와 level 을 볼 수 있는 getter
    public String getId() {
        return id;
    }

    public int getLevel() {
        return level;
    }
}

 

◽ TraceId 클래스

: 로그 추적기는 트랜잭션ID 와 깊이를 표현하는 방법이 필요하다.

 

◽ advanced > trace > TraceStatus

package hello.advanced.trace;

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;
    }
}

 

TraceStatus 클래스

: 로그의 상태 정보를 나타낸다.

 

TraceStatus는 로그를 시작할 때의 상태 정보를 가지고 있다. 이 상태 정보는 로그를 종료할 때 사용한다.

• traceId : 내부에 트랜잭션 ID와 level을 가지고 있다.

• startTimeMs : 로그 시작시간이다. 로그 종료 시 이 시작 시간을 기준으로 시작~종료까지 전체 수행 시간을 구할 수 있다.

• message : 시작 시 사용한 메세지이다. 이 후 로그 종료 시에도 이 메세지를 사용해서 출력한다.

 

 

◽ advanced > trace > hellotrace > HelloTraceV1

package hello.advanced.trace.hellotrace;
import hello.advanced.trace.TraceId;
import hello.advanced.trace.TraceStatus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@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 을 사용해서 실제 로그를 시작하고 종료할 수 있다. 로그를 출력하고 실행 시간도 측정할 수 있다.

 

◽ test > java > hello > advanced > trace > hellotrace > HelloTraceV1Test

package hello.advanced.trace.hellotrace;

import hello.advanced.trace.TraceStatus;
import org.junit.jupiter.api.Test;

public class HelloTraceV1Test {

    @Test
    void begin_end() {
        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());
    }
}

begin_end() 실행

begin_exception() 실행

 

 

 

 

 

 

 

인프런 ) 스프링 핵심 원리 - 고급편 (김영한)

반응형