๊ธฐ์กด JPA๋กœ๋งŒ ์ž‘์—…ํ•˜๋˜ ํ”„๋กœ์ ํŠธ์—์„œ QueryDSL์„ ์ถ”๊ฐ€๋กœ ์ ์šฉํ•˜๋ฉด์„œ

'์–ด๋–ค ๊ฑธ ๋‘๊ณ , ์–ด๋–ค ๊ฑธ QueryDSL๋กœ ์˜ฎ๊ฒจ์•ผ ํ• ๊นŒ?'์— ๋Œ€ํ•ด์„œ ์ฐพ์•„๋ดค๋‹ค.

 

โœ… QueryDSL๋กœ ์˜ฎ๊ธฐ๋Š” ๊ธฐ์ค€

  1. ๋™์  ์กฐ๊ฑด์ด ์žˆ๋Š” ์กฐํšŒ
    โžก๏ธ QueryDSL ํ•„์ˆ˜!! JPQL @Query๋Š” ์กฐ๊ฑด ์กฐํ•ฉ์ด ๋Š˜์–ด๋‚ ์ˆ˜๋ก ๋ฌธ์ž์—ด ์ด์–ด ๋ถ™์ด๊ธฐ ์ง€์˜ฅ์ด ๋จ.
  2. exists, ์„œ๋ธŒ์ฟผ๋ฆฌ, ๋ณต์žกํ•œ OR ์กฐ๊ฑด
    โžก๏ธ QueryDSL์ด ์•ˆ์ „!! ๋ฌธ์ž์—ด JPQL์€ ์‹ค์ˆ˜๋‚˜ ๋ฆฌํŒฉํ† ๋ง์— ์ทจ์•ฝ
  3. ํŽ˜์ด์ง• + ์ •๋ ฌ + count ๋ถ„๋ฆฌ ํ•„์š”
    โžก๏ธ Page / Slice ๊ตฌํ˜„: QueryDSL์ด ๊ตฌ์กฐ์ ์œผ๋กœ ์œ ๋ฆฌ
  4. ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์กฐ์ธ DTO ํ”„๋กœ์ ์…˜
    โžก๏ธ QueryDSL์ด ์ปดํŒŒ์ผ ํƒ€์ž„ ํƒ€์ž… ์ฒดํฌ: JPQL ๋ฌธ์ž์—ด์€ ํ•„๋“œ๋ช… ์˜คํƒ€๊ฐ€ ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ
  5. ๊ฒ€์ƒ‰ ์กฐ๊ฑด์ด ๊ณ„์† ๋Š˜์–ด๋‚  ๊ฐ€๋Šฅ์„ฑ ์žˆ์Œ
    โžก๏ธ ์ดˆ๊ธฐ์— ๋‹จ์ˆœํ•ด ๋ณด์—ฌ๋„ ์•ž์œผ๋กœ ํ•„ํ„ฐ ์ถ”๊ฐ€ ์š”์ฒญ ์˜ˆ์ƒ๋˜๋ฉด QueryDSL

 

โŒ ๊ตณ์ด ์•ˆ ์˜ฎ๊ฒจ๋„ ๋˜๋Š” ๊ฒฝ์šฐ

  1. ๋‹จ์ˆœ CRUD (ex. `findById`, `deleteById`, `existsById`...)
    โžก๏ธ Spring Data ๊ธฐ๋ณธ ๋ฉ”์„œ๋“œ๋กœ ์ถฉ๋ถ„
  2. ๋‹จ์ˆœ ๋ฒŒํฌ ์ˆ˜์ •/์‚ญ์ œ (ex. `@Modifying @Query("delete ... where id = :id")`)
    โžก๏ธ ๊ทธ๋Œ€๋กœ ๋‘ฌ๋„ ๋จ (์˜คํžˆ๋ ค QueryDSL ๋ฒŒํฌ๋Š” flush/clear ์ง์ ‘ ๊ด€๋ฆฌ ํ•„์š”)
  3. DB ์ „์šฉ ๊ธฐ๋Šฅ (CTE / window function)
    โžก๏ธ QueryDSL-JPA๋Š” ํ‘œํ˜„ ๋ชปํ•จ. JDBC / native SQL ์œ ์ง€๊ฐ€ ๋” ๋‚ซ๋‹ค.
  4. ์ ˆ๋Œ€ ์•ˆ ๋ณ€ํ•  ์ •์  ์ฟผ๋ฆฌ (ex. “ํ™œ์„ฑ Y ๋ฐ์ดํ„ฐ ์ „๋ถ€ ์กฐํšŒ”)
    โžก๏ธ `@Query` ํ•˜๋‚˜๋กœ ๋๋‚˜๊ณ  ๋๊นŒ์ง€ ์•ˆ ๋ณ€ํ•˜๋ฉด ์œ ์ง€

 

