2026년 02월 11일

📦 JPA 완벽 가이드

Java Spring Boot
Cover Image

JPA 완벽 가이드

💡 핵심: Java 객체와 관계형 DB를 연결하는 다리
SQL 없이 객체 중심으로 데이터베이스를 다룰 수 있는 ORM 표준 기술


🎯 JPA란 무엇인가?

정의

JPA (Java Persistence API)

근본적인 문제: 패러다임 불일치

// 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 인터페이스
HibernateJPA 구현체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);

차이점:


🔥 핵심 개념 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 JoinN+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

항목JDBCJPA
코드량많음 (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 추천:

MyBatis 추천:

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
}

🎯 마지막 체크포인트

다음 질문에 답할 수 있다면 이해 완료:

Remember:

JPA = 객체와 DB를 연결하는 다리
생산성 ↑ 코드량 ↓ 유지보수성 ↑
CRUD 중심 웹 서비스의 거의 필수 기술
← 목록으로 돌아가기