본문 바로가기
실습/리눅스 서버 + 스프링 부트

[Spring JPA] Dynamic Insert/Update

by 이민우 2023. 12. 19.
728x90
반응형

설정에 큰 신경을 쓰지않고 JPA를 사용해보면 거슬리는 부분이 하나 발견된다.

 

줄글로 설명을 하기는 뭐하니, 코드를 통해 확인해보자.

 

우선 아래 쿼리를 이용해 테이블을 생성했다.

DROP TABLE IF EXISTS test_tbl;

CREATE TABLE test_tbl (
	row_id INT PRIMARY KEY,
	row_data_one VARCHAR(100),
	row_data_two VARCHAR(100)
);

 

그리고 위 테이블에 대한 도메인 클래스를 아래와 같이 생성해주었다.

 

TestDomain.java

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@ToString
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Table(name="test_tbl")
public class TestDomain {
	@Id
	@Column(name="row_id")
	private Integer rowId;
	
	@Column(name="row_data_one")
	private String rowDataOne;
	
	@Column(name="row_data_two")
	private String rowDataTwo;
	
}

 

 

Repository 클래스를 만든 후, application.properties 설정을 추가한다. Repository 클래스는 생략하고, 설정은 아래와 같이 입력해 쿼리문을 볼 수 있도록 만들었다.

spring.jpa.format-sql=true
spring.jpa.show-sql=true
logging.level.org.hibernate.orm.jdbc.bind=trace

 

완료되었다면 아래의 테스트 코드를 실행시켜보자.

@Test
void doTest() {
	// 데이터 초기화
	log.info("\n\n@@ Delete Rows Before Start");
	repo.deleteAll();
	
	// 데이터 생성
	log.info("\n\n@@ Create Domain With rowDataOne");
	TestDomain td = new TestDomain();
	td.setRowId(0);
	td.setRowDataOne("row data one");	
	repo.save(td);
	
	// 일부 컬럼 수정
	log.info("\n\n@@ Update Domain With rowDataTwo");
	td.setRowDataTwo("row data two");
	repo.save(td);
	
	// 데이터 삭제
	log.info("\n\n@@ Delete All");
	repo.deleteAll();
}

 

콘솔을 보면 아래와 같은 로그가 출력된다.

 

코드와 로그를 자세히 비교해보면 알겠지만,

첫 Insert 시에는 도메인 클래스 내에 rowId와 rowDataOne만을 삽입했다.

 

그러면 이를 쿼리로 변경하면 아래와 같은 쿼리를 사용할 수 있을 것이다.

INSERT INTO test_tbl (row_id, row_data_one) VALUES (0, 'row data one');

 

하지만 로그에 출력된 쿼리를 보면 아래와 같이 null 값인 컬럼도 함께 쿼리에 포함해서 던지고 있다.

insert into test_tbl (row_data_one,row_data_two,row_id) values ('row data one',null,0);

 

이는 Update도 마찬가지인데, 코드 내 Update 부분을 쿼리로 바꾸면 아래와 같이 사용할 수 있다.

UPDATE test_tbl SET row_data_two='row data two' WHERE row_id=0;

 

하지만 로그에 출력된 쿼리는 변하지 않은 row_data_one까지 함꼐 업데이트를 수행하고 있다.

update test_tbl set row_data_one='row data one',row_data_two='row data two' where row_id=0

 

당연한 말이지만, 굳이 전송할 필요가 없는 null 데이터나 변하지 않은 칼럼의 데이터까지 전송을 하게 되면 네트워크 트래픽이 낭비되고, 리소스 낭비가 발생할 수 있다.

그리고 이 낭비는 테스트용 테이블처럼 컬럼 수가 적으면 큰 문제가 되지 않겠지만, 컬럼 수가 수십 개에 달하는 테이블에 적용된다면 특히 커질 것이다.

 

이러한 낭비를 해결하는 방법은 의외로 간단하다. DynamicInsertDynamicUpdate를 사용하는 것이다.

 

사용법도 간단한데, 그냥 도메인 클래스에 아래와 같은 어노테이션을 추가해주기만 하면 된다.

 

@DynamicInsert

