본문 바로가기
Spring JPA

스프링 데이터 JPA - 15

by 홍굴이 2021. 7. 31.

데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있다.

 

JpaRepository 계층 구조

  • save(S) : 새로운 엔티티를 저장하고 이미 있는 엔티티는 수정한다.
  • delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove()를 호출한다.
  • findOne(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find()를 호출한다.
  • getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference()를 호출한다.
  • findAll(...) : 모든 엔티티를 조회한다. 정렬이나 페이징조건을 파라미터로 제공할 수 있다.

쿼리 메소드 기능

메소드 이름만으로 쿼리를 생성하는 기능 -> 인터페이스에 메소드만 선언하면 해당 메소드의 이름으로 적절한 JPQL 쿼리를 생성해서 실행한다.

  • 메소드 이름으로 쿼리 생성
  • 메소드 이름으로 JPA NamedQuery 호출
  • @Query 어노테이션을 사용해서 레파지토리 인터페이스에 쿼리 직접 정의

메소드 이름으로 쿼리 생성

public interface UserRepository extends JpaRepository<UserEntity, Long> {
    List<UserEntity> findByName(String name);
}

인터페이스에 정의한 메소드를 실행하면 스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행한다.

select user from UserEntity user where user.name = ?1

메소드 이름으로 JPA NamedQuery 호출

JPA Named 쿼리는 이름 그대로 쿼리에 이름을 부여해서 사용하는 방법이다.

 

스프링 데이터 JPA는 선언한 도메인 클래스 + .(점) + 메소드 이름 으로 Named 쿼리를 찾아서 실행한다.

public interface UserRepository extends JpaRepository<UserEntity, Long> {
    List<UserEntity> findByName(@Param("name) String name);
}

@Query 어노테이션을 사용해서 레파지토리 인터페이스에 쿼리 직접 정의

실행할 메소드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있다.

JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있는 장점이 있다.

public interface UserRepository extends JpaRepository<UserEntity, Long> {

    @Query("select user from UserEntity user where user.name = ?1")
    List<UserEntity> findByName(String name);
    
}

 


파라미터 바인딩

위치 기반 파라미터 바인딩, 이름 기반 파라미터 바인딩 둘 다 지원한다.

public interface UserRepository extends JpaRepository<UserEntity, Long> {

    @Query("select user from UserEntity user where user.name = :name")
    List<UserEntity> findByName(@Param("name") String name);
    
}

벌크성 수정 쿼리

Modifying 어노테이션을 사용해서 벌크성 수정, 삭제 쿼리를 작성할 수 있다.

벌크성 쿼리를 실행하고 영속성 컨텍스트를 수정하고 싶다면 @Modifying(clearAutomatically = true)를 하면 된다.

public interface UserRepository extends JpaRepository<UserEntity, Long> {

    @Modifying
    @Query("update ProductEntity product set product.price 
    	= product.price * 1.1 where product.stockAmount < :stockAmount")
    int bulkPriceUp(@Param("stockAmount") String stockAmount);
    
}

 


반환 타입

결과가 한 건 이상이면 컬렉션 인터페이스를 사용하고, 단건이면 반환 타입을 지정한다.

List<UserEntity> findByName(String name);
UserEntity findByIdentity(String identity);

조회결과가 없으면 컬렉션은 빈 컬렉션을 단건은 null을 반환한다.


페이징과 정렬

스프링 데이터 JPA는 쿼리 메소드에 페이징과 정렬 기능을 사용할 수 있도록 2가지 파라미터를 제공한다.

  • org.springframework.data.domain.Sort : 정렬 기능
  • org.springframework.data.domain.Pageable : 페이징 기능(내부에 Sort 포함)
public interface UserRepository extends JpaRepository<UserEntity, Long> {
    
    //count 쿼리 사용
    Page<UserEntity> findByName(String name, Pageable pageable);
    
    //count 쿼리 사용 안함
    List<UserEntity> findByName(String name, Pageable pageable);
    
    List<UserEntity> findByName(String name, Sort sort);
    
}

예제 코드

@Test
void Page사용테스트코드() {
    //given
    UserEntity userEntity1 = userRepository.save(UserEntity.builder()
            .name("홍영준")
            .build());

    UserEntity userEntity2 = userRepository.save(UserEntity.builder()
            .name("이재범")
            .build());

    UserEntity userEntity3 = userRepository.save(UserEntity.builder()
            .name("라영지")
            .build());

    Pageable pageable = PageRequest.of(0, 10, Sort.by("name").descending());

    Page<UserEntity> userEntityPage = userRepository.findByNameStartingWith("홍", pageable);
        
    assertEquals(userEntityPage.getTotalPages(), 1);
    assertEquals(userEntityPage.getContent().get(0).getName(), userEntity1.getName());
}

명세

명세를 이해하기 위한 핵심 단어는 술어(Predicate) -> 참이나 거짓으로 평가된다.

함께 검색할 검색의 조합이 다양해지면 만들어야 하는 쿼리 메서드도 많아진다.

이럴 때 명세(Specification)를 이용하면 원하는 조건을 상황에 맞게 선택하여 추가할 수 있다.

 

상속 추가

public interface UserRepository extends JpaRepository<UserEntity, Long>, 
	JpaSpecificationExecutor<UserEntity> {
}

JpaSpecificationExecutor 인터페이스

public interface JpaSpecificationExecutor<T> {

    Optional<T> findOne(@Nullable Specification<T> spec);
    List<T> findAll(@Nullable Specification<T> spec);
    Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
    List<T> findAll(@Nullable Specification<T> spec, Sort sort);
    long count(@Nullable Specification<T> spec);
    
}

 

UserEntitySpecification 정의

public class UserEntitySpecification {
    public static Specification<UserEntity> userName(String name) {
        return new Specification<UserEntity>() {
            @Override
            public Predicate toPredicate(Root<UserEntity> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
                return criteriaBuilder.equal(root.get("name"), name);
            }
        };
    }
}

테스트 코드

@Test
void 명세테스트코드() {

    UserEntity userEntity = userRepository.save(UserEntity.builder()
            .identity("identity1")
            .name("홍영준")
            .build());

    List<UserEntity> userEntityList = userRepository.findAll(where(userName("홍영준")));

    assertEquals(userEntityList.get(0).getName(), userEntity.getName());
}

명세는 검색조건을 추상화해 새로운 검색조건을 쉽게 만들 수 있다.

하지만 결국엔 JPA Criteria를 사용하여 만드는 것이기 때문에 코드가 복잡해지면 이해하기 어려워질 수 있다.

결국엔 QueryDSL을 사용하는 것을 권한다.


사용자 정의 레파지토리 구현

스프링 데이터 JPA로 레파지토리를 개발하면 인터페이스만 정의하고 구현체는 만들지 않는다.

하지만 다양한 이유로 메소드를 직접 구현해야 할 때도 있다. 

스프링 데이터 JPA는 필요한 메소드만 구현할 수 있는 방법을 제공한다.

 

사용자 정의 인터페이스

public interface UserRepositoryCustom {
    List<UserEntity> findUserEntityCustom();
}

Custom 인터페이스를 다음과 같이 상속한다.

public interface UserRepository extends JpaRepository<UserEntity, Long>, UserRepositoryCustom {
}

구현체

public class UserRepositoryImpl implements UserRepositoryCustom {
    @Override
    public List<UserEntity> findUserEntityCustom() {
    	...
    }
}

Web 확장

식별자로 도메인 클래스를 바로 바인딩해주는 도메인 클래스 컨버터 기능과, 페이징과 정렬 기능을 제공한다.

도메인 클래스 컨버터는 HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩한다.

받는 파라미터에 엔티티를 직접적으로 매칭하면 자동으로 조회를 해준다.

 

@EnableSpringDataWebSupport 어노테이션을 등록하면 도메인 클래스 컨버터와 페이징과 정렬을 위한 HandlerMethodArgumentResolver가 스프링 빈으로 등록된다.


도메인 클래스 컨버터 기능

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
public class WebAppConfig {
    ...
}

 

컨트롤러

@Controller
public class UserController {

    @RequestMapping("user/userUpdateForm")
    public String userUpdateForm(@RequestParam("id") UserEntity userEntity, Model model) {
    	model.addAttribute("userEntity", userEntity);
        return "user/userSaveForm";
    }
    
}

HTTP 요청으로 회원 아이디(id)를 받지만 도메인 클래스 컨버터가 중간에 동작해서 아이디를 회원 엔티티 객체로 변환해서 넘겨준다.

 

※ 주의

도메인 클래스 컨버터를 통해 넘어온 회원 엔티티를 컨트롤러에서 직접 수정해도 실제 데이터베이스에 반영되지 않는다.

즉, 트랜잭션이 없는 범위에서 엔티티를 조회한 것이 때문에 영속성 컨텍스트가 관리하지 않기 때문이다.


페이징과 정렬 기능

스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있도록 HandlerMethodArgumentResolver를 제공한다.

  • 페이징 기능 : PageableHandlerMethodArgumentResolver
  • 정렬 기능 : SortHandlerMethodArgumentResolver
@Controller
public class UserController {

    @RequestMapping("/users", method = RequestMethod.GET)
    public String pagingUsers(Pageable pageable, Model model) {
    	Page<UserEntity> userEntityPage = userService.findUsers(pageable);
        model.addAttribute("users", userEntityPage.getContent());
        return "users/userList";
    }
    
}

파라미터로 Pageable을 받은 것을 확인할 수 있다.

Pageable은 인터페이스이므로 실제로는 PageRequest 객체가 생성된다.

사용자는 쿼리 스트링으로 데이터를 매칭해서 보내야한다.

 

쿼리 스트링 예시

/users?page=0&size=10&sort=id,desc&sort=username,desc


스프링 데이터 JPA가 사용하는 구현체

스프링 데이터 JPA가 제공하는 공통 인터페이스는 org.springframework.data.jpa.repository.support.SimpleJpaRepositroy 클래스가 구현한다.

 

SimpleJpaRepository 클래스

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

    @Transactional
    @Override
    public <S extends T> S save(S entity) {  
        Assert.notNull(entity, "Entity must not be null.");

        if (entityInformation.isNew(entity)) {	
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        }
    }
    
    ...
    
}

 

JpaRepositoryImplementation 인터페이스

@NoRepositoryBean
public interface JpaRepositoryImplementation<T, ID> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
	...
}
  • @Repository 적용 : JPA 예외를 스프링이 추상화한 예외로 변환한다.
  • @Transactional 트랜잭션 적용 : JPA의 모든 변경이 트랜잭션 안에서 이루어져야 한다.
  • @Transactional (readOnly = true) : 데이터를 조회하는 메소드에서는 readOnly = true 옵션이 적용된다. 데이터를 변경하지 않는 트랜잭션에서 이 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있다.
  • save() 메소드 : 이 메소드는 저장할 엔티티가 새로운 엔티티면 저장(persist)하고 이미 있는 엔티티면 병합(merge) 한다.

'Spring JPA' 카테고리의 다른 글

컬렉션과 부가 기능 - 17  (0) 2021.08.10
애플리케이션과 영속성 관리 - 16  (0) 2021.08.08
객체 지향 쿼리 언어 - 14  (0) 2021.07.21
객체지향 쿼리 언어 - 13  (0) 2021.07.17
값 타입 - 12  (0) 2021.07.16

댓글