Post

๐ŸŽฑ Querydsl์—์„œ Entity๋ฅผ DTO๋กœ ์ž๋™์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ @QueryProjection์˜ ์˜์กด์„ฑ์„ ์ œ๊ฑฐํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ๊ณ ์ฐฐโ€ฆ

๐ŸŽฑ Querydsl์—์„œ Entity๋ฅผ DTO๋กœ ์ž๋™์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ @QueryProjection์˜ ์˜์กด์„ฑ์„ ์ œ๊ฑฐํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ๊ณ ์ฐฐโ€ฆ

๐Ÿ–ค Introโ€ฆ

์ตœ๊ทผ์— Querydsl์— ๋Œ€ํ•œ ์Šคํ„ฐ๋””๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ Querydsl์˜ ์•„์ฃผ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜์ธ DTO Projections์— ๋Œ€ํ•ด์„œ ๊ฐ„๋žตํ•˜๊ฒŒ ์†Œ๊ฐœํ–ˆ๋‹ค. ํŠนํžˆ ์ด ์ค‘ @QueryProjection์ด ๋งค์šฐ ํŽธ๋ฆฌํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๊ธฐ์— ์ด๊ฑธ ์œ„์ฃผ๋กœ ์Šคํ„ฐ๋””๋ฅผ ์ง„ํ–‰ํ–ˆ๋Š”๋ฐ, ๋ฌธ๋“ ์˜๋ฌธ์ด ์ƒ๊ฒผ๋‹ค.

๐Ÿซจ โ€œ์ด๊ฑธ ์‚ฌ์šฉํ•˜๋ฉด QueryDSL์— ์˜์กด์„ฑ์ด ์ƒ๊ธด๋‹คโ€๋Š” ๋ฌธ์ œ์ ์ด ์žˆ๋Š”๊ฑด ์•Œ๊ฒ ๋Š”๋ฐ, ๊ทธ๋ ‡๋‹ค๋ฉด ๋ฌธ์ œ์ ์ด ๊ณผ์—ฐ ์ด๊ฑฐ ํ•˜๋‚˜์ผ๊นŒ?

์ด๋ ‡๊ฒŒ ๊ฐ„๋‹จํ•œ ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š”๊ฒŒ ์žˆ๋‹ค๋ฉด, ๊ตณ์ด Projections.bean์ด๋ผ๋Š” ๊ฒƒ์€ ์™œ ์กด์žฌํ•˜๋Š” ๊ฒƒ์ผ๊นŒ?

API ๋ช…์„ธ์— ๋“ค์–ด๊ฐ€๋Š” Request, Response ๊ฐ™์€ ๊ฒฝ์šฐ๋Š” ์™ธ๋ถ€ ์š”์†Œ์— ์˜ํ•œ ๋ณ€๋™์ด ์—†์–ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์œผ๋กœ ์•Œ๊ณ  ์žˆ๋Š”๋ฐ, ๊ทธ๋ ‡๋‹ค๋ฉด ๋Œ€๋ถ€๋ถ„์ด ์ด Request, Response์— ๋„๋ฉ”์ธ ์˜์—ญ์ธ Entity๋ฅผ ์ง์ ‘ ๋…ธ์ถœํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด์„œ DTO๋กœ ์ด๋ฅผ ๋ณ€ํ™˜ํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์•Œ๊ณ  ์žˆ๋Š”๋ฐ, ์ด DTO์— QueryDSL์˜์กด์„ฑ์ด ์ƒ๊ธฐ๋Š” ๊ฒƒ์ด ๊ณผ์—ฐ โ€œ์•ˆ์ „ํ•œโ€๊ฒƒ์ธ๊ฐ€? ์˜ค๋Š˜์€ ์ด ๋ถ€๋ถ„์— ๋Œ€ํ•œ ์˜๋ฌธ์„ ํ•ด์†Œํ•ด๋ณด์ž!

๐Ÿฉถ Start