์ฐพ์•„๋ณธ ๊ฒฐ๊ณผ ์ด๋ ‡๊ฒŒ ์ •๋ฆฌํ–ˆ๋‹ค.

์ด๋Œ€๋กœ ํŒ€ ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ ๋ฌธ์„œ๋‚˜, ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ ์‹œ ์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค!

`gradle dependencies`์—๋Š” api ์„ ์–ธ๊ณผ implementation ์„ ์–ธ์ด ์กด์žฌํ•œ๋‹ค.

ํŠนํžˆ ๋ฉ€ํ‹ฐ๋ชจ๋“ˆ ๊ตฌ์กฐ์ธ ํ”„๋กœ์ ํŠธ์˜ ๊ฒฝ์šฐ ๋‘ ๊ฐ€์ง€๋ฅผ ํ•จ๊ป˜ ์“ฐ๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ๊ธฐ ๋•Œ๋ฌธ์— ์ •๋ฆฌ๋ฅผ ํ•ด๋‘๋ ค๊ณ  ํ•œ๋‹ค!


๐Ÿ˜ api, implementation

dependencies {
	api 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-security'
}

 

api ์„ ์–ธ์„ ์‚ฌ์šฉํ•˜๋ฉด ์„ ์–ธ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ํ•ด๋‹น ๋ชจ๋“ˆ์„ ์˜์กดํ•˜๋Š” ๋ชจ๋“ˆ์˜ ์ปดํŒŒ์ผ, ๋Ÿฐํƒ€์ž„์˜ ํด๋ž˜์ŠคํŒจ์Šค์— ๋ชจ๋‘ ํฌํ•จ๋œ๋‹ค.

โ€ป ์˜ˆ์‹œ์˜ spring-boot-starter-data-jpa๋Š” api ์„ ์–ธ์ด๊ธฐ ๋•Œ๋ฌธ์—, ์ด ๋ชจ๋“ˆ์„ ์˜์กดํ•˜๋Š” ๋‹ค๋ฅธ ๋ชจ๋“ˆ๋“ค๋„ ํ•ด๋‹น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

ํ•˜์ง€๋งŒ api ์„ ์–ธ์„ ๋‚จ์šฉํ•˜๊ฒŒ ๋˜๋ฉด ๋ชจ๋“ˆ๊ฐ„ ๊ฒฐํ•ฉ์ด ์ฆ๊ฐ€ํ•˜๊ฒŒ ๋˜๊ณ , ๋ณ€๊ฒฝ ์ „ํŒŒ๊ฐ€ ์ปค์ง„๋‹ค.

 

implementation ์„ ์–ธ์€ ํ˜„์žฌ ๋ชจ๋“ˆ ๋‚ด์—์„œ๋งŒ ์ปดํŒŒ์ผ, ๋Ÿฐํƒ€์ž„์— ์‚ฌ์šฉ๋˜๊ณ  ์™ธ๋ถ€์—๋Š” ๊ณต๊ฐœ๋˜์ง€ ์•Š๋Š”๋‹ค.

โ€ป ์˜ˆ์‹œ์˜ spring-boot-starter-security๋Š” ์ด ๋ชจ๋“ˆ์„ ์˜์กดํ•˜๋Š” ๋‹ค๋ฅธ ๋ชจ๋“ˆ๋“ค์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ณ , ๊ฐ๊ฐ ์„ ์–ธ์„ ๋‹ค์‹œ ํ•ด์ค˜์•ผ ํ•œ๋‹ค.


๐Ÿšฉ ๋ชจ๋“ˆ ์„ ์–ธํ•˜๊ธฐ

`core`๋ชจ๋“ˆ๊ณผ `service`๋ชจ๋“ˆ์ด ์žˆ๊ณ , `service`๋ชจ๋“ˆ์—์„œ `core`๋ชจ๋“ˆ์„ ๊ฐ€์ ธ๋‹ค ์“ฐ๋ ค๊ณ  ํ•œ๋‹ค.

// core
plugins {
	id 'java-library'
}
dependencies {
	api 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-security'
}

// service
dependencies {
	implementation project(":core")
	implementation 'org.springframework.boot:spring-boot-starter-security'
}

 

core ๋ชจ๋“ˆ์—์„œ๋Š” `java-library` ํ”Œ๋Ÿฌ๊ทธ์ธ๋„ ์„ ์–ธ์„ ํ•ด์ค˜์•ผ ํ•œ๋‹ค.

์ดํ›„ service ๋ชจ๋“ˆ์—์„œ `implementation project`๋ฅผ ํ†ตํ•ด core์— ์žˆ๋Š” api ์„ ์–ธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค.

 

`spring-boot-starter-security`๋Š” core์—์„œ implementation์œผ๋กœ ์„ ์–ธ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์—

