Java/JPA

[JPA] Spring Jpa Repository의 영속성 컨텍스트 동작방식

yaini 2022. 4. 17. 01:59
반응형

개발을 하다보면 Root 도메인에 종속적인 Sub 도메인을 어떻게 처리할지 고민이 될 때가 있다.

예를 들면, 재생 목록의 음악들은 재생 목록이라는 Root 도메인에 종속적인 Sub 도메인 들이다.

Delete And Insert 방식을 실행할 수도 있고, Update 방식을 사용할 수도 있다.

그러다보니 JPA를 사용했을때, 각 개발 방식이 어떻게 동작하는지 알고 싶어서 직접 엔티티를 생성하며 테스트 코드를 작성해 보았다.

코드는 모두 깃허브에 작성해두었다.

 

 

    private SimpleEntity create(String name) {
        SimpleEntity entity = SimpleEntity.builder()
                .name(name)
                .build();

        return repository.save(entity);
    }

우선 각 함수에 공통적으로 실행될 엔티티를 저장하는 로직은 함수로 작성했다.

BeforeEach를 사용할 수도 있지만 name을 명시하며 작성하고 싶어서 함수를 사용했다.

 

 

SAVE

    @Test
    @Transactional
    public void 영속성_컨텍스트의_엔티티를_조회하면_SELECT_쿼리가_실행되지_않는다() {

        SimpleEntity saved = create("simpleName");

        repository.findById(saved.getId());

    }

엔티티를 생성하고 해당 엔티티를 조회하는 함수를 실행해보자

 

Hibernate: 
    insert 
    into
        simple
        (name) 
    values
        (?)

작성된 로직대로 라면 insert 후 select 쿼리가 실행되어야 할 것 같지만 insert 쿼리만 실행된다.

이유는 insert 쿼리를 실행하면서 해당 엔티티를 영속성 컨텍스트에 저장했기 때문에 db를 조회하지 않고 해당 엔티티를 가져온다.

 

 

UPDATE

    @Test
    @Transactional
    public void 엔티티의_값을_변경하면_UPDATE_쿼리가_실행된다() {

        SimpleEntity saved = create("simpleName");

        SimpleEntity entity = SimpleEntity.builder()
                .id(saved.getId())
                .name("change name")
                .build();

        repository.save(entity);

    }

엔티티를 생성하고 수정하는 테스트를 실행해보자. jpa repository 는 update 함수가 아닌 id를 지정하여 save 함수를 통해 수정해준다.

 

Hibernate: 
    insert 
    into
        simple
        (name) 
    values
        (?)
Hibernate: 
    update
        simple 
    set
        name=? 
    where
        id=?

예상대로 update 쿼리가 실행된다. 수정된 엔티티는 위 테스트와 같이 영속성 컨텍스트에 저장되었기 때문에 추가적인 select 쿼리를 발생시키지 않는다. 

 

    @Test
    @Transactional
    public void 엔티티의_값을_변경하지_않으면_UPDATE_쿼리가_실행되지_않는다() {

        SimpleEntity saved = create("simpleName");

        SimpleEntity entity = SimpleEntity.builder()
                .id(saved.getId())
                .name(saved.getName())
                .build();

        repository.save(entity);

    }

이번엔 생성한 엔티티와 같은 값을 가지도록 한 다음 save를 실행해보자

 

Hibernate: 
    insert 
    into
        simple
        (name) 
    values
        (?)

영속성 컨텍스트에선 저장된 엔티티와 수정된 엔티티의 값을 비교하여 수정되었을 때만 udpate 쿼리를 실행하는 것을 알 수 있다.

 

    @Test
    @Transactional
    public void saveAll을_실행해도_bulk_INSERT와_UPDATE가_실행되지_않는다() {

        List<SimpleEntity> list = new ArrayList<>();
        list.add(SimpleEntity.builder().name("simpleName").build());
        list.add(SimpleEntity.builder().name("simpleName").build());
        repository.saveAll(list);

        list.forEach(
                entity -> entity.setName("changeName")
        );

        repository.saveAll(list);
    }

