종우의 삶 (전체 공개)

Market Project - 6 // 장바구니, 주문 + 마무리 본문

개발/Spring

Market Project - 6 // 장바구니, 주문 + 마무리

jonggae 2024. 3. 10. 01:41

마켓 프로젝트의 마지막 도메인인 장바구니 (Cart) 와 주문 (Order)를 만들어 주었다.

 

이러한 기능들을 만드는 입장에서 고려해야 할 것들이 생각보다 많았다.

 

물론 이것은 개인의 생각이므로 실제 커다란 서비스에는 엄청나게 많은 요구사항들이 있을 것이다.

 

혼자서 여러 도메인을 개발하다 보니 다양한 고려들을 해야했다. 아무튼 무엇이 필요했는지 살펴보자.


1. 장바구니 기능 

우선 남아있는 개발 사항이 단순 CRUD 부분이라, 크게 어려운 부분은 없었다. 

뭔가 면밀하게 무엇이 필요한지 체크해본다.

 

1. 장바구니는 장바구니 객체 자체와 장바구니 내의 '상품'(장바구니 내의 상품)이 있어야 한다.

-> 2개의 엔티티 Cart, CartItem을 만들어준다.

 

2. 회원가입 시 회원 개개인의 장바구니가 함께 생성되어야 한다.

-> 회원 가입 메서드에 회원 가입이 진행될 때 Cart 객체가 Customer마다 생성되도록 로직을 추가해준다.

 

3. 사용자는 장바구니를 조회하거나, 상품 추가, 상품 제거, 담긴 수량 수정, 단일 항목 삭제, 전체 항목 삭제(비우기)를 할 수 있다.

-> 최소한의 서비스를 개발하기로 한다.


2. 주문 기능

주문은 조금 더 고려할 사항이 많았다.

 

단순히 장바구니와 비슷한 로직인줄 알았으나, 주문에는 주문의 '상태'가 존재한다.

 

그리고 [주문 하기] 따위의 버튼을 누르면 주문함에 들어있는 다양한 상품들이 실제 주문이 되고 

다음 진행사항에 따라 계속해서 상태가 변해야한다.

 

이러한 주문의 상태를 OrderStatus enum 으로 만들어 사용하기로 한다.

 

 public enum OrderStatus {
        PENDING_ORDER, // 주문 대기
        PENDING_PAYMENT, //결제 대기
        PAID, // 결제 완료
        PREPARING_FOR_SHIPMENT, // 배송 준비 중
        SHIPPED, // 배송 중
        DELIVERED, // 배송 완료
        CANCELLED // 주문 취소
    }

 

우선 적당히 주문의 상태들을 미리 정해놓고 사용하기로 한다.

 

결제시스템은 존재하지 않기때문에 결제 대기인 PENDING_PAYMENT 상태로 변하는 과정이 바로 '주문하기' -> 주문의 확정이 되겠다.

 

주문함 이라는 가상의 객체에 주문하고자하는 상품을 넣으면, 자동으로 미확정 상태인 주문 객체(일반 주문 객체임)가 생성되고 

PENDING_ORDER 상태로 저장된다.

 

이후 주문 하기 등의 작업을 거쳐 주문이 확정되면 PENDING_ORDER 에서 PENDING_PAYMENT로 결제 대기 상태가 된다. 이러한 상태의 제어를 통해 주문의 프로세스를 진행하기로 한다.


 

1. 주문은 주문주문내 상품 객체가 존재해야 한다.

-> Order와 OrderItem 엔티티를 생성해준다.

 

2. 주문 조회, 주문에 상품 추가, 수량 변경, 삭제 등 CRUD를 할 수 있다.

 

3. 주문 상태에 따라 수정할 수 있는 범위가 달라진다.

-> 주문의 의사가 확정되었다면, 취소가 되기 전까지는 수량 변경이나 삭제를 할 수가 없다. OrderStatus를 확인하는 로직을 추가하여 주문 상태에 따라 접근할 수 있는 메서드들을 제어해준다.

 

장바구니, 주문 전부 엔티티사이의 연관성을 잘 고려햐여 CRUD를 진행해주면 된다.

크게 어렵지는 않았지만 다양한 상황을 전부 신경써주어야겠다.


주문, 장바구니 도메인은 어차피 CRUD가 다인 도메인이다.

자신이 원하는대로 기능을 추가하거나, 수정할 수 있겠다. 

상세 코드는 git에서 확인 가능 -> https://github.com/Jonggae/market

 

이후 테스트코드를 작성하고, Postman으로 테스트를 해준다.  큰 문제는 발생하지 않는다.


3. ApiResponseDto로 응답 균일하게 만들기

API 테스트를 진행하면서 클라이언트에게 반환되는 정보들이 중구난방인 점을 발견했다. 

 

각 Controller에서 return되는 값들을 ApiResponseDto로 균일하게 만들어 사용하기로 한다.

@Builder
@AllArgsConstructor
@Getter
@JsonInclude(JsonInclude.Include.NON_NULL) // null 값이 아닌 필드만 포함
public class ApiResponseDto<T> {

