댓글 목록 보기

1. 기존 코드 수정

BoardService 코드 수정 (게시글 상세 보기)
/**
 * 게시글 상세보기 서비스, 게시글 주인 여부 판별
 */
public Board getBoardDetails(Integer boardId, User sessionUser) {
    Board board = boardJPARepository
            .findById(boardId)
            .orElseThrow(() -> new Exception404("존재하지 않는 게시글입니다."));
    boolean boardOwner = false;
    if (board.getUser().getId().equals(sessionUser.getId())) {
        boardOwner = true;
    }

    // 내가 작성한 댓글인가를 구현 해야 한다.
    board.getReplies().forEach(reply -> {
        reply.setReplyOwner(sessionUser.getId().equals(reply.getUser().getId()));
    });
    board.setBoardOwner(boardOwner);
    return board;
}

 

2. Fetch 전략 비교

지연 로딩
Hibernate: 
    select
        b1_0.id,
        b1_0.content,
        b1_0.created_at,
        b1_0.title,
        b1_0.user_id 
    from
        board_tb b1_0 
    where
        b1_0.id=?
Hibernate: 
    select
        r1_0.board_id,
        r1_0.id,
        r1_0.comment,
        r1_0.created_at,
        r1_0.status,
        r1_0.user_id 
    from
        reply_tb r1_0 
    where
        r1_0.board_id=?
Hibernate: 
    select
        u1_0.id,
        u1_0.created_at,
        u1_0.email,
        u1_0.password,
        u1_0.role,
        u1_0.username 
    from
        user_tb u1_0 
    where
        u1_0.id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

 

즉시 로딩
Hibernate: 
    select
        b1_0.id,
        b1_0.content,
        b1_0.created_at,
        b1_0.title,
        b1_0.user_id,
        r1_0.board_id,
        r1_0.id,
        r1_0.comment,
        r1_0.created_at,
        r1_0.status,
        r1_0.user_id 
    from
        board_tb b1_0 
    left join
        reply_tb r1_0 
            on b1_0.id=r1_0.board_id 
    where
        b1_0.id=?
Hibernate: 
    select
        u1_0.id,
        u1_0.created_at,
        u1_0.email,
        u1_0.password,
        u1_0.role,
        u1_0.username 
    from
        user_tb u1_0 
    where
        u1_0.id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

 

  • 지연 로딩 - 쿼리 3번, 즉시 로딩 쿼리 2번 즉, 표면적으로는 EAGER 이 효율적으로 보일 수 있으나
    카테시안 곱(Cartesian Product)과 데이터 중복 등 잠재적 문제들이 발생 할 수 있다.

3. Fetch Strategy 어떻게 사용해야 할까?

  1. 모든 연관 관계(OneToOne, ManyToOne, OneToMany, ManyToMany)에서는 기본적으로 지연 로딩(LAZY)을 사용한다. (특히 컬렉션일 때)
    1. 한 엔티티가 다른 엔티티와 연결될 때, 필요한 시점까지 로딩을 지연하는 것이 성능 면에서 유리하다.
    2. 연관된 엔티티를 실제로 필요로 할 때만 로딩하여 자원 낭비를 줄일 수 있다.
  2. 필요한 경우에만 연관된 엔티티를 함께 로딩한다.
    1. JPQL의 FETCH JOIN이나 네이티브 쿼리를 사용하여 연관된 엔티티를 한 번에 로딩한다.
    2. 이를 통해 N+1 문제를 방지하고 성능을 최적화할 수 있다.
  3. 페이징 처리 등으로 많은 데이터를 가져와야 할 때는 지연 로딩(LAZY) 전략에 배치 사이즈(batch size)를 설정한다.
    1. 배치 사이즈를 설정하면 지연 로딩 시 한 번에 가져오는 엔티티의 수를 조절할 수 있다.
    2. N+1 문제를 완화하고, 데이터베이스 쿼리 횟수를 줄여 성능을 향상시킬 수 있다.
    3. application.yml 설정 확인
	spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 20

 

결론 : 기본적으로 LAZY 전략 설정을 하고 많은 양에 데이터는 페이칭 처리를 한다.

 

BoardService - 게시글 상세보기 수정

JPQL JOIN FETCH 사용 즉, Board 와 USER 엔티티를 한번에 조인 처리
Replay 은 LAZY 전략에 배치 사이즈 설정으로 가져오는 것
@Query("select b from Board b join fetch b.user u where b.id = :id")
Optional<Board> findByIdJoinUser(@Param("id") int id);
Board board = boardJPARepository
                .findByIdJoinUser(boardId)
                .orElseThrow(() -> new Exception404("존재하지 않는 게시글입니다."));
  • 그런데 한번에 Reply 정보도 조인해서 들고 오면 안될까?
String jpql = "SELECT b FROM Board b JOIN FETCH b.user LEFT JOIN FETCH b.replies r LEFT JOIN FETCH r.user WHERE b.id = :id";

 

데이터 중복 발생

  • 여러 개의 JOIN FETCH를 사용하면 결과 집합에 중복 데이터가 포함될 수 있다.
    • Board가 하나이고 Reply가 여러 개라면, Board와 User의 정보가 Reply의 개수만큼 중복된다.
    • 이는 애플리케이션 레벨에서 데이터 중복을 처리해야 하는 부담을 준다.

JPA의 제약사항

  • JPA에서는 한 쿼리에서 둘 이상의 컬렉션을 페치 조인하는 것을 권장하지 않는다.
    • JPA 표준 스펙에서는 컬렉션을 둘 이상 페치 조인하면 결과가 정의되지 않는다고 명시하고 있다.
    • 일부 JPA 구현체(Hibernate 등)에서는 동작할 수 있지만, 예상치 못한 동작이나 성능 문제가 발생할 수 있다.

팁!! 컬렉션은 지연 로딩(LAZY)으로 유지하고 필요한 경우에만 로딩한다.

목차로 돌아가기