[๊ฐ์] ์ค์ ! ์คํ๋ง ๋ถํธ์ JPA ํ์ฉ2 - API ๊ฐ๋ฐ๊ณผ ์ฑ๋ฅ ์ต์ ํ 2
์ค์ ! ์คํ๋ง ๋ถํธ์ JPA ํ์ฉ2 - API ๊ฐ๋ฐ๊ณผ ์ฑ๋ฅ ์ต์ ํ - ์ธํ๋ฐ | ๊ฐ์
์คํ๋ง ๋ถํธ์ JPA๋ฅผ ํ์ฉํด์ API๋ฅผ ๊ฐ๋ฐํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ JPA ๊ทนํ์ ์ฑ๋ฅ ์ต์ ํ ๋ฐฉ๋ฒ์ ํ์ตํ ์ ์์ต๋๋ค., - ๊ฐ์ ์๊ฐ | ์ธํ๋ฐ...
www.inflearn.com
4. API ๊ฐ๋ฐ ๊ณ ๊ธ - ์ปฌ๋ ์ ์กฐํ ์ต์ ํ
- ์ฃผ๋ฌธ ์กฐํ V1: ์ํฐํฐ ์ง์ ๋ ธ์ถ
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
@GetMapping("/api/v1/orders")
public List<Order> ordersV1(){
List<Order> all = orderRepository.findAllByString(new OrderSearch());
for (Order order : all) {
order.getMember().getName();
order.getDelivery().getAddress();
List<OrderItem> orderItems = order.getOrderItems();
orderItems.forEach(o -> o.getItem().getName());
}
return all;
}
}
์ ๋ถ lazy๊ธฐ ๋๋ฌธ์ ํ๋ก์๋ฅผ ๊ฐ์ ์ด๊ธฐํ ํ ํ ๋ฐ์ดํฐ๋ฅผ ๋ฟ๋ฆฌ๊ฒ ํจ
๊ฒฐ๊ตญ ์ํฐํฐ ์ง์ ๋ ธ์ถ์ด๋ผ ๋ณ๋ก
- ์ฃผ๋ฌธ ์กฐํ V2: ์ํฐํฐ๋ฅผ DTO๋ก ๋ณํ
@RestController
@RequiredArgsConstructor
public class OrderApiController {
/*์๋ต*/
@GetMapping("/api/v2/orders")
public List<OrderDto> orderV2(){
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<OrderDto> collect = orders.stream()
.map(o-> new OrderDto(o))
.collect(Collectors.toList());
return collect;
}
//@Data ๊ฐ ๋๋ฌด ๋ง์ผ๋ฉด ์ ๋งคํด์ ์์ฐ๋ ๊ฒ ๋์ ์๋ ์์
@Getter
static class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemDto> orderItems;
//๋์น๊ธฐ ์ํ ์ค์ : OrderItem๋ DTO๋ก ๋ฐ๊ฟ์ ๋ณด๋ด์ผํจ
//private List<OrderItem> orderItems;
public OrderDto(Order order){
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getMember().getAddress();
/*//์ด๊ฑฐ ์์ฐ๋ฉด lazy๋ผ ์๋ฌด๊ฒ๋ ๋์ค์ง ์์
order.getOrderItems().stream().forEach(o -> o.getItem().getName());
orderItems = order.getOrderItems();*/
//-> OrderItem๋ Dto๋ก ๋ณ๊ฒฝ
orderItems = order.getOrderItems().stream()
.map(orderItem -> new OrderItemDto(orderItem))
.collect(Collectors.toList());
}
}
@Getter
static class OrderItemDto{
private String itemName;
private int orderPrice;
private int count;
public OrderItemDto(OrderItem orderItem){
itemName = orderItem.getItem().getName();
orderPrice = orderItem.getOrderPrice();
count = orderItem.getCount();
}
}
}
์ํ๋ ์ ๋ณด๋ง ๋ณด์ด๋ ๊ฒ์ ์ ์ ์๋ค.
์ง์ฐ ๋ก๋ฉ์ด ๋ง์์
Order ์กฐํ์ ๋๊ฐ๋์ด
๊ฐ Order๋ง๋ค
๋ฉค๋ฒ ํ ๋ช ์กฐํ
delivery ์ ๋ณด
Items ๋๊ฐ ์กฐํ
Items๋ง๋ค Item ์กฐํ์ 2๋ฒ์ฉ
...
SQL ์ฟผ๋ฆฌ๊ฐ ์กฐ์ง๊ฒ ๋ง์ด ๋๊ฐ
๋ฑ ๋ด๋ ์ฑ๋ฅ ๋ ๋จ์ด์ ธ ๋ณด์
- ์ฃผ๋ฌธ ์กฐํ V3: ์ํฐํฐ๋ฅผ DTO๋ก ๋ณํ - ํจ์น ์กฐ์ธ ์ต์ ํ
@Repository
@RequiredArgsConstructor
public class OrderRepository {
/*์๋ต*/
public List<Order> findAllWithItem() {
return em.createQuery(
"select 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();
}
}
DB์์ ์ผ๋๋ค์ผ๋ ๋ค์ ๋ง์ถฐ์ ๋ฆฌํดํ๊ธฐ ๋๋ฌธ์ order 4๊ฐ๊ฐ ๋ฆฌํด๋๋ค -> distinct๋ฅผ ์ถ๊ฐํ๋ฉด 2๊ฐ ๋ฆฌํด
์ฌ์ค ์ฟผ๋ฆฌ์์๋ ์์ ํ ๋๊ฐ์์ผ ์ค๋ณต์ ๊ฑฐ์ด๋ฏ๋ก, ์ฌ์ ํ 4๊ฐ๊ฐ ๋์ค์ง๋ง,
JPA์์ ์์ฒด์ ์ผ๋ก distinct๋ฅผ ๋ณด๋ฉด ์ค๋ณต์ ๊ฑธ๋ฌ์ ์ปฌ๋ ์
์ ๋ด์์ค๋ค.
distinct๊ฐ ์ค๋ณต ์กฐํ๋ฅผ ๋ง์์ค๋ค
@RestController
@RequiredArgsConstructor
public class OrderApiController {
/*์๋ต*/
@GetMapping("/api/v3/orders")
public List<OrderDto> orderV3(){
List<Order> orders = orderRepository.findAllWithItem();
List<OrderDto> collect = orders.stream()
.map(o-> new OrderDto(o))
.collect(Collectors.toList());
return collect;
/*์๋ต*/
}
}
ํจ์น ์กฐ์ธ์ผ๋ก SQL์ด ํ ๋ฒ๋ฐ ์คํ๋จ
ํ์ง๋ง ๋จ์ , ์ผ๋๋ค๋ฅผ ํ๋ ์๊ฐ ํ์ด์ง์ด ๋ถ๊ฐ๋ฅํ๋ค
.setFirstResult(1), .setMaxResults(100) : 1๋ถํฐ 100๊ฐ ๊ฐ์ ธ์ด
WARN : firstResult/maxResults sepcified with collection fetch; applying in memory
Distinct๋ ๊ฒฐ๊ณผ ๊ธฐ์ค์ด ์๋, ๋ปฅํ๊ธฐ ๋ ์ํ, order item์ ๊ธฐ์ค์ผ๋ก ์ ์ฉ์ด ๋จ
์ ์ ํ ๊ฐ์๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ด ๋งค์ฐ ์ํํจ(ex) OutOfMemory), ํ์ด์ง์ด ๋ถ๊ฐ๋ฅํจ
๋ํ ์ปฌ๋ ์ ํจ์น ์กฐ์ธ์ ํ ๋ฒ๋ง ์ฌ์ฉํ ์ ์๋ค.
๋๊ฐ ์ด์ ์ฌ์ฉํ๊ฒ ๋๋ฉด ๋ปฅํ๊ธฐ์ ๋ปฅํ๊ธฐ ํด์ ํผ๋์ ์ผ์ผํฌ ์ ์์
- ์ฃผ๋ฌธ ์กฐํ V3.1: ์ํฐํฐ๋ฅผ DTO๋ก ๋ณํ - ํ์ด์ง๊ณผ ํ๊ณ ๋ํ
@Repository
@RequiredArgsConstructor
public class OrderRepository {
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();
}
@RestController
@RequiredArgsConstructor
public class OrderApiController {
/*์๋ต*/
@GetMapping("/api/v3/orders")
public List<OrderDto> orderV3(){
List<Order> orders = orderRepository.findAllWithItem();
List<OrderDto> collect = orders.stream()
.map(o-> new OrderDto(o))
.collect(Collectors.toList());
return collect;
}
@GetMapping("/api/v3.1/orders")
public List<OrderDto> orderV3_page(
@RequestParam(value = "offset" , defaultValue = 0) int offset,
@RequestParam(value = "limit" , defaultValue = 100) int limit){
//ToOne ๊ด๊ณ๊น์ง๋ง ๋ญํฑ์ด๋ก ํ๋ฒ์ ๊ฐ์ ธ์จ๋ค
List<Order> orders = orderRepository.findAllWithMemberDelivery(offset, limit);
// ์ฌ๊ธฐ๊น์ง๋ง ํ๋ฉด 1 + orders ๋ฃจํ (2๋ฒ + 1) * (orderItems 1 + orderitem 2 * item 2)... ๊ณ์ฐ ๋ค์์ ๋ง์ ์ผ๋จ ์ด๋ผ ๋ง์
List<OrderDto> collect = orders.stream()
.map(o-> new OrderDto(o))
.collect(Collectors.toList());
return collect;
}
/*์๋ต*/
}
jpa.hibernate.properties.default_batch_fetch_size: 100 ์ถ๊ฐ
100์ ์๋ฏธ๋ inquery์ ๊ฐ์๋ฅผ ๋ช๊ฐ๋ก ํ ๊ฒ์ธ๊ฐ
ํ ๋ฐฉ์ 100๊ฐ๋ฅผ ๋ค ๋ก๊ฒจ์จ๋ค
์ฌ์ด์ฆ : ๊ณ ๊ณ ์ต์ ํ์ง๋ง ์๊ฐ ๋ถํ๋ฅผ ๊ฒฌ๋ ์ ์์ด์ผํจ ์ฃผ๋ก 100~1000 ์ฌ์ด
1 + N + N๋ฌธ์ ํด๊ฒฐ
๋ฐ์ดํฐ ์์ด ์์ฒญ ๋ง์์ง๋ฉด ์คํ๋ ค ํ๋ฒ์ ์ ๋ถ joinํด ๊ฐ์ ธ์ค๋ ์ ๋ฐฉ๋ฒ๋ณด๋ค ์ด ๋ฐฉ๋ฒ์ด ๋ ์ข์ ์ ๋ ์๋ค
๊ฒฐ๋ก ์ฅ์ :
์ฟผ๋ฆฌ ํธ์ถ ์๊ฐ 1 + N -> 1 + 1 ๋ก ์ต์ ํ
์กฐ์ธ๋ณด๋ค DB ๋ฐ์ดํฐ ์ ์ก๋์ด ์ต์ ํ , ์ค๋ณต๋ ๋ฐ์ดํฐ ์ ์ก ์ํด๋ ๋จ
ํจ์น ์กฐ์ธ ๋ฐฉ์๊ณผ ๋น๊ตํด์ ์ฟผ๋ฆฌ ํธ์ถ ์๊ฐ ์ฝ๊ฐ ์ฆ๊ฐํ์ง๋ง, DB ๋ฐ์ดํฐ ์ ์ก๋์ ๊ฐ์
์ปฌ๋ ์
ํ์น ์กฐ์ธ์ ํ์ด์ง ๋ถ๊ฐ๋ฅ ํ์ง๋ง ์ด ๋ฐฉ๋ฒ์ ํ์ด์ง ๊ฐ๋ฅ
- ์ฃผ๋ฌธ ์กฐํ V4: JPA์์ DTO ์ง์ ์กฐํ
repository ์๋ก ์์ฑ
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
public List<OrderQueryDto> findOrderQueryDtos(){
//์ปฌ๋ ์
์ ๋ฐ๋ก ๋ฃ์ ์๋ ์์
List<OrderQueryDto> result = findOrders();
//์ปฌ๋ ์
์ ์ง์ ์ฑ์ด๋ค
result.forEach(o ->{
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
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();
}
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();
}
}
@RestController
@RequiredArgsConstructor
public class OrderApiController {
/*์๋ต*/
@GetMapping("/api/v4/orders")
public List<OrderQueryDto> orderV4(){
return orderQueryRepository.findOrderQueryDtos();
}
/*์๋ต*/
}
DTO ์์ฑ
@Data
public class OrderQueryDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemQueryDto> orderItems;
public OrderQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
}
}
@Data
public class OrderItemQueryDto {
@JsonIgnore
private Long orderId;
private String itemName;
private int orderPrice;
private int count;
public OrderItemQueryDto(Long orderId, String itemName, int orderPrice, int count) {
this.orderId = orderId;
this.itemName = itemName;
this.orderPrice = orderPrice;
this.count = count;
}
}
๊ฒฐ๊ณผ๋ ๋ญ... ๋น์ท
row ์๊ฐ ์ฆ๊ฐํ์ง ์๋ ToOne ๊ด๊ณ๋ ์กฐ์ธ์ผ๋ก ์ต์ ํ ํ๊ธฐ ์ฌ์ฐ๋ฏ๋ก ํ๋ฒ์ ์กฐํํ๊ณ , ToMany
๊ด๊ณ๋ ์ต์ ํ ํ๊ธฐ ์ด๋ ค์ฐ๋ฏ๋ก findOrderItems() ๊ฐ์ ๋ณ๋์ ๋ฉ์๋๋ก ์กฐํํ๋ค.
๋ณ๋์ ๋ฉ์๋๋ฅผ ์ฐ๋ ๊ณผ์ ์์ N+1๋ฒ ์ฟผ๋ฆฌ๊ฐ ๋๊ฐ๋ ๋จ์ ์ด ์์
- ์ฃผ๋ฌธ ์กฐํ V5: JPA์์ DTO ์ง์ ์กฐํ - ์ปฌ๋ ์ ์กฐํ ์ต์ ํ
@RestController
@RequiredArgsConstructor
public class OrderApiController {
/*์๋ต*/
@GetMapping("/api/v5/orders")
public List<OrderQueryDto> orderV5(){
return orderQueryRepository.findAllByDto_optimization();
}
/*์๋ต*/
}
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
public List<OrderQueryDto> findAllByDto_optimization(){
List<OrderQueryDto> result = findOrders();
List<Long> orderIds = result.stream().map(o -> o.getOrderId())
.collect(Collectors.toList());
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();
//์ฌ์ฉํ๊ธฐ ํธํ๊ฒ ๋งต ํ์์ผ๋ก ๋ณ๊ฒฝ
Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
.collect(Collectors.groupingBy(orderItemQueryDto -> orderItemQueryDto.getOrderId()));
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
}
์ฟผ๋ฆฌ ๋ ๋ฒ์ผ๋ก ์ต์ ํ ๊ฐ๋ฅ
๋ฃจํธ 1๋ฒ, ์ปฌ๋ ์ 1๋ฒ
Map์ ์ฌ์ฉํด์ ๋งค์นญ ์ฑ๋ฅ ํฅ์
- ์ฃผ๋ฌธ ์กฐํ V6: JPA์์ DTO ์ง์ ์กฐํ, ํ๋ซ ๋ฐ์ดํฐ ์ต์ ํ
์ฟผ๋ฆฌ ํ๋ฒ์ผ๋ก ํด๊ฒฐ ๊ฐ๋ฅ
@RestController
@RequiredArgsConstructor
public class OrderApiController {
/*์๋ต*/
//์ฟผ๋ฆฌ๊ฐ ํ ๋ฒ๋ง ๋๊ฐ
//ํ์ง๋ง join์ ์ด์ฉํ๊ธฐ ๋๋ฌธ์ ๋ค์ ๋ง์ถฐ์ ๋ฟ๋ ค์ ธ ๋์ด
//๋ฐ๋ผ์ ํ์ด์ง์ ๋ชปํจ
//API ์คํ์ด ์๋ง๋ ๋ฌธ์ ๋ ๊ทธ๋ฅ ๋
ธ๊ฐ๋ค๋ก ๋ฐ๋ผ๋ด๋ ์ฝ๋๋ฅผ ์ง๋ฉด ๋จ
@GetMapping("/api/v6/orders")
public List<OrderFlatDto> orderV6(){
return orderQueryRepository.findAllByDto_flat();
}
/*์๋ต*/
}
package jpabook.jpashop.repository.order.query;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
/*์๋ต*/
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();
}
}
์ง์ ์ ๋ฆฌํ๋ ์ฝ๋(์ ๋ฆฌ ์๋์ด์์, ๊ทธ๋ฅ ๋ณต๋ถ)
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());
๊ทธ๋ฆฌ๊ณ OrderQueryDto์ @EqualsAndHashCode(of = orderId) ์ถ๊ฐ
orderId ๊ธฐ์ค์ผ๋ก ๋ฌถ์ด์ค
๋จ :
์ฟผ๋ฆฌ๋ ํ๋ฒ์ด์ง๋ง ์กฐ์ธ์ผ๋ก ์ธํด DB์์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ๋ฌํ๋ ๋ฐ์ดํฐ์ ์ค๋ณต ๋ฐ์ดํฐ๊ฐ ์ถ๊ฐ
-> V5 ๋ณด๋ค ๋ ๋๋ฆด ์ ๋
์ ํ๋ฆฌ์ผ์ด์
์์ ์ถ๊ฐ ์์
์ด ํผ
ํ์ด์ง ๋ถ๊ฐ๋ฅ
- API ๊ฐ๋ฐ ๊ณ ๊ธ ์ ๋ฆฌ
์ํฐํฐ ์กฐํ
- ์ํฐํฐ๋ฅผ ์กฐํํด์ ๊ทธ๋๋ก ๋ฐํ: V1
- ์ํฐํฐ ์กฐํ ํ DTO๋ก ๋ณํ: V2
- ํ์น ์กฐ์ธ์ผ๋ก ์ฟผ๋ฆฌ ์ ์ต์ ํ: V3
- ์ปฌ๋ ์
ํ์ด์ง๊ณผ ํ๊ณ ๋ํ: V3.1
์ปฌ๋ ์ ์ ํ์น ์กฐ์ธ์ ํ์ด์ง์ด ๋ถ๊ฐ๋ฅ
ToOne ๊ด๊ณ๋ ํ์น ์กฐ์ธ์ผ๋ก ์ฟผ๋ฆฌ ์ ์ต์ ํ
์ปฌ๋ ์ ์ ํ์น ์กฐ์ธ ๋์ ์ ์ง์ฐ ๋ก๋ฉ์ ์ ์งํ๊ณ , hibernate.default_batch_fetch_size ,
@BatchSize ๋ก ์ต์ ํ\
DTO ์ง์ ์กฐํ
- JPA์์ DTO๋ฅผ ์ง์ ์กฐํ: V4
- ์ปฌ๋ ์ ์กฐํ ์ต์ ํ - ์ผ๋๋ค ๊ด๊ณ์ธ ์ปฌ๋ ์ ์ IN ์ ์ ํ์ฉํด์ ๋ฉ๋ชจ๋ฆฌ์ ๋ฏธ๋ฆฌ ์กฐํํด์ ์ต์ ํ: V5
- ํ๋ซ ๋ฐ์ดํฐ ์ต์ ํ - JOIN ๊ฒฐ๊ณผ๋ฅผ ๊ทธ๋๋ก ์กฐํ ํ ์ ํ๋ฆฌ์ผ์ด์
์์ ์ํ๋ ๋ชจ์์ผ๋ก ์ง์ ๋ณํ: V6
๊ถ์ฅ ์์
1. ์ํฐํฐ ์กฐํ ๋ฐฉ์์ผ๋ก ์ฐ์ ์ ๊ทผ
1. ํ์น์กฐ์ธ์ผ๋ก ์ฟผ๋ฆฌ ์๋ฅผ ์ต์ ํ
2. ์ปฌ๋ ์
์ต์ ํ
1. ํ์ด์ง ํ์ hibernate.default_batch_fetch_size , @BatchSize ๋ก ์ต์ ํ
2. ํ์ด์ง ํ์X ํ์น ์กฐ์ธ ์ฌ์ฉ
2. ์ํฐํฐ ์กฐํ ๋ฐฉ์์ผ๋ก ํด๊ฒฐ์ด ์๋๋ฉด DTO ์กฐํ ๋ฐฉ์ ์ฌ์ฉ
3. DTO ์กฐํ ๋ฐฉ์์ผ๋ก ํด๊ฒฐ์ด ์๋๋ฉด NativeSQL or ์คํ๋ง JdbcTemplate
5. API ๊ฐ๋ฐ ๊ณ ๊ธ - ์ค๋ฌด ํ์ ์ต์ ํ
- OSIV์ ์ฑ๋ฅ ์ต์ ํ