본문 바로가기

JPA 실전

[JPA] JPA 실전 - 공통 인터페이스

 

 

 

JPA에서는 엔티티의 변경을 감지하는 기능(Dirty Checking)을 통해 엔티티를 수정할 수 있습니다. 트랜잭션 내에서 엔티티를 조회하고 필드 값을 변경하면, 트랜잭션 종료 시점에 JPA가 자동으로 변경된 엔티티를 감지하고 데이터베이스에 UPDATE 쿼리를 실행합니다.

 

순수JPA 기반 리포지토리

package com.example.jpaspring.repository;

import com.example.jpaspring.domain.Member;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;

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

@Repository
public class MemberRepository {
    
    @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);
    }
    
}

 

순수 JPA 팀 리포지토리

package com.example.jpaspring.repository;

import com.example.jpaspring.domain.Team;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;

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

 

테스트코드 확인

package com.example.jpaspring.repository;

import com.example.jpaspring.domain.Member;
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.transaction.annotation.Transactional;

import java.util.List;

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

@SpringBootTest
@Transactional
class MemberRepositoryTest {

    @Autowired
    private MemberRepository memberRepository;

    @Test
    public void testMember(){
        Member member = Member.builder().username("memberA").build();
        Member savedMember = memberRepository.save(member);
        Member findMember = memberRepository.find(savedMember.getId());

        assertThat(findMember.getId()).isEqualTo(member.getId());
        assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
        assertThat(findMember).isEqualTo(member); // jpa 동일성 보장
    }

    @Test
    public void basicCRUD(){
        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();

        assertThat(findMember1).isEqualTo(member1);
        assertThat(findMember2).isEqualTo(member2);

        List<Member> all = memberRepository.findAll();
        assertThat(all.size()).isEqualTo(2);


        assertThat(memberRepository.count()).isEqualTo(2);

        memberRepository.delete(member1);
        memberRepository.delete(member2);

        assertThat(memberRepository.count()).isEqualTo(0);

    }

}

 

@Transactional 어노테이션은 트랜잭션을 관리하는 역할을 합니다. 트랜잭션이 커밋되거나 롤백될 때, 영속성 컨텍스트와의 상호작용도 이루어집니다. 트랜잭션 커밋이 일어나는 과정에서 영속성 컨텍스트의 상태가 어떻게 변화하는지 설명드리겠습니다.

트랜잭션 커밋과 영속성 컨텍스트의 상호작용

  1. 트랜잭션 시작:
    • 트랜잭션이 시작되면 영속성 컨텍스트가 활성화됩니다. 이 시점부터 엔티티 매니저는 영속성 컨텍스트 내에서 엔티티를 관리합니다.
  2. 엔티티의 변경:
    • 트랜잭션 범위 내에서 엔티티를 생성, 수정, 삭제하는 등의 작업이 이루어집니다. 이때, 엔티티 매니저는 영속성 컨텍스트를 통해 엔티티를 관리하고, 변경된 엔티티는 영속성 컨텍스트에 보관됩니다.
  3. 트랜잭션 커밋:
    • 트랜잭션이 커밋될 때, 영속성 컨텍스트에 보관된 변경 사항이 데이터베이스에 반영됩니다. 즉, 플러시(Flush) 작업이 수행되어 영속성 컨텍스트의 변경 내용이 데이터베이스에 동기화됩니다.
  4. 영속성 컨텍스트의 상태:
    • 트랜잭션이 커밋된 후에도, 영속성 컨텍스트는 기본적으로 유지됩니다. 즉, 트랜잭션이 끝난 후에도 영속성 컨텍스트는 여전히 활성 상태이며, 이미 로드된 엔티티들은 그대로 유지됩니다.
  5. 트랜잭션 종료 후 작업:
    • 트랜잭션이 종료된 후에도 영속성 컨텍스트를 통해 엔티티에 접근할 수 있습니다. 하지만 새로운 트랜잭션이 시작되면, 새로운 영속성 컨텍스트가 생성될 수 있습니다.

 

순수 JPA

순수 JPA에서는 트랜잭션 관리를 위해 명시적으로 트랜잭션을 시작하고 커밋해야 합니다. 그렇지 않으면 데이터베이스 변경사항이 커밋되지 않습니다. 따라서, 트랜잭션이 필요한 메소드에는 @Transactional을 붙여야 합니다.

@Repository
public class MemberRepository {

    @PersistenceContext
    private EntityManager em;

    @Transactional
    public Member save(Member member) {
        em.persist(member);
        return member;
    }

    @Transactional
    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);
    }
}

위와 같이 순수 JPA 리포지토리 메소드에서 @Transactional을 붙여줘야 트랜잭션이 시작되고, 메소드 실행이 끝날 때 트랜잭션이 커밋됩니다.

 

스프링 데이터 JPA

스프링 데이터 JPA에서는 기본적으로 CRUD 메소드들에 대해 트랜잭션을 자동으로 관리해줍니다. 따라서, 별도로 @Transactional을 붙이지 않아도 스프링 데이터 JPA가 제공하는 메소드들은 트랜잭션 내에서 실행됩니다.

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

위와 같이 스프링 데이터 JPA 리포지토리를 사용하면, 기본적인 CRUD 메소드(save, delete, findAll, findById 등)에 대해 스프링이 자동으로 트랜잭션을 관리합니다.

