안녕하세요.

회사와 함께 성장하고 싶은 KOSE입니다.

 

이번 포스팅은 SpringBoot에서 Entity를 생성할 때, 자주 사용하는 @GeneratedValue에 대해 분석하는 글을 작성하고자 합니다.

 

1. @GeneratedValue 일반적 사용

@GeneratedValue는 필드의 생성 전략에 활용되는 기술로 데이터 베이스의 Sequence Object를 사용하여 데이터베이스가 자동으로 기본키를 생성하도록 합니다.

해당 어노테이션을 받는 필드가 객체라면 null을, primitive 타입이라면 0으로 판단하여 적용됩니다. (default)

 

Book 객체를 생성할 때, id 없이 객체를 생성하면 GeneratedValue로 인해 객체에 id가 생성되어 주입이 되는 것을 확인할 수 있습니다.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Book {

    @Id @GeneratedValue
    private Long bookId;

    private String bookName;

    public Book(String bookName) {
        this.bookName = bookName;
    }
}
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
}
@DataJpaTest
class BookRepositoryTest {

    @Autowired
    private BookRepository bookRepository;

    @Autowired
    private EntityManager em;

    @Test
    @DisplayName("책 저장")
    public void save() throws Exception {
        //given
        Book book = new Book("JPA 원리");

        //when
        Book saveBook = bookRepository.save(book);
        em.flush();

        //then
        Assertions.assertThat(saveBook.getBookId()).isNotNull();

    }
}

 

 

 

2. 원리 파악하기

JpaRepository의 구현체에 해당하는 SimpleJpaRepository는 repository.save가 호출되면 다음의 절차가 실행됩니다.

 

 

이를 디버깅 모드로 확인하면, 다음의 절차에 따라 id가 생성됩니다.

 

- entityInformation.isNew(entity)

만약 해당 객체가 새로 생성된 객체라고 판단을 하면, em.persist(entity)가 실행됩니다.

해당 객체를 새로 생성된 객체라고 판단할 수 있는 절차는 isNew메소드에 설정한 필드에 따라 바뀌는데, @GeneratedValue를 설정한 필드가 기본값이 되어 판단되는 원리입니다.

 

- em.persist()

@GeneratedValue는 JPA 이벤트가 실행이 되면 persist를 거치면서 영속성과 더불어 객체의 id를 생성하고 주입받습니다.

따라서, 최종적으로 entity가 반환이 되면 id가 생성되면서 반환되는 것을 확인할 수 있습니다.

 

3. 만약 @GeneratedValue를 안 쓴다면 어떻게 될까?

@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Music {

    @Id
    private String musicSeq;

    private String musicName;

}
@Repository
public interface MusicRepository extends JpaRepository<Music, String> {
}
@DataJpaTest
class MusicRepositoryTest {

    @Autowired
    MusicRepository musicRepository;

    @Autowired
    EntityManager em;

    @Test
    @DisplayName("GeneratedValue를 안쓰고 repository에 저장하기")
    public void save() throws Exception {
        //given
        Music hypeBoy = new Music("music-3001", "Hype boy");

        //when
        Music saveHypeBoy = musicRepository.save(hypeBoy);
        em.flush();

        //then
        Assertions.assertThat(saveHypeBoy.getMusicSeq()).isEqualTo("music-3001");
    }

}

만약 Music 객체를 생성할 때, Id를 직접 입력하여 저장한다면, 다음과 같이 조회(select) 후 저장(insert)이 실행되는 것을 확인할 수 있습니다.

 

 

 

4. Select 후 Insert 되는 이유는 무엇일까?

 

디버깅 모드를 통해, MusicRepository의 save() 메서드가 실행되는 과정을 살펴보면 다음과 같습니다.

Entity 내부에 musicSeq가 "music-3001"로 값이 있으므로

em.persist를 거치지 않고 바로 merge하는 과정을 수행하게 됩니다.

따라서, merge할 대상을 조회해야 하므로 쿼리가 select문과 insert문이 동시에 나가게 되는 것입니다.

 

5. 해결법!

 

만약 @GeneratedValue를 쓰지 않고 @Id를 적절한 상황에 맞게 설정하려면 어떻게 해야 할까요? 이때는 isNew()메소드에 적용되는 필드를 오버라이딩하여 해결할 수 있습니다.

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseTimeEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;

}

 

@Entity
@EntityListeners(AuditingEntityListener.class)
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Computer extends BaseTimeEntity implements Persistable<String> {

    @Id
    private String computerSeq;

    private String computerName;

    @Override
    public String getId() {
        return computerSeq;
    }

    @Override
    public boolean isNew() {
        return super.getCreatedDate() == null;
    }
}

여기서 중요한 점이, Persistavle 인터페이스를 구현하는 과정에서 isNew() 메서드를 오버라이딩 하는 과정입니다.

BaseTimeEntity는 객체를 데이터베이스에 저장할 때, 생성 시간 및 변경 시간을 저장하는데 사용하는 엔티티입니다. @CreadDate의 어노테이션은 Jpa 이벤트가 실행되고 나서 persist가 진행될 때, 해당 값이 입력이 됩니다.

 

이를 활용하면, SimpleJpaRepository 구현체에서 isNew() 메소드가 null 인지 판단할 때는 해당 값이 null 이므로 persist의 영향을 받아 값이 입력되면서 자연스럽게 insert 단일 쿼리만 실행이 됩니다. 또한, @CreatedValue는 그 과정에서 값이 입력되므로  추후 해당 값이 null인지 판단하는 과정에서는 false가 됩니다.

 

@Repository
public interface ComputerRepository extends JpaRepository<Computer, String> {
}

따라서, 해당 쿼리를 확인하면 insert 단일 쿼리만 생성된 것을 확인할 수 있습니다.

 

정리하면, @GeneratedValue는 데이터베이스에 저장되는 PK값 자동 생성 전략에 사용할 수 있으며, 만약 쓰지 않을 경우는 Persistable 인터페이스를 구현하여 isNew 메서드를 오버라이딩 하는 과정이 필요하다고 할 수 있습니다.

 

 

소스는 제가 만들었지만 해당 내용에 대한 좋은 지식과 베이스 소스는 영한님께서 공유해주셨습니다.

JPA에 대해 더 깊게 공부하실 수 있는 링크 공유해 드리겠습니다.

감사합니다.!

 

참고자료: 영한님 실전! 스프링 데이터 JPA https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84

+ Recent posts