์šฐ์„ , ์ด ๋ถ€๋ถ„์„ ์•Œ์•„๋ณด๊ธฐ ์œ„ํ•ด โ€œํ”„๋กœ์ ์…˜โ€์˜ ๊ฐœ๋…์„ ๋จผ์ € ์•Œ์•„๋ณด๋„๋ก ํ•˜์ž. ํ”„๋กœ์ ์…˜์ด๋ž€, select๋กœ ๋Œ€์ƒ์„ ์ง€์ •ํ•˜๋Š” ๊ฒƒ์„ ๋งํ•œ๋‹ค. QueryDSL์€ ์ด Projections์—์„œ, Entity๋ฅผ DTO๋กœ ์‰ฝ๊ฒŒ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ํš๊ธฐ์ ์ธ ๋ฐฉ์‹์„ ๋ช‡๊ฐ€์ง€ ์ œ๊ณตํ•œ๋‹ค. ์ด์— ๋Œ€ํ•ด ๋จผ์ € ์•Œ์•„๋ณด์ž.

QueryDSL์—์„œ ํ”„๋กœ์ ์…˜ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•

QueryDSL์—์„œ ํ”„๋กœ์ ์…˜ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ํฌ๊ฒŒ DTO๋กœ ์กฐํšŒํ•˜๋Š” ๊ฒƒ, ๊ทธ๋ฆฌ๊ณ  ํŠœํ”Œ๋กœ ์กฐํšŒํ•˜๋Š” ๊ฒƒ ๋‘ ๊ฐ€์ง€๊ฐ€ ์žˆ๋‹ค. ํŠœํ”Œ์€ QueryDSL์—์„œ ์ œ๊ณตํ•˜๋Š” Tuple ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ด์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
ํ”„๋กœ์ ์…˜ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ - ์—ฌ๋Ÿฌ๊ฐœ (ํŠœํ”Œ)

- ํ”„๋กœ์ ์…˜ ๊ฒฐ๊ณผ๊ฐ€ ์—ฌ๋Ÿฌ๊ฐœ์ผ๋•Œ, ํŠœํ”Œ๋กœ ๋ฐ˜ํ™˜
- ํŠœํ”Œ์˜ ๊ฒฝ์šฐ, QueryDSL์—์„œ ์ œ๊ณตํ•˜๋Š” Tuple ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋ฏ€๋กœ QueryDSL์— ์˜์กด์ ์ด๋ผ๋Š” ๋‹จ์ ์ด ์กด์žฌํ•œ๋‹ค.
 */
@Test
void ํ”„๋กœ์ ์…˜_ํŠœํ”Œ() throws Exception {
    //given

    //when
    List<Tuple> result = queryFactory
            .select(member.username, member.age) // member.username๊ณผ member.age๋ฅผ ์„ ํƒ
            .from(member)
            .fetch();

    //then
    for (Tuple tuple : result) {
        String username = tuple.get(member.username);
        Integer age = tuple.get(member.age);
        System.out.println("username = " + username + ", age = " + age);
    }
}

์ด๊ฒƒ ์—ญ์‹œ QueryDSL์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋ฏ€๋กœ, ์ด๋ ‡๊ฒŒ ์˜์กด์„ฑ์„ ๊ฐ€์ง„ ๊ฐ์ฒด๊ฐ€ repository ๋ฐ–์œผ๋กœ ์ƒˆ์–ด ๋‚˜๊ฐ€๋Š” ๊ฒƒ์€ ์ข‹์€ ์„ค๊ณ„๊ฐ€ ์•„๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ๋ง‰๊ฐ„์„ ์ด์šฉํ•ด์„œ ์™œ ๊ธฐ์ˆ  ์˜์กด์„ฑ์„ ๊ฐ€์ง„ ๊ฐ์ฒด๊ฐ€ repository ๋ฐ–์œผ๋กœ ์ƒˆ์–ด ๋‚˜๊ฐ€๋Š” ๊ฒƒ์ด ์ข‹์ง€ ์•Š์€์ง€๋ฅผ ์•Œ์•„๋ณด๋„๋ก ํ•˜์ž.

์™œ ์˜์กด์„ฑ์„ ๊ฐ€์ง„ ๊ฐ์ฒด๊ฐ€ repository ๋ฐ–์œผ๋กœ ์ƒˆ์–ด ๋‚˜๊ฐ€๋Š” ๊ฒƒ์ด ์ข‹์ง€ ์•Š์€๊ฑธ๊นŒ?

