1. 순수 JPA 기반 Repository 생성하기
(1) 우선 JPA만을 활용한 Repository를 생성하고 이후 Spring Data JPA의 공통 인터페이스(Common Interface)에 대해 알아본 뒤 이전에 생성했던 JPA 기반 Repository를 스프링 데이터 JPA로 적용해 보고 분석을 진행한다.
1-1. MemberJpaRepository 클래스 정의
package study.datajpa.repository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;
import study.datajpa.entity.Member;
import java.util.List;
import java.util.Optional;
@Repository
public class MemberJpaRepository {
@PersistenceContext
private EntityManager em;
public Member save(Member member) {
em.persist(member);
return member;
}
public void delete(Member member) {
em.remove(member);
}
public List<Member> findAll() {
// JPQL
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
public long count() {
return em.createQuery("select count(m) from Member m", Long.class)
.getSingleResult();
}
public Member find(Long id) {
return em.find(Member.class, id);
}
}
1-2. TeamRepository 클래스 정의
package study.datajpa.repository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;
import study.datajpa.entity.Team;
import java.util.List;
import java.util.Optional;
@Repository
public class TeamRepository {
@PersistenceContext
private EntityManager em;
public Team save(Team team) {
em.persist(team);
return team;
}
public void delete(Team team) {
em.remove(team);
}
public List<Team> findAll() {
return em.createQuery("select t from Team t", Team.class)
.getResultList();
}
public Optional<Team> findById(Long id) {
Team team = em.find(Team.class, id);
return Optional.ofNullable(team);
}
public long count() {
return em.createQuery("select count(t) from Team t", Long.class)
.getSingleResult();
}
}
1-3. MemberJpaRepository 테스트 코드 작성
package study.datajpa.repository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import study.datajpa.entity.Member;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
@Rollback(false)
class MemberJpaRepositoryTest {
@Autowired MemberJpaRepository memberJpaRepository;
@Test
public void memberTest() throws Exception {
// Given
Member member = new Member("memberA");
// When
Member savedMember = memberJpaRepository.save(member);
Member findMember = memberJpaRepository.find(savedMember.getId());
// Then
Assertions.assertThat(findMember.getId()).isEqualTo(member.getId());
Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
Assertions.assertThat(findMember).isEqualTo(member);
}
@Test
public void basicCRUD() {
Member member1 = new Member("member1");
Member member2 = new Member("member2");
memberJpaRepository.save(member1);
memberJpaRepository.save(member2);
// 단건 조회 Validation
Member findMember1 = memberJpaRepository.findById(member1.getId()).get();
Member findMember2 = memberJpaRepository.findById(member2.getId()).get();
Assertions.assertThat(findMember1).isEqualTo(member1);
Assertions.assertThat(findMember2).isEqualTo(member2);
// List 조회 Validation
List<Member> all = memberJpaRepository.findAll();
Assertions.assertThat(all.size()).isEqualTo(2);
// Count validation
long count = memberJpaRepository.count();
Assertions.assertThat(count).isEqualTo(2);
// 삭제 검증
memberJpaRepository.delete(member1);
memberJpaRepository.delete(member2);
long deletedCount = memberJpaRepository.count();
Assertions.assertThat(deletedCount).isEqualTo(0);
}
}
- @Transactional의 경우 JPA의 모든 동작들은 트랜잭션 내부에서 진행되어야 하기 때문에 선언되었고 @Rollback(false) 어노테이션의 경우 스프링 부트 테스트는 Repository 관련 테스트가 종료되면 테스트 사항을 커밋하지 않고 롤백한다. 하지만 false 옵션을 주게 되면 테스트 케이스에 대해서도 커밋이 진행되기 때문에 데이터베이스에서 반영된 내용을 바로 확인해 볼 수 있다는 특징을 가지고 있다.
(1) 테스트 결과
- 새로운 Member를 저장하고 해당 엔티티에 대한 이름과 ID의 조회, Member에 대한 기본적인 CRUD 테스트 코드가 통과한 것을 확인해 볼 수 있다.
1-4. MemberJpaRepository, TeamRepository의 특징
(1) 해당 클래스들에 대해 저장, 삭제, 조회 등 기본적인 CRUD 코드가 반복되는 것을 알 수 있다.
(2) 이 문제를 해결하기 위해 수많은 시행착오가 존재했고 이후 이 문제를 공통 인터페이스로 해결을 하게 된다.
2. 공통 인터페이스(Common Interface) 설정
(1) 스프링 데이터 JPA가 제공하는 공통 인터페이스 기능을 사용하기 위해 이 부분을 설정하는 과정을 확인해 보자.
2-1. JavaConfig 설정 (스프링 부트 사용 시 생략 가능)
@SpringBootApplication
@EnableJpaRepositories(basePackages = "study.datajpa.repository")
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
}
(1) 스프링 부트를 사용하면 @SpringBootApplication 어노테이션이 포함된 프로젝트 디렉토리부터 그 하위 디렉토리까지 자동으로 스프링 데이터 JPA를 사용할 수 있도록 설정해 준다.
2-2. 스프링 데이터 JPA가 공통 인터페이스의 구현 클래스를 대신 생성한다.
public interface MemberRepository extends JpaRepository<Member, Long> {
}
(1) 해당 MemberRepository 인터페이스의 경우 구현체가 없어도 정상적으로 동작한다. 어떻게 가능한 걸까?
@Autowired MemberRepository memberRepository;
@Test
public void testMember() throws Exception {
// memberRepository.getClass() = class jdk.proxy2.$Proxy112
System.out.println("memberRepository.getClass() = " + memberRepository.getClass());
(2) 위의 테스트 코드를 보면 구현체가 없음에도 불구하고 @Autowired를 통해 구현체를 직접 주입받고 있다. 이 부분은 현재 스프링 데이터 JPA가 해당 인터페이스를 보고 자바의 동적 프록시 기술을 사용하여 구현체를 생성해서 직접 Injection을 받을 수 있도록 설정을 완료해 준 것이다. 이를 통해 개발자는 해당 구현체를 사용할 수 있다.
(3) 주석으로 된 부분을 보면 프록시 기술을 사용한 것을 알 수 있다.
(4) @Repository 어노테이션 생략 가능
- 해당 인터페이스는 @Repository가 없어도 스프링 컴포넌트 스캔의 대상이 된다. 스프링 데이터 JPA가 자동으로 처리해 주기 때문이다. 추가적으로 @Repository 어노테이션의 경우 스프링의 컴포넌트 스캔은 물론 JPA 예외를 스프링 예외로 전환하는 기능을 가지고 있다.
3. 공통 인터페이스 적용
3-1. 공통 인터페이스 기능 적용해보기 - 스프링 데이터 JPA 기반 MemberRepository
(1) public interface Repository_Name extends JpaRepository<Entity_Type, Identifier_Type> { }
public interface MemberRepository extends JpaRepository<Member, Long> {
}
3-2. MemberRepository 테스트 코드 작성
(1) 이전 MemberJpaRepository에서 사용된 save(), findById, findAll() 등 모든 메서드가 스프링 데이터 JPA의 공통 인터페이스에 선언된 메서드이기 때문에 MemberRepository의 구현 클래스에서도 해당 메서드를 동일하게 모두 사용할 수 있다.
@SpringBootTest
@Transactional
@Rollback(false)
class MemberRepositoryTest {
@Autowired MemberRepository memberRepository;
@Test
public void basicCRUD() {
Member member1 = new Member("member1");
Member member2 = new Member("member2");
memberRepository.save(member1);
memberRepository.save(member2);
// 단건 조회 Validation
Member findMember1 = memberRepository.findById(member1.getId()).get();
Member findMember2 = memberRepository.findById(member2.getId()).get();
Assertions.assertThat(findMember1).isEqualTo(member1);
Assertions.assertThat(findMember2).isEqualTo(member2);
// List 조회 Validation
List<Member> all = memberRepository.findAll();
Assertions.assertThat(all.size()).isEqualTo(2);
// Count validation
long count = memberRepository.count();
Assertions.assertThat(count).isEqualTo(2);
// 삭제 검증
memberRepository.delete(member1);
memberRepository.delete(member2);
long deletedCount = memberRepository.count();
Assertions.assertThat(deletedCount).isEqualTo(0);
}
}
(2) 정상적으로 테스트 케이스 검증이 완료된 것을 확인할 수 있다.
4. 공통 인터페이스 분석
4-1. JpaRepository는 기본적인 CRUD 기능을 제공한다.
@NoRepositoryBean
public interface JpaRepository<T, ID> extends ListCrudRepository<T, ID>, ListPagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
/**
* Flushes all pending changes to the database.
*/
void flush();
/**
* Saves an entity and flushes changes instantly.
*
* @param entity entity to be saved. Must not be {@literal null}.
* @return the saved entity
*/
<S extends T> S saveAndFlush(S entity);
// ...
(1) 우선 JpaRepository 인터페이스를 확인해 보자.
- 스프링 데이터 공통 프로젝트가 존재하고 하위에도 다양한 프로젝트가 존재한다. 해당 스프링 데이터에서 기본적인 CRUD를 제공하고 있고 스프링 데이터 JPA에서는 JPA에 특화된 기능들을 부가적으로 제공하고 있는 것이다.
4-2. 스프링 데이터, 스프링 데이터 JPA 인터페이스의 상속 계층도
(1) 상속 계층도를 보면 스프링 데이터, 스프링 데이터 JPA가 존재한다.
(2) 스프링 데이터는 스프링 데이터 JPA와 별개로 데이터의 속성들이 모두 집합(RDB, NoSQL의 기능 등)되어 있는 공통 영역, 스프링 데이터 JPA는 이러한 공통 기능을 모두 상속받으면서 JPA에 대한 부가적인 기능이 포함된 영역이라고 볼 수 있다.
4-3. 인터페이스의 제네릭 타입(Generic Type)
(1) 제네릭 타입
- T : 엔티티 타입
- ID : 엔티티의 식별자(PK) 타입
- S : 엔티티와 그 자식 레벨 타입
4-4. 주요 메서드
(1) save(T)
- 새로운 엔티티는 저장하고 기존에 존재하던 엔티티는 병합(merge)한다.
(2) delete(T)
- 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove()를 호출하게 된다.
(3) findById(ID)
- 엔티티 하나를 조회한다. 내부에서 EntityManager.find()를 호출하게 된다.
(4) getOne(ID)
- 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference()를 호출한다.
(5) findAll(...)
- 모든 엔티티를 조회힌다. Sort나 Paging 조건을 파라미터로 넘길 수 있다.
4-5. 만약 공통 영역을 벗어난 기능이 필요하다면?
- 문제는 공통 기능이 아닌 공통 영역을 벗어난 영역의 기능이 필요하다면 어떻게 해야 될까? 예를 들어 사용자의 이름으로 엔티티를 조회해야 한다. 아래의 코드를 보자.
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(String username);
}
(1) 위의 코드는 공통 영역을 벗어난 도메인에 특화된 기능이다. 도메인에 특화된 기능들은 공통화가 불가능하다. 해당 인터페이스만을 가지고 이러한 문제를 어떻게 해결할까?
(2) 해당 기능 하나 때문에 JpaRepository의 추상 메서드와 findByUsername() 메서드에 대한 구현체를 만드는 것은 거의 불가능에 가깝다. 하지만 이러한 문제는 스프링 데이터 JPA에서 제공하는 쿼리 메서드 기능을 통해 해결할 수 있다.
5. Reference
※ 해당 포스팅은 InFlearn에서 (전) 우아한형제들, 배달의 민족 서비스 개발팀장(기술이사)으로 재직하셨던 김영한님의 "실전! 스프링 데이터 JPA" 강의를 듣고 공부한 내용을 정리하였습니다.
※ 해당 포스팅에 대해 내용 추가가 필요하다고 생각되면 기존 포스팅 내용에 다른 내용이 추가될 수 있습니다.
개인적으로 공부하며 정리한 내용이기에 오타나 틀린 부분이 있을 수 있으며, 이에 대해 댓글로 알려주시면 감사하겠습니다!
'백엔드(Back-End) > JPA(Java Persistence API)' 카테고리의 다른 글
[Spring Data JPA] - 쿼리 메서드 기능, JPA NamedQuery, Repository 사용자 정의 쿼리 메서드 (0) | 2023.10.11 |
---|---|
[Spring Data JPA] - Spring Data, Spring Data JPA는 무엇일까? (0) | 2023.10.06 |
[JPA] - 객체와 테이블 매핑, 필드와 컬럼 매핑, 기본 키 매핑 (0) | 2023.10.05 |
[JPA] - 영속성 컨텍스트(Persistence Context), Entity의 생명 주기(Life-Cycle), 영속성 컨텍스트의 다양한 이점 정리 (2) | 2023.09.25 |
[JPA] - JPA란 무엇일까? JPA를 통해 해결할 수 있는 문제들 (0) | 2023.09.19 |
댓글