초기화 문제(LazyInitializationException)

Kkd·2025년 1월 3일
0

매일메일 개념정리

목록 보기
58/93

초기화 문제(LazyInitializationException)

초기화 문제는 Lazy Loading 방식에서 프록시 객체(proxy object)가 초기화되지 못하는 상황에서 발생합니다. 이 문제는 LazyInitializationException이라는 예외로 나타나며, 일반적으로 Hibernate/JPA 환경에서 자주 겪게 됩니다.


LazyInitializationException의 발생 원인

  1. Session/EntityManager 종료 후 데이터 접근

    • Hibernate/JPA는 프록시 객체를 사용하여 Lazy Loading을 수행합니다.
    • Lazy Loading에서 연관 데이터를 가져오려면 Session 또는 EntityManager가 열려 있어야 합니다.
    • 만약 데이터 접근 시점에 Session/EntityManager가 이미 닫혀 있으면 프록시 객체를 초기화할 수 없어 예외가 발생합니다.
  2. 잘못된 트랜잭션 관리

    • Lazy Loading은 데이터베이스와 연결된 트랜잭션 내에서만 동작합니다.
    • 트랜잭션이 종료된 상태에서 Lazy Loading이 시도되면 데이터 로드에 실패합니다.

LazyInitializationException 재현 예시

상황

  • User 엔터티는 orders라는 연관 데이터를 Lazy Loading으로 설정.
  • 서비스 계층에서 데이터를 조회한 후, 컨트롤러 계층에서 orders에 접근.
@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Order> orders;
}

@Entity
public class Order {
    @Id
    @GeneratedValue
    private Long id;

    private String product;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}

문제 코드

@Service
public class UserService {
    public User getUserById(Long id) {
        return userRepository.findById(id).orElseThrow();
    }
}

@RestController
public class UserController {
    @GetMapping("/users/{id}")
    public String getUserOrders(@PathVariable Long id) {
        User user = userService.getUserById(id);
        // LazyInitializationException 발생: orders 초기화 불가
        return "Orders: " + user.getOrders().size();
    }
}
  • 문제 원인
    • userService.getUserById(id)에서 User를 반환할 때 orders는 아직 초기화되지 않은 상태입니다.
    • Session이 이미 닫힌 상태에서 컨트롤러가 orders에 접근하려고 시도하면 예외가 발생합니다.

초기화 문제 해결 방법

  1. Fetch Join 사용

    • JPQL 또는 Query DSL을 사용하여 연관 데이터를 한 번에 가져옵니다.
    • SQL 쿼리를 통해 Lazy Loading 대신 즉시 로딩 방식으로 데이터를 로드합니다.
    @Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :id")
    Optional<User> findByIdWithOrders(@Param("id") Long id);
  2. Open Session In View(OSIV) 패턴 사용

    • Spring Boot에서 기본적으로 활성화된 기능으로, 뷰 렌더링 단계에서도 Session을 유지하여 Lazy Loading을 가능하게 합니다.
    • 장점: 쉽게 LazyInitializationException을 방지.
    • 단점: 트랜잭션 범위가 넓어져 성능 문제가 생길 수 있음.
    • 설정:
      • 기본적으로 Spring Boot에서 활성화되어 있으므로 따로 설정이 필요하지 않음.
      • 비활성화: application.properties에서 설정.
        spring.jpa.open-in-view=false
  3. DTO(Data Transfer Object) 활용

    • 서비스 계층에서 필요한 데이터를 미리 로드한 뒤, DTO로 전달합니다.
    • Lazy Loading의 의존성을 제거하고, 명확한 데이터 구조를 전달합니다.
    public class UserDTO {
        private String name;
        private List<String> orders;
    
        public UserDTO(User user) {
            this.name = user.getName();
            this.orders = user.getOrders().stream()
                                .map(Order::getProduct)
                                .collect(Collectors.toList());
        }
    }
  4. Hibernate Batch Fetching

    • 여러 연관 데이터를 한 번에 가져올 수 있도록 Hibernate의 Batch Fetching을 설정합니다.
    • 설정 예시:
      @Entity
      @BatchSize(size = 10)
      public class User {
          // 필드
      }
  5. Transaction 범위 확장

    • 컨트롤러 계층에서 데이터에 접근하기 전에 트랜잭션 범위를 넓혀 Lazy Loading이 가능하도록 설정합니다.
    • Spring에서 @Transactional을 사용하여 범위를 확장.
    @Transactional
    public User getUserWithOrders(Long id) {
        User user = userRepository.findById(id).orElseThrow();
        user.getOrders().size(); // Lazy Loading 강제 초기화
        return user;
    }

결론

  • Lazy Loading은 효율적이지만, 초기화 문제로 예외를 유발할 수 있습니다.
  • Fetch Join, DTO 사용, OSIV 패턴 등의 다양한 해결 방법 중 프로젝트의 요구사항과 성능에 따라 적절한 방식을 선택해야 합니다.
  • 초기화 문제를 피하려면 Lazy Loading을 신중하게 설계하고, 데이터 사용 흐름을 명확히 정의하는 것이 중요합니다.

추가 학습 자료

profile
🌱

0개의 댓글