[JPA] could not initialize proxy - no Session

728x90

 

들어가며


토이 프로젝트를 하며 이 오류를 처음 접하게 되었다. 

아래 내용을 통해 어떻게 해결하는지 알아보자

 

아래 코드를 보자

@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {
	private final LoginEntity login;
	private MemberEntity member;

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities () {
		Collection<GrantedAuthority> collection = new ArrayList<>();

		collection.add(new GrantedAuthority() {
			@Override
			public String getAuthority () {
				return login.getMember().getRole().getCode(); // !!문제 부분!!
			}
		});

		return collection;
	}
 }

 

 

위 코드를 보자 위 코드는 SpringSecurity 클래스를 사용하여 로그인을 처리하는 로직 중 하나이다.

 

getAuthorities() 메소드를 통해 로그인 시 처리할 Role 값을 얻어야 했다.

 

필자는 한 테이블에 user 정보 및 로그인 정보를 담기보다는 User 테이블 따로 Login 테이블 따로 관리하는 방법을 택했다.

그래서 Login 테이블에 로그인ID, Password 를 저장하는 방법을 사용하였다.

 

아래 Entity 를 보자.

 

@Getter
@EntityListeners(AuditingEntityListener.class)
@Table(name = "login")
@Entity
public class LoginEntity {

	@OneToOne(fetch = FetchType.LAZY)
	@JoinColumn(name="member_id")
	private MemberEntity member;
    
    // 다른 필드 생략

}

 

로그인 엔티티를 작성한 로직이다.

 

JPA 는 직접 해보면서 공부하자는 마인드로 여러 블로그를 참조하였고, 90% 이상의 블로그에서

연관관계에서 자식 엔티티에 속하는 연관관계 부분에 @XToOne(fetch = FetchType.LAZY) 

즉 One 으로 끝나는 어노테이션에는 Lazy 로딩하게 작성이 되있는걸 보았고, 나도 그걸 활용하였다.

 

아래 부모 엔티티를 보자

@Builder
@NoArgsConstructor @AllArgsConstructor
@Getter
@Table(name="member")
@Entity
public class MemberEntity {

	@OneToOne(mappedBy = "member")
	private LoginEntity login;

}

 

부모 엔티티 또한 자식 엔티티를 참조하기 위한 1:1 관계를 의미하는 @OneToOne 어노테이션을 사용하였다.

 

얼핏보기에는 문제가 없어 보인다.

 

여러분들은 뭐가 문제인지 알겠는가? 

필자는 오류를 보기 전까지 전혀 몰랐다..

 

could not initialize proxy [org.petmeet.http.db.member.MemberEntity#2] - no Session

 

대충 요약해서 위 오류 메세지가 나왔다. 왜 이 에러 메시지가 나왔을까? 본격적으로 봐보자

 

 

본론


근본적으로 Hibernate 가 EntityManager 를 통해 세션을 생성을 했고 프록시 객체를 생성했지만,

그 생성된 프록시 객체를 세션이 끝난 후에 사용하려고 했기 때문에 위 문제가 발생한 것이다.

 

아래서 자세히 보자.

 

엔티티에서 연관관계를 맺을때 @OneToOne, @ManyToOne 등 어노테이션을 사용한다.

 

JPA에서 엔티티 간의 연관 관계를 설정할 때, 패치 전략(fetch strategy)은 데이터베이스에서

데이터를 가져오는 방식을 결정하는 중요한 설정이다.

 

위 어노테이션은 기본적으로 구현이 fetch 전략이 Eager 로 되어있다.

 

그래서 연관관계를 맺은 엔티티들이 조회를 할 때마다 Join 을 걸어서 불필요한 조회를 하게 된다.

 

내 상황을 비유하자면 그냥 간단한 Member 를 조회하려고 api 를 만들면 login 정보는 필요가 없는데
연관된 엔티티인 Login 엔티티도 매번 같이 조회가 된다는 말이다. 즉 불필요한 Join 으로 인한 성능이 안좋아질 것이다.

 

그에 비해 Lazy 전략은 내가 필요할 때 까지 연관 엔티티를 로드하지않고 , 나중에 필요할때 직접 작업을 해야지
연관 엔티티를 Join 후 조회를 하는 것이다. 

즉 Lazy 는 필요할 때만 Join 을 사용하므로 원래 이게 맞다고 생각한다.

 

그에 반해 @OneToMany, @ManyToMany 처럼 1:다 관계일 경우는 기본이 Lazy 전략을 사용한다.

 

그럼 이제 본론으로 돌아가보자

 

 

위 문제가 왜 발생할까?

LoginEntity 클래스에서 member 필드는 FetchType.LAZY로 설정되어 있다.

이는 login 엔티티를 처음 로드할 때 member 엔티티가 로드되지 않고 프록시 객체로 남아 있음을 의미합니다.

 

CustomUserDetails 클래스에서 login.getMember().getRole().getCode() 메서드를 호출할 때, 

member 엔티티에 접근한다. 이 시점에서 member는 아직 로드되지 않았고,

 

로드하려고 시도하면 이미 트랜잭션이 발생한 직후로 member 는 비어있는 값으로 되어있는 것으로 추정된다.

 

결과적으로 프록시 객체를 초기화하려고 시도할 때 LazyInitializationException이 발생합니다.

 

즉 JPA 엔티티가 트랜잭션 범위 밖에서 작동해서 접근해서 발생하는 것 같다. 

지연 로딩으로 설정한 연관 객체를 사용해야 하는 경우 EntityManger 를 종료하기 전에 연관된 객체에 접근해야 한다.

 

 

위 문제를 어떻게 해결해야 할까?

1) fetch 전략 Eager 로 변경

 

