C
Spring Boot/JPA/Lesson 08

JPA — *자바 객체로 DB 다루기*

60분·theory
이 챕터
1/2

JPA — *자바 객체로 DB 다루기*

🎯 이 lesson 을 읽고 나면

이 lesson 을 다 읽고 나면 아래 3가지를 자신 있게 할 수 있습니다.

  • ✅ JpaRepository 만 상속해서 0줄 코드로 CRUD 구현
  • ✅ N+1 문제 → @EntityGraph 또는 JOIN FETCH 로 해결
  • ✅ Lazy vs Eager + 실무에서 무조건 Lazy 인 이유

학습 목표를 체크리스트로 두고 다 답할 수 있게 되면 lesson 을 닫으세요.

JPA 가 뭐냐 — *SQL 안 쓰고도 DB 사용*

핵심 한 줄

JPA (Java Persistence API) = 자바 객체를 DB 테이블에 자동 매핑 해주는 표준. SQL 직접 쓰지 않고 자바 메서드 호출만 으로 CRUD 가능.

옛 방식 — JDBC 의 고통

java
// 사용자 1명 조회
Connection conn = DriverManager.getConnection(...);
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
ps.setLong(1, 42L);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
    User u = new User();
    u.setId(rs.getLong("id"));
    u.setName(rs.getString("name"));
    u.setEmail(rs.getString("email"));
    // ... 30 줄
}
rs.close(); ps.close(); conn.close();

매번 연결 관리·SQL 작성·결과 매핑 다 직접. 500개 테이블이면 수만 줄 의 반복 코드.

새 방식 — JPA 의 마법

java
@Entity
@Table(name = "users")
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private String email;
}

// 사용
User u = entityManager.find(User.class, 42L);     // 1줄
u.setName("홍길동 수정");                            // setter 호출만으로 UPDATE 예약
// 트랜잭션 종료 시 자동 UPDATE

SQL 한 줄 없이 자바 객체 만 다루면 DB 가 알아서 동기화됩니다. "객체로 일하고 DB 는 잊는다" — JPA 의 핵심 사상.

Hibernate vs JPA

  • JPA표준 명세 (인터페이스). javax.persistence 패키지
  • Hibernate구현체. 가장 인기 (95%+ 점유). EclipseLink·OpenJPA 도 있음

Spring Data JPA = Spring 이 JPA 를 더 쉽게 쓰게 해주는 추상화. 실무 표준 스택.

Entity 매핑 — 객체와 테이블 짝짓기

java
@Entity                              // JPA 관리 대상
@Table(name = "users")                // 매핑 테이블명
public class User {

    @Id                              // PK
    @GeneratedValue(strategy = IDENTITY)  // 자동 증가
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @Column(unique = true, nullable = false)
    private String email;

    @Enumerated(EnumType.STRING)      // enum → 문자열 컬럼
    private UserStatus status;

    @CreationTimestamp                 // 생성 시각 자동
    private LocalDateTime createdAt;

    @UpdateTimestamp                   // 수정 시각 자동
    private LocalDateTime updatedAt;
}

관계 매핑 — 외래키를 객체 참조로

java
@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne                        // 다대일 (여러 주문 → 한 사용자)
    @JoinColumn(name = "user_id")     // FK 컬럼
    private User user;
}

@Entity
public class User {
    @OneToMany(mappedBy = "user")     // 일대다 (한 사용자 → 여러 주문)
    private List<Order> orders;
}

DB 의 외래키 관계자바 객체 참조 로 변환. order.getUser().getName() 처럼 자연스럽게 탐색.

영속성 컨텍스트 — JPA 의 비밀병기

JPA 는 영속성 컨텍스트 (Persistence Context) 라는 1차 캐시 를 가집니다. 한 트랜잭션 안에서 같은 엔티티는 1번만 DB 조회.

java
@Transactional
public void update(Long id) {
    User u1 = userRepo.findById(id).get();    // DB SELECT
    User u2 = userRepo.findById(id).get();    // 캐시 hit, DB 안 감
    System.out.println(u1 == u2);              // true (같은 객체)

    u1.setName("변경");
    // 별도 .save() 호출 안 해도 → 트랜잭션 종료 시 자동 UPDATE
    // 이게 "dirty checking"
}

