이 글에서 얻는 것
- JPA/ORM의 개념과 SQL을 직접 작성하는 것과의 차이를 이해합니다.
- **엔티티(Entity)와 영속성 컨텍스트(Persistence Context)**의 핵심 역할을 설명할 수 있습니다.
- **엔티티 상태(비영속/영속/준영속/삭제)**와 1차 캐시, 더티 체킹, 쓰기 지연을 이해합니다.
- EntityManager vs Repository의 차이를 알고, Spring Data JPA와의 관계를 파악합니다.
0) JPA는 “객체와 테이블을 매핑"하는 표준
JDBC/MyBatis (SQL 중심)
// JDBC: SQL 직접 작성
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "Alice");
pstmt.setString(2, "alice@example.com");
pstmt.executeUpdate();
// 조회도 직접 매핑
String sql = "SELECT * FROM users WHERE id = ?";
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
// ...
}
문제점:
- SQL을 직접 작성 → 데이터베이스 종속적
- 객체 ↔ ResultSet 변환 코드 반복
- 연관관계(JOIN)를 수동으로 처리
JPA (객체 중심)
// JPA: 객체만 다루면 됨
User user = new User("Alice", "alice@example.com");
entityManager.persist(user); // SQL 자동 생성
// 조회도 객체로
User found = entityManager.find(User.class, 1L);
System.out.println(found.getName()); // "Alice"
장점:
- SQL 자동 생성 → 데이터베이스 독립적
- 객체 그대로 저장/조회
- 연관관계를 객체 참조로 처리
1) JPA 핵심 개념
1-1) JPA vs Hibernate vs Spring Data JPA
┌─────────────────────────────────┐
│ Spring Data JPA (Repository) │ ← 가장 높은 추상화 (Spring)
├─────────────────────────────────┤
│ JPA (javax.persistence) │ ← Java 표준 인터페이스
├─────────────────────────────────┤
│ Hibernate (구현체) │ ← JPA 구현 (가장 많이 사용)
└─────────────────────────────────┘
- JPA: Java Persistence API (인터페이스, 명세)
- Hibernate: JPA의 구현체 (실제로 동작하는 라이브러리)
- Spring Data JPA: JPA를 더 쉽게 쓰게 해주는 Spring 모듈
1-2) 엔티티 (Entity)
데이터베이스 테이블과 매핑되는 객체
@Entity // JPA 엔티티임을 표시
@Table(name = "users") // 테이블 이름 지정 (생략 시 클래스명)
public class User {
@Id // 기본 키
@GeneratedValue(strategy = GenerationType.IDENTITY) // 자동 증가
private Long id;
@Column(name = "user_name", nullable = false, length = 50) // 컬럼 매핑
private String name;
@Column(unique = true)
private String email;
@Column(name = "created_at")
private LocalDateTime createdAt;
// 기본 생성자 필수 (JPA 스펙)
protected User() {}
public User(String name, String email) {
this.name = name;
this.email = email;
this.createdAt = LocalDateTime.now();
}
// Getter/Setter
}
핵심 어노테이션:
@Entity: JPA가 관리하는 엔티티@Id: 기본 키 (Primary Key)@GeneratedValue: 기본 키 생성 전략IDENTITY: 데이터베이스가 자동 생성 (AUTO_INCREMENT)SEQUENCE: 시퀀스 사용 (Oracle, PostgreSQL)TABLE: 키 생성 전용 테이블AUTO: 데이터베이스에 맞게 자동 선택
@Column: 컬럼 매핑 (생략 가능, 필드명과 컬럼명 자동 매핑)@Table: 테이블 이름 지정
1-3) 영속성 컨텍스트 (Persistence Context)
영속성 컨텍스트란?
- 엔티티를 영구 저장하는 환경 (메모리 상의 임시 저장소)
- EntityManager가 관리
- 1차 캐시, 동일성 보장, 쓰기 지연, 변경 감지 등의 기능 제공
┌──────────────────────────────┐
│ Application │
│ ↓ │
│ EntityManager │
│ ↓ │
│ Persistence Context │ ← 1차 캐시, 더티 체킹
│ ↓ │
│ Database │
└──────────────────────────────┘
2) 엔티티 상태 (Entity State)
new User()
↓
[비영속]
↓ persist()
[영속] ──────→ [준영속] (detach, clear, close)
↓ remove()
[삭제]
2-1) 비영속 (New/Transient)
JPA와 전혀 관계 없는 상태
User user = new User("Alice", "alice@example.com");
// 아직 영속성 컨텍스트에 없음
2-2) 영속 (Managed)
영속성 컨텍스트가 관리하는 상태
entityManager.persist(user); // 영속 상태로 전환
// 이 시점에는 아직 INSERT 쿼리 실행 안 됨!
영속 상태의 특징:
- 1차 캐시에 저장
- 변경 감지 (Dirty Checking)
- 쓰기 지연 (Write-Behind)
2-3) 준영속 (Detached)
영속성 컨텍스트에서 분리된 상태
entityManager.detach(user); // 준영속 상태
// 또는
entityManager.clear(); // 영속성 컨텍스트 전체 초기화
2-4) 삭제 (Removed)
삭제 예정 상태
entityManager.remove(user); // 삭제 상태
// 트랜잭션 커밋 시 DELETE 쿼리 실행
3) 영속성 컨텍스트의 핵심 기능
3-1) 1차 캐시
영속성 컨텍스트 내부에 엔티티를 캐싱
// 첫 번째 조회: DB에서 가져와서 1차 캐시에 저장
User user1 = entityManager.find(User.class, 1L);
// SELECT * FROM users WHERE id = 1
// 두 번째 조회: 1차 캐시에서 반환 (쿼리 안 나감!)
User user2 = entityManager.find(User.class, 1L);
System.out.println(user1 == user2); // true (같은 객체)
장점:
- 같은 트랜잭션 내에서 반복 조회 시 쿼리 절약
- 동일성(Identity) 보장:
==비교 가능
한계:
- 트랜잭션 범위 안에서만 유효 (글로벌 캐시 아님)
- 다른 트랜잭션에서는 효과 없음
3-2) 쓰기 지연 (Transactional Write-Behind)
INSERT/UPDATE/DELETE를 모았다가 한 번에 실행
@Transactional
public void saveUsers() {
entityManager.persist(new User("Alice", "alice@example.com"));
entityManager.persist(new User("Bob", "bob@example.com"));
entityManager.persist(new User("Charlie", "charlie@example.com"));
// 여기까지 INSERT 쿼리 실행 안 됨!
// 트랜잭션 커밋 시 한 번에 3개 INSERT 실행
}
장점:
- 쿼리 배치 실행 가능 (성능 향상)
- 트랜잭션 범위 내에서 일관성 유지
3-3) 변경 감지 (Dirty Checking)
영속 상태의 엔티티 변경을 자동으로 감지하고 UPDATE
@Transactional
public void updateUser(Long userId) {
User user = entityManager.find(User.class, userId); // 영속 상태
user.setName("Updated Name"); // 엔티티 수정
// entityManager.update(user); 같은 코드 불필요!
// 트랜잭션 커밋 시 자동으로 UPDATE 쿼리 실행
}
// 실행되는 쿼리:
// SELECT * FROM users WHERE id = ?
// UPDATE users SET name = 'Updated Name' WHERE id = ?
동작 원리:
- 최초 조회 시 스냅샷 저장
- 트랜잭션 커밋 시 현재 상태와 스냅샷 비교
- 변경 감지 시 UPDATE 쿼리 자동 생성
3-4) 지연 로딩 (Lazy Loading)
연관된 엔티티를 실제 사용할 때 조회 (나중에 자세히 다룸)
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
private User user;
}
Order order = entityManager.find(Order.class, 1L);
// Order만 조회, User는 아직 조회 안 함
String userName = order.getUser().getName();
// 이 시점에 User 조회 (SELECT)
4) 기본 CRUD 작업
4-1) EntityManager 직접 사용
@Service
@Transactional
public class UserService {
@PersistenceContext // EntityManager 주입
private EntityManager entityManager;
// Create
public User save(User user) {
entityManager.persist(user);
return user;
}
// Read
public User findById(Long id) {
return entityManager.find(User.class, id);
}
// Update
public User update(Long id, String newName) {
User user = entityManager.find(User.class, id);
user.setName(newName); // 더티 체킹으로 자동 UPDATE
return user;
}
// Delete
public void delete(Long id) {
User user = entityManager.find(User.class, id);
entityManager.remove(user);
}
// JPQL 쿼리
public List<User> findByName(String name) {
return entityManager.createQuery(
"SELECT u FROM User u WHERE u.name = :name", User.class)
.setParameter("name", name)
.getResultList();
}
}
4-2) Spring Data JPA Repository (더 간단)
// 인터페이스만 정의하면 구현체 자동 생성!
public interface UserRepository extends JpaRepository<User, Long> {
// 메서드 이름으로 쿼리 자동 생성
List<User> findByName(String name);
Optional<User> findByEmail(String email);
List<User> findByNameContaining(String keyword);
}
@Service
@Transactional
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User save(User user) {
return userRepository.save(user); // persist or merge
}
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
public List<User> findByName(String name) {
return userRepository.findByName(name);
}
public void delete(Long id) {
userRepository.deleteById(id);
}
}
5) 자주 하는 실수
5-1) 영속성 컨텍스트 밖에서 엔티티 수정
// ❌ @Transactional 없음 → 변경 감지 안 됨
public void updateUser(Long id, String newName) {
User user = userRepository.findById(id).orElseThrow();
user.setName(newName); // UPDATE 쿼리 실행 안 됨!
}
// ✅ @Transactional 필수
@Transactional
public void updateUser(Long id, String newName) {
User user = userRepository.findById(id).orElseThrow();
user.setName(newName); // 트랜잭션 커밋 시 UPDATE 실행
}
5-2) 준영속 엔티티 수정 시도
// ❌ 준영속 상태의 엔티티
User user = new User(1L, "Alice", "alice@example.com"); // 직접 생성
user.setName("Updated"); // 변경 감지 안 됨!
// ✅ 병합(merge) 또는 다시 조회
@Transactional
public void update(User user) {
User managed = entityManager.merge(user); // 영속 상태로 전환
managed.setName("Updated");
}
// 또는
@Transactional
public void update(Long id, String newName) {
User user = entityManager.find(User.class, id); // 영속 상태로 조회
user.setName(newName);
}
5-3) 식별자 직접 설정 시 persist vs merge
User user = new User();
user.setId(1L); // 식별자 직접 설정
user.setName("Alice");
entityManager.persist(user); // ❌ 이미 ID가 있으면 예외 발생 가능
// ✅ merge 사용 (존재하면 UPDATE, 없으면 INSERT)
entityManager.merge(user);
6) 실전 팁
6-1) @GeneratedValue 전략 선택
// IDENTITY: MySQL/PostgreSQL AUTO_INCREMENT
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// SEQUENCE: Oracle/PostgreSQL Sequence
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
@SequenceGenerator(name = "user_seq", sequenceName = "user_sequence")
private Long id;
// TABLE: 모든 DB 지원 (성능 낮음)
@GeneratedValue(strategy = GenerationType.TABLE)
private Long id;
6-2) 엔티티 설계 원칙
@Entity
public class User {
// ✅ 기본 생성자: protected (외부 직접 생성 방지)
protected User() {}
// ✅ 생성자: 필수 값만 받기
public User(String name, String email) {
this.name = name;
this.email = email;
}
// ✅ Setter 최소화 (의미 있는 메서드 제공)
public void changeName(String newName) {
if (newName == null || newName.isBlank()) {
throw new IllegalArgumentException("Name cannot be empty");
}
this.name = newName;
}
}
6-3) @Transactional 위치
// ✅ Service 계층에 @Transactional
@Service
@Transactional(readOnly = true) // 기본 읽기 전용
public class UserService {
@Transactional // 쓰기 작업만 readOnly = false
public User save(User user) {
return userRepository.save(user);
}
public User findById(Long id) {
return userRepository.findById(id).orElseThrow();
}
}
연습 (추천)
간단한 엔티티 생성
- User, Post 엔티티 설계
- CRUD 작업 구현
영속성 컨텍스트 동작 확인
- 1차 캐시 확인 (같은 ID 두 번 조회)
- 더티 체킹 확인 (엔티티 수정 후 UPDATE 쿼리 확인)
- 쿼리 로그 활성화:
spring.jpa.show-sql=true
실수 재현
- @Transactional 없이 엔티티 수정
- 준영속 엔티티 수정 시도
요약: 스스로 점검할 것
- JPA/Hibernate/Spring Data JPA의 관계를 설명할 수 있다
- 영속성 컨텍스트의 역할(1차 캐시, 쓰기 지연, 더티 체킹)을 이해한다
- 엔티티의 4가지 상태(비영속/영속/준영속/삭제)를 구분할 수 있다
- @Transactional 없이 엔티티를 수정하면 안 되는 이유를 안다
- EntityManager vs Repository의 차이를 설명할 수 있다
다음 단계
- JPA 연관관계 매핑:
/learning/deep-dive/deep-dive-jpa-associations/ - Spring Data JPA:
/learning/deep-dive/deep-dive-spring-data-jpa/ - JPA N+1 문제:
/learning/deep-dive/deep-dive-jpa-n-plus-1/ - JPA 트랜잭션 경계:
/learning/deep-dive/deep-dive-jpa-transaction-boundaries/
💬 댓글