본문 바로가기
JPA

OSIV와 지연로딩(feat. SSE)

by 고선제 2025. 2. 22.

문제 배경

프로젝트 기능 중 사용자가 동영상 업로드 시 AWS MediaConvert가 변환을 모두 완료하여 저장됨을 알리기 위해 SSE를 사용하였다. SSE는 간단하게 설명하면 HTTP의 비연결성을 해결하기 위해 서버와 클라이언트 간 연결을 해주는 것이다. Socket과의 차이점은 서버만 클라이언트로 데이터를 보낼 수 있는 단방향 통신이라는 점이다. 우리는 완료되었다는 알람만 보내면 되므로 해당 기술을 선택하였다.

 

JPA의 Open-Session-In-View의 설정을 비활성화하게 된 이유는 위 SSE의 사용 때문이다. 먼저 OSIV에 대해 간략히 설명하면, OSIV의 활성화 여부는 JPA의 영속성 컨텍스트의 범위를 지정해 준다.

OSIV를 활성화하게 되면 Servlet Container의 Filter부분부터 영속성 컨텍스트가 열려있게 된다. 그리고 요청이 종료되면 영속성 컨텍스트가 닫힌다.

OSIV를 비활성화하게 되면 영속성 컨텍스트의 범위가 트랜잭션의 범위와 동일하게 된다. 트랜잭션이 시작되면 열리게 되고 종료되면 닫히게 된다.

 

SSE는 하나의 HTTP 요청을 계속 유지하는 것이어서 OSIV가 활성화되어 있다면 영속성 컨텍스트가 계속 유지된다. 그렇다면 지속적인 연결 상태에서 트랜잭션 외부 범위에 지연 로딩이 발생한다면, 이를 해결하기 위해 DB 커넥션을 계속해서 재사용하게 될 위험이 있다. 그래서 OSIV를 비활성화하게 되었다.

문제 상황

문제가 된 이유는 앞선 문제 배경에서 모두 설명하였다. 만약 내가 위에 작성한 지식들을 모두 알고 있었다면, 쉽게 해결할 수 있었을 것이다.

 

SSE를 도입하고 OSIV를 비활성화하니 이전에 위 개념들을 모른 채 개발한 부분에서 LazyInitializationException가 발생하였다.

(아래에 적혀있는 no Session은 HTTP Session이 아닌 JPA의 EntityManager를 구현한 Hibernate의 Session이다)

 

발생한 부분은 아래와 같다.

Club 엔터티에서 1 : n 관계를 가진 ClubMember가 있다.

(참고로, @OneToMany의 기본 fetch 전략은 지연 로딩이다.)

 

Club의 필드를 사용하여 ClubMember를 조회하고자 하였을 때, 트랜잭션이 종료된 이후에 조회를 했기에 LazyInitializationException이 발생하였다.

 

OSIV를 비활성화시킨 채, 트랜잭션 범위 밖에서 지연로딩을 시도했기 때문이다. 당연히 영속성 컨텍슽트는 닫혀있기 때문에 프록시 객체로 남아있게 되어 에러가 발생하게 된 것이다.

 

해결 방법

내가 아는 해결방법에는 총 3가지가 있다.

 

1. DTO 활용하기

말 그대로 DTO를 활용하는 것이다. 프록시 객체인 상태로 넘기지 않고 실제 값을 전달하면 된다.

위 예제로 이야기하면, ClubMember를 매개변수로 받는 것이 아니라, ClubMember에서 가져오는 6개의 값을 매개변수로 받으면 될 것 같다.

혹은, 트랜잭션 범위 내에서 ClubMember를 사용하면 될 것 같다.

 

2. JOIN FETCH

JOIN FETCH는 JPQL에서 연관된 엔터티를 모두 즉시 로딩하도록 하는 방법이다.

즉, 자신의 Entity와 JOIN 되어있는 Entity를 JOIN 하여 객체를 할당하지 않고 실제 객체를 바로 주입해 주는 것이다. N+1 문제를 해결하는 방법으로 유명하다. 단 1개의 쿼리로 연관되어 있는 Entity의 정보도 가져올 수 있기 때문이다.

필요한 엔터티만 JOIN 해서 로딩할 수 없다는 단점이 있어 불필요한 데이터도 로딩될 수 있다.

 

3. @EntityGraph

@EntityGraph도 JOIN FETCH와 마찬가지로 자신의 Entity와 연관된 Entity를 즉시 로딩할 수 있게 해 준다. 특징으로는 쿼리를 작성하지 않고, 즉시로딩할 연관 Entity를 직접 지정할 수 있다.

 

해결

 

나는 3번 @EntityGraph를 사용하여 해결했다.

 

그 이유로 첫 번째 DTO 활용은 내가 원하는 근본적인 해결책이 되지 못했다. 단지 프록시 객체를 트랜잭션 밖으로 꺼내지 않음으로써 해결한다는 점 때문이다. 프록시 객체에 실제 값을 주입하는 방법으로 해결해보고 싶었다.

두 번째 JOIN FETCH는 쿼리를 작성해야 한다는 점에서 불편했다. 기본적으로 JPA에서 제공해 주는 쿼리를 작성해야 한다. 또한, 원하는 연관 Entity만 즉시 로딩될 수 없었고, 연관된 모든 Entity를 즉시 로딩해야 했다.

@EntityGraph는 원하는 연관 Entity만 즉시로딩이 가능했고, 쿼리를 작성하지 않아도 되어서 선택하였다.

 

하고 싶은 말

해결방법을 @EntityGraph로 했다고 해서 이 방법이 가장 좋은 방법이 아니라는 것만 인식하면 좋을 것 같다. 어쩌면 내 경우에도 @EntityGraph가 최선이 아니라 DTO를 사용하는 방법이 최선일 수도 있고, JOIN FETCH가 최선일 수도 있다.

하지만 나는 내가 생각한 이유대로 위의 방법을 선택한 것뿐이다.

그리고 이 글을 작성하다 보니, 의문점이 몇 가지 들었다.

  1. 단지 SSE를 도입했다고 해서 OSIV의 설정을 바꾸는 게 맞을까? 만약 엄청 큰 프로젝트일 경우 OSIV 활성화 기반으로 코드를 작성했다면 어떤 해결 방법이 있을까? SSE로 인해 DB 커넥션이 재사용될 위험을 감수해야 하는 건가?
  2. 현재 프로젝트가 너무 JPA와 강하게 결합되어 있다는 생각이 들기도 한다. 실무에서 ORM을 변경할 일이 있을까? 그렇다면 그것을 대비에서 코드를 작성하나?

앞으로 고민 좀 해봐야겠다.

'JPA' 카테고리의 다른 글

JPA로 인한 테스트 실패  (0) 2024.08.13