service ๋ชจ๋“ˆ์—์„œ ์‚ฌ์šฉํ•˜๋ ค๋ฉด core ๋ชจ๋“ˆ์—์„œ api ์„ ์–ธ์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜, service ๋ชจ๋“ˆ์—์„œ ์žฌ์„ ์–ธ ํ•ด์•ผ ํ•œ๋‹ค.

`v-data-table`์—์„œ `:items="itemList"`๋กœ ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ๋„˜๊ฒผ๋Š”๋ฐ

`#body` ์Šฌ๋กฏ์—์„œ `items.length`๋ฅผ ์ฐ์–ด๋ณด๋ฉด 10๊ฐœ๋งŒ ๋‚˜์˜ค๋Š” ํ˜„์ƒ์ด ์ƒ๊ธด๋‹ค.

 

โœจ ์›์ธ: v-data-table ๋‚ด๋ถ€ ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ

`v-data-table`์€ `:items`๋กœ ์ „๋‹ฌ๋œ ์›๋ณธ ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์“ฐ์ง€ ์•Š๊ณ ,

๋‚ด๋ถ€์—์„œ ํŽ˜์ด์ง•/์ •๋ ฌ/ํ•„ํ„ฐ ์ฒ˜๋ฆฌ๊ฐ€ ์ž๋™์œผ๋กœ ์ˆ˜ํ–‰๋œ๋‹ค.

  • ํŽ˜์ด์ง• (page / items-per-page)
  • ์ •๋ ฌ (sort)
  • ํ•„ํ„ฐ (search)

์ด ๊ณผ์ •์„ ๊ฑฐ์นœ ๋’ค ํ˜„์žฌ ํ™”๋ฉด์— ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ๋งŒ ๋งŒ๋“ค์–ด ๋ Œ๋”๋ง๋œ๋‹ค.

`#body` ์Šฌ๋กฏ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๊ธฐ๋ณธ ๋ Œ๋”๋ง ์‹œ์—๋„ ์ด๋ฏธ ๋‚ด๋ถ€ ์ฒ˜๋ฆฌ๋œ ๊ฒฐ๊ณผ๊ฐ€ ๋„˜์–ด์˜จ๋‹ค.

 

๋”ฐ๋ผ์„œ ์›๋ณธ์ด 30๊ฐœ์—ฌ๋„ `items-per-page` ๊ธฐ๋ณธ๊ฐ’(10) ๋•Œ๋ฌธ์—

ํ˜„์žฌ ํŽ˜์ด์ง€์— ํ•ด๋‹นํ•˜๋Š” 10๊ฐœ๋งŒ `items`๋กœ ์ „๋‹ฌ๋œ๋‹ค.

 

`v-data-table`์— `items-per-page`๋ฅผ ์ง€์ •ํ•ด์ฃผ๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์ง€๋งŒ, ๊ณ ์ •๋œ ๊ฐ’์ด๊ธฐ ๋•Œ๋ฌธ์—

๋ณดํ†ต ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ๊ฐ€ ๋œ ๊ฐ’์„ `items-per-page`์— ์ ์šฉ์‹œ์ผœ์ค˜์„œ ์‚ฌ์šฉํ•œ๋‹ค.

์ •๋ ฌํ•ด๋‘” `List`๋ฅผ ` Collectors.toMap()`์œผ๋กœ ์ค‘๋ณต ์ œ๊ฑฐ ํ›„ ๋‹ค์‹œ `List`๋กœ ๋งŒ๋“ค์—ˆ๋”๋‹ˆ ์ˆœ์„œ๊ฐ€ ์—‰ํ‚ค๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

๊ตฌ๊ธ€๋ง ํ•œ ๊ฒฐ๋ก ์€ `Collectors.toMap()`์€ ๊ธฐ๋ณธ์ ์œผ๋กœ `HashMap`์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ˆœ์„œ๋ฅผ ์žƒ์–ด๋ฒ„๋ฆฐ๋‹ค.

`LinkedHashMap::new`๋กœ ์ˆ˜์ง‘ํ•˜๋ฉด ์ˆœ์„œ๊ฐ€ ๊ทธ๋Œ€๋กœ ์œ ์ง€๋œ๋‹ค.

 

๐Ÿ’ฅ ๊ธฐ์กด์— ์‚ฌ์šฉํ–ˆ๋˜ ์ฝ”๋“œ

List<User> userList = // ID ๊ธฐ์ค€ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ๋˜์–ด ์žˆ๋‹ค๊ณ  ๊ฐ€์ •

List<User> result = userList.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toMap(User::uniqueKey, Function.identity(), (exist, repl) -> exist),
        map -> new ArrayList<>(map.values())
    ));

