김영한 "실전! 스프링 데이터 JPA "을 최근에 다시 들었다.
내용을 전부 정리하기 보다는 챕터별로 중요하게 느낀 내용 위주로 간략하게 정리하는게 목표다. 

 

세팅에 대한 설명 

일반 JPA세팅하듯이 일단 진행(jpa의존성와 DB의존성 추가, application.yml에 DB연동 설정,  jpa 동작방식 설정,  p6spy 스프링부트 버전에 맞게 설정)

참고 : 스프링부트를 사용하면 persistence.xml도 없고 LocalContainerEntityManagerFactoryBean도 없다.

 

예제 도메인 모델 

실무에서는 Setter사용하지 말기, 기본 생성자 막고 싶어도 protected로 열어 두어야햠 (롬복으로 생성가능)

 

공통 인터페이스 기능 

순수 JPA로 리포지토리를 만들 경우 -> 반복되는 코드를 계속 작성해줘야한다. 

//순수 JPA의 CRUD리포지토리
 @Repository
 public class MemberJpaRepository {
	@PersistenceContext
     private EntityManager em;
     public Member save(Member member) {
         em.persist(member);
         return member;
}
 	// CRUD ~~~ 쭉 작성 다른 엔티티도 마찬가지로 반복되는 CRUD코드가 존재~~! 
}

 

 

스프링 Data JPA를 활용할 경우 리포지토리가 생성되는 방식

구현클래스를 개발자가 구현하지 않아도 된다. 반복적인 코드를 개발자가 작성하지 않아도 된다.

 

스프링 Data JPA로 리포지토리 인터페이스를 말들 경우 -> 구현클래스는 Spring Data JPA가 생성

public interface MemberRepository extends JpaRepository<Member, Long> {
 }

// extends JpaRepository<Member, Long>를 확장해서 인터페이스 생성 Member = 엔티티클래스 / Long = id(=PK) 타입

// 스프링 JPA가 만들어준 리포지토리 구현클래스 테스트 코드 예시 잘 작동한다. 
@SpringBootTest
@Transactional
public class MemberRepositoryTest {
    @Autowired
    MemberRepository memberRepository;
    @Test
    public void testMember() {
        Member member = new Member("memberA");
        Member savedMember = memberRepository.save(member);
        Member findMember =
memberRepository.findById(savedMember.getId()).get();
        Assertions.assertThat(findMember.getId()).isEqualTo(member.getId());
Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
Assertions.assertThat(findMember).isEqualTo(member); //JPA 엔티티 동일성 보장 }
}

 

인터페이스 상속 구조

SPING Data JPA를 활용하니까 CRUD리포지토리를 개발자가 구현하지 않아도 된다. 대부분의 공통 메서드를 제공한다. 

 

참고 : JPA 수정은 변경감지 기능을 이용하도록하자 트랜잭션 안에서 엔티티를 조회한 다음에 데이터를 변경하면, 트랜잭션 종료 시점에 변경감지 기능이 작동해서 변경된 엔티티를 감지하고 UPDATE SQL을 실행한다.

 

쿼리 메서드 기능

 

특정 요소로 조회할 때 이건 공통사항이 아니기 때문에 개발자가 여전히 만들어할 것이다. 이 부분에 대해서도 제공해주는 기능이 있음!


쿼리 메소드 기능 3가지 

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

 

순수 JPA 리포지토리였다면  JPQL로 작성했을 것이다. 하지만 스프링 데이터 JPA라면 

쿼리 메소드 

 public interface MemberRepository extends JpaRepository<Member, Long> {
     List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
//스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 요청받으면 실행

 

스프링 데이터 JPA가 제공하는 쿼리 메소드 기능

조회: find...By ,read...By ,query...By get...By,
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query- methods.query-creation
예:) findHelloBy 처럼 ...에 식별하기 위한 내용(설명)이 들어가도 된다.
COUNT: count...By 반환타입 `long`
EXISTS: exists...By 반환타입 `boolean`
삭제: delete...By, remove...By 반환타입 `long` DISTINCT: findDistinct, findMemberDistinctBy LIMIT: findFirst3, findFirst, findTop, findTop3

 

참고 :  쿼리 메서드을 사용하면 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야 한다. 그렇지 않으면 애플리케이션 시작하는 시점에 오류가 발생한다. 

 

 

JPA NamedQuery

@NamedQuery로 엔티티 위에 작성 > JPA를 직접 사용해서 repository에서 Named 쿼리 호출 (나의 개인적으로 별로인듯 )

자동으로 찾아주는 기능 존재 (스프링 데이터 JPA는 선언한 "도메인 클래스 + .(점) + 메서드 이름"으로 Named 쿼리를 찾아서 실행)

 

참고 : 스프링 데이터 JPA를 사용하면 실무에서 Named Query를 직접 등록해서 사용하는 일은 드물다. 대신 @Query를 사용해서 리파지토리 메소드에 쿼리를 직접 정의한다. 

 

@Query 리포지토리 메소드에 쿼리 정의하기