    private boolean success;
    private String message;
    private int statusCode;
    private T data;
    private String errorCode;
    private Object errorDetails;
    private LocalDateTime timeStamp;

    public ApiResponseDto(String message) {
        this.message = message;
    }

    public ApiResponseDto(String message, T data) {
        this.message = message;
        this.data = data;
    }
}

 

이렇게 원하는대로 api응답의 형태를 만들 수 있다.

 

Generic (타입 매개변수) T를 이용하여  data라는 객체를 다양하게 표현해줄수 있다.

예를들어, 장바구니 조회의 경우

ApiResponseDto의 사용

 

이런 식으로 표현이되는데 (장바구니는 비어있음)

응답 코드, 적당한 메시지, 장바구니의 내용등 클라이언트 입장에서 필요한 정보들을 제공해 주는 것이다.

 

반대로 예외, 에러상황이 발생하였을 때에도 비어있는 응답이 아닌 에러 메시지를 날려준다.

 

-> 상품 조회시, 존재하지 않는 상품을 조회하려 했을때.

ApiResponseDto의 사용 - 에러 메시지

 

이러한 작업을 진행하기 위해

 

ApiResponseUtil이라는 실제 사용할 코드들을 모아놓은 클래스를 만들어준다.

성공 success, 실패 error 두가지의 형태를 만들어놓았다.

@Builder
public class ApiResponseUtil {

    public static <T> ResponseEntity<ApiResponseDto<T>> success(String message, T data, int statusCode) {
        ApiResponseDto<T> response = ApiResponseDto.<T>builder()
                .success(true)
                .message(message)
                .statusCode(statusCode)
                .timeStamp(LocalDateTime.now())
                .data(data)
                .build();
        return ResponseEntity.ok(response);
    }

    public static <T> ResponseEntity<ApiResponseDto<T>> error(String message, int statusCode, String errorCode, Object errorDetails) {
        ApiResponseDto<T> response = ApiResponseDto.<T>builder()
                .success(false)
                .message(message)
                .statusCode(statusCode)
                .errorCode(errorCode)
                .errorDetails(errorDetails)
                .timeStamp(LocalDateTime.now())
                .build();
        return new ResponseEntity<>(response, HttpStatus.valueOf(statusCode));
    }
}

 

내부에 ApiResponseDtobuilder 패턴을 이용하여 각 컨트롤러에 사용할 수 있도록 작성한다.

 

상품 조회 컨트롤러

@GetMapping("/{id}")
    public ResponseEntity<ApiResponseDto<ProductDto>> getProduct(@PathVariable Long id) {
        ProductDto product = productService.showProductInfo(id);
        return ApiResponseUtil.success(product.getProductName() + "의 상품 정보입니다", product, 200);
    }

 

성공뿐만 아니라 실패상황에서도 이러한 응답을 보낼 수 있게 예외 처리를 해준다.

 

GlobalExceptionHandler 에서 다양한 에러에 대응할 수 있는 메시지를 생성해준다.

@RestControllerAdvice
public class GlobalExceptionHandler {  

    @ExceptionHandler(NotFoundProductException.class)
    public ResponseEntity<ApiResponseDto<Object>> handleNotFoundProductException(NotFoundProductException ex) {
        String errorMessage = ex.getMessage();
        return ApiResponseUtil.error(
                errorMessage,
                404,
                "NOT_FOUND_PRODUCT",
                null);
    }

 

다양한 예외 처리

 

개개의 상황에 맞는 예외들을 생성하여 처리하는데, 이러한 과정을 다시 한곳에 모아놓는 리팩토링이 가능 할 것이다.

 

적절한 응답과, 에러 처리를 통해 좀 더 완성도있는 프로젝트를 만들 수 있을것이다.

 


마치며,

마지막 장바구니와 주문은 정리가 꼼꼼하지는 못했는데, readme를 업데이트 하면서 기능들을 한번 설명해 볼 생각이다.

지금까지 Market Project라고 이름지은 전자상거래 서비스를 만들어보았다. 

 

아주 최소한의 기능들 위주로 생성하였으므로 많이 추가할 것이 생길것이다.

 

앞으로는 리팩토링 위주로 블로그에 정리해보도록 하겠다.

 

도메인의 기능 자체를 만드는 것은 그리 어려운 내용이 아니었다. CRUD는 다양한 엔티티들의 관계가 더 중요했다.

 

작성한 코드들의 가독성, 머리속으로 만들어 놓은 것들을 옮겨낼 수 있는 능력.

로직을 간소화하여 성능을 좀 더 빠르고 정확하게 실행할 수 있는 방법들을 항상 생각해보아야겠다.


 

다음의 목표들은 무엇이 있을까.?

 

1. 새로운 기능 추가하기.

2. 대용량 데이터 (몇천, 몇만개)를 어떻게 처리할 수 있는가. 캐싱?

3. 프론트엔드, view페이지를 만들어 적용해 볼 수 있을까?

4. CI/CD

5. 배포

6. 다양한 리팩토링 등등......

 

많은 시도들을 해보며 지식을 쌓아보아야겠다. 우선은 리팩토링부터! 파이팅!!

Comments