반응형

JPA를 사용하여 개발할 때 Entity간 연관관계를 지정하는 것은 일반적이다

 

이러한 연관관계를 지정하는 경우 N+1문제가 발생하는 경우 때문에 아래와 같이 FetchType을 Lazy로 한다

(물론 JPA에서는 Eager가 기본)

 

연관관계의 FetchType을 Lazy로 한 경우 조회 기능을 개발할 때 아래와 같이 LazyInitializationException 가 발생하는 경우가 있다

LazyInitializationExceptionorg.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: kr.co.kimga.order.domain.entity.order.Order.orderItems: could not initialize proxy - no Session at org.hibernate.collection.spi.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:635) at org.hibernate.collection.spi.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:219) at org.hibernate.collection.spi.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:150) at org.hibernate.collection.spi.PersistentBag.size(PersistentBag.java:353) at kotlin.collections.CollectionsKt__IterablesKt.collectionSizeOrDefault(Iterables.kt:39) at kr.co.kimga.order.infrastructure.service.order.OrderService.findOrderDetails(OrderService.kt:82) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568)

 

발생 이유

왜 이런 문제가 발생하는 것일까?

LazyInitializationException이 발생하는 코드를 살펴보자

Order (
	.....

    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST])
    val orderItems: MutableList<OrderItem> = mutableListOf
    
	.....
) {
	.....
}
fun findOrderDetails(orderId: Long): FindOrderDetailsDto {

    val findOrder = orderRepository.findById(orderId)
        .orElseThrow { throw CanNotFindOrder() }

    return FindOrderDetailsDto(
        orderId = findOrder.id!!,
        orderStatus = findOrder.status,
        orderDate = findOrder.orderDate,
        items = findOrder.orderItems.map {
            FindOrderItemDto(
                productId = it.productId!!,
                productName = it.productName,
                quantity = it.remainQuantity()
            )
        }.toList(),
        payedAmount = findOrder.orderPays.sumOf { it.amount },
        discountAmount = findOrder.orderPays.sumOf { it.discountAmount },
        totalAmount = findOrder.orderPays.sumOf { it.amount + it.discountAmount }
    )
}

 

위 코드는 주문 정보를 조회하고 이를 DTO에 맞춰 가공한 후 전 반환하는 코드이다

코드 자체로만 보면 특별히 문제가 없다고 볼 수 도 있지만, 실행하면 LazyInitializationException이 발생하게 된다

 

그 이유는 FetchType이 Lazy이기 때문이다

 

해당 함수의 트랜잭션 범위는 orderRepository.findById로 조회를 하는 전후 이다

findById로 조회가 끝나는 경우 트랜잭션이 종료가 되는데, DTO객체로 Entity를 가공하는 과정에서 Lazy로 지정된 객체에 접근하게 된다

Lazy로 지정된 객체에 접근하게 되면 JPA는 해당 객체의 값을 데이터베이스로 부터 읽어오게 된다

그러나 이미 트랜잭션의 범위가 끝났기 때문에 조되된 Entity는 영속 상태가 아니게 되고 이 때문에 LazyInitializationException이 발생하게 된다

 

해결 방법

이를 해결하는 대표적인 두가지 방법은 Eager로 설정하는 방법과 Transaction 범위를 넗히는 것이다

 

1. FetchType Eager

Entity 조회할 때 연관관계의 Entity도 함께 조회하는 방법이다

연관관계의 Entity를 한번에 조회하게 되면 조회 이후에 연관관계 Entity를 추가로 조회할 필요가 없기 때문에 영속 상태가 아니여도 데이터를 활용할 수 있다

Order (
	.....

    @OneToMany(mappedBy = "order", fetch = FetchType.EAGER, cascade = [CascadeType.PERSIST])
    val orderItems: MutableList<OrderItem> = mutableListOf
    
	.....
) {
	.....
}

 

❗️Eager를 사용하게 되면 한번에 많은 데이터를 가져오는 것을 조심해야한다