์ด๋Š” ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๊ฒฝ๊ณ„๋ฅผ ๋„˜์–ด์„œ ์˜์กด์„ฑ์„ ๊ฐ€์ง„ ๊ฐ์ฒด(์ฆ‰, ํ”„๋ ˆ์ž„ ์›Œํฌ๋‚˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์˜์กดํ•˜๋Š” ์ˆœ์ˆ˜ํ•œ ์ƒํƒœ๊ฐ€ ์•„๋‹Œ ๊ฐ์ฒด๋ฅผ ์˜๋ฏธํ•œ๋‹ค.)๊ฐ€ ์„œ๋น„์Šค ๋‹จ์œผ๋กœ ์ƒˆ์–ด๋‚˜๊ฐ€๋ฉด, ๊ณ„์ธต๊ฐ„ ๊ฒฐํ•ฉ์ด ์ปค์ง€๊ณ  ๊ต์ฒด, ํ…Œ์ŠคํŠธ, ๋ฆฌํŒฉํ„ฐ๋ง ๋น„์šฉ์ด ํญ์ฆํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์ฆ‰, ์‚ฌ์‹ค SOLID ์›์น™์„ ์ค€์ˆ˜ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๊ฒฐํ•ฉ๋„๋Š” ๋‚ฎ์ถ”๊ณ  ์‘์ง‘์„ฑ์€ ๊ฐ•ํ•˜๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด ๋งค์šฐ ์ค‘์š”ํ•œ๋ฐ, ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด โ€œ๊ฐ•ํ•œ ๊ฒฐํ•ฉโ€์ด ๋œ๋‹ค๋Š” ๋ฌธ์ œ์ ์ด ์กด์žฌํ•œ๋‹ค. ๋˜ํ•œ, ์ˆœ์ˆ˜ํ•œ ํ™˜๊ฒฝ์—์„œ ์ง„ํ–‰ํ•ด์•ผ ํ•˜๋Š” ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์—ญ์‹œ ์ด๋ ‡๊ฒŒ ์˜์กด์„ฑ์„ ๊ฐ€์งˆ ๊ฒฝ์šฐ ์ƒ๋‹นํ•œ ์ œ์•ฝ์„ ๋ฐ›๋Š”๋‹ค.

QueryDSL์—์„œ ํ”„๋กœ์ ์…˜ ๊ฒฐ๊ณผ๋ฅผ DTO๋กœ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•

๊ทธ๋ ‡๋‹ค๋ฉด DTO๋กœ ์ง์ ‘ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์–ด๋–จ๊นŒ? QueryDSL์€ ํ”„๋กœ์ ์…˜ ๊ฒฐ๊ณผ๋ฅผ DTO๋กœ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ์˜ ๋ฐฉ๋ฒ•์„ ์ •์˜ํ•˜๊ณ  ์žˆ๋‹ค.

  1. Projections.bean
  2. Projections.fields
  3. Projections.constructor
  4. @QueryProjection

ํ•˜๋‚˜์”ฉ ์‚ดํŽด๋ณด์ž.

Projections.bean

setter ๋ฉ”์„œ๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. ์ฆ‰, Entity์˜ ๋ชจ๋“  ํ•„๋“œ๋ฅผ DTO์— setter๋ฅผ ์ด์šฉํ•ด์„œ ์ฃผ์ž…ํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. ๋”ฐ๋ผ์„œ ์ด ๋ฐฉ์‹์„ ์ด์šฉํ•  ์‹œ DTO์˜ ๋ชจ๋“  ํ•„๋“œ์— Setter๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ๊ทธ๋ž˜์„œ @Setter๋ฅผ ๊ฐ™์ด ํ™œ์šฉํ•ด์•ผ ํ•œ๋‹ค. ๋งˆ์น˜ MyBatis์—์„œ ๋งคํ•‘ํ•˜๋Š” ๋ฐฉ์‹๊ณผ ์œ ์‚ฌํ•˜๋‹ค. (MyBatis์—ญ์‹œ Setter ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•˜๋ฏ€๋กœ..)

1
2
3
4
5
6
7
8
//when
//ํ”„๋กœํผํ‹ฐ ์ ‘๊ทผ - Setter
List<MemberDTO> result1 = queryFactory
        .select(Projections.bean(MemberDTO.class,
                member.username,
                member.age))
        .from(member)
        .fetch();

