JPA

공통 인터페이스 기능

경딩 2024. 12. 7. 19:49

순수 JPA 기반 리포지토리 만들기

 

순수한 JPA 기반 레파지토리를 만들기

기본 CRUD

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

 

참고 : JPA 에서 수정은 변경감지 기능을 사용하면 된다.

트랜잭션 안에서 엔티티를 조회한 다음에 데이터를 변경하면 , 트랜잭션 종료 시점에 변경 감지 기능이 작동해서 변경된 엔티티를 감지하고 update sql 을 실행한다.

 

순수 JPA 기반 리포지토리 - 회원

package study.data_jpa.repository;


import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import org.springframework.stereotype.Repository;
import study.data_jpa.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 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 List<Member> findAll() {
        // JPQL 테이블이 아닌 객체를 대상으로 하는 쿼리
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }

    public Member find(Long id) {
        return em.find(Member.class, id);
    }

}

 

 

 

순수 JPA 기반 리포지토리 - 팀

 

package study.data_jpa.repository;


import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;
import study.data_jpa.entity.Team;

import java.util.List;
import java.util.Optional;

@Repository
public class TeamJPARepository {

    @PersistenceContext
    private EntityManager em;

    public Team save(Team team){
        em.persist(team);
        return team;
    }

    public void delete(Team team) {
        em.remove(team);
    }

    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();
    }

    public List<Team> findAll(){
        return  em.createQuery("select t from Team t", Team.class)
                .getResultList();
    }

    public Team find(Long id){
        return em.find(Team.class, id);
    }
}

회원 레포지토리와 거의 동일하다.

 

순수 JPA 기반 리포지토리 테스트

package study.data_jpa;

import jakarta.transaction.Transactional;
import org.junit.jupiter.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 study.data_jpa.entity.Member;
import study.data_jpa.repository.MemberJpaRepository;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;


@SpringBootTest
@Transactional
@Rollback(value = false)
class MemberJpaRepositoryTest {

    @Autowired
    MemberJpaRepository memberJpaRepository;

    @Test
    public void testMember(){
        Member member = new Member("memberA");
        Member savedMember = memberJpaRepository.save(member);

        Member findMember = memberJpaRepository.find(savedMember.getId());
        assertThat(findMember.getId()).isEqualTo(member.getId());
        assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
        assertThat(findMember).isEqualTo(savedMember); //  //JPA 엔티티 동일성 보장
    }

    @Test
    public void basicCRUD() {
        Member member1 = new Member("member11");
        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();

        findMember1.setUsername("member !!!!!!!!");
        assertThat(findMember1).isEqualTo(member1);
        assertThat(findMember2).isEqualTo(findMember2);


        // 리스트 조회 검증
        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);
    }
}

기본 crud 를 검증한다.

 

공통 인터페이스 설정

 

JavaConfig 설정- 스프링 부트 사용시 생략 가능

@Configuration
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
public class AppConfig {}
  • 스프링 부트 사용시 @SpringBootApplication 위치를 지정(해당 패키지와 하위 패키지 인식)
  • 만약 위치가 달라지면 @EnableJpaRepositories 필요

MemberRepository 는 인터페이스일 뿐인데 어떻게 구현체가 없는데 @Autowired 시 

    @Autowired
    MemberJpaRepository memberJpaRepository;

객체가 들어오고 코드가 실행될까?

 

객체를 확인해보자!

@SpringBootTest
@Transactional
@Rollback(value = false)
class MemberRepositoryTest {
    @Autowired MemberRepository memberRepository;

    @Test
    public void testMember() {
      System.out.println("memberRepository = " + memberRepository.getClass());
    }
}

 

스프링 데이터 JPA 가 해당 인터페이스를 보고 구현 클래스를 만들어 주입 시킨다.

 

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

 

org.springframework.data.jpa.repository 를 구현한 클래스는 스캔 대상

MemberRepository 인터페이스가 동작한 이유

 

 

공통 인터페이스 적용

순수 JPA로 구현한 MemberJpaRepository 대신에 스프링 데이터 JPA 가 제공하는 공통 인터페이스 적용

package study.data_jpa;

import org.springframework.data.jpa.repository.JpaRepository;
import study.data_jpa.entity.Member;

public interface MemberRepository extends JpaRepository<Member, Long> {

}

MemberRepository 테스트

package study.data_jpa;

import jakarta.transaction.Transactional;
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 study.data_jpa.entity.Member;

import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;


@SpringBootTest
@Transactional
@Rollback(value = false)
class MemberRepositoryTest {
    @Autowired MemberRepository memberRepository;

    @Test
    public void testMember() {
//        System.out.println("memberRepository = " + memberRepository.getClass());
        Member member = new Member("memberA");
        Member savedMember = memberRepository.save(member);

        Member findMember = memberRepository.findById(savedMember.getId()).get();

        assertThat(findMember.getId()).isEqualTo(member.getId());
        assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
        assertThat(findMember).isEqualTo(savedMember);
    }


    @Test
    public void basicCRUD() {

        Member member1 = new Member("member11");
        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();

        findMember1.setUsername("member !!!!!!!!");
        assertThat(findMember1).isEqualTo(member1);
        assertThat(findMember2).isEqualTo(findMember2);


        // 리스트 조회 검증
        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);
    }



}

기존 순수 JPA 기반 테스트에서 사용해썬 코드 그대로 스프링 데이터 JPA 레파지토리 기반 테스트로 변경해도 동일하게 동작

 

TeamRepository 생성

package study.data_jpa;

import org.springframework.data.jpa.repository.JpaRepository;
import study.data_jpa.entity.Team;

public interface TeamRepository extends JpaRepository<Team, Long> {
}

TeamRepository는 테스트 생략

  • Generic
    • T: 엔티티 타입
    • ID: 식별자 타입(PK)

공통 인터페이스 분석

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

JpaRepository 공통 기능 인터페이스

public interface JpaRepository<T, ID> extends ListCrudRepository<T, ID>, 
ListPagingAndSortingRepository<T, ID>, 
QueryByExampleExecutor<T> {
...
}

 

JpaRepository 를 사용하는 인터페이스

public interface MemberRepository extends JpaRepository<Member, Long> {

}

 

 

제네릭 타입

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

주요 메서드

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

 

참고: JpaRepository 는 대부분의 공통 메서드를 제공한다.

 

 

 

참고 자료 : 실전! 스프링 데이터 JPA

'JPA' 카테고리의 다른 글

양방향 연관관계와 연관관계 주인  (0) 2024.12.05
[JPA] 영속성 컨텍스트  (0) 2024.11.16