데브코스 : 1차 프로젝트 (3일차-오전)

슬키·2025년 12월 17일

🗓️ 3일차

정말 매일 블로그 쓰기란 쉽지 않다.

게다가 나는 주문 관리 파트를 맡았는데,
다른 파트를 맡은 분들의 작업이 아직 끝나지 않은 상태에서
개발을 진행하다보니 이런 요청을 할 일이 많았다.

  • “유저 정보 가져와야 해서 레포지토리에 코드 좀 추가해줄 수 있나요?”
  • “이거 머지 한 번만 해주세요”

알겠다고는 했는데…
왜 머지를 안 해주는지 모르겠다!! 😇
다들 이렇게 팀플을 진행하는 건가…?

문득 멋사 때가 평화로웠다는 생각이 들었다.
아무튼 그렇게 오늘도 얼렁뚱땅 3일차 시작.


어제는 주문 생성 / 조회 API를 구현했고,
오늘은 팀장님의 피드백을 반영하는 걸 오전 목표로,
오후에는 전체적인 코드 리팩토링을 진행하기로 했다.


🔧 코드 리팩토링 예정 목록

  • 한 고객이 하루에 여러 번 주문하더라도 하나의 주문으로 합배송 처리
  • 배송 처리 완료 시 statusCOMPLETE로 변경
  • status = COMPLETE 이고 주문일자가 2일 지난 경우 스케줄러로 삭제
  • MySQL DB 연결 후 전체 기능 테스트
  • User 생성 시 setter 대신 Builder 패턴 적용
  • 기존 주문이 존재할 경우
    (같은 유저, 같은 상품) → quantity 누적 처리

피드백 반영

1️⃣ 응답 형식 통일

기존에는 Controller에서 서비스 레이어의 반환값을 그대로 리턴하는 방식으로 작성되어 있었다.

public OrderResponse getOrder(@PathVariable Long orderId) {
    return orderService.getOrder(orderId);
}

팀장님 피드백으로는,
응답을 ResponseEntity와 공통 응답 객체로 감싸
모든 엔드포인트의 응답 형식을 통일하는 것이 좋아 보인다는 의견이었다.

public ResponseEntity<Response<OrderResponse>> getOrder(@PathVariable Long orderId) {
    OrderResponse res = orderService.getOrder(orderId);
    return ResponseEntity.ok(CommonResponse.success(res), "주문 생성을 성공하였습니다.");
}

코드를 다시 확인해보니,
기존 구현에서는 ResponseEntity를 사용하지 않아
HTTP 상태 코드를 명확하게 지정할 수 없는 상태였다.

ResponseEntity로 감싸면
200 OK, 201 Created, 400 Bad Request
상황에 맞는 응답 상태 코드를 지정할 수 있다.

주문 생성 API의 경우,
리소스를 새로 생성하는 요청이기 때문에
201 Created를 내려주는 것이 적절하다고 판단했다.