엔티티의 상태에 기반해 SQL 삽입 쿼리를 동적으로 생성한다. 엔티티 객체 속성 중 null이 아닌 속성만을 포함해 삽입 쿼리를 구성한다.
불필요한 컬럼 삽입을 방지함으로써 쿼리 성능을 향상시킬 수 있다.
@DynamicUpdate

엔티티의 상태에 기반해 SQL 업데이트 커리를 동적으로 생성한다. 엔티티 객체의 속성 중 변경된 속성만을 포함해 업데이트 쿼리를 구성한다.
변경되지 않은 컬럼에 대한 불필요한 DB 작업을 방지함으로써 네트워크 트래픽과 리소스 사용을 줄일 수 있다.

 

그러면 이제 다시 테스트 코드를 돌려보자.

 

Insert 시에는 null 칼럼이 배제되어 쿼리가 생성되었고, Update 시에는 변동된 컬럼만 사용해 쿼리가 생성되었음을 확인할 수 있다.

 

APPENDIX. J2DBC에서 DynamicUpsert

 

J2DBC에서도 DynamicInsert와 DynamicUpsert가 사용되는지 궁금해졌고, 한 번 테스트해보기로 했다.

 

우선 J2DBC 프로젝트를 만들고, DB연동 및 로그 확인을 위해 아래 설정을 application.properties에 추가했다.

spring.r2dbc.url=r2dbc:mariadb://localhost:3306/test
spring.r2dbc.username=root
spring.r2dbc.password=password

logging.level.org.springframework.r2dbc.core=debug

 

Domain, Repository 클래스를 생성한다.

 

TestDomain.java

import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.domain.Persistable;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@ToString
@Getter
@Setter
@Table(name="test_tbl")
public class TestDomain implements Persistable<Integer>{
	
	@Id
	@Column("row_id")
	private Integer rowId;
	
	@Column("row_data_one")
	private String rowDataOne;
	
	@Column("row_data_two")
	private String rowDataTwo;

	@Transient
	private Boolean isNew = false;
	
	@Override
	public Integer getId() {
		return this.rowId;
	}

	@Override
	public boolean isNew() {
		return this.isNew;
	}
	
	public TestDomain setNew() {
		this.isNew = true;
		return this;
	}
}

 

TestRepository.java

import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface TestRepository extends R2dbcRepository<TestDomain, Integer>{
	
}

 

그리고 테스트 코드를 작성했다.

@Test
void doTest() throws InterruptedException {
	
	// 데이터 초기화
	log.info("\n\n@@ Delete Rows Before Start");
	// 비동기식이므로 block을 해주지 않으면 쿼리가 완료되기도 전에 테스트 코드가 끝남.
	repo.deleteAll().block();
	
	// 데이터 생성
	log.info("\n\n@@ Create Domain With rowDataOne");
	TestDomain td = new TestDomain();
	td.setRowId(1);
	td.setRowDataOne("row data one");	
	repo.save(td.setNew()).block();
	
	// 일부 컬럼 수정
	log.info("\n\n@@ Update Domain With rowDataTwo");
	td.setRowDataTwo("row data two");
	td.setIsNew(false);
	repo.save(td).block();
	
	// 데이터 삭제
	log.info("\n\n@@ Delete All");
	repo.deleteAll().block();
}

 

이제 한 번 동작을 해보자.

이유는 모르겠지만 Insert는 Dynamic하게 동작하고, Update는 Dynamic하지 못하게 동작한다.

*Persistable 때문에 Insert가 Dynamic하게 동작하는 줄 알고 일반 JPA에서 실험해보았으나 아니었다.

 

어쨌든 이제 Domain 클래스에 @DynamicInsert와 @DynamicUpdate를 추가해보려고 했는데...?!

 

지원하지 않는다...

 

이전 포스팅에서 언급했던 Composite PK 때와 마찬가지인데,

R2DBC는 ORM기능보다는 DB와의 비동기 통신에 중점을 두고 있기 때문에, 이러한 고급 ORM 기능은 제공하지 않는다고 한다.

즉 직접 구현할 수 밖에 없다.
728x90
반응형