문제
오늘 회사에서 특정 기능을 배포하자마자 문제가 발생했다. 핀포인트에서 발생하고 스택 트레이스는 다음과 같았다.
> lazyinitializationexception could not initialize proxy
Spring Boot, JPA 활용하는 개발자에게는 흔히 발생되는 예외중 하나이다.
이 예외가 발생하게된 이유는 무엇일까?
코드를 통해 이해해보자.
@Entity
public class Order {
private long id;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> orderItems;
}
public interface OrderRepository extends JpaRepository<Order, Long> {
}
@Service
public class OrderService {
private final OrderRepository repository;
//@Transcational
public Order getOrder(long id){
Order order = repository.findById(id);
order.getOrderItem().get(0).getName(); // 여기서 문제가 발생함!
return order
}
}
spring:
jpa:
open-in-view: false
코드를 살펴보면, OrderService 의 getOrder 에서 @Transactional 붙이지 않게 되면서, getOrder의 Transactional 의 영속성이 인터페이스까지 연결되지 않아 orderItems entity 를 조회할 수 없어 발생하게 된다.
이 문제에 대해 개발자가 코드를 잘못 작성했기 때문에 문제가 발생했다고 할 수 있을까? 휴먼에러라 결정하고 다음부터는 코드를 잘 작성해야 한다고 결정하는게 맞는걸까?
우리가 겪었던 문제 상황에 대해서 다시 돌아가보자.
method에 @Transactional 추가하지 않아 문제가 발생했다.
JPA의 객체그래프는 Lazy Loading 방식으로 호출되도록 설정되었기 때문에 orderItem 을 호출하는 시점까지 Proxy로 물고있던 부분이 @Service 클래스에서는 Thread에 Hibernate의 Session이 유지되지 않아 예외가 발생하게 되었다.
질문으로 돌아가 '영속성 처리하는 Method에 @transactional는 항상 붙이는게 맞을까?' 에 대해서 이 글을 읽는 사람은 어떻게 생각할까? OpenSessionInView가 False 이기 때문에 무조건 붙이는게 맞을까?
내 생각이 틀릴 수도 있지만 적어도 나는 이렇게 생각했다.
1. 관심사의 분리가 잘되고 있는걸까?
흔히 JPA 에서 사용되는 @Entity 애노테이션을 활용해 객체그래프를 만들게 된다. @OneToMany, @ManyToOne 등등 여기서 fetch=Lazy 를 설정하도록 여러 선배개발자들은 권고하고 있다. @Entity 를 통해 만들어진 객체그래프를 활용하는 @Service 가 붙은 클래스 관점에서는 fetch=Lazy 설정을 알 수 있을까?
우리가 개발하는 대부분의 아키텍쳐는 레이어드아키텍쳐를 따르게 되는게 이 아키텍쳐를 설명할 때 첫번째로 말하는 핵심은 관심사의 분리를 말한다.
각각의 계층의 역할을 관심사에 따라 독립적으로 분리될 수 있어야 한다. 관심사의 분리에 의해 우리는 비지니스 로직에 집중해 코드를 작성할 수 있는데, @Service가 붙은 클래스에서는 @Entitiy 에 붙은 Lazy 설정에 관심을 갖고 코딩해야 되는걸까?
뭔가 이상하다고 느꼈다.
JPA 의 객체그래프 설정시 Lazy Loading, Eager Loading 등의 설정은 JPA 를 활용한 개발 복잡도를 더 높이는게 아닌가? 라는 생각이 든다.
2. 무조건 @Transactional 붙여야할까?
@Service
public class OrderService {
private final OrderRepository repository;
private final OrderItemRepository orderItemRepository;
private final SlackSender slackSender;
@Transcational
public void increaseViewCount(long id){
Order order = repository.findById(id);
order.increaseViewCount();
slackSender.send("성공"); // 만약 여기가 실패한다면?
}
@Transcational
public OrderItem getOrderItem(long id){
OrderItem order = orderItemRepository.findById(id);
return order;
}
}
예를 들어, 관리자에게 View Count가 올라갔다는 슬랙 메세지 전송이 포함된 @Transactional 이 붙은 메소드라면, 서드파트 라이브러리 슬랙 API 가 사용되는데, increaseViewCount(id) 메소드 자체가 실패하는 문제가 발생했다고 가정해보자. 그렇다면 increaseViewCount() 메소드도 rollback 이 되면서 유저는 좋지 못한 경험을 할 가능성이 높아진다.
서드파트 라이브러리가 @Transactional 에 함께 포함되어 호출될 경우 조심해야 한다.
이번에는 getOrderItem() 메소드를 살펴보자. @Transactional가 필요할까? 이미 findById 에서 @Transactional 이 정의되어 있기 때문에 불필요한 비용을 발생하게 된다.
결론
JPA 을 잘 알고 사용하자....!
참고자료
https://medium.com/@jkha7371/is-transactional-readonly-true-a-silver-bullet-1dbf130c97f8
'가치관 쌓기 > 개발 돌아보기' 카테고리의 다른 글
스프링에서 제공하는 XXTemplate 은 무엇일까? (RestTemplate, JdbcTemplate, TransactionTemplate, HibernateTemplate, SqlSessionTemplate...) (0) | 2024.11.08 |
---|---|
(수정)DB 에서 하는 동시성제어와 애플리케이션에서 하는 동시성 제어는 어떤게 다른걸까? (0) | 2024.10.08 |
잘 알아보고 사용하자 Java Annotation(주석) (0) | 2024.03.10 |
왜 코드 리팩토링을 수행해야 될까? (5) | 2023.09.26 |
코드가 부채다. - 일주일간 작성한 코드를 오늘 삭제했다. (0) | 2023.07.31 |
댓글