@PostMapping("/create")
public ResponseEntity<Response<OrderResponse>> createOrder(@RequestBody CreateOrderRequest req) {
    OrderResponse res = orderService.createOrder(
            req.email(),
            req.address(),
            req.zipcode(),
            req.productId(),
            req.quantity()
    );

    return ResponseEntity.status(201)
            .body(CommonResponse.success(res, "주문 생성을 성공하였습니다."));

2️⃣ 주문 조회 응답 구조 개선 (유저 기준)

기존 구현

전체 주문 조회 API는 주문 리스트를 그대로 반환하는 구조였다.

@GetMapping
    public List<OrderResponse> getAllOrders() {
        return orderService.getAllOrders();

피드백으로는,
추후 고도화 시 유저 기준으로 주문을 묶어서 제공하는 방식
더 직관적일 것 같다는 의견을 받았다.

Map<userId, List<OrderData>>

특히 동일한 유저임에도
주문 일시 기준으로 주문이 나뉘어 있으면
확인하기 어렵다는 점이 있었다.

기존 유저별 주문 조회 API에서는
각 주문 데이터마다 유저 정보가 중복되어 반환되고 있었다.

{
  "email": "john@example.com",
  "address": "some address",
  "zipcode": 12345,
  "orders": [
    {
      "orderId": 3,
      "productId": 4,
      "totalPrice": 20000,
      "orderDate": "2025-04~~",
      "deliveryDate": "2025-04~~"
    },
    {
      "orderId": 6,
      "productId": 1,
      "totalPrice": 10000,
      "orderDate": "2025-04~~",
      "deliveryDate": "2025-04~~"
    }
  ]
}

이를 개선하기 위해
유저 정보는 한 번만 내려주고,
주문 정보만 리스트로 제공하는 구조로 변경했다.

피드백 반영을 위해
유저 기준 주문 응답 전용 DTO를 새로 추가했다.

public record UserOrdersResponse(
        String email,
        String address,
        int zipcode,
        List<OrderResponse> orders
) {

}

Service 수정

  @Transactional(readOnly = true)
public UserOrdersResponse getOrdersByEmail(String email) {
    User user = userRepository.findByEmail(email)
            .orElseThrow(() -> new IllegalArgumentException("유저 없음"));

    List<OrderResponse> orders = orderRepository.findAllByUserOrderByOrderDateDesc(user)
            .stream()
            .map(OrderResponse::from)
            .toList();

    return new UserOrdersResponse(
            user.getEmail(),
            user.getAddress(),
            user.getZipcode(),
            orders
    );
}

Controller 수정

@GetMapping("/user")
public ResponseEntity<Response<UserOrdersResponse>> getOrdersByEmail(@RequestParam String email) {
    UserOrdersResponse res = orderService.getOrdersByEmail(email);
    return ResponseEntity.ok(CommonResponse.success(res, "주문 목록 조회를 성공하였습니다."));
} 

🔁 추가 피드백 반영 (DTO 분리)

응답 데이터를 확인해보니
주문 정보 내부에도 유저 정보가 포함되어 있어
중복 데이터가 발생하고 있었다.

이를 해결하기 위해
주문 아이템 전용 DTO를 새로 분리했다.

 public record UserOrderItemResponse(
        Long orderId,
        Long productId,
        String productName,
        int quantity,
        int totalPrice,
        String status,
        LocalDateTime orderDate,
        LocalDateTime deliveryDate
) {
    public static UserOrderItemResponse from(Order order) {
        return new UserOrderItemResponse(
                order.getOrderId(),
                order.getProduct().getId(),
                order.getProduct().getName(),
                order.getQuantity(),
                order.getTotalPrice(),
                order.getStatus().name(),
                order.getOrderDate(),
                order.getDeliveryDate()
        );
    }
}

이에 맞게 service도 다시 수정해줬다.

service

//    유저별 주문 조회
@Transactional(readOnly = true)
public UserOrdersResponse getOrdersByEmail(String email) {
  User user = userRepository.findByEmail(email)
          .orElseThrow(() -> new OrderException(OrderErrorCode.UNKNOWN_USER));

  List<UserOrderItemResponse> orders =
          orderRepository.findAllByUserOrderByOrderDateDesc(user)
                  .stream()
                  .map(UserOrderItemResponse::from)
                  .toList();

  return new UserOrdersResponse(
          user.getEmail(),
          user.getAddress(),
          user.getZipcode(),
          orders
  );
}

변경 후

변경 후 응답 구조가 훨씬 깔끔해졌다.

DTO를 새로 만든 이유는,
기존 DTO가 이미 다른 곳에서도 사용 중이었기 때문에
연관 관계가 깨질 가능성을 고려하여
분리된 DTO를 새로 생성하는 방식을 선택했다.

🛠️ 커스텀 에러 코드 수정

  • 팀장이 미리 커스텀 에러 구조를 만들어두어, 해당 구조에 맞게 커스텀 에러를 적용하였다.
  Product product = productRepository.findById(productId)
    .orElseThrow(() ->
        new ProductException(
            ProductErrorCode.UNKNOWN_PRODUCT,
            "상품이 존재하지 않습니다. productId=" + productId
        )
    );

🧩 커스텀 에러 코드 생성

팀장에서 커스텀 에러 코드를 만들어보자는 제안이 있었다.

처음에는 구조를 잘 몰라 질문을 통해 방향을 잡았고,
주문 도메인에 맞는 커스텀 에러 코드를 직접 생성했다.

OrderErrorCode

public enum OrderErrorCode implements ErrorCode{
    UNKNOWN_ORDER(HttpStatus.NOT_FOUND, "존재하지 않는 주문입니다.");

    private final HttpStatus httpStatus;
    private final String message;

    OrderErrorCode(HttpStatus httpStatus, String message) {
        this.httpStatus = httpStatus;
        this.message = message;
    }

    @Override
    public HttpStatus getHttpStatus() {
        return httpStatus;
    }

    @Override
    public String getMessage() {
        return message;
    }
}

OrderException

public class OrderException extends BaseException {
    public OrderException(OrderErrorCode errorCode) {
        super(errorCode);
    }
}

🔧 기존 코드 수정

기존에는 주문이 존재하지 않을 경우
명확한 예외 메시지가 내려가지 않았다.

커스텀 에러를 적용하여
아래와 같이 수정하였다.

  Order order = orderRepository.findById(orderId)
    .orElseThrow(() ->
        new OrderException(
            OrderErrorCode.UNKNOWN_ORDER,
            "존재하지 않는 주문입니다. orderId=" + orderId
        )
    );

✅ 적용 후 변화

기존에는 에러 발생 시 응답이 명확하지 않았다.

커스텀 에러 적용 후,
사용자에게 에러 원인이 명확하게 전달되었다.

➕ 추가한 커스텀 에러 코드

UNKNOWN_USER(HttpStatus.NOT_FOUND, "존재하지 않는 사용자 입니다."),
  ALREADY_CANCELED(HttpStatus.BAD_REQUEST, "이미 취소된 주문입니다."),
  COMPLETED_ORDER_CANNOT_DELETE(HttpStatus.BAD_REQUEST, "완료된 주문은 삭제할 수 없습니다."),
INVALID_QUANTITY(HttpStatus.BAD_REQUEST, "주문 수량은 1개 이상이어야 합니다.");

주문 도메인에서 발생할 수 있는 예외 상황을 기준으로
커스텀 에러 코드를 추가 정의하였다.

profile
풀스택 개발자 성장일기

0개의 댓글