JVM/JPA

[JPA] save() isNew를 사용하여 성능개선 하기

Hyo Kim 2021. 10. 2. 23:59
728x90
반응형

😎 서론

사내 모든 프로젝트에서는 데이터베이스에 값을 저장할 때

아이디를 유니크한 값으로 자동생성해주는 로직을 만들어 별도로 태운 후 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를 직접 만들어서 하는 경우에는 위처럼 오버라이딩을 하여 진행하는 것이 좋을 것 같다.

728x90
반응형