-
[Spring Data JPA] open-in-view 설정에 대해서Spring Framework 2024. 7. 12. 15:50
여느 때처럼 공부하던 중 우연히 다음과 같은 경고 로그를 보게 되었다.
WARN 5971 --- [open-in-view] [main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
다른 로그들은 모두 정상적이었는데 이것 하나만 경고 로그였다. 매우 불편하여 알아보니 이 로그는 스프링 부트 애플리케이션이 실행될 때 spring.jpa.open-in-view (이하 open-in-view) 설정이 true이면, 볼 수 있는 경고 로그라고 한다.
open-in-view란?
스프링 부트에서 JPA를 사용할 때, Hibernate 세션을 뷰 렌더링까지 유지할지 여부를 결정하는 설정이다. true와 false로 설정할 수 있다. true는 기본값이며, 뷰 렌더링 시점에서도 엔티티에 대한 지연 로딩이 가능하다. 하지만 데이터베이스 트랜잭션이 오래 유지되어야 하므로 성능에 악영향을 미칠 수 있다. false로 설정하면 지연 로딩된 엔티티를 뷰에서 사용할 수 없다. 따라서 필요한 데이터라면 미리 로딩하거나 DTO로 변환하여 전달해야 한다. 이렇게 false로 설정하면 성능 최적화에 도움이 된다. 대부분의 경우, 특히 복잡한 애플리케이션이나 대규모 시스템에서는 false로 설정하고, 필요한 데이터를 서비스 계층에서 명시적으로 로딩하는 것이 좋다. 이를 통해 데이터 접근 로직을 명확히 하고 성능을 최적화 할 수 있다.
개념을 알았으니 이제 예제로 알아보자. 먼저 기본 설정에서 어떻게 동작하는지 알아보도록 한다.
open-in-view 설정이 true일 때
Member, Team 엔티티
- Member : Team = N : 1
- 지연 로딩 설정
@Entity @Getter @Setter @ToString(of = {"id", "name"}) public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "team_id") private Team team; public Member(String name, Team team) { this.name = name; if (team != null) { changeTeam(team); } } public Member() { } public void changeTeam(Team team) { this.team = team; team.getMembers().add(this); } }
@Entity @Getter @Setter @ToString(of = {"id", "name"}) public class Team { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToMany(mappedBy = "team") private List<Member> members = new ArrayList<>(); public Team(String name) { this.name = name; } public Team() { } }
MemberService
@Service @Transactional @RequiredArgsConstructor public class MemberService { private final MemberRepository memberRepository; public void addMember(Member member) { memberRepository.save(member); } public Member getMemberV1(Long memberId) { return memberRepository.findMemberV1(memberId); } }
MemberRepository (페치 조인 미사용)
public interface MemberRepository extends JpaRepository<Member, Long> { @Query("select m from Member m where m.id = :id") Member findMemberV1(@Param("id") Long id); }
MemberController
@Controller @RequiredArgsConstructor public class MemberController { private final MemberService memberService; private final TeamService teamService; private final EntityManagerFactory emf; @GetMapping("/memberV1") public String memberV1(Model model) { Member member = memberService.getMemberV1(1L); boolean loaded = emf.getPersistenceUnitUtil().isLoaded(member.getTeam()); System.out.println("teamLoaded = " + loaded); model.addAttribute("member", member); return "memberV1"; } @PostConstruct public void init() { Team team = new Team("teamA"); teamService.addTeam(team); Member member1 = new Member("member1", team); memberService.addMember(member1); } }
memberV1.html
간단히 사용자의 이름과 사용자의 팀의 이름 렌더링
<html lang="ko" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Member</title> </head> <body> <h2>Member</h2> <div> <span th:text="${member.name}"></span> <span th:text="${member.team.name}"></span> </div> </body> </html>
/memberV1 경로로 접근 (로그 결과)
- 지연 로딩으로 설정하고 페치 조인을 사용하지 않았기 때문에 member에 대한 쿼리만 먼저 실행된다.
- teamLoaded도 당연히 false이다.
- 뷰에서 ${member.team.name} 이렇게 member의 team에 접근하는 순간에 team에 대한 쿼리가 실행된다.
- open-in-view 설정이 true이기 때문에 뷰 레이어에서 지연 로딩된 엔티티에 접근할 수 있는 것이다.
Hibernate: select m1_0.id, m1_0.name, m1_0.team_id from member m1_0 where m1_0.id=? teamLoaded = false Hibernate: select t1_0.id, t1_0.name from team t1_0 where t1_0.id=?
open-in-view 설정이 false일 때
application.properties
기본값은 true이기 때문에 별다른 설정이 필요하지 않지만 false로 설정해주기 위해 다음과 같이 설정한다.
spring.jpa.open-in-view=false
/memberV1 경로로 접근 (로그 결과)
open-in-view를 false로 설정했기 때문에 /memberV1 경로로 접근하면 뷰가 렌더링되지 않고 다음과 같은 예외가 발생한다. false 설정으로 인해 영속성 컨텍스트가 닫혀 지연 로딩된 team의 프록시 객체를 초기화할 수 없다는 내용이다.
org.hibernate.LazyInitializationException: could not initialize proxy [project.open_in_view.entity.Team#1] - no Session
MemberRepository (페치 조인 사용)
이번에는 페치 조인을 사용하여 member를 조회할 때 team도 같이 조회하도록 해본다.
public interface MemberRepository extends JpaRepository<Member, Long> { @Query("select m from Member m left join fetch m.team where m.id = :id") Member findMemberV2(@Param("id") Long id); }
MemberController
@Controller @RequiredArgsConstructor public class MemberController { private final MemberService memberService; private final TeamService teamService; private final EntityManagerFactory emf; @GetMapping("/memberV2") public String memberV2(Model model) { Member member = memberService.getMemberV2(1L); boolean loaded = emf.getPersistenceUnitUtil().isLoaded(member.getTeam()); System.out.println("teamLoaded = " + loaded); model.addAttribute("member", member); return "memberV2"; } @PostConstruct public void init() { Team team = new Team("teamA"); teamService.addTeam(team); Member member1 = new Member("member1", team); memberService.addMember(member1); } }
MemberService
@Service @Transactional @RequiredArgsConstructor public class MemberService { private final MemberRepository memberRepository; public void addMember(Member member) { memberRepository.save(member); } public Member getMemberV2(Long memberId) { return memberRepository.findMemberV2(memberId); } }
memberV2.html
memberV1.html과 동일
/memberV2 경로로 접근 (로그 결과)
- 페치 조인을 사용했기 때문에 select 쿼리가 한 번만 실행되는 것을 확인할 수 있다.
- teamLoaded의 결과도 true이다.
- 뷰도 정상적으로 렌더링 되었다.
- open-in-view 설정을 false로 하여 뷰 레이어에서 영속성 컨텍스트에 접근할 수 없게 되었지만, 페치 조인을 사용하여 team에 대한 정보를 이미 로드하여 제공했기 때문에 정상적으로 실행될 수 있었다.
Hibernate: select m1_0.id, m1_0.name, t1_0.id, t1_0.name from member m1_0 left join team t1_0 on t1_0.id=m1_0.team_id where m1_0.id=? teamLoaded = true
경고 로그가 불편해서 단순한 호기심으로 알아 보았지만 알고 보니 깊은 내용이 있었다. 우연한 발견으로 전혀 몰랐던 open-in-view 설정에 대해 이해할 수 있어서 기분이 좋다. 기본 설정이 true인 것은 아마도 초기 개발을 편리하게 진행하라는 의미인 것 같았다. 하지만 결국 나중에는 성능 문제로 이어질 수 있기 때문에.. 처음부터 false로 설정하고 데이터를 제공할 때 연관된 엔티티를 한 번에 로드하여 제공하는 습관을 들여야겠다.
'Spring Framework' 카테고리의 다른 글
[동시성 문제] 게시글 조회수 증가 (0) 2024.08.18 Spring AI 맛보기 (0) 2024.08.13 [Spring Data JPA] 벌크 연산 후 영속성 컨텍스트를 초기화 해야 하는 이유 (0) 2024.07.09 [Spring MVC] 파일 업로드 예제 (1) 2024.05.24 [Spring Security] OAuth2 소셜 로그인 중복 사용자 검증 (0) 2024.05.22