[SPRING] JPA와 영속성 컨텍스트, 테스트코드로 맛만 보기

wannabeing·2025년 4월 22일
2

SPRING

목록 보기
9/12
post-thumbnail

✅ ORM(Object-Relational-Mapping)

객체와 관계형DB를 매핑(연결)해주는 기술을 ORM이라고 한다.
해당 기술을 통해 우리는 보다 쉽게 객체를 통해 DB작업을 할 수 있다.

✅ JPA(Java-Persistence-API)

JPA는 ORM을 사용하기 위한 표준 API 인터페이스이면서,
어플리케이션과 JDBC 사이에서 동작하는 기술이다.
구현체로 대부분 하이버네이트(라이브러리)를 사용하게 된다!

💡 JPQL..?

  • JPA로 개발하게 되면 엔티티 객체를 중심으로 개발하게 된다.
  • 모든 데이터를 객체로 변환해서 검색하는 것은 데이터가 많다면 불가능에 가깝다.
  • 따라서 검색조건의 SQL과 함께 쓰게 될 수 밖에 없다!
  • JPA는 SQL을 추상화하여 만든 객체지향 쿼리언어인 JPQL을 지원해준다.
  • 우리는 JPQL을 통해서 객체를 대상으로 쿼리를 만들 수 있게 된다.
  • 따라서 특정 DB에 종속적이지 않다!

✅ JPA 동작 방식

Persistence → EntityManagerFactory → EntityManager

1. Persistence

persistence.xml (또는 application.yml) 설정 정보를 이용해 EntityManagerFactory를 생성한다.

💡 스프링부트에서는 해당 과정을 생략하고 EntityManagerFactory를 생성해준다.

2. EntityManagerFactory

DB와 연결된 EntityManager 객체를 필요할 때마다 생성한다.
무겁고 비용이 크기 때문에 보통 1개만 생성된다.

3. EntityManager

JPA에서 실제 DB와 상호작용을 담당하는 핵심 객체이다.
트랜잭션 단위로 동작한다.


영속성 컨텍스트(PersistenceContext) ⭐️⭐️⭐️⭐️⭐️

영속성 컨텍스트는 "엔티티를 영구적으로 저장하는 환경"이라는 뜻이다.
서버와 DB 사이에서 엔티티 객체를 저장하는 가상의 DB 역할이라고 할 수 있다.

<EntityManager.persist(entity);

persist 메서드는 객체를 즉시 DB에 저장하지 않고, 영속성 컨텍스트에 저장한다.

1개의 엔티티매니저가 생성될 때, 1개의 영속성 컨텍스트가 생성된다.
스프링에서는 트랜잭션 범위에서 엔티티 매니저가 생성된다.


✅ 엔티티의 생명주기

  • 비영속(new/transient)
    영속성 컨텍스트와 상관없는 상태
    Memeber memeber = new member(1L, "이름"); // 비영속 상태
  • 영속(managed)
    영속성 컨텍스트가 관리하는 상태
    EntityManager.persist(member); // 영속 상태
  • 준영속(detached)
    영속성 컨텍스트에 저장되었다가 분리된 상태
    EntityManager.detach(member); // 영속 상태에서 분리된 상태
  • 삭제(removed)
    삭제된 상태
    EntityManager.remove(member); // 삭제된 상태

✅ 영속성 컨텍스트의 이점

  • 1차 캐시 기능
    조회할 때, JPA는 우선 영속성 컨텍스트에서 캐시값이 있다면 그대로 반환한다.
    1차 캐시에 없을 경우, DB에서 찾아와 1차캐시에 저장하고 반환한다.
    한 트랙잭션에서만 유효하기 때문에 큰 성능의 이점은 없다..!

  • 영속 엔티티의 동일성 보장
    한 트랙잭션에서 JPA가 영속 엔티티의 동일성(==)을 보장해준다.

    Member member1 = em.find(Member.class, 1L);
     Member member2 = em.find(Member.class, 1L);
     // member1 == member2: true
  • 쓰기 지연
    persist 메서드 실행되면 우선 1차 캐시에 저장된다.
    그리고, 쓰기지연 SQL 저장소에 insert 쿼리를 저장한다.

    EntityManager.persist(member1); // insert 쿼리문 나가지 않음
     EntityManager.persist(member2); // insert 쿼리문 나가지 않음
     EntityManager.commit(); // 커밋하는 순간 insert 쿼리 실행
  • 변경 감지(dirtyChecking)
    한 트랜잭션 안에서 영속 엔티티가 변경이 된다면, JPA가 1차 캐시와 비교하여
    달라졌다면 update쿼리를 생성하여 쓰기지연 SQL 저장소에 쿼리를 저장한다.

    Memeber memeber = new member(1L, "name");
     member.setName("이름변경"); // JPA가 1차 캐시와 비교하여 update 쿼리 생성
     EntityManager.commit(); // update 쿼리 실행

