← Documents

JPA · 자바 ORM 표준

🗄️

한 줄 소개 — JPA 는 자바 객체와 관계형 DB 를 이어주는 ORM 표준이다. SQL 을 직접 쓰지 않고 객체를 다루듯 DB 를 CRUD 한다. 이 글은 JPA 의 정체(표준/Hibernate)부터, 그 핵심인 영속성 컨텍스트(1차 캐시 · 변경 감지 · 쓰기 지연), 엔티티 매핑, 연관관계 매핑까지 코드와 다이어그램으로 정리한 노트다. (김영한님의 JPA 강의 + 개별 학습 정리)

1 · JPA 란?

JPA(Java Persistence API) 는 자바 진영의 ORM(Object–Relational Mapping) 표준이다. "자바 표준"이라는 말은 곧 인터페이스(기능 명세서) 라는 뜻이고, 이를 실제로 구현한 것이 Hibernate · EclipseLink · DataNucleus 다. (실무 비중은 Hibernate 가 90% 이상.)

💡

왜 JPA 인가? 자바 애플리케이션에서 DB 데이터를 객체(Object) 로 매핑해 주므로, 코드단에서 DB 접근이 쉬워진다. SQL 을 직접 날리지 않고 API 를 쓰듯 객체를 저장·조회·수정·삭제할 수 있어 생산성 · 유지보수 · 패러다임 불일치 해소 에 유리하다.

ORM — 객체와 테이블을 매핑 Object (Member) Long id;String name;member.getName(); Table MEMBER IDNAME 1Lee 매핑 JPA(표준 인터페이스) → 구현체 Hibernate 가 SQL 을 대신 생성·실행
객체와 테이블의 형상 차이를 JPA 가 매핑해 준다 — 개발자는 객체만 다루고, Hibernate 가 SQL 을 생성한다

2 · 순수 JPA vs Spring Data JPA

SQL 로 하려면 INSERT 구문을 짜고 필드를 일일이 바인딩해야 하지만, JPA 는 객체 자체를 저장한다. Spring Data JPA 는 여기서 한 발 더 나아가, Repository 인터페이스만 선언하면 기본 CRUD 메서드를 자동으로 만들어 준다.

// 순수 JPA — EntityManager 로 직접 jpa.persist(member); // 등록(영속화) Member found = jpa.find(Member.class, memberId); // 조회 member.setName("변경할 이름"); // 수정 (변경 감지로 자동 UPDATE) jpa.remove(member); // 삭제 // Spring Data JPA — Repository 인터페이스만 선언하면 끝 User user = new User(name, id, password); @Autowired UserRepository userRepository; userRepository.save(user); // save·findById·delete … 자동 제공

3 · JPA 동작 — EntityManager 와 트랜잭션

JPA 의 동작 단위는 EntityManagerFactory(EMF)EntityManager(EM) 다. EMF 는 애플리케이션당 하나 만들고(생성 비용이 큼), EM 은 요청/트랜잭션마다 하나 만들어 쓰고 닫는다. 그리고 모든 데이터 변경은 반드시 트랜잭션 안에서 일어나야 한다.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello"); // 앱당 1개 EntityManager em = emf.createEntityManager(); // 트랜잭션 단위마다 생성 EntityTransaction tx = em.getTransaction(); // 트랜잭션 없이 변경하면 에러 tx.begin(); try { Member member = new Member(); member.setId(1L); member.setName("HelloA"); em.persist(member); // 영속화 tx.commit(); // commit 시점에 flush → DB 반영 } catch (Exception e) { tx.rollback(); } finally { em.close(); // EM 은 쓰고 나면 꼭 닫는다 } emf.close();
EMF → EM → Transaction EntityManagerFactory앱당 1개 (무겁다) EntityManager #1 EntityManager #2 tx.begin → commit 요청/트랜잭션마다 생성
EMF 는 앱당 하나, EM 은 트랜잭션마다 만들어 쓰고 닫는다. 변경은 항상 트랜잭션 안에서

4 · 영속성 컨텍스트 (핵심)

JPA 의 심장. 영속성 컨텍스트(Persistence Context) 는 EntityManager 안의 "엔티티를 보관·관리하는 메모리 버퍼" 다. 객체가 이 버퍼에 올라가 관리 대상이 된 상태를 영속화 라 한다. 이 버퍼 덕분에 — 같은 조회는 1차 캐시 로 쿼리를 아끼고, 변경은 자동 감지(Dirty Check) 하며, INSERT/UPDATE 는 쓰기 지연 SQL 저장소 에 모았다가 한 번에 보낸다.

영속성 컨텍스트 (EntityManager 내부) Persistence Context 1차 캐시 @IdEntitySnapshot 1member{…} 쓰기 지연 SQL 저장소 INSERT member …UPDATE member … persist/find DB flush / commit 시 한 번에 전송
EM 안의 영속성 컨텍스트 = 1차 캐시 + 쓰기 지연 SQL 저장소. flush(commit) 시점에 모아둔 SQL 을 DB 로 보낸다

