본문 바로가기
실습/리눅스 서버 + 스프링 부트

OSIV

by 이민우 2023. 7. 14.
728x90
반응형

JPA를 사용하다보면, 종종 특수한 상황에서 LAZY LOADING을 사용해야하는 경우가 있다.

  • 한 번에 관계 데이터까지 가져오기에는 데이터가 너무 대용량인 경우
  • 너무 복잡한 관계를 가진 데이터인 경우
  • 자주 사용하지 않는 데이터를 관계로 가진 경우

그런데 이렇게 LAZY LOADING을 채택해서 Domain 클래스를 구성했을 때 종종 발생하는 에러가 있다.

바로 LazyInitializationException 에러이다.

 

지연 로딩은 객체가 실제로 필요한 시점에 DB에서 데이터를 로딩하는 방식이다. 그런데 위의 에러는 DB에서 데이터를 로딩해서 세션(영속성)이 종료된 후인데, 다시 한 번 DB에 접근해서 필요한 데이터를 불러오려고 해서 발생하는 문제이다.

 

즉, View에서 데이터를 보여줄 때 이미 DB에 접근하는 세션은 Service 혹은 Repository에서 끝난 상태인데, View에서 다시 한 번 지연 로딩으로 데이터를 가져오려고 해서 발생하는 문제이다.

 

예를 들어보자. 마침 이전 포스팅에 쓸만한 게 있어서 가져와봤다.

https://123okk2.tistory.com/457

 

[SPRING JPA] N+1 문제

N+1 문제 연관 관계가 설정된 엔티티 조회 시 조회된 데이터의 갯수(N)만큼 연관관계의 조회 춰리가 추가로 발생해서 데이터를 읽어오는 현상이다. 처음 이 말을 들었을 때 무슨 말인지 알지 못했

123okk2.tistory.com

 

우선 위 포스팅에서 작성했던 코드 중 Student 내 School을 삭제해주었다. (안그러면 return시 서로가 서로를 참조한다.)

그리고 School의 Student 로딩 방식을 LAZY로 설정했다.

 

마지막으로 아래 컨트롤러를 추가했다.

package com.mwlee.n1test.controller;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.mwlee.n1test.domain.School;
import com.mwlee.n1test.repository.SchoolRepository;

@RestController
public class SchoolController {
	// 편의상 서비스 클래스 생략 및 리포지토리와 곧바로 연결
	@Autowired SchoolRepository schoolRepository;
	
	@GetMapping("/schools")
	public List<School> getSchools() {
		List<School> schools = new LinkedList<>();
		Iterator<School> schoolsItr = schoolRepository.findAll().iterator();
		
		while(schoolsItr.hasNext()) {
			schools.add(schoolsItr.next());
		}
		
		return schools;
		
	}
}

한 번 실제로 접속해보자.

 

잘 되는 것을 확인할 수 있다.

그런데 application.properties에 다음 설정을 추가하면 어떻게 될까?

spring.jpa.open-in-view=false

작아서 안보인다...

앞서 설명한 LazyInitializationException 에러가 발생함을 확인할 수 있다.

 

 

OSIV (Open Session In View)

JPA는 Session이 아니라 EntityManager을 사용하므로 엄밀히 말하자면 OEIV가 맞다. (하이버네이트가 Session이다.)

 

어쨌든 OSIV는 말 그대로 세션을 View에서도 열 수 있게 만들어주는 기능이다. 즉, 뷰가 랜더링이 완료될 때까지 DB 세션을 유지한다.

 

사용 방법은 간단하다. 아래 설정을 application.properties에 추가하기만 하면 된다.

spring.jpa.open-in-view=true

 

그런데 View에서도 세션을 유지한다라는 말은 그만큼 DB 커넥션이 오래 유지된다는 말인데, 운영자 전용 웹 페이지처럼 커넥션이 많지 않은 웹 페이지라면 상관이 없겠지만 실제 사용자에게 제공되는, 즉 DB 커넥션이 많이 일어나는 웹 페이지에서는 사용을 할 수가 없다.

 

