내가 공부한 것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼 겸 상세히 기록하고 얕은 부분들은 가볍게 포스팅하겠습니다.
이번 시간에는 우선 순수한 JPA기반의 repository를 만들어 본 후, 이를 점차 Spring Data JPA를 사용하도록 변경할 것 이다.
1. 순수 JPA 기반 리포지토리 만들기
우선 Team Repository의 코드는 다음과 같다.
@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();
}
}
다음으로 Member Repository의 코드는 다음과 같다.
@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() {
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);
}
}
간단하게 작성한 테스트 코드는 다음과 같다.
@Transactional
@SpringBootTest
@Rollback(false)
class MemberJpaRepositoryTest {
@Autowired MemberJpaRepository memberJpaRepository;
@Test
public void basicCRUD() {
Member member1 = new Member("member1");
Member member2 = new Member("member2");
memberJpaRepository.save(member1);
memberJpaRepository.save(member2);
Member findMember1 = memberJpaRepository.findById(member1.getId()).get();
Member findMember2 = memberJpaRepository.findById(member2.getId()).get();
assertThat(findMember1).isEqualTo(member1);
assertThat(findMember2).isEqualTo(member2);
List<Member> all = memberJpaRepository.findAll();
assertThat(all.size()).isEqualTo(2);
long count = memberJpaRepository.count();
assertThat(count).isEqualTo(2);
memberJpaRepository.delete(member1);
memberJpaRepository.delete(member2);
long deleteCount = memberJpaRepository.count();
assertThat(deleteCount).isEqualTo(0);
}
}
Member, Team Repository만 해도 기본적인 CRUD 기능 구현에 있어서 코드의 중복이 많은 상황이다.
이러한 중복된 코드 작성을 줄일수 있는 방법을 다음 단락부터 알아가 보자!
2. 공통 인터페이스 설정
원래 기본적으로는 다음 코드와 같이 JavaConfig를 통해 basePackages 를 설정해줘야 한다.
@Configuration
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
public class AppConfig {}
하지만, SpringBoot를 사용한다면 자동 설정되기 때문에 설정할필요가 없다!!
스프링부트 사용시 @SpringBootApplication가 추가된 Class의 경로를 basePackeages로 지정한다. (해당 패키지와 하위 패키지 인식)
만약 페키지 위치가 많이 달라지면 @EnableJpaRepositories 을 직접 사용하면 된다.
Spring Data JPA를 통해서 만든 MemberRepository 코드는 다음과 같다.
public interface MemberRepository extends JpaRepository<Member, Long> {
}
인터페이스만 있고, 구현부분이 없는 상황이다. 하지만 정상적으로 잘 작동한다.
구현체가 없는데 어떻게 동작하는 것 일까?
test코드에서 이를 직접 출력해보자.
@Transactional
@SpringBootTest
@Rollback(false)
class MemberRepositoryTest {
@Autowired MemberRepository memberRepository;
@Test
public void member_test() {
System.out.println("memberRepository = " + memberRepository.getClass());
}
}
결과는 다음과 같다.
프록시가 등록되고 있었던 것 이다.
Spring이 interface를 보고 Proxy 객체를 삽입하고 있었던 것 이다.
인터페이스만 설정해두면, Spring Data JPA가 구현체를 만들어 주입해준다.
Spring Data JPA가 에플리케이션 로딩 시점에 Spring Data JPA 관련된 인터페이스를 가지고 있으면, 구현 클래스를 자기가 다 만든다.
@Repository 에노테이션 생략 가능
- 컴포넌트 스캔을 스프링 데이터 JPA가 자동으로 처리
- JPA 예외를 스프링 예외로 변환하는 과정도 자동으로 처리
3. 공통 인터페이스 적용
이전 시간에 순수 JPA를 통해 구현한 MemberJpaRepository를 Spring Data JPA로 구현해 보자.
public interface MemberRepository extends JpaRepository<Member, Long> {
}
기존 순수 JPA 기반 테스트에서 사용했던 코드를 그대로 스프링 데이터 JPA 리포지토리 기반 테스트로 변경해도 동일한 방식으로 동작.
다음 테스트 코드를 살펴보자.
@Transactional
@SpringBootTest
@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);
Member findMember1 = memberRepository.findById(member1.getId()).get();
Member findMember2 = memberRepository.findById(member2.getId()).get();
assertThat(findMember1).isEqualTo(member1);
assertThat(findMember2).isEqualTo(member2);
List<Member> all = memberRepository.findAll();
assertThat(all.size()).isEqualTo(2);
long count = memberRepository.count();
assertThat(count).isEqualTo(2);
memberRepository.delete(member1);
memberRepository.delete(member2);
long deleteCount = memberRepository.count();
assertThat(deleteCount).isEqualTo(0);
}
}
4. 공통 인터페이스 분석
위에서 계속 사용해왔던 JpaRepository interface를 열어보면 다음과 같다.
T 라는 type 과 ID 라는 식별자의 type을 받게된다.
또한 JpaRepository는 PagingAndSoringRepository를 상속받고 있는데, 상속도는 다음과 같다.
공통적인 기능은 Spring Data에 있고, 좀더 JPA에 특화된 Repository는 Spring Data JPA 에 있다.
주의사항
T findOne(ID) -> Optional<T> findById(ID) 로 변경
예전에는 findOne 호출시 객체를 직접 반환했지만, 요즘은 Optional로 감싸도록 변경되었다.
제네릭 타입
- T : 엔티티
- ID : 엔티티의 식별자 타입
- S : 엔티티와 그 자식 타입
주요 메서드
- save(S) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합한다.
- delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 호출
- findById(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 호출
- getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference() 호출
- findAll(...) : 모든 엔티티를 조회한다. 정렬( Sort )이나 페이징( Pageable ) 조건을 파라미터로 제공할 수 있다.
'BackEnd > JPA' 카테고리의 다른 글
[JPA] 쿼리 메소드 기능 - 2 (0) | 2022.05.04 |
---|---|
[JPA] 쿼리 메소드 기능 - 1 (0) | 2022.05.03 |
[JPA] 예제 도메인 모델 (0) | 2022.05.02 |
[JPA] Open Session In View (OSIV) (0) | 2022.04.24 |
[JPA] 컬렉션 조회 최적화 - 4 (0) | 2022.04.24 |
댓글