๊ทธ๋Ÿฌ๋‚˜, Setter๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค๋Š” ๊ฒƒ ์ž์ฒด๊ฐ€ ์ด ๋ฐฉ์‹์˜ ๊ฐ€์žฅ ํฐ ๋‹จ์ ์ด๋‹ค. Setter๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ, ๋ฐ์ดํ„ฐ์˜ ๋ณ€๊ฒฝ์— ์ทจ์•ฝํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๋ง‰๊ฐ„์„ ์ด์šฉํ•ด์„œ ์™œ Setter๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๊ฒŒ ์ข‹์€์ง€ ์•Œ์•„๋ณด๋„๋ก ํ•˜์ž.

์™œ Setter๋ฅผ ์ง€์–‘ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์„๊นŒ?

Setter ๊ธฐ๋ฐ˜์ด๋ผ๋Š” ๊ฒƒ์€, ๊ฒฐ๊ตญ ๊ฐ์ฒด์˜ ์ƒํƒœ๋ฅผ ์™ธ๋ถ€์—์„œ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด๋ผ๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค. ์ฆ‰, Setter๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ˆ„๊ฐ€, ์–ด๋””์„œ๋“ ์ง€ ๊ฐ’์„ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค. ์ƒ์„ฑ์ž(๋นŒ๋”)๋ฅผ ์ด์šฉํ•˜๋˜, ์ ‘๊ทผ ์ œํ•œ์ž๋ฅผ private, protected๋“ฑ์œผ๋กœ ์„ค์ •ํ•˜๋ฉด ์™ธ๋ถ€ ์ž„์˜ ์ƒ์„ฑ์„ ๋ง‰์„ ์ˆ˜ ์žˆ๋Š”๊ฒƒ์— ๋ฐ˜ํ•ด, Setter๋Š” ์ด๋Ÿฐ ๋ณด์กฐ ๊ธฐ๋Šฅ์„ ๋ถ™์ผ ์ˆ˜ ์—†์–ด์„œ ๋งค์šฐ ์ทจ์•ฝํ•˜๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์™ธ๋ถ€์—์„œ ์€ํ–‰ ๊ณ„์ขŒ์— ์ ‘๊ทผํ•ด์„œ setBalance(0)๋“ฑ์œผ๋กœ ์„ค์ •์„ ํ•ด๋ฒ„๋ฆฐ๋‹ค๋ฉดโ€ฆ? ์ƒ๊ฐ๋งŒํ•ด๋„ ๋“ฑ๊ณจ์ด ์˜ค์‹นํ•œ ๊ฒฝํ—˜์„ ํ•  ๊ฒƒ์ด๋‹คโ€ฆ

Projections.fields

์ด ๋ฐฉ์‹์€ ํ•„๋“œ์— ์ง์ ‘ ์ ‘๊ทผํ•ด์„œ ๊ฐ’์„ ์ฃผ์ž…ํ•˜์—ฌ, DTO๋กœ ๋‚ด๋ณด๋‚ด๋Š” ๋ฐฉ์‹์ด๋‹ค.

1
2
3
4
5
6
7
//ํ•„๋“œ ์ง์ ‘ ์ ‘๊ทผ - ํ•„๋“œ์— ์ง์ ‘ ์ ‘๊ทผํ•˜์—ฌ ๊ฐ’์„ ์„ค์ •
List<MemberDTO> result2 = queryFactory
        .select(Projections.fields(MemberDTO.class,
                member.username,
                member.age))
        .from(member)
        .fetch();

๋‹จ, type์ด ๋‹ค๋ฅผ ๊ฒฝ์šฐ ๋งค์นญ๋˜์ง€ ์•Š์œผ๋ฉฐ, dto์—์„œ ์„ค์ •ํ•œ ๋ณ„์นญ๊ณผ ์—”ํ‹ฐํ‹ฐ์˜ ํ•„๋“œ๋ช…์ด ๋‹ค๋ฅผ ๊ฒฝ์šฐ, as๋“ฑ์„ ์ด์šฉํ•ด์„œ ์ถ”๊ฐ€์ ์œผ๋กœ ๋งž์ถฐ์ค˜์•ผ ํ•œ๋‹ค๋Š” ๋‹จ์ ์ด ์กด์žฌํ•œ๋‹ค.

1
member.username.as("name")

