😎 서론
사내 모든 프로젝트에서는 데이터베이스에 값을 저장할 때
아이디를 유니크한 값으로 자동생성해주는 로직을 만들어 별도로 태운 후 save()를 하게 된다.
결국 이로 인해 처음 INSERT를 하기 위해서 save를 보낸 모든 데이터 또한 SELECT를 꼭 호출하고,
INSERT를 호출하게 되는 문제가 있다.
이는 데이터가 많으면 많을 수록 성능저하에 큰 문제가 있기에
이 로직을 파악하고 성능개선하는 방법을 공유해본다.
😉 본론
⏳ save() 로직 파악하기.
SimpleJpaRepository 클래스에 있는 save()메소드는 아래와 같이 동작을 한다.
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
entityInformation.isNew(entity) 를 통해
TRUE = persist() 호출,
FALSE = merge() 호출을 한다.
여기서 위 3가지를 알고 가야한다.
isNew(), persist(), merge()
isNew()
getId()를 통해 해당 객체 id값이 있는지 체크하는 로직인 걸 알 수 있다.
persist()
= 해당 객체를 영속성으로 만들어준다.
merge()
= 해당 객체를 조회한 후 있으면 update, 없다면 insert를 통해 새로운 영속성 객체를 반환해준다.
그러면..
merge()가 호출된다면 조회 후 update() or insert()가 발생하고,
persist()가 호출되면 insert()만 호출 할 수 있다는 뜻!
⏳ isNew()를 오버라이딩을 해보자.
public interface Persistable<ID> {
/**
* Returns the id of the entity.
*
* @return the id. Can be {@literal null}.
*/
@Nullable
ID getId();
/**
* Returns if the {@code Persistable} is new or was persisted already.
*
* @return if {@literal true} the object is new.
*/
boolean isNew();
}
해당 Persistable 인터페이스를 implements 시켜보자.
@MappedSuperclass
@Getter @Setter @SuperBuilder
@NoArgsConstructor @AllArgsConstructor
public abstract class DefaultDTO implements Persistable<String> {
@Column(updatable = false)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
protected LocalDateTime createdDateTime;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
protected LocalDateTime updatedDateTime;
@Transient
protected boolean isCreatedMode;
@Override
public boolean isNew() {
return isCreatedMode;
}
@PrePersist
private void prePersist() {
createdDateTime = LocalDateTime.now();
}
@PreUpdate
private void preUpdate() {
updatedDateTime = LocalDateTime.now();
}
}
위 처럼 공통으로 사용될 DefaultDTO를 추상화 클래스로 만들어준 후
해당 DTO를 상속을 받는 방식으로 하게 된다면
상속받은 모든 Entity에서는 isCreatedMode를 통해서 isNew 값을 바꿀 수 있게 된다.
@Getter @Setter @SuperBuilder @Entity
@Table(name = "item")
@NoArgsConstructor @AllArgsConstructor
public class ItemEntity extends DefaultDTO {
@Id
@Column(name = "item_id")
protected String itemId;
@Column(name = "item_name")
protected String itemName;
@Column(name = "color")
protected String color;
@Override
public String getId() {
return itemId;
}
}
이렇게 DefaultDTO를 상속받은 후 getId()를 필수로 오버라이딩을 하게 되면 끝이 난다.
🎊 테스트 해보기
@SpringBootTest
@ExtendWith(SpringExtension.class)
@Transactional
@Rollback(false)
class ItemServiceTest {
@Autowired
private ItemService itemService;
@Test
void itemSaveTestCreatedMode() {
var item = itemService.save(ItemEntity.builder().itemId("createdModeTEST").itemName("createdModeTEST").color("red").isCreatedMode(true).build());
assertThat(item.getCreatedDateTime()).isNotNull();
}
@Test
void itemSaveTestNotCreatedMode() {
var item = itemService.save(ItemEntity.builder().itemId("notCreatedModeTEST").itemName("notCreatedModeTEST").color("blue").build());
assertThat(item.getCreatedDateTime()).isNotNull();
}
}
junit의 @Transactional rollback 설정은 default가 true이기 때문에 false로 변경하여 insert를 확인해보자.
itemSaveTestCreatedMode()
itemSaveTestNotCreatedMode()
위처럼 createdMode가 true로 설정한 메소드에서는 select가 안 날라간 것을 확인할 수 있다.
🤗성능비교 테스트
insert batch를 200으로 설정 후 10만 건을 돌려보았다.
itemSaveTestCreatedMode()
882800 nanoseconds spent acquiring 1 JDBC connections;
0 nanoseconds spent releasing 0 JDBC connections;
3035100 nanoseconds spent preparing 1 JDBC statements;
0 nanoseconds spent executing 0 JDBC statements;
1954177600 nanoseconds spent executing 500 JDBC batches;
0 nanoseconds spent performing 0 L2C puts;
0 nanoseconds spent performing 0 L2C hits;
0 nanoseconds spent performing 0 L2C misses;
11157535800 nanoseconds spent executing 1 flushes (flushing a total of 100000 entities and 0 collections);
0 nanoseconds spent executing 0 partial-flushes (flushing a total of 0 entities and 0 collections)
itemSaveTestNotCreatedMode()
1125100 nanoseconds spent acquiring 1 JDBC connections;
0 nanoseconds spent releasing 0 JDBC connections;
608056600 nanoseconds spent preparing 100001 JDBC statements;
11401706600 nanoseconds spent executing 100000 JDBC statements;
1897912000 nanoseconds spent executing 500 JDBC batches;
0 nanoseconds spent performing 0 L2C puts;
0 nanoseconds spent performing 0 L2C hits;
0 nanoseconds spent performing 0 L2C misses;
10280599700 nanoseconds spent executing 1 flushes (flushing a total of 100000 entities and 0 collections);
0 nanoseconds spent executing 0 partial-flushes (flushing a total of 0 entities and 0 collections)
약 2배 이상의 시간차이가 나는 걸 확인할 수 있다.
확실히 n개의 수의 SELECT문을 만들지 않았기에 큰 성능차이를 실감할 수 있었다.
🤔🤨주의점
- SELECT로 조회하지 않기 때문에 확실히 INSERT일 때에만 사용하는 것이 좋다.
- 만약 ID가 있을 경우에 대해 예외처리를 진행해야 한다.
😉결론
상황에 따라서 ID값을 자동생성하여 하면 isNew를 오버라이딩할 필요는 없지만,
만약 ID를 직접 만들어서 하는 경우에는 위처럼 오버라이딩을 하여 진행하는 것이 좋을 것 같다.
'JVM > JPA' 카테고리의 다른 글
[JPA] 트랜잭션 overrideafterCommit, beforeCommit ... (0) | 2023.04.07 |
---|---|
[QueryDSL] return 으로 Map(집합) 받기 (2) | 2022.01.25 |
[QueryDSL]like, contains 차이 (0) | 2021.05.01 |
[QueryDSL] 표현식 정리 (0) | 2021.04.18 |
[JPA] PostgreSQL @Lob 에러 (0) | 2021.04.17 |