Dirty Checking — JPA 가 시작 시점 vs 종료 시점 의 객체를 비교해서 변경된 필드만 UPDATE. setter 호출만으로 자동 저장됩니다.

한 번 정리

  • JPA = 자바 객체 ↔ DB 테이블 자동 매핑 표준
  • Hibernate = 가장 인기 구현체
  • Spring Data JPA = Spring 이 더 쉽게 해주는 추상화
  • Entity 어노테이션으로 매핑, 관계는 객체 참조, 변경은 자동 감지

Spring Data JpaRepository — *쿼리 자동 생성*

핵심 한 줄

JpaRepository 인터페이스만 정의하면 기본 CRUD 메서드자동 만들어집니다. 게다가 메서드 이름만 봐도 쿼리를 추측해 자동 생성.

기본 CRUD — 즉시 사용 가능

java
public interface UserRepository extends JpaRepository<User, Long> {
}

이 한 줄로 다음 모든 메서드 가 제공됩니다:

java
userRepo.save(user);              // INSERT 또는 UPDATE
userRepo.findById(42L);           // SELECT WHERE id=?
userRepo.findAll();               // SELECT *
userRepo.delete(user);            // DELETE
userRepo.count();                 // COUNT(*)
userRepo.existsById(42L);         // EXISTS

쿼리 한 줄 안 짜고도 전체 CRUD 가능. 마법 같죠.

쿼리 메서드 — 이름이 곧 쿼리

java
public interface UserRepository extends JpaRepository<User, Long> {
    // findBy + 필드명 → 자동 쿼리
    Optional<User> findByEmail(String email);
    // → SELECT * FROM users WHERE email = ?

    List<User> findByAgeGreaterThan(int age);
    // → SELECT * FROM users WHERE age > ?

    List<User> findByStatusAndCreatedAtAfter(Status s, LocalDateTime t);
    // → SELECT * FROM users WHERE status = ? AND created_at > ?

    boolean existsByEmail(String email);
    long countByStatus(Status s);
}

규칙: find·exists·count·delete 로 시작 + By + 필드명·조건어. Spring 이 파싱해서 쿼리 만들어줍니다.

@Query — 복잡한 쿼리

이름 규칙으로 표현 안 되면 직접:

java
public interface UserRepository extends JpaRepository<User, Long> {

    @Query("SELECT u FROM User u WHERE u.email LIKE %:keyword% OR u.name LIKE %:keyword%")
    List<User> search(@Param("keyword") String keyword);

    @Modifying       // INSERT·UPDATE·DELETE 에 필수
    @Query("UPDATE User u SET u.status = :status WHERE u.lastLogin < :date")
    int deactivateInactive(@Param("status") Status status, @Param("date") LocalDateTime date);
}

JPQL (JPA Query Language) — SQL 비슷하지만 테이블 이름이 아닌 엔티티 이름 사용. DB 독립적.

native SQL 도 가능:

java
@Query(value = "SELECT * FROM users WHERE ...", nativeQuery = true)

QueryDSL — 동적 쿼리의 표준

@Query 도 한계가 있습니다 — 조건이 동적 일 때 (검색 필터). 그럴 땐 QueryDSL:

java
QUser u = QUser.user;
BooleanBuilder where = new BooleanBuilder();
if (keyword != null) where.and(u.name.contains(keyword));
if (status != null)  where.and(u.status.eq(status));

List<User> result = queryFactory.selectFrom(u).where(where).fetch();

타입 안전하고 컴파일 타임에 검증. Spring Data JPA 와 함께 실무 표준 입니다.

페이징 — 자동 지원

java
Page<User> page = userRepo.findByStatus(Status.ACTIVE, PageRequest.of(0, 20));

page.getContent();          // 현재 페이지 결과
page.getTotalElements();    // 전체 개수
page.getTotalPages();        // 전체 페이지

Pageable 만 인자로 받으면 자동으로 LIMIT·OFFSET 처리. 응답 헤더에 페이징 정보 도 자동.

한 번 정리

  • JpaRepository 만 상속하면 기본 CRUD 무료
  • 메서드 이름으로 쿼리 자동 생성
  • 복잡하면 @Query 또는 QueryDSL
  • 페이징·정렬도 Pageable 한 줄로

N+1 문제 — *JPA 의 가장 흔한 함정*

문제 상황