๋˜ํ•œ ๊ฒฐ์ •์ ์œผ๋กœ, ์ปดํŒŒ์ผ ์‹œ์ ์— ์—๋Ÿฌ๋ฅผ ์ฐพ์„ ์ˆ˜๊ฐ€ ์—†๋‹ค!!! ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ๋งŒ ๋ฐœ์ƒํ•œ๋‹ค. ์ด๋Š” ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ dto์— ํ•ด๋‹น ํ•„๋“œ๋“ค์ด ์žˆ๋Š”์ง€๋ฅผ ์ „ํ˜€ ๋ชจ๋ฅด๊ธฐ ๋•Œ๋ฌธ์—, ๋Ÿฐํƒ€์ž„์— ์ด๋ฅผ ๋งค์นญํ•˜๊ณ ์ž ์‹œ๋„ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๊ฐœ์ธ์ ์œผ๋กœ ๋‚˜๋Š” ์ปดํŒŒ์ผ ์—๋Ÿฌ๋ฅผ ๋งค์šฐ ์‚ฌ๋ž‘ํ•˜๊ณ โ€ฆ.๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ๋ฅผ ๋งค์šฐ ์‹ซ์–ดํ•œ๋‹คโ€ฆ.๋””๋ฒ„๊น…์ด ์–ด๋ ต๋‹ค ๊ทธ๋ž˜์„œ ์ด ๋‹จ์ ์ด ์ข€ ํฌ๊ฒŒ ๋‹ค๊ฐ€์™”๋‹ค.

Projections.constructors

1
2
3
4
5
6
7
//์ƒ์„ฑ์ž ์‚ฌ์šฉ - ์ƒ์„ฑ์ž๋ฅผ ํ†ตํ•ด ๊ฐ’์„ ์„ค์ •
List<MemberDTO> result3 = queryFactory
        .select(Projections.constructor(MemberDTO.class,
                member.username,
                member.age))
        .from(member)
        .fetch();

์ƒ์„ฑ์ž๋กœ ๊ฐ’์„ ์„ค์ •ํ•ด์„œ DTO๋กœ ๋‚ด๋ณด๋‚ด๋Š” ๋ฐฉ์‹์ด๋‹ค. ์ฆ‰, ์ƒ์„ฑ์ž ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ์ž…ํ•˜๋Š” ๊ฒƒ์ธ๋ฐ ์ƒ์„ฑ์ž๋กœ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์œ„์—์„œ ๋งํ–ˆ๋˜ ๊ฒƒ์ฒ˜๋Ÿผ, setter์™€ ๋‹ค๋ฅด๊ฒŒ ๊ฐ์ฒด์˜ ๋ถˆ๋ณ€์„ฑ์„ ๊ฐ€์ ธ๊ฐˆ ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์กด์žฌํ•œ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜, ๊ฐ’์„ ๋„˜๊ธธ๋•Œ ์ƒ์„ฑ์ž์™€ ํ•„๋“œ์˜ ์ˆœ์„œ๋ฅผ ๋ฐ˜๋“œ์‹œ ์ผ์น˜์‹œ์ผœ์•ผ๋งŒ ํ•œ๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๊ธฐ์—, ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์œผ๋ฉด ๋ฐ”์ธ๋”ฉ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•  ํ™•๋ฅ ์ด ๋งค์šฐ ํฌ๋‹ค ๋˜ํ•œ, ์ด Projections.constructors ์—ญ์‹œ ๋Ÿฐํƒ€์ž„ ์‹œ์ ์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ํ˜•์‹์ด๋ผ, ์ปดํŒŒ์ผ ์—๋Ÿฌ๋ฅผ ์žก์„ ์ˆ˜ ์—†๋‹ค๋Š” ๋‹จ์ ์ด ์กด์žฌํ•œ๋‹ค.

@QueryProjection

์–ด๋…ธํ…Œ์ด์…˜์„ ์ด์šฉํ•ด์„œ ํ”„๋กœ์ ์…˜์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. ์ด ์–ด๋…ธํ…Œ์ด์…˜์„ ์ ์šฉํ•  DTO์˜ ์ƒ์„ฑ์ž์— ๋ถ™์—ฌ์ฃผ๋ฉด, QDTO๋ผ๋Š” ๊ฒƒ์„ ์ƒ์„ฑํ•˜๋Š”๋ฐ ์ด๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์ƒ์„ฑ์ž ๋ฐฉ์‹์œผ๋กœ ์†์‰ฝ๊ฒŒ ์—”ํ‹ฐํ‹ฐ > DTO ๋ณ€ํ™˜์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
ํ”„๋กœ์ ์…˜ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ - @QueryProjection