System.out.println(result.stream().map(User::getId).collect(Collectors.toList()));
  • ๊ธฐ๋Œ€: ์›๋ณธ `userList` ์ˆœ์„œ (1, 2, 3, ... , 10, 12, 13, ...)
  • ์‹ค์ œ: ID ์ˆœ์„œ๊ฐ€ ํ‹€์–ด์ง€๊ฑฐ๋‚˜, ๊ฒฝ์šฐ์— ๋”ฐ๋ผ ๋ˆ„๋ฝ/์ค‘๋ณต์ด ๋ฐœ์ƒ

 

๐Ÿค” ์ˆœ์„œ๊ฐ€ ๊นจ์ง€๋Š” ์ด์œ 

๊ตฌํ˜„์ฒด ํŠน์ง•
HashMap (๊ธฐ๋ณธ) ํ‚ค/๊ฐ’ ์กฐํšŒ O / ์‚ฝ์ž… ์ˆœ์„œ ๋ณด์žฅ X
LinkedHashMap ํ‚ค/๊ฐ’ ์กฐํšŒ O / ์‚ฝ์ž… ์ˆœ์„œ ๋ณด์žฅ O

`Collectors.toMap()` โžก `new HashMap<>()` ์‚ฌ์šฉ โžก `values()`๋ฅผ ๊บผ๋‚ผ ๋•Œ ๋‚ด๋ถ€ ๋ฐฐ์—ด ์ˆœ์„œ๋ฅผ ์ƒˆ๋กœ ์ •๋ ฌํ•ด์„œ ์‚ฝ์ž… ์ˆœ์„œ ์œ ์ง€ ์‹คํŒจ

โ€ป Java 8 ์ดํ›„์—๋„ ๊ธฐ๋ณธ Map์€ ์—ฌ์ „ํžˆ `HashMap`์ด๋ผ๊ณ  ํ•œ๋‹ค.

 

โœจ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•: `LinkedHashMap` ์ง€์ •

List<User> result = userList.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toMap(
            User::uniqueKey,                // key mapper
            Function.identity(),            // value mapper
            (exist, repl) -> exist,         // ์ค‘๋ณต key ์ •์ฑ… : ๊ธฐ์กด๊ฐ’ ์œ ์ง€
            LinkedHashMap::new              // โญ๏ธ ์ˆœ์„œ ์œ ์ง€ Map ์ƒ์„ฑ
        ),
        map -> new ArrayList<>(map.values())
    ));
  • ์‹คํ–‰ ๊ฒฐ๊ณผ: ์›๋ณธ `userList` ์ˆœ์„œ (1, 2, 3, ... , 10, 11, 12, ...)

โœ… ํ˜„์ƒ ์š”์•ฝ

Vue CLI ํ”„๋กœ์ ํŠธ์—์„œ `src/assets/images/image.png`์ฒ˜๋Ÿผ ์ด๋ฏธ์ง€๋ฅผ ๋„ฃ๊ณ  ๋นŒ๋“œํ•˜๋ฉด, `/img/image.[hash].png`๋กœ ๊ฒฝ๋กœ๊ฐ€ ๋ณ€๊ฒฝ๋œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  `src/assets/images/`์— ์žˆ๋Š” ๋ช‡๊ฐ€์ง€ ํŒŒ์ผ๋“ค์€ `/img/`๊ฒฝ๋กœ ์•„๋ž˜์— ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค.

์™œ `images`ํด๋”์— ๋„ฃ์—ˆ๋Š”๋ฐ `img`๋กœ ๋ฐ”๋€Œ๋Š”์ง€, ๋‹ค๋ฅธ ํŒŒ์ผ๋“ค์€ ์™œ ์—†๋Š”์ง€ ์˜์•„ํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋งํ•˜์ž๋ฉด, Webpack + Vue CLI๊ฐ€ ์ž๋™์ ์œผ๋กœ ์ •์  ์ž์‚ฐ์„ ์ตœ์ ํ™”ํ•˜๋ฉด์„œ ๊ฒฝ๋กœ๋ฅผ ์žฌ๊ตฌ์„ฑํ•˜๊ณ  ํŒŒ์ผ ํฌ๊ธฐ์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•์ด ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

๐Ÿ’ก ๊ฒฝ๋กœ๊ฐ€ ๋ฐ”๋€Œ๋Š” ์ด์œ 

