Querydsl CASE WHEN 쿼리 작성하기
Write Case Statement with Querydsl
애플리케이션 개발 과정에서는 다양한 조건에 따라 결과를 분류하고 가공해야 하는 상황이 자주 발생합니다. 이러한 경우, SQL의 CASE 문은 매우 유용하게 사용됩니다. 이번 글에서는 Querydsl을 사용하여 다양한 상황에서 CASE 문을 작성하는 방법을 상세히 알아보겠습니다.
Prerequisites
Querydsl을 활용하여 쿼리를 작성하고 그 결과를 확인하기 위해 필요한 데이터베이스 스키마를 설정합니다.
# JOBS
+-----------+-------+---+--------------+
|Column Name|Type |Key|Extra |
+-----------+-------+---+--------------+
|id |bigint |PRI|auto_increment|
|name |varchar| | |
+-----------+-------+---+--------------+
# MEMBERS
+-----------+-------+---+--------------+
|Column Name|Type |Key|Extra |
+-----------+-------+---+--------------+
|id |bigint |PRI|auto_increment|
|job_id |bigint |MUL| |
|username |varchar| | |
|age |int | | |
+-----------+-------+---+--------------+
직업 정보를 저장하는 JOBS
테이블은 직업의 ID와 이름을 저장하며 다음 DDL 문을 통해 생성할 수 있습니다:
create table JOBS
(
id bigint auto_increment primary key,
name varchar(255)
);
그리고, 회원 정보를 저장하는 MEMBERS
테이블은 회원의 ID, 직업 ID, 사용자명, 나이를 저장하며, JOBS
테이블과 연관 관계에 있습니다:
create table MEMBERS
(
id bigint auto_increment primary key,
job_id bigint,
username varchar(255),
age int,
foreign key (job_id) references JOBS (id)
);
쿼리의 조회 결과를 직접 확인하고 검증하기 위해 다양한 샘플 데이터도 준비합니다:
insert into
JOBS (name)
values
('Engineer'),
('Doctor' ),
('Artist' );
insert into
MEMBERS (job_id, username, age)
values
(1, 'Robert Downey Jr.', 10),
(1, 'Scarlett Johansson', 13),
(2, 'Chris Evans', 21),
(null, 'Mark Ruffalo', 27),
(3, 'Chris Hemsworth', 34),
(3, 'Jeremy Renner', 80),
(null, 'Tom Holland', 45),
(2, 'Paul Rudd', 52),
(3, 'Brie Larson', 53),
(1, 'Chadwick Boseman', 73),
(2, 'Elizabeth Olsen', 57),
(3, 'Tom Hiddleston', 65);
이제 데이터베이스 스키마와 더미 데이터가 준비되었습니다. 이러한 데이터를 바탕으로 Querydsl을 사용하여 다양한 CASE 구문을 살펴보도록 하겠습니다.
Using CASE in SELECT
Querydsl에서 CASE 문을 작성할 때는 CaseBuilder
객체를 사용합니다. CaseBuilder
는 SQL의 CASE 문과 유사한 방식으로 조건을 설정하고, 해당 조건에 맞는 값을 반환할 수 있도록 지원합니다. 다음은 CaseBuilder
를 활용하여 기본적인 CASE 문을 작성하는 방법입니다:
@Override
public List<MemberResult> useCaseInSelect() {
return queryFactory
.select(Projections.constructor(
MemberResult.class,
new CaseBuilder()
.when(member.age.lt(18)).then("Minor")
.when(member.age.between(18, 60)).then("Adult")
.otherwise("Senior").as("type"),
member.age.as("age"),
member.username.as("name"))
)
.from(member)
.fetch();
}
JPAQueryFactory
를 활용하여 member
테이블에서 데이터를 조회하고, 연령대에 따라 분류한 결과를 반환합니다.
CaseBuilder
: Querydsl의 CASE 문을 작성할 수 있는 클래스입니다.when()
: 조건을 지정합니다.then()
: 조건이 참일 때 반환할 값을 지정합니다.otherwise()
: 모든 조건이 거짓일 때 반환할 값을 지정합니다.as()
: 생성된 CASE 표현식에 별칭을 지정합니다.
Querydsl로 작성한 쿼리는 실행 시점에 다음과 같은 SQL 쿼리로 변환됩니다. 이 변환은 Querydsl이 JPAQueryFactory
를 통해 쿼리를 생성하고 실행할 때 자동으로 이루어집니다:
select
case
when (age < 18) then 'Minor'
when (age between 18 and 60) then 'Adult'
else 'Senior' end as type,
age,
username
from
members
Querydsl로 작성한 쿼리가 제대로 동작하는지 테스트 코드를 통해 확인해보겠습니다:
@SpringBootTest
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Test
@DisplayName("case in select")
void shouldCaseInSelect() throws Exception {
List<MemberResult> results = memberRepository.useCaseInSelect();
for (MemberResult result : results) {
System.out.println(String.format(
"type=%s age=%s, name=%s",
result.getType(),
result.getAge(),
result.getName()));
}
}
}
위 테스트 코드는 리포지토리에 정의한 useCaseInSelect()
메서드를 호출하여 연령대별로 분류한 회원 리스트를 콘솔에 출력합니다. 실제 출력 결과는 다음과 같습니다:
type=Minor age=10, name=Robert Downey Jr.
type=Minor age=13, name=Scarlett Johansson
type=Adult age=21, name=Chris Evans
type=Adult age=27, name=Mark Ruffalo
type=Adult age=34, name=Chris Hemsworth
type=Senior age=80, name=Jeremy Renner
type=Adult age=45, name=Tom Holland
type=Adult age=52, name=Paul Rudd
type=Adult age=53, name=Brie Larson
type=Senior age=73, name=Chadwick Boseman
type=Adult age=57, name=Elizabeth Olsen
type=Senior age=65, name=Tom Hiddleston
위 결과에서 알 수 있듯이, CASE 구문이 적용되어 각 회원의 나이에 따라 적절하게 분류된 것을 확인할 수 있습니다.
복잡한 쿼리의 경우 가독성과 재활용성을 높이기 위해 CASE 구문을 메서드로 분리하는 것이 좋습니다:
@Override
public List<MemberResult> useCaseInSelect() {
return queryFactory
.select(Projections.constructor(
MemberResult.class,
determineByUserAge(),
member.age.as("age"),
member.username.as("name"))
)
.from(member)
.fetch();
}
private static StringExpression determineByUserAge() {
CaseBuilder caseBuilder = new CaseBuilder();
return caseBuilder
.when(member.age.between(10, 19)).then("10대")
.when(member.age.between(20, 29)).then("20대")
.when(member.age.between(30, 39)).then("30대")
.when(member.age.between(40, 49)).then("40대")
.when(member.age.between(50, 59)).then("50대")
.when(member.age.between(60, 69)).then("60대")
.when(member.age.between(70, 79)).then("70대")
.when(member.age.between(80, 89)).then("80대")
.otherwise("기타").as("type");
}
CASE 구문을 별도의 메서드로 정의하여 연령대를 세분화하였습니다. 이렇게 별도의 메서드로 정의하면 코드의 재사용성과 가독성이 크게 향상됩니다. 정의된 메서드는 다른 쿼리에서도 활용할 수 있어 중복을 줄일 수 있으며, 메서드 네이밍을 통해 쿼리의 의도를 명확하게 전달할 수 있습니다.
type=10대 age=10, name=Robert Downey Jr.
type=10대 age=13, name=Scarlett Johansson
type=20대 age=21, name=Chris Evans
type=20대 age=27, name=Mark Ruffalo
type=30대 age=34, name=Chris Hemsworth
type=80대 age=80, name=Jeremy Renner
type=40대 age=45, name=Tom Holland
type=50대 age=52, name=Paul Rudd
type=50대 age=53, name=Brie Larson
type=70대 age=73, name=Chadwick Boseman
type=50대 age=57, name=Elizabeth Olsen
type=60대 age=65, name=Tom Hiddleston
또한, 여러 조건을 결합하여 다양한 결과를 반환하는 복잡한 CASE 구문도 Querydsl을 활용하면 편리하게 작성할 수 있습니다. 예를 들어, 회원의 나이와 직업 상태를 기반으로 분류할 수 있습니다:
@Override
public List<MemberResult> useCaseInSelect() {
return queryFactory
.select(Projections.constructor(
MemberResult.class,
determineByStatus(),
member.age.as("age"),
member.username.as("name"))
)
.from(member)
.fetch();
}
public StringExpression determineByStatus() {
return new CaseBuilder()
.when(member.age.lt(18)).then("Minor ")
.when(member.age.between(18, 65).and(member.job.id.isNotNull())).then("Worker")
.when(member.age.between(18, 65).and(member.job.id.isNull())).then("Layoff")
.when(member.age.gt(65)).then("Senior")
.otherwise("Others");
}
이렇게 CASE 구문 내부에 BooleanExpression
을 사용하여 논리적 조건을 구성할 수 있습니다.
type=Minor age=10, name=Robert Downey Jr.
type=Minor age=13, name=Scarlett Johansson
type=Worker age=21, name=Chris Evans
type=Layoff age=27, name=Mark Ruffalo
type=Worker age=34, name=Chris Hemsworth
type=Senior age=80, name=Jeremy Renner
type=Layoff age=45, name=Tom Holland
type=Worker age=52, name=Paul Rudd
type=Worker age=53, name=Brie Larson
type=Senior age=73, name=Chadwick Boseman
type=Worker age=57, name=Elizabeth Olsen
type=Worker age=65, name=Tom Hiddleston
BooleanExpression
은 Querydsl에서 논리적 조건을 표현하는 인터페이스로, 다양한 메서드를 통해 복잡한 조건을 손쉽게 조합할 수 있게 합니다. 이를 통해 보다 직관적이고 읽기 쉬운 쿼리를 작성할 수 있습니다:
and()
: 두 조건을 결합하여 모두 참일 때 참이 되는 조건을 생성합니다.or()
: 두 조건을 결합하여 하나라도 참일 때 참이 되는 조건을 생성합니다.eq()
: 두 값이 같을 때 참이 되는 조건을 생성합니다.isNull()
: 값이 null일 때 참이 되는 조건을 생성합니다.
이러한 다양한 메서드를 활용하면, 복잡한 비즈니스 로직을 간결하고 명확하게 표현할 수 있으며, 쿼리의 가독성과 유지보수성을 높일 수 있습니다.
Using CASE in ORDER BY
Querydsl을 사용하여 ORDER BY 절에서 CASE 문을 활용하면 특정 조건에 따라 정렬 순서를 동적으로 변경할 수 있습니다. 이 기능은 데이터의 우선순위를 조건에 따라 조정해야 하는 다양한 상황에서 유용합니다.
다음 코드에서는 회원을 나이 그룹에 따라 정렬합니다:
public List<Member> useCaseInOrderBy() {
return queryFactory
.select(member)
.from(member)
.orderBy(
new CaseBuilder()
.when(member.age.lt(18)).then(1)
.when(member.age.between(18, 65)).then(2)
.otherwise(3).asc())
.fetch();
}
회원 테이블에서 데이터를 조회하여 나이를 기준으로 정렬합니다. 정렬 우선순위는 다음과 같습니다:
- 18세 미만: 최상위
- 18세 이상 65세 미만: 중간
- 65세 이상: 최하위
이 쿼리는 실행 시점에 다음과 같은 SQL 쿼리로 변환됩니다:
select
*
from
members
order by
case
when (age < 18) then 1
when (age between 18 and 65) then 2
else 3 end;
ORDER BY 절에 사용한 CASE 구문이 올바르게 동작하는지 확인하기 위해 테스트 코드를 작성해보겠습니다:
@SpringBootTest
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Test
@DisplayName("case in order by")
void shouldCaseInOrderBy() throws Exception {
List<Member> results = memberRepository.useCaseInOrderBy();
for (Member result : results) {
System.out.println(String.format(
"age=%s, name=%s",
result.getAge(),
result.getUsername()));
}
}
}
이 테스트 코드는 useCaseInOrderBy()
메서드를 호출하여 회원의 나이 그룹별로 정렬된 결과를 콘솔에 출력합니다. 출력 결과는 다음과 같습니다:
age=10, name=Robert Downey Jr.
age=13, name=Scarlett Johansson
age=21, name=Chris Evans
age=27, name=Mark Ruffalo
age=34, name=Chris Hemsworth
age=45, name=Tom Holland
age=52, name=Paul Rudd
age=53, name=Brie Larson
age=57, name=Elizabeth Olsen
age=65, name=Tom Hiddleston
age=80, name=Jeremy Renner
age=73, name=Chadwick Boseman
위 결과에서 볼 수 있듯이, 회원들이 나이 그룹에 따라 정렬된 결과가 제대로 출력됩니다. CaseBuilder
를 사용하여 ORDER BY 절에서 CASE 문을 쉽게 작성하고 데이터를 동적으로 정렬할 수 있습니다.
Querydsl을 통해 다양한 상황에서 CASE 문을 작성하고, SELECT 및 ORDER BY 절에서 이를 활용할 수 있습니다. 이러한 기능을 통해 더욱 효율적이고 유지보수하기 쉬운 쿼리를 작성할 수 있습니다.
- Spring
- JPA
- Querydsl