요즘 백엔드 관련한 내용을 매일마다 질문해주는 Maeil-Mail이라는 서비스를 사용하여 하루하루 지식을 체크중이다.
(이거 생각보다 재밌을지도?)
오늘의 주제는 바로 Spring Data JPA이다.
항상 인터페이스처럼 편하게 JPQL을 사용하게 해주는 녀석... 하지만 EntityManager를 사용한 영속성 컨텍스트 관리를 한다는 것 말고는 제대로 동작 방식을 알고 있었을까?
위 질문(제목에 게시)에 뭐 영속성 컨텍스트가 새로운 엔티티를 판별하겠지? 만 생각이 들고 정확한 동작 관리를 알지 못했다.
...
아직 갈 길이 멀다. Spring Data JPA 뜯어보기. 바로 시작해보도록 하자
Spring Data JPA란?
- 우선 JPA란 무엇일까?
JPA란 Java Persistentce API의 약자로 JAVA에서 관계형 DB를 사용하는 방식을 정의한 인터페이스.
말 그대로 인터페이스이기 때문에 구현체가 없다.
- 그럼 Spring Data JPA는 뭔데?
JPA를 한 단계 더 추상화 시켜 개발 용이성을 상당히 올려주는 인터페이스.
Spring Data JPA는 'Repository'라는 인터페이스를 제공
이 Repository는 정해진 규칙으로 인터페이스를 선언하기만 해도 자동으로 내부에서 구현체를 만들어 동작시켜준다.
특히 자주 사용하는 매서드들은 별도로 선언하지 않아도 사용 할 수 있다는 점!!
(ex. findAll(), save(), delete())
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(String username);
}
- JpaRepository<엔티티, PK type>
- findByUsername(String username) : 인터페이스를 선언하면 해당 매서드를 내부에서 자동으로 만들어 줌.
Repository 인터페이스의 기본 구현체인 SimpleJpaRepository의 내부를 보면 EntityManager를 사용하고 있다.
이를 통해 Spring Data JPA는 JPA를 추상화 시켰다는 것을 알 수 있는 것.
본론) Spring Data JPA는 어떻게 새로운 엔티티를 어떻게 판단하는데?
JpaEntityInfromation의 isNew(T entity) 에 의해 판단됨.
@Override
public boolean isNew(T entity) {
if(versionAttribute.isEmpty())
|| versionAttribute.map(Attribute::getJavaType).map(Class::isPrimitive).orElse(false)) {
return super.isNew(entity);
}
BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity);
return versionAttribute.map(it -> wrapper.getPropertyValue(it.getName()) == null).orElse(true);
}
다른 설정이 없으면 JpaEntityInformation의 구현체 중 JpaMetamodelEntityInformation 클래스가 동작한다.
@Version이 사용된 필드가 없거나 @Version이 사용된 필드가 primitive 타입이면 AbstractEntityInformation의 isNew()를 호출한다.
@Version이 사용된 필드가 wrapper class이면 null 여부를 확인.
public boolean isNew(T entity) {
Id id = getId(entity);
Class<ID> idType = getIdType();
if (!idType.isPrimitive()) {
return id == null;
}
if (id instanceof Number) {
return ((Number) id).longValue() == 0L;
}
throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
}
@Version이 사용된 필드가 없어서 AbstractEntityInformation 클래스가 동작하면 @Id 어노테이션을 사용한 필드를 확인해서 primitive 타입이 아니라면 null 여부, Number의 하위 타입이면 0인지 여부를 확인한다.
@GeneratedValue 어노테이션으로 키 생성 전략을 사용하면 데이터베이스에 저장될 때 id가 할당된다.
따라서 데이터베이스에 저장되기 전에 메모리에서 생성된 객체는 id가 비어있기 때문에 isNew()는 true가 되어 새로운 entity로 판단합니다.
직접 ID를 할당하는 경우에는 어떻게 동작할까?
키 생성 전략을 사용하지 않고 직접 ID를 할당하는 경우 새로운 entity로 간주되지 않는다.
이 때는 엔티티에서 Persistable<T> 인터페이스를 구현해서 JpaMetamodelEntityInformation 클래스가 아닌 JpaPersistableEntityInformation의 isNew()가 동작하도록 해야 한다.
public class JpaPersistableEntityInformation<T extends Persistable<ID, ID>
extends JpaMetamodelEntityInformation<T, ID> {
public JpaPersistableEntityInformation(Class<T> domainClass, Metamodel metamodel,
PersistenceUnitUtil persistenceUnitUtil) {
super(domainClass, metamodel, persistenceUnitUtil);
}
@Override
public boolean isNew(T entity) {
return entity.isNew();
}
@Nullable
@Override
public ID getId(T entity) {
return entity.getId();
}
}
새로운 Entity인지 판단하는게 왜 중요할까?
@Override
@Transactional
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);
}
}
SimpleJpaRepository의 save() 메서드에서 isNew()를 사용하여 persist를 수행할지 merge를 수행할지 결정!!
만약 ID를 직접 지정해주는 경우에는 신규 entity라고 판단하지 않기 때문에 merge를 수행한다.
이때 해당 entity는 신규임에도 불구하고 DB를 조회하기 때문에 비효율적이다.
따라서, 새로운 entity인지 판단하는 것은 매우매우 중요한 것이다.
참고
- 매일 메일 서비스
'Back-end > Spring' 카테고리의 다른 글
[Spring] Spring Batch를 활용한 배치 프로세싱 (1) | 2025.01.20 |
---|---|
[Spring] Private 메서드에 @Transactional 선언 시 트랜잭션이 동작하는가? (0) | 2025.01.16 |
[Spring] AWS SageMaker 엔드포인트 연결 시도하기 (0) | 2024.11.23 |
[Spring] self-hosted Runner를 통한 CI/CD 배포 (0) | 2024.11.21 |
[Spring] @RequestPart vs @RequestParam vs @RequestBody (1) | 2024.10.11 |