saveAll을 사용하면 insert 혹은 update가 한 쿼리로 실행되지 않을까 해서 테스트 해보았다.

 

Hibernate: 
    insert 
    into
        simple
        (name) 
    values
        (?)
Hibernate: 
    insert 
    into
        simple
        (name) 
    values
        (?)
Hibernate: 
    update
        simple 
    set
        name=? 
    where
        id=?
Hibernate: 
    update
        simple 
    set
        name=? 
    where
        id=?

jpa data repository만으로는 bulk update와 bulk insert를 수행할 수 없다. querydsl 혹은 native query를 사용하여 작성해주어야 한다.

 

DELETE

    @Test
    @Transactional
    public void DELETE_BY_ID를_실행하면_각_ID마다_SELECT_쿼리가_실행된다() {

        List<SimpleEntity> list = new ArrayList<>();
        for (int i = 0; i < 2; i++) {
            list.add(create("deleteTest"));
            em.detach(list.get(i));
        }

        for (int i = 1; i < 2; i++) {
            repository.deleteById(list.get(i).getId());
        }

    }

생성과 삭제는 실제로 다른 기능으로 사용될 경우가 많으므로 ( e.g. 재생목록의 음악은 추가된 후 시간이 지나고 삭제됨), 데이터 베이스에 저장되어 있지만 영속성 컨텍스트에 없는 상태를 표현하기 위해 detach를 사용했다.

 

Hibernate: 
    insert 
    into
        simple
        (name) 
    values
        (?)
Hibernate: 
    insert 
    into
        simple
        (name) 
    values
        (?)
Hibernate: 
    select
        simpleenti0_.id as id1_0_0_,
        simpleenti0_.name as name2_0_0_ 
    from
        simple simpleenti0_ 
    where
        simpleenti0_.id=?
Hibernate: 
    select
        simpleenti0_.id as id1_0_0_,
        simpleenti0_.name as name2_0_0_ 
    from
        simple simpleenti0_ 
    where
        simpleenti0_.id=?
Hibernate: 
    delete 
    from
        simple 
    where
        id=?
Hibernate: 
    delete 
    from
        simple 
    where
        id=?

delete 함수를 실행했을때, 바로 쿼리를 실행하는 것이 아닌 select를 통해 해당 값이 존재하는지 먼저 확인하는 것을 알 수 있다. 또한 delete 하나 당 select 쿼리가 한 개씩 실행된다.

 

 

    @Test
    @Transactional
    public void DELETE_ALL_BY를_실행하면_하나의_SELECT_쿼리를_통해_엔티티를_조회한다() {

        List<SimpleEntity> list = new ArrayList<>();
        for (int i = 0; i < 2; i++) {
            list.add(create("deleteTest"));
            em.detach(list.get(i));
        }

        repository.deleteAllByName("deleteTest");
        
    }

그러면 deleteAll은 어떨까?

 

Hibernate: 
    insert 
    into
        simple
        (name) 
    values
        (?)
Hibernate: 
    insert 
    into
        simple
        (name) 
    values
        (?)
Hibernate: 
    select
        simpleenti0_.id as id1_0_,
        simpleenti0_.name as name2_0_ 
    from
        simple simpleenti0_ 
    where
        simpleenti0_.name=?
Hibernate: 
    delete 
    from
        simple 
    where
        id=?
Hibernate: 
    delete 
    from
        simple 
    where
        id=?

select 쿼리는 하나만 발생되고 delete 쿼리는 각각 실행된다.

 

알게된 것

  • 영속성 컨텍스트에 저장된 엔티티는 추가적으로 select 쿼리를 발생시키지 않는다.
  • update(save)와 delete만 사용해도 select 쿼리를 사용한 조회 쿼리가 추가로 실행된다.
    • 영속성 컨텍스트에 저장되므로 조회 쿼리를 먼저 사용하여도 실행되는 쿼리 수는 같다.
  • delete를 개별적으로 수행하는 것보다 deleteAll을 수행하면 하나의 select 쿼리가 실행된다.
  • bulk 쿼리는 spring data jpa 만으로는 수행할 수 없다.
반응형