본문 바로가기
개발 잡담

@Transactional 은 최소화, fetch join 최대화

by 휴일이 2025. 12. 18.

 

1탄

https://hyuil.tistory.com/433

 

 

 

 

*너무너무너무 졸려서 글이 두서없음 주의.. 기록용으로 적음

 

 

 

JPA 를 사용하는 경우 영속성 컨텍스트를 사용하게 된다.

그리하여 영속성 컨텍스트를 사용하겠다는 표시인 @Transactional 애노테이션이 붙어있을 때야만 LAZY 로딩 객체가 제대로 가져와짐.

 

 

 

트랜잭션은 최대한 작은 단위로 쪼개져야하기 때문에 (안그러면 락이 길어지자나..)

나 같은 경우 이런식으로 두 메시지 저장을 한 트랜잭션에 넣었음

 

 

diary, user 모두 LAZY loading ...

 

근데 메서드에 @Transactional 애노테이션이 붙어있지 않으면 ,,, 우리는 영속성 컨텍스트를 활용할 수 없음 ㅜㅜ

LAZY 로딩인 객체들을 가져올 수 없다 이거임..

 

참고) 조회용 메서드라면 read only 옵션을 추가하면 되긴 하는데 이건 조회 후 -> 생성하는 로직이라 read only 옵션은 적합하지 않음

 

 

 

 

그리하여 저는 어떻게 하기로 했냐면요

 

 

 

    @Query("""
select cs from ChatSession cs
join fetch cs.diary d
join fetch d.user
where cs.id = :id
""")
    Optional<ChatSession> findWithDiaryAndUser(Long id);

 

 

연관 된 데이터를 fetch join 하여 한번에 다 가져오도록 했답니다!

 

 

 

 

 

 

만약 @Transactional 을 메서드 레벨에 붙인다면

    @Transactional
    public CreateChatMsgRes createChatMessage(Long chatSessionId, CreateChatMsgReq req) {
        User user = userService.getActiveUser(req.userId());
        ChatSession chatSession = chatSessionRepository.findById(chatSessionId).orElseThrow(() -> {
            return ChatException.chatSessionNotFound(chatSessionId);
        });


        if (chatSession.getDiary() == null) {
            // throw exception
        }
        
        if (chatSession.getDiary().getUser() == null) {
            //throw exception
        }

        if (!user.equals(chatSession.getDiary().getUser())) {
            throw ChatException.chatSessionUserMismatch(
                    chatSession.getId(),
                    user.getId(),
                    chatSession.getDiary().getUser().getId()
            );
        }
  
  ...(생략)
 }

 

 

- 트랜잭션이 넘 길어짐 ㅜㅜ

- 일일히 null 체크 코드를 추가해줘야 함 -> 코드가 더러워짐

- 조회 쿼리를 계속 날려야 함.. too bad ~

 

 

 

@Transactional 을 메서드 레벨에 붙이지 않고 findById 만 사용할 경우

- 일일히 조회 코드를 작성해줘야 함 -> 코드가 더러워짐

- 역시 조회 쿼리를 계속  날려야하니 bad

 

 

 

 

하지만 fetch join 을 할 경우

 

Hibernate: 
    select
        cs1_0.chat_session_id,
        cs1_0.bot_id,
        cs1_0.completed_at,
        cs1_0.created_at,
        cs1_0.diary_id,
        d1_0.diary_id,
        d1_0.content,
        d1_0.created_at,
        d1_0.date,
        d1_0.deleted_at,
        d1_0.emotion,
        d1_0.encryption_context_id,
        d1_0.summary_short,
        d1_0.updated_at,
        d1_0.user_id,
        u1_0.user_id,
        u1_0.bot_id,
        u1_0.created_at,
        u1_0.deleted_at,
        u1_0.email,
        u1_0.encryption_context_id,
        u1_0.nickname,
        u1_0.password,
        u1_0.is_sns,
        u1_0.updated_at,
        cs1_0.encryption_context_id,
        cs1_0.send_count,
        cs1_0.status,
        cs1_0.summary 
    from
        mmebot.chat_session cs1_0 
    join
        mmebot.diary d1_0 
            on d1_0.diary_id=cs1_0.diary_id 
    join
        mmebot.users u1_0 
            on u1_0.user_id=d1_0.user_id 
    where
        cs1_0.chat_session_id=?

 

 

테이블을 한번에 조회하기 때문에 조회 쿼리는 단 한 번 !

이렇게 (fetch)join 을 이용할 경우 필요한 데이터를 한번에 처음부터 가져와, 위기를 모면할 수 있답니다

 

아 굿ㅋ

좋다 좋아

경사났네 경사났어

 

 

queryDSL 을 적용할까도 했지만

일단 간단하게 빨리빨리 개발하고 싶어서 JPQL 을 사용함..

좀 정리되고 나면 또는 더 더 복잡한 쿼리를 사용해야한다면 그 땐 QueryDSL 사용을 고려해보겠음...

 

 

 

 

 

 

 

 

 

 

사실 코드를 좀 더 수정해야해서...(더 완벽한 검증 코드를 추가하는 것이 필요)

일단 여기까지 쓰겠당 ㅜ

 

 

너무너무너무너무졸리고 피곤하여 글이 두서가 없을 텐데

사실 이 글 보고 JPA 에 대해 배우려는 분 없으시잖아요? 없죠..?(ㅎㅋㅎㅋ..)

그래서 그냥 나 혼자 여기까지 정리하고 마무리지음.

 

728x90
반응형