스프링 데이터 JPA 분석

구본식·2023년 3월 4일
0

Spring Data JPA

목록 보기
7/8
post-thumbnail

1. 스프링 데이터 JPA 구현체 분석

스프링 데이터 JPA가 제공하는 공통 인터페이스 구현체는 SimpleJapRepository이다.

SimpleJapRepository 내부를 보게 되면 findAll findBy, save, exists, delete, count 등의 공통 인터페이스 메서드가 구현되어있는 것을 알 수 있다.

간단히 특징을 살펴보자.

  • @Repository

    • 컴포넌트 스캔 대상이 되므로 스프링 컨테이너의 스프링 빈으로 등록된다.
    • JPA 예외를 스프링이 추상화한 예외로 변환해준다!
      -> 하부 기술(JPA,JDBC, Mybatis 등)들 마다 예외가 다르게 발생하게 되는데, 이 예외들을 공통으로 스프링이 추상화한 예외로 바꾸어준다. 즉 하부 기술들이 바뀌어도 예외를 처리한 서비스 로직을 수정하지 않아도 된다.
  • @Transactional(readOnly=true)

    • 기본적으로 readOnly=true로 트랜잭션이 설정되어 있어, 데이터를 단순히 조회하는 트랜잭션에서
      트랜잭션 flush를 생략하게 되어 약간의 성능 향상 이점을 얻을 수 있다.
  • @Transactional

    • 데이터의 모든 변경(등록,수정,삭제)이 트랜잭션내에서 수행될 수 있도록 @Transactional이 설정되어있다.

    • 서비스 계층에서 트랜잭션을 시작하지 않았을 경우에 Repository에서 트랜잭션이 시작되게 된다.

    • 만약 서비스 계층에서 트랜잭션을 시작하면 리포지토리에서 해당 트랜잭션을 전파 받아서 사용하게 된다.

2. 새로운 엔티티 구별하는 법

먼저 구현체 메소드 중에서 save 를 살펴보자.

앞서 특징에서 말했던거와 같이 데이터 변경이 트랜잭션 내에서 일어날수 있도록 @Transactional이 설정된 것을 볼 수 있다.

내부 로직을 보게 되면 새로운 엔티티일 경우에는 persist가 동작하고,
기존에 있는 엔티티인 경우에는 merge가 동작하는 것을 볼 수 있다.

🔎merge(병합)이란?

준영속 상태의 엔티티를 영속 상태로 변경할 떄 사용하는 기능이다.
(준영속 상태란 영속성 컨텍스트가 관리하지 않은 엔티티를 의미한다.)

merge의 동작 방식을 간단히 살펴보겠다.

  1. 준영속 상태인 엔티티의 식별자 값으로 1차 캐쉬에서 엔티티를 조회한다.
    -> 만약 엔티티가 1차 캐쉬에 없다면 DB에서 조회하게 된다. (select 쿼리가 발생하기 된다!!)

  2. 조회했던 영속 상태 엔티티값을 준영속 상태였던 엔티티의 값으로 모두 바꾼다.

    주위❗
    엔티티의 값을 변경시킬 때 병합(merge)인 경우에는 모든 필드가 교체된다.
    즉, 병합시 특정 필드값이 없을 경우에 해당 필드값이 null로 채워지게 된다.

  3. 모든 값을 바꾼 영속 상태의 객체를 반환한다.
    -> 트랜잭션 커밋 시점에 변경 감지 기능이 동작하여 update 쿼리문 실행.

그럼 새로운 엔티티를 어떻게 구별하게 될까?

새로운 엔티티를 판단하는 JPA 전략은 아래와 같다.

  • 식별자가 객체일 때 null
  • 식별자가 자바 기본 타입(객체x)일 때 0으로 판단

Test(@GeneratedValue 전략)

@Entity
@Getter
public class NewEntityTest {

    @Id
    @GeneratedValue
    private Long id;
    
     public NewEntityTest(){
    }
}

@SpringBootTest
class NewEntityTestRepositoryTest {

    @Autowired NewEntityTestRepository newEntityTestRepository;

    @Test
    public void 새로운객체판별테스트() throws Exception {

        //기본 "@GeneratedValue"일 경우
        NewEntityTest entity = new NewEntityTest();
        newEntityTestRepository.save(entity);
    }
}

위와 같이 테스트를 진행하게 되면, @GenerateValuesave() 호출 시점에 식별자가 없으므로 새로운엔티티로 인식해서 persist로 동작하게 되고 Insert 쿼리만 발생하게 된다.
(save 호출 후에는 식별자 값이 들어가있게 된다.)

위의 같은 경우에는 PK 값을 @GenerateValue 전략으로 사용했을 경우이다.

만약에 UUID와 같은 전략이나 PK값을 따로 생성하여 사용하는 경우,
@GenerateValue 전략을 사용하지 않은 경우에는 엔티티를 생성할 때 PK값을 직접 할당해야할 것이다.

이런 경우에는 새로운 엔티티를 판별하는 전략에 해당되지 않는다.(객체의 식별자가 null이 아니기 때문에)

그럼 merge가 동작하게 되고 DB를 호출해서 값을 찾게 되고, 값이 없으면 새로운 엔티티로 확인하는등 불필요한 select 문이 발생하게 될 것이다.

이러한 경우에는 Persistable를 사용하여 새로운 엔티티를 판별하는 전략을 새로 구현하면 된다.

Test(@GeneratedValue 전략X, Persistable 사용)

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class NewEntityTest extends BaseTimeEntity implements Persistable<String> {

    @Id
    //@GeneratedValue -> 사용안하는 전략일 경우
    private String id;

    public NewEntityTest(String id){
        this.id = id;
    }

    @Override //새로운 엔티티 판변하는 메서드
    public boolean isNew() {
        return getCreatedDate() == null;
    }
}

@Getter
@MappedSuperclass //엔티티가 아닌 클래스 상속 -> 매핑 정보 속성만 제공
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
}

@SpringBootTest
class NewEntityTestRepositoryTest {

    @Autowired NewEntityTestRepository newEntityTestRepository;

    @Test
    public void 새로운객체판별테스트() throws Exception {

        //"@GeneratedValue"가 아닌 String 와 같은 자료형 일 경우
        NewEntityTest entity = new NewEntityTest("A");
        newEntityTestRepository.save(entity);
    }
}

Persistable 를 사용해서 새로운 엔티티 확인 여부를 체크하게 된다.

새로운 엔티티를 체크하기 위한 좋은 방법으로는 등록시간(@CreateDate)을 조합해서 사용하면 새로운 엔티티 여부를 편리하게 판단할 수 있다.
(@CreateDate에 값이 없으면 새로운 엔티티로 판단!)

profile
백엔드 개발자를 꿈꾸며 기록중💻

0개의 댓글