처음으로 떠오른게 그냥 간단하게 fetch 전략을 Eager로 하면 되는거 아닌가? 라는 생각이 들었다.

근데 이거는 최후의 수단이다. 

 

제일 쉬운 방법인 만큼 나중에 후폭풍이 분명 있을 것이다.

 

 

2) @Transactional 을 사용해서 트랜잭션 범위로 만들어 버린다.

위 방법을 사용하였지만, 해결이 되지 않았다... 추후 다시 다뤄보는걸로

 

 

3) Entity 가 아닌 DTO 를 만들어서 전달한다.

저는 위 방법을 사용해서 해결을 했습니다. 제일 보편적으로 사용하는 방식이라고 합니다.

 

@Getter
@Builder
public class MemberDTO {
	private Role role;

	public static MemberDTO from(MemberEntity member) {
		return MemberDTO.builder()
			.role(member.getRole())
			.build();
	}
}

 

MemberEntity 를 DTO 로 만들어서 처리하게 만들었다. 그 후 비즈니스 로직에 담았다.

 

@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {
	private final LoginEntity login;
	private final MemberDTO memberDTO; // 추가

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities () {
		Collection<GrantedAuthority> collection = new ArrayList<>();

		collection.add(new GrantedAuthority() {
			@Override
			public String getAuthority () {
				return memberDTO.getRole().getCode(); // 교체
			}
		});

		return collection;
	}
}

 

그리고 실제 로직을 사용할 서비스인 로직에서 또한 생성자를 통한 초기화를 함으로써 에러를 해결할 수 있었다.

 

@Slf4j
@RequiredArgsConstructor
@Service
public class CustomUserDetailService implements UserDetailsService {
	private final MemberRepository memberRepository;
	private final LoginRepository loginRepository;

	@Transactional
	@Override
	public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException {
		LoginEntity login = loginRepository.findByUsername(username)
			.orElseThrow( () -> new UsernameNotFoundException(username + "Not Found"));
        
        // 바뀐 부분
		MemberEntity member = login.getMember();
		MemberDTO memberDTO = MemberDTO.from(member);

		return new CustomUserDetails(login, memberDTO);

	}

}

 

 

 

결론


역시 맞으면서 배우고 삽질을 해보니 어지간해서 까먹지 않을 것 같다.

역시 뭐든 경험이 최고인 것 같다.

 

실무도 중요하지만, 역시 이론이 뒷받침된 실무가 제일 좋은 것 같다는 생각이 들었고

에러를 해결하기 위해 전반적인 JPA의 개념을 한번 더 잡아서 좋았으며

 

대표적으로 JPA 영속성 컨텍스트의 프록시 -> Lazy Loading 개념을 확실히 이해했다. 

추후 같은 에러가 발생해도 금방 해결할 수 있을 것 같은 자신감을 얻었다..ㅎㅎ

 

 

728x90