새로운 엔티티 구별 방법

CJY·2023년 6월 21일
0

JPA

목록 보기
1/2
post-thumbnail

스프링 데이터 JPA에서 엔티티를 저장하는 방법을 잘 보자.

JpaRepository 인터페이스 구현체 중에 SimpleJpaRepository 클래스를 찾아가보자.

구현 메소드 중에 save를 보자.

(인텔리제이 단축키 Ctrl+F12)


내용을 보면 엔티티의 정보를 분석하여 새로운 엔티티라고 판단하면 엔티티 메니저(em)로 persist()를 실행하고 새로운 엔티티가 아니라면 merge()를 실행한다.

새로운 엔티티 구별 방법

그럼 어떻게 새로운 엔티티를 구별할까 ?
방법은 우리가 엔티티를 정의할 때 식별자로 정의한 클래스 필드값을 보고 판단한다.

식별자가 클래스

예를 들어,

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

    @Id @GeneratedValue
    private Long id;

}

다음과 같이 Item 엔티티를 정의하면, 필드 id에 @Id 어노테이션을 붙여줬기 때문에 JPA에서는 id를 식별자로 본다.

만약 이 id값이 null이라면 새로운 엔티티로 판단한다.

JPA에 대해 잘 알고 있다면 우리가 새로운 엔티티 인스턴스를 생성할 때 id값을 설정하지 않고 엔티티 메니저에게 넘겨주기 때문에 이런 로직이 나온 것을 알 수 있다.

엔티티 메니저가 persist()함수를 통해 id sequence를 얻어 flush하기 전에 값을 넣어준다. 물론 식별자 전략에 따라 그 과정이나 방법이 다를 수 있다.

식별자가 primitive type

다 같은데 식별자의 타입이 자바의 primitive type인 경우는?

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

    @Id @GeneratedValue
    private long id;

}

식별자의 타입이 Long이 아니라 long이다. 즉 자바의 primitive type인데 이 값은 null이 될 수 없다.

따라서 이 경우에는 id가 0이면 새로운 엔티티라고 판단한다.

@GeneratedValue를 생략하면?

비즈니스 정책에 따라 혹은 필요에 따라 우리가 직접 식별자의 값을 지정하는 상황도 있을 것이다.

예시 코드를 보면

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

    @Id
    private String id;

}

다음과 같은 경우에는 우리가 엔티티 인스턴스를 생성할 때 id값을 직접 주입할 수밖에 없을 것이다.

그렇다면 JPA에서는 무조건 새로운 엔티티가 아니라고 판단하게 될 것이다.

문제점

여기서 문제가 생기는데,
새로운 엔티티를 저장할 때마다 우리가 원하지 않는 셀렉트 쿼리가 한번 더 나가게 된다.

이게 무슨말이냐?

새로운 엔티티가 아니라면 merge()를 호출하는데, 이 merge()라는 것은 DB에 엔티티가 이미 존재하므로 그 값을 가져와서 지금 저장하려는 엔티티로 바꿔치기 하는 것이다.

즉, 나는 새로운 엔티티를 저장하려고 하는데 DB에서 있지도 않은 id값으로 조회하는 쿼리가 한번 실행되고 나서 insert 쿼리가 나간다는 뜻이다.

그럼 이걸 어떻게 해결해야 할까?

해결책

우리가 엔티티 구현에 자주 사용하는 CreatedDate를 이용하여
Persistable<T> 인터페이스를 구현해주면 해결할 수 있다.

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item extends BaseEntity implements Persistable<String> {

    @Id
    private String id;

    public Item(String id) {
        this.id = id;
    }

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

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

쉽게 설명하기 위해
부모 클래스 BaseEntity를 풀어서 한 클래스 안에 사용하면 아래와 같은 코드가 된다.

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListner.class)
public class Item implements Persistable<String> {

    @Id
    private String id;
    
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime CreatedDateTime;
    

    public Item(String id) {
        this.id = id;
    }
    
    public LocalDateTime getCreatedDateTime() {
    	return createdDateTime;
    }

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

    @Override
    public boolean isNew() {
        return getCreateDateTime() == null;
    }
}

@Override로 인터페이스 구현 메소드를 정의하면 되는데, getId()는 식별자를 그대로 넘겨주면 된다.

isNew()는 새로운 엔티티를 구별하는 방법을 우리가 새로 정의하는 메소드임.

생성날짜는 우리가 새로운 엔티티 인스턴스를 생성할 때 null임을 이용하여 새로운 엔티티를 구별할 수 있게 해준다.

정리

새로운 엔티티 여부에 따라 Spring Data JPA에서 저장하는 방법이 다르다.
persist(), merge()로 나뉘게 되는데 merge()가 호출되면 성능상 비효율적이다.
만약 @GeneratedValue를 사용하지 않고 식별자의 값을 우리가 정하게 된다면 추가적으로 엔티티를 손 봐줘야한다.
대표적인 방법으로 Auditing을 이용하여 Persistable 인터페이스 구현하는 방법이 있다.

profile
열심히 성장 중인 백엔드

0개의 댓글