- ์ด๊ฑธ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ, DTO๋„ QํŒŒ์ผ๋กœ ์ƒ์„ฑํ•ด์„œ ์ฟผ๋ฆฌ ๋‚ ๋ฆด๋•Œ๋„ ํ™œ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋‹ค!
- ๋‹ค๋งŒ, DTO์— @QueryProjection ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์—ฌ์•ผ ํ•œ๋‹ค. ์ฆ‰, QueryDSL์— ์˜์กดํ•ด์•ผ ํ•œ๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ์Œ.
 */
@Test
void ํ”„๋กœ์ ์…˜_QueryProjection() throws Exception {
    //given

    //when
    List<MemberDTO> result = queryFactory
            .select(new QMemberDTO(member.username, member.age)) // QMemberDTO๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ DTO ์ƒ์„ฑ
            .from(member)
            .fetch();

    //distinct ์‚ฌ์šฉ๋ฒ•
    List<String> resultDist = queryFactory
            .select(member.username)
            .distinct() // ์ค‘๋ณต ์ œ๊ฑฐ
            .from(member)
            .fetch();

    //then

}

์ด ๋ฐฉ์‹์˜ ์žฅ์ ์€ โ€œ์ปดํŒŒ์ผ ์‹œ์ ์— ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค๋Š” ๊ฒƒโ€์ด๋‹ค!!! ์ด๋Š” Querydsl์ด ์• ๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด Qํด๋ž˜์Šค (QDTO)๋ฅผ ์ƒ์„ฑํ•ด ์ฃผ๊ธฐ ๋•Œ๋ฌธ์—, apt๊ฐ€ ์ด๋ฅผ ํ˜ธ์ถœํ•ด์„œ ์ƒ์„ฑํ•˜๋Š” ๊ณผ์ •์—์„œ ์•ž๋‹จ์—์„œ ๋จผ์ € ์—๋Ÿฌ๋ฅผ ๋‚ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ด๋ ‡๊ฒŒ ํŽธ๋ฆฌํ•œ ๊ธฐ๋Šฅ์—๋Š” ๋‹จ์ ์ด ์กด์žฌํ•˜๊ธฐ ๋งˆ๋ จ์ด๋‹ค.

์šฐ๋ฆฌ๋Š” ์•ž์—์„œ ์™ธ๋ถ€์— ๊ธฐ์ˆ  ์ข…์†์ ์ธ ๊ฐ์ฒด๋ฅผ ๋…ธ์ถœํ•˜๋ฉด ์•ˆ๋œ๋‹ค๋Š” ๊ฒƒ์„ ๋งํ–ˆ๋‹ค ๊ทธ๋Ÿฌ๋‚˜ @QueryProjection์€ ํŠนํžˆ Q๊ฐ€ ๋ถ™์€ DTO๋ฅผ ์ด์šฉํ•ด์„œ ๋ณ€ํ™˜ํ•œ๋‹ค๋Š” ์ ์—์„œ ์ด์— ์™„๋ฒฝํžˆ ์œ„๋ฐฐ๋œ๋‹ค. (์‚ฌ์‹ค ๋‹ค๋ฅธ Projections ์—ญ์‹œ ๋งˆ์ฐฌ๊ฐ€์ง€๋‹คโ€ฆ.) ๊ทธ๋ž˜์„œ ์šฐ๋ฆฌ๋Š” ๋ชจ๋“  ๋ ˆ์ด์–ด์—์„œ ์ด QDTO๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ํ•˜๋Š” ์ด ํŒจํ„ด์—์„œ ๋ฒ—์–ด๋‚˜๋Š” ๋ฐฉ์‹์„ ๊ณ ์•ˆํ•˜์—ฌ ์ด๋ฅผ ํ™œ์šฉํ•ด์•ผ ํ•œ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ @QueryProjection์„ ๋น„์ข…์†์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜?