  • `@org.springframework.data.jpa.repository.Query` 어노테이션을 사용 
  • 실행할 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있음
  • JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있음(매우 큰 장점!)
public interface MemberRepository extends JpaRepository<Member, Long> {
     // @Query 기본
     @Query("select m from Member m where m.username= :username and m.age = :age")
     	List<Member> findUser(@Param("username") String username, @Param("age") int
     age);

	
    //DTO 바인딩 제공
	//  DTO 경로 다 적어줘서 귀찮지만 해주자. 
    // !! DTO에는 생성자가 맞는 DTO가 필요하다.(JPA와 사용방식이 동일하다.)
	@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) " +
         "from Member m join m.team t")
	List<MemberDto> findMemberDto(); 
    
    //컬렉션 파라미터 바인딩 결과 없으면 참고로 컬랙션은 빈컬랙션 반환 null아니다! 
    //`Collection` 타입으로 in절 지원
    @Query("select m from Member m where m.username in :names")
    List<Member> findByNames(@Param("names") List<String> names);

	//단건 조회는 없으면 null 2건이상 javax.persistence.NonUniqueResultException 발생
}

파라미터 바인딩은 코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하자 (위치기반은 순서 실수가 바꾸면...)

 

참고: 실무에서는 메소드 이름으로 쿼리 생성 기능은 파라미터가 증가하면 메서드 이름이 매우 지저분해진다. 따 라서 `@Query` 기능을 자주 사용하게 된다.

 

페이징

순수 JPA라면  검색 조건으로 조회하는 JPQL(검색 조건 + 정렬 조건 + 페이징 조건)과 Count를 위한 JPQL코드 작성  

 

스프링 데이터 JPA 페이징과 정렬 

페이징과 정렬 파라미터

org.springframework.data.domain.Sort : 정렬 기능

org.springframework.data.domain.Pageable : 페이징 기능 (내부에 `Sort` 포함)
특별한 반환 타입 지원 
`org.springframework.data.domain.Page` : 추가 count 쿼리 결과를 포함하는 페이징 `org.springframework.data.domain.Slice` : 추가 count 쿼리 없이 다음 페이지만 확인 가능(내부적으
로 limit + 1조회)
`List` (자바 컬렉션): 추가 count 쿼리 없이 결과만 반환

 

메서드 생성 및  카운트 쿼리 사용여부 

// 반환 타입에 따른 Count 쿼리 사용 여부
Page<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 
Slice<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함 
List<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함 
List<Member> findByUsername(String name, Sort sort);

// Page 사용 예저 정의 코드 Count 쿼리를 사용한다. 
public interface MemberRepository extends Repository<Member, Long> {
     Page<Member> findByAge(int age, Pageable pageable);
}

 

사용하기 

//페이징 조건과 정렬 조건 설정
@Test
public void page() throws Exception {
	//given
     memberRepository.save(new Member("member1", 10));
     memberRepository.save(new Member("member2", 10));
     memberRepository.save(new Member("member3", 10));
     memberRepository.save(new Member("member4", 10));
     memberRepository.save(new Member("member5", 10));
 	 //when
     PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC,
     "username"));
     Page<Member> page = memberRepository.findByAge(10, pageRequest);
     //then
     List<Member> content = page.getContent(); //조회된 데이터 
     assertThat(content.size()).isEqualTo(3); //조회된 데이터 수 
     assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수 
     assertThat(page.getNumber()).isEqualTo(0); //페이지 번호 
     assertThat(page.getTotalPages()).isEqualTo(2); //전체 페이지 번호 
     assertThat(page.isFirst()).isTrue(); //첫번째 항목인가? 
     assertThat(page.hasNext()).isTrue(); //다음 페이지가 있는가?
}

