SQL, JPQL 같은 쿼리를 Java 코드로 작성할 수 있게 해주는 타입 안전한 ORM 기반 쿼리 빌더
SQL을 Java 코드로 작성하면서, 가독성과 유지보수성을 대폭 개선할 수 있는 도구
1.2 ☝️ 왜 사용해야할까
1.2.1 SQL/JPQL의 한계 해결
문법 오류 - 쿼리의 오류를 런타임에서만 확인 가능
필드 이름 변경 - 엔티티 필드가 변경되면 문자열 기반 쿼리에서 이를 수동으로 수정해야 함.
동적 쿼리 작성의 어려움 - 조건에 따라 쿼리를 유연하게 작성하려면 코드 복잡도 향상
코드 비교
// JPQL
// 필터가 많아질수록 코드가 복잡
public List<User> findUsersDynamicJPQL(EntityManager em, Integer age, String name) {
StringBuilder jpql = new StringBuilder("SELECT u FROM User u WHERE 1=1");
if (age != null) {
jpql.append(" AND u.age > :age");
}
if (name != null) {
jpql.append(" AND u.name = :name");
}
TypedQuery<User> query = em.createQuery(jpql.toString(), User.class);
if (age != null) query.setParameter("age", age);
if (name != null) query.setParameter("name", name);
return query.getResultList();
}
// QueryDSL
// BooleanBuilder 사용
public List<User> findUsersDynamicQueryDSL(JPAQueryFactory queryFactory, Integer age, String name) {
QUser user = QUser.user;
BooleanBuilder builder = new BooleanBuilder(); // 동적 조건 빌더
if (age != null) {
builder.and(user.age.gt(age));
}
if (name != null) {
builder.and(user.name.eq(name));
}
return queryFactory
.selectFrom(user)
.where(builder) // 동적 조건 적용
.fetch();
}
Spring Data JPA에서 제공하는 Specification 인터페이스를 활용하면 동적 쿼리 구현
✅ 장점
Spring Data JPA와 통합: Spring 프로젝트와 자연스럽게 연동.
동적 조건 작성이 비교적 간단.
❌ 단점
QueryDSL에 비해 표현력이 제한적.
복잡한 쿼리 작성에는 적합하지 않음.
예제코드
public class UserSpecifications {
public static Specification<User> hasAgeGreaterThan(int age) {
return (root, query, cb) -> cb.gt(root.get("age"), age);
}
public static Specification<User> hasStatus(String status) {
return (root, query, cb) -> cb.equal(root.get("status"), status);
}
}
// 사용
Specification<User> spec = Specification.where(UserSpecifications.hasAgeGreaterThan(18))
.and(UserSpecifications.hasStatus("ACTIVE"));
List<User> users = userRepository.findAll(spec);
1.4.3 네이티브 SQL
✅ 장점
유연성: 데이터베이스에 특화된 쿼리를 작성 가능
성능 최적화에 유리
❌ 단점
타입 안전성 부족: 문자열 기반으로 작성
데이터베이스 종속적 → 이식성이 낮음
유지보수가 어려움
예제 코드
@Query(value = "SELECT * FROM users WHERE age > :age AND status = :status", nativeQuery = true)
List<User> findUsersNative(@Param("age") int age, @Param("status") String status);
1.4.4 jOOQ (Java Object Oriented Querying)
jOOQ는 SQL을 Java 코드로 작성하면서도, 데이터베이스와 밀접하게 통합하여 고급 기능을 사용할 수 있는 강력한 도구
✅ 장점
SQL 친화적 설계: SQL의 고급 기능(윈도우 함수, CTE 등)을 Java 코드로 직접 활용 가능.
타입 안전성: SQL 컬럼과 Java 필드 간의 타입을 컴파일 타임에 검증.
DBMS 특화 기능 지원: 데이터베이스에 따라 최적화된 SQL 생성 가능.
코드 자동 생성: 테이블 스키마 기반으로 Java 코드를 생성하여, 변경 사항을 자동 반영.
❌ 단점
SQL 종속성: 데이터베이스 변경 시 쿼리 코드 수정이 필요.
학습 난이도: SQL에 익숙하지 않은 개발자에게는 진입 장벽이 높음.
ORM 부족: 객체 지향적 추상화가 부족하고, SQL 중심의 설계에 초점.
예제 코드
// 동적 조건 추가
public List<User> findUsersDynamicJooq(DSLContext context, Integer age, String name) {
Condition condition = DSL.trueCondition();
if (age != null) {
condition = condition.and(USER.AGE.gt(age));
}
if (name != null) {
condition = condition.and(USER.NAME.eq(name));
}
return context.selectFrom(USER)
.where(condition)
.fetchInto(User.class);
}
라이브러리
특징
장점
단점
QueryDSL
- 타입 안전한 동적 쿼리 작성을 지원 - Q클래스를 통해 메서드 체인 방식으로 쿼리를 작성
- 타입 안전성(Type Safety) 보장 - 가독성 높은 코드 작성 가능 - IDE 자동완성 지원
- 초기 설정 복잡 - Q클래스 생성 필요
Spring Data JPA Specifications
- JPA의 Criteria API를 기반으로 동적 쿼리를 작성 - Specification 인터페이스를 사용
- 추가 라이브러리 없이 Spring Data JPA와 자연스러운 통합 - 동적 쿼리 작성 가능
- 코드가 장황하고 가독성이 떨어질 수 있음
JOOQ
- SQL 빌더 라이브러리로, SQL 문법을 코드로 표현 - 고급 SQL 기능을 지원
- SQL 문법을 코드로 직관적으로 표현 가능 - 복잡한 쿼리와 고급 SQL 기능 활용 가능
DIP를 적용하게 되면, 응용 영역과 도메인 영역의 코드 수정 없이 인프라스트럭처부분만 코드를 추가/수정 및 변경하여 요구사항을 충족시킬 수 있다.
2.4 도메인 영역의 주요 구성요소
구성요소
설명
예시
엔티티(Entity)
고유 식별자를 가지며, 상태와 동작을 포함하는 객체
User, Order, Product
값 객체(Value Object)
고유 식별자가 없으며, 불변 객체로 주로 여러 속성을 하나의 개념으로 묶을 때 사용
Address, Money, DateRange
애그리게잇(Aggregate)
관련된 엔티티와 값 객체의 그룹으로, 일관된 변경을 보장하기 위한 경계를 정의하고 루트 엔티티를 통해 접근
Order (루트: Order, 구성 요소: OrderLine)
리포지토리(Repository)
애그리게잇을 영구 저장소에서 조회하고 저장하는 메커니즘을 제공, 인터페이스와 구현체로 구성
OrderRepository, UserRepository
도메인 서비스(Domain Service)
특정 엔티티에 속하지 않는 도메인 로직을 캡슐화하는 서비스
PricingService, PaymentService
2.4.1 엔티티와 벨류
도메인 모델의 엔티티 vs DB 테이블의 엔티티
도메인 모델의 엔티티는 단순 데이터만 담고 있는 구조가 아닌 데이터와 함께 도메인 기능을 제공
도메인 모델 두개이상의 데이터가 개념적으로 하나인 경우에 Value 타입을 이용해서 표현할 수 있다.
ex) Order → name, email 을 Orderer라는 Value타입 객채를 만들어 관리 가능
2.4.2 애그리거트
지도를 볼 때 매우 상세하게 나온 대축척 지도를 보면 큰 수준에서 어디에 위치하고 있는지 이해하기 어려우므로 큰 수준에서 보여주는 소축척 지도를 함께 봐야 현재 위치를 보다 정확하게 이해할 수 있다. 이와 비슷하게 도메인 모델도 개별 객체뿐만 아니라 상위 수준에서 모델을 볼 수 있어야 전체 모델의 관계와 개별 모델을 이해하는데 도움이 된다.
ex) ‘주문’, ‘배송지 정보’, ‘주문자’, ‘주문 목록’, ‘총 결제 금액’의 하위 모델들을 ‘주문’이라는 상위 개념으로 표현할 수 있다.
2.5 요청 처리 흐름
2.6 인프라스트럭처 개요
표현 영역, 응용 영역, 도메인 영역을 지원한다.
DIP에서 언급한 내용처럼 인프라스트럭처의 기능을 직접 사용하는것보다 인터페이스를 만들어서 사용하게 되면 시스템을 더 유연하고 테스트하기 쉽게 만들어준다. 하지만 무조건 인프라스트럭처에 대한 의존을 없애게 되면 오히려 더 복잡하고 어려운 코드를 유도할 수 있다.
ex) 인프라 스트럭처를 직접 사용하지 않기 위해 @Transaction 어노테이션을 사용하지 않고 개발을 하려고 하면 한줄로 처리할 수 있는 코드를 복잡하고 개발시간만 더 늘어날 뿐이다.
2.7 모듈 구성
도메인이 크면 하위 도메인으로 나누고 각 하위 도메인 마다 별도 패키지를 구성한다.
모듈 구성에 대해서는 정답이 없으며, 한 패키지에 10~15개 미만으로 타입 개수를 유지하려고 노력한다.