엔티티 아이디 자동생성 - 2

Kim Dong Kyun·2023년 10월 23일
1

이전글

상황

Spring Data JPA 사용 중이다.

엔티티에 상응하는 테이블이 있다. 이 테이블의 PK 생성 전략은 동일 DB 안에 존재하는 Sequence로 자동 생성한다.

DB에서 해당 시퀀스 값을 가져와서, +1 하는 것이 자동 생성 전략이다.

문제

다음 코드는 무사히 save 가능한가?

@Transactional
public Long saveEntity(){
    Entity foo = new Entity();
    fooRepo.save(foo)

    if (foo.getId() == null){
        throw new Exepction();
    }

    return foo.getId();
}

JPA Repository의 save(Entity) 매서드는 entity의 필드를 자바의 리플렉션을 사용해서 set한다. id 자동 생성 전략이 Sequence 인 경우 DB의 시퀀스로의 셀렉트 쿼리가 나가고,(해당 시퀀스 값에 대해, allocation_size마다) IDENTITY의 경우 다르게 작동한다.


흔한 오해

일반적으로 Entity의 id는 save() 매소드의 호출 시점이 아닌, 매서드의 종료시점 DB에 의존하여 늘어난다고 알려져있다.

위에서 말했듯, JpaRepository.save(Entity); 매서드를 호출하게 되면 "리플렉션" 으로 해당 엔티티의 아이디 필드를 채워준다.

JpaRepository 는 CRUDRepository 의 save() 매서드를 호출하며, 해당 인터페이스의 구현체는 SimpleJpaRepository 로 설정되어 있다.

이 구현체에서는 엔티티가 "새로운 엔티티"인가를 판단해서 persist 할지 merge 할지를 정하는데, (해당 부분더 자세한 내용은 여기) 매소드의 모습은 아래와 같다.

이 때, persiste() 매서드의 호출 시 상황을 주목해보자.

사진으로 볼 수 있듯, Argument 로 들어온 User@11484 객체는 리턴 할 때도 같은 객체이지만 처음 매소드 호출 시 인자로 들어 올 때는 존재하지 않았던(null이던) id가 리턴시에 채워지는 것을 볼 수 있다.

즉, save() 매서드를 통해 호출되는 persist 객체는 자바의 리플렉션을 사용해서 해당 엔티티의 필드를 set 해주며, set 해서 리턴된 객체는 원래 엔티티와 완전히 같은 객체라는 것이다.


결론

일반적으로 Entity의 id는 save() 매소드의 호출 시점이 아닌, 매서드의 종료 시점 DB에 의존하여 늘어난다고 알려져있지만, 사실은 그렇지 않다.

@ID 를 자동으로 생성하는 경우 Sequence, Identity(AUTO_INCREMENT) 와 관계 없이 세이브 매서드 호출 시점에 아이디가 완성된다.

이 말은 즉, persiste() 시점에 이미 DB에 insert 쿼리가 날아간다는 얘기가 된다. (Identity전략은 insert 시점 이외에는 아이디를 알 수 없으므로)

Identity 전략은 정말로 쿼리를 바로 쏘는가?

간단한 실험을 해보자.

    @Override
    @Transactional
    public MovieResponseDto createMovie(MovieRequestRecord movieRequestDto) {
        Movie movie = Movie.builder()
                .movieName(movieRequestDto.movieName())
                .genre(movieRequestDto.genre())
                .director(movieRequestDto.director())
                .posterImageUrl(movieRequestDto.postImageUrl())
                .releaseDate(movieRequestDto.releaseDate())
                .synopsis(movieRequestDto.synopsis())
                .runningTime(movieRequestDto.runningTime())
                .originalTitle(movieRequestDto.originalTitle())
                .build();

        movieRepository.save(movie);
        

        movie.setDirector("foo");

        throw new IllegalArgumentException();
    }

1. save() 매서드가 바로 쿼리를 날리지 않는다면?

아무 쿼리도 날아가지 않은 채 끝날것이다.

  1. save() 매서드 호출과 함께 엔티티는 id가 없는 상태로 존재 (id를 알 수 없으므로)

  2. set 매서드 또한 불리지 않는다 (영속 상태가 아니므로)

  3. 영속성 컨텍스트 플러쉬 타이밍에 persist. (이 때, set한 필드 기준으로 세이브될것)

