F-LAB JAVA · 7주차 · Phase 3 · JPA 입문
★ 깊이 파기 — 실무 표준 조합, Phase 3 완주
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
Spring Data JPA 는 JPA 위에 또 다른 추상화를 얹어 Repository 인터페이스만 정의하면 자동 구현되고 findByXxx 같은 메서드 이름만으로 쿼리가 자동 생성되며, Querydsl 은 타입 안전한 동적 쿼리를 컴파일 시점 오류 검출과 함께 작성할 수 있게 해줘서 실무에선 단순 CRUD = Spring Data JPA + 복잡 동적 쿼리 = Querydsl 의 조합이 표준이다.
Spring Data JPA 는 JPA 위에 얹어진 또 다른 추상화 — Repository 인터페이스만 정의하면 Spring 이 자동으로 구현체를 만들어주고,findByName,findByStatusAndCreatedAtBetween같은 메서드 이름만으로 SQL 이 자동 생성 된다.
더 복잡한 쿼리에는 Querydsl — 타입 안전 (Type-safe) 동적 쿼리 빌더 로, Q 클래스 (QShipment) 를 컴파일 시점에 생성해 IDE 자동완성과 컴파일 오류 검출이 가능하다.
컴파일 시점 오류 검출은 — 문자열 SQL 의 오타 (shipmnts) 가 런타임에야 발견되는 것 vs Querydsl 의 잘못된 필드 (shipment.bblNo) 가 컴파일 단계에서 즉시 빨간 줄로 표시되는 결정적 차이다.
실무 표준 조합은 — (1) 단순 CRUD = Spring Data JPA Repository, (2) 동적/복잡 조건 = Querydsl, (3) 매우 복잡 통계 = 네이티브 쿼리 또는 JdbcTemplate — 박승제의 ILIC 와 한국 기업 백엔드의 표준이다.
Spring Data JPA + Querydsl = 자동차의 자동 변속기 + 매뉴얼:
Spring Data JPA = 자동 변속기:
- 90% 운전 (CRUD)
- 편함
- 시동만 켜면 동작
- Repository 만 만들면 끝
Querydsl = 매뉴얼 옵션:
- 산악 / 스포츠 (복잡 쿼리)
- 정밀 제어
- 타입 안전 (수동 변속 전 확인)
- 컴파일 시점 검증
자동 + 매뉴얼 = 최고:
- 일상은 자동
- 필요 시 매뉴얼
- 모두 안전
JPA 만 (기본):
- 오토만 (변속 불가)
- 단순 CRUD 만
- 복잡 동적 X
Spring Data JPA 의 마법:
- Repository 인터페이스만 (구현 X)
- Spring 이 자동 구현
- findByName → SELECT WHERE name = ?
- 메서드 이름이 SQL
Querydsl 의 정밀:
- QShipment.shipment (Q 클래스, 컴파일 생성)
- .where(shipment.status.eq("SHIPPED"))
- IDE 자동완성
- 오타 = 빨간 줄
실무:
- 한국 기업 90% 이상 사용
- 박승제 ILIC 도 동일
- 면접 필수
→ Spring Data JPA = 자동 (CRUD), Querydsl = 매뉴얼 (동적), 둘 다 = 최고.
1. Spring Data JPA 정의
2. Repository 자동 구현
3. 메서드 이름 기반 쿼리
4. JPA vs Spring Data JPA
5. Querydsl 정의
6. 타입 안전 동적 쿼리
7. 컴파일 시점 오류 검출
8. 실무 표준 조합
9. 단순 vs 복잡 도구 선택
Spring Data JPA:
JPA 위에 얹은 또 다른 추상화:
- Repository 인터페이스만 정의
- 구현은 Spring 이 자동
- 메서드 이름 → 쿼리
→ JPA 의 편의 레이어
Spring Data 가족:
- Spring Data JPA (관계형 DB, 가장 인기)
- Spring Data MongoDB
- Spring Data Redis
- Spring Data JDBC
- Spring Data Elasticsearch
- ...
→ 모두 비슷한 패턴
JPA 만 사용 시:
EntityManager em;
em.find(Shipment.class, 1L);
em.persist(shipment);
em.createQuery("SELECT s FROM Shipment s WHERE s.status = :status")
.setParameter("status", "SHIPPED")
.getResultList();
Spring Data JPA:
interface ShipmentRepository extends JpaRepository<Shipment, Long> {
List<Shipment> findByStatus(String status);
}
// 구현 X (자동)
→ 코드 줄 90% ↓
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// 자동 포함:
// - JPA (jakarta.persistence)
// - Hibernate
// - Spring Data JPA
// - HikariCP
// ILIC 의 Spring Data JPA
@Entity
@Table(name = "shipments")
public class Shipment {
@Id @GeneratedValue
private Long id;
private String blNo;
private String status;
// ...
}
// Repository (인터페이스만!)
@Repository
public interface ShipmentRepository extends JpaRepository<Shipment, Long> {
// Spring 이 자동 구현
// 기본 CRUD: save / findById / findAll / delete / count / ...
// 커스텀 메서드 (메서드 이름 → 자동 SQL)
List<Shipment> findByStatus(String status);
Optional<Shipment> findByBlNo(String blNo);
List<Shipment> findByStatusAndWeightGreaterThan(String status, BigDecimal weight);
}
// 사용
@Service
public class ShipmentService {
@Autowired ShipmentRepository repo;
public Shipment get(Long id) {
return repo.findById(id).orElseThrow();
}
public List<Shipment> findShipped() {
return repo.findByStatus("SHIPPED");
}
}
// → 코드 매우 짧음
Spring Data JPA 의 정의는?
답:
1. Spring Data JPA:
Repository:
자동:
가족:
Repository 자동 구현:
개발자:
interface ShipmentRepository extends JpaRepository<Shipment, Long> {}
// 구현 클래스 X
Spring (런타임):
- 인터페이스 스캔
- 동적 프록시 생성
- 구현체 자동 주입
→ 마법처럼 동작
// JpaRepository<T, ID> 가 제공하는 기본
public interface JpaRepository<T, ID> {
// 저장
T save(T entity);
Iterable<T> saveAll(Iterable<T> entities);
// 조회
Optional<T> findById(ID id);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> ids);
long count();
boolean existsById(ID id);
// 삭제
void deleteById(ID id);
void delete(T entity);
void deleteAll();
// 페이징 / 정렬 (PagingAndSortingRepository)
Page<T> findAll(Pageable pageable);
Iterable<T> findAll(Sort sort);
// flush
void flush();
}
// 모두 자동 구현 (Spring Data 가)
class Page<T> {}
class Pageable {}
class Sort {}
class Optional<T> {}
class Iterable<T> {}
동작 원리 (대략):
1. @EnableJpaRepositories 또는 자동
2. 인터페이스 스캔
3. JpaRepositoryFactory 가 동적 프록시 생성
4. 메서드 호출 시:
- 기본 메서드 → SimpleJpaRepository 위임
- 커스텀 메서드 → 이름 분석 → 쿼리 생성
→ Spring 의 강력함
// Spring Data JPA 의 기본 구현체 (내부)
public class SimpleJpaRepository<T, ID> implements JpaRepository<T, ID> {
private final EntityManager em;
private final JpaEntityInformation<T, ?> entityInfo;
@Override
public Optional<T> findById(ID id) {
return Optional.ofNullable(em.find(entityInfo.getJavaType(), id));
}
@Override
public T save(T entity) {
if (entityInfo.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
// ...
}
class EntityManager {
<T> T find(Class<T> c, Object id) { return null; }
void persist(Object o) {}
<T> T merge(T t) { return null; }
}
class JpaEntityInformation<T, ID> {
Class<T> getJavaType() { return null; }
boolean isNew(T entity) { return false; }
}
class Optional<T> { static <T> Optional<T> ofNullable(T t) { return null; } }
interface JpaRepository<T, ID> { Optional<T> findById(ID id); T save(T t); }
가치:
1. 코드 ↓:
- 매 Repository 클래스 X
- 메서드만 정의
2. 일관:
- 모든 Repository 같은 패턴
- 학습 쉬움
3. 유지보수:
- 인터페이스만 보면 됨
- 구현 신경 X
→ 생산성 ↑
// ILIC 의 Repository 패턴 (102 테이블)
// 모든 Repository 가 같은 패턴
public interface ShipmentRepository extends JpaRepository<Shipment, Long> {}
public interface CustomerRepository extends JpaRepository<Customer, Long> {}
public interface BookingRepository extends JpaRepository<Booking, Long> {}
public interface PortRepository extends JpaRepository<Port, Long> {}
// ... 102 개
// 각 Repository 자동:
// - save, findById, findAll, delete, count, ...
// 사용
shipmentRepository.save(shipment);
customerRepository.findById(1L);
bookingRepository.findAll();
// → 학습 / 사용 일관
// ILIC = 102 × Repository × 평균 10 메서드
// → JPA 없으면 1020 × ~50 줄 = 51000 줄
// → Spring Data JPA = 1020 메서드 정의만
class Shipment {}
class Customer {}
class Booking {}
class Port {}
ShipmentRepository shipmentRepository;
CustomerRepository customerRepository;
BookingRepository bookingRepository;
Repository 인터페이스 자동 구현 원리는?
답:
1. 자동:
JpaRepository:
동적 프록시:
SimpleJpaRepository:
// 메서드 이름으로 SQL 자동
public interface ShipmentRepository extends JpaRepository<Shipment, Long> {
// findBy + 필드명
List<Shipment> findByStatus(String status);
// → SELECT * FROM shipments WHERE status = ?
// findBy + 필드 + And + 필드
List<Shipment> findByStatusAndWeightGreaterThan(String status, BigDecimal weight);
// → SELECT * FROM shipments WHERE status = ? AND weight > ?
// findBy + 필드 + Or + 필드
List<Shipment> findByStatusOrBlNo(String status, String blNo);
// → SELECT * FROM shipments WHERE status = ? OR bl_no = ?
// findBy + Between
List<Shipment> findByCreatedAtBetween(LocalDateTime from, LocalDateTime to);
// → WHERE created_at BETWEEN ? AND ?
// findBy + Contains
List<Shipment> findByBlNoContaining(String keyword);
// → WHERE bl_no LIKE %?%
// countBy
long countByStatus(String status);
// → SELECT COUNT(*) FROM shipments WHERE status = ?
// existsBy
boolean existsByBlNo(String blNo);
// deleteBy
void deleteByStatus(String status);
// findFirstBy, findTop10By
List<Shipment> findTop10ByOrderByCreatedAtDesc();
// 객체 그래프
List<Shipment> findByCustomer_Region(String region);
// → Customer.region 필드 → JOIN
}
class Shipment {}
메서드 이름 규칙:
주어 (action):
- findBy, getBy, queryBy, searchBy, streamBy
- countBy
- existsBy
- deleteBy, removeBy
술어 (predicate):
- 필드 (직접)
- And, Or
- Between
- LessThan, GreaterThan
- Like, Containing, StartingWith, EndingWith
- In, NotIn
- IsNull, IsNotNull
- OrderBy
객체 그래프:
- 필드명_하위필드 (Customer_Region)
동작 원리:
1. Spring 이 메서드 이름 파싱
2. 필드명 / 키워드 추출
3. JPQL 또는 SQL 생성
4. 실행
예:
findByStatusAndWeightGreaterThan
→ "status = ? AND weight > ?"
한계:
- 메서드 이름 길어짐 (5+ 조건)
- 동적 조건 어려움 (null 처리)
- 복잡 OR / 괄호 어려움
- JOIN 복잡
→ Querydsl 또는 @Query
// 복잡한 쿼리는 @Query
public interface ShipmentRepository extends JpaRepository<Shipment, Long> {
// JPQL
@Query("SELECT s FROM Shipment s WHERE s.status = :status AND s.customer.region = :region")
List<Shipment> findByStatusAndRegion(@Param("status") String status, @Param("region") String region);
// 네이티브 SQL
@Query(value = "SELECT * FROM shipments WHERE status = ?1", nativeQuery = true)
List<Shipment> findByStatusNative(String status);
// Modifying (UPDATE/DELETE)
@Modifying
@Query("UPDATE Shipment s SET s.status = :newStatus WHERE s.status = :oldStatus")
int updateStatus(@Param("oldStatus") String old, @Param("newStatus") String newSt);
}
class Shipment {}
@interface Query { String value(); String value2(); boolean nativeQuery(); }
@interface Param { String value(); }
@interface Modifying {}
// ILIC 의 다양한 쿼리 패턴
public interface ShipmentRepository extends JpaRepository<Shipment, Long> {
// 1. 메서드 이름 (단순)
Optional<Shipment> findByBlNo(String blNo);
List<Shipment> findByStatus(String status);
List<Shipment> findByCustomer_Id(Long customerId);
// 2. 메서드 이름 (중간)
List<Shipment> findByStatusAndCreatedAtBetween(
String status,
LocalDateTime from,
LocalDateTime to);
long countByStatus(String status);
// 3. @Query (JPQL, 객체 그래프)
@Query("""
SELECT s FROM Shipment s
JOIN FETCH s.customer
WHERE s.status = :status
""")
List<Shipment> findWithCustomer(@Param("status") String status);
// 4. @Query (네이티브, 복잡)
@Query(value = """
SELECT s.* FROM shipments s
WHERE s.weight > (SELECT AVG(weight) FROM shipments)
""", nativeQuery = true)
List<Shipment> findAboveAverageWeight();
// 5. 더 복잡한 동적 → Querydsl (다음 섹션)
}
class Shipment {}
@interface Query { String value(); boolean nativeQuery(); }
@interface Param { String value(); }
메서드 이름 기반 쿼리 (findByXxx) 는?
답:
1. 자동 SQL:
규칙:
한계:
대안:
| 항목 | JPA | Spring Data JPA |
|---|---|---|
| 성격 | 표준 명세 (인터페이스) | Spring 의 추가 추상화 |
| 사용 | EntityManager 직접 | Repository 인터페이스 |
| CRUD | em.persist, em.find ... | save, findById ... 자동 |
| 쿼리 | JPQL 작성 | 메서드 이름 / @Query |
| 페이징 | 직접 setMaxResults | Pageable 객체 |
| 학습 곡선 | 보통 | 낮음 (위에 얹음) |
| Spring Boot | 사용 가능 | 자동 통합 |
추상화 계층:
Application
↓
Spring Data JPA (Repository, 메서드 이름)
↓
JPA (EntityManager, 표준)
↓
Hibernate (구현체)
↓
JDBC
↓
DB
→ 4-5 계층!
JPA 직접 사용 시기:
- 매우 복잡한 트랜잭션 제어
- 멀티 DataSource
- 학습 / 디버깅
- Spring 안 쓰는 경우 (드물게)
실무는:
- Spring Data JPA 90%
- JPA 직접 10%
→ Spring Data JPA 가 표준
추가 기능:
- Repository 자동 구현
- 메서드 이름 쿼리
- Page / Pageable
- Sort
- @Query
- Specifications (동적)
- Auditing (@CreatedDate)
- Projections (DTO 자동)
→ JPA + 편의 + 생산성
// 페이징 (Spring Data JPA)
public interface ShipmentRepository extends JpaRepository<Shipment, Long> {
Page<Shipment> findByStatus(String status, Pageable pageable);
}
// 사용
Pageable pageable = PageRequest.of(0, 20, Sort.by("createdAt").descending());
Page<Shipment> page = repo.findByStatus("SHIPPED", pageable);
page.getContent(); // List<Shipment>
page.getTotalPages();
page.getTotalElements();
page.hasNext();
// JPA 직접:
// - setFirstResult, setMaxResults 수동
// - count 쿼리 별도
// - Spring Data 가 모두 자동
class Shipment {}
class Page<T> { java.util.List<T> getContent(){return null;} int getTotalPages(){return 0;} long getTotalElements(){return 0;} boolean hasNext(){return false;} }
interface Pageable {}
class PageRequest { static Pageable of(int p, int s, Sort o) { return null; } }
class Sort { static Sort by(String s) { return null; } Sort descending() { return this; } }
ShipmentRepository repo;
interface ShipmentRepository {
Page<Shipment> findByStatus(String s, Pageable p);
}
// ILIC 의 Spring Data JPA 활용
@Service
public class ShipmentService {
@Autowired ShipmentRepository repo;
// 1. 기본 CRUD
public Shipment get(Long id) {
return repo.findById(id).orElseThrow();
}
public Shipment save(Shipment s) {
return repo.save(s);
}
// 2. 메서드 이름 쿼리
public List<Shipment> findByStatus(String status) {
return repo.findByStatus(status);
}
// 3. 페이징 (대시보드)
public Page<Shipment> getDashboard(String status, int page, int size) {
Pageable pageable = PageRequest.of(page, size,
Sort.by("createdAt").descending());
return repo.findByStatus(status, pageable);
}
// 4. 카운트
public long countByStatus(String status) {
return repo.countByStatus(status);
}
// 5. 존재 확인
public boolean existsByBlNo(String blNo) {
return repo.existsByBlNo(blNo);
}
}
// → JPA 직접보다 훨씬 간결
class Shipment {}
ShipmentRepository repo;
interface ShipmentRepository {
java.util.Optional<Shipment> findById(Long id);
Shipment save(Shipment s);
java.util.List<Shipment> findByStatus(String s);
Page<Shipment> findByStatus(String s, Pageable p);
long countByStatus(String s);
boolean existsByBlNo(String b);
}
class Page<T> {}
interface Pageable {}
class PageRequest { static Pageable of(int p, int s, Sort o) { return null; } }
class Sort { static Sort by(String s) { return null; } Sort descending() { return this; } }
JPA vs Spring Data JPA 차이는?
답:
1. JPA:
Spring Data JPA:
추가 기능:
실무:
Querydsl:
타입 안전한 쿼리 빌더:
- 자바 코드로 쿼리 작성
- 컴파일 시점 타입 체크
- 동적 쿼리 강력
→ SQL 문자열의 대안
Q 클래스:
엔티티별 메타 모델 (컴파일 생성):
@Entity Shipment
→ 컴파일 시 QShipment 자동 생성
QShipment.shipment.blNo
QShipment.shipment.status
QShipment.shipment.weight
// ↑ IDE 자동완성 가능!
// Querydsl 사용
QShipment shipment = QShipment.shipment;
QCustomer customer = QCustomer.customer;
List<Shipment> result = queryFactory
.selectFrom(shipment)
.join(shipment.customer, customer)
.where(
shipment.status.eq("SHIPPED")
.and(customer.region.eq("Asia"))
.and(shipment.weight.gt(new BigDecimal("100")))
)
.orderBy(shipment.createdAt.desc())
.limit(20)
.fetch();
// 자바 코드 + 메서드 체인
class QShipment {
static QShipment shipment = new QShipment();
Customer customer;
StringExpression status, blNo;
NumberExpression<java.math.BigDecimal> weight;
DateExpression createdAt;
}
class QCustomer {
static QCustomer customer = new QCustomer();
StringExpression region;
}
class Shipment {}
class Customer {}
class StringExpression { BooleanExpression eq(String s) { return null; } }
class NumberExpression<T> { BooleanExpression gt(T t) { return null; } }
class DateExpression { OrderSpecifier desc() { return null; } }
class BooleanExpression { BooleanExpression and(BooleanExpression e) { return null; } }
class OrderSpecifier {}
JPAQueryFactory queryFactory;
class JPAQueryFactory {
JPAQuery<Shipment> selectFrom(QShipment s) { return null; }
}
class JPAQuery<T> {
JPAQuery<T> join(Customer c, QCustomer qc) { return this; }
JPAQuery<T> where(BooleanExpression... e) { return this; }
JPAQuery<T> orderBy(OrderSpecifier... o) { return this; }
JPAQuery<T> limit(int n) { return this; }
java.util.List<T> fetch() { return null; }
}
// build.gradle
dependencies {
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
}
// Q 클래스 자동 생성:
// build/generated/sources/annotationProcessor/...
// Querydsl 설정
@Configuration
public class QuerydslConfig {
@PersistenceContext
private EntityManager em;
@Bean
public JPAQueryFactory queryFactory() {
return new JPAQueryFactory(em);
}
}
// 사용
@Repository
public class ShipmentQueryRepository {
@Autowired JPAQueryFactory queryFactory;
public List<Shipment> findCustom() {
QShipment s = QShipment.shipment;
return queryFactory
.selectFrom(s)
.where(s.status.eq("SHIPPED"))
.fetch();
}
}
class EntityManager {}
@interface PersistenceContext {}
@interface Bean {}
@interface Configuration {}
class JPAQueryFactory { JPAQueryFactory(EntityManager em){} }
class Shipment {}
class QShipment {
static QShipment shipment = new QShipment();
StringExpression status;
}
class StringExpression { BooleanExpression eq(String s){return null;} }
class BooleanExpression {}
// ILIC 의 Querydsl 사용
@Repository
public class ShipmentQueryRepository {
@Autowired JPAQueryFactory queryFactory;
// 정적 쿼리
public List<Shipment> findShippedHeavyAsia() {
QShipment s = QShipment.shipment;
QCustomer c = QCustomer.customer;
return queryFactory
.selectFrom(s)
.join(s.customer, c)
.where(
s.status.eq("SHIPPED"),
s.weight.gt(new BigDecimal("100")),
c.region.eq("Asia")
)
.orderBy(s.createdAt.desc())
.fetch();
}
}
// → 자바 코드 + 메서드 체인
// → IDE 자동완성 (Q 클래스)
// → 컴파일 시점 검증
class JPAQueryFactory {
JPAQuery<Shipment> selectFrom(QShipment s) { return null; }
}
class JPAQuery<T> {
JPAQuery<T> join(Object o, QCustomer c) { return this; }
JPAQuery<T> where(BooleanExpression... e) { return this; }
JPAQuery<T> orderBy(OrderSpecifier... o) { return this; }
java.util.List<T> fetch() { return null; }
}
class Shipment {}
class QShipment {
static QShipment shipment = new QShipment();
StringExpression status;
NumberExpression<java.math.BigDecimal> weight;
DateExpression createdAt;
Customer customer;
}
class QCustomer { static QCustomer customer = new QCustomer(); StringExpression region; }
class Customer {}
class StringExpression { BooleanExpression eq(String s) { return null; } }
class NumberExpression<T> { BooleanExpression gt(T t) { return null; } }
class DateExpression { OrderSpecifier desc() { return null; } }
class BooleanExpression {}
class OrderSpecifier {}
Querydsl 의 정의는?
답:
1. 정의:
Q 클래스:
사용:
목적:
동적 쿼리:
조건이 런타임에 결정:
- 검색 폼 (조건 가변)
- status, weight, region ...
- 일부만 입력
문자열로:
- StringBuilder + if
- SQL 인젝션 위험
- 가독성 ↓
Querydsl:
- BooleanBuilder
- 타입 안전
- 자연
// 동적 쿼리 (Querydsl)
public List<Shipment> search(
String status,
BigDecimal minWeight,
String region) {
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(c.region.eq(region));
}
return queryFactory
.selectFrom(s)
.join(s.customer, c)
.where(where)
.fetch();
}
// 조건 가변, 코드 깔끔
class BooleanBuilder { BooleanBuilder and(Object o) { return this; } }
class QShipment {
static QShipment shipment = new QShipment();
StringExpression status;
NumberExpression<java.math.BigDecimal> weight;
Customer customer;
}
class QCustomer {
static QCustomer customer = new QCustomer();
StringExpression region;
}
class StringExpression { BooleanExpression eq(String s) { return null; } }
class NumberExpression<T> { BooleanExpression gt(T t) { return null; } }
class BooleanExpression {}
class Shipment {}
class Customer {}
JPAQueryFactory queryFactory;
class JPAQueryFactory {
JPAQuery<Shipment> selectFrom(QShipment s) { return null; }
}
class JPAQuery<T> {
JPAQuery<T> join(Object o, QCustomer c) { return this; }
JPAQuery<T> where(BooleanBuilder b) { return this; }
java.util.List<T> fetch() { return null; }
}
// SQL 문자열 동적 (지양)
StringBuilder sql = new StringBuilder("SELECT * FROM shipments WHERE 1=1");
List<Object> params = new ArrayList<>();
if (status != null) {
sql.append(" AND status = ?");
params.add(status);
}
if (minWeight != null) {
sql.append(" AND weight > ?");
params.add(minWeight);
}
// ...
// 문제:
// - 가독성 ↓
// - SQL 인젝션 위험 (직접 concatenation 시)
// - 컴파일 검증 X
String status;
java.math.BigDecimal minWeight;
타입 안전의 가치:
- IDE 자동완성
- 컴파일 오류 검출
- 리팩토링 안전
- 가독성 ↑
→ "코드는 자바"
// BooleanExpression 메서드 분리 (재사용)
private BooleanExpression statusEq(String status) {
return status != null ? QShipment.shipment.status.eq(status) : null;
}
private BooleanExpression weightGt(BigDecimal weight) {
return weight != null ? QShipment.shipment.weight.gt(weight) : null;
}
// 사용
return queryFactory
.selectFrom(QShipment.shipment)
.where(
statusEq(status), // null 이면 무시
weightGt(minWeight)
)
.fetch();
// → 더 깔끔
class BooleanExpression {}
class QShipment {
static QShipment shipment = new QShipment();
StringExpression status;
NumberExpression<java.math.BigDecimal> weight;
}
class StringExpression { BooleanExpression eq(String s) { return null; } }
class NumberExpression<T> { BooleanExpression gt(T t) { return null; } }
class Shipment {}
JPAQueryFactory queryFactory;
class JPAQueryFactory {
JPAQuery<Shipment> selectFrom(QShipment s) { return null; }
}
class JPAQuery<T> {
JPAQuery<T> where(BooleanExpression... e) { return this; }
java.util.List<T> fetch() { return null; }
}
// ILIC 의 동적 쿼리 (검색 폼)
@Repository
public class ShipmentSearchRepository {
@Autowired JPAQueryFactory queryFactory;
public Page<Shipment> search(ShipmentSearchCondition cond, Pageable pageable) {
QShipment s = QShipment.shipment;
QCustomer c = QCustomer.customer;
List<Shipment> content = queryFactory
.selectFrom(s)
.leftJoin(s.customer, c)
.where(
statusEq(cond.getStatus()),
blNoContains(cond.getBlNo()),
weightBetween(cond.getMinWeight(), cond.getMaxWeight()),
regionEq(cond.getRegion()),
createdAtBetween(cond.getFromDate(), cond.getToDate())
)
.orderBy(s.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
long total = queryFactory
.select(s.count())
.from(s)
.where(/* 같은 where */)
.fetchOne();
return new PageImpl<>(content, pageable, total);
}
private BooleanExpression statusEq(String status) {
return status != null ? QShipment.shipment.status.eq(status) : null;
}
// ...
}
// → 100+ 필드 동적 검색도 가능
// → ILIC 의 다양한 검색 폼 지원
class ShipmentSearchCondition {
String getStatus() { return null; }
String getBlNo() { return null; }
java.math.BigDecimal getMinWeight() { return null; }
java.math.BigDecimal getMaxWeight() { return null; }
String getRegion() { return null; }
java.time.LocalDateTime getFromDate() { return null; }
java.time.LocalDateTime getToDate() { return null; }
}
class Shipment {}
class Pageable { long getOffset() { return 0; } int getPageSize() { return 0; } }
class Page<T> {}
class PageImpl<T> implements Page<T> { PageImpl(java.util.List<T> c, Pageable p, long t) {} }
class JPAQueryFactory {
JPAQuery<Shipment> selectFrom(QShipment s) { return null; }
JPAQuery<Long> select(NumberExpression<Long> n) { return null; }
}
class JPAQuery<T> {
JPAQuery<T> leftJoin(Object o, QCustomer c) { return this; }
JPAQuery<T> where(BooleanExpression... e) { return this; }
JPAQuery<T> orderBy(OrderSpecifier... o) { return this; }
JPAQuery<T> offset(long o) { return this; }
JPAQuery<T> limit(long l) { return this; }
JPAQuery<T> from(QShipment s) { return this; }
java.util.List<T> fetch() { return null; }
Long fetchOne() { return null; }
}
class QShipment {
static QShipment shipment = new QShipment();
StringExpression status;
DateExpression createdAt;
Customer customer;
NumberExpression<Long> count() { return null; }
}
class QCustomer { static QCustomer customer = new QCustomer(); }
class Customer {}
class StringExpression { BooleanExpression eq(String s) { return null; } }
class NumberExpression<T> {}
class DateExpression { OrderSpecifier desc() { return null; } }
class BooleanExpression {}
class OrderSpecifier {}
JPAQueryFactory queryFactory;
타입 안전 동적 쿼리의 의미는?
답:
1. 동적:
타입 안전:
BooleanBuilder:
방식:
오류 검출 시점:
컴파일 시점:
- IDE 빨간 줄 즉시
- 컴파일 실패
- 빨리 발견
런타임:
- 실행 후 발견
- 사용자에게 노출
- 늦게 발견
→ "Shift Left" (왼쪽으로 옮기기)
// SQL 문자열 (런타임에 오류)
String sql = "SELECT * FROM shipmnts WHERE status = ?";
// ↑ 오타!
jdbcTemplate.queryForList(sql, "SHIPPED");
// 실행 시 에러:
// Table 'ilic.shipmnts' doesn't exist
// 또는
String sql = "SELECT * FROM shipments WHERE statu = ?";
// ↑ 오타!
// Unknown column 'statu' in 'where clause'
// → 모두 런타임에 발견
JdbcTemplate jdbcTemplate;
class JdbcTemplate {
java.util.List queryForList(String s, Object... a) { return null; }
}
// Querydsl (컴파일 시점 검증)
QShipment s = QShipment.shipment;
queryFactory
.selectFrom(s)
.where(s.statu.eq("SHIPPED")) // ❌ 컴파일 에러!
// ↑ 빨간 줄
// "Cannot resolve symbol 'statu'"
.fetch();
// 컴파일 실패 → 빌드 안 됨
// → 운영에 절대 도달 X
class QShipment {
static QShipment shipment = new QShipment();
StringExpression status;
}
class StringExpression { BooleanExpression eq(String s) { return null; } }
class BooleanExpression {}
class Shipment {}
JPAQueryFactory queryFactory;
class JPAQueryFactory { JPAQuery<Shipment> selectFrom(QShipment s) { return null; } }
class JPAQuery<T> { JPAQuery<T> where(BooleanExpression e) { return this; } java.util.List<T> fetch() { return null; } }
리팩토링 안전:
필드명 변경 시:
- SQL 문자열: 일일이 찾아 수정 (검색)
- Querydsl: IDE 가 자동 리팩토링
예:
Shipment.blNo → billOfLadingNumber
SQL: "SELECT bl_no ..." → 직접 수정 (위험)
Querydsl: QShipment.shipment.billOfLadingNumber → IDE 자동
IDE 자동완성:
Querydsl:
QShipment.shipment.
↑ 점 입력하면 모든 필드 자동완성
QShipment.shipment.status.
↑ StringExpression 메서드 (eq, contains, startsWith ...) 자동완성
→ 학습 곡선 ↓
→ 생산성 ↑
컴파일 시점의 가치:
1. 빠른 피드백:
- 작성 즉시 발견
- 컴파일 단계
2. 운영 안전:
- 런타임 에러 ↓
- 사용자 노출 ↓
3. 자신감:
- 컴파일 통과 = 일정 안전
- 리팩토링 자유
→ 타입 안전 언어의 강점
ILIC 의 컴파일 시점 안전
ILIC 의 102 테이블 + Querydsl:
오타 / 잘못된 필드:
- SQL 문자열: 런타임에 발견 (운영 사고)
- Querydsl: 컴파일 시 발견 (즉시)
리팩토링:
- shipments.bl_no → shipments.billOfLadingNumber 변경
- SQL 문자열: grep 으로 찾아 수정 (누락 위험)
- Querydsl: IDE 자동 (안전)
신입 개발자:
- SQL 문자열: 학습 시간 ↑
- Querydsl: IDE 자동완성, 자연
→ ILIC 의 Querydsl 도입 이유
→ 운영 안정성 + 생산성
컴파일 시점 오류 검출 이점은?
답:
1. 시점:
SQL 문자열:
Querydsl:
이점:
실무 표준 조합:
Spring Data JPA
+ Querydsl
+ (필요 시 JdbcTemplate)
= 한국 기업 백엔드 표준
= 박승제 ILIC 동일
역할 분담:
Spring Data JPA Repository:
- 단순 CRUD (90%)
- findByXxx 메서드 이름
- JpaRepository 기본 메서드
Querydsl:
- 동적 쿼리
- 복잡한 조건
- 검색 폼
- 타입 안전 필요
JdbcTemplate:
- 매우 복잡 통계
- 윈도우 함수, CTE
- 네이티브 SQL
// 1. Repository (Spring Data JPA)
public interface ShipmentRepository extends
JpaRepository<Shipment, Long>,
ShipmentQueryRepository { // Querydsl 인터페이스 결합
// 메서드 이름 쿼리
Optional<Shipment> findByBlNo(String blNo);
List<Shipment> findByStatus(String status);
}
// 2. Querydsl 커스텀 인터페이스
public interface ShipmentQueryRepository {
Page<Shipment> search(ShipmentSearchCondition cond, Pageable pageable);
List<Shipment> findHeavyByRegion(String region);
}
// 3. Querydsl 구현체
@Repository
public class ShipmentQueryRepositoryImpl
implements ShipmentQueryRepository {
@Autowired JPAQueryFactory queryFactory;
@Override
public Page<Shipment> search(...) { /* Querydsl 코드 */ return null; }
@Override
public List<Shipment> findHeavyByRegion(String region) {
QShipment s = QShipment.shipment;
return queryFactory
.selectFrom(s)
.where(
s.weight.gt(new BigDecimal("100")),
s.customer.region.eq(region)
)
.fetch();
}
}
// → Spring Data JPA + Querydsl 자연스러운 결합
class Shipment {}
class ShipmentSearchCondition {}
class Pageable {}
class Page<T> {}
class JPAQueryFactory {
JPAQuery<Shipment> selectFrom(QShipment s) { return null; }
}
class JPAQuery<T> {
JPAQuery<T> where(BooleanExpression... e) { return this; }
java.util.List<T> fetch() { return null; }
}
class QShipment {
static QShipment shipment = new QShipment();
NumberExpression<java.math.BigDecimal> weight;
Customer customer;
}
class Customer { StringExpression region; }
class StringExpression { BooleanExpression eq(String s) { return null; } }
class NumberExpression<T> { BooleanExpression gt(T t) { return null; } }
class BooleanExpression {}
interface ShipmentRepository extends JpaRepository<Shipment, Long>, ShipmentQueryRepository {
java.util.Optional<Shipment> findByBlNo(String b);
java.util.List<Shipment> findByStatus(String s);
}
interface JpaRepository<T, ID> {}
@interface Repository {}
@interface Autowired {}
도구 선택 기준:
단순 CRUD / 1-2 조건:
→ Spring Data JPA (메서드 이름)
3+ 조건 동적 / 검색 폼:
→ Querydsl
객체 그래프 조회 / fetch join:
→ @Query JPQL 또는 Querydsl
복잡 통계 (윈도우/CTE):
→ 네이티브 쿼리 (@Query nativeQuery)
→ 또는 JdbcTemplate
대량 배치:
→ JdbcTemplate (JPA 부적합)
면접 질문 (단골):
Q: Spring Data JPA 의 메서드 이름 쿼리 한계는?
A: 동적 조건 어려움, 5+ 조건 시 메서드 이름 폭증
→ Querydsl 도입
Q: Querydsl 의 장점?
A: 타입 안전, 동적 쿼리, IDE 자동완성, 리팩토링 안전
Q: JPA + Querydsl + JdbcTemplate 같이 쓰는 이유?
A: 각자 적합한 영역 (CRUD / 동적 / 복잡 통계)
Q: 한국에서 Spring Data JPA + Querydsl 표준 이유?
A: 생산성 + 안전 + 인기
// ILIC 의 표준 구조
// 1. 단순 CRUD
@Repository
public interface ShipmentRepository extends JpaRepository<Shipment, Long>, ShipmentQueryRepository {
Optional<Shipment> findByBlNo(String blNo);
List<Shipment> findByStatusAndCustomer_Id(String status, Long customerId);
}
// 2. Querydsl (동적)
public interface ShipmentQueryRepository {
Page<Shipment> searchShipments(SearchCondition cond, Pageable pageable);
List<Shipment> findActiveByRegion(String region);
}
@Repository
public class ShipmentQueryRepositoryImpl implements ShipmentQueryRepository {
// Querydsl 구현
}
// 3. JdbcTemplate (복잡 통계)
@Repository
public class ShipmentStatsDao {
private final JdbcTemplate jdbcTemplate;
public List<RegionStat> regionStats() {
return jdbcTemplate.query("""
WITH region_data AS (
SELECT region, COUNT(*) AS cnt,
AVG(amount) AS avg_amount,
PERCENT_RANK() OVER (ORDER BY amount) AS rank
FROM shipments
GROUP BY region
)
SELECT * FROM region_data WHERE rank > 0.8
""", /* RowMapper */ null);
}
}
// → ILIC 의 102 테이블 / 다양한 요구 사항 모두 대응
class Shipment {}
class SearchCondition {}
class Pageable {}
class Page<T> {}
class RegionStat {}
class JdbcTemplate {
<T> java.util.List<T> query(String s, Object r) { return null; }
}
interface JpaRepository<T, ID> {}
interface ShipmentRepository extends JpaRepository<Shipment, Long>, ShipmentQueryRepository {
java.util.Optional<Shipment> findByBlNo(String b);
java.util.List<Shipment> findByStatusAndCustomer_Id(String s, Long id);
}
실무 표준 조합 (Spring Data JPA + Querydsl) 의 이유는?
답:
1. 조합:
역할 분담:
안전 + 생산성:
한국 표준:
| 시나리오 | 도구 | 이유 |
|---|---|---|
| findById, save | JpaRepository 기본 | 자동 |
| findByXxx (1-2 조건) | 메서드 이름 | 간결 |
| 3+ 정적 조건 + JOIN | @Query JPQL | 명확 |
| 동적 조건 (검색 폼) | Querydsl | 타입 안전 |
| 객체 그래프 fetch | JPQL JOIN FETCH | N+1 회피 |
| 복잡 통계 (윈도우) | @Query nativeQuery | JPA 한계 |
| 매우 복잡 / 대량 | JdbcTemplate | 자유 |
의사 결정:
Q: 단순 CRUD 인가?
Y → JpaRepository
N → Q: 조건이 정적인가?
Y → 메서드 이름 또는 @Query
N → Q: 동적인가?
Y → Querydsl
N → Q: 매우 복잡한가?
Y → JdbcTemplate
N → @Query nativeQuery
ILIC 의 도구 사용 비율 (추정):
Spring Data JPA Repository: 60%
- findById, save, findByXxx
@Query JPQL: 15%
- JOIN FETCH, 객체 그래프
Querydsl: 20%
- 검색 폼, 동적
JdbcTemplate: 5%
- 복잡 통계, 배치
→ 100%
학습 우선순위:
1. JPA 기본 (@Entity, 어노테이션)
2. Spring Data JPA Repository
3. 메서드 이름 쿼리
4. @Query JPQL
5. Querydsl
6. JdbcTemplate (6주차)
→ 순서대로
ILIC 학습 흐름
박승제 학습 경로:
6주차 → JdbcTemplate (이미 깊이 학습)
7주차 → JPA / Spring Data JPA / Querydsl
ILIC 의 도구 활용:
- 102 테이블
- 다양한 비즈니스 시나리오
- 검색 폼 (Querydsl)
- 복잡 통계 (JdbcTemplate)
- 단순 CRUD (Repository)
- 객체 그래프 (JPQL)
→ 7주차 학습이 ILIC 의 실무 직접 활용
| Q | 핵심 답변 |
|---|---|
| Spring Data JPA? | JPA 위 추상화 |
| Repository 자동 구현? | 동적 프록시 |
| 메서드 이름 쿼리? | 자동 SQL |
| @Query? | JPQL / 네이티브 |
| Querydsl? | 타입 안전 |
| Q 클래스? | 컴파일 생성 |
| 동적 쿼리? | BooleanBuilder |
| 컴파일 검증? | 이점 |
| 실무 조합? | JPA + Querydsl |
| 도구 선택? | 상황별 |
답:
답:
답:
답:
답:
1. Spring Data JPA = JPA 위 추상화
2. Querydsl = 타입 안전 동적 쿼리
3. 실무 표준 조합
🌱 Phase 3 — JPA 입문
✅ Unit 3.1 SQL Mapper 의 한계
✅ Unit 3.2 JPA 의 등장 ★깊이
✅ Unit 3.3 JPA 동작 위치
✅ Unit 3.4 Spring Data JPA + Querydsl ★깊이 ← 여기, Phase 3 완주
→ JPA 정의 / 위치 / 활용 도구
→ 실무 표준 조합
→ Phase 4 (엔티티 매핑) 의 기반
🏷️ Phase 4 — JPA 엔티티 매핑
Unit 4.1 — @Entity 와 엔티티 개념
Unit 4.2 — @Id 와 PK 매핑
Unit 4.3 — @GeneratedValue 전략 ★깊이
Unit 4.4 — @Column 과 컬럼 매핑
Unit 4.5 — 자동 매핑 규칙 (camelCase ↔ snake_case)
Phase 4 주제:
🗂️ Part A — 데이터 모델링과 ORM
✅ Phase 1 (5)
✅ Phase 2 (2)
✅ Phase 3 (4) ← 완주
⏭ Phase 4 — JPA 엔티티 매핑 (5)
🔄 Part B — 트랜잭션 추상화의 진화
⏭ Phase 5 (2)
⏭ Phase 6 (3)
⏭ Phase 7 (3)
총: 11/24 Unit (Phase 3 완주!)
★ 깊이 파기 — Spring Data JPA + Querydsl, Phase 3 완주