open-in-view WARNING 로그
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 라는 설정이 기본 설정값에 의해 활성화 되어있습니다.
- view 렌더링 과정에서 쿼리가 수행될 수 있음을 경고하고 있습니다.
- view 렌더링 과정까지 쿼리가 수행될 수 있기 때문에, 그 과정까지 DB 커넥션을 풀에 반환하지 않습니다.
- 테스트나 간단한 애플리케이션 수준에선 활성화 되어있는 것이 편의성을 제공하는 측면이 있습니다.
- 꼭 필요하지 않다면 이 설정을 해제하는 것은 성능 개선을 위해 유의미한 행동입니다.
open-in-view 활성화하기
spring:
jpa:
open-in-view: true
- spring.jpa.open-in-view 설정의 기본값은 true입니다.
- 그러나 true로 직접 명시를 한다면 WARNING 로그도 발생하지 않습니다.
예제 코드 구성
@RequestMapping("/api/teams")
@RestController
public class TeamController {
private final TeamService teamService;
public TeamController(final TeamService teamService) {
this.teamService = teamService;
}
@GetMapping
public List<Team> teams() {
return teamService.findAll();
}
}
@Transactional(readOnly = true)
@Service
public class JpaTeamService implements TeamService {
private final TeamRepository teamRepository;
public JpaTeamService(final TeamRepository teamRepository) {
this.teamRepository = teamRepository;
}
@Override
public List<Team> findAll() {
return teamRepository.findAll();
}
}
public interface TeamRepository extends Repository<Team, Long> {
void saveAll(Iterable<Team> teams);
List<Team> findAll();
}
@Getter
@Entity
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL)
private List<Member> members = new ArrayList<>();
}
@Getter
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "team_id", foreignKey = @ForeignKey(name = "fk_member_team_id"))
private Team team;
}
View-Rendering 과정에서 N+1
@GetMapping
public List<Team> teams() {
return teamService.findAll();
}
- Team은 Member를 OneToMany로 알고 있습니다.
- Member도 Team을 ManyToOne으로 알고 있습니다.
- 컨트롤러에서 Team을 직접 반환한다면, Team 안에 Member를 렌더링하려다가 Member 안의 Team을...
- 순환참조가 발생하며 StackOverFlow 에러가 발생합니다.
- 이러한 현상은 응답 DTO를 별도로 만드는 것을 권장하는 이유 중에 하나입니다.
open-in-view 를 false로 준다면
Resolved [org.springframework.http.converter.HttpMessageNotWritableException:
Could not write JSON: failed to lazily initialize a collection of role:
com.study.cache.domain.Team.members: could not initialize proxy - no Session]
- open-in-view 옵션을 false로 주더라도 N+1 이 발생하지 않는 것은 아닙니다.
- 다만 view-rendering 과정에서 N+1 쿼리를 내보낼 세션이 없어서 No Session 오류가 발생한 것입니다.
- Team이 List로 가지고 있는 Member는 OneToMany 기본 옵션에 따라 Lazy로 fetch 합니다.
- 즉 프록시 객체로 가져오게 되는 것입니다.
- 뷰 렌더링 과정에서 이 프록시 객체의 값을 꺼내려하게 되고, 이때 세션이 없어서 오류가 발생한 것입니다.
- 세션이 없는 이유는 open-in-view를 껐기 때문입니다.
- 즉, 기존에는 개발자가 직접 작성한 영역인 컨트롤러의 return teamService.findAll(); 를 떠나서도
DB 커넥션이 유지되고 있었다는 것입니다. - 여기까지 알아보면 open-in-view 설정이 어떤 역할인지 파악이 되었네요!
open-in-view: false + join fetch
@Query("select t from Team t join fetch t.members")
List<Team> findAll();
- open-in-view를 false로 해서 뷰 렌더링 과정에서 프록시 객체를 초기화할 수 없어서 안된거라면..
- join fetch 를 이용해 한번에 초기화를 한다면 어떨까요?
- @Query 애너테이션을 이용해 Team을 조회할 떄 Member도 한번에 조회하도록 했습니다.
- 프록시 객체가 아니라 실제 엔티티가 한번에 조회됩니다.
- 즉, 뷰 렌더링 과정에서 추가적으로 세션을 통한 쿼리 호출이 필요하지 않게 됩니다.
- 이렇게 함으로써 N+1 문제는 해결이 됐습니다.
- 그러나 순환 참조의 문제는 여전히 발생하고 있습니다.
- 데이터는 이미 있어서 getter를 이용해 데이터를 꺼내는 데에는 문제가 없으나,
Team안에 Member안에 Team 안에.. 이렇게 계속 렌더링해야하기 때문입니다.
open-in-view:false + join fetch + 응답 DTO
- open-in-view 를 false로 주었고, N+1문제도 해결되었고, 정상 응답도 완료되었습니다.
- open-in-view와 N+1은 직접적인 연관이 있는 것은 아닙니다.
- 다만, open-in-view가 기본값이 true상태이고, 컨트롤러에서 프록시 객체를 리턴하게 된다면,
뷰 렌더링 과정에서 프록시 초기화를 위해 DB 쿼리를 호출하게 되는데,
이때 List로 프록시 객체를 가지고 있다면 뷰 렌더링 시점에 N+1이 발생하는 것입니다.
각각의 설정에 따른 변화 살펴보기
open-in-view : 뷰 렌더링 시점까지 DB 커넥션을 유지할 것인지
join fetch : 연관 관계에 있는 엔티티를 조회 시점에 같이 가져오기
응답DTO의 적용 : 엔티티 그대로 반환 시 순환 참조 발생 가능
open-in-view | join fetch | 응답 | no Session | 순환참조 | N+1 | DB커넥션 유지 | 비고 |
true | X | List<Team> | X | O | O | 최종응답까지 | |
false | X | List<Team> | O | O | O | 트랜잭션 범위까지 | DB커넥션 빠른 반환 |
false | O | List<Team> | X | O | X | 트랜잭션 범위까지 | N+1 해소되며 no Session도 해소 |
false | O | List<TeamResponse> | X | X | X | 트랜잭션 범위까지 | 응답 DTO로 순환참조 해소 |
정리
응답DTO 도입, N+1 없애기, open-in-view는 다르면서도 약간씩 연관되어 있습니다.
- 도메인 엔티티 대신 응답 DTO를 반환해야겠습니다.
- 응답 DTO는 View 영역의 변경과 도메인을 분리해주는 역할을 수행합니다.
- 추가적으로 순환참조를 방지해줍니다.
- 여기까지만 해도 애플리케이션이 굴러가는 데에는 문제가 없을 수 있습니다.
- N+1 문제는 애플리케이션 성능에 치명적입니다.
- 소규모에선 아무 문제가 없는 것처럼 느껴질 수도 있으나…
- 애플리케이션 성능을 좌우하는 가장 큰 병목지점은 역시나 DB I/IO입니다.
- 불필요한 쿼리가 나가지 않게 N+1을 해결해야 DB I/O를 최소화하여 성능을 개선할 수 있습니다.
- N+1해결에는 @Query + join fetch 또는 batchSize 애너테이션 또는 전역속성, EntityGraph 등이 사용됩니다.
- 대부분 db의 in절 최대가 1000이어서 batchSize는 1000 이하를 권장합니다.
- EntityGraph는 left outer join이 기본이므로 유의해야 합니다.
- join fetch 는 한번에 둘 이상 컬렉션에 사용할 순 없습니다.
- open-in-view false 는 트랜잭션 범위를 벗어나면 즉시 DB 커넥션을 반환하는 설정입니다.
- 기본값은 true이고 경고 로그를 띄웁니다.
- 명시적으로 true를 설정해주면 경로 로그가 나오지 않습니다.
- 간단한 테스트 개발중이 아니라면 가급적 false를 주는 것이 나을 듯 합니다.
- true를 두고 개발하더라도 뷰렌더링 시점까지도 DB 커넥션이 끝까지 유지된다는 사실을 유념해야겠습니다.
위 예제에서는 최종적으로 응답 DTO 도입 + join fetch 선언 + open-in-view:false 를 사용함으로 마무리했습니다.
'Java & Spring' 카테고리의 다른 글
Nested Class (0) | 2023.03.26 |
---|---|
Spring Cache Abstraction (0) | 2022.11.28 |
Spring 외부 설정 파일 import 및 프로퍼티 리팩터링 (2) | 2022.11.17 |
🙌 쓰레드풀을 이용해 두 비동기 작업 처리 완료 후 후속 처리하기 (0) | 2022.09.29 |
🥄 쓰레드풀 한 스푼 (2) | 2022.09.29 |