추가적인 비즈니스 로직이 포함된 메소드에는 @Transactional을 붙여줘야 트랜잭션이 적용됩니다.

@Service
public class MemberService {

    @Autowired
    private MemberRepository memberRepository;

    @Transactional
    public void saveMember(Member member) {
        memberRepository.save(member);
    }

    public Member findMember(Long id) {
        return memberRepository.findById(id).orElse(null);
    }
}

 

위 예시에서 saveMember 메소드에는 @Transactional이 붙어있어 트랜잭션이 적용되고, findMember 메소드는 기본적으로 트랜잭션이 필요하지 않기 때문에 붙이지 않았습니다.

결론

  • 순수 JPA: 트랜잭션이 필요한 메소드마다 @Transactional을 명시적으로 붙여줘야 합니다.
  • 스프링 데이터 JPA: 기본적인 CRUD 메소드들에 대해 스프링이 자동으로 트랜잭션을 관리합니다. 추가적인 비즈니스 로직 메소드에는 @Transactional을 붙여줘야 합니다.
 

스프링 데이터 리포지토리 인터페이스

  1. Repository
    • 최상위 인터페이스로, 모든 리포지토리 인터페이스의 공통 조상입니다.
    • 특별한 메소드를 정의하지 않습니다.
  2. CrudRepository
    • Repository를 확장하며, 기본적인 CRUD (Create, Read, Update, Delete) 기능을 제공합니다.
    • 주요 메소드:
      • save(S entity): 엔티티를 저장하거나 업데이트합니다.
      • findOne(ID id): ID로 엔티티를 조회합니다.
      • exists(ID id): ID로 엔티티가 존재하는지 확인합니다.
      • count(): 총 엔티티 수를 반환합니다.
      • delete(T entity): 엔티티를 삭제합니다.
  3. PagingAndSortingRepository
    • CrudRepository를 확장하며, 페이징 및 정렬 기능을 추가로 제공합니다.
    • 주요 메소드:
      • findAll(Sort sort): 정렬 기준에 따라 모든 엔티티를 조회합니다.
      • findAll(Pageable pageable): 페이징과 정렬 기준에 따라 엔티티를 조회합니다.

스프링 데이터 JPA 리포지토리 인터페이스

  1. JpaRepository
    • PagingAndSortingRepository를 확장하며, JPA와 관련된 추가 기능을 제공합니다.
    • 주요 메소드:
      • findAll(): 모든 엔티티를 조회합니다.
      • findAll(Sort sort): 정렬 기준에 따라 모든 엔티티를 조회합니다.
      • findAll(Iterable<ID> ids): 주어진 ID 목록에 해당하는 모든 엔티티를 조회합니다.
      • saveAndFlush(T entity): 엔티티를 저장하고 바로 플러시합니다.
      • deleteInBatch(Iterable<T> entities): 주어진 엔티티들을 배치로 삭제합니다.
      • deleteAllInBatch(): 모든 엔티티를 배치로 삭제합니다.
      • getOne(ID id): 주어진 ID로 엔티티의 프록시 객체를 반환합니다.

요약

  • Repository는 최상위 인터페이스로, 기본 기능을 제공하지 않습니다.
  • CrudRepository는 기본적인 CRUD 기능을 제공합니다.
  • PagingAndSortingRepository는 페이징 및 정렬 기능을 추가합니다.
  • JpaRepository는 JPA와 관련된 추가 기능을 제공하여, 더욱 강력한 리포지토리 기능을 사용할 수 있게 합니다.

제네릭 타입

  • T : 엔티티 타입을 나타냅니다.
  • ID : 엔티티의 식별자 타입을 나타냅니다.
  • S : 엔티티 타입 또는 그 자식 타입을 나타냅니다.

주요 메서드

  • Optional<T> findById(ID id):
    • 엔티티 하나를 조회합니다. 내부에서 EntityManager.find()를 호출합니다.
    • 조회된 결과가 없으면 Optional.empty()를 반환합니다.
  • boolean existsById(ID id):
    • 주어진 ID를 가진 엔티티가 존재하는지 확인합니다.
  • S save(S entity):
    • 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합합니다.
  • void delete(T entity):
    • 엔티티 하나를 삭제합니다. 내부에서 EntityManager.remove()를 호출합니다.
  • T getOne(ID id):
    • 엔티티를 프록시로 조회합니다. 내부에서 EntityManager.getReference()를 호출합니다.
  • List<T> findAll():
    • 모든 엔티티를 조회합니다.
  • List<T> findAll(Sort sort):
    • 정렬 조건을 사용하여 모든 엔티티를 조회합니다.
  • Page<T> findAll(Pageable pageable):
    • 페이징 조건을 사용하여 모든 엔티티를 조회합니다.
  • long count():
    • 엔티티의 총 개수를 반환합니다.

이 메서드들은 스프링 데이터 JPA가 제공하는 기본 기능으로, 간단한 데이터 액세스 로직을 쉽게 구현할 수 있게 해줍니다. 특히, Optional을 사용하여 조회 결과가 없을 때의 상황을 안전하게 처리할 수 있으며, Sort와 Pageable을 통해 정렬과 페이징 기능을 손쉽게 사용할 수 있습니다.