그렇다면 OSIV를 사용하지 않으면서 LazyInitializationException을 발생시키지 않는 방법은 없을까?

 

대안 방법이야 물론 존재한다.

애초에 즉시 로딩(EAGER LOADING)이나 FETCH JOIN, Entity Graph를 사용하면 된다. 사용 방법은 N+1 문제 해결 포스팅에 기재해놓았다.

 

그런데 위에서 언급했듯, LAZY LOADING을 사용해야 하는 상황은 반드시 존재하기 마련이다. 그런데 OSIV를 피하기 위해 무조건 위의 방식으로 사용한다는 것은 말이 되지 않는다.

 

그런데 사실 나는 실무에서 OSIV 라는 것을 들어본 적이 없다. 그저 공부를 하다가 우연히 발견한 에러였다. 애초에 발생할 일이 없는 에러였기 때문이다. 물론 JPA를 많이 사용하지 않기 때문이기도 하지만, 애초에 Domain을 대놓고 웹으로 반환할 일 자체가 없기 때문이다.

 

 

Domain, DTO, VO

요즘 자주 헷갈리는 세 가지이다. 일단 먼저 설명을 해보자면 아래와 같다.

Domain (=Entity)
실제 DB의 테이블과 매핑되는 클래스

DTO (Data Transfer Object)
계층간 데이터 교환을 위한 객체

VO (Value Object)
Read Only 버전의 DTO. 즉 불변 속성을 가진 DTO

 

애초에 도메인은 DB에 직접 매핑되는 클래스이다. 그리고 DB에 저장된 정보 중에는 분명 외부 사용자에게 보여져서는 안될 데이터 (주민등록번호 같은 개인정보 등)가 존재한다.

 

그래서 우리 팀은 절대로 Domain을 그대로 외부로 보내지 않았다. 대신 DB에서 필요한 데이터를 불러와 DTO나 VO로 변환해서 출력하는 방식을 채택했었다.

 

이 방식은 반드시 사용자에게 필요한 데이터만을 선별해서 제공하기에, 즉시 로딩 등의 방식에 비해 불필요한 데이터를 로딩하지 않아도 된다.

 

DTO나 VO를 만드는 방법은 필요 데이터들을 가져와서 Service단에서 코드로 직접 집어넣어 사용해도 되고, 굳이 그러기 싫다면 애초에 JPQL을 이용해서 호출하면 된다.

 

예시로 잡은 테이블의 칼럼들이 너무 없어서 사실 예시로 들만한 코드가 없는데, 굳이 또 예시를 들어보자면 아래처럼 VO를 만들어 사용자에게 return할 수 있을 것 같다.

@Getter
@Setter
@ToString
@AllArgsConstructor
public class StudentVO {

	private String schoolName;
	private String stdntName;
	
}


@Repository
public interface StudentRepository extends CrudRepository<Student, String> {

	@Query("SELECT "
			+ "new com.mwlee.n1test.vo.StudentVO( "
			+ "	school.schoolName, "
			+ "	stdntName "
			+ ") "
			+ "FROM Student")
	Iterable<StudentVO> findAllStudentVO();
	
}

 

*컬럼이 하나씩밖에 없어서 예시라고 들기 너무 민망하다.

 

어쨌든 결론적으로 DTO나 VO를 만들어서 날리면 굳이 일어나지 않는 문제이다.

 

마지막으로 어떻게 포스팅을 끝내야 할 지 모르겠으니 우리의 챗지피티에게 질문을 하나 던져주고 끝내자.

728x90
반응형

'실습 > 리눅스 서버 + 스프링 부트' 카테고리의 다른 글

[Apache Server] 리버스 프록시_CentOS, Ubuntu  (1) 2023.09.28
Spring Batch  (0) 2023.07.19
[SPRING JPA] N+1 문제  (0) 2023.07.04
Spring Boot + Mybatis  (0) 2023.07.02
Spring Boot + JSP  (0) 2023.07.02