본문 바로가기
Spring Framework

Spring Data JPA 2.4.15 Document Memo

by Jordy-torvalds 2022. 6. 14.
반응형

여는 글

이 글은 Spring Data JPA 2.4.15 도큐먼트를 읽고 인상 깊었던 내용 일부만을 무자비한 의역 및 관련 개념을 정리하는 글 입니다.

https://docs.spring.io/spring-data/jpa/docs/2.4.15/reference/html/#reference

4. Working with Spring Data Repositories

스프링 데이터 리포지토리 추상화의 목표는 데이터 액세스 레이어를 구현하는데 필요한 많은 보일러플레이트 코드를 막대하게 줄이는 것이다.

4.1. Core concepts

스프링 데이터 리포지토리 추상화의 중심이 되는 인터페이스는 Repository 입니다. 리포지토리는 관리 하기 위한 도메인 클래스와 아이디 타입을 가집니다. 이 인터페이스는 주로 Marker Interface 처럼 작동합니다. Marker Interface는 사용할 타입을 캡쳐하고 그것을 확장한 인터페이스를 찾는데 도움이 되는 인터페이스를 말합니다. Repository 를 확장한 대표적인 인터페이스인 CrudRepository 는 관리 중인 엔티티에 수준 높은 CRUD 기능을 제공합니다.

CrudRepository 에서 확장된 PagingAndSortingRepository 도 있습니다!

4.2. Query Methods

리포지토리 인터페이스를 위한 프록시 인스턴스를 만들기 위해서 위와 같은 스프링 설정이 필요합니다.

프록시 인터페이스를 만든다는 말은 해당 인터페이스의 메소드를 호출 했을 때 실제로 실행되는 구현체가 따로 있고 이를 만들어 준다는 의미입니다. 기본적인 구현체는 SimpleJpaRepository 이다. (확인 필요)

CrudRepository 인터페이스의 기본 구현입니다. 이 구현체는 일반 EntityManager보다 더 정교한 인터페이스를 제공합니다.

4.3. Defining Repository Interfaces

리포지토리 인터페이스를 정의하기 위해서 당신은 도메인 클래스 별 리포지토리 인터페이스를 정의하는 것이 필요합니다. 이 인터페이스는 Repository 를 확장해야 하고 도메인 클래스와 아이디 타입이 입력되어야 합니다. 만약 당신이 CRUD 메소드를 노출하기를 원한다면 Repository를 대신해서 CrudRepository를 확장하세요.

여러분이 구현할 Repository가 가졌으면 하는 기능의 정도에 따라 Spring Data JPA가 제공하는 Repository, CrudRepository, PagingAndSortingRepository 중 선택하거나 커스텀하게 정의된 리포지토리를 사용할 수도 있습니다. 다만, SOLID 중 ISP에 따라 불필요하게 과도한 기능의 노출을 지양하는 것이 좋습니다.

커스텀하게 선택적으로 필요한 CRUD 기능만을 노출시킨 리포지토리 인터페이스의 예시 입니다. MyBaseRepository@NoRepositoryBean 이 붙어 있어서 프록시 인스턴스를 만들지 않고 확장하는데사용될 수 있습니다.

4.3.2. Using Repositories with Multiple Spring Data Modules

Spring Data Modules 에는 JPA 외에 Mongo, Redis 등 많이 알려진 디비 벤더를 포함해 정말 다양한 모듈이 있습니다.

위 예시는 도메인 클래스가 JPA와 Spring Data MongoDB 어노태이션 둘 모두를 사용하는 것을 보여준다. 이 도메인 클래스는 두 리포지토리(JpaPersonRepository 와 MongoDBPersonRepository)를 정의합니다. 스프링 데이터는 더 이상 리포지토리를 구분하지 못하므로, 정의되지 않은 동작이 발생합니다.

@Enable{module-name]Repositories 에 base package 속성을 사용해 엄격한 구분이 가능하긴 하지만 anti-pattern 이니 특정 클래스에 여러 스프링 데이터 모듈의 어노태이션을 붙이는 것을 지양 합시다.

