JAVA

[JPA] JPA 정복하기 - 05. 스프링 데이터 JPA 분석

코린이s 2024. 1. 21. 13:55
728x90

Jpa 관련 어노테이션 중 알고 가야하는 어노테이션에 대해 정리 하였다.

어노테이션 설명
@Repository JPA 예외시 JPA Exception이 아닌 스프링이 정의한 공통 예외로 나가 공통 예외 처리가 가능하다.
@Transactional 트랜젝션 적용으로 JPA 의 모든 변경 동작은 트랜젝션 안에서 동작해야하여 꼭 @Transactional 을 사용하는 클래스에서 선언해야 동작 하나 Spring Data JPA는 레파지토리 내에서 @Transactional 처리를 했기에 없어도 처리 가능하다.
@Transactional(readOnly = true) 읽기 전용이기에 flush를 안해서 성능 향상 이점이 있다.

이제 Save 에 대해서 알아보자!

Spring Data Jpa Save 로직을 한번 보면 이와 같이 isNew 를 통해 새로운 엔티티인지 보고 이에 따라 persist 또는 merge 로 처리하는것을 볼 수 있다.

	@Transactional
	@Override
	public <S extends T> S save(S entity) {

		Assert.notNull(entity, "Entity must not be null");

		if (entityInformation.isNew(entity)) {
			entityManager.persist(entity);
			return entity;
		} else {
			return entityManager.merge(entity);
		}
	}

이미 조회해온 엔티티가 아닌 새로운 엔티티면 persist(DB 바로 삽입), 아니면 merge (DB에서 조회하고 데이터 업데이트를 한다.)
merge는 조회를 한번 하기 때문에 업데이트와 같이 꼭 필요한 경우에만 사용하는것이 좋다.

merge를 타는경우가 새로운 엔티티가 아닐 경우인데

새로운 엔티티인지 파악하는 isNew 함수는 엔티티의 식별자가 객체면 null, 자바 기본타입일 경우 0일때(DB 조회하지 않은 엔티티면 식별자는 셋팅되어 있지 않기 때문에) 새로운 엔티티로 취급한다.

만약 식별자를 DB가 아닌 개발자가 직접 셋팅해야 하는 경우라면

식별자가 null이거나 0 이 아닐수 밖에 없기 때문에 신규로 생성한 엔티티라도 새로운 엔티티가 아니라고 판단하여 무조건 merge를 통해 조회 한번 쿼리 나가고 삽입이 이뤄져 비효율적이다.

실제 merge로 동작 하는지 테스트를 해보자!

우선 식별자를 개발자가 아닌 DB가 셋팅할 수 있도록 @GeneratedValue 를 사용한다.

package study.datajpa.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class BasicEntity {
    @Id @GeneratedValue
    private Long id;
    private String username;

    public BasicEntity(String username) {
        this.username = username;
    }
}

레파지토리를 만들고

package study.datajpa.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import study.datajpa.entity.BasicEntity;

public interface BasicRepository extends JpaRepository<BasicEntity,Long> {
}

테스트 코드로 실행해보자!

@Transactional
@Rollback(value = false)
@SpringBootTest
class MemberRepositoryTest {

    @Autowired
    BasicRepository basicRepository;
    
    @Test
    public void merge(){
        BasicEntity basicEntity = new BasicEntity("corin");
        basicRepository.save(basicEntity);
    }

persist 로 동작하여 삽입 쿼리만 나간것을 볼 수 있다.

이제 식별자를 직접 셋팅하는 방식으로 변경하고

package study.datajpa.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class BasicEntity {
    @Id
    private Long id;
    private String username;

    public BasicEntity(Long id, String username) {
        this.id = id;
        this.username = username;
    }
}

테스트 코드를 돌려보면

@Test
public void merge(){
    BasicEntity basicEntity = new BasicEntity(1L,"corin");
    basicRepository.save(basicEntity);
}

식별자인 id 가 null 이 아니기에 새로운 엔티티가 아니라고 판단하여 merge 로 동작하게 되어 조회 쿼리 한번 나가고 삽입이 이뤄졌다.


그렇기에 이런 경우에는 Persistable 인터페이스를 구현하여 직접 신규 객체 여부를 파악하는 메소드를 구현하면 된다.

isNew 라는 메소드 이며 createDate 칼럼의 경우 생성일자로 식별자와 같이 신규 엔티티의 경우 조회하기 전까지는 무조건 빈값이므로 해당 변수로 체크한다.

package study.datajpa.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.data.domain.Persistable;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class BasicEntity extends BaseEntity implements Persistable<Long> {
    @Id
    private Long id;
    private String username;

    @Override
    public Long getId() {
        return this.id;
    }

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

    public BasicEntity(Long id, String username) {
        this.id = id;
        this.username = username;
    }
}

다시 테스트를 돌리면 merge가 아닌 persist 로 동작하는것을 볼 수 있다.

728x90