두 번째 파라미터로 받은 `Pageable` 은 인터페이스다. 따라서 실제 사용할 때는 해당 인터페이스를 구현한 `org.springframework.data.domain.PageRequest` 객체를 사용한다.
`PageRequest` 생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력
한다. 여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다. 참고로 페이지는 0부터 시작한다

참고 Page는 0부터

 

Slice 인터페이스는 다음이 있는지만 단순히 알려준다 궁금하면 찾아보는 걸로 하자!

 

스프링 데이터 JPA를 사용한 벌크성 수정 쿼리

벌크성 쿼리를 사용하고 나서 영속성 컨텍스트를 초기화 하자! 영속성컨텍스트에과거값이남아서문제가될수있다.

@Modifying(clearAutomatically = true) // 벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션 사용 사용하지 않으면 예외발생!!!!
 @Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

 

 

@EntityGraph

엔티티 그래프는 사실상 패치 조인의 간편 버전. LEFT OUTER JOIN을 사용한다. 

 

배경

지연로딩을 하고 조회해서 다른 엔티티에 접근하면 N + 1문제가 발생 가능하다. (다른 엔티티들은 사용할 때 가져오니까 그래도 지연로딩은 필수로 걸고 필요한 경우 함께 가져오는 방식으로 구현하는게 맞다.)  해결법중에 하나는 fetchJoin이 있다.

 

//공통 메서드 오버라이드
@Override
@EntityGraph(attributePaths = {"team"}) List<Member> findAll();

//JPQL + 엔티티 그래프 
@EntityGraph(attributePaths = {"team"}) 
@Query("select m from Member m") 
List<Member> findMemberEntityGraph();

//메서드 이름으로 쿼리에서 특히 편리하다. 
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username)

 

 

확장 기능

 

사용자 정의 리포지토리 구현 

스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동 생성 

스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많음 

다양한 이유로 인터페이스의 메서드를 직접 구현하고 싶다면?

  • JPA 직접 사용( `EntityManager` ) 
  • 스프링 JDBC Template 사용 
  • MyBatis 사용
    데이터베이스 커넥션 직접 사용 등등... 
  • Querydsl 사용

Auditing

엔티티들에게 공통 필드를 적용하고 싶다면 Auditing 기능을 활용하자. 

  • 등록일
  • 수정일
  • 등록자
  • 수정자

참고 : 수정자는 최초에 등록할 때 등록자와 함께 초기화 추천 

 

Web확장  

도메인 클래스 컨버터

HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩

페이징과 정렬

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

글로벌 페이지 사이즈 최대 페이지 사이즈 수정가능 (개별도 가능 )

@GetMapping("/members")
 public Page<Member> list(Pageable pageable) {
     Page<Member> page = memberRepository.findAll(pageable);
     return page;
 }
 
 요청 파라미터**
예) `/members?page=0&size=3&sort=id,desc&sort=username,desc`
page: 현재 페이지, **0부터 시작한다.**
size: 한 페이지에 노출할 데이터 건수
sort: 정렬 조건을 정의한다. 예) 정렬 속성,정렬 속성...(ASC | DESC), 정렬 방향을 변경하고 싶으면 `sort` 파라 미터 추가 ( `asc` 생략 가능)

 

스프링 데이터 JPA 분석 

`@Repository` 적용: JPA 예외를 스프링이 추상화한 예외로 변환 

  • `@Transactional` 트랜잭션 적용
  • JPA의 모든 변경은 트랜잭션 안에서 동작
  • 스프링 데이터 JPA는 변경(등록, 수정, 삭제) 메서드를 트랜잭션 처리
  • 서비스 계층에서 트랜잭션을 시작하지 않으면 리파지토리에서 트랜잭션 시작
  • 서비스 계층에서 트랜잭션을 시작하면 리파지토리는 해당 트랜잭션을 전파 받아서 사용
  • 그래서 스프링 데이터 JPA를 사용할 때 트랜잭션이 없어도 데이터 등록, 변경이 가능했음(사실은 트랜잭션 이 리포지토리 계층에 걸려있는 것임

 

나머지 기능들

Specifications

Query By Example

Projections

네티이브 쿼리

Projections + 네이티브 쿼리 (이것만 한번 찾아서 보든하자)

 

장단점이 존재하지만 한계가 명확해서 QueryDsl을 먼저 배우고 고민하는게 좋을 듯하다. 

 

 

+ Recent posts