개발자는 기록이 답이다

org.hibernate.LazyInitializationException 에러 해결 방법 본문

Spring/트러블 슈팅

org.hibernate.LazyInitializationException 에러 해결 방법

slow-walker 2023. 10. 25. 18:53

개발을 하다보면 org.hibernate.LazyInitializationException 에러와 마주치게 될 때가 있습니다. 이 문제는 JPA에서 1:N 참조 관계를 사용할 때 흔히 발생하는 문제 중 하나입니다. JPA를 오랜만에 사용해서 이 문제를 해결하는데 2시간 정도 소요된것 같습니다.

 

org.hibernate.lazyinitializationexception: could not initialize proxy - no session

 

문제 원인

+-----------------------+             +----------------------+
|   Compensation        |             |      Customer        |
+-----------------------+             +----------------------+
| compensationId (PK)   |    N:1      | customerId (PK)      |
| amount                |------------>| customerName         |
| Customer customer     |<------------| List<Compensation>   |
+-----------------------+             +----------------------+


LazyInitializationException은 주로 "지연 로딩(Lazy Loading)"을 사용할 때 발생하는 예외입니다. 이 예외는 Entity(Customer)를 로딩하려고 할 때, 해당 엔티티가 이미 영속성 컨텍스트(Persistence Context/Session)가 이미 닫혀 있거나 접근할 수 없는 상태에서 에서 발생합니다. 지연 로딩은 필요한 데이터를 실제로 사용할 때 로딩하는 방식으로, 이로 인해 엔티티가 분리될 수 있습니다.

 

 

1. FetchType.EAGER 사용

  • FetchType.EAGER는 연관된 엔티티를 즉시 로딩하도록 지시하는 옵션입니다.
  • 필요한 데이터를 한 번에 가져올 수 있으므로 성능 면에서 유리할 수 있습니다.
  • 특정 상황에서 연관된 엔티티의 크기가 크거나 깊게 중첩된 경우, 불필요한 데이터까지 함께 가져오므로 성능 저하나 메모리 부담이 발생할 수 있습니다.


2. Open Session in View 패턴(Optimistic Locking)

  • Spring 프레임워크에서 제공하는 Open Session in View(OSIV) 패턴을 활용하여 세션을 유지하도록 설정합니다
  • OSIV 패턴은 클라이언트 요청 처리 동안 영속성 컨텍스트와 세션을 유지하여 LazyInitializationException이 발생하지 않게 합니다.
  • 하지만 OSIV 패턴은 장기 실행 트랜잭션이나 대량의 데이터 조회 등으로 인해 성능 이슈가 발생할 수 있다는 점에 주의해야 합니다.

3. DTO로 래핑

  • Entity를 DTO(Data Transfer Object)로 래핑하는 것입니다.
  • DTO는 엔티티의 필요한 부분만 포함하는 단순한 데이터 전송용 객체로, 엔티티와의 연관성 문제를 해결할 수 있습니다.
  • 하지만 DTO 객체와 엔티티 사이의 변환 작업이 추가적으로 필요하며, 복잡한 구조의 객체일 경우 많은 변환 작업이 필요해질 수도 있습니다. 

 

해결 방법

해결 방법 3가지를 보고, 저는 EAGER로 다 바꿔보기도하고, open-in view를 true로 설정도 해보고 false로도 설정해봤는데 계속 에러가 발생했습니다. 게다가 이미 저는 전부다 DTO로 변경했는데 왜 안되는 것인지 고민했습니다.

 

지연 로딩 : 프록시 객체로 실제 엔티티 객체 참조

즉시 로딩 : 연관관계에 있는 객체까지 바로 조회

 

게다가 즉시 로딩을 쓰면, 모든 테이블 다 연관시키고, 생각 못한 쿼리가 나간다고해서 즉시로딩(EAGER)으로 바꾸고 싶지 않았습니다.

 

프록시와 지연로딩의 필요성

프록시(Proxy), 지연로딩(LAZY)와 즉시로딩(EAGER)

 

FetchType을 EAGER로 설정하지 않고 LAZY로 사용하려면, 세션(Session)이 엔티티를 로딩할 수 있는 상태여야 합니다.

 

그래서 스택 트레이스를 자세히 살펴봤는데,  CompensationResponse DTO에서 customer.getName();와 같이 엔티티의 필드에 접근하려고 할 때 발생했습니다. 이때, customer 필드는 LAZY 로딩으로 설정되어 있으므로, 실제 데이터베이스에서 해당 필드를 로딩하는 시점에 세션이 열려있어야 합니다. 

 

CompensationResponse DTO 객체 내부에서 엔티티(Customer)를 참조이 있었다는 것을 깨달았고, 해당부분도 DTO 객체로 변경했습니다. 또한 spring.jpa.open-in-view=true로 설정을 변경하면서 문제가 해결되었습니다.

 

  Request
     +
     V