Entity를 조회할 때 연관관계 Entity를 모두 가져오게 되면 자칫 많은 데이터를 가져올 수 있고, Out Of Memory Exception이 발생할 수 있다

 

2. @Transactional 활용

Fetch Lazy 상태에서 연관관계 Entity의 데이터에 접근하게 되면 해당 Entity의 데이터를 DB로 부터 읽어오게 된다

이를 위해서 Entity가 영속 상태인 것이 중요한데, 영속 상태를 유지하기 위해 Transaction의 범위 확장할 필요가 있다

이를 위해 메소드에 @Transactional 어노테이션을 활용할 수 있다

 

메소드에 @Transactional 어노테이션을 붙이게 되면 트랜잭션 범위를 함수 내로 확장할 수 있다

이를 통해 Entity는 영속 상태를 유지하게 되고 연관관계의 Entity를 조회해올 수 있게 된다

@Transactional(readOnly = true)
fun findOrderDetails(orderId: Long): FindOrderDetailsDto {

    val findOrder = orderRepository.findById(orderId)
        .orElseThrow { throw CanNotFindOrder() }

    return FindOrderDetailsDto(
        orderId = findOrder.id!!,
        orderStatus = findOrder.status,
        orderDate = findOrder.orderDate,
        items = findOrder.orderItems.map {
            FindOrderItemDto(
                productId = it.productId!!,
                productName = it.productName,
                quantity = it.remainQuantity()
            )
        }.toList(),
        payedAmount = findOrder.orderPays.sumOf { it.amount },
        discountAmount = findOrder.orderPays.sumOf { it.discountAmount },
        totalAmount = findOrder.orderPays.sumOf { it.amount + it.discountAmount }
    )
}

 

* Transactional 어노테이션의 readOnly 속성을 true로 지정하게 되면 조회만을 수행하는 함수에 불필요한 동작(flush, drity checking 등)을 수행하지 않아 성능 최적화를 할 수 있다

 

반응형

'공부 > JPA' 카테고리의 다른 글

JPA 순환 참조  (1) 2025.06.24
JPA 시작  (0) 2021.04.25
반응형

JPA를 사용하면서 자칫하면 순환참조가 발생하는 경우가 있다

그렇다면 왜 JPA를 사용할 때 이러한 일이 발생할까

1. 원인

일반적으로 JPA에서 순환참조가 발생하는 이유는 양방향 연관관계가 존재하는 경우 이다

/* 양방향 연관관계 */
@Entity
data class Customer(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val name: String,

    @OneToMany(mappedBy = "customer")
    val orders: List<Order> = mutableListOf()
)

@Entity
data class Order(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val orderDate: String,

    @ManyToOne
    @JoinColumn(name = "customer_id")
    val customer: Customer
)

이 처럼 Customer(고객)가 여러 Order(주문)을 가지는 경우에 대해 필요 시 양방향 연관 관계를 설정할 수 있다

 

그렇다면 이러한 상황에서 Customer의 정보를 조회하여 조회된 결과를 그냥 반환한다고 하면 순환 참조가 발생할 수 있게 된다

@Service
class CustomerService(
    private val customerRepository: CustomerRepository
) {
	fun getCustomerById(id: Long): Customer? = 
		customerRepository.findById(id).orElseThrow(EntityNotFoundException("Customer not found"))
}

@Repository
interface CustomerRepository : JpaRepository<Customer, Long>

이렇게 조회된 Customer를 별도의 DTO 객체로 변환하거나 하지 않고 Service를 지나 Controller에서 반환된다면 JSON으로 변환될 때 순환 참조를 맛볼 수 있다

 

그렇다면 왜 JSON으로 변환하는 과정에서 순환 참조가 발생하게 되는 걸까?

 

2. 이유

그 이유를 알아보기 위해서는 객체를 JSON으로 변환해주는 라이브러리를 살펴볼 필요가 있다

