Today I Learned!
지난 1차 과제에 이어 2차 과제를 진행했다. 1차 과제 관련 TIL 은 아래 링크를 통해 확인할 수 있다.
[내일배움캠프] 260415 TIL #8 CRUD Code Shadowing
들어가며입문 프로젝트에 앞서, CRUD Code Shadowing 과제가 주어졌다. 오늘은 "따라 치기 과제"로, 작성되어 있는 코드를 보고 따라서 작성하면 되는 과제이다. 노션 페이지를 보고 단순히 따라쳐도
sxxlx.tistory.com
작성한 코드는 아래 GitHub Repository에서 확인할 수 있다.
GitHub - seola12e/CRUD-Code-Shadowing: [내일배움캠프 단기심화 7기] CRUD Code Shadowing 과제 레포지토리
[내일배움캠프 단기심화 7기] CRUD Code Shadowing 과제 레포지토리. Contribute to seola12e/CRUD-Code-Shadowing development by creating an account on GitHub.
github.com
이번 2회차 때에는 OrderRepository, Order 관련 요청/응답 DTO, 그리고 비즈니스 로직이 분리가 되지 않은 OrderController를 작성했다.
1. 코드 작성 (비즈니스 로직 분리 전)
OrderRepository
package com.sparta.miniorder.order.repository;
import com.sparta.miniorder.order.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
public interface OrderRepository extends JpaRepository<Order,Long> {
}
OrderRequest
package com.sparta.miniorder.order.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class OrderRequest {
@NotNull(message = "주문하기 위해서는 상품 ID가 필요합니다.")
private Long productId;
}
OrderResponse
package com.sparta.miniorder.order.dto;
import com.sparta.miniorder.order.entity.Order;
import lombok.Getter;
@Getter
public class OrderResponse {
private final Long orderId;
/**
* Product 관련 반환은 productId, productName, productPrice 로 한다.
*/
private final Long productId;
private final String productName;
private final Integer productPrice;
public OrderResponse(Order saved) {
this.orderId = saved.getId();
this.productId = saved.getProduct().getId();
this.productName = saved.getProduct().getName();
this.productPrice = saved.getProduct().getPrice();
}
}
OrderController (비즈니스 로직 분리 전)
// import 생략
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
// 주문 생성 API
@PostMapping
@Transactional
public ResponseEntity<OrderResponse> createOrder(@Valid @RequestBody OrderRequest request) {
Product product = productRepository.findById(request.getProductId())
.orElseThrow(() -> new IllegalArgumentException(
"해당 상품이 존재하지 않습니다. id = " + request.getProductId()
));
Order order = new Order(product);
Order saved = orderRepository.save(order);
OrderResponse response = new OrderResponse(saved);
return ResponseEntity.created(URI.create("/api/orders/" + response.getOrderId())).body(response);
}
// ID별 주문 조회 API
@GetMapping("/{id}")
@Transactional(readOnly = true)
public ResponseEntity<OrderResponse> getOrder(@PathVariable Long id) {
Order order = orderRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException(
"해당 주문이 존재하지 않습니다. id = " + id
));
return ResponseEntity.ok(new OrderResponse(order));
}
}
2. 프로젝트 실행 (비즈니스 로직 분리 전)
코드를 작성한 후 프로젝트를 실행시켜서 JPA를 통해 orders 라는 테이블이 생성되었음을 확인할 수 있었다.

이전에 실습했던 내용 그대로 products 테이블 안에는 3개의 상품 데이터가 존재했다.
이 중 상품 ID가 4번인 데이터로 API 테스트를 진행했다.

3. API 테스트 (비즈니스 로직 분리 전)
지난 번과 동일하게 Postman을 통해 테스트를 진행했다.
a. 주문 생성 API 테스트
OrderRequest에는 productId 필드가 있기 때문에 productId 에 4를 Body에 넣어서 요청을 보냈더니 201 Created 와 함께 잘 생성되었음을 확인할 수 있었다. 데이터베이스에도 마찬가지로 order 테이블에 id 가 2(이미 테스트를 한 번 했기 때문), product_id가 4로 잘 들어가 있음을 확인했다.


실패 테스트를 위해 존재하지 않는 상품 ID 10번을 넣어서 테스트 해봤더니, 작성한 메시지와 함께 오류가 throw 되었음을 확인할 수 있었다.

b. 주문 조회 API 테스트
데이터베이스 상에서 Order ID 값이 2로 되어 있으니 파라미터 값에 2를 넣고 GET 요청을 보냈더니 200 OK 상태코드와 함께 정상적으로 조회되었음을 확인할 수 있었다.

마찬가지로 실패 테스트를 진행하기 위해 존재하지 않는 주문 ID인 10번을 넣었더니, 내가 작성해준 default 메시지와 함께 오류가 throw 되었음을 확인할 수 있었다.

트러블 슈팅
주문 조회 API 테스트를 할 때 아래와 같은 오류가 발생했었다.

확인해 보니 OrderResponse DTO 필드에 Product 엔티티를 그대로 반환하고 있던 게 문제가 됐다.
@Getter
public class OrderResponse {
private final Long orderId;
private final Product product; // 문제 구간
public OrderResponse(Order saved) {
this.orderId = saved.getId();
this.product = saved.getProduct();
}
}
위와 같이 코드를 작성하게 되면 HIbernate 가 감싼 대리 객체 즉, 실제 객체가 아닌 프록시 객체가 들어있기 때문에 JSON으로 Parsing하지 못하게 된다.
주문 생성 테스트를 진행했을 때에는 아래와 같이 Product 엔티티를 DTO에 담아서 그대로 반환하는 것에 성공했었다.

이렇게 잘 반환할 수 있었던 이유는 프록시 객체가 아닌 실제 객체를 들고 있었기 때문에, 생성 후 바로 JSON 형식으로 반환할 수 있었던 것이다.
하지만 Order 엔티티에서 Product 엔티티를 참조할 때 지연 로딩으로 매핑되어 있기 때문에 조회 시점에서는 프록시 객체를 가져오게 된다. log를 찍어봤더니 실제로도 com.sparta.miniorder.product.entity.Product$HibernateProxy 로 나왔다. 즉, Product를 조회하는 해당 시점에서는 Proxy 객체를 들고 있다는 뜻이 된다.

따라서 아래와 같이 DTO를 재작성하여 문제를 해결했다.
@Getter
public class OrderResponse {
private final Long orderId;
private final Long productId;
private final String productName;
private final Integer productPrice;
public OrderResponse(Order saved) {
this.orderId = saved.getId();
this.productId = saved.getProduct().getId();
this.productName = saved.getProduct().getName();
this.productPrice = saved.getProduct().getPrice();
}
}
API 테스트를 통해서도 아래와 같이 정상적으로 반환되는 것을 확인할 수 있었다.

'내일배움캠프 > TIL' 카테고리의 다른 글
| [내일배움캠프] 260430 TIL #12 입문프로젝트 마무리 (0) | 2026.04.30 |
|---|---|
| [내일배움캠프] 260419 TIL #11 CRUD Code Shadowing 3차(최종) (0) | 2026.04.19 |
| [내일배움캠프] 260416 TIL #9 입문 프로젝트 시작 (0) | 2026.04.16 |
| [내일배움캠프] 주특기 입문 MSA 요약본 (0) | 2026.04.15 |
| [내일배움캠프] 260415 TIL #8 CRUD Code Shadowing (0) | 2026.04.15 |