✅ 플러시(flush)

  • 영속성 컨텍스트 변경 내용을 데이터베이스에 반영하는 역할을 한다.
  • 플러시는 영속성 컨텍스트를 비우지 않는다!
 Memeber memeber = new member(1L, "name");
 EntityManager.persist(member); // 영속 상태
 EntityManager.flush(); // insert 쿼리문 실행

✅ 준영속 상태 (detached)

영속상태의 엔티티가 분리된 상태이다.
영속성 컨텍스트가 제공하는 기능들을 사용하지 못하게 된다.


🚀 스프링 부트로 JPA를 맛만 보자

CREATE TABLE member
(
    id   BIGINT AUTO_INCREMENT NOT NULL,
    name VARCHAR(255)          NOT NULL,
    CONSTRAINT pk_member PRIMARY KEY (id)
);

@Entity
@NoArgsConstructor
@Getter
public class Member {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	private String name;

	void setName(String name){
		this.name = name;
	}
}

위와 같은 테이블, 엔티티 클래스가 있다고 가정하고
EntityManager 객체를 이용해 CRUD 기능을 구현해보자.

✅ 멤버 생성


@DataJpaTest
class MemberTest {
	@Autowired
	private EntityManager em; // 엔티티 매니저

	@Test
	@Transactional
	void 멤버_저장(){
		// 1. 멤버 객체 생성
		Member member = new Member();
		member.setName("ㅎㅇ");

		// 2. 멤버 객체 DB에 Insert
		em.persist(member); // 영속 상태
		em.flush(); // ✅ insert 쿼리 실행
		em.clear(); // 영속성 컨텍스트 초기화

		// 3. DB 조회를 통해 멤버 객체 출력하기
		Member found = em.find(Member.class, member.getId()); // ✅ select 쿼리
		assertNotNull(found);
		assertEquals("ㅎㅇ", found.getName()); // true
		System.out.println("✅ 조회된 멤버\n" + found.getName() + "\n" + found.getId());
	}
    ....
  • 하이버네이트가 실제로 쿼리를 날린 이미지이다.
  • Insert 쿼리 이후, Select 쿼리로 DB에 저장된 멤버를 조회했다!

✅ 멤버 수정

@Test
@Transactional
void 멤버_수정(){
	// given
	Member newMember = new Member();
	newMember.setName("ㅎㅇ");

	// when
	em.persist(newMember); // 영속 상태
	em.flush(); // ✅ insert 쿼리 실행

	newMember.setName("바꾸자");
	em.flush(); // ✅ update 쿼리 실행
	em.clear();  // 영속성 컨텍스트 초기화

	// then
    // ✅ select 쿼리 실행
	Member found = em.find(Member.class, newMember.getId());
	System.out.println("✅ 조회된 멤버\n" + found.getName() + "\n" + found.getId());
	}
  • 순서대로 insert, update, select 쿼리문이 실행되었다.

✅ 멤버 삭제

@Test
@Transactional
void 멤버_삭제(){
	// given
	Member newMember = new Member();
	newMember.setName("ㅎㅇ");

	// when
	em.persist(newMember); // 영속 상태
	em.flush();  // ✅ insert 쿼리 실행
	em.clear();  // 영속성 컨텍스트 초기화

	// ✅ select 쿼리 실행
	Member find = em.find(Member.class, newMember.getId()); 

	em.remove(find); 
	em.flush();  // ✅ 삭제 쿼리 실행
	em.clear();  // 영속성 컨텍스트 초기화

	// then
    // ✅ select 쿼리 실행
	Member deleted = em.find(Member.class, newMember.getId()); // null
	assertNull(deleted);
}
  • 조금짤렸지만 insert, select delete, select 순서대로 쿼리가 실행되었다.

🧾 마무리하며

JPA의 핵심 개념인 영속성 컨텍스트를 중심으로
엔티티의 생명주기, 1차 캐시, 쓰기 지연, 변경 감지 등
JPA가 내부적으로 어떤 방식으로 동작하는지를 살펴봤다!

다음번엔 매핑에 대해 알아보고, 실제 순환참조 문제를 해결해보려고 한다!

profile
wannabe---ing

4개의 댓글

comment-user-thumbnail
2025년 4월 24일

명쾌한 정리!

1개의 답글
comment-user-thumbnail
2025년 4월 25일

정말 깔끔한 정리입니다! 잘 배우고 갑니다~ 😀

1개의 답글