Spring Data JPA를 사용하면 JPA를 더 쉽고 편리하게 사용할 수 있다.
@Entity
public class Item {
@Id @GeneratedValue(strategy = IDENTITY)
private Long id;
private String name;
public Item(String name) {
this.name = name;
}
}
Spring Data JPA를 활용하면 반복으로 사용되는 기능들을 추상화해두었다.
public interface ItemRepository extends JpaRepository<Item, Long> {
}
선언하면 준비는 끝났다. 다들 아시겠지만 이렇게 선언만 해주어도 JPA 관련 기능들을 전부 제공받을 수 있다.
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
Optional<T> findById(ID id);
boolean existsById(ID id);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> ids);
long count();
void deleteById(ID id);
void delete(T entity);
void deleteAllById(Iterable<? extends ID> ids);
void deleteAll(Iterable<? extends T> entities);
void deleteAll();
}
기본적으로 findById 메서드의 반환은 Optional 이기에 null 에 대한 처리를 해주어야 한다.
Item item = itemRepository.findById(itemId).orElseThrow(() -> new IllegalStateException("Item not found"));
이런식으로 사용할 수 있겠다. CrudRepository의 기능 중 findById 메서드를 통해 엔티티를 조회할 수 있다.
JPA는 더티 체킹이라는 기술로 변경 사항을 감지해 업데이트 문을 날려주게 되는데,
public class ItemService {
private final ItemRepository items;
@Transactional
public void modifyItemName(String name) {
Item item = itemRepository.findById(itemId).orElseThrow(() -> new IllegalStateException("Item not found"));
item.modifyName(name)
}
}
와 같이 조회를 통해 Persistence Context에 퍼올린 후 변경감지를 통해 엔티티의 값을 수정하는 코드들이 빈번하게 발생한다.
public class ItemService {
private final ItemRepository items;
@Transactional
public void modifyItemName(Long id, String name) {
Item item = this.getByItemId(id);
item.modifyName(name)
}
private Item getItemByIdOrElseThrow(Long itemId) {
return itemRepository.findById(itemId).orElseThrow(() -> new IllegalStateException("Item not found"));
}
@Transactional(readOnly = true)
public ItemResponse getItemById(Long item) {
Item item = this.getByItemId(id);
return new ItemResponse(item.getId(), item.getName());
}
}
private Item getItemById(Long itemId) {
return itemRepository.findById(itemId).orElseThrow(() -> new IllegalStateException("Item not found"));
}
재사용성을 챙기기 위해 위와 같은 형태로(?) 풀어낼 수도 있겠지만,
당장 이 코드 뿐 아니라 값을 변경하거나 id로 조회하는 모든 단건 조회에서 사용될 것이 뻔한데, 공통적으로 사용할 수 있게 제공해주는 게 좋을 것 같았다.
그래서 조금 더 쉽게 쓰고 싶은 욕구가 차올랐다.
다시 코드로 돌아와,
.orElseThrow(() -> new IllegalStateException("Item not found"));
이 걸 붙여주기만 하면 될 것 같다.
어디다 붙여줄까 고민하다가 JpaRepository 쪽에 디폴트 메서드로 붙였다.
@NoRepositoryBean
public interface JpaRepositoryExtension<T, ID> extends JpaRepository<T, ID> {
default T getByIdOrElseThrow(ID id) {
return getByIdOrElseThrow(id, "Entity not found with id: " + id); }
default T getByIdOrElseThrow(ID id, String errorMessage) {
return findById(id).orElseThrow(() -> new IllegalStateException(errorMessage));
}
}
public interface ItemRepository extends JpaRepositoryExtension<Item, Long> {
}
로 변경하면 디폴트 메서드를 활용할 수 있다.
어떤 인터페이스를 확장할까 고민하던 찰 나, CrudRepository와 JpaRepository가 보였다.
CrudRepository 가 getByIdOrElseThrow 라는 시그니처를 갖는게 좋아보였지만
CrudRepository는 Spring Data 프로젝트의 고수준 인터페이스로써 우리 애플리케이션 코드에서 공통적으로 확장한 인터페이스를 상속해야하는데 제한이 있을 수 밖에 없다, 또한 getByIdOrElseThrow 뿐 아니라, 추가적으로 EntityManager의 기능들을 제공하기에 적합하다 생각했다.
public interface CustomJpaRepository<T, ID> extends JpaRepositoryExtension<T, ID>, CrudRepositoryExtension<T, ID> {}
이런식으로 만들 수도 있겠지만, 아래와 같이 간편하게 만들었다.
@NoRepositoryBean
public interface JpaRepositoryExtension<T, ID> extends JpaRepository<T, ID> {
void clear();
void detach(T entity);
boolean contains(T entity);
default T getByIdOrElseThrow(ID id) {
return getByIdOrElseThrow(id, "Entity not found with id: " + id); }
default T getByIdOrElseThrow(ID id, String errorMessage) {
return findById(id).orElseThrow(() -> new IllegalStateException(errorMessage));
}
}
다들 아시다 싶히 SimpleJpaRepository 가 concrete class
다
final
키워드가 붙어있지 않기에 상속이 가능하고 이를 확장하여 공통 기능들을 정의해주었다.
public class SimpleJpaRepositoryExtension<T, ID> extends SimpleJpaRepository<T, ID> implements ExtendedJpaRepository<T, ID> {
private final EntityManager entityManager;
public ExtendedSimpleJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityManager = entityManager;
}
public ExtendedSimpleJpaRepository(Class<T> domainClass, EntityManager entityManager) {
super(domainClass, entityManager);
this.entityManager = entityManager;
}
@Override
public boolean contains(T entity) {
return this.entityManager.contains(entity);
}
@Override
public void clear() {
this.entityManager.clear();
}
@Override
public void detach(T entity) {
this.entityManager.detach(entity);
}
}
spring boot의 강력한 autoconfigure 의 자동 구성 방식으로는 확장한 클래스를 모르기 때문에 그대로 사용할 수 없다는 제약이 있다.
아래와 같이 직접 지정해주어야 한다.
@EnableJpaRepositories(repositoryBaseClass = ExtendedSimpleJpaRepository.class)
JPA, Datasource 쪽에 구성을 설정하고 애플리케이션에서는 컴포넌트 스캔을 통한 구성을 유도하는 방식으로 사용 하고 있다.
@Configuration(proxyBeanMethods = false)
@EnableJpaRepositories(repositoryBaseClass = SimpleJpaRepositoryExtension.class)
// @EnableJpaAuditing
// @EnableTransactionManagement
// @EntityScan 등등 Jpa와 관련된 설정을 이쪽에 몰아넣는다.
public class JpaConfigurer {
}
@DataJpaTest
class JpaExtensionTest {
@Autowired
private ItemRepository items;
@Test
void t1() {
// 비영속화 상태
Item item = new Item("item");
assertThat(items.contains(item)).isFalse();
// 영속화 상태
Item saved = items.save(item);
assertThat(items.contains(item)).isTrue();
items.clear();
assertThat(itemJpaRepository.contains(item)).isFalse();
Item existingItem = itemJpaRepository.getByIdOrElseThrow(item.getId());
// 영속화 상태
assertThat(itemJpaRepository.contains(existingItem)).isTrue();
}
}
Persistence Context를 날리거나 하는 작업들을 따로 엔티티매니저를 주입받지 않고 내가 주입한 Repository를 통해 entity manager의 기능도 함께 사용할 수 있다.