์šฐ์„  @QueryProjection์ด๊ฑธ ์ข…์†์ ์œผ๋กœ ์‚ฌ์šฉํ•œ ๋‚ด ๋ฆฌํŒฉํ„ฐ๋ง ์ „ ์ฝ”๋“œ๋ฅผ ํ•œ ๋ฒˆ ์‚ดํŽด๋ณด์ž.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Repository
@RequiredArgsConstructor //์ƒ์„ฑ์ž ์ฃผ์ž…
public class ShortsQueryRepositoryImpl implements ShortsQueryRepository {

    private final JPAQueryFactory query; //config ํŒŒ์ผ์„ ํ†ตํ•ด ๋ฐ”๋กœ ์ฃผ์ž…

    @Override
    public List<ShortsResponseDto> searchShorts(String nickname, String keyword) {
        return query
                .select(
                        new QShortsResponseDto(
                        shorts.id,
                        shorts.shortsName,
                        shorts.thumbnail
                ))
                .from(shorts)
                .where(
                        nicknameEq(nickname),
                        keywordContains(keyword)
                ).orderBy(shorts.shortsName.desc())
                .fetch();
    }

    // --- ์กฐ๊ฑด ๋ฉ”์„œ๋“œ (๋™์  ์ฟผ๋ฆฌ) ---
    // ์กฐ๊ฑด ๋ฉ”์„œ๋“œ๋ผ๋ฆฌ ์กฐํ•ฉํ•ด์„œ ์žฌ์‚ฌ์šฉ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค. (๋ชจ๋“ˆํ™”๊ฐ€ ์ž˜ ๋˜์–ด ์žˆ์Œ)
    private BooleanExpression nicknameEq(String nickname) {
        return nickname != null ? shorts.customer.nickname.eq(nickname) : null;
    }