Vue CLI๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ Webpack์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ์ฒ˜๋ฆฌํ•  ๋•Œ ์•„๋ž˜์™€ ๊ฐ™์€ ๊ธฐ์ค€์œผ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.

  1. 8KB ์ดํ•˜์˜ ์ž‘์€ ์ด๋ฏธ์ง€ → Base64 ์ธ๋ผ์ธ ์ฒ˜๋ฆฌ
    • `base64`๋กœ ๋ณ€ํ™˜๋˜์–ด JS ๋ฒˆ๋“ค ์•ˆ์— ์ธ๋ผ์ธ์œผ๋กœ ์‚ฝ์ž…
    • ์ด ๊ฒฝ์šฐ ์ด๋ฏธ์ง€ ํŒŒ์ผ์€ ๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ์— ์ƒ์„ฑ๋˜์ง€ ์•Š์Œ
    • ๊ฐœ๋ฐœ์ž๋„๊ตฌ(Networkํƒญ)์—์„œ `data:image/png;base64,...` ํ˜•ํƒœ๋กœ ํ™•์ธ ๊ฐ€๋Šฅ
  2. 8KB ์ดˆ๊ณผ ์ด๋ฏธ์ง€ → `/img/` ํ•˜์œ„ ํด๋”๋กœ ์ถœ๋ ฅ
    • `file-loader`๋ฅผ ํ†ตํ•ด ๋ณต์‚ฌ๋˜๋ฉฐ, ํ•ด์‹œ๊ฐ€ ๋ถ™์€ ์ด๋ฆ„์œผ๋กœ `/img` ํด๋”์— ์ €์žฅ๋จ
    • ex: `src/assets/images/image.png` → `/img/image.8d3f2c12.png`

์ฆ‰, ` src/assets/images/`๋ผ๊ณ  ํ•ด๋„ ๋นŒ๋“œ ์‹œ Webpack์ด ์•Œ์•„์„œ `img/`, `css/`, `js/` ๋“ฑ์˜ ํด๋”๋กœ ์ •๋ฆฌํ•ด์„œ ์ถœ๋ ฅํ•˜๋Š” ๊ตฌ์กฐ๋‹ค.

 

๐Ÿช„ ๊ฒฝ๋กœ ๋ณ€๊ฒฝ ์ œ์–ด ๋ฐฉ๋ฒ•

  1. `public/` ํด๋” ํ™œ์šฉ
    `public/images/image.png`์— ๋„ฃ์œผ๋ฉด ๋นŒ๋“œ ์‹œ ๊ทธ๋Œ€๋กœ `images/image.png` ๊ฒฝ๋กœ๋กœ ์œ ์ง€๋จ
  2. ์ธ๋ผ์ธ ์ฒ˜๋ฆฌ ๋ฐฉ์ง€ (`base64`๋กœ ๋“ค์–ด๊ฐ€์ง€ ์•Š๊ฒŒ) or ์ œํ•œ ํฌ๊ธฐ ๋ณ€๊ฒฝ
    ์•„๋ž˜์ฒ˜๋Ÿผ ์ฒ˜๋ฆฌํ•˜๋ฉด ๋ชจ๋“  ์ด๋ฏธ์ง€ or ์ œํ•œ ์ดˆ๊ณผ ์ด๋ฏธ์ง€  ๋ชจ๋‘ `/img/` ํด๋”์— ํŒŒ์ผ๋กœ ์ƒ์„ฑ๋จ
// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.module
      .rule('images')
      	// ์ œํ•œ ํฌ๊ธฐ ๋ณ€๊ฒฝ ์‹œ
      	.set('parser', {
          dataUrlCondition: {
            maxSize: 4 * 1024 // 4KB
          }
        })
      .use('url-loader')
      .tap(options => {
        options.limit = 0; // ์šฉ๋Ÿ‰๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋ชจ๋‘ ํŒŒ์ผ๋กœ ์ฒ˜๋ฆฌ
        return options;
      });
  }
};

๐Ÿค” Stream์œผ๋กœ ์ˆ˜๋™ ํŽ˜์ด์ง•?

์‹œ์ž‘ํ•˜๊ธฐ์— ์•ž์„œ, ๋ณดํ†ต JPA์—์„œ ํŽ˜์ด์ง•์ด ํ•„์š”ํ•˜๋ฉด `Page<T>` ํƒ€์ž…์œผ๋กœ ๋ฆฌํ„ด๋ฐ›๋Š” ๊ฒŒ ๊ฐ€์žฅ ๊น”๋”ํ•˜๋‹ค.

Page<Entity> res = repository.findByStatus("PENDING", pageable);

์ด ๋ฐฉ์‹์ด ๊ฐ€์žฅ ์ด์ƒ์ ์ธ ํ˜•ํƒœ๊ณ , ์„ฑ๋Šฅ์ƒ์œผ๋กœ๋„ ํšจ์œจ์ ์ด๋‹ค.

