JPA 완벽 가이드
💡 핵심: Java 객체와 관계형 DB를 연결하는 다리
SQL 없이 객체 중심으로 데이터베이스를 다룰 수 있는 ORM 표준 기술
🎯 JPA란 무엇인가?
정의
JPA (Java Persistence API)
- **Java 객체(Object)**와 **관계형 데이터베이스(RDB)**를 매핑해주는
- ORM 표준 인터페이스
근본적인 문제: 패러다임 불일치
// Java의 세계: 객체 지향
Member member = new Member("kobe");
member.updateEmail("kobe@example.com");
// 데이터베이스의 세계: 관계형 테이블
INSERT INTO member (name) VALUES ('kobe');
UPDATE member SET email = 'kobe@example.com' WHERE id = 1;
문제점:
Java 세계 Database 세계
↓ ↓
객체 테이블
상속 외래키
참조 JOIN
캡슐화 정규화
→ 이 간극을 해결하는 것이 ORM (Object Relational Mapping)
🔍 JPA = 인터페이스, Hibernate = 구현체
계층 구조
┌─────────────────────────────────────┐
│ Spring Data JPA (Repository) │ ← 개발자가 주로 사용
├─────────────────────────────────────┤
│ Hibernate (구현체) │ ← 실제 SQL 생성
├─────────────────────────────────────┤
│ JPA (표준 명세) │ ← 인터페이스
└─────────────────────────────────────┘
비교 정리
| 구분 | 역할 | 비유 |
|---|---|---|
| JPA | 인터페이스 (표준) | List 인터페이스 |
| Hibernate | JPA 구현체 | ArrayList 구현체 |
| EclipseLink | 또 다른 JPA 구현체 | LinkedList 구현체 |
Spring Boot의 기본 선택: Hibernate
🚀 JPA를 쓰면 무엇이 달라지나?
Before: SQL 직접 작성
// SQL을 직접 작성하고 관리
public class MemberDao {
public Member findById(Long id) {
String sql = "SELECT * FROM member WHERE id = ?";
// PreparedStatement 설정
// ResultSet 매핑
// 예외 처리
// 리소스 정리
// ... 반복적인 코드 50줄
}
public void save(Member member) {
String sql = "INSERT INTO member (name, email) VALUES (?, ?)";
// ... 또 50줄
}
}
After: 객체 중심 개발
// JPA Repository 사용
public interface MemberRepository extends JpaRepository<Member, Long> {
// 끝! 기본 CRUD 자동 제공
}
// 사용
Member member = memberRepository.findById(1L).orElseThrow();
memberRepository.save(member);
차이점:
- ❌ SQL 직접 작성 → ✅ 메서드 호출
- ❌ ResultSet 매핑 → ✅ 자동 매핑
- ❌ Connection 관리 → ✅ 자동 관리
- ❌ 반복적인 CRUD → ✅ 자동 생성
🔥 핵심 개념 3가지
1. Entity (엔티티)
DB 테이블과 매핑되는 Java 클래스
@Entity
@Table(name = "members")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String name;
@Column(nullable = false, unique = true)
private String email;
}
자동 생성되는 테이블:
CREATE TABLE members (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE
);
2. EntityManager (엔티티 매니저)
엔티티를 관리하는 핵심 객체
@PersistenceContext
private EntityManager entityManager;
// CRUD 작업
entityManager.persist(member); // INSERT
entityManager.find(Member.class, 1L); // SELECT
entityManager.remove(member); // DELETE
하지만 실무에서는:
// EntityManager 직접 사용 X
// Spring Data JPA Repository 사용 ✅
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);
List<Member> findByNameContaining(String name);
}
3. 영속성 컨텍스트 (Persistence Context)
엔티티를 관리하는 1차 캐시 + 변경 감지 환경
@Transactional
public void updateMember(Long id, String newEmail) {
// 1. 조회 (DB → 영속성 컨텍스트)
Member member = memberRepository.findById(id).orElseThrow();
// 2. 수정 (영속성 컨텍스트에서 변경 감지)
member.updateEmail(newEmail);
// 3. 저장 호출 불필요!
// memberRepository.save(member); ← 필요 없음!
// 트랜잭션 종료 시 자동 UPDATE 쿼리 실행
}
영속성 컨텍스트의 마법:
1차 캐시
┌─────────────────────────┐
│ Member(id=1) → 캐싱 │
│ Member(id=2) → 캐싱 │
└─────────────────────────┘
↓
같은 트랜잭션에서 재조회 시
→ DB 접근 없이 캐시 반환 (성능↑)
변경 감지 (Dirty Checking)
┌─────────────────────────┐
│ 조회 시 스냅샷 저장 │
│ 수정 후 변경사항 감지 │
│ 트랜잭션 종료 시 UPDATE │
└─────────────────────────┘
↓
save() 호출 없이 자동 UPDATE
📊 JPA vs JDBC 코드 비교
JDBC: 100줄의 반복
public class MemberDao {
private final DataSource dataSource;
public void save(Member member) {
String sql = "INSERT INTO member (name, email) VALUES (?, ?)";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS)) {
pstmt.setString(1, member.getName());
pstmt.setString(2, member.getEmail());
pstmt.executeUpdate();
ResultSet rs = pstmt.getGeneratedKeys();
if (rs.next()) {
member.setId(rs.getLong(1));
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
// update, delete, findAll... 계속 반복
}
JPA: 10줄의 간결함
public interface MemberRepository extends JpaRepository<Member, Long> {
// 기본 CRUD 자동 제공:
// - save(member)
// - findById(id)
// - findAll()
// - delete(member)
}
// 커스텀 쿼리도 메서드 이름으로
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);
List<Member> findByNameContaining(String keyword);
}
🎯 언제 JPA를 사용하나?
✅ JPA가 매우 적합한 경우 (⭐⭐⭐⭐⭐)
| 프로젝트 유형 | 특징 | 예시 |
|---|---|---|
| CRUD 중심 웹 서비스 | 기본적인 등록/조회/수정/삭제 | 회원 관리, 상품 관리 |
| 관리자 페이지 | 복잡하지 않은 비즈니스 로직 | 백오피스, 대시보드 |
| 쇼핑몰 | 주문, 결제, 재고 관리 | 커머스 서비스 |
| 게시판/커뮤니티 | 게시글, 댓글, 좋아요 | SNS, 커뮤니티 |
| REST API 서버 | Entity → DTO 변환 | 모바일 백엔드 |
🔶 상황에 따라 판단 필요 (⭐⭐⭐)
| 상황 | 해결 방법 | 비고 |
|---|---|---|
| 복잡한 통계 쿼리 | QueryDSL, JPQL 사용 | 동적 쿼리 생성 가능 |
| 대용량 조회 | Batch Size, Fetch Join | N+1 문제 해결 필요 |
| 특정 DB 기능 | Native Query 사용 | DB 벤더 종속성 발생 |
❌ JPA가 불리한 경우 (⭐)
| 상황 | 이유 | 대안 |
|---|---|---|
| 초고속 로그 수집 | 영속성 컨텍스트 오버헤드 | JDBC Batch Insert |
| 대규모 배치 처리 | 메모리 부담 | JdbcTemplate |
| 레거시 복잡한 SQL | 기존 SQL 그대로 사용 필요 | MyBatis |
| 극한의 성능 최적화 | ORM 추상화 레이어 오버헤드 | Native Query |
🛠️ JPA 사용의 5가지 이점
1. 생산성 증가 ⚡
// JDBC: 반복적인 CRUD 코드 작성
// 100줄 × 10개 테이블 = 1,000줄
// JPA: Repository 인터페이스만 선언
// 10줄 × 10개 테이블 = 100줄
→ 코드량 90% 감소
2. 객체지향 개발 가능 🎯
// 객체 그래프 탐색
Order order = orderRepository.findById(1L).orElseThrow();
Member member = order.getMember(); // JOIN 쿼리 자동 실행
String memberName = member.getName();
// 연관관계 편리
order.addOrderItem(new OrderItem(product, 5));
orderRepository.save(order); // OrderItem도 자동 저장
3. SQL 작성 감소 📝
// 메서드 이름만으로 쿼리 자동 생성
List<Member> findByNameAndEmailContaining(String name, String email);
// 실행되는 SQL (자동 생성)
// SELECT * FROM member
// WHERE name = ? AND email LIKE ?
4. 유지보수 용이 🔧
// 컬럼 추가 시
@Entity
public class Member {
private String name;
private String email;
private String phone; // 컬럼 추가
}
// SQL 수정 불필요! DDL 자동 업데이트
5. 트랜잭션 관리 편리 💼
@Transactional
public void updateMemberAndOrder(Long memberId, Long orderId) {
Member member = memberRepository.findById(memberId).orElseThrow();
Order order = orderRepository.findById(orderId).orElseThrow();
member.updateEmail("new@email.com");
order.updateStatus(OrderStatus.SHIPPED);
// save() 불필요
// 트랜잭션 커밋 시 자동으로 UPDATE
}
📋 핵심 요약
JPA의 3가지 핵심
1. Entity → 테이블과 매핑되는 객체
2. EntityManager → 엔티티를 관리하는 핵심
3. 영속성 컨텍스트 → 1차 캐시 + 변경 감지
JPA vs JDBC
| 항목 | JDBC | JPA |
|---|---|---|
| 코드량 | 많음 (100줄) | 적음 (10줄) |
| 생산성 | 낮음 | 높음 |
| SQL 작성 | 직접 작성 | 자동 생성 |
| 객체지향 | 어려움 | 자연스러움 |
| 성능 튜닝 | 세밀함 | 추가 학습 필요 |
사용 기준
// CRUD 중심, 일반 웹 서비스
→ JPA + Spring Data JPA ✅
// 복잡한 쿼리 추가 필요
→ JPA + QueryDSL ✅
// 대용량 배치, 극한 성능
→ JdbcTemplate / MyBatis ✅
🎯 실전 예제
기본 Entity 설계
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "members")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String name;
@Column(nullable = false, unique = true)
private String email;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
@Builder
public Member(String name, String email) {
this.name = name;
this.email = email;
}
public void updateEmail(String email) {
this.email = email;
}
}
Repository 설계
public interface MemberRepository extends JpaRepository<Member, Long> {
// 이메일로 조회
Optional<Member> findByEmail(String email);
// 이름으로 검색
List<Member> findByNameContaining(String keyword);
// 이메일 존재 여부
boolean existsByEmail(String email);
// 커스텀 쿼리
@Query("SELECT m FROM Member m JOIN FETCH m.orders WHERE m.id = :id")
Optional<Member> findByIdWithOrders(@Param("id") Long id);
}
Service 설계
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberService {
private final MemberRepository memberRepository;
@Transactional
public MemberResponse createMember(MemberCreateRequest request) {
// 중복 체크
if (memberRepository.existsByEmail(request.getEmail())) {
throw new DuplicateEmailException();
}
// Entity 생성 및 저장
Member member = Member.builder()
.name(request.getName())
.email(request.getEmail())
.build();
Member saved = memberRepository.save(member);
return MemberResponse.from(saved);
}
public MemberResponse getMember(Long id) {
Member member = memberRepository.findById(id)
.orElseThrow(() -> new MemberNotFoundException(id));
return MemberResponse.from(member);
}
@Transactional
public void updateEmail(Long id, String newEmail) {
Member member = memberRepository.findById(id)
.orElseThrow(() -> new MemberNotFoundException(id));
// 변경 감지로 자동 UPDATE
member.updateEmail(newEmail);
}
}
💭 자주 묻는 질문 (FAQ)
Q1. JPA와 MyBatis 중 무엇을 선택해야 하나?
JPA 추천:
- CRUD 중심 프로젝트
- 빠른 개발 속도 필요
- 객체지향적 설계 선호
MyBatis 추천:
- 복잡한 SQL 많음
- 레거시 DB 연동
- SQL 완벽 제어 필요
Q2. save()는 언제 호출해야 하나?
// 1. 새로운 엔티티 저장 시
Member member = Member.builder()
.name("kobe")
.email("kobe@example.com")
.build();
memberRepository.save(member); // ✅ INSERT
// 2. 기존 엔티티 수정 시
@Transactional
public void updateMember(Long id) {
Member member = memberRepository.findById(id).orElseThrow();
member.updateName("New Name");
// memberRepository.save(member); ← 불필요!
// 변경 감지로 자동 UPDATE
}
Q3. 영속성 컨텍스트란 정확히 무엇인가?
@Transactional
public void example() {
// 1. 조회 (DB → 영속성 컨텍스트)
Member member1 = memberRepository.findById(1L).orElseThrow();
// 2. 같은 ID로 재조회 (캐시에서 반환, DB 접근 X)
Member member2 = memberRepository.findById(1L).orElseThrow();
// 3. 동일성 보장
System.out.println(member1 == member2); // true
// 4. 변경 감지
member1.updateName("New Name");
// 트랜잭션 커밋 시 자동 UPDATE
}
🎯 마지막 체크포인트
다음 질문에 답할 수 있다면 이해 완료:
- JPA가 무엇인지 설명할 수 있는가?
- JPA와 Hibernate의 차이를 아는가?
- Entity, EntityManager, 영속성 컨텍스트를 설명할 수 있는가?
- JPA가 적합한 상황과 불리한 상황을 구분할 수 있는가?
- save() 호출이 필요한 시점을 아는가?
Remember:
JPA = 객체와 DB를 연결하는 다리
생산성 ↑ 코드량 ↓ 유지보수성 ↑
CRUD 중심 웹 서비스의 거의 필수 기술