LessonService.java
public LessonInfoResponseDto lessonInfo(Long lessonId) {
Lesson lesson = findLessonByLessonId(lessonId);
Instructor instructor = lesson.getInstructor();
Users users = instructor.getUsers();
return LessonInfoResponseDto.builder()
.username(users.getUsername())
.lessonName(lesson.getLessonName())
.centerAddress(lesson.getCenterAddress())
.centerName(lesson.getCenterName())
.startDateTime(lesson.getStartDateTime())
.endDateTime(lesson.getEndDateTime())
.maxEnrollment(lesson.getMaxEnrollment())
.build();
}
위 코드는 단순히 레슨 정보를 조회하는 로직이다.
하지만 아래와 같은 에러가 발생을 했다.
{
"occurredTime": "2023-04-16T16:35:26.0428141",
"code": 500,
"message": "could not initialize proxy [com.fitnesspartner.domain.Instructor#1] - no Session"
}
에러를 자세히보면 Instructor 프록시 객체를 initialize 할 수 없는 걸로 보인다.
문제에 대해서 고민 해보기 전에 우선 해결할 방법을 구글에 검색해서 해결했다.
@Transactional
public LessonInfoResponseDto lessonInfo(Long lessonId) {
Lesson lesson = findLessonByLessonId(lessonId);
Instructor instructor = lesson.getInstructor();
Users users = instructor.getUsers();
return LessonInfoResponseDto.builder()
.username(users.getUsername())
.lessonName(lesson.getLessonName())
.centerAddress(lesson.getCenterAddress())
.centerName(lesson.getCenterName())
.startDateTime(lesson.getStartDateTime())
.endDateTime(lesson.getEndDateTime())
.maxEnrollment(lesson.getMaxEnrollment())
.build();
}
@Transactional 처리하니까 간단하게 해결되었다.
구글링 해보니까 대부분 @Transactional로 해결 했다고 한다.
❓왜 해결된 걸까?
그냥 적용 후 해결되었다고 끝내지 말고 조금 더 살펴보자.
우선 트랜잭션 처리로 인해서 해결된 쿼리를 직접 확인해보기로 했다.
Hibernate:
select
lesson0_.lesson_id as lesson_i1_1_0_,
....
from
lesson lesson0_
where
lesson0_.lesson_id=?
Hibernate:
select
instructor0_.instructor_id as instruct1_0_0_,
....
from
instructor instructor0_
where
instructor0_.instructor_id=?
Hibernate:
select
users0_.users_id as users_id1_3_0_,
....
from
users users0_
where
users0_.users_id=?
해결은 되었지만 위와 같이 select 쿼리만 3 개가 날라간다.
select 쿼리가 3개 날라가는 문제와 해결된 이유를 모르는 것을 보고 아래와 같은 생각을 했다.
차근차근 찾아보고 공부해보자.
could not initialize proxy - no Session
JPA에서 Session은 영속성 컨텍스트를 의미한다.
영속성 컨텍스트는 엔티티 객체들을 관리하고 데이터베이스와의 상호작용을 담당하는 일종의 캐시이다.
JPA가 스프링 컨테이너가 제공하는 전략을 따르고 있는데,
기본 전략으로 트랜잭션 범위의 영속성 컨텍스트 전략을 사용하기 때문이다.
위 내용과 오류에 대한 내용으로 추론해보면,
@Transactional 어노테이션을 사용하여 해당 로직을 묶었기 때문에 영속성 컨텍스트가 없는 문제가 해결된 것이다.
이유는 이제 명확하게 알게 되었다.
그렇다면 @Transactional을 사용해서 생기는 문제와 다른 해결 방법을 찾아보자.
멘토링 시간에 코드 리뷰와 함께 위와 같은 의견을 주셨다.
생각해보니까 레슨 정보를 조회하는데 트랜잭션이 필요가 없었다.
그 이유를 트랜잭션의 특징과 연결 지어서 생각해봤다.
트랜잭션의 특징 4가지를 살펴보니,
해당 로직에 @Transactional을 달아야 하지 말아야 할 이유가 더욱 명확해졌다.
그렇다면 단순히 트랜잭션을 지우고 끝나는게 아니라 조금 더 고민해보자.
고민되는 부분을 chatGPT에게 한번 물어봤다.
조금만 생각해보면 당연한 것이였다.
구현을 하고, 작동 하는데만 집중 하다보니까 위와 같은 결과가 나왔다.
그러면 @Transactional이 아니라 다른 해결 방법에는 무엇이 있을까?
문제는 아래의 코드에 있었다.
// 변경 전(Instructor 엔티티)
@OneToOne(fetch = FetchType.Lazy)
@JoinColumn(name = "users_id", nullable = false)
private Users users;
// 변경 후(Instructor 엔티티)
@OneToOne(fetch = FetchType.Eager)
@JoinColumn(name = "users_id", nullable = false)
private Users users;
-------------------------------------------------------------------------------
// 변경 전(Lesson 엔티티)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "instructor_id", nullable = false)
private Instructor instructor;
// 변경 후(Lesson 엔티티)
@ManyToOne(fetch = FetchType.Eager)
@JoinColumn(name = "instructor_id", nullable = false)
private Instructor instructor;
Lazy에서 Eager로 번경하면, 연관 관계 설정이 되어있는 Entity까지 모두 가져온다.
(ManyToOne은 디폴트가 Eager이지만, 적어 놓는게 헷갈리지 않고 도움이 되어서 작성 중)
Eager로 변경 전 쿼리
Hibernate:
select
lesson0_.lesson_id as lesson_i1_1_0_,
....
instructor1_.users_id as users_id7_0_1_
from
lesson lesson0_
inner join
instructor instructor1_
on lesson0_.instructor_id=instructor1_.instructor_id
where
lesson0_.lesson_id=?
Eager로 변경 후 쿼리
select
lesson0_.lesson_id as lesson_i1_1_0_,
....
instructor1_.instructor_id as instruct1_0_1_,
....
users2_.users_id as users_id1_3_2_,
....
from
lesson lesson0_
inner join
instructor instructor1_
on lesson0_.instructor_id=instructor1_.instructor_id
inner join
users users2_
on instructor1_.users_id=users2_.users_id
where
lesson0_.lesson_id=?
Eager로 처리하지 않고 다른QueryDSL을 사용해서 Join 쿼리를 날리는 방법도 있다.
Eager가 현재의 로직에서는 필요한 데이터를 가져오는 장점도 있다.
하지만 다른 로직에서는 불필요한 데이터를 가져오는 것으로 인해,
성능 및 로직에 혼란을 줄 수 있다고 해서 Lazy로 변경했다.
FetchType을 Lazy로 사용했기 때문에 Join을 하기 위해 QueryDSL을 아래와 같이 사용했다.
public LessonInfoResponseDto lessonInfo(Long lessonId) {
QLesson qLesson = QLesson.lesson;
QInstructor qInstructor = QInstructor.instructor;
QUsers qUsers = QUsers.users;
Lesson lesson = jpaQueryFactory.selectFrom(qLesson)
.leftJoin(qLesson.instructor, qInstructor).fetchJoin()
.leftJoin(qInstructor.users, qUsers).fetchJoin()
.where(qLesson.lessonId.eq(lessonId))
.fetchOne();
Users users = lesson.getInstructor().getUsers();
return LessonInfoResponseDto.builder()
.username(users.getUsername())
.lessonName(lesson.getLessonName())
.centerAddress(lesson.getCenterAddress())
.centerName(lesson.getCenterName())
.startDateTime(lesson.getStartDateTime())
.endDateTime(lesson.getEndDateTime())
.maxEnrollment(lesson.getMaxEnrollment())
.build();
}
위 로직을 실행하면 아래와 같아 Select 쿼리가 나간다.
Hibernate:
select
lesson0_.lesson_id as lesson_i1_1_0_,
instructor1_.instructor_id as instruct1_0_1_,
users2_.users_id as users_id1_3_2_,
lesson0_.center_address as center_a2_1_0_,
....
instructor1_.address_details as address_2_0_1_,
....
users2_.created_at as created_2_3_2_,
....
from
lesson lesson0_
left outer join
instructor instructor1_
on lesson0_.instructor_id=instructor1_.instructor_id
left outer join
users users2_
on instructor1_.users_id=users2_.users_id
where
lesson0_.lesson_id=?
QueryDSL로 해결은 했지만,
과연 QueryDSL을 사용하는게 최고의 선택일까?
우선 Fetch Join에 대해서 알아보자.
FetchJoin은 JPA에서 제공하느 기능으로, 엔티티를 조회할 때 연관된 엔티티도 함께 조회하는 방법 중 하나이다. Fetch Join을 사용하면 즉시 로딩(Eager Loading)을 수행하며, 지연로딩(Lazy Loading)을 사용하는 경우에는 N+1 문제가 발생하지 않도록 해결 할 수 있다.