๐ OSIV๋ ๋ฌด์์ธ๊ฐ?
**OSIV(Open Session In View)**๋ Spring Boot์์ ๊ธฐ๋ณธ์ ์ผ๋ก ํ์ฑํ๋์ด ์๋ ๊ธฐ๋ฅ์ผ๋ก, HTTP ์์ฒญ์ด ์์๋ ๋๋ถํฐ ์๋ต์ด ์๋ฃ๋ ๋๊น์ง JPA ์์์ฑ ์ปจํ ์คํธ(EntityManager)๋ฅผ ์ด์ด๋๋ ์ ๋ต์ ๋๋ค.
OSIV์ ๋์ ๋ฐฉ์
# Spring Boot ๊ธฐ๋ณธ ์ค์ (OSIV ํ์ฑํ)
spring:
jpa:
open-in-view: true # ๊ธฐ๋ณธ๊ฐ
// OSIV๊ฐ ์ผ์ ธ์์ ๋์ ๋์
@GetMapping("/api/orders")
public List<OrderDto> getOrders() {
List<Order> orders = orderService.findAll();
// ๐ฏ ์ฌ๊ธฐ์๋ ์ง์ฐ ๋ก๋ฉ ๊ฐ๋ฅ! (OSIV ๋๋ถ)
return orders.stream()
.map(order -> new OrderDto(
order.getId(),
order.getMember().getName(), // ์ง์ฐ ๋ก๋ฉ ๋ฐ์
order.getOrderItems().size() // ์ง์ฐ ๋ก๋ฉ ๋ฐ์
))
.collect(toList());
}
๐จ OSIV์ ๋ฌธ์ ์ ๋ค
1. ์ปค๋ฅ์ ํ ๋ญ๋น
@GetMapping("/api/orders")
public List<OrderDto> getOrders() {
// 1. DB ์ปค๋ฅ์
ํ๋ (OSIV ์์)
List<Order> orders = orderService.findAll();
// 2. ์ธ๋ถ API ํธ์ถ (3์ด ์์) - ์ฌ์ ํ DB ์ปค๋ฅ์
์ ์ !
for (Order order : orders) {
paymentService.callExternalApi(order); // ๐ฅ ๋ฌธ์ !
}
// 3. DTO ๋ณํ ์ค์๋ ์ปค๋ฅ์
์ ์
return convertToDto(orders);
// 4. ์๋ต ์๋ฃ ํ์์ผ DB ์ปค๋ฅ์
๋ฐํ
}
๋ฌธ์ : ์ค์ DB ์์ ์ 1์ด์ธ๋ฐ, ์ ์ฒด ์์ฒญ ์ฒ๋ฆฌ ์๊ฐ์ธ 5์ด ๋์ ์ปค๋ฅ์ ์ ์ ์ !
2. N+1 ๋ฌธ์ ์ํ
// OSIV=true์ผ ๋ - ์กฐ์ฉํ N+1 ๋ฌธ์ ๋ฐ์
@GetMapping("/api/orders")
public List<OrderDto> getOrders() {
List<Order> orders = orderRepository.findAll(); // 1๋ฒ ์ฟผ๋ฆฌ
return orders.stream()
.map(order -> new OrderDto(
order.getId(),
order.getMember().getName(), // ๊ฐ Order๋ง๋ค Member ์กฐํ (N๋ฒ ์ฟผ๋ฆฌ)
order.getOrderItems().size() // ๊ฐ Order๋ง๋ค OrderItems ์กฐํ (N๋ฒ ์ฟผ๋ฆฌ)
))
.collect(toList());
}
// ์ด 1 + N + N = 2N+1 ๋ฒ์ ์ฟผ๋ฆฌ ์คํ! ๐ฑ
3. ๋ ์ด์ด ๋ถ๋ฆฌ ์์น ์๋ฐ
// Controller์์ ์ํฐํฐ์ ์ง์ฐ ๋ก๋ฉ ์์ฑ์ ์ง์ ์ ๊ทผ
@GetMapping("/api/orders/{id}")
public OrderDetailDto getOrder(@PathVariable Long id) {
Order order = orderService.findById(id);
// ๐ซ Controller์์ ์ง์ฐ ๋ก๋ฉ - ๋ ์ด์ด ๋ถ๋ฆฌ ์๋ฐ
return OrderDetailDto.builder()
.orderId(order.getId())
.memberName(order.getMember().getName())
.itemNames(order.getOrderItems().stream()
.map(oi -> oi.getItem().getName())
.collect(toList()))
.build();
}
๐ ๊ตญ๋ด vs ํด์ธ ํ์ ์ฌ์ฉ ํํฉ
๐ฐ๐ท ๊ตญ๋ด ํ์ ํธ๋ ๋
๋์ฉ๋ ํธ๋ํฝ ์๋น์ค
# ๋ค์ด๋ฒ, ์นด์นด์ค, ์ฟ ํก ๋ฑ
spring:
jpa:
open-in-view: false # ์ฑ๋ฅ ์ฐ์
์/์ค๊ท๋ชจ ์๋น์ค
# ์คํํธ์
, ์ค์๊ธฐ์
spring:
jpa:
open-in-view: true # ๊ฐ๋ฐ ํธ์์ฑ ์ฐ์
ํ์ด๋ธ๋ฆฌ๋ ์ ๊ทผ๋ฒ
# application-api.yml (๊ณ ๊ฐ์ฉ API)
spring:
jpa:
open-in-view: false
# application-admin.yml (๊ด๋ฆฌ์)
spring:
jpa:
open-in-view: true
๐ ํด์ธ ํ์ ํธ๋ ๋
ํด์ธ ๊ฐ๋ฐ ์ปค๋ฎค๋ํฐ๋ ์๋์ ์ผ๋ก OSIV ๋ฐ๋ ์ ์ฅ:
"Open Session in View is Evil" ๐
- ์ํฐํจํด์ผ๋ก ๊ฐ์ฃผ
- ํ๋ก๋์ ์์ ์ ๋ ์ฌ์ฉ ๊ธ์ง
- ๋ช ์์ ์ฝ๋ > ํธ์์ฑ
Netflix, Amazon ๋ฑ ๊ธ๋ก๋ฒ ๊ธฐ์ ๋ค
// ์ฒ ์ ํ fetch ์ ๋ต ์ฌ์ฉ
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o " +
"JOIN FETCH o.member " +
"JOIN FETCH o.orderItems oi " +
"JOIN FETCH oi.item")
List<Order> findAllWithDetails();
}
๐ก ํ์ ์์ ๊ถ์ฅํ๋ ํด๊ฒฐ ๋ฐฉ๋ฒ๋ค
1. Command์ Query ๋ถ๋ฆฌ (๊น์ํ๋ ์ถ์ฒ)
// Command์ฉ Service (ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง)
@Service
@Transactional
public class OrderService {
public Long createOrder(OrderCreateRequest request) {
// ์ฃผ๋ฌธ ์์ฑ ๋ก์ง
return order.getId();
}
}
// Query์ฉ Service (์กฐํ ์ ์ฉ)
@Service
@Transactional(readOnly = true)
public class OrderQueryService {
public List<OrderDto> findOrdersWithDetails() {
List<Order> orders = orderRepository.findAllWithMemberAndItems();
return orders.stream()
.map(this::convertToDto)
.collect(toList());
}
private OrderDto convertToDto(Order order) {
return OrderDto.builder()
.orderId(order.getId())
.memberName(order.getMember().getName()) // ์ด๋ฏธ fetch join๋จ
.itemCount(order.getOrderItems().size()) // ์ด๋ฏธ fetch join๋จ
.build();
}
}
@RestController
public class OrderController {
private final OrderService orderService; // Command
private final OrderQueryService orderQueryService; // Query
@PostMapping("/api/orders")
public ApiResponse<Long> createOrder(@RequestBody OrderCreateRequest request) {
Long orderId = orderService.createOrder(request);
return ApiResponse.success(orderId);
}
@GetMapping("/api/orders")
public ApiResponse<List<OrderDto>> getOrders() {
List<OrderDto> orders = orderQueryService.findOrdersWithDetails();
return ApiResponse.success(orders);
}
}
2. Repository์์ ๋ชจ๋ ์ฐ๊ด๊ด๊ณ ํด๊ฒฐ
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
// ํ์ํ ์ฐ๊ด๊ด๊ณ๋ฅผ ๋ฏธ๋ฆฌ ๋ชจ๋ fetch join
@Query("SELECT DISTINCT o FROM Order o " +
"JOIN FETCH o.member m " +
"JOIN FETCH o.orderItems oi " +
"JOIN FETCH oi.item i")
List<Order> findAllWithMemberAndItems();
// ํน์ ์ํ์ ์ฃผ๋ฌธ๋ง ์กฐํ
@Query("SELECT o FROM Order o " +
"JOIN FETCH o.member " +
"WHERE o.status = :status")
List<Order> findByStatusWithMember(@Param("status") OrderStatus status);
// ํ์ด์ง๊ณผ ํจ๊ป ์ฌ์ฉ
@Query(value = "SELECT DISTINCT o FROM Order o " +
"JOIN FETCH o.member",
countQuery = "SELECT COUNT(o) FROM Order o")
Page<Order> findAllWithMember(Pageable pageable);
}
3. QueryDSL ํ์ฉํ ๋์ ์ฟผ๋ฆฌ
@Repository
public class OrderQueryRepository {
private final JPAQueryFactory queryFactory;
public List<OrderDto> findOrdersWithCondition(OrderSearchCondition condition) {
return queryFactory
.select(Projections.constructor(OrderDto.class,
order.id,
member.name,
order.orderDate,
order.status))
.from(order)
.join(order.member, member)
.where(
statusEq(condition.getStatus()),
memberNameContains(condition.getMemberName())
)
.fetch();
}
private BooleanExpression statusEq(OrderStatus status) {
return status != null ? order.status.eq(status) : null;
}
private BooleanExpression memberNameContains(String memberName) {
return StringUtils.hasText(memberName) ?
member.name.contains(memberName) : null;
}
}
โ๏ธ ์ ํ ๊ธฐ์ค๊ณผ ๊ถ์ฅ์ฌํญ
๐ ์๋น์ค ํน์ฑ๋ณ ๊ถ์ฅ ์ค์
์๋น์ค ์ ํ OSIV ์ค์ ์ด์ ์ถ๊ฐ ๊ณ ๋ ค์ฌํญ
๋์ฉ๋ API | false | ์ปค๋ฅ์ ํ ์ ์ฝ ํ์ | ์ฒ ์ ํ fetch ์ ๋ต |
Admin ํ์ด์ง | true | ๊ฐ๋ฐ ํธ์์ฑ, ๋ฎ์ ํธ๋ํฝ | ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง |
๋ฐฐ์น ์์ | false | ๋๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ | ๋ฉ๋ชจ๋ฆฌ ํจ์จ์ฑ |
์๊ท๋ชจ ์๋น์ค | true | ๋น ๋ฅธ ๊ฐ๋ฐ ์๋ | ํ์ฅ์ฑ ๋๋น |
๐ฏ ์ ํ๋ก์ ํธ ๊ถ์ฅ์ฌํญ
# ์ฒ์๋ถํฐ OSIV=false๋ก ์์ (๊ถ์ฅ)
spring:
jpa:
open-in-view: false
show-sql: true # ๊ฐ๋ฐ ์ค ์ฟผ๋ฆฌ ํ์ธ
์ฅ์ :
- ์ฑ๋ฅ ๋ฌธ์ ์กฐ๊ธฐ ๋ฐ๊ฒฌ
- ๋ช ์์ ์ธ fetch ์ ๋ต ๊ฐ์
- ํ์ฅ์ฑ ์๋ ๊ตฌ์กฐ ๊ตฌ์ถ
- ๋ ์ด์ด ๋ถ๋ฆฌ ์์น ์ค์
๐จ OSIV ์ฌ์ฉ ์ ์ฃผ์์ฌํญ
- ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง ํ์
// ์ ํ๋ฆฌ์ผ์ด์
์์ ์ ๊ฒฝ๊ณ ๋ก๊ทธ ํ์ธ
// WARN: spring.jpa.open-in-view is enabled by default.
// Therefore, database queries may be performed during view rendering.
- N+1 ๋ฌธ์ ์ ๊ทน์ ํ์ง
# ๊ฐ๋ฐ ํ๊ฒฝ์์ ์ฟผ๋ฆฌ ๋ก๊น
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
use_sql_comments: true
- ์ปค๋ฅ์ ํ ๋ชจ๋ํฐ๋ง
// HikariCP ๋ชจ๋ํฐ๋ง
@Component
public class DatabaseMonitor {
@Autowired
private HikariDataSource dataSource;
@Scheduled(fixedRate = 30000)
public void logConnectionPoolStatus() {
HikariPoolMXBean poolBean = dataSource.getHikariPoolMXBean();
log.info("Active connections: {}, Idle connections: {}, Total connections: {}",
poolBean.getActiveConnections(),
poolBean.getIdleConnections(),
poolBean.getTotalConnections());
}
}
๐ ๊ฒฐ๋ก
ํ์ ํธ๋ ๋ ์์ฝ
- ๊ตญ๋ด: ์ค์ฉ์ฃผ์์ ์ ๊ทผ (์ํฉ์ ๋ฐ๋ผ ์ ํ)
- ํด์ธ: ์์น์ฃผ์์ ์ ๊ทผ (OSIV ์์ ๊ธ์ง)
์ต์ข ๊ถ์ฅ์ฌํญ
- ์ ํ๋ก์ ํธ: OSIV=false๋ก ์์
- ๊ธฐ์กด ํ๋ก์ ํธ: ์ ์ง์ ๋ง์ด๊ทธ๋ ์ด์
- ์ฑ๋ฅ ์ค์: ๋ฌด์กฐ๊ฑด OSIV=false
- ๊ฐ๋ฐ ์๋ ์ค์: OSIV=true + ๋ชจ๋ํฐ๋ง
ํต์ฌ์ ์ ํ์ ์ด์ ๋ฅผ ๋ช ํํ ์๊ณ , ํธ๋ ์ด๋์คํ๋ฅผ ์ธ์งํ๋ ๊ฒ์ ๋๋ค!
์ฑ๋ฅ๊ณผ ํ์ฅ์ฑ์ ์ํด์๋ ๋ค์ ๋ณต์กํ๋๋ผ๋ ๋ช ์์ ์ธ fetch ์ ๋ต์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ฅ๊ธฐ์ ์ผ๋ก ๋ ์์ ํ ์ ํ์ด๋ผ๋ ๊ฒ์ด ํด์ธ ๊ฐ๋ฐ ์ปค๋ฎค๋ํฐ์ ์ผ๋ฐ์ ์ธ ์๊ฒฌ์ ๋๋ค. ๐
'Back-End > JPA' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[JPA] DTO ๋ฐํ vs fetchJoin โ ์ธ์ ์ด๋ค ๊ฑธ ์จ์ผ ํ ๊น? (feat. ์ฐ๊ด๊ด๊ณ ๋งคํ ์ ๋ต) (1) | 2025.08.02 |
---|---|
JPA EAGER vs LAZY ์ ๋ฆฌ (0) | 2025.08.02 |
JPA Cascade Type ์ ๋ฆฌ (1) | 2025.07.28 |