그러나 실제 날아가는 쿼리를 본다면?

insert 쿼리가 날아간다.

결론은 아래와 같다.

  1. IDENTITY 방식은 바로 인서트 쿼리를 생성하며,
  2. 엔티티의 더티 체킹은 플러쉬 타이밍에 행해지므로 해당 영속성 컨텍스트가 예외(IllegalArgumentException)로 인해 트랜잭션 커밋이 되지 않음을 확인 한 순간 UPDATE 쿼리도 발생하지 않는다.

2. save 를 정상적으로 마무리하는 상항이라면?

우리가 예상한대로 save() 호출 시 바로 insert 쿼리가 발생한다면, (IDENTITY id값을 알기 위해) movie.set() 매서드는 하나의 추가적인 쿼리를 날릴 것이다.

   @Override
    @Transactional
    public MovieResponseDto createMovie(MovieRequestRecord movieRequestDto) {
        Movie movie = movieRequestDto.toEntity();

        movieRepository.save(movie);


        movie.setDirector("foo");

//        throw new IllegalArgumentException();

        return MovieResponseDto.of(movie);
    }

위 코드의 실행 결과는 아래와 같다.

  1. save() 매서드 호출로 insert문 호출
  2. insert 문에 대응하는 엔티티는 플러쉬 타이밍에 더티체킹으로 필드의 변경 쿼리 발생(UPDATE)

IDENTITY 방식은 save() 매서드 호출 시 쿼리가 발생함을 다시 한 번 확인 가능하다.


더불어서 table 전략은 하이버네이트에서 새로 Sequence 를 만들어주는 역할을 하는데, H2(mode mySql) 의 경우 전략을 AUTO 로 할 경우 table 전략을 사용한다.

해당 작업을 hibernate 에서 처리하며, create sequence~ 를 통해서 sequence와 유사하게 동작하도록 만들었다는 점을 알 수 있다.

왜 TABLE 전략을 사용하게 되나요? Identity 전략이 더 낫지 않나요?

위는 AUTO 전략이 선택되는 알고리즘 도식표이다.

결과적으로 hibernate.id.new_generator-mappings 라는 설정이 application.properties(or yaml) 에 어떻게 설정되어 있냐에 따라서 native generator(하이버네이트가 채택한 DB의 생성전략) 을 사용할지, 아닐지가 결정된다. (이 설정은 디폴트로 true 이다)

이에 따라 Sequence를 지원하면 시퀀스 전략, 시퀀스를 지원하지 않는 경우 TABLE 전략으로 자동 설정 되는데, MySQL의 경우는 시퀀스 오브젝트를 지원하지 않으므로 자동으로 고정된다

(다만 위에서는 sequence 생성이 되는 모습인데, H2 DB가 기본적으로 시퀀스를 지원해서 그런것이 아닌가 싶다.)

++ 추가, 위 시퀀스 전략 선택 이유는 Hibernate 가 자동으로 선택하는 DB Dialect(방언) 때문이다. 아래는 방언 설정 부분의 로그.

더불어서 위와 같이 SpringBoot 3.x 버전 이상에서는 Deprecated 되어 기본 설정인 true로 고정되었다.

참고 블로그


Todo (더 알아야 할 것들)

  • IDENTITY 전략은 어째서 쿼리가 발생하지 않는지?
  • Persist 와 Merge 의 내부 동작은 어떻게 진행되는지?
  • TABLE 전략은 어떤 방식으로 진행되는지?

이외에 새로운 키워드가 있다면 추천 부탁드려요!


테스트 환경

  • H2 DB (MySQL mode)
  • SpringBoot 3.1.3 / SpringBoot 2.7.7 (save() 매서드 호출 후 아이디 필드가 채워지는 것이 부트 3.x 버전의 최적화인지 알아보기 위해 두 버전에서 진행)
  • JDK 17

1개의 댓글

comment-user-thumbnail
2023년 10월 23일

멋져용~!

답글 달기