영속성 컨텍스트(Persistence Context)
엔티티를 영구 저장하는 환경이라는 뜻으로 애플리케이션과 데이터베이스 사이에 존재하는 논리적인 영역(일종의 버퍼 또는 캐시)이다.
엔티티 메니저를 호출하면, 데이터베이스에 바로 저장되는 것이 아니라 이 영속성 컨텍스트에 먼저 저장된다.
엔티티란?
사용하기 전에 spring-boot-starter-data-jpa 의존성을 추가하고 동기화를 시켜줘야 엔티티를 사용할 수 있다.
DB에서 영속성으로 저장된 데이터를 자바 객체로 매핑해 인스턴스 혀앹로 존재하는 데이터이다.
테이블에 대응하는 하나의 클래스로 @Entity 어노테이션을 붙여주면 된다.
@Entity
public class member{
@Id
Long memberNo;
String name;
Integer age;
}
즉, JPA에서 하나의 엔티티 타입을 생성한다는 것은 하나의 클래스를 작성한다는 의미이다.
사용 시 주의할 점
- @Entity 필수: 클래스 위에 붙여야 한다.
- @Id 필수: DB의 Primary Key(PK)와 매핑되는 식별자 필드가 반드시 하나 있어야 한다.
- 기본 생성자(No-arg Constructor) 필수:
- JPA가 리플렉션(Reflection) 기술을 사용해 객체를 프록시하거나 생성하기 때문이다.
- public 또는 protected여야 한다. (보통 안전하게 protected 권장)
- final 클래스, interface, enum 사용 불가: 엔티티는 상속과 프록시 생성이 가능해야 하므로 final을 쓰면 안 된다.
- 필드에 final 사용 불가: 필드 값을 변경할 수 있어야 하므로 final을 쓰면 안 된다.
- 엔티티를 절대 컨트롤러나 화면까지 노출시키지 말고 DTO로 변환해서 반환해야 한다.
엔티티 생명 주기

- 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
- 영속(managed) : 영속성 컨텍스트에 관리되는 상태
- 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제(removed) : 삭제된 상태
엔티티 메니저란?
관리되는 엔티티들은 영속성 컨텍스트에 넣어두고 객체의 생명주기를 관리한다.

"EntityManager는 절대로 여러 스레드가 동시에 접근하면 안 된다."
- 이유: 엔티티 매니저는 내부적으로 DB 커넥션을 하나 물고 동작한다. 여러 스레드가 하나의 엔티티 매니저를 공유하면, A 사용자의 트랜잭션 도중에 B 사용자의 데이터가 끼어들거나 롤백되어 데이터가 엉망이 된다.
- 스프링에서의 처리: 스프링 프레임워크를 쓰면 개발자가 직접 매니저를 생성/삭제할 필요가 없다. 스프링이 "트랜잭션 범위의 영속성 컨텍스트" 전략을 통해, 요청마다 별도의 엔티티 매니저(정확히는 프록시)를 주입해 주어 이 문제를 해결한다.
보통 엔티티 매니저는 JpaRepository를 사용한다. 하지만, 복잡한 쿼리 등을 위해 직접 EntityManager를 써야 할 때는 아래와 같이 주입한다.
@Service
@Transactional
public class MemberService {
// 스프링이 스레드 안전한 '프록시 엔티티 매니저'를 주입.
@PersistenceContext
private EntityManager em;
public void save(Member member) {
em.persist(member);
}
}
영속성 컨텍스트 기능
- 1차 캐시: 영속성 컨텍스트 내부에 있는 맵 형태의 캐시이다. find() 호출 시 DB보다 먼저 여기를 조회한다.
- 동일성 (indentity): 같은 트랜잭션 내에서 조회한 객체는 주소값(==)이 같음을 보장한다.
- 트랜잭션을 지원하는 쓰기 지원 (Transactional Write-Behind): commit() 전까지 INSERT SQL을 모아 두었다가 한 번에 보낸다.
- 변경 감지 (Dirty Checking): 객체의 필드 값만 바꾸면, 별도의 update(0 호출 없이도 DB에 수정 쿼리가 나간다.
- 지연 로딩 (Lazy Loading): 연관된 엔티티를 실제 사용할 때 조회한다.
1차 캐시
"트랜잭션이 끝날 때까지만 살아있는 짧은 메모리 저장소"
영속성 컨텍스트 내부에는 Map<Key, Value> 형태의 저장소가 있다. Key는 @Id로 매핑한 식별자(PK), Value는 엔티티 객체 자체이다.
작동원리
- em.find("member")호출. (여기서 em은 엔티티 매니저)
- 1차 캐시에 member가 있는지 확인
- 있으면: DB 조회 없이 캐시 된 객체 반환 (속도 매우 빠름)
- 없으면: DB 조회(Select) 후, 1차 캐시에 저장하고 반환
특징
- 애플리케이션 전체가 공유하는 캐시가 아닌 한 트랜잭션(요청) 내에서만 유효하다.
- 따라서 성능 향상의 목적보다는 뒤에 설명할 메커니즘(변경 감지 등)을 위한 기반이 된다는 점이 더 중요하다.
동일성 보장 (Identity)
"DB에서 두 번 조회해도, 자바 컬렉션에서 꺼낸 것처럼 똑같은 객체임을 보장"
자바의 컬렉션 프레임워크 중 List에서 객체를 두 번 꺼내면 주소 값이 같은 것처럼 JPA도 DB에서 데이터를 가져와도 이 원칙을 지켜준다.
작동원리
- 1차 캐시 덕분에 가능하다.
- 같은 트랜잭션 내에서 ID가 같은 엔티티를 find()하면, JPA는 1차 캐시에 있는 이미 생성된 인스턴스 참조값(주소)을 그대로 반환한다.
- 코드 예시:
Member a = em.find(Member.class, 100L);
Member b = em.find(Member.class, 100L); // DB 쿼리 안 나감 (캐시 조회)
System.out.println(a == b); // true (참조값 비교 성공)
효과:
- 반복 가능한 읽기 등급의 트랜잭션 격리 수준을 DB가 아닌 애플리케이션 차원에서 지원해 준다.
트랜잭션을 지원하는 쓰기 지원 (Transactional Write-Behind)
"버퍼에 쿼리를 모았다가, 커밋 직전에 한 방에 쏘기"
em.persist()를 한다고 해서 바로 DB에 INSERT SQL을 날리자 않는다.
작동원리
- persist(memberA) 실행
-> 1차 캐시에 저장 & 동시에 쓰기 지연 SQL 저장소에 INSERT SQL생성해서 보관 - persist(memberB) 실행
-> 위와 동일. (단, 아직 DB와 통신하지 않음) - transaction.commit() 실행
-> 엔티티 매니저가 flush() 호출 - 저장소에 쌓인 SQL들(MemberA, MemberB)을 DB로 한 번에 전송 후 커밋
장점:
- DB와의 네트워크 통신 횟수를 줄일 수 있다.
- JDBC Batch SQL 기능을 이용해 한 번에 쿼리를 보내는 최적화가 가능하다.
- JDBC Batch SQL 기능: 여러 개의 SQL 명령(INSERT, UPDATE, DELETE 등)을 하나의 작업으로 묶어 데이터베이스에 한 번에 전송 및 실행함으로써 네트워크 왕복 횟수를 줄여 성능을 크게 향상시키는 기술.
- Statement나 PreparedStatement의 addBatch()와 executeBatch() 메서드를 사용.
변경 감지 (Dirty Checking)
"세터(Setter)만 불렀는데 UPDATE 쿼리가 나가는 마법"
JPA를 쓰면서 가장 편한 기능 중 하나로, update() 메서드가 필요 없는 이유이다.
작동원리 (스냅샷)
- JPA는 엔티티를 1차 캐시에 저장할 때, 최초 상태를 복사해서 스냅샷을 따로 보관해 둔다.
- 개발자가 member.setName("NewName")으로 값을 바꾼다.
- commit() 시점에 JPA가 엔티티(현재 값)와 스냅샷(최초 값)을 일일이 비교한다.
- 다른 점이 발견되면 자동으로 UPDATE SQL을 생성하여 쓰기 지연 저장소에 담고 DB에 보낸다.
주의사항:
- 영속성 상태인 엔티티에만 적용된다. (준영속성, 비용속성 객체는 값을 바꿔도 반응 안 함)
- 기본적으로 모든 필드를 업데이트하는 쿼리가 생성된다. (동적 쿼리 생성을 원한다면, @DynamicUpdate 사용)
지연 로딩 (Lazy Loading)
"진짜 필요할 때까지 DB 조회를 미루는 기술 (feat. 가짜 객체)"
사원 정보를 조회할 때 부서 정보가 꼭 필요할 필요는 없고 사원 이름만 필요한 경우가 있을 것이다.
작동원리 (프록시)
- 사원만 조회(SELECT * FROM Employee)하면, 부서는 조회하지 않는다.
- 대신 부서 자리에 프록시(Proxy)라는 가짜 객체를 넣어둔다.
- employee.getDepartment()를 호출해도 아직 쿼리는 안 나간다. (프록시 반환)
- employee.getDepartment().getName() 처럼 실제 부서 데이터에 손을 대는 순간, 그때 DB에 SELECT * FROM Department 쿼리가 나간다(초기화).
@ManyToOne(fetch = FetchType.LAZY) // 실무에서 필수 권장 옵션!
private Department department;
반대 개념: FetchType.EAGER (즉시 로딩) - 사원을 조회할 때 무조건 부서까지 조인을 걸어서 가져옵니다. (성능상 예측이 어려워 실무에선 거의 안 씁니다.)
면접 답변식 요약
영속성 컨텍스트는 애플리케이션과 DB 사이의 중간 계층 역할을 수행하며, 이를 통해 DB 접근 횟수를 줄이고(성능 최적화), 객체와 관계형 데이터베이스 간의 패러다임 불일치를 해결하여 개발자가 객체 중심의 비즈니스 로직에만 집중할 수 있게 도와줍니다.
'Daily Dev Q&A 정리 템플릿' 카테고리의 다른 글
| 26.01.13 N+1 문제가 무엇이고 어떻게 해결하는가? (0) | 2026.01.13 |
|---|---|
| 26.01.09 JPA와 MyBatis를 비교해서 설명해주세요. (0) | 2026.01.09 |
| 26.01.07 ORM이란? (0) | 2026.01.07 |
| 26.01.06 WAS와 웹서버란? (0) | 2026.01.06 |
| 26.01.05 JPA를 사용하는 이유 (0) | 2026.01.05 |