스프링 데이터 JPA
가 제공하는 공통 인터페이스 구현체는 SimpleJapRepository
이다.
SimpleJapRepository
내부를 보게 되면 findAll findBy, save, exists, delete, count 등의 공통 인터페이스 메서드가 구현되어있는 것을 알 수 있다.
간단히 특징을 살펴보자.
@Repository
@Transactional(readOnly=true)
readOnly=true
로 트랜잭션이 설정되어 있어, 데이터를 단순히 조회하는 트랜잭션에서@Transactional
데이터의 모든 변경(등록,수정,삭제)이 트랜잭션내에서 수행될 수 있도록 @Transactional
이 설정되어있다.
서비스 계층에서 트랜잭션을 시작하지 않았을 경우에 Repository에서 트랜잭션이 시작되게 된다.
만약 서비스 계층에서 트랜잭션을 시작하면 리포지토리에서 해당 트랜잭션을 전파 받아서 사용하게 된다.
먼저 구현체 메소드 중에서 save 를 살펴보자.
앞서 특징에서 말했던거와 같이 데이터 변경이 트랜잭션 내에서 일어날수 있도록 @Transactional
이 설정된 것을 볼 수 있다.
내부 로직을 보게 되면 새로운 엔티티일 경우에는 persist
가 동작하고,
기존에 있는 엔티티인 경우에는 merge
가 동작하는 것을 볼 수 있다.
🔎
merge
(병합)이란?준영속 상태의 엔티티를 영속 상태로 변경할 떄 사용하는 기능이다.
(준영속 상태
란 영속성 컨텍스트가 관리하지 않은 엔티티를 의미한다.)
merge
의 동작 방식을 간단히 살펴보겠다.
준영속 상태인 엔티티의 식별자 값으로 1차 캐쉬에서 엔티티를 조회한다.
-> 만약 엔티티가 1차 캐쉬에 없다면 DB에서 조회하게 된다. (select 쿼리가 발생하기 된다!!)조회했던 영속 상태 엔티티값을 준영속 상태였던 엔티티의 값으로 모두 바꾼다.
주위❗
엔티티의 값을 변경시킬 때병합(merge)
인 경우에는 모든 필드가 교체된다.
즉, 병합시 특정 필드값이 없을 경우에 해당 필드값이 null로 채워지게 된다.모든 값을 바꾼 영속 상태의 객체를 반환한다.
-> 트랜잭션 커밋 시점에 변경 감지 기능이 동작하여 update 쿼리문 실행.
그럼 새로운 엔티티를 어떻게 구별하게 될까?
새로운 엔티티를 판단하는 JPA
전략은 아래와 같다.
@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);
}
}
위와 같이 테스트를 진행하게 되면, @GenerateValue
면 save()
호출 시점에 식별자가 없으므로 새로운엔티티로 인식해서 persist
로 동작하게 되고 Insert
쿼리만 발생하게 된다.
(save 호출 후에는 식별자 값이 들어가있게 된다.)
위의 같은 경우에는 PK 값을 @GenerateValue
전략으로 사용했을 경우이다.
만약에 UUID
와 같은 전략이나 PK값을 따로 생성하여 사용하는 경우,
즉 @GenerateValue
전략을 사용하지 않은 경우에는 엔티티를 생성할 때 PK값을 직접 할당해야할 것이다.
이런 경우에는 새로운 엔티티를 판별하는 전략에 해당되지 않는다.(객체의 식별자가 null이 아니기 때문에)
그럼 merge
가 동작하게 되고 DB를 호출해서 값을 찾게 되고, 값이 없으면 새로운 엔티티로 확인하는등 불필요한 select 문이 발생하게 될 것이다.
이러한 경우에는 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
에 값이 없으면 새로운 엔티티로 판단!)