getReference() 관찰하기

SuYeong·2023년 3월 2일
1

JPA에서 Hibernate를 구현체로 사용했을 때, getReference()의 행동을 상황별로 알아보겠습니다.

특히, getReference()을 어떻게 사용하면 불필요한 SELECT문을 줄일 수 있는지 알아보겠습니다.

사용하는 엔티티 클래스

사용하는 클래스는 아래와 같습니다.

@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Game {
    @Id
    private Long id;

    private String name;
}
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Player {
    @Id
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    private Game game;

    public Player(Long id, String name) {
        this.id = id;
        this.name = name;
    }
}

GamePlayer가 다대일로 매핑되어 있습니다.

테스트 코드 뼈대

@SpringBootTest
@Transactional
public class PlayerTest {

    @PersistenceContext
    EntityManager entityManager;

    @BeforeEach
    void setUp() {
        entityManager.persist(new Game(1L, "Game 1"));
        entityManager.persist(new Game(2L, "Game 2"));
        entityManager.persist(new Player(1L,"Player 1"));
        entityManager.persist(new Player(2L, "Player 2"));
        entityManager.persist(new Player(3L, "Player 3"));
        entityManager.flush();
        entityManager.clear();
        System.out.println("===========데이터 셋업 종료 =============");
    }
}

@BeforeEach로 사용할 데이터를 미리 넣어두도록 했습니다.
@Transactional 이 테스트 코드에 붙어있으면, 기본 설정이 롤백입니다.
JPA는 DB에 변화가 없을 것으로 판단되면 쿼리문을 아예 보내지 않습니다.
다음의 테스트 코드를 실행하기 위해, flush()로 데이터를 DB에 넣어두고 clear()로 영속성 컨텍스트의 1차 캐시를 비워줍니다.
1차 캐시를 비워줘야 값을 1차 캐시에서 가져오지 않고, DB에서 가져오므로 정확한 쿼리 관찰이 가능합니다.

find()로 인스턴스 가져오기

@Test
void test1(){
    Game game1 = entityManager.find(Game.class, 1L);
    game1.setName("Game Updated 1");
    entityManager.persist(game1);
    entityManager.flush();
}
=======find실행=======
Hibernate: 
    select
        game0_.id as id1_2_0_,
        game0_.name as name2_2_0_ 
    from
        game game0_ 
    where
        game0_.id=?
2023-03-02 16:19:32.785 TRACE 13540 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
=======find실행=======
=======flush실행=======
Hibernate: 
    update
        game 
    set
        name=? 
    where
        id=?
2023-03-02 16:19:32.802 TRACE 13540 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [Game Updated 1]
2023-03-02 16:19:32.802 TRACE 13540 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
=======flush실행=======
  1. find()에서 SELECT문 실행 (1차 캐시에 저장)
  2. 변경 감지된 인스턴스에 대한 UPDATE문을 flush()에서 실행

getReference()로 인스턴스 가져오기

@DisplayName("getRef로 찾아오고 set으로 수정")
@Test
void test2(){
    System.out.println("=======getReference실행=======");
    Game game1 = entityManager.getReference(Game.class, 1L);
    System.out.println("=======getReference실행=======");
    
    System.out.println("=======setName실행=======");
    game1.setName("Game Updated 1");
    System.out.println("=======setName실행=======");
    
    System.out.println("=======flush실행=======");
    entityManager.flush();
    System.out.println("=======flush실행=======");
}
=======getReference실행=======
=======getReference실행=======
=======setName실행=======
Hibernate: 
    select
        game0_.id as id1_2_0_,
        game0_.name as name2_2_0_ 
    from
        game game0_ 
    where
        game0_.id=?
2023-03-02 16:22:31.011 TRACE 29388 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
=======setName실행=======
=======flush실행=======
Hibernate: 
    update
        game 
    set
        name=? 
    where
        id=?
2023-03-02 16:22:31.029 TRACE 29388 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [Game Updated 1]
2023-03-02 16:22:31.029 TRACE 29388 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
=======flush실행=======
  1. getReference()에서는 프록시 인스턴스만 가져옴. SELECT문 실행 안함
  2. setName()이 실행되며 SELECT문 실행
  3. 변경 감지된 인스턴스에 대한 UPDATE문을 flush()에서 실행

SetterSELECT 쿼리를 발생시킨 이유?

EntityManager는 엔티티 상태 정보를 가져오기 전에는 어떠한 값도 저장할 수 없습니다.
따라서 setter가 호출되었을 때, 상태 정보를 가져오기 위해 SELECT문이 나가는 것입니다.

지금까지의 경우,

find()getReference() 을 사용했을 때, 발생되는 쿼리 갯수에 차이가 없었습니다.

setter 대신 EntityManager.remove() 를 넣어도 같은 결과가 나옵니다.

이번부터는, 관계를 연결하고 저장하는 예제를 살펴보겠습니다.

find() -> find()

@Test
void test3(){
    System.out.println("=======find실행=======");
    Game game1 = entityManager.find(Game.class, 1L);
    System.out.println("=======find실행=======");
    
    System.out.println("=======find실행=======");
    Player player1 = entityManager.find(Player.class, 1L);
    System.out.println("=======find실행=======");
    
    player1.setGame(game1);
    
    System.out.println("=======flush실행=======");
    entityManager.flush();
    System.out.println("=======flush실행=======");
}
=======find실행=======
Hibernate: 
    select
        game0_.id as id1_2_0_,
        game0_.name as name2_2_0_ 
    from
        game game0_ 
    where
        game0_.id=?
