백엔드/스프링 데이터 JPA

[Spring Data JPA] [3] 공통 인터페이스 기능

RE-Heat 2023. 9. 8. 23:36

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84/dashboard

 

실전! 스프링 데이터 JPA - 인프런 | 강의

스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제공합니다.

www.inflearn.com

인프런 김영한 님의 강의를 듣고 작성한 글입니다.

 

[1] 순수 JPA 기반 리포지토리 만들기

 

■ 기본 CRUD

  • 저장
  • 변경 - 변경감지사용 
  • 삭제
  • 전체조회 
  • 단건조회 
  • 카운트

수정(update)은 JPA가 제공하는 변경 감지 기능(dirty checking)을 쓰면 된다. 트랜잭션 안에서 엔티티를 조회한 다음 데이터를 변경하면 트랜잭션 종료 시점에 변경 감지 기능이 작동해 update SQL을 실행한다.

 

MemberJpaRepository

@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {
    private final EntityManager em;

    //Create(저장)
    public Member save(Member member){
        em.persist(member);
        return member;
    }

    //Delete(삭제)
    public void delete(Member member){
        em.remove(member);
    }

    //Read(조회)
    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);
    }
}
  • @PersistenceContext를 쓰는 대신 생성자로 주입함
  • 저장/삭제/전체조회/단건조회/카운트 기능을 추가
  • JPQL은 count() 같은 SQL도 지원한다.
  • Optional.ofNullable은 null인지 아닌지 확신할 수 없는 객체를 담고 있는 Optional 객체를 생성한다.
    • findById를 했을 때 null값이 올 수도 있으므로 스프링 데이터 JPA도 Optional<> 객체로 받는다.
    • 참고로 Optional이 담고 있는 객체에 접근하는 방법은 get()이다.

 

TeamJpaRepository

@Repository
@RequiredArgsConstructor
public class TeamJpaRepository {

    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();
    }
}
  • 회원 리포지토리와 거의 동일하다.

 

MemberJpaRepositoryTest

    @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 deletedCount = memberJpaRepository.count();
        assertThat(deletedCount).isEqualTo(0);
    }
  • Optional로 꺼내면 귀찮으니 있다고 가정하고 편의상 get()으로 꺼냄

 

참고] 변경감지로 update SQL 날아가는지 확인

 

실행결과

변경감지를 통해 update SQL이 실행되는 걸 알 수 있다.

 

[2] 공통 인터페이스 설정

스프링 데이터 JPA가 제공하는 공통 인터페이스를 사용해 CRUD 코드를 간단히 해결!

 

■ 스프링 데이터 JPA가 구현 클래스 대신 생성

  • 개발자가 interface만 선언해 주면 스프링 데이터 JPA가 자동으로 구현 클래스를 만들어 준다.

 

  • org.springframework.data.repository.Repository를 구현한 클래스는 스캔대상 
    • MemberRepository 인터페이스가 동작한 이유 
    • 실제 출력해 보기(Proxy)
    • memberRepository.getClass()     class co m.sun.proxy.$ProxyXXX

 

[3] 공통 인터페이스 적용

MemberRepository - interface

public interface MemberRepository extends JpaRepository<Member, Long> {
}
  • JpaRepository 상속함
  • @Repository 애노테이션을 생략해도 된다.
    • 컴포넌트 스캔을 스프링 데이터 JPA가 자동으로 처리 
    • JPA 예외를 스프링 예외로 변환하는 과정도 자동으로 처리

 

MemberRepositoryTest

    @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 deletedCount = memberRepository.count();
        assertThat(deletedCount).isEqualTo(0);
    }
  • 순수 JPA로 만든 Test와 거의 유사하다. memberJpaRepository -> memberRepository로 바뀐 게 차이점
  • 이유는 스프링 데이터 JPA의 메소드 이름과 맞춰 놓았기 때문
  • 이제 순수 JPA를 만들 때처럼 일일이 구현하지 않아도 간편하게 쓸 수 있다.

 

[4] 공통 인터페이스 분석

  • JpaRepository 인터페이스: 공통 CRUD 제공 
  • 제네릭은 <엔티티타입, 식별자타입> 설정

 

■ 공통 인터페이스 구성

  • JpaRepository는 JPA 특화
  • common 부분은 몽고 DB로 바꿔도 사용할 수 있다고는 하지만, 실무에선 적용하기 어렵다.

 

■ 제네릭 타입

  • T: 엔티티
  • ID: 엔티티의 식별자 타입
  • S: 엔티티와 그 자식 타입

■ 주요 메서드

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