[TIL] CRUD Code Shadowing 2회차
주문과 상품의 다대일 관계를 JPA @ManyToOne으로 매핑하고 지연 로딩을 정리한 글입니다.
For the English version of this post, see here.
[TIL] CRUD Code Shadowing 2회차
Entity
@ManyToOne어노테이션- 관계 설정 - 여러 개의 Order가 하나의 Product를 참조한다
- Order : Product = N : 1
- 헷갈리니까 Order 클래스에서 작성하였으니까, Many Order to One Product로 해석해보자
fetch = FetchType.LAZY- 지연 로딩
- Order 조회할 때 Product를 바로 가져오지 않음
- 실제로 order.getProduct() 할 때 DB 조회
- ‘일단 주문만 가져오고, 상품은 필요할 때만 꺼내보자’ 라는 의미
optional = false- null 허용 안함
- product는 반드시 있어야 함
- 즉, 상품 없는 주문은 허용되지 않는다는 뜻
@JoinColumn(name = "product_id", nullable = false)어노테이션- FK 설정
- DB에서 실제 컬럼 생성됨
- orders 테이블에 product_id 컬럼 생성
nullable = false
- DB에서도 null 금지
생성자
1
2
3
public Order(Product product) {
this.product = product;
}
- Order 만들 때 반드시 Product를 넣게 강제함 ex)
new Order();-> 안됨: 기본 생성자 protected라 못씀 ex)new Order(product);-> 가능! - 상품 없는 주문은 만들지도 못하게 막음
Prodcut를 참조하는지 어떻게 아는가
private Product product;
- 어노테이션 밑에 정의된 필드 타입이 Product이므로,
- JPA는 @ManyToOne + 필드 타입을 보고 판단함 -> 어노테이션은 관계 종류를 알려주고, 타입은 누구랑 관계인지 알려줌
- @JoinColumn도 마찬가지
- 현재 테이블에 어떤 FK 컬럼을 만들지 알려줌
LAZY를 안 쓰면 왜 N+1 문제가 생기는지
- @ManyToOne의 기본 fetch 전략은 EAGER -> 즉, LAZY를 안 쓰면 보통 Order를 조회할 때 Product도 같이 가져오려고 함 ex)
List<Order> orders = orderRepository.findAll();주문이 10개 있을 때, 각 주문은 product를 가지고 있기 때문에, JPA 입장에서는 ‘주문만 가져오면 안 되고 상품도 채워놔야지’라고 생각할 수 있음- 주문 목록 조회
select * from orders;: 이걸로 주문 10개를 가져옴 = 이게 1번 쿼리 - 각 주문마다 연결된 상품 조회
1 2 3 4
select * from product where id = 1; select * from product where id = 2; select * from product where id = 3; ...
-> 주문 수만큼 추가 조회가 나감 = 이게 N번 쿼리 => N+1: 처음 목록 조회 1번 + 연관 엔티티 조회 N번
- 주문 목록 조회
- LAZY
- Product가 필요할 때까지 조회 미뤄줌
- LAZY 자체가 N+1을 자동 해결하는 건 아니지만, 대신 불필요한 즉시 조회를 막아주고
- 필요할 때는 fetch join 같은 걸로 직접 최적화할 수 있게 해줌
- EAGER
- order만 가져오는 게 아니라 연결된 product도 함께 조회하려고 함
- 무조건 조인 한 번으로 가져오는 게 아님 (즉시 로딩 != SQL JOIN 한 번) -> 즉시 로딩해야 한다는 건 보장하지만, 어떤 SQL로 가져올지를 보장하지 않음
- 어떤 경우엔 join으로 한 번에 가져올 수도 있고
- 어떤 경우엔 먼저 orders로 조회하고
- 그 다음 product를 하나씩 따로 조회할 수도 있음
Controller
@RestController어노테이션- @Controller + @ResponseBody 합친 것
- 이 클래스의 모든 메서드는 결과를 JSON으로 반환한다는 뜻
@RequestMapping어노테이션- 기본 URL 설정
- 이 컨트롤러의 모든 API 앞에 붙음
@RequiredArgsConstructor어노테이션- Lombok
- final 붙은 필드 자동으로 생성자 만들어줌 + @NonNull 붙은 필드
- 필수 값만 받는 생성자 (@NoArgsConstructor: 아무 값도 안 받는 기본 생성자)
@RequiredArgsConstructor와 @NoArgsConstructor의 차이점
- @RequiredArgsConstructor: 필수 값만 받는 생성자
1 2 3 4 5 @RequiredArgsConstructor public class OrderService { private final ProductRepository productRepository; private final OrderRepository orderRepository; }
- @RequiredArgsConstructor를 사용하면
1 2 3 4 5 public OrderService(ProductRepository productRepository, OrderRepository orderRepository) { this.productRepository = productRepository; this.orderRepository = orderRepository; }- 위 코드가 자동 생성됨
- @NoArgsConstructor: 아무 값도 안 받는 기본 생성자
1 2 3 4 5 @NoArgsConstructor public class Order { private Long id; private String name; }
- @NoArgsConsturctor를 사용하면
1 2 public Order() { }- 위 코드가 자동 생성됨
@PostMapping어노테이션- HTTP POST 요청 처리
1
public ResponseEntity<ProductResponse> createProduct(@Valid @RequestBody ProductRequest request)
@RequestBody어노테이션- 요청 Body(JSON)를 객체로 변환
@Valid어노테이션 ex) DTO에 @NotBlank 같은 게 있으면, 값이 없으면 자동으로 에러 발생- 반환 타입
ResponseEntity<ProductResponse> - HTTP 응답 전체를 컨트롤
- 상태코드 + 바디 같이 반환 가능
ResponseEntity: Spring Framework에서 제공하는 클래스- HTTP 응답 전체(상태코드 + 헤더 + 바디)를 담는 객체
- 제네릭 문법 ResponseEntity
-> 여기서 T = 응답 바디 타입
- 반환 타입
- 핵심 로직
ProductResponse response = productService.createProduct(request);- 흐름
- 요청 받음
- DTO로 변환됨
- 서비스에 넘김
- 결과를 받아옴
- 흐름
- 반환 값
return ResponseEntity.created(URI.create("/api/products/" + response.getId())).body(response);ResponseEntity.created(...): HTTP 201 Created 상태코드로 응답한다는 의미- Location 헤더도 같이 설정함
URI.create("/api/products/" + response.getId())-> 예를 들어 id가 10이면, `Location: /api/products/10’ -> 이 리소스 여기서 확인 가능해요! 라는 의미
- Location 헤더도 같이 설정함
.body(response): 실제 응답 데이터 넣는 부분 ex) 상태코드(201), 헤더(Location), 바디(response(JSON)) ``` http HTTP/1.1 201 Created Location: /api/products/10 Content-Type: application/json
{ “id”: 10, “name”: “콜라”, “price”: 2000 } ``` -> 실행하면 이런 식으로 HTTP 응답이 구성됨
- 반환 타입
ResponseEntity<void>- Void: 응답 body 없음
ResponseEntity.noContent().build()- HTTP 204 No Content-> body 없음 (진짜 텅 비어있음)
1
HTTP/1.1 204 No Content
Respository
- DB 접근 담당하는 인터페이스 (Repository)
- Product를 DB에서 저장/조회/삭제하는 도구
구현 코드가 없는데 왜 동작하는지
- Spring Data JPA가 자동으로 구현해줌
- 인터페이스만 정의하면, 스프링이 런타임에 구현체를 만들어줌
JpaRepository<엔티티(Product), 타입(Long)>- Product = 어떤 테이블 다룰지
- Long = PK 타입 (id 타입) -> ex) save(product); findById(id); findAll(); deleteById(id); existsById(id); 등의 메서드를 다 사용할 수 있게 됨 (직접 SQL로 안 짜도 됨) -> 실제 사용 ex)
productRepository.save(product);,productRepository.findById(1L);
- 인터페이스로 정의하는 이유: 구현은 스프링이 대신 해주기 때문
- 추가 기능도 만들 수 있음 ex)
List<Product> findByName(String name);-> 메서드 이름만 같은 형식으로 지으면, 메서드 이름을 보고 자동으로 쿼리가 생성되기 때문에 구현할 필요도 없음