코알못

[JPA] JPA 정복하기 - 02. 공통 인터페이스 기능 본문

JAVA

[JPA] JPA 정복하기 - 02. 공통 인터페이스 기능

코린이s 2024. 1. 7. 13:25
728x90

이번에는 스프링 Data JPA를 이용하여 CRUD를 실습 해보도록 하자!

그 전에 스프링 Data JPA 가 주는 이점을 확실하게 알 기 위해 순수 JPA 기반 repository 만들고 스프링 데이터 JPA 공통 인터페이스를 사용하여 만들어 보도록 하자!

순수 JPA 기반 Repository를 아래와 같이 만든다.

TeamRepository.java

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

MemberRepository.java

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
     * - Autowired와 차이 구글링 해보니 동일하게 동작하나 JPA를 위한 어노테이션인 해당 어노테이션을 사용하는것이 좋을것 같다는 내용이 많다.
     */
    @PersistenceContext
    /***
     * EntityManager
     * - 영속(=관리=스프링에서 만든) 컨텍스트 공간에 있는것을 불러온다(현재는 영속 공간에 있는 EntityManager을 불러온다)
     * - em.persist, em.find시 영속된 공간에 저장(=캐싱)하고 찾기에 결과는 정상적으로 나오나 DB에 실제로 가보면 없다.
     * - 그래서 EntoryTransaction의 commit 메소드를 사용하여 영속 공간에서 DB로 저장된다.
     * - 쓰기 지연 : 한번에 DB에 접근하기 떄문에 DB 부하를 줄일 수 있다.
     * - 지연 로딩 : 쓸 시점에 조회하여 가져온다.
     */
    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);
    }

}

그러나 repository를 보면 update가 구현되어 있지 않다..!

이는 트랜젝션 Commit시에 자동으로 변경 감지 되어 업데이트 되는 기능을 JPA 에서 제공하고 있기 때문에 따로 update 는 repository 에 정의하지 않아도 된다.

이제 테스트 검증을 위한 테스트 코드를 작성한다.

MemberJpaRepositoryTest.java

package study.datajpa.repository;

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

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

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

    @Autowired
    private MemberJpaRepository memberJpaRepository;

    @Test
    void testCRUD() {
        Member member1 = Member.builder()
                .username("member1")
                .build();
        Member member2 = Member.builder()
                .username("member2")
                .build();
        memberJpaRepository.save(member1);
        memberJpaRepository.save(member2);

        // 단건 검증
        Member findMember1 = memberJpaRepository.findById(member1.getId()).get();
        Member findMember2 = memberJpaRepository.findById(member2.getId()).get();
        assertEquals(findMember1, member1);
        assertEquals(findMember2, member2);

        // 리스트 조회 검증
        List<Member> all = memberJpaRepository.findAll();
        assertEquals(all.size(), 2);

        // 카운트 검증
        long count = memberJpaRepository.count();
        assertEquals(count, 2);

        // 변경 검증
        member1.setUsername("member1!");
        Member findMember = memberJpaRepository.findById(member1.getId()).get();
        assertEquals(member1.getUsername(), findMember.getUsername());

        // 삭제 검증
        memberJpaRepository.delete(member1);
        memberJpaRepository.delete(member2);

        long deleteCount = memberJpaRepository.count();
        assertEquals(deleteCount, 0);
    }
}

이제 테스트 코드 testCRUD 메소드를 실행해보면 정상적으로 검증이 통과 한것을 볼 수 있다.

그러나 자세히 보면 TeamRepository 에 작성한 코드와 MemberRepository 에 작성한 코드가 사실상 엔티티를 제외하고 동일하다.

이를 해결해준것이 Spring Data JPA 이다.

우리는 Spring Data JPA 관련 Interface만 상속하면 알아서 Scan(@Repository 생략 가능) 하고 Spring Data JPA가 알아서 구현체를 만들어서 주입한다. 

이를 실습 해보자!

MemberRepository.java 

package study.datajpa.repository;

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

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

이게 끝이다... 해당 interface 만들고 Spring Data JPA 인터페이스인 JpaRepository 상속하고 <엔티티, PK 타입> 만 정의해주면 끝난다!

이제 테스트 코드를 작성해보자!

이전 테스트 코드 가져와서 repository명만 Spring Data JPA 인터페이스로 변경한다.

여기서 @Rollback 은 Test 코드의 경우 자동으로 Transaction 이 끝나면 Rollback 이 되기 때문에 롤백 되지 않도록 false로 정의한다.

MemberRepositoryTest.java

package study.datajpa.repository;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.PersistenceContext;
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.*;

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

    @Autowired
    MemberRepository memberRepository;
    

    @Test
    void testCRUD() {
        Member member1 = Member.builder()
                .username("member1")
                .build();
        Member member2 = Member.builder()
                .username("member2")
                .build();
        memberRepository.save(member1);
        memberRepository.save(member2);

        // 단건 검증
        Member findMember1 = memberRepository.findById(member1.getId()).get();
        Member findMember2 = memberRepository.findById(member2.getId()).get();
        assertEquals(findMember1, member1);
        assertEquals(findMember2, member2);

        // 리스트 조회 검증
        List<Member> all = memberRepository.findAll();
        assertEquals(all.size(), 2);

        // 카운트 검증
        long count = memberRepository.count();
        assertEquals(count, 2);

        // 변경 검증
        member1.setUsername("member1!");
        Member findMember = memberRepository.findById(member1.getId()).get();
        assertEquals(member1.getUsername(), findMember.getUsername());

        // 삭제 검증
        memberRepository.delete(member1);
        memberRepository.delete(member2);

        long deleteCount = memberRepository.count();
        assertEquals(deleteCount, 0);
    }

}

테스트 돌려보면 정상적으로 통과 됨을 알 수 있다. 정말 편리하게 할 수 있다...

다음시간에는 쿼리 메소드 기능(메소드로 쿼리 호출 기능)을 배워보도록 하자!

끝~!

728x90
Comments