ํ•˜์ง€๋งŒ ์˜ค๋Š˜ ๋‚˜์˜ ์ƒํ™ฉ์—์„œ๋Š” stream์œผ๋กœ ํ•„ํ„ฐ๋ง ํ›„, `PageImpl`๋กœ ์ˆ˜๋™ ํŽ˜์ด์ง•์„ ํ•˜๋Š”๊ฒŒ ๋‚˜์„ ๋•Œ๊ฐ€ ์กด์žฌํ•œ๋‹ค.

  1. ์‚ฌ์šฉ์ž์˜ ํ•„ํ„ฐ ์กฐ๊ฑด์ด ๋งŽ๊ณ 
  2. ๊ทธ ์ค‘ ์ผ๋ถ€๋Š” null์ผ ์ˆ˜๋„ ์žˆ๊ณ 
  3. ์ผ๋ถ€๋Š” ํฌํ•จ ๊ฒ€์ƒ‰์ด๊ณ 
  4. ๋‚ ์งœ ๋ฒ”์œ„ ์กฐ๊ฑด๊นŒ์ง€ ์กด์žฌํ–ˆ๋‹ค.

์ด ์กฐ๊ฑด๋“ค์„ ๋ชจ๋‘ JPQL์ด๋‚˜ @Query๋กœ ์งœ๋Š” ๊ฑด ๋ณต์žก๋„๋„ ๋†’๊ณ  ์œ ์ง€๋ณด์ˆ˜์˜ ์ง€์˜ฅ์ด ๋  ๊ฒƒ ๊ฐ™์•˜๋‹ค.

๊ทธ๋ž˜์„œ `findAll()`์ด๋‚˜ ๊ฐ„๋‹จํ•œ ์กฐ๊ฑด์„ ๊ฑธ์–ด์„œ repository์—์„œ ์กฐํšŒ ํ›„ `stream().filter(...)`๋กœ ํ•„ํ„ฐ๋ง์„ ํ–ˆ๋‹ค.

๋ฌผ๋ก  ๋ฐ์ดํ„ฐ ์ˆ˜๊ฐ€ ์ ์„๋•Œ๋งŒ ํ•ด๋‹นํ•œ๋‹ค.

 

๐Ÿ“Œ ์ „์ฒด ์ฝ”๋“œ ์˜ˆ์‹œ

// 1. ๋ฐ์ดํ„ฐ ์กฐํšŒ
List<Entity> entityList = repository.findById(id);

// 2. ์กฐ๊ฑด ํ•„ํ„ฐ๋ง + ์ •๋ ฌ
List<Entity> filteredEntityList = entityList.stream()
	.filter(entity -> entity.getStatus().equals(dto.getStatus()))
    .filter(entity -> entity.getCont().getContNm().toLowerCase().contains(dto.getSearchNm().toLowerCase())
    .filter(entity -> dto.getTypeCodeList().contains(entity.getTypeCode())
    .sorted(Comparator.comparing(Entity::getCrtTime).reversed())
    .toList();

// 3. Pageable ๊ฐ์ฒด ์ƒ์„ฑ (pageable์€ F/E์—์„œ ๋„˜์–ด์˜ด)
Pageable page = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize());

// 4. ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ
int start = (int) page.getOffset();
int end = Math.min((start + page.getPageSize()), filteredEntityList.size());

Page<Entity> entityPage = new PageImpl<>(
	filteredEntityList.subList(start, end),
    page,
    filteredEntityList.size()
);

 

๐Ÿ’ฅ .filter(...) ์กฐ๊ฑด์ด ๋งŽ์•„์งˆ ๊ฒฝ์šฐ ๋ฆฌํŒฉํ† ๋ง

Jave Stream์˜ `filter()`์€ ์กฐ๊ฑด์ด ๋งŽ์•„์งˆ์ˆ˜๋ก ๊ฐ€๋…์„ฑ์ด ๋–จ์–ด์ง„๋‹ค.

์ด๋ฅผ ์œ„ํ•ด ์กฐ๊ฑด์„ ๋ณ„๋„ ๋ฉ”์„œ๋“œ๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

List<Entity> filteredEntityList = entityList.stream()
    .filter(entity -> filterSearchNm(entity, dto.getSearchNm()))
    .filter(entity -> filterReqTypeCode(entity, dto.getReqTypeCodeList()))
    .sorted(Comparator.comparing(Entity::getCrtTime).reversed())
    .toList();

// ์กฐ๊ฑด ๋ถ„๋ฆฌ ๋ฉ”์„œ๋“œ
private boolean filterSearchNm(Entity entity, String searchNm) {
	if (StringUtils.isEmpty(searchNm)) return true;
    
    String keyword = searchNm.toLowerCase();
    return entity.getCont().getContNm().toLowerCase().contains(keyword) ||
    	   entity.getComp().getCompNm().toLowerCase().contains(keyword);
}

private boolean filterReqTypeCode(Entity entity, List<String> codeList) {
	return Collectionutils.isEmpty(codeList) ||
    	   codeList.contains(entity.getReqTypeCode());
}

๐Ÿงธ Pageable

Spring Data JPA์—์„œ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹œ ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ๋ฅผ ์ง€์›ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ด๋‹ค.

`PageRequest.of(ํŽ˜์ด์ง€๋ฒˆํ˜ธ, ํŽ˜์ด์ง€ํฌ๊ธฐ)`๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 

โฌ‡๏ธ ์ •๋ ฌ๊นŒ์ง€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ

Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "createdDate"));
  • `0`: ์ฒซ ํŽ˜์ด์ง€ (0๋ถ€ํ„ฐ ์‹œ์ž‘)
  • `10`: ํ•œ ํŽ˜์ด์ง€์— 10๊ฐœ์”ฉ
  • `"createDate"`: ์ •๋ ฌ ๊ธฐ์ค€ ์ปฌ๋Ÿผ
  • `DESC`: ๋‚ด๋ฆผ์ฐจ์ˆœ

 

