could not initialize proxy – no Session
개념을 설명하기 위해 작성한 코드입니다.
테스트는 거치지 않는 코드이니 오류가 있을 수 있습니다.
소스코드
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
@OneToMany
private Set<Role> roles;
}
@Entity
@Table(name = "role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int id;
@Column(name = "role_name")
private String roleName;
}
오류 코드
public UserDto getUser(Long userId) {
User user = findUserById(userId);
return UserDto.builder()
.id(user.getId())
.firstName(user.getFirstName())
.lastName(user.getLastName())
.roles(user.getRoles())
.build();
}
원인
- JPA에서 Session 은 영속성 컨텍스트를 의미한다.
- 영속성 컨텍스트 안에서 JPA 객체들이 관리된다.
- 영속성 컨텍스트는 트렌젝션 시작시 생성되고 트렌젝션 종료시 사라진다.
User user = findUserById(userId);
JPA 는 기본적으로 필요한 최소한의 DB 접근을 하도록 되어 있습니다.
@Transactional
설정이 없는 경우,
트렌젝션은 메소드 호출 단위로 이루어지고,
위 메소드 호출이 끝남과 동시에 트렌젝션이 종료됩니다.
@OneToMany
private Set<Role> roles;
문제는 위 부분의 DB 데이타를 가져오지 않은 상태에서,
트렌젝션이 종료(no Session) 된 것입니다.
해결책
@Transactional 추가
해결책 중 하나는 오류가 발생한 메소드를 트렌젝션으로 묶어주는 것입니다.
문제는 두번의 쿼리로 분리되어 데이타베이스에 접근하게 됩니다.
(user 테이블 조회 한번, role 테이블 조회 한번)
@Transactional
public UserDto getUser(Long userId) { ... }
FetchType.Eager
또 하나는 FetchType.Eager
를 설정하는 것입니다.
이렇게 되면 항상 roles 정보도 동시에 가져오게 됩니다.
해당 정보가 불필요한 상황도 있을 수 있는데도 항상 조회를 하는 방법이므로 좋지 않습니다.
이 방식의 가장 큰 문제는 User 목록 검색 기능이 필요할 경우,
엄청난 성능저하가 발생합니다.
@Transactional
방식은 user 테이블 한번, role 테이블 한번의 조회가 실행되는데,
FetchType.Eager
는 user 테이블을 한번 조회해서 1000개의 데이타를 가져왔다면,
role 테이블을 1000번 추가로 조회해서 데이타를 가져오게 됩니다.
(N+1 문제)
@OneToMany(fetch = FetchType.Eager)
private Set<Role> roles;
Fetch Join
또 하나는 Fetch Join
을 하는 것입니다.
FetchType.Eager
와 기능은 동일하지만,
쿼리 단위로 Fetch 모드를 조절할 수 있습니다.
SELECT u FROM User u JOIN FETCH u.roles
구현 방법은 JPQL 이 될수도 있고, QueryDSL 이 될수도 있습니다.
필요한 시점에 필요한 데이타를 한번에 가져오는 유일한 방법입니다.