    private BooleanExpression keywordContains(String keyword) {
        return keyword != null ? shorts.shortsName.containsIgnoreCase(keyword) : null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Getter
@NoArgsConstructor
@Builder
@ToString
public class ShortsResponseDto {
    private Long id;
    private String shortsName;
    private String thumbnail;

    //์ด๋ฅผ ํ†ตํ•ด QDTO๋ฅผ ์ƒ์„ฑํ•ด์„œ, DTO ๋ณ€ํ™˜ ๊ณผ์ •์—†์ด (QueryDSL์ด ์•Œ์•„์„œ ํ•ด์คŒ) ํŽธํ•˜๊ฒŒ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.
    @QueryProjection
    public ShortsResponseDto(Long id, String shortsName, String thumbnail) {
        this.id = id;
        this.shortsName = shortsName;
        this.thumbnail = thumbnail;
    }
}

๋ณด๋ฉด ์•Œ๊ฒ ์ง€๋งŒ, API์— ๋…ธ์ถœํ•˜๋Š” ResponseDto์— Qdto๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ–ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•  ๊ฒฝ์šฐ, ํŠนํžˆ ์™ธ๋ถ€์— ์˜ํ•œ ๋ณ€ํ™” ๊ฐ€๋Šฅ์„ฑ์„ ์ตœ๋Œ€ํ•œ ์ œ๊ฑฐํ•ด์•ผ ํ•˜๋Š” Requsest, Response์— ํฐ ๋ฌธ์ œ..๊ฐ€ ์ƒ๊ธด๋‹ค. ์‚ฌ์‹ค์ƒ ์™ธ๋ถ€ ๋ณ€ํ™” ์ „ํŒŒ๋ฅผ ๋ง‰์œผ๋ ค๊ณ  Entity๋ฅผ ์•ˆ์“ฐ๊ธฐ๋กœ ํ•œ๊ฑด๋ฐ ์ด๋Ÿฌ๋ฉด ์˜๋ฏธ๊ฐ€ ์—†์–ด์ง€๋Š” ๊ฒƒ์ด๋‹คโ€ฆ. ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๊ตฌ์กฐ๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ฐ”๊ฟ”์•ผ ํ•œ๋‹ค. image.png

์ด๋ฅผ ์ฝ”๋“œ์— ๋ฐ˜์˜ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

DTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
@QueryProjection ์‚ฌ์šฉ์„ ์œ„ํ•œ ์ค‘๊ฐ„ ๋‚ด๋ถ€ DTO
 */

@Getter
@NoArgsConstructor
@Builder
@ToString
public class FilteredShortsResponse {
    private Long id;
    private String shortsName;
    private String thumbnail;

    //์ด๋ฅผ ํ†ตํ•ด QDTO๋ฅผ ์ƒ์„ฑํ•ด์„œ, DTO ๋ณ€ํ™˜ ๊ณผ์ •์—†์ด (QueryDSL์ด ์•Œ์•„์„œ ํ•ด์คŒ) ํŽธํ•˜๊ฒŒ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.
    @QueryProjection
    public FilteredShortsResponse(Long id, String shortsName, String thumbnail) {
        this.id = id;
        this.shortsName = shortsName;
        this.thumbnail = thumbnail;
    }
}

1
2
3
4
5
6
7
8
9
10
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class ShortsResponse {
    private Long id;
    private String shortsName;
    private String thumbnail;
}

์ปค์Šคํ…€ Repository

1
2
3
4
public interface ShortsQueryRepository {
    //๋™์  ์ฟผ๋ฆฌ ์˜ˆ์ œ : ํŠน์ • ์กฐ๊ฑด์œผ๋กœ ์‡ผ์ธ ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๋Š” ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž.
    List<FilteredShortsResponse> searchShorts(String nickname, String keyword);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Repository
@RequiredArgsConstructor //์ƒ์„ฑ์ž ์ฃผ์ž…
public class ShortsQueryRepositoryImpl implements ShortsQueryRepository {

    private final JPAQueryFactory query; //config ํŒŒ์ผ์„ ํ†ตํ•ด ๋ฐ”๋กœ ์ฃผ์ž…

    @Override
    public List<FilteredShortsResponse> searchShorts(String nickname, String keyword) {
        return query
                .select(
                        new QFilteredShortsResponse(
                        shorts.id,
                        shorts.shortsName,
                        shorts.thumbnail
                ))
                .from(shorts)
                .where(
                        nicknameEq(nickname),
                        keywordContains(keyword)
                ).orderBy(shorts.shortsName.desc())
                .fetch();
    }

    // --- ์กฐ๊ฑด ๋ฉ”์„œ๋“œ (๋™์  ์ฟผ๋ฆฌ) ---
    // ์กฐ๊ฑด ๋ฉ”์„œ๋“œ๋ผ๋ฆฌ ์กฐํ•ฉํ•ด์„œ ์žฌ์‚ฌ์šฉ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค. (๋ชจ๋“ˆํ™”๊ฐ€ ์ž˜ ๋˜์–ด ์žˆ์Œ)
    private BooleanExpression nicknameEq(String nickname) {
        return nickname != null ? shorts.customer.nickname.eq(nickname) : null;
    }

    private BooleanExpression keywordContains(String keyword) {
        return keyword != null ? shorts.shortsName.containsIgnoreCase(keyword) : null;
    }
}

JPA์™€ Querydsl์€ ์•Œ๋ฉด ์•Œ์ˆ˜๋ก ์‹ฌ์˜คํ•œ ๊ธฐ์ˆ ์ธ ๊ฒƒ ๊ฐ™๋‹ค. ๋งˆ์น˜ ๊ฐ์ฒด์ง€ํ–ฅ์„ ์ถ”๊ตฌํ•˜๊ธฐ ์œ„ํ•œ ๊ฐœ๋ฐœ์ž๋“ค์˜ ์‹ฌ์˜คํ•œ ๋…ธ๋ ฅ์ด ๋А๊ปด์ง„๋‹ฌ๊นŒ ์ด๋Ÿฐ ๊ธฐ์ˆ ์„ ์จ๋ณผ ์ˆ˜ ์žˆ๋‹ค๋Š”๊ฑฐ ์ž์ฒด๊ฐ€ ์ฐธ ํ–‰์šด์ธ ๊ฒƒ ๊ฐ™๋‹คโ€ฆ.๊ตณ

์ฐธ๊ณ  ์ž๋ฃŒ

๐Ÿค[Querydsl] Querydsl๊ณผ DTO, @QueryProjection ๋น„์ข…์†์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ

๐Ÿค[๊ธฐ๋ณธ ๊ฐœ๋…] QueryDSL ๊ฐœ๋… ์Šคํ„ฐ๋”” ์ž๋ฃŒ

This post is licensed under CC BY 4.0 by the author.