๐ŸŽก Repository ๋ฉ”์„œ๋“œ์—์„œ ์‚ฌ์šฉ

Page<Entity> findAll(Pageable pageable);

// Specification๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉ
Page<Entity> findAll(Specification<Entity> spec, Pageable pageable);

 

๐ŸŽก Controller์—์„œ ์‚ฌ์šฉ

@GetMapping("/list")
public Page<ResponseDto> getList(@RequestParam int page,
                                 @RequestParam int size) {
    Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdDate"));
    return repository.findAll(spec, pageable).map(entity -> toDto(entity));
}

 

๐Ÿช„ Pageable(Sort ํฌํ•จ) ์‚ฌ์šฉ ์‹œ ์ฝ˜์†” ์ถœ๋ ฅ ์ฟผ๋ฆฌ

    order by
        mer1_0.[CREATE_DATE] desc 
    offset
        ? rows 
    fetch
        first ? rows only

'Backend > Spring' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[QueryDSL] QueryDSL ์ ์šฉ ๊ธฐ์ค€  (0) 2026.01.27
[Gradle] implementation, api ์„ ์–ธ  (0) 2026.01.22
[Spring Data JPA] Stream Filter ํ›„ Page๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ  (0) 2025.05.23

๋ฌธ์ œ

 

Brazilian E-Commerce Public Dataset by Olist ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ๋ธŒ๋ผ์งˆ์˜ ์ด์ปค๋จธ์Šค ์›น์‚ฌ์ดํŠธ์ธ Olist Store์˜ ํŒ๋งค ๋ฐ์ดํ„ฐ ์ž…๋‹ˆ๋‹ค. ๊ทธ ์ค‘ `olist_orders_dataset` ํ…Œ์ด๋ธ”์—๋Š” ์ฃผ๋ฌธ ID, ๊ณ ๊ฐ ID, ์ฃผ๋ฌธ ์ƒํƒœ, ๊ตฌ๋งค ์‹œ๊ฐ ๋“ฑ ์ฃผ๋ฌธ ๋‚ด์—ญ ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด์žˆ์Šต๋‹ˆ๋‹ค. Olist์˜ ์ฃผ๋ฌธ๋ถ€ํ„ฐ ๋ฐฐ์†ก๊นŒ์ง€ ํ”„๋กœ์„ธ์Šค๋Š” ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ํ†ตํ•ด ์ด๋ฃจ์–ด์ง€๊ณ , ๊ฐ ๋‹จ๊ณ„๋งˆ๋‹ค ์‹œ๊ฐ์„ ๊ธฐ๋กํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

  1. ๊ณ ๊ฐ์˜ ๊ตฌ๋งค
    • `order_purchase_timestamp` ์ปฌ๋Ÿผ์— ๊ตฌ๋งค ์‹œ์ ์ด ์ €์žฅ๋จ
  2. ํŒ๋งค์ž๊ฐ€ ์ฃผ๋ฌธ์„ ์Šน์ธ
    • `order_approved_at` ์ปฌ๋Ÿผ์— ์Šน์ธ ์‹œ์ ์ด ์ €์žฅ๋จ
  3. ํƒ๋ฐฐ์‚ฌ์— ๋„์ฐฉํ•˜์—ฌ ๋ฐฐ์†ก ์‹œ์ž‘
    • `order_delivered_carrier_date` ์ปฌ๋Ÿผ์— ๋ฐฐ์†ก ์‹œ์ž‘ ์‹œ์ ์ด ์ €์žฅ๋จ
  4. ๋ฐฐ์†ก ์™„๋ฃŒ
    • `order_delivered_customer_date` ์ปฌ๋Ÿผ์— ๋ฐฐ์†ก ์™„๋ฃŒ ์‹œ์ ์ด ์ €์žฅ๋จ

