F-LAB JAVA · 7주차 · Phase 3 · JPA 입문
★ 깊이 파기 — 자바 진영 ORM 표준의 본질
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
JPA (Java Persistence API) 는 자바 진영의 ORM 표준 인터페이스이며 Hibernate · EclipseLink 같은 구현체가 실제 동작을 담당하는 "인터페이스 + 구현" 구조로, 어노테이션 기반 매핑과 자동 SQL 생성을 제공해 SQL Mapper 의 한계를 해결하지만 복잡 통계는 여전히 네이티브 쿼리가 필요하고 show-sql 옵션으로 생성된 SQL 을 확인할 수 있다.
JPA (Java Persistence API) 는 자바 진영의 ORM 표준 인터페이스 로, JSR-338 명세로 정의된 자바 표준 (Jakarta EE 의 일부) 이다.
JPA 자체는 인터페이스/명세이고 실제 동작하는 구현체 는 — Hibernate (가장 대중적, 사실상 표준), EclipseLink (Eclipse 공식), DataNucleus, OpenJPA — 5주차의 DataSource (인터페이스) + HikariCP (구현체) 와 같은 구조다.
사용 방식은 어노테이션 기반 매핑 —@Entity, @Id, @Column, @ManyToOne, @OneToMany등을 객체에 선언하면 JPA 가 SQL 을 자동 생성 하고 객체 ↔ DB 변환을 처리한다.
단 JPA 도 SQL 완전 대체는 아니다 — 복잡 통계·윈도우 함수·DB 특수 기능은 네이티브 쿼리 가 필요하며, 생성된 SQL 은spring.jpa.show-sql=true+format-sql=true로 확인해 디버깅한다.
5주차의 인터페이스/구현 분리 정신이 JPA 에서도 — 애플리케이션은 JPA (표준) 에만 의존하므로 Hibernate 를 EclipseLink 로 교체해도 코드 그대로다 (이론상).
JPA = JDBC 의 ORM 버전:
JDBC (6주차) 와 JPA 비교:
JDBC:
- 자바의 DB 접근 표준 (인터페이스)
- Connection, Statement, ResultSet
- DB 드라이버가 구현
JPA:
- 자바의 ORM 표준 (인터페이스)
- EntityManager, @Entity, ...
- Hibernate, EclipseLink 가 구현
같은 패턴:
- 자바 표준 (javax.persistence 또는 jakarta.persistence)
- 다양한 구현체
- 인터페이스 의존 = OCP
핵심 차이:
- JDBC: SQL 직접
- JPA: SQL 자동 (객체 매핑)
어노테이션:
- @Entity: 이 객체는 DB 매핑
- @Id: PK
- @Column: 컬럼 매핑
- @OneToMany: 1:N 관계
자동 SQL:
- find(id) → SELECT
- persist(entity) → INSERT
- 객체 수정 → 자동 UPDATE
- remove → DELETE
복잡 쿼리 한계:
- 윈도우 함수, CTE 등
- 네이티브 쿼리 필요
- 또는 Querydsl
SQL 확인:
- show-sql: true
- 디버깅 / 학습 / 튜닝
→ JPA = 자바 ORM 표준 인터페이스, Hibernate 구현체, 어노테이션 자동 SQL, 복잡 쿼리는 보조.
1. JPA 정의
2. 자바 표준 인터페이스
3. 구현체 (Hibernate 등)
4. JPA vs Hibernate
5. JSR 과 Jakarta
6. 어노테이션 기반 매핑
7. SQL Mapper vs JPA 비교
8. SQL 완전 대체 X
9. SQL 확인 (show-sql)
JPA (Java Persistence API):
자바의 ORM 표준 명세:
- 인터페이스 모음
- 어노테이션 정의
- 동작 방식 규정
→ 자바 진영의 ORM 표준
"Persistence":
영속성 (지속성):
- 데이터를 영구 저장
- 메모리 → DB
- JVM 종료 후에도
→ "지속되는 데이터 다루기"
명세 vs 구현:
JPA = 명세 (Spec):
- 인터페이스
- 약속
- 동작 X (자체로는)
Hibernate = 구현체:
- JPA 명세 따라
- 실제 SQL 생성
- 실제 동작
→ "JPA 명세 + Hibernate 구현"
// JPA 사용 (ILIC)
import jakarta.persistence.*; // JPA (표준 인터페이스)
@Entity
@Table(name = "shipments")
public class Shipment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "bl_no", length = 50)
private String blNo;
@ManyToOne
@JoinColumn(name = "customer_id")
private Customer customer;
@OneToMany(mappedBy = "shipment")
private List<ShipmentItem> items;
}
// 사용
@PersistenceContext
private EntityManager em; // JPA 인터페이스
Shipment s = em.find(Shipment.class, 1L); // SQL 자동
em.persist(s); // SQL 자동
// → JPA 인터페이스만 의존
// → 실제는 Hibernate 가 동작
class Customer {}
class ShipmentItem {}
class EntityManager {
<T> T find(Class<T> c, Object id) { return null; }
void persist(Object o) {}
}
@interface PersistenceContext {}
JPA 의 정의는?
답:
1. JPA:
Persistence:
명세:
구현체:
표준의 의미:
JPA = 자바 공식 표준:
- JSR (Java Specification Request)
- Jakarta EE (구 Java EE)
- 자바 진영 합의
→ 모든 자바 개발자의 약속
패키지:
Java EE 시대:
- javax.persistence.*
Jakarta EE (현재):
- jakarta.persistence.*
Spring Boot 3+ 부터:
- jakarta.* 사용
표준 정의:
핵심 인터페이스:
- EntityManager (영속성 관리)
- EntityManagerFactory (팩토리)
- Query (쿼리)
- TypedQuery<T> (타입 안전 쿼리)
- EntityTransaction (트랜잭션)
핵심 어노테이션:
- @Entity, @Id, @GeneratedValue
- @Column, @Table
- @ManyToOne, @OneToMany, @ManyToMany
- @JoinColumn, @JoinTable
- + 30개 이상
표준의 가치:
1. 호환성:
- 한 번 학습 → 모든 구현체 사용
- 코드 이식성
2. 안정성:
- 표준 유지
- 갑작스러운 변경 X
3. 풍부한 생태계:
- 학습 자료
- 도구 (JPA Buddy 등)
- 커뮤니티
→ 자바 진영의 큰 강점
자바 외 ORM 과 차이:
Python SQLAlchemy:
- 사실상 표준 (de facto)
- 명세 X
- 단일 라이브러리
Ruby ActiveRecord:
- Rails 내장
- 사실상 표준
- 명세 X
Java JPA:
- 공식 명세 (JSR)
- 여러 구현체
- 인터페이스 + 구현체 분리
→ 자바는 표준 중심
// 표준 활용 (ILIC)
// JPA 표준 인터페이스
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.PersistenceContext;
// Spring Boot 가 자동으로 Hibernate 주입
@Service
public class ShipmentService {
@PersistenceContext
private EntityManager em; // JPA 인터페이스
public Shipment get(Long id) {
return em.find(Shipment.class, id);
// em 은 인터페이스
// 실제는 Hibernate 의 구현
}
}
// 코드는 표준만 의존
// 구현체 (Hibernate) 모름
// → 5주차 DI 정신
class Shipment {}
class EntityManager {
<T> T find(Class<T> c, Object id) { return null; }
}
자바 진영 ORM 표준 인터페이스의 의미는?
답:
1. 표준:
패키지:
인터페이스:
가치:
JPA 구현체:
1. Hibernate (가장 대중적)
- Red Hat 후원
- 사실상 표준
- 압도적 점유율
2. EclipseLink
- Eclipse 공식
- 참조 구현체 (Reference Impl)
3. DataNucleus
- 다양한 데이터 저장소
- 다목적
4. OpenJPA (Apache)
- Apache 프로젝트
- 덜 인기
→ Hibernate 가 압도적
Hibernate:
- 자바 최초 ORM (2001)
- JPA 등장 (2006) 전부터
- JPA 명세에 영향 줌
특징:
- 풍부한 기능
- 안정성
- 강력한 캐시 (1차/2차)
- HQL (Hibernate Query Language)
→ 사실상 자바 ORM = Hibernate
Spring Boot 의 기본:
spring-boot-starter-data-jpa:
- JPA 인터페이스
- Hibernate (구현체)
- Spring Data JPA (추상화)
- HikariCP (Connection Pool)
→ Spring Boot = JPA + Hibernate
구현체 교체:
이론적으로:
- Hibernate → EclipseLink
- 의존성 변경
- 코드 그대로
실제로:
- Hibernate 고유 기능 사용 시 어려움
- 일부 어노테이션 차이
- 거의 안 함
→ 보통 Hibernate 고정
// ILIC build.gradle (Spring Boot 3)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// 자동 포함:
// - JPA 인터페이스 (jakarta.persistence)
// - Hibernate 6.x (구현체)
// - Spring Data JPA
// - HikariCP
runtimeOnly 'com.mysql:mysql-connector-j'
}
// ILIC 코드:
// - JPA 인터페이스 (표준) 만 사용
// - 실제는 Hibernate 가 동작
// - 102 테이블 매핑
// JPA 코드 (ILIC) — 표준 어노테이션
import jakarta.persistence.*;
@Entity
@Table(name = "shipments")
public class Shipment {
@Id
@GeneratedValue
private Long id;
// 표준 어노테이션 사용
// 구현체 (Hibernate) 가 해석
}
JPA 구현체 (Hibernate, EclipseLink) 는?
답:
1. 목록:
Hibernate:
Spring Boot:
교체:
| 항목 | JPA | Hibernate |
|---|---|---|
| 성격 | 명세 (인터페이스) | 구현체 (라이브러리) |
| 패키지 | jakarta.persistence | org.hibernate |
| 어노테이션 | 표준 (@Entity 등) | 표준 + 자기 (@DynamicUpdate 등) |
| 쿼리 언어 | JPQL | HQL (JPQL + 확장) |
| 단독 사용 | X (구현체 필요) | O (가능) |
| 의존성 | 인터페이스만 | 인터페이스 + 구현 |
5주차 패턴 매핑:
JPA = 인터페이스 (추상)
Hibernate = 구현체 (구체)
- 5주차 DI 정신
- 5주차 DIP (인터페이스 의존)
- 6주차 DataSource 와 같은 구조
어떤 걸 학습:
먼저:
- JPA 표준 (인터페이스, 어노테이션)
- 95% 표준만으로 충분
깊이:
- Hibernate 고유 기능
- 캐시 (2차)
- 성능 튜닝
- 트러블슈팅
→ JPA 먼저 → Hibernate 깊이
// JPA 표준 (권장)
import jakarta.persistence.*;
@Entity
@Id
@Column
// Hibernate 고유 (필요 시)
import org.hibernate.annotations.*;
@DynamicUpdate // Hibernate 만
@DynamicInsert // Hibernate 만
@BatchSize(size = 100) // Hibernate 만
호환성:
JPA 표준만 사용:
- 다른 구현체 교체 가능
- 학습 비용 ↓
- 안전
Hibernate 고유 사용:
- 다른 구현체 교체 어려움
- 하지만 더 강력
- Hibernate 종속
→ 보통 둘 다 (필요한 만큼)
// ILIC 의 JPA + Hibernate 활용
// 1. 기본은 JPA 표준
@Entity
public class Shipment {
@Id @GeneratedValue
private Long id;
@Column(name = "bl_no")
private String blNo;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
}
// 2. 필요 시 Hibernate 고유
@Entity
@org.hibernate.annotations.DynamicUpdate // 변경된 컬럼만 UPDATE
@org.hibernate.annotations.BatchSize(size = 100) // 1+N/100
public class Shipment {
// ...
}
// → 90% JPA, 10% Hibernate
// → 균형
class Customer {}
JPA vs Hibernate 차이는?
답:
1. JPA:
Hibernate:
5주차 패턴:
학습:
JSR (Java Specification Request):
자바 공식 표준 명세 요청:
- JCP (Java Community Process)
- 새 표준 제안 → 검토 → 승인
- 명세 문서 발행
JPA 관련 JSR:
- JSR-220 (EJB 3.0 with JPA 1.0, 2006)
- JSR-317 (JPA 2.0, 2009)
- JSR-338 (JPA 2.1, 2013) / 2.2 (2017)
- JPA 3.0 (Jakarta EE, 2020+)
Java EE → Jakarta EE:
2017: Oracle 이 Java EE 를 Eclipse Foundation 으로 이양
이름 변경: Java EE → Jakarta EE
패키지 변경: javax → jakarta
Spring Boot 3.0+ : jakarta 사용
Spring Boot 2.x : 여전히 javax
→ 점진적 마이그레이션
명세의 의미:
- 인터페이스 정의
- 어노테이션 정의
- 동작 규정
- 호환성 보장
→ 모든 구현체가 따라야
JCP (Java Community Process):
- 자바 표준 관리 기구
- JSR 검토 / 승인
- Expert Group 운영
- 명세 발행
→ 자바 표준의 보증
JPA 버전:
JPA 1.0 (2006): EJB 3.0 과 함께
JPA 2.0 (2009): @ElementCollection, JPQL 강화
JPA 2.1 (2013): @ConstructorResult, @StoredProcedure
JPA 2.2 (2017): Stream<T> 지원
Jakarta Persistence 3.0 (2020): 패키지 jakarta
3.1 (2022), 3.2 (2024) ...
→ 점진적 발전
// JSR / Jakarta 영향 (ILIC)
// Spring Boot 3 (Jakarta)
import jakarta.persistence.*; // jakarta.*
// Spring Boot 2 (Java EE)
import javax.persistence.*; // javax.*
// 마이그레이션 시:
// 1. Spring Boot 2 → 3 업그레이드
// 2. javax → jakarta 일괄 변경
// 3. 호환성 테스트
// ILIC = Spring Boot 3+
// → jakarta.* 사용
class JakartaPersistence {}
JSR (Java Specification Request) 은?
답:
1. JSR:
JPA JSR:
Java EE → Jakarta:
JCP:
어노테이션 매핑:
객체에 어노테이션 선언:
- @Entity (이것은 엔티티)
- @Id (PK)
- @Column (컬럼 매핑)
- @ManyToOne (1:N 의 N 측)
- 등
→ XML 보다 간결
@Entity // ① 엔티티 선언
@Table(name = "shipments") // ② 테이블명
public class Shipment {
@Id // ③ PK
@GeneratedValue(strategy = GenerationType.IDENTITY) // ④ 자동 키
private Long id;
@Column(name = "bl_no", length = 50, nullable = false, unique = true)
private String blNo; // ⑤ 컬럼 매핑
@Column(name = "created_at")
private LocalDateTime createdAt;
@ManyToOne(fetch = FetchType.LAZY) // ⑥ 1:N 의 N
@JoinColumn(name = "customer_id") // ⑦ FK
private Customer customer;
@OneToMany(mappedBy = "shipment", // ⑧ 1:N 의 1
cascade = CascadeType.ALL)
private List<ShipmentItem> items;
@Enumerated(EnumType.STRING) // ⑨ Enum 매핑
private ShipmentStatus status;
}
enum ShipmentStatus { BOOKED, SHIPPED, DELIVERED }
class Customer {}
class ShipmentItem {}
매핑 어노테이션 분류:
엔티티 선언:
@Entity, @Table
필드 매핑:
@Id, @GeneratedValue
@Column, @Transient (제외)
@Enumerated, @Temporal
관계 매핑:
@ManyToOne, @OneToMany
@ManyToMany, @OneToOne
@JoinColumn, @JoinTable
상속 매핑:
@Inheritance
@DiscriminatorColumn
@DiscriminatorValue
+ 라이프사이클 콜백, 검증 등
<!-- XML 매핑 (거의 사용 X) -->
<entity class="com.ilic.Shipment">
<table name="shipments"/>
<attributes>
<id name="id">
<generated-value strategy="IDENTITY"/>
</id>
<basic name="blNo">
<column name="bl_no"/>
</basic>
</attributes>
</entity>
<!-- 어노테이션이 압도적 -->
어노테이션 장점:
- 코드와 함께 (직관)
- 짧음
- 타입 안전 (IDE 지원)
- 리팩토링 쉬움
XML 단점:
- 별도 파일
- 길음
- 타입 안전 X
- 동기화 부담
→ 어노테이션 압도적
// ILIC 의 엔티티 매핑 (실제)
@Entity
@Table(name = "shipments")
public class Shipment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "bl_no", length = 50, nullable = false, unique = true)
private String blNo;
@Column(name = "weight", precision = 10, scale = 2)
private BigDecimal weight;
@Enumerated(EnumType.STRING)
@Column(name = "status", length = 20)
private ShipmentStatus status;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
@OneToMany(mappedBy = "shipment", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ShipmentItem> items = new ArrayList<>();
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
void onCreate() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
@PreUpdate
void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}
// → ILIC 의 102 테이블이 이런 패턴
enum ShipmentStatus { BOOKED, CONFIRMED, SHIPPED, DELIVERED, CANCELLED }
class Customer {}
class ShipmentItem {}
어노테이션 기반 매핑은?
답:
1. 어노테이션:
분류:
XML 대비:
표준:
| 항목 | SQL Mapper (JdbcTemplate, MyBatis) | JPA |
|---|---|---|
| SQL 작성 | 개발자 직접 | JPA 자동 생성 |
| 매핑 | RowMapper 수동 | 어노테이션 선언 |
| 객체 그래프 | 수동 처리 | 자동 (Lazy/Eager) |
| 변경 감지 | UPDATE 직접 | Dirty Checking 자동 |
| 1차 캐시 | 없음 | 자동 (영속성 컨텍스트) |
| 학습 곡선 | 낮음 | 높음 |
| 복잡 쿼리 | 자유 | 어려움 (네이티브) |
| DB 종속 | 높음 | 낮음 (Dialect) |
| 생산성 | 보통 | 매우 높음 |
// SQL Mapper (JdbcTemplate)
@Repository
public class ShipmentDao {
private final JdbcTemplate jdbcTemplate;
public Shipment get(Long id) {
return jdbcTemplate.queryForObject(
"SELECT * FROM shipments WHERE id = ?",
(rs, n) -> {
Shipment s = new Shipment();
s.setId(rs.getLong("id"));
s.setBlNo(rs.getString("bl_no"));
// ... 모든 컬럼 매핑
return s;
},
id);
}
public void update(Shipment s) {
jdbcTemplate.update(
"UPDATE shipments SET bl_no = ?, weight = ? WHERE id = ?",
s.getBlNo(), s.getWeight(), s.getId());
}
}
// JPA
@Repository
public interface ShipmentRepository extends JpaRepository<Shipment, Long> {
// CRUD 자동 (findById, save, ...)
// 추가 메서드 (메서드 이름 자동 SQL)
List<Shipment> findByStatus(String status);
}
// 사용
Shipment s = shipmentRepository.findById(1L).orElseThrow();
s.setStatus("SHIPPED");
// 자동 UPDATE (Dirty Checking, 트랜잭션 commit 시)
class Shipment {
void setId(Long id) {}
void setBlNo(String s) {}
void setStatus(String s) {}
String getBlNo() { return null; }
java.math.BigDecimal getWeight() { return null; }
Long getId() { return null; }
}
JdbcTemplate jdbcTemplate;
class JdbcTemplate {
<T> T queryForObject(String s, RowMapper<T> r, Object... a) { return null; }
void update(String s, Object... a) {}
interface RowMapper<T> { T mapRow(ResultSet rs, int n) throws SQLException; }
}
ShipmentRepository shipmentRepository;
interface JpaRepository<T, ID> { java.util.Optional<T> findById(ID id); }
interface ShipmentRepository extends JpaRepository<Shipment, Long> {
java.util.List<Shipment> findByStatus(String status);
}
코드 줄 비교 (단순 CRUD):
SQL Mapper:
- DAO ~50 줄
- SQL 직접
JPA:
- @Entity 1 클래스
- Repository 인터페이스 (메서드만)
- ~5 줄
→ 90% 감소
// SQL Mapper: 1:N 처리
Shipment s = ...;
List<ShipmentItem> items = jdbcTemplate.query(
"SELECT * FROM shipment_items WHERE shipment_id = ?",
itemMapper, s.getId());
s.setItems(items);
// JPA: 1:N 자동
Shipment s = shipmentRepository.findById(1L).orElseThrow();
s.getItems(); // 자동 (Lazy) 또는 JOIN (Eager)
class Shipment {
Long getId() { return null; }
void setItems(java.util.List<ShipmentItem> items) {}
java.util.List<ShipmentItem> getItems() { return null; }
}
class ShipmentItem {}
JdbcTemplate jdbcTemplate;
RowMapper<ShipmentItem> itemMapper;
ShipmentRepository shipmentRepository;
class JdbcTemplate {
<T> java.util.List<T> query(String s, RowMapper<T> r, Object... a) { return null; }
}
interface RowMapper<T> {}
interface ShipmentRepository {
java.util.Optional<Shipment> findById(Long id);
}
적합 영역:
SQL Mapper:
- 통계 / 분석 / 리포트
- 윈도우 함수, CTE
- DB 특수 기능
- 매우 동적인 쿼리
JPA:
- CRUD
- 도메인 모델
- 객체 그래프
- 변경 감지 필요
→ 혼용 권장
// ILIC 의 혼용 패턴
// 1. JPA 주력 (95%)
@Repository
public interface ShipmentRepository extends JpaRepository<Shipment, Long> {
List<Shipment> findByCustomer_Id(Long customerId);
List<Shipment> findByStatusAndCreatedAtBetween(
String status, LocalDateTime from, LocalDateTime to);
}
// 2. JdbcTemplate 보조 (5%)
@Repository
public class ShipmentStatsDao {
private final JdbcTemplate jdbcTemplate;
public List<Map<String, Object>> regionalStats() {
return jdbcTemplate.queryForList("""
WITH region_stats AS (
SELECT region, amount,
RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS rk
FROM shipments_view
)
SELECT * FROM region_stats WHERE rk <= 5
""");
// 윈도우 함수 + CTE
// JPA 어려움
}
}
// → 도구별 적합한 영역
class Shipment {}
JdbcTemplate jdbcTemplate;
class JdbcTemplate {
java.util.List<java.util.Map<String, Object>> queryForList(String s) { return null; }
}
ShipmentRepository shipmentRepository;
SQL Mapper vs JPA 비교는?
답:
1. SQL:
매핑:
객체 그래프:
혼용:
JPA 의 한계:
1. 복잡 통계
- 윈도우 함수, CTE
- 어려움
2. DB 특수 기능
- JSON 함수
- 공간 데이터
- DB 별 SQL
3. 대량 배치
- INSERT 1000건
- JPA 비효율 (영속성 컨텍스트)
4. 매우 동적
- 조건이 매우 다양
- Querydsl 도 한계
// JPA 의 네이티브 쿼리
@Query(value = """
SELECT s.*, c.name AS customer_name,
ROW_NUMBER() OVER (PARTITION BY s.customer_id ORDER BY s.created_at DESC) AS rn
FROM shipments s
JOIN customers c ON s.customer_id = c.id
WHERE rn <= 5
""", nativeQuery = true)
List<Object[]> findRecentByCustomer();
// SQL 직접 + JPA 통합
// 결과는 Object[] 또는 DTO 매핑
class Object {}
@interface Query { String value(); boolean nativeQuery(); }
// JPQL (객체 지향 쿼리)
@Query("""
SELECT s FROM Shipment s
JOIN s.customer c
WHERE c.region = :region
ORDER BY s.createdAt DESC
""")
List<Shipment> findByRegion(@Param("region") String region);
// JPA 가 SQL 로 변환
// 객체 그래프 활용 (JOIN s.customer)
class Shipment {}
@interface Query { String value(); }
@interface Param { String value(); }
// Querydsl (타입 안전, 동적)
QShipment s = QShipment.shipment;
QCustomer c = QCustomer.customer;
BooleanBuilder where = new BooleanBuilder();
if (status != null) where.and(s.status.eq(status));
if (minWeight != null) where.and(s.weight.gt(minWeight));
if (region != null) where.and(s.customer.region.eq(region));
List<Shipment> result = queryFactory
.selectFrom(s)
.join(s.customer, c)
.where(where)
.orderBy(s.createdAt.desc())
.fetch();
// 동적 쿼리 강력
// 매우 복잡: JdbcTemplate (6주차)
public List<Map<String, Object>> complexReport() {
return jdbcTemplate.queryForList("""
WITH RECURSIVE category_tree AS (
-- 재귀 CTE
SELECT id, name, parent_id, 0 AS depth
FROM categories WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, ct.depth + 1
FROM categories c
JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree
""");
// 재귀 CTE — JPA 거의 불가능
}
JdbcTemplate jdbcTemplate;
class JdbcTemplate {
java.util.List<java.util.Map<String, Object>> queryForList(String s) { return null; }
}
적재적소:
단순 CRUD:
- JPA Repository (자동)
객체 그래프:
- JPA + JPQL
동적 조건:
- Querydsl
복잡 통계:
- 네이티브 쿼리 (@Query nativeQuery=true)
매우 복잡:
- JdbcTemplate
→ 도구별 강점
// ILIC 의 도구 활용 패턴
// 1. 단순 CRUD (JPA Repository)
shipmentRepository.findById(id);
shipmentRepository.findByStatus("SHIPPED");
// 2. 객체 그래프 (JPQL fetch join)
@Query("SELECT s FROM Shipment s JOIN FETCH s.customer WHERE s.status = :status")
List<Shipment> findWithCustomer(@Param("status") String status);
// 3. 동적 조건 (Querydsl)
queryFactory.selectFrom(shipment)
.where(/* 동적 조건 */)
.fetch();
// 4. 복잡 통계 (네이티브)
@Query(value = "SELECT region, COUNT(*) FROM shipments GROUP BY region", nativeQuery = true)
List<Object[]> regionStats();
// 5. 매우 복잡 (JdbcTemplate)
jdbcTemplate.queryForList("WITH ... ");
// → 각자 적합한 도구
class Shipment {}
ShipmentRepository shipmentRepository;
JdbcTemplate jdbcTemplate;
class JdbcTemplate { java.util.List queryForList(String s) { return null; } }
interface ShipmentRepository {
java.util.Optional<Shipment> findById(Long id);
java.util.List<Shipment> findByStatus(String s);
}
JPA 가 SQL 을 완전히 대체하는가?
답:
1. NO:
JPQL:
Querydsl:
네이티브 / JdbcTemplate:
SQL 확인의 중요성:
JPA 가 자동 생성하지만:
- 어떤 SQL?
- 효율적인가?
- N+1?
→ 확인 필수
→ 디버깅 / 튜닝 / 학습
# application.yml
spring:
jpa:
show-sql: true # SQL 출력
properties:
hibernate:
format_sql: true # 보기 좋게
use_sql_comments: true # 주석 (JPQL 표시)
-- show-sql + format-sql
Hibernate:
select
shipment0_.id as id1_0_,
shipment0_.bl_no as bl_no2_0_,
shipment0_.weight as weight3_0_,
shipment0_.status as status4_0_,
shipment0_.customer_id as customer_id5_0_
from
shipments shipment0_
where
shipment0_.id = ?
더 자세한 도구:
1. p6spy:
- 실제 파라미터 값 표시
- 실행 시간
- 로그
2. datasource-proxy:
- 비슷한 기능
- Spring Boot 통합
3. JPA Buddy (IntelliJ 플러그인):
- SQL 실시간 미리보기
- 매핑 확인
// build.gradle
dependencies {
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
}
# 결과 (p6spy)
2024-01-01 12:00:00.000 INFO p6spy :
#1709283600000 | took 5ms | statement |
connection 1 |
url jdbc:mysql://localhost:3306/ilic
SELECT s.id, s.bl_no, s.weight FROM shipments s WHERE s.id = 1
→ ? 가 아니라 실제 값 (1) 표시
디버깅 활용:
1. N+1 발견:
- SELECT 가 N 번 반복?
- fetch join 으로 해결
2. 비효율 발견:
- 너무 많은 컬럼?
- 불필요한 JOIN?
3. 학습:
- JPA 가 어떻게 SQL 만드는가
- 어노테이션의 효과
- JPQL → SQL 변환
운영 주의:
show-sql:
- 개발에서만
- 운영에선 OFF
- 성능 영향 ↑
- 로그 폭증
운영:
- 슬로우 쿼리 로그 (DB)
- APM (모니터링 도구)
- 필요 시만
# ILIC application.yml
# 개발 (local/dev)
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
use_sql_comments: true
# 운영 (prod)
spring:
jpa:
show-sql: false
# 운영은 슬로우 쿼리 + APM
// ILIC 의 SQL 확인 패턴
// 개발 시 show-sql 로 매핑 확인
// 의심스러우면 p6spy 활성화
// 예: N+1 발견 시
List<Shipment> all = shipmentRepository.findAll();
all.forEach(s -> s.getCustomer().getName());
// 로그:
// SELECT * FROM shipments (1번)
// SELECT * FROM customers WHERE id = ? (N번!)
// → N+1 확인 → fetch join 으로 해결
class Shipment { Customer getCustomer() { return null; } }
class Customer { String getName() { return null; } }
ShipmentRepository shipmentRepository;
interface ShipmentRepository { java.util.List<Shipment> findAll(); }
| Q | 핵심 답변 |
|---|---|
| JPA? | 자바 ORM 표준 |
| Persistence 의미? | 영속성 |
| 구현체? | Hibernate / EclipseLink |
| JPA vs Hibernate? | 명세 vs 구현 |
| JSR? | 자바 표준 명세 |
| Java EE → Jakarta? | 패키지 변경 |
| 어노테이션 매핑? | @Entity 등 |
| vs SQL Mapper? | 추상화 수준 |
| SQL 완전 대체? | NO |
| SQL 확인? | show-sql |
답:
답:
답:
답:
답:
1. JPA = 자바 진영 ORM 표준 인터페이스
2. SQL Mapper 와 차이
3. SQL 완전 대체 X
이번 Unit에서 JPA 의 정의를 봤다면, 다음은 JPA 의 위치 (애플리케이션 ↔ JDBC 사이).
🌱 Phase 3 — JPA 입문
✅ Unit 3.1 SQL Mapper 의 한계
✅ Unit 3.2 JPA 의 등장 ★깊이 ← 여기
⏭ Unit 3.3 JPA 동작 위치
⏭ Unit 3.4 Spring Data JPA + Querydsl ★깊이
🗂️ Part A — 데이터 모델링과 ORM
✅ Phase 1 (5)
✅ Phase 2 (2)
🌱 Phase 3 (2/4)
총: 9/24 Unit
★ 깊이 파기 — JPA 의 등장 완료