2 분 소요

지난 편을 보고 오자 JPA 구현체 분석

매우 중요 !!!

  • save() 메서드
    • 새로운 엔티티면 저장 (persist)
    • 새로운 엔티티가 아니면 병합 (merge)
  • 새로운 엔티티를 판단하는 기본 전략
    • 식별자가 객체일 때 null로 판단
    • 식별자가 자바 기본 타입일 때 0으로 판단
    • Persistable인터페이스를 구현해서 판단 로직 변경 가능
1
2
3
4
5
6
@Test  
@DisplayName("")  
void save() throws Exception {  
    Item item = new Item();  
    itemRepository.save(item);  
}

다음과 같이 간단하게 새로 저장하는 코드를 디버그 해봤다.

신규라서 null로 들어온다. 그 후 persist 후에

값이 들어온다.

그런데 이런 상황이 있다고 가정해 보자. Item 엔티티의 아이디를 @GeneratedValue를 사용하지 않고 특정 값을 사용한다 치고 이렇게 만든다.

1
2
3
4
5
6
7
8
9
10
11
@Entity  
@Getter  
@NoArgsConstructor(access = AccessLevel.PROTECTED)  
public class Item {  
    @Id  
    private String id;   
    
    public Item(String id) {  
        this.id = id;  
    }  
}

이런 식으로 만들었다. 그 다음에 테스트를 해보자

1
2
3
4
5
6
@Test  
@DisplayName("이건 persist 일까 머지일까?")  
void save() throws Exception {  
    Item item = new Item("A");  
    itemRepository.save(item);  
}

이렇게 저장하면 save() 에서 if문에 merge를 할까 persist()를 할까?

정답은 merge()를 해버린다.

왜냐면, 일단 엔티티에서 id를 생성자로 세팅을 해버렸고 이렇게 되면서 save 호출 시점에 null이 아니라 다른 값이 들어가게 된다.

다음과 같이 이미 들어오기 전부터 null이 아니라, A 값을 가지고 있어서 merge를 해버린다. 그럼 좀 더 복잡해지는게, merge는 DB에서 조회 한 후에 있는지 없는지를 조회 하고 (DB에 있다고 가정하고) 없으면 그 다음에 insert 하기 때문에 쿼리가 또 2번 나가게 된다.

2번 나간 걸 볼 수 있다.

JPA를 사용할 땐 변경 = 변경 감지 저장 = persist 를 써야 한다.

그럼 @GeneratedValue를 사용하지 않을 경우에는 어떻게 해주면 좋을까?

Persistable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity  
@Getter  
@NoArgsConstructor(access = AccessLevel.PROTECTED)  
public class Item implements Persistable<String> {  
  
    @Id  
    private String id;  
  
    public Item(String id) {  
        this.id = id;  
    }  
  
    @Override  
    public boolean isNew() {  
        return false;  
    }  
}

다음과 같이 엔티티에 Persistable 인터페이스를 implements 해주고 isNew() 메서드를 오버라이딩 해주면 된다.

이때 사용해주면 좋은게 @CreateDate이다. 생성일은 처음 생성될 때는 값이 없으니깐 이걸로 판단하면 좋을 것 같다.

그래서 최종적인 Entity클래스는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Entity  
@Getter  
@EntityListeners(AuditingEntityListener.class)  
@NoArgsConstructor(access = AccessLevel.PROTECTED)  
public class Item implements Persistable<String> {  
  
    @Id  
    private String id;  
  
    @CreatedDate  
    private LocalDateTime createDate;  
  
    public Item(String id) {  
        this.id = id;  
    }  
  
    @Override  
    public boolean isNew() {  
        return createDate==null;  
    }  
}

createDateNull이면 true를 준다.

그럼다시 테스트를 돌려보자.

isNew가 createDate를 검증하면서 true로 persist()를 실행하게 되었다.

그 후 리턴할 때는 값이 생겼다.

쿼리도 한번만 나가는 것을 볼 수 있다.

참고 : JPA 식별자 생성 전략이 @GenerateValuesave() 호출 시점에 식별자가 없으므로 새로운 엔티티로 인식해서 정상 작동한다. 하지만 JPA 식별자 생성 전략이 @Id만 사용해서 직접 할당 이면 이미 식별자 값이 있는 상태로 save()를 호출한다. 따라서 이 경우 merge()가 호출 된다.merge()는 우선 DB를 호출해서 값을 확인하고, 값이 없다면 새로운 엔티티로 인식하기 때문에 매우 비효율 적이다. 따라서 Persistable를 사용해서 새로운 엔티티 확인 여부를 직접 구현하는게 효과적이다. 참고로 등록시간 (@CreateDate)을 조합해서 사용하면 이 필드는 새로운 엔티티 여부를 편리하게 확인할 수 있다. (@CreateDate에 값이 없으면 새로운 엔티티로 판단.)

태그: ,

카테고리:

업데이트:

댓글남기기