์ถ”๊ฐ€๋กœ `order_estimated_delivery_date` ์ปฌ๋Ÿผ์—๋Š” ์ฃผ๋ฌธ ์‹œ์ ์— ๊ณ„์‚ฐํ•œ ๋ฐฐ์†ก ์˜ˆ์ • ์‹œ๊ฐ์ด ์ €์žฅ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ปฌ๋Ÿผ์— ๊ฐ’์ด ‘2017-02-24 00:00:00’๋กœ ๋“ค์–ด์žˆ๋Š” ๊ฒฝ์šฐ, ๋ฐฐ์†ก์„ 2017๋…„ 2์›” 24์ผ ์ž์ •๊นŒ์ง€ ์™„๋ฃŒํ•˜๊ฒ ๋‹ค๋Š” ์˜๋ฏธ๋ฅผ ๋‹ด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 

2017๋…„ 1์›” ํ•œ ๋‹ฌ ๋™์•ˆ ๋ฐœ์ƒํ•œ ์ฃผ๋ฌธ์˜ ๋ฐฐ์†ก ์˜ˆ์ธก์ด ์ •ํ™•ํ–ˆ๋Š”์ง€ ๋ถ„์„์„ ํ•˜๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ๊ณ ๊ฐ์˜ ๊ตฌ๋งค ์ผ์ž๋ณ„๋กœ ๋ฐฐ์†ก ์˜ˆ์ • ์‹œ๊ฐ ์•ˆ์— ๊ณ ๊ฐ์—๊ฒŒ ๋„์ฐฉํ•œ ์ฃผ๋ฌธ๊ณผ, ๋ฐฐ์†ก ์˜ˆ์ • ์‹œ๊ฐ์ด ์ง€๋‚˜์„œ ๊ณ ๊ฐ์—๊ฒŒ ๋„์ฐฉํ•œ ์ฃผ๋ฌธ์„ ๊ฐ๊ฐ ์ง‘๊ณ„ํ•˜๋Š” ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”. ๋ฐฐ์†ก ์™„๋ฃŒ ๋˜๋Š” ๋ฐฐ์†ก ์˜ˆ์ • ์‹œ๊ฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ๋Š” ๊ณ„์‚ฐ์—์„œ ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค. ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋Š” ๊ตฌ๋งค ๋‚ ์งœ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ๋˜์–ด์•ผ ํ•˜๊ณ , ์•„๋ž˜ ์ปฌ๋Ÿผ์„ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  • `purchase_date` - ๊ตฌ๋งค ๋‚ ์งœ (์˜ˆ: 2017-01-01)
  • `success` - ๋ฐฐ์†ก ์˜ˆ์ • ์‹œ๊ฐ ์•ˆ์— ๊ณ ๊ฐ์—๊ฒŒ ๋„์ฐฉํ•œ ์ฃผ๋ฌธ ์ˆ˜
  • `fail` - ๋ฐฐ์†ก ์˜ˆ์ • ์‹œ๊ฐ์ด ์ง€๋‚˜ ๊ณ ๊ฐ์—๊ฒŒ ๋„์ฐฉํ•œ ์ฃผ๋ฌธ ์ˆ˜

๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ ์˜ˆ์‹œ

purchase_date success fail
2017-01-06 4 0
2017-01-07 3 1
  • 2017๋…„ 1์›” 6์ผ ๊ตฌ๋งค๋œ ์ฃผ๋ฌธ์€ ์ด 4๊ฑด์ด๊ณ  ๋ชจ๋‘ ๋ฐฐ์†ก ์˜ˆ์ •์ผ ์•ˆ์— ๋ฐฐ์†ก ์™„๋ฃŒ๋จ
  • 2017๋…„ 1์›” 7์ผ ๊ตฌ๋งค๋œ ์ฃผ๋ฌธ์€ ์ด 4๊ฑด์ด๊ณ  ๊ทธ ์ค‘ 3๊ฑด์€ ๋ฐฐ์†ก ์˜ˆ์ •์ผ ์•ˆ์— ๋ฐฐ์†ก ์™„๋ฃŒ๋˜์—ˆ์œผ๋‚˜, 1๊ฑด์€ ๋ฐฐ์†ก ์˜ˆ์ •์ผ ์ดํ›„์— ๋ฐฐ์†ก ์™„๋ฃŒ๋จ

 

์ œ์ถœ
SELECT DATE(order_purchase_timestamp) AS 'purchase_date'
     , SUM(IIF(order_estimated_delivery_date >= order_delivered_customer_date, 1, 0)) AS 'success'
     , SUM(IIF(order_estimated_delivery_date < order_delivered_customer_date, 1, 0)) AS 'fail'
FROM olist_orders_dataset
WHERE order_delivered_customer_date IS NOT NULL
  AND order_estimated_delivery_date IS NOT NULL
  AND purchase_date LIKE '2017-01%'
GROUP BY purchase_date
ORDER BY purchase_date
;

+ Recent posts