2023-03-02 16:43:09.475 TRACE 35740 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
=======find실행=======
=======find실행=======
Hibernate: 
    select
        player0_.id as id1_3_0_,
        player0_.game_id as game_id3_3_0_,
        player0_.name as name2_3_0_ 
    from
        player player0_ 
    where
        player0_.id=?
2023-03-02 16:43:09.491 TRACE 35740 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
=======find실행=======
=======flush실행=======
Hibernate: 
    update
        player 
    set
        game_id=?,
        name=? 
    where
        id=?
2023-03-02 16:43:09.497 TRACE 35740 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2023-03-02 16:43:09.497 TRACE 35740 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [Player 1]
2023-03-02 16:43:09.497 TRACE 35740 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]
=======flush실행=======
  1. Game을 가져오는 find()에서 SELECT문 실행
  2. Player를 가져오는 find()에서 SELECT문 실행
  3. 변경 감지된 인스턴스에 대한 UPDATE문을 flush()에서 실행

getInstance() -> find()

@DisplayName("[relation]getRef로 game, find로 player 찾아오고 setGame 정상 결과 관찰")
@Test
void test4(){
    System.out.println("=======getReference실행=======");
    Game game2 = entityManager.getReference(Game.class, 2L);
    System.out.println("=======getReference실행=======");
    
    System.out.println("=======find실행=======");
    Player player1 = entityManager.find(Player.class, 1L);
    System.out.println("=======find실행=======");
    
    player1.setGame(game2);
    
    System.out.println("=======flush실행=======");
    entityManager.flush();
    System.out.println("=======flush실행=======");
}
=======getReference실행=======
=======getReference실행=======
=======find실행=======
Hibernate: 
    select
        player0_.id as id1_3_0_,
        player0_.game_id as game_id3_3_0_,
        player0_.name as name2_3_0_ 
    from
        player player0_ 
    where
        player0_.id=?
2023-03-02 16:47:07.192 TRACE 5772 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
=======find실행=======
=======flush실행=======
Hibernate: 
    update
        player 
    set
        game_id=?,
        name=? 
    where
        id=?
2023-03-02 16:47:07.213 TRACE 5772 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [2]
2023-03-02 16:47:07.213 TRACE 5772 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [Player 1]
2023-03-02 16:47:07.213 TRACE 5772 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]
=======flush실행=======

앞선 경우와 차이점이 발생했습니다.

  1. getReference()에서는 프록시 인스턴스만 가져옴. SELECT문 실행 안함
  2. Player를 가져오는 find()에서 SELECT문 실행
  3. 변경 감지된 인스턴스에 대한 UPDATE문을 flush()에서 실행

Game의 Id가 테이블에 존재하는 것이 확실하다면, 위 방법을 사용할 수 있고, 불필요한 SELECT문 한개를 줄일 수 있습니다.

관계를 맺는 데에 필요한 정보는 프록시 인스턴스만으로 충분하기 때문입니다. (프록시 인스턴스가 Id를 가지고 있음)

Id가 존재하지 않는다면, Player의 FK인 Game Id를 설정하면서 예외가 발생합니다.

다음 예제에서 확인해보겠습니다.

getInstance FK 설정 시 예외

@Test
void test5() {
    System.out.println("=======getReference실행=======");
    Game game2 = entityManager.getReference(Game.class, 3L);
    System.out.println("=======getReference실행=======");
    
    System.out.println("=======find실행=======");
    Player player1 = entityManager.find(Player.class, 1L);
    System.out.println("=======find실행=======");
    
    player1.setGame(game2);
    
    System.out.println("=======flush실행=======");
    entityManager.flush();
    System.out.println("=======flush실행=======");
}
=======getReference실행=======
=======getReference실행=======
=======find실행=======
Hibernate: 
    select
        player0_.id as id1_3_0_,
        player0_.game_id as game_id3_3_0_,
        player0_.name as name2_3_0_ 
    from
        player player0_ 
    where
        player0_.id=?
2023-03-02 17:39:30.976 TRACE 30608 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
=======find실행=======
=======flush실행=======
Hibernate: 
    update
        player 
    set
        game_id=?,
        name=? 
    where
        id=?
2023-03-02 17:39:30.994 TRACE 30608 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [3]
2023-03-02 17:39:30.995 TRACE 30608 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [Player 1]
2023-03-02 17:39:30.995 TRACE 30608 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]
2023-03-02 17:39:30.997  WARN 30608 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 23506, SQLState: 23506

예외 로그

javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: could not execute statement
...
Caused by: org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Referential integrity constraint violation: "FK8095BT0VV5CAPCCV9870LN2N: PUBLIC.PLAYER FOREIGN KEY(GAME_ID) REFERENCES PUBLIC.GAME(ID) (CAST(3 AS BIGINT))"; SQL statement:
update player set game_id=?, name=? where id=? [23506-214]
...
  1. getReference()에서는 프록시 인스턴스만 가져옴. SELECT문 실행 안함
  2. Player를 가져오는 find()에서 SELECT문 실행
  3. 변경 감지된 인스턴스에 대한 UPDATE문을 flush()에서 실행
  4. 존재하지 않는 FK 설정으로, 예외 발생

Game 테이블에 사용하려는 Id가 존재하는지 확인하는 과정이 없으므로, PlayerUPDATE할 때, 예외가 발생하는 것을 확인할 수 있습니다.

정리

  • getInstance()를 활용하면 불필요한 쿼리문 발생을 줄일 수 있다.

참고자료

profile
안녕하세요

0개의 댓글