Kafka로 주문 완료 메시지를 주고받는 스마트ETA 시스템을 개발 중이었다.
Spring Boot + JPA 기반의 애플리케이션에서 Kafka 메시지를 수신한 후, Order
엔티티를 조회하고 관련 Store
정보를 활용해 지연 통계를 갱신하는 로직을 구현했다.
하지만 메시지 수신 처리 도중 예상치 못한 예외가 발생했다.
Kafka 메시지 리스너에서 예외 발생:
org.hibernate.LazyInitializationException: Could not initialize proxy [com.example.delivery.store.domain.Store#1] - no session
관련 로그 중 일부:
DeliveryCompletedListener.listen(DeliveryCompletedEvent event) → processCompletedOrder(order)
→ order.getStore().getName() 호출 시 예외 발생
OrderService.java
일부
/**
* 배달을 완료하는 메서드
* @param orderId 주문 ID
* @param deliveredAt 배달 완료 시각
*/
public void completeDelivery(Long orderId, LocalDateTime deliveredAt) {
Optional<Order> order = orderRepository.findById(orderId);
if(order.isEmpty()) throw new RuntimeException("주문을 찾을 수 없습니다.");
Order o = order.get();
o.completeDelivery(deliveredAt);
orderRepository.save(o);
// Kafka 메시지 발행
DeliveryCompletedEvent event = new DeliveryCompletedEvent(o.getId(), deliveredAt);
producer.send("delivery-status", event);
}
DeliveryCompletedListener.java
일부
/**
* Kafka로부터 배달 완료 이벤트를 수신하고 처리하는 리스너
*
* - 토픽: delivery-status
* - 그룹 ID: smarteta-group
* - 메시지 형식: DeliveryCompletedEvent (주문 ID, 배달 완료 시간 포함)
*
* 처리 로직:
* 1. 전달받은 주문 ID로 Order를 조회
* 2. 해당 Order의 배달 완료 시간(deliveredAt)을 업데이트
* 3. StoreDelaySummaryService를 통해 지연 통계를 갱신
*/
@KafkaListener(topics = "delivery-status", groupId = "smarteta-group")
public void listen(DeliveryCompletedEvent event) {
try {
log.info("배달 완료 메시지 수신: {}", event);
Optional<Order> optionalOrder = orderRepository.findById(event.orderId());
if (optionalOrder.isEmpty()) {
log.warn("주문 ID={} 에 해당하는 주문을 찾을 수 없습니다.", event.orderId());
return;
}
Order order = optionalOrder.get();
order.completeDelivery(event.deliveredAt());
summaryService.processCompletedOrder(order);
log.info("주문 처리 완료 → 주문 ID: {}", order.getId());
}catch (Exception e){
log.error(" Kafka 메시지 처리 중 예외 발생", e);
}
}
Order.java
의 일부
/* 주문 도메인 */
@Table(name = "orders")
public class Order {
// 주문 아이디
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 유저 ID
private Long userId;
// 매장
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "store_id")
private Store store;
// 매장과 목적지까지의 거리
private double distanceKm;
하나의 EC2 안에 Kafka, MySQL, 그리고 Spring 서버(Docker 컨테이너)가 포함되어 있음
Spring 서버는 Kafka 메시지를 수신하여 JPA를 통해 DB 조회 후 지연 통계 업데이트 로직을 실행함
Kafka Listener가 비동기적으로 메시지를 수신함
Listener는 orderRepository.findById(event.getOrderId()) 로 Order 객체를 가져옴
이 Order는 내부에 Store를 갖고 있지만, 아직 store는 프록시 상태
store.getName()을 호출하는 순간 JPA가 Store를 로딩하려 함
문제는 이 시점엔 이미 Hibernate 세션이 닫혀 있음 (Kafka Listener는 트랜잭션 바깥에서 실행됨)
→ 따라서 DB에 접근할 수 없어 LazyInitializationException 예외 발생
JPA는 @ManyToOne(fetch = LAZY)
관계에서 실제 연관 객체(Store
)를 처음부터 조회하지 않고, 프록시 객체로 감싸 놓는다.
이 프록시는 실제 데이터가 필요한 시점(getName()
호출 등)에 DB에 접근해 데이터를 가져오려 한다.
하지만 이 시점에 JPA 세션(Session) 이 이미 닫혀 있으면 프록시가 초기화되지 못하고 예외가 발생한다.
Kafka 메시지를 수신하는 메서드 listen()
내부에서는 트랜잭션이 자동으로 생성되지 않는다.
따라서 orderRepository.findById(orderId)
로 조회한 Order는 프록시 상태로 존재하며,
해당 Order에 연결된 Store
를 조회하려고 할 때 세션이 이미 닫혀 예외가 발생했다.
즉,
Order order = orderRepository.findById(...).get();
order.getStore().getName(); // 여기서 LazyInitializationException 발생
@Transactional
public void listen(DeliveryCompletedEvent event) {
...
}
Kafka 메시지를 처리하는 메서드에 @Transactional
을 붙이면,
Spring은 해당 메서드 수행 중 JPA 세션을 유지시켜 프록시 초기화가 가능하도록 한다.
결과적으로 LazyInitializationException이 해결되었다.