4.4. Defining Query Methods

쿼리 메소드의 이름으로 어떤 동작을 할지 정의할 수 있습니다.

Spring Data JPA - Reference Documentation

4.5. Creating Repository Instances

4.5.2. Java Configuration

여러분은 리포지토리 인프라스트럭쳐를 스토어별 @Enable${store}Repositories 어노태이션을 자바 설정 클래스에서 사용해 트리거 할 수 있씁니다.

4.6. Custom Implementations for Spring Data Repositories

쿼리 메소드가 다른 행위를 요구하거나, 쿼리 파생물로 구현될 수 없을 때, 여러분은 커스텀 구현체가 필요합니다. 스프링 데이터 리포지토리는 커스텀 리포지토리 코드를 제공하며 일반적인 crud 추상화, 쿼리 메소드 기능과 통합해줍니다

4.6.1. Customizing Individual Repositories

// Snippet 1 : 커스텀 리포지토리 기능을 위한 인터페이스
interface CustomizedUserRepository {
  void someCustomMethod(User user);
}
// Snippet 2 : 커스텀 리포지토리 기능의 구현
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  public void someCustomMethod(User user) {
    // Your custom implementation
  }
}
// Snippet 3 : 리포지토리 인터페이스를 아래와 같이 구현
interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {

  // Declare query methods here
}

Snippet 1 과 같이 인터페이스를 정의하고, Snippet 2 처럼 해당 인터페이스의 구현체를 만든 후에 Snippet 3처럼 Snippet 1의 인터페이스와 Spring Data JPA가 제공하는 Repository 인터페이스를 통합하면 되겠습니다. 이것이 유용한 케이스는 필요한 비즈니스 조회 로직이 Spring Data JPA에서 제공하는 기능으로 구현이 어려울 때 커스텀한 구현체에 JdbcTemplate이나 QueryDSL 등의 라이브러리 등을 활용한 조회 기능을 구현함으로써 이를 보완할 수 있습니다.

interface HumanRepository {
  void someHumanMethod(User user);
}

class HumanRepositoryImpl implements HumanRepository {

  public void someHumanMethod(User user) {
    // Your custom implementation
  }
}

interface ContactRepository {

  void someContactMethod(User user);

  User anotherContactMethod(User user);
}

class ContactRepositoryImpl implements ContactRepository {

  public void someContactMethod(User user) {
    // Your custom implementation
  }

  public User anotherContactMethod(User user) {
    // Your custom implementation
  }
}

interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {

  // Declare query methods here
}

위는 도큐먼트의 예시 입니다.

설정 방법은 @EnableJpaRepositoriesrepositoryImplementationPostfix 속성을 사용해서 구현체의 접미사를 지정해주면 되겠씁니다. 기본값은 Impl 입니다.

Resolution of Ambiguity(모호성 해결)

서로 다른 패키지에 동일한 이름을 가진 리포지토리 구현체가 있을 경우 빈 생성간 충돌이 발생할 수 있는데, @Component 를 사용해 충돌되는 것 중 하나의 리포지토리 구현체에 빈 이름을 변경해줌으로써 이를 회피할 수 있습니다. 근데 굳이 이렇게 까지 하지 말고 클래스 명을 달리 갑시다.

4.6.2. Customize the Base Repository

모든 리포지토리가 영향을 받도록 하기 위해 베이스 리포지토리의 동작을 커스텀하기를 원할 때 필요한 접근법을 설명합니다. 모든 리포지트를 위해 동작을 변경하는 것 대신해서, 기술 별 리포지토리 기준 클래스를 확장해서 구현체를 만들 수 있습니다. 이 클래스는 리포지토리 프록시를 위해서 커스텀 베이스 클래스로서 동작합니다.

class MyRepositoryImpl<T, ID> extends SimpleJpaRepository<T, ID> {

  private final EntityManager entityManager;

  MyRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager) {
    super(entityInformation, entityManager);

    // Keep the EntityManager around to used from the newly introduced methods.
    this.entityManager = entityManager;
  }

  @Transactional
  public <S extends T> S save(S entity) {
    // implementation goes here
  }
}

주의 사항

리포지토리 베이스 클래스는 스토어 별 리포지토리 팩토리 구현으로 사용 하기 위해 슈퍼 클래스의 생성자를 super(…) 를 사용해 가지고 있어야 합니다. 만약 리포지토리 베이스 클래스의 생성자가 많더라도 하나는 EntityInformation 와 스토어 별 인프라스트럭쳐 객체(ex. EntityManager)를 super(…) 를 사용해 슈퍼 클래스에 전달해야 합니다. 그 이유는 리포지토리 베이스 클래스가 상속 받는 SimpleJpaRepository의 생성자가 아래와 같기 떄문입니다.

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {

...

    /**
     * Creates a new {@linkSimpleJpaRepository} to manage objects of the given {@linkJpaEntityInformation}.
     *
     * @paramentityInformationmust not be {@literalnull}.
     * @paramentityManagermust not be {@literalnull}.
     */
    public SimpleJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {

       Assert.notNull(entityInformation, "JpaEntityInformation must not be null!");
       Assert.notNull(entityManager, "EntityManager must not be null!");

       this.entityInformation = entityInformation;
       this.em = entityManager;
       this.provider = PersistenceProvider.fromEntityManager(entityManager);
    }

    /**
     * Creates a new {@linkSimpleJpaRepository} to manage objects of the given domain type.
     *
     * @paramdomainClassmust not be {@literalnull}.
     * @paramemmust not be {@literalnull}.
     */
    public SimpleJpaRepository(Class<T> domainClass, EntityManager em) {
       this(JpaEntityInformationSupport.getEntityInformation(domainClass, em), em);
    }

    ...

}

그렇게 만들어진 리포지토리 베이스 클래스는 아래와 같이 설정해야 합니다.

@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }

4.7. Publishing Events from Aggregate Roots

리포지토리에 의해 관리되는 엔티티는 애그리거트 루트입니다. DDD 애플리케이션에서, 이러한 애그리거트 루트는 보통 도메인 이벤트를 발행합니다. 스프링 데이터는 @DomainEvents 라 불리는 어노태이션을 제공하며 이 어노태이션은 쉽게 이벤트를 발행하는데 사용될 수 있습니다.

class AnAggregateRoot {

    @DomainEvents 
    Collection<Object> domainEvents() {
        // … return events you want to get published here
    }

    @AfterDomainEventPublication 
    void callbackMethod() {
       // … potentially clean up domain events list
    }
}

@DomainEvents 어노태이션이 붙은 메소드는 Spring Data Repository의 메소드인 save(..), saveAll(..), delete(..) 등이 실행될 때 호출 됩니다, 이 메소드는 이벤트 인스턴스를 반환힙니다.@AfterDomainEventPublications 는 모든 이벤트가 발행된 후에 해당 어노태이션이 붙은 메소드가 호출됩니다. 여러분은 발행된 이벤트의 리스트를 정리하는데 이 메소드를 사용할 수 있습니다.

4.8. Spring Data Extensions

4.8.1. Querydsl Extension

QueryDSL은 Fluent API를 통해 정적 타입의 SQL 유사 쿼리를 구성할 수 있는 프레임워크입니다.

일부 스프링 데이터 모듈은 QuerydslPredicate를 통해 Querydsl과의 통합을 제공합니다.

QueryDSL support를 사용하기 위해서 QuerydslPredicateExecutor 를 리포지토리에서 확장하면 되겠다.

interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}

그렇게 확장된 리포지토리 인터페이스는 아래와 같이 사용할 수 있다.

Predicate predicate = user.firstname.equalsIgnoreCase("dave")
    .and(user.lastname.startsWithIgnoreCase("mathews"));

userRepository.findAll(predicate);
반응형