엔티티(Entity)를 영속적으로 저장하는 환경이라는 뜻입니다. 하지만 JPA에서는 엔티티의 생명주기를 관리하고, 애플리케이션과 데이터베이스 사이에서 수많은 최적화 작업을 해주는 논리적인 공간(메모리 공간)이라고 이해해야 합니다.
마치 효율적인 작업을 위해 필요한 도구와 재료들을 미리 꺼내놓는 '작업대'와 같습니다. JPA는 이 작업대 위에서 엔티티들을 관리하다가, 작업이 끝나면(트랜잭션이 끝나면) 최종 결과물만 DB에 반영합니다.
1차 캐시:
영속성 컨텍스트는 일종의 Map 형태의 캐시 저장소를 갖고 있습니다.
동일성 보장:
// 같은 트랜잭션 내에서
// Member를 저장
Member savedMember = memberRepository.save(newMember);
// ...
// 위에서 저장한 Member의 id로 Member를 조회
Member member = memberRepository.findById(dto.getId()).orElseThrow(
() -> new IllegalStateExcetion("존재하지 않는 멤버입니다.")
)
// 여기서 savedMember는 영속성 컨텍스트에 저장되고,
// 그 이후에 동일한 엔티티를 조회하면 DB를 조회하지 않고 캐시의 엔티티를 반환합니다.
이 기능 덕분에 불필요한 DB 조회를 줄여 성능을 크게 향상시킬 수 있습니다.
우리는 UPDATE 쿼리를 직접 작성할 필요가 없습니다.
동작 원리:
@Transactional
public void updateMemberName(Long id, String newName) {
Member member = memberRepository.findById(id).orElseThrow(
() -> new IllegalStateExcetion("존재하지 않는 유저 입니다.")
); // 영속성 컨텍스트가 관리 시작
// member.updateName()만 호출했을 뿐이고, update 쿼리는 없는 상황!
member.updateName(newName);
} // 👈🏻 트랜잭션이 끝나는 이 시점에 JPA가 변경을 감지하고 UPDATE 쿼리를 날려준다.
예를 들어, 위에서 memberRepository.save() 를 호출했다고 해서 즉시 INSERT SQL이 DB로 전송되는 것이 아닙니다.
memberRepository.save() 호출되면, 해당 엔티티는 1차 캐시에 저장되고, JPA는 이 엔티티를 DB에 반영하기 위한 INSERT SQL을 생성하여 차곡차곡 모아둡니다.UPDATE나 DELETE 쿼리도 마찬가지로 이 저장소에 쌓입니다.한 번에 DB로 보냅니다. (이 과정을 Flush라고 합니다.)영속성 컨텍스트의 변경 내용을 DB에 동기화하는 것을 말합니다.
Flush는 영속성 컨텍스트의 '쓰기 지연 SQL 저장소'에 쌓인 SQL들을 데이터베이스에 전송하여, 컨텍스트의 상태와 DB의 상태를 맞추는 작업이고, 트랜잭션 커밋과는 다릅니다!
Flush는 DB에 SQL을 보내는 것일 뿐, 트랜잭션을 확정(Commit)하는 것은 아닙니다.
즉, 원래대로라면 쓰기 지연으로 인해 모아두었던 SQL을 트랜잭션이 끝나는 시점에 한꺼번에 DB로 보내지만, Flush를 사용하면 그 시점에 바로 SQL을 DB로 보내게 됩니다.
단, Flush를 한다고 하더라도 여전히 트랜잭션으로 관리가 되고 있는 상태이기 때문에 트랜잭션 롤백 시, Flush한 데이터도 롤백됩니다.
EntityManager는 내부에 영속성 컨텍스트(Persistence Context)라는 공간을 가지고 있습니다.
→ 위에서 이야기한 영속성 컨텍스트 관련한 내용은 모두 EntityManager가 해주는 일입니다!
EntityManager는 JPA의 핵심 인터페이스로, 애플리케이션과 데이터베이스 사이에서 엔티티 객체를 관리하고 실제 데이터베이스 작업을 수행하는 객체입니다. Entity(엔티티) Manager(관리자)라는 이름 그대로, 모든 엔티티는 EntityManager를 통해 관리되어야 합니다.

Persistence Unit (설계도):
💡 데이터베이스와 통신하기 위한 설계도 또는 명세서와 같습니다.
이 설계도에는 어떤 재료(엔티티)를 사용하고, 어떤 공장(데이터베이스)에 연결할지에 대한 모든 정보가 담겨 있습니다.EntityManagerFactory (공장):
Persistence Unit의 설정 정보를 바탕으로 EntityManager를 생성하는 팩토리(공장)입니다. 애플리케이션 전체에서 단 하나만 생성되어 공유됩니다.💡 설계도(Persistence Unit)를 받아 실제로 부품(EntityManager)을 찍어낼 수 있는 공장 건물을 짓는 것과 같습니다.
공장을 짓는 데는 시간과 비용이 많이 들지만, 한번 지어두면 계속해서 제품을 생산할 수 있습니다.EntityManager (작업자):
💡 공장(EntityManagerFactory)에서 나온 로봇 팔과 같습니다.
각 로봇 팔은 하나의 특정 작업을 맡아 수행하며, 작업이 끝나면 원래 자리로 돌아가거나 폐기됩니다.
여러 작업이 동시에 한 로봇 팔을 사용하면 혼란이 발생합니다.💡 정리
설계도(Persistence Unit)를 바탕으로
→ 공장(EntityManagerFactory)을 짓고
→ 공장에서 필요할 때마다 작업자(EntityManager)를 생산하여
→ 작업자가 작업대(영속성 컨텍스트)에서 일을 하는 것으로 비유를 할 수 있습니다.
Spring Data JPA는 JPA의 핵심인 EntityManager를 더욱 편리하게 사용하도록 추상화한 것입니다. 즉, 개발자가 EntityManager를 직접 다루는 번거로운 작업을 대신 해주는 친절한 도구입니다. 우리들이 사용하고 있는 JpaRepository 내부적으로는 EntityManager를 사용하고 있는 것입니다.
Spring Data JPA가 EntityManager 관리를 대신 해주고 있으니, 개발자는 필요한 JpaRepository 인터페이스의 메서드만 호출하면 됩니다. (ex. save(), findById(), findAll() 등)
따라서 개발자는 더욱 더 편하게 핵심 로직에만 집중할 수 있게 됩니다.
트랜잭션에 대해서는 배웠습니다. 하지만 만약 트랜잭션 안에 또 따른 트랜잭션이 있거나, 그렇게 관리해야 하는 상황이 있으면 어떨까요?
예를 들면, "새로운 작업(메서드)을 시작해야 하는데, 혹시 이미 진행 중인 다른 작업(트랜잭션)이 있다면 어떻게 해야 할까?” 같은 상황입니다.
이에 대해 답하려면, 트랜잭션 전파에 대해 알아야합니다.
Spring에서는 @Transactional 어노테이션을 사용해 메서드를 트랜잭션 안에서 실행하게 할 수 있습니다. 이때 propagation 옵션으로 여러 가지 전략을 설정할 수 있습니다.
1. REQUIRED (default)
2. REQUIRES_NEW
3. NESTED
4. SUPPORTS
5. NOT_SUPPORTED
6. MANDATORY
7. NEVER
트랜잭션 격리 수준은 동시에 실행되는 여러 트랜잭션들이 서로 어느 정도까지 격리되어 실행될지를 결정하는 데이터베이스 설정입니다.
트랜잭션 격리 수준은 격리(고립) 수준이 낮은 순서대로 READ UNCOMMITED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE가 존재합니다.
DIRTY READ 발생* DIRTY READ: 트랜잭션이 작업이 완료되지 않았는데도 다른 트랜잭션에서 볼 수 있게 되는 현상입니다.
- 예시: `트랜잭션 A` 가 실행되고 update 됐을 때 그 사이에 `트랜잭션 B`가 실행된다면
`트랜잭션 A`에서 커밋이 되지 않았음에도 불구하고,
`트랜잭션 B`에서 `트랜잭션 A`가 update한 내용을 확인할 수 있게 된다.NON-REPEATABLE READ 발생* NON-REPEATABLE READ : 트랜잭션 내에서 같은 쿼리를 두 번 실행할 때,
그 사이에 다른 트랜잭션이 해당 데이터를 수정하고 커밋하면 두 쿼리의 결과가 달라지는 현상입니다.
- 예시: `트랜잭션 A` 가 SELECT로 잔액 1000원을 조회하고,
트랜잭션이 아직 끝나지 않은 이 때 `트랜잭션 B`가 해당 계좌에서 200원을 출금하고 COMMIT합니다.
그리고 `트랜잭션 A`가 다시 SELECT 실행 시 800원이 조회됩니다. (같은 트랜잭션 내에서 결과가 달라지는 현상)Undo 영역에 백업된 레코드에서 값을 가져옴* Undo 영역: 간단하게 데이터를 저장하는 버퍼 기능을 한다고 보면 되고,
트랜잭션에서 연산(커밋 롤백)이 이루어지지 않아도 질의문에 의해 수정이 생기면 수정되기 이전 값이 저장되는 영역.MySQL의 default 격리 수준* PHANTOM READ: 한 트랜잭션 내에서 같은 범위 쿼리를 실행할 때,
다른 트랜잭션이 새로운 레코드를 INSERT하거나 DELETE하여 결과 집합의 행 개수가 달라지는 현상
- 예시: `트랜잭션 A`가 `SELECT * FROM users WHERE age > 20`의 결과로 5개 행을 반환받았습니다.
그리고 `트랜잭션 B`가 age=25인 user를 INSERT 후 COMMIT합니다.
그리고 `트랜잭션 A`가 같은 쿼리를 재실행했을 때 5개가 아닌 6개 행이 반환됩니다.
(새로운 행이 ‘유령’처럼 나타나는 현상) * NON-REPEATABLE READ와 PHANTOM READ의 차이:
NON-REPEATABLE READ는 "기존 데이터의 값 변경" 문제이고,
PHANTOM READ는 "데이터 행의 추가/삭제" 문제입니다.
| 격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| READ UNCOMMITTED | ✅ 발생 | ✅ 발생 | ✅ 발생 |
| READ COMMITTED | ❌ | ✅ 발생 | ✅ 발생 |
| REPEATABLE READ | ❌ | ❌ | ✅ 발생 |
| SERIALIZABLE | ❌ | ❌ | ❌ |
💡 정리
REQUIRED로 충분합니다.REQUIRES_NEW를 사용해주세요.