java
@Transactional
public void printAllOrders() {
    List<User> users = userRepo.findAll();      // 1번: SELECT * FROM users
    for (User u : users) {
        System.out.println(u.getOrders().size());  // N번: SELECT * FROM orders WHERE user_id = ?
    }
}

사용자 100명이면 총 101번 쿼리. 사용자 1만명이면 1만 1번. 페이지 응답 시간이 5초 → 50초 로 폭증.

이게 N+1 문제. JPA 의 지연 로딩 (Lazy Loading) 이 의도와 다르게 동작하는 클래식 함정.

왜 발생하나

JPA 는 연관 객체 를 기본적으로 Lazy 로 로드합니다. user.getOrders()처음 호출하는 순간 DB 에 추가 쿼리. 한 번에 모두 가져오면 불필요한 데이터 가 많을 수 있으니 필요할 때만 가져오는 거죠.

하지만 반복문 안 에서 호출하면 매번 새 쿼리 → N+1.

해결책 1 — Fetch Join

java
@Query("SELECT u FROM User u JOIN FETCH u.orders")
List<User> findAllWithOrders();

JOIN 으로 한 번에 모두 가져옵니다. 1번 쿼리로 끝.

생성되는 SQL:

sql
SELECT u.*, o.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;

해결책 2 — @EntityGraph

java
public interface UserRepository extends JpaRepository<User, Long> {
    @EntityGraph(attributePaths = "orders")
    List<User> findAll();
}

특정 메서드에만 eager 로드 적용. Fetch Join 보다 깔끔 합니다.

해결책 3 — @BatchSize

연관 데이터를 N+1 이 아닌 N/배치 단위 로 가져옵니다.

java
@Entity
public class User {
    @OneToMany
    @BatchSize(size = 100)
    private List<Order> orders;
}

사용자 1000명 → 10 쿼리 (100개씩). 완전 해결은 아니지만 큰 개선.

컬렉션 두 개 Fetch Join — 위험

java
@Query("SELECT u FROM User u JOIN FETCH u.orders JOIN FETCH u.reviews")
List<User> findAllFull();
// MultipleBagFetchException 폭발!

컬렉션 두 개를 동시 Fetch Join 하면 데이터 카테시안 곱 (M × N) 으로 폭발. 해결:

  • 하나만 Fetch Join + 나머지는 BatchSize
  • 또는 DTO Projection 으로 필요한 것만

DTO Projection — 더 깊은 최적화

Entity 전체 말고 필요한 필드만 조회:

java
public interface UserSummary {
    Long getId();
    String getName();
    Long getOrderCount();
}

@Query("SELECT u.id AS id, u.name AS name, COUNT(o) AS orderCount " +
       "FROM User u LEFT JOIN u.orders o " +
       "GROUP BY u.id, u.name")
List<UserSummary> findSummaries();

엔티티 매핑·Lazy 로딩 완전 우회. 가장 빠른 옵션입니다.

진단 — 로그로 확인

yaml
spring:
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true

또는 더 강력한 P6Spy — 실제 파라미터 값까지 보여줍니다. 모든 쿼리를 눈으로 확인 하면서 N+1 발생 즉시 발견.

한 번 정리

  • N+1 은 Lazy 로딩 + 반복문 의 만남
  • 해결: Fetch Join > @EntityGraph > @BatchSize > DTO Projection
  • show-sql 로 항상 쿼리 추적
  • 컬렉션 2개 Fetch Join 은 위험 — 하나만

🤖 AI 에게 이렇게 요청해보세요

이 lesson 의 개념을 알면 AI 에게 구체적으로 지시할 수 있습니다. 막연한 "고쳐줘" 가 아니라 어휘를 가진 요청 — 그게 토큰 절약의 출발점입니다.

  • "이 findAll + 반복문이 N+1 인데 @EntityGraph 로 해결해줘"
  • "이 Entity 를 UserResponse DTO 로 변환하는 static factory 메서드 추가해줘"
  • "JpaRepository 에 findByEmailAndActive 메서드 시그니처 추가해줘"

왜 이게 토큰을 줄이나

개념을 모를 땐 AI 답변을 받고도 "그게 뭐예요?" 를 다시 물어야 합니다. 그 "다시 물음" 이 토큰을 잡아먹습니다. 개념 한 번 익혀두면 대화가 한 번에 끝납니다.

JPA — 자바 객체로 DB 다루기 - Spring Boot