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 어노테이션은 트랜잭션을 관리하는 역할을 합니다. 트랜잭션이 커밋되거나 롤백될 때, 영속성 컨텍스트와의 상호작용도 이루어집니다. 트랜잭션 커밋이 일어나는 과정에서 영속성 컨텍스트의 상태가 어떻게 변화하는지 설명드리겠습니다.
트랜잭션 커밋과 영속성 컨텍스트의 상호작용
- 트랜잭션 시작:
- 트랜잭션이 시작되면 영속성 컨텍스트가 활성화됩니다. 이 시점부터 엔티티 매니저는 영속성 컨텍스트 내에서 엔티티를 관리합니다.
- 엔티티의 변경:
- 트랜잭션 범위 내에서 엔티티를 생성, 수정, 삭제하는 등의 작업이 이루어집니다. 이때, 엔티티 매니저는 영속성 컨텍스트를 통해 엔티티를 관리하고, 변경된 엔티티는 영속성 컨텍스트에 보관됩니다.
- 트랜잭션 커밋:
- 트랜잭션이 커밋될 때, 영속성 컨텍스트에 보관된 변경 사항이 데이터베이스에 반영됩니다. 즉, 플러시(Flush) 작업이 수행되어 영속성 컨텍스트의 변경 내용이 데이터베이스에 동기화됩니다.
- 영속성 컨텍스트의 상태:
- 트랜잭션이 커밋된 후에도, 영속성 컨텍스트는 기본적으로 유지됩니다. 즉, 트랜잭션이 끝난 후에도 영속성 컨텍스트는 여전히 활성 상태이며, 이미 로드된 엔티티들은 그대로 유지됩니다.
- 트랜잭션 종료 후 작업:
- 트랜잭션이 종료된 후에도 영속성 컨텍스트를 통해 엔티티에 접근할 수 있습니다. 하지만 새로운 트랜잭션이 시작되면, 새로운 영속성 컨텍스트가 생성될 수 있습니다.
순수 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을 붙여줘야 합니다.

스프링 데이터 리포지토리 인터페이스
- Repository
- 최상위 인터페이스로, 모든 리포지토리 인터페이스의 공통 조상입니다.
- 특별한 메소드를 정의하지 않습니다.
- CrudRepository
- Repository를 확장하며, 기본적인 CRUD (Create, Read, Update, Delete) 기능을 제공합니다.
- 주요 메소드:
- save(S entity): 엔티티를 저장하거나 업데이트합니다.
- findOne(ID id): ID로 엔티티를 조회합니다.
- exists(ID id): ID로 엔티티가 존재하는지 확인합니다.
- count(): 총 엔티티 수를 반환합니다.
- delete(T entity): 엔티티를 삭제합니다.
- PagingAndSortingRepository
- CrudRepository를 확장하며, 페이징 및 정렬 기능을 추가로 제공합니다.
- 주요 메소드:
- findAll(Sort sort): 정렬 기준에 따라 모든 엔티티를 조회합니다.
- findAll(Pageable pageable): 페이징과 정렬 기준에 따라 엔티티를 조회합니다.
스프링 데이터 JPA 리포지토리 인터페이스
- 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을 통해 정렬과 페이징 기능을 손쉽게 사용할 수 있습니다.
'JPA 실전' 카테고리의 다른 글
[JPA] JPA 실전 - 확장 기능 (0) | 2024.07.02 |
---|---|
[JPA] JPA 실전 - 쿼리 메소드 (3) (0) | 2024.07.02 |
[JPA] JPA 실전 - 쿼리 메소드 (2) -페이징과 정렬 (0) | 2024.07.02 |
[JPA] JPA 실전 - 쿼리 메소드 (1) (0) | 2024.07.02 |
[JPA] JPA 실전 - 개발 기본 (0) | 2024.06.30 |