+--------------------+              +--------------------------------------------+
| CompensationService|              |             CompensationController         |
+--------------------+              +--------------------------------------------+
| getAllCompensations|--->(Return)---> ResponseEntity<List<CompensationResponse>>|
+--------------------+              +--------------------------------------------+
                      V
                  Response

            List<CompensationResponse>
                 +
                 V
       +-------------------------------+
       |     CompensationResponse      |
       +-------------------------------+
       | compensationId                |
       | amount                        |
       | customerId                    |- - - - - -> CustomerResponse 
       | customerName                  |- - - - - -> (DTO)
       | contactPerson                 |- - - - - -
       | contactNumber                 |- - - - - -
       +-------------------------------+

         CustomerResponse
               +
               V 
         +------------------------------+
         |      CustomerResponse        |
         +------------------------------+
         | id                           |
         ...
spring.jpa.open-in-view=true // false에서 true로 변경

결론

org.hibernate.LazyInitializationException 에러는 JPA와 영속성 컨텍스트 관련된 문제로, JPA에 대한 더 깊은 이해가 필요하다고 생각했습니다.

 

스프링과 JPA를 사용하면 트랙잭션 범위내에서만 영속성 컨텍스트를 생성하고 관리한다

 

수정 전  vs 수정 후 코드 비교

///// 수정 전 코드 /////
package com.api.teamfresh.controller.dto.response;

import com.api.teamfresh.domain.entity.Compensation;
import com.api.teamfresh.domain.entity.Customer;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class CompensationResponse {
    private long compensationId;
    private Float amount;
    private Customer customer;

    private CompensationResponse(Compensation compensation) {
        this.compensationId = compensation.getId();
        this.amount = compensation.getAmount();
        this.customer = compensation.getCustomer();
    }

    public static CompensationResponse of (Compensation compensation) {
        if (compensation == null) {
            return null;
        }
        return new CompensationResponse(compensation);
    }
}

///// 수정 후 코드 /////
package com.api.teamfresh.controller.dto.response;

import com.api.teamfresh.domain.entity.Compensation;
import com.api.teamfresh.domain.entity.Customer;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class CompensationResponse {

    private long compensationId;
    private Float amount;
     // -> 여기부분을 Customer라는 엔티티 객체를 참조중이었음
    private CustomerResponse customer; // 수정된 부분

    private CompensationResponse(Compensation compensation) {
        this.compensationId = compensation.getId();
        this.amount = compensation.getAmount();
        // -> CustomerResponse에 있는 생성자 사용해서 참조 변경
        this.customer = CustomerResponse.of(compensation.getCustomer()); // 수정된 부분
    }

    public static CompensationResponse of(Compensation compensation) {
        if (compensation == null) {
            return null;
        }
        return new CompensationResponse(compensation);
    }
}
// Customer 엔티티 객체 대신 CustomerResponse 클래스 생성

package com.api.teamfresh.controller.dto.response;

import com.api.teamfresh.domain.entity.Customer;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class CustomerResponse {

    private long customerId;
    private String customerName;
    private String contactPerson;
    private String contactNumber;
    
    public CustomerResponse(Customer customer) {
        this.customerId = customer.getId();
        this.customerName = customer.getName();
        this.contactPerson = customer.getContactPerson();
        this.contactNumber = customer.getContactNumber();
    }
    
    public static CustomerResponse of(Customer customer) {
        return new CustomerResponse(customer);
    }
}

 

// 컨트롤러
@RequiredArgsConstructor
@RequestMapping("/api")
@RestController
public class CompensationController {
    private final CompensationService compensationService;

    // 배상 전체 목록 API
    @GetMapping("/compensation")
    public ResponseEntity<List<CompensationResponse>> getAllCompensations() {
        return ResponseEntity.status(HttpStatus.OK).body(compensationService.getAllCompensations());
    }
}
// 서비스 (수정 전 - for문 사용)
@RequiredArgsConstructor
@Service
public class CompensationService {
    private final CompensationRepository compensationRepository;
    // 배상 전체 목록 조회
    public List<CompensationResponse> getAllCompensations() {
        List<Compensation> compensations = compensationRepository.findAll();
        List<CompensationResponse> response = new ArrayList<>();
        for (Compensation x : compensations) {
            CompensationResponse compensationResponse = CompensationResponse.of(x);
            response.add(compensationResponse);
        }
        return response;
    }
}

// 위의 for문을 stream으로 변경해서 가독성 향상

// 서비스 (수정 전 - stream 사용)
@RequiredArgsConstructor
@Service
public class CompensationService {

	private final CompensationRepository compensationRepository;

    public List<CompensationResponse> getAllCompensations() {
        List<Compensation> compensations = compensationRepository.findAll();

        return compensations.stream()
                .map(CompensationResponse::of)
                .collect(Collectors.toList());
    }
}