Spring에서는 @RestController가 있는 Controller에서 객체를 반환하면 JSON 형식으로 변환되어 응답이 나가게 된다

이러한 것은 Spring 내부에 있는 Message Converter(HttpMessageConverter)가 우리의 객체를 Http 메시지로 변환하는 과정을 통해 이루어 진다

Controller에서 반환된 객체는 ReturnValue Handler에 의해 처리가 되고,

반환된 객체는 Jackson Message Coverter에 의해 처리가 되고 내부에 ObjectMapper에 의해 객체가 json으로 serialize 되는 과정에서 객체 내부의 필드에 대해 재귀 호출이 발생하고 이 과정에서 양방향 연관관계가 있는 객체에 대해 순환 참조가 발생하게 되고 StackOverFlow Exception이 발생하게된다

3. 해결 방법

1) @JsonIgnore

@Entity
class Child(
	@Id
	@GenerateValue
	val id: Long? = null,
	
	@ManyToOne
	@JoinColumn(name = "parent_id")
	@JsonIgnore
	val parent: Parent
)

JsonIgnore 어노테이션을 사용하면 직렬화를 할 때 해당 변수에 대한 직렬화를 제외하게 되고 순환 참조가 일어나지 않는다

단, 이러한 경우 해당 변수에 대한 값은 직렬화 되지 못하여 반환된 값에서 제외 된다

 

2) DTO활용 (가장 적절한 방법)

데이터를 반환할 때는 DTO를 통해 의미 있는 값만 전달 반환하는 것이 바람직하다

data class ParentDto(
    val id: Long?,
    val children: List<ChildDto>
)

data class ChildDto(
    val id: Long?,
    val parentId: Long?
)

fun Parent.toDto(): ParentDto = ParentDto(
    id = this.id,
    children = this.children.map {it.toDto()}
)

fun Child.toDto(): ChildDto = ChildDto(
    id = this.id,
    parentId = this.parent.id
)

이 처럼 원하는 데이터만 DTO에 포함하여 반환하면 순환참조를 제어할 수 있고, 진짜 필요한 값만 반환할 수 있기 때문에 가장 추천되는 방법이다

 

위에서 설명한 것 외에도 순환 참조가 발생하는 케이스들이 존재하기 때문에 JPA를 사용하는 경우 이를 유의하며 코드를 작성해야 한다

반응형

'공부 > JPA' 카테고리의 다른 글

JPA 조회 함수에서 @Transactional이 필요한 경우(LazyInitializationException)  (0) 2025.07.06
JPA 시작  (0) 2021.04.25
반응형

공부한 것을 기록하기 위한 글입니다.

 

JPA(Java Persistence Api)

 

우선 JPA를 배우기 전에 JPA가 무엇인지 부터 알아야할 것같습니다.

일반적으로 Spring으로 개발 시에 Mybatis와 같은 ORM 프레임워크를 활용합니다.

 

ORM을 사용하게 되면 쿼리를 통해 DB로 부터 데이터를 가져온 후 이를 VO 또는 DTO 객체로 받아서 데이터를 가공 또는 처리하는 순서로 진행이 될 것입니다.

이는 아주 간단한 데이터를 조회할 때도 동일하게 동작합니다.

 

Service(Java, xml) - Mybatis - JDBC - DB

간단하게 작성하면 위 구조로 동작을 한다고 볼 수 있다.

 

여기서 작성한 쿼리를 통해 가져온 결과를 DTO 객체에 매핑을 해주게 된다

 

위와 같이 동작을 하게 되면 단순 조회나 값 변경 등에 있어서 많은 과정을 거쳐야된다.

 

JPA는 이러한 문제점을 해결해주고 객체를 이용해서 데이터 베이스의 값을 변경하고 제어할 수 있게 해주는 API 명세서이다.

그리고 이러한 JPA를 구현한 것이 많이들 사용하고 있는 Hibernate 프레임워크 이다.

 

JPA는 명세서

Hibernate는 명세서를 구현한 프레임워크

 

반응형

+ Recent posts