엔티티의 생명주기 는 네 상태로 나뉜다 — 비영속(new 로 막 만든 상태) · 영속(persist/find 로 컨텍스트가 관리) · 준영속(detach/close 로 관리에서 떨어짐) · 삭제(remove).

비영속 (new) 영속 (managed) 준영속 (detach) 삭제 (remove) persist / find detach / close remove
엔티티 생명주기: 비영속 → (persist/find) 영속 → (detach/close) 준영속 / (remove) 삭제

5 · 1차 캐시 · 동일성 · 변경 감지

1차 캐시 — 같은 EM 안에서 같은 엔티티를 두 번 조회하면, 첫 번째만 DB 쿼리가 나가고 두 번째는 캐시에서 가져온다. 덕분에 동일성(==) 도 보장된다.

Member m1 = em.find(Member.class, 1L); // ① DB 조회 → 1차 캐시에 적재 Member m2 = em.find(Member.class, 1L); // ② 1차 캐시에서 반환 (쿼리 X) System.out.println(m1 == m2); // true — 같은 인스턴스 (동일성 보장)

변경 감지(Dirty Checking) — 영속 상태의 엔티티는 값만 바꿔도 된다. 굳이 persist 를 다시 호출할 필요가 없다. JPA 가 최초 영속 시점의 스냅샷 과 flush 시점의 값을 비교해, 달라졌으면 UPDATE 쿼리를 자동 생성 해 쓰기 지연 저장소에 넣는다.

Member member = em.find(Member.class, 1L); // 영속 상태 member.setName("Lee"); // 값만 변경 // em.persist(member); // ❌ 필요 없음 — 변경 감지가 알아서 UPDATE tx.commit(); // commit 직전 flush → 스냅샷 비교 → UPDATE 쿼리 전송
🔎

flush 는 쓰기 지연 SQL 저장소의 쿼리들을 DB 에 반영 하는 것이다(캐시를 비우는 게 아니다). em.flush() 를 직접 호출하거나, 트랜잭션 commit 시점, JPQL 쿼리 실행 직전에 자동으로 일어난다. em.detach() 하면 그 엔티티는 준영속 이 되어 더는 변경 감지 대상이 아니다.

6 · 엔티티 매핑 (Entity Mapping)

같은 데이터를 애플리케이션(객체)과 DB(테이블)라는 다른 형상에서 다루려면 매핑 정보 가 필요하다. 어노테이션으로 객체–테이블, 필드–컬럼, 기본 키를 매핑한다.

@Entity // JPA 가 관리하는 엔티티 @Table(name = "MEMBER") // 매핑할 테이블 (생략 시 클래스명) public class Member { @Id // 기본 키(PK) 매핑 @GeneratedValue(strategy = GenerationType.IDENTITY) // PK 생성 전략 (IDENTITY/SEQUENCE/AUTO) private Long id; @Column(name = "name", nullable = false, length = 50) // 필드 ↔ 컬럼 매핑 private String name; @Enumerated(EnumType.STRING) // enum 은 반드시 STRING 으로 private Grade grade; }

스키마 자동 생성hibernate.hbm2ddl.auto(Spring: spring.jpa.hibernate.ddl-auto) 로 엔티티를 보고 테이블 DDL 을 자동 생성할 수 있다. 옵션은 create · create-drop · update · validate · none — 운영에서는 validate/none 만 쓴다.

7 · 연관관계 매핑

객체는 참조 로, 테이블은 외래 키(FK) 로 관계를 맺는다 — 이 패러다임 차이를 연관관계 매핑 으로 잇는다. 다대일(@ManyToOne)·일대다(@OneToMany) 가 가장 흔하다.

연관관계 — Member(N) : Team(1) Member (N)@ManyToOne → FK(team_id) Team (1)@OneToMany(mappedBy) team_id (FK) FK 를 가진 쪽(Member)이 연관관계의 주인 — 반대편은 mappedBy 로 읽기 전용
FK 를 관리하는 쪽이 연관관계의 주인. 양방향이면 반대편은 mappedBy 로 "거울"만 본다
@Entity public class Member { @Id @GeneratedValue private Long id; @ManyToOne // 다(Member) : 일(Team) @JoinColumn(name = "team_id") // FK 컬럼 — 이쪽이 "연관관계의 주인" private Team team; } @Entity public class Team { @Id @GeneratedValue private Long id; @OneToMany(mappedBy = "team") // 주인이 아님(읽기 전용) — Member.team 이 주인 private List members = new ArrayList<>(); }
⚠️

연관관계의 주인 — 양방향 매핑에서 FK 를 실제로 관리(등록·수정)하는 쪽 이 주인이다. 주인이 아닌 쪽(mappedBy)은 조회만 가능. 값을 넣을 땐 주인 쪽에 반드시 설정 해야 DB 에 반영된다. (양쪽 다 세팅하는 연관관계 편의 메서드를 두면 안전하다.)


📓 2022–2023년 제가 백엔드를 준비하며 김영한님의 JPA 강의 + 개별 학습을 직접 정리한 노트입니다 · 원본 Notion 에서 보기 ↗