JPA API and Performance Optimization
์ํ๋์ ์ค์ ! ์คํ๋ง ๋ถํธ์ JPA ํ์ฉ2 - API ๊ฐ๋ฐ๊ณผ ์ฑ๋ฅ ์ต์ ํ ๊ฐ์๋ฅผ ์์ฝํ ๋ด์ฉ์ ๋๋ค.
API Basic
API์ Template Engin์ ๊ณตํต ์ฒ๋ฆฌ๋ฅผ ํด์ผ ํ๋ ์์๊ฐ ๋ค๋ฅด๋ฏ๋ก ํจํค์ง๋ฅผ ๋ถ๋ฆฌํ์ฌ ๊ด๋ฆฌํ๋ ๊ฒ์ด ์ข์.
ใด src.main.java
ใด example
ใด api
ใด controller
ํ์ ๋ฑ๋ก API
์ํฐํฐ ๋์ API ์์ฒญ ์คํ์ ๋ง๋ ๋ณ๋ DTO๋ฅผ ์ฌ์ฉ
ํ๊ธฐ.
์ํฐํฐ์ ํ๋ ์ ํ ์ด์ (API) ๊ณ์ธต์ ์ํ ๋ก์ง ๋ถ๋ฆฌํ ์ ์์
์ํฐํฐ๊ฐ ๋ณ๊ฒฝ๋์ด๋ API ์คํ์ด ๋ณํ์ง ์์
์ํฐํฐ ํ๋๊ฐ ๋ณ๊ฒฝ๋๋๋ผ๋ ์ปดํ์ผ ์๋ฌ๋ก ๋ฐ๋ก ์ฒดํฌ ๊ฐ๋ฅ
์ํฐํฐ์ API ์คํ์ ๋ช ํํ๊ฒ ๋ถ๋ฆฌํ ์ ์์
์ค๋ฌด์์๋ ์ ๋ ์ํฐํฐ๋ฅผ API ์คํ์ ๋ ธ์ถํ์ง ๋ง์!
@PostMapping("/api/members")
public CreateMemberResponse saveMember(@RequestBody @Valid CreateMemberRequest request) {
Member member = new Member();
member.setName(request.getName());
member.setAddress(request.getAddress());
Long id = memberService.join(member);
return new CreateMemberResponse(id);
}
@Data
static class CreateMemberRequest {
@NotEmpty
private String name;
@Embedded
private Address address;
}
@Data
static class CreateMemberResponse {
private Long id;
public CreateMemberResponse(Long id) {
this.id = id;
}
}
ํ์ ์์ API
๋ฑ๋ก๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก ๋ณ๋ DTO ์ฌ์ฉ
ํ๊ธฐ
๋ณ๊ฒฝ๊ฐ์ง๋ฅผ ํ์ฉํด์ ๋ฐ์ดํฐ ์์ ํ๊ธฐ
CQS(Command-Query Separation) : ๊ฐ๊ธ์ ์ด๋ฉด Command์ Query๋ฅผ ๋ถ๋ฆฌํ์.
Controller
@PutMapping("/api/members/{id}")
public UpdateMemberResponse updateMember(@PathVariable("id") Long id, @RequestBody @Valid UpdateMemberRequest request) {
memberService.update(id, request.getName()); // Command
Member findMember = memberService.findOne(id); // Query
return new UpdateMemberResponse(findMember.getId(), findMember.getName());
}
@Data
static class UpdateMemberRequest {
private String name;
}
@Data
@AllArgsConstructor
static class UpdateMemberResponse {
private Long id;
private String name;
}
Service
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
//...
@Transactional
public void update(Long id, String name) {
Member member = memberRepository.findOne(id);
member.setName(name);
// Transactional commit -> flush (๋ณ๊ฒฝ๊ฐ์ง)
}
}
ํ์ ์กฐํ API
๋ฑ๋ก, ์์ ๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก ์ํฐํฐ๋ฅผ API ์๋ต ์คํ ๋ง๋ ๋ณ๋ DTO๋ก ๋ณํํ์ฌ ๋ฐํ
ํ๊ธฐ
์ํฐํฐ๊ฐ ๋ณ๊ฒฝ๋์ด๋ API ์คํ์ด ๋ณ๊ฒฝ๋์ง ์์
Result ํด๋์ค๋ก ์ปฌ๋ ์ ์ ๊ฐ์ธ์ฃผ๋ฉด์ ํฅํ ํ์ํ ํ๋๋ฅผ ์์ ๋กญ๊ฒ ์ถ๊ฐ ๊ฐ๋ฅ
API ์์ฒญ์ ํ์ํ ํ๋๋ง ๋ ธ์ถ (์ฉ๋์ ๋ฐ๋ผ DTO๋ฅผ ์์ฑ)
@GetMapping("/api/members")
public Result member() {
List<Member> findMembers = memberService.findMembers();
List<MemberDto> collect = findMembers.stream()
.map(m -> new MemberDto(m.getName()))
.collect(Collectors.toList());
return new Result(collect.size(), collect);
}
@Data
@AllArgsConstructor
static class Result<T> {
private int count;
private T data;
}
@Data
@AllArgsConstructor
static class MemberDto {
private String name;
}
์ง์ฐ ๋ก๋ฉ๊ณผ ์กฐํ ์ฑ๋ฅ ์ต์ ํ
์ง์ฐ ๋ก๋ฉ์ผ๋ก ๋ฐ์ํ๋ ์ฑ๋ฅ ๋ฌธ์ ๋ฅผ ๋จ๊ณ์ ์ผ๋ก ํด๊ฒฐํด๋ณด์.
.
์ํฐํฐ๋ฅผ ์ง์ ๋
ธ์ถ
์ํ ์ฐธ์กฐ ๋ฌธ์ ๋ฐ์
StackOverflowError
@JsonIgnore ์ค์ ์ผ๋ก ํด๊ฒฐ ๊ฐ๋ฅ
@JsonIgnore ์ ์ถ๊ฐํ๋๋ผ๋ ์ง์ฐ๋ก๋ฉ์ผ๋ก ์ธํ proxy(bytebuddy) ๊ฐ์ฒด๋ฅผ jackson ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ฝ์ ์ ์๋ ๋ฌธ์ ๋ฐ์
Type definition error
Hibernate5Module ์ Spring Bean ์ผ๋ก ๋ฑ๋กํ๋ฉด ํด๊ฒฐ ๊ฐ๋ฅ
๋จ, ์ง์ฐ ๋ก๋ฉ ๊ฐ์ฒด๋ null ์ถ๋ ฅ, ๊ฐ์ ์ง์ฐ ๋ก๋ฉ๋ ๊ฐ๋ฅํ์ง๋ง ์ฑ๋ฅ ์ ํ ๋ฐ์
// OrderSimpleApiController.java
@GetMapping("/api/simple-orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAllByString(new OrderSearch());
for (Order order : all) {
order.getMember().getName(); // Lazy ๊ฐ์ ์ด๊ธฐํ
order.getDelivery().getAddress(); // Lazy ๊ฐ์ ์ด๊ธฐํ
}
return all;
}
์ํฐํฐ๋ฅผ DTO๋ก ๋ณํ
์ํฐํฐ๋ฅผ ์ง์ ๋ ธ์ถํ์ง ์๊ณ ,
์ํฐํฐ๋ฅผ DTO๋ก ๋ณํ
1 + N ๋ฌธ์ ๋ฐ์ (์ํฐํฐ ์ง์ ๋ ธ์ถ๊ณผ ๋์ผ)
์ฒซ ๋ฒ์งธ ์ฟผ๋ฆฌ์ ๊ฒฐ๊ณผ N๋ฒ ๋งํผ ์ฟผ๋ฆฌ๊ฐ ์ถ๊ฐ๋ก ์คํ๋๋ ๋ฌธ์
ex) Order ์กฐํ ์ Member - N๋ฒ, Delivery - N ๋ฒ, ์ด 1 + N + N ๊ฐ์ ์ฟผ๋ฆฌ ๋ฐ์
// OrderSimpleApiController.java
@GetMapping("/api/simple-orders")
public List<SimpleOrderDto> ordersV2() {
List<SimpleOrderDto> result = orderRepository.findAllByString(new OrderSearch()).stream()
.map(SimpleOrderDto::new)
.collect(toList());
return result;
}
@Data
static class SimpleOrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
public SimpleOrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName(); // LAZY ์ด๊ธฐํ
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress(); // LAZY ์ด๊ธฐํ
}
}
ํ์น ์กฐ์ธ ์ต์ ํ
ํ์น ์กฐ์ธ์ ์ฌ์ฉํด์ 1 + N ๋ฌธ์ ๋ฅผ ์ฟผ๋ฆฌ 1๋ฒ ๋ง์ ์กฐํ
์ฝ๋๊ฐ ๊ฐ๊ฒฐํ๊ณ , ๋ค๋ฅธ API์์ ์ฌ์ฌ์ฉ์ด ์ฌ์
// OrderRepository.java
public List<Order> findAllWithMemberDelivery() {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class)
.getResultList();
}
DTO๋ก ๋ฐ๋ก ์กฐํ
์กฐํ๋ ์ํฐํฐ๋ฅผ DTO๋ก ๋ณํํ๋ ๊ณผ์ ํ์ ์์ด, ๋ฐ๋ก DTO ์กฐํํด์ ์ฑ๋ฅ ์ต์ ํํ๊ธฐ
์ํ๋ ํ๋๋ง ์ ํ(SELECT)ํด์ ์กฐํ
DB <-> ๋คํธ์ํฌ ์ฉ๋ ์ต์ ํ (์๊ฐ๋ณด๋ค ๋ฏธ๋นํ ์ฐจ์ด)
new ๋ช ๋ น์ด๋ฅผ ์ฌ์ฉํด์ JPQL์ ๊ฒฐ๊ณผ๋ฅผ DTO๋ก ์ฆ์ ๋ณํ
API ์คํ์ ๋ง์ถ๋ค๋ณด๋ ๋ณ๊ฒฝ์ด ์ด๋ ค์ฐ๋ฏ๋ก, ๋ค๋ฅธ API์์ Repository ์ฌ์ฌ์ฉ์ด ์ด๋ ค์
์ฌ์ฉํ ๊ฒฝ์ฐ ์์ํ ์ํฐํฐ๋ฅผ ์กฐํํ๋ ๋ ํ์งํ ๋ฆฌ์ ํ๋ฉด ์ข ์์ ์ธ ๋ ํ์งํ ๋ฆฌ๋ฅผ ๋ถ๋ฆฌํ๋ ๊ฒ์ ์ถ์ฒ
// OrderRepository.java
public List<OrderSimpleQueryDto> findOrderDtos() {
return em.createQuery(
"select new jpabook.jpashop.repository.order.simplequery.OrderSimpleQueryDto(o.id, m.name, o.orderDate, o.status, d.address)" +
" from Order o" +
" join o.member m" +
" join o.delivery d", OrderSimpleQueryDto.class)
.getResultList();
}
์ปฌ๋ ์
์กฐํ ์ต์ ํ
toOne(OneToOne, ManyToOne)๊ด๊ณ์ ์ด์ด์ ์ปฌ๋ ์ ์ธ ์ผ๋๋ค ๊ด๊ณ(OneToMany)๋ฅผ ์ต์ ํํด๋ณด์.
toOne ๊ด๊ณ์ ๋์ผํ ๋ถ๋ถ๋ค์ด ํฌํจ๋์ด ์๋ค.
.
์ํฐํฐ๋ฅผ ์ง์ ๋
ธ์ถ
์ํฐํฐ๊ฐ ๋ณํ๋ฉด API ์คํ์ด ๋ณํจ
ํธ๋์ญ์ ์์์ ์ง์ฐ ๋ก๋ฉ(LAZY) ๊ฐ์ ์ด๊ธฐํ ํ์
์๋ฐฉํฅ ์ฐ๊ด๊ด๊ณ ๋ฌธ์ (@JsonIgnore ์ค์ ์ผ๋ก ํด๊ฒฐ ๊ฐ๋ฅํ์ง๋ง ์ง์ฐ๋ก๋ฉ ๊ฐ์ฒด๋ฅผ ์ฝ์ ์ ์๋ ๋ฌธ์ ๋ฐ์ํ ์ ์์)
// OrderApiController.java
@GetMapping("/api/orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAllByString(new OrderSearch());
for (Order order : all) {
order.getMember().getName(); // Lazy ๊ฐ์ ์ด๊ธฐํ
order.getDelivery().getAddress(); // Lazy ๊ฐ์ ์ด๊ธฐํ
List<OrderItem> orderItems = order.getOrderItems();
orderItems.stream().forEach(o -> o.getItem().getName()); // Lazy ๊ฐ์ ์ด๊ธฐํ
}
return all;
}
์ํฐํฐ๋ฅผ DTO๋ก ๋ณํ
ํธ๋์ญ์ ์์์ ์ง์ฐ ๋ก๋ฉ ํ์ (์ง์ฐ ๋ก๋ฉ์ผ๋ก ๋๋ฌด ๋ง์ SQL ์คํ)
์ง์ฐ ๋ก๋ฉ์ ์์์ฑ ์ปจํ ์คํธ์ ์๋ ์ํฐํฐ ์ฌ์ฉ์ ์๋ํ๊ณ ์์ผ๋ฉด SQL์ ์คํ
ex) Order ์กฐํ ์ Member - N๋ฒ, Address - N ๋ฒ, OrderItem - N ๋ฒ, item M๋ฒ
N : order ์กฐํ ์, M : orderItem ์กฐํ ์
์ด 1 + N + N + N + M ๊ฐ์ ์ฟผ๋ฆฌ ๋ฐ์
@GetMapping("/api/orders")
public List<OrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<OrderDto> result = orders.stream()
.map(OrderDto::new)
.collect(toList());
return result;
}
@Data
static class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemDto> orderItems;
public OrderDto(Order order) {
this.orderId = order.getId();
this.name = order.getMember().getName();
this.orderDate = order.getOrderDate();
this.orderStatus = order.getStatus();
this.address = order.getDelivery().getAddress();
this.orderItems = order.getOrderItems().stream() // ์ํฐํฐ ํ๋ ๋ํ DTO๋ก ๋ณํ
.map(OrderItemDto::new)
.collect(toList());
}
}
@Data
static class OrderItemDto {
private String itemName;
private int orderPrice;
private int count;
public OrderItemDto(OrderItem orderItem) {
this.itemName = orderItem.getItem().getName();
this.orderPrice = orderItem.getOrderPrice();
this.count = orderItem.getCount();
}
}
ํ์น ์กฐ์ธ ์ต์ ํ
ํ์น ์กฐ์ธ์ผ๋ก SQL 1๋ฒ๋ง ์คํ
๋จ, ์ปฌ๋ ์ ํ์น ์กฐ์ธ ์ฌ์ฉ ์
ํ์ด์ง์ด ๋ถ๊ฐ๋ฅํ ๋จ์
์ด ์กด์ฌํ์ด๋ฒ๋ค์ดํธ๋ ๊ฒฝ๊ณ ๋ก๊ทธ๋ฅผ ๋จ๊ธฐ๋ฉด์ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ DB์์ ์ฝ์ด์ค๊ณ , ๋ฉ๋ชจ๋ฆฌ์์ ํ์ด์ง ์์ - OOM ๋ฐ์ ์ํ
์ปฌ๋ ์ ํ์น ์กฐ์ธ์
1๊ฐ๋ง ์ฌ์ฉ ๊ฐ๋ฅ
๋ ์ด์์ ์ปฌ๋ ์ ์ ํ์น ์กฐ์ธ์ ์ฌ์ฉํ๋ฉด ๋ฐ์ดํฐ๊ฐ ๋ถ์ ํฉํ๊ฒ ์กฐํ๋ ์ ์์ (1 * N * N..)
JPA
distinct
๋ SQL์ distinct ์ถ๊ฐ ๋ฐ ๊ฐ์ ์ํฐํฐ(=id)๊ฐ ์กฐํ๋๋ฉด ์ ํ๋ฆฌ์ผ์ด์ ์์์ค๋ณต์ ์ ๊ฑฐ
1:N ์กฐ์ธ์ด ์์ผ๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค row๊ฐ ๋ปฅํ๊ธฐ๋์ด distinct ํ์
// OrderApiController.java
@GetMapping("/api/orders")
public List<OrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithItem();
List<OrderDto> result = orders.stream()
.map(OrderDto::new)
.collect(toList());
return result;
}
// OrderRepository.java
public List<Order> findAllWithItem() {
return em.createQuery(
"select distinct o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d" +
" join fetch o.orderItems oi" +
" join fetch oi.item i", Order.class)
.getResultList();
}
ํ์ด์ง
์ปฌ๋ ์
ํ์น ์กฐ์ธ์์ ํ์ด์ง์ด ๋ถ๊ฐ๋ฅํ ๋จ์ ์กด์ฌ
์ผ๋๋ค(1:N) ์กฐ์ธ์ด ๋ฐ์ํ๋ฏ๋ก ๋ฐ์ดํฐ๊ฐ ์ผ(1) ๊ธฐ์ค์ด ์๋ ๋ค(N)๋ฅผ ๊ธฐ์ค์ผ๋ก row๊ฐ ์์ธกํ ์ ์์ด ์ฆ๊ฐ
์ผ(1)์ธ Order ๊ธฐ์ค์ผ๋ก ํ์ด์ง ํ๊ณ ์ถ์ง๋ง, ๋ค(N)์ธ OrderItem์ ์กฐ์ธํ๋ฉด OrderItem์ด ๊ธฐ์ค์ด ๋์ด๋ฒ๋ฆฌ๋ ๋ฌธ์
์ด ๊ฒฝ์ฐ ํ์ด๋ฒ๋ค์ดํธ๋ ๊ฒฝ๊ณ ๋ก๊ทธ๋ฅผ ๋จ๊ธฐ๊ณ ๋ชจ๋ DB ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ํ ๋ฉ๋ชจ๋ฆฌ์์ ํ์ด์ง์ ์๋ (์ต์ ์ ๊ฒฝ์ฐ OOM ์ฅ์ ๋ฐ์)
ํ์ด์ง + ์ปฌ๋ ์ ์ํฐํฐ ์กฐํ ๋ฌธ์ ํด๊ฒฐ
ToOne(OneToOne, ManyToOne) ๊ด๊ณ๋ ๋ชจ๋
ํ์น ์กฐ์ธ
์ผ๋ก์ปฌ๋ ์ ์
์ง์ฐ ๋ก๋ฉ
์ผ๋ก ์กฐํ์ง์ฐ ๋ก๋ฉ ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํด
hibernate.default_batch_fetch_size
(๊ธ๋ก๋ฒ ์ค์ ) ๋๋@BatchSize
(๊ฐ๋ณ ์ต์ ํ) ์ ์ฉ์ปฌ๋ ์ ์ด๋, ํ๋ก์ ๊ฐ์ฒด๋ฅผ ํ๊บผ๋ฒ์ ์ค์ ํ size ๋งํผ IN ์ฟผ๋ฆฌ ์กฐํ
default_batch_fetch_size ์ฌ์ด์ฆ ์ ํ
์ ๋นํ ์ฌ์ด์ฆ๋ 100~1000 ์ฌ์ด ๊ถ์ฅ
IN ์ ํ๋ผ๋ฏธํฐ๋ฅผ 1000 ์ผ๋ก ์ ํํ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ์์(max size = 1,000)
์ฌ์ด์ฆ๋ฅผ ๋๊ฒ ์ค์ ํ ๊ฒฝ์ฐ ํ๊บผ๋ฒ์ DB์์ ์ ํ๋ฆฌ์ผ์ด์ ์ผ๋ก ๋ถ๋ฌ์ค๋ฏ๋ก DB์ ์๊ฐ ๋ถํ๊ฐ ์ฆ๊ฐํ ์ ์์.
ํ์ง๋ง ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฌ์ด์ฆ๊ฐ ์ด๋ป๊ฒ ์ค์ ์ด ๋์ด์๋ ๊ฒฐ๊ตญ ์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ๋ก๋ฉํด์ผ ํ๋ฏ๋ก ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ด ๊ฐ๋ค.
1000 ์ผ๋ก ์ค์ ํ๋ ๊ฒ์ด ์ฑ๋ฅ์ ๊ฐ์ฅ ์ข์ง๋ง, ๊ฒฐ๊ตญ DB๋ ์ ํ๋ฆฌ์ผ์ด์ ์ด๋ ์๊ฐ ๋ถํ๋ฅผ ์ด๋๊น์ง ๊ฒฌ๋ ์ ์๋์ง ํ ์คํธ๋ฅผ ์งํํด๋ณด๋ฉฐ ๊ฒฐ์ ํ๋ ๊ฒ์ด ์ค์
์ฅ์
์ฟผ๋ฆฌ ํธ์ถ ์
๊ฐ 1+N ์์ 1+1 ๋ก์ต์ ํ
IN ์ฟผ๋ฆฌ ์ฌ์ฉ์ผ๋ก ์ผ๋ฐ ์กฐ์ธ๋ณด๋ค
DB ๋ฐ์ดํฐ ์ ์ก๋ ์ต์ ํ
ํ์น ์กฐ์ธ ๋ฐฉ์๊ณผ ๋น๊ตํด์ ์ฟผ๋ฆฌ ํธ์ถ ์๊ฐ ์ฝ๊ฐ ์ฆ๊ฐํ์ง๋ง(IN ์ฟผ๋ฆฌ),
DB ๋ฐ์ดํฐ ์ ์ก๋ ๊ฐ์
(์ค๋ณต ์ ๊ฑฐ)์ปฌ๋ ์ ํ์น ์กฐ์ธ์์
ํ์ด์ง์ด ๋ถ๊ฐ๋ฅํ ๋จ์ ์ ํด๊ฒฐ
// OrderApiController.java
@GetMapping("/api/orders")
public List<OrderDto> ordersV3_page(@RequestParam(value = "offset", defaultValue = "0") int offset,
@RequestParam(value = "limit", defaultValue = "100") int limit) {
List<Order> orders = orderRepository.findAllWithMemberDelivery(offset, limit);
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
// OrderRepository.java
public List<Order> findAllWithMemberDelivery(int offset, int limit) {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
// application.yml
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
DTO ์ง์ ์กฐํ
ToOne(N:1, 1:1) ๊ด๊ณ ์กฐํํ ํ, ToMany(1:N) ๊ด๊ณ๋ ๋ณ๋ ์ฒ๋ฆฌ
ToOne ๊ด๊ณ๋ ์กฐ์ธ ์ Row ์๊ฐ ์ฆ๊ฐํ์ง ์์ง๋ง, ToMany ๊ด๊ณ๋ ์กฐ์ธ ์ Row ์๊ฐ ์ฆ๊ฐํ์ฌ ์ต์ ํ๊ฐ ์ด๋ ค์ฐ๋ฏ๋ก ๋ณ๋ ์กฐํ
๋ฃจํธ 1 ๋ฒ, ์ปฌ๋ ์ N ๋ฒ, ์ด 1 + N ๋ฒ์ ์ฟผ๋ฆฌ ์คํ
์ฝ๋๊ฐ ๋จ์ํ๊ณ , ์ ์ง๋ณด์๊ฐ ์ฌ์ฐ๋ฉฐ, ๋จ๊ฑด ์กฐํ์์๋ ์ ์ฉํ ๋ฐฉ๋ฒ
public List<OrderQueryDto> findOrderQueryDtos() {
// ๋ฃจํธ ์กฐํ : ToOne ๊ด๊ณ๋ฅผ ํ ๋ฒ์ ์กฐํ (1 ๋ฒ์ ์ฟผ๋ฆฌ N ๊ฐ์ Row)
List<OrderQueryDto> result = findOrders();
// ์ปฌ๋ ์
์กฐํ : ์ปฌ๋ ์
์ ๋ฃจํ๋ฅผ ๋๋ฉด์ ๋ณ๋๋ก ์กฐํ (N ๋ฒ์ ์ฟผ๋ฆฌ)
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
private List<OrderQueryDto> findOrders() {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderQueryDto(o.id, m.name, o.orderDate, o.status, d.address)" +
" from Order o" +
" join o.member m" +
" join o.delivery d", OrderQueryDto.class)
.getResultList();
}
private List<OrderItemQueryDto> findOrderItems(Long orderId) {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
" from OrderItem oi" +
" join oi.item i" +
" where oi.order.id = : orderId",
OrderItemQueryDto.class)
.setParameter("orderId", orderId)
.getResultList();
}
์ปฌ๋ ์
์กฐํ ์ต์ ํ
IN ์ ์ ํ์ฉํด์ ๋ฉ๋ชจ๋ฆฌ์ ๋ฏธ๋ฆฌ ์กฐํ ํ ์ต์ ํ
๋ฃจํธ 1 ๋ฒ, ์ปฌ๋ ์ 1 ๋ฒ ์กฐํ
Map์ ์ฌ์ฉํ์ฌ ๋งค์นญ ์ฑ๋ฅ ๊ฐ์ - O(1)
ToOne ๊ด๊ณ๋ฅผ ๋จผ์ ์กฐํํ ํ, ์ป์ ์๋ณ์ Id๋ก ToMany ๊ด๊ณ๋ฅผ ํ๊บผ๋ฒ์ ์กฐํ
์ ๋ฐฉ๋ฒ๊ณผ ๋น๊ตํ๋ฉด
๋ฐ์ํ๋ N + 1 ๋ฌธ์ ๋ฅผ 1 + 1 ๋ก ํด๊ฒฐ
์ฝ๋๊ฐ ๋ณต์กํด์ง๊ธด ํ์ง๋ง, ๋ค์์ ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ์กฐํ ํ ๊ฒฝ์ฐ ํ๊ฒฝ์ ๋ฐ๋ผ 100๋ฐฐ ์ด์ ์ฑ๋ฅ ์ต์ ํ ๊ฐ๋ฅ
๋ณดํต ๋ง์ด ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ
public List<OrderQueryDto> findAllByDto_optimization() {
// ๋ฃจํธ ์กฐํ : ToOne ๊ด๊ณ๋ฅผ ํ ๋ฒ์ ์กฐํ (1 ๋ฒ์ ์ฟผ๋ฆฌ)
List<OrderQueryDto> result = findOrders();
// ์ปฌ๋ ์
์กฐํ : IN ์ ์ ํ์ฉํ์ฌ ํ ๋ฒ์ ์กฐํ (1๋ฒ์ ์ฟผ๋ฆฌ)
Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(toOrderIds(result));
// ๋ฃจํ๋ฅผ ๋๋ฉด์ ์ปฌ๋ ์
์ธํ
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
private List<Long> toOrderIds(List<OrderQueryDto> result) {
return result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
}
private Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds) {
List<OrderItemQueryDto> orderItems = em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
" from OrderItem oi" +
" join oi.item i" +
" where oi.order.id in :orderIds", OrderItemQueryDto.class)
.setParameter("orderIds", orderIds)
.getResultList();
return orderItems.stream()
.collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));
}
ํ๋ซ ๋ฐ์ดํฐ ์ต์ ํ
JOIN ๊ฒฐ๊ณผ๋ฅผ ๊ทธ๋๋ก ์กฐํํ ํ, ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ํ๋ ์คํฉ์ผ๋ก ์ง์ ๋ณํ
์ฟผ๋ฆฌ๋ฅผ ํ ๋ฒ ์คํํ๋ ์ฅ์ ์ด ์์ง๋ง,
์กฐ์ธ์ผ๋ก ์๊ธฐ๋ ์ค๋ณต ๋ฐ์ดํฐ๊ฐ DB์์ ์ ํ๋ฆฌ์ผ์ด์ ์ผ๋ก ์ ๋ฌ๋์ด ์ํฉ์ ๋ฐ๋ผ ์ ๋ฐฉ๋ฒ๋ณด๋ค ๋๋ฆด ์ ์์
๋ฐํ Dto ์คํ์ผ๋ก ๋ณํ์ ์ํด ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ถ๊ฐ ์์ (๋ณํ ๋ก์ง)์ด ํ์
ํ์ด์ง ๋ถ๊ฐ๋ฅ
์ ๋ฐฉ๋ฒ๊ณผ ๋น๊ตํ๋ฉด
์ฟผ๋ฆฌ ์คํ์ด ํ ๋ฒ์ผ๋ก ์ต์ ํ๊ฐ ๊ฐ๋ฅํ์ง๋ง, ํ์ด์ง์ด ๋ถ๊ฐ๋ฅ
๋ฐ์ดํฐ๊ฐ ๋ง์์ง๋ฉด ์ค๋ณต ์ ์ก์ด ์ฆ๊ฐํ์ฌ ์ ๋ฐฉ๋ฒ๊ณผ ์ฑ๋ฅ ์ฐจ์ด๋ ๋ฏธ๋น
// OrderApiController.java
@GetMapping("/api/orders")
public List<OrderQueryDto> ordersV6() {
List<OrderFlatDto> flats = orderQueryRepository.findAllByDto_flat();
return flats.stream()
.collect(groupingBy(o -> new OrderQueryDto(o.getOrderId(), o.getName(), o.getOrderDate(), o.getOrderStatus(), o.getAddress()),
mapping(o -> new OrderItemQueryDto(o.getOrderId(), o.getItemName(), o.getOrderPrice(), o.getCount()), toList())
)).entrySet().stream()
.map(e -> new OrderQueryDto(e.getKey().getOrderId(),
e.getKey().getName(), e.getKey().getOrderDate(), e.getKey().getOrderStatus(),
e.getKey().getAddress(), e.getValue()))
.collect(toList());
}
// OrderQueryRepository.java
public List<OrderFlatDto> findAllByDto_flat() {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderFlatDto(o.id, m.name, o.orderDate, o.status, d.address, i.name, oi.orderPrice, oi.count)" +
" from Order o" +
" join o.member m" +
" join o.delivery d" +
" join o.orderItems oi" +
" join oi.item i", OrderFlatDto.class)
.getResultList();
}
์กฐํ ์ฟผ๋ฆฌ ๊ถ์ฅ ์์
โญ๏ธ ์กฐํ ์ฟผ๋ฆฌ ๋ฐฉ์ ์ ํ ๊ถ์ฅ ์์ โญ๏ธ
์ํฐํฐ๋ฅผ DTO๋ก ๋ณํ
ํ์ ์
ํ์น ์กฐ์ธ์ผ๋ก ์ฑ๋ฅ ์ต์ ํ
(๋๋ถ๋ถ์ ์ฑ๋ฅ ์ด์๊ฐ ํด๊ฒฐ)๊ทธ๋๋ ์๋๋ฉด
DTO๋ก ์ง์ ์กฐํ
์ตํ์ ๋ฐฉ๋ฒ์
JPA๊ฐ ์ ๊ณตํ๋ ๋ค์ดํฐ๋ธ SQL
ํน์Spring JDBC Template
์ ์ฌ์ฉํด์ SQL์ ์ง์ ์ฌ์ฉ
.
โญ๏ธ ์ปฌ๋ ์ ์กฐํ ์ฟผ๋ฆฌ ๋ฐฉ์ ์ ํ ๊ถ์ฅ ์์ โญ๏ธ
์ํฐํฐ ์กฐํ ๋ฐฉ์
์ผ๋ก ์ ๊ทผํ์ด์ง ํ์ ์์ ์
ํ์น์กฐ์ธ
์ผ๋ก ์ฟผ๋ฆฌ ์ ์ต์ ํํ์ด์ง ํ์ ์
hibernate.default_batch_fetch_size
,@BatchSize
๋ก ์ปฌ๋ ์ ์ต์ ํ
์ํฐํฐ ์กฐํ ๋ฐฉ์์ผ๋ก ํด๊ฒฐ์ด ์๋๋ฉด
DTO ์กฐํ ๋ฐฉ์
์ฌ์ฉDTO ์กฐํ ๋ฐฉ์์ผ๋ก ํด๊ฒฐ์ด ์๋๋ฉด
NativeSQL
orSpring JdbcTemplate
์ฌ์ฉ
.
์ํฐํฐ ์กฐํ ๋ฐฉ์๊ณผ DTO ์กฐํ ๋ฐฉ์
์ํฐํฐ ์กฐํ ๋ฐฉ์์ fetch join, default_batch_fetch_size, @BatchSize ๋ฑ์ผ๋ก ์ฝ๋๋ฅผ ๊ฑฐ์ ์์ ํ์ง ์๊ณ , ์ต์ ์ค์ ๋ง์ผ๋ก ๋ค์ํ ์ฑ๋ฅ ์ต์ ํ ์๋๊ฐ ๊ฐ๋ฅ
๋ฐ๋ฉด, DTO ์กฐํ ๋ฐฉ์์ ๋ง์ ์ฝ๋ ๋ณ๊ฒฝ์ด ํ์ํ๋ฏ๋ก ๋ ์ฌ์ด์ ์คํ๊ธฐ๊ฐ ํ์
OSIV์ ์ฑ๋ฅ ์ต์ ํ
OSIV(
Open Session In View
): hibernateJPA ์์๋ Open EntityManager In View
์์์ฑ ์ปจํ ์คํธ๊ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ปค๋ฅ์ ์ ๊ฐ์ง๊ณ ์๋ ๋ฒ์
OSIV ON
spring.jpa.open-in-view
: true (default)

OSIV๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ, (View Template์ด๋ API ์ปจํธ๋กค๋ฌ์์์)
์ง์ฐ๋ก๋ฉ
์ ์ํด View rendering | API response ์๋ฃ ์์ ๊น์ง ์์์ฑ ์ปจํ ์คํธ์๋ฐ์ดํฐ๋ฒ ์ด์ค ์ปค๋ฅ์ ์ ์ ์ง
ํด์ผ ํ๋ค.Service Layer์์ ํธ๋์ ์ ์ด ๋๋๋๋ผ๋ rendering | API response ์๋ฃ ํ์์ผ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ปค๋ฅ์ ์ ๋ฐํํ๊ณ , ์์์ฑ ์ปจํ ์คํธ๋ฅผ ๋ซ๊ฒ ๋๋ค.
์ง์ฐ ๋ก๋ฉ์ ์์์ฑ ์ปจํ ์คํธ๊ฐ ์ด์์์ด์ผ ํ๋ฏ๋ก, ์์์ฑ ์ปจํ ์คํธ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ปค๋ฅ์ ์ ์ ์งํ๋ ์ฅ์
ํ์ง๋ง! ๋๋ฌด ์ค๋์๊ฐ๋์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ปค๋ฅ์ ๋ฆฌ์์ค๋ฅผ ์ ์งํ๋ฏ๋ก, ์ค์๊ฐ ํธ๋ํฝ์ด ์ค์ํ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ์ปค๋ฅ์ ์ด ๋ถ์กฑํ์ฌ ์ฅ์ ๋ก ์ด์ด์ง ์ ์๋ค.
์ปจํธ๋กค๋ฌ์์ ์ธ๋ถ API๋ฅผ ํธ์ถํ๋ฉด ์ธ๋ถ API ์๋ต ๋๊ธฐ ์๊ฐ ๋งํผ ์ปค๋ฅ์ ๋ฆฌ์์ค๋ฅผ ๋ฐํํ์ง ๋ชปํ๊ฒ ๋๋ ๋จ์
OSIV OFF
spring.jpa.open-in-view:
false

OSIV๋ฅผ ๋๋ฉด ํธ๋์ญ์ ์ ์ข ๋ฃํ ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ปค๋ฅ์ ์ ๋ฐํํ๊ณ , ์์์ฑ ์ปจํ ์คํธ๋ฅผ ๋ซ์ ์ปค๋ฅ์ ๋ฆฌ์์ค๋ฅผ ๋ญ๋นํ์ง ์์
๋จ, ๋ชจ๋ ์ง์ฐ๋ก๋ฉ์ ํธ๋์ญ์ ์์์ ์ฒ๋ฆฌํด์ผ ํ์ฌ, ๊ธฐ์กด ์ง์ฐ ๋ก๋ฉ ์ฝ๋๋ฅผ ํธ๋์ญ์ ์์ผ๋ก ๋ฃ๊ฑฐ๋ fetch join์ ์ฌ์ฉํด์ผ ํ๋ ๋จ์ ์ด ์กด์ฌ
view template์์ ์ง์ฐ๋ก๋ฉ์ด ๋์ํ์ง ์์.
ํธ๋์ญ์ ์ด ๋๋๊ธฐ ์ , ์ง์ฐ ๋ก๋ฉ ๊ฐ์ ํธ์ถ ํ์
CQS
CQS(Commandโquery separation)
์ปค๋ฉ๋์ ์ฟผ๋ฆฌ ๋ถ๋ฆฌํ์ฌ OSIV๋ฅผ ๋ ์ํ๋ก ๋ณต์ก์ฑ์ ๊ด๋ฆฌ
๋ณดํต ๋น์ฆ๋์ค ๋ก์ง(๋ฑ๋ก/์์ )์์๋ ์ฑ๋ฅ์ด ํฌ๊ฒ ๋ฌธ์ ์์ง๋ง, ๋ณต์กํ ํ๋ฉด์ ์ถ๋ ฅํ๊ธฐ ์ํ ์ฟผ๋ฆฌ๋ ์ฑ๋ฅ์ ์ต์ ํ ํ๋ ๊ฒ์ด ์ค์
ํฌ๊ณ ๋ณต์กํ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฐ๋ฐํ๋ค๋ฉด, ์ด ๋์ ๊ด์ฌ์ฌ๋ฅผ ๋ช ํํ๊ฒ ๋ถ๋ฆฌํ์ฌ ์ ์ง๋ณด์ํ๊ธฐ ์ฝ๊ฒ ๋ง๋ค์.
OrderService: ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง
OrderQueryService: ํ๋ฉด์ด๋ API์ ๋ง์ถ ์๋น์ค (์ฃผ๋ก ์กฐํ ์ ์ฉ ํธ๋์ญ์ )
๋ณดํต ์๋น์ค ๊ณ์ธต์์ ํธ๋์ญ์ ์ ์ ์งํ๋ฏ๋ก, ๋ ์๋น์ค ๋ชจ๋ ํธ๋์ญ์ ์ ์ ์งํ๋ฉด์ ์ง์ฐ ๋ก๋ฉ ์ฌ์ฉ ๊ฐ๋ฅ
์ถ์ฒ: ๊ณ ๊ฐ ์๋น์ค์ ์ค์๊ฐ API๋ OSIV OFF, ADMIN ๊ณผ ๊ฐ์ด ์ปค๋ฅ์ ์ ๋ง์ด ์ฌ์ฉํ์ง ์๋ ๊ณณ์์๋ OSIV ON
์๊ฐ
Spring Data JPA
Spring Data JPA๋ JPA ์ฌ์ฉ ์ ๋ฐ๋ณต๋๋ ์ฝ๋๋ฅผ ์๋ํ
org.springframework.boot:spring-boot-starter-data-jpa
JpaRepository ์ธํฐํ์ด์ค์์ ๊ธฐ๋ณธ์ ์ธ CRUD ๊ธฐ๋ฅ์ ๋ชจ๋ ์ ๊ณต
์ผ๋ฐํํ๊ธฐ ์ด๋ ค์ด ๊ธฐ๋ฅ๋ ๋ฉ์๋ ์ด๋ฆ์ผ๋ก ์ ํํ JPQL ์ฟผ๋ฆฌ ์คํ ๊ฐ๋ฅ
๊ฐ๋ฐ์๋ ์ธํฐํ์ด์ค๋ง ๋ง๋ค๋ฉด ๊ตฌํ์ฒด๋ Spring Data JPA๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ์คํ์์ ์ ์ฃผ์
QueryDSL
QueryDSL ๋ก ์กฐ๊ฑด์ ๋ฐ๋ผ ์คํ๋๋ ๋์ ์ฟผ๋ฆฌ๋ฅผ ๋ง๋ค ์ ์๋ค.
์๋ฐ ์ฝ๋๋ก ๋์ ์ฟผ๋ฆฌ๋ฅผ SQL(JPQL)๊ณผ ์ ์ฌํ๊ฒ ์์ฑ (JPQL์ ์ฝ๋๋ก ๋ง๋๋ ๋น๋ ์ญํ )
QueryDSL ์ฅ์
์ง๊ด์ ์ธ ๋ฌธ๋ฒ
์ปดํ์ผ ์์ ์ ๋น ๋ฅธ ๋ฌธ๋ฒ ์ค๋ฅ ๋ฐ๊ฒฌ
์ฝ๋ ์๋์์ฑ
์ฝ๋ ์ฌ์ฌ์ฉ
JPQL new ๋ช ๋ น์ด์ ๋ฐ๋๋ก ์ฌํํ DTO ์กฐํ ์ง์
Querydsl์ JPA๋ก ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฐ๋ฐ ํ ๋ ์ ํ์ด ์๋ ํ์!
Last updated