JPA(Java Persistence API)
한눈에 보기
JPA는 자바 객체와 관계형 데이터베이스 테이블의 매핑을 표준으로 정의한 ORM 명세다. 명세와 구현체의 관계, 영속성 컨텍스트의 기능, 엔티티 생명주기, N+1 문제까지 면접 관점에서 정리한다.
최종 업데이트 2026.09.17
JPA(Java Persistence API)는 자바 객체와 관계형 데이터베이스 테이블 사이의 매핑을 표준으로 정의한 ORM(Object Relational Mapping) 명세다. 개발자는 SQL을 직접 작성하는 대신 엔티티(Entity) 객체를 다루는 방식으로 데이터를 저장하고 조회하며, 실제 동작은 하이버네이트(Hibernate) 같은 구현체가 담당한다. Spring Boot 3.x에서는 Jakarta EE 전환에 따라 jakarta.persistence 패키지를 기반으로 동작한다.
명세와 구현체의 분리
JPA는 인터페이스와 애너테이션(Annotation)으로 이루어진 표준 명세이고, 명세 자체는 아무 일도 하지 않는다. EntityManager, EntityManagerFactory 같은 인터페이스의 실제 구현은 하이버네이트나 EclipseLink 같은 ORM 프레임워크가 제공한다. 스프링 부트에서 spring-boot-starter-data-jpa 의존성을 추가하면 기본 구현체로 하이버네이트가 선택되고, 자동 구성(Auto Configuration)이 EntityManagerFactory와 트랜잭션 매니저를 스프링 컨테이너에 빈(Bean)으로 등록한다.
실무에서 함께 쓰는 Spring Data JPA는 JPA를 한 번 더 추상화한 라이브러리다. 리포지토리(Repository) 인터페이스만 정의하면 스프링이 구현체를 프록시(Proxy)로 만들어 주입하므로, 개발자가 EntityManager를 직접 다루는 코드가 크게 줄어든다. 즉 JPA는 표준, 하이버네이트는 구현, Spring Data JPA는 사용 편의를 위한 추상화 계층이라는 세 층의 관계를 구분해서 이해해야 한다.
영속성 컨텍스트가 하는 일
JPA 내부 동작의 중심에는 영속성 컨텍스트(Persistence Context)가 있다. 엔티티 매니저를 통해 저장하거나 조회한 엔티티는 영속성 컨텍스트라는 논리적 저장소에서 관리되며, 이 저장소가 다음 기능을 제공한다.
- 1차 캐시: 같은 트랜잭션 안에서 같은 식별자로 다시 조회하면 데이터베이스에 접근하지 않고 캐시된 엔티티를 반환한다.
- 동일성 보장: 같은 트랜잭션에서 같은 식별자로 조회한 엔티티는 == 비교가 참이 되는 하나의 인스턴스다.
- 쓰기 지연(Write-Behind): INSERT, UPDATE, DELETE SQL을 곧바로 보내지 않고 내부 큐에 모았다가 플러시(Flush) 시점에 한꺼번에 전송한다.
- 변경 감지(Dirty Checking): 영속 상태 엔티티의 필드 값이 바뀌면 플러시 시점에 자동으로 UPDATE SQL을 생성한다.
- 지연 로딩(Lazy Loading): 연관 엔티티 자리에 프록시 객체를 두었다가 실제로 접근하는 순간 조회 쿼리를 실행한다.
스프링 환경에서 영속성 컨텍스트의 생존 범위는 기본적으로 트랜잭션과 같다. 트랜잭션이 시작될 때 영속성 컨텍스트가 만들어지고, 커밋 직전에 플러시가 일어나며, 트랜잭션이 끝나면 함께 사라진다.
엔티티 생명주기
엔티티는 영속성 컨텍스트와의 관계에 따라 네 가지 상태를 오간다.
| 상태 | 의미 | 대표 전이 방법 |
|---|---|---|
| 비영속(New) | 객체를 new로 만들었을 뿐 컨텍스트와 무관한 상태 | new Member() |
| 영속(Managed) | 영속성 컨텍스트가 관리하며 변경 감지 대상인 상태 | persist(), find() |
| 준영속(Detached) | 한때 영속이었지만 관리에서 벗어난 상태 | detach(), clear(), 트랜잭션 종료 |
| 삭제(Removed) | 플러시 시점에 DELETE가 실행될 예정인 상태 | remove() |
준영속 엔티티는 1차 캐시, 변경 감지, 지연 로딩의 대상이 아니라는 점이 중요하다. 트랜잭션 밖에서 지연 로딩을 시도하면 LazyInitializationException이 발생하는 이유가 여기에 있다.
코드로 보는 JPA
엔티티와 Spring Data JPA 리포지토리를 사용하는 전형적인 예시는 다음과 같다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
protected Member() {}
public Member(String name, Team team) {
this.name = name;
this.team = team;
}
}
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByName(String name);
}
@Service
@Transactional(readOnly = true)
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Transactional
public Long join(String name, Team team) {
Member member = memberRepository.save(new Member(name, team));
return member.getId();
}
}
findByName처럼 메서드 이름 규칙에 맞춰 선언하면 Spring Data JPA가 쿼리를 만들어 실행하고, 복잡한 조회는 @Query의 JPQL(Java Persistence Query Language)이나 QueryDSL로 작성한다. JPQL은 테이블이 아니라 엔티티를 대상으로 하는 객체지향 쿼리 언어라는 점에서 SQL과 다르다.
SQL 매퍼와의 비교와 주의할 점
| 구분 | JPA | MyBatis 같은 SQL 매퍼 |
|---|---|---|
| SQL 작성 | 대부분 자동 생성, 필요할 때만 직접 작성 | 모든 쿼리를 개발자가 직접 작성 |
| 패러다임 | 객체 중심, 연관관계를 객체 그래프로 탐색 | 테이블과 결과 매핑 중심 |
| 변경 반영 | 변경 감지로 자동 UPDATE | UPDATE 쿼리를 명시적으로 호출 |
| 학습 비용 | 영속성 컨텍스트와 연관관계 이해가 필요해 높음 | SQL만 알면 되어 낮음 |
JPA를 쓸 때 가장 자주 겪는 문제는 N+1 조회다. 목록을 한 번 조회한 뒤 각 엔티티의 연관 엔티티에 접근할 때마다 추가 쿼리가 나가는 현상으로, 페치 조인(Fetch Join)이나 @EntityGraph, 배치 사이즈 설정으로 해결한다. 또한 연관관계 기본 페치 전략은 지연 로딩으로 두고, 통계나 대량 배치 처리처럼 ORM의 장점이 약해지는 영역에서는 네이티브 쿼리나 별도 도구를 병행하는 판단도 필요하다.
면접에서 이렇게 물어본다
- JPA, 하이버네이트, Spring Data JPA는 각각 어떤 관계인지 설명해 보라.
- 영속성 컨텍스트가 제공하는 기능을 1차 캐시, 쓰기 지연, 변경 감지 중심으로 설명해 보라.
- 엔티티의 네 가지 생명주기 상태와 준영속 상태에서 지연 로딩이 실패하는 이유를 말해 보라.
- N+1 문제가 왜 생기고 어떤 방법으로 해결할 수 있는지 설명해 보라.
- JPA와 MyBatis 중 하나를 선택해야 한다면 어떤 기준으로 판단할 것인지 말해 보라.
Spring / JPA에서 같이 읽기
- 더티 체킹(Dirty Checking)더티 체킹은 영속성 컨텍스트가 엔티티 변경을 감지해 플러시 시점에 UPDATE 쿼리를 자동 생성하는 JPA 기능이다. 스냅샷 비교 원리, 플러시 시점, 준영속 엔티티와 벌크 연산의 주의점을 다룬다.
- SpringApplication과 부트스트랩 과정SpringApplication은 스프링 부트의 main 메서드에서 부트스트랩을 총괄하는 클래스다. run() 내부의 환경 준비, 컨텍스트 생성, refresh, 러너 실행 순서와 자동 구성 원리를 설명한다.
- 스프링 빈 스코프(Bean Scope)빈 스코프는 스프링 컨테이너가 빈을 언제 생성하고 어디까지 공유할지 정하는 규칙이다. 싱글턴과 프로토타입의 동작 차이, 웹 스코프, 스코프 혼용 시 문제와 ObjectProvider, 프록시 해결법을 정리한다.
- Spring Boot 테스트 코드 작성Spring Boot 테스트는 컨테이너를 어디까지 띄우느냐에 따라 통합 테스트와 슬라이스 테스트로 나뉜다. @SpringBootTest, @WebMvcTest, @DataJpaTest 활용법과 컨텍스트 캐싱 주의점을 다룬다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기