hbeeni / online-store

fake online store api
0 stars 0 forks source link

๐Ÿ›’ ์˜จ๋ผ์ธ ์‡ผํ•‘๋ชฐ API (online store API)

์‡ผํ•‘๋ชฐ ์›น ์‚ฌ์ดํŠธ๋ฅผ ์œ„ํ•œ REST API


1. ์ œ์ž‘ ๊ธฐ๊ฐ„ & ์ฐธ์—ฌ ์ธ์›


2. ์‚ฌ์šฉ ๊ธฐ์ˆ 


3. ERD


4. ๊ธฐ๋Šฅ

์‚ฌ์šฉ์ž๋Š” Swagger ๋ฌธ์„œ๋ฅผ ํ†ตํ•ด ์‡ผํ•‘๋ชฐ์˜ ๊ธฐ๋Šฅ์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


4.1. ์ „์ฒด ํ๋ฆ„




4.2. ์ „์ฒด ๊ธฐ๋Šฅ


4.3. ํ•ต์‹ฌ ๊ธฐ๋Šฅ

๋Œ€๋ถ€๋ถ„์€ ๊ธฐ๋ณธ์ ์ธ ๋กœ์ง์ด๋ฏ€๋กœ, ์„ค๋ช…์ด ํ•„์š”ํ•œ ๊ธฐ๋Šฅ๋งŒ์„ ๊ธฐ์ˆ ํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

์ƒํ’ˆ ๋“ฑ๋ก

**Controller** - **Multipart ํƒ€์ž… ์š”์ฒญ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/controller/admin/AdminProductApiController.java#L55) - ์ƒํ’ˆ ์ •๋ณด์™€ ์ƒํ’ˆ ์ด๋ฏธ์ง€๋ฅผ `Multipart` ํƒ€์ž…์œผ๋กœ ์š”์ฒญ๋ฐ›์Šต๋‹ˆ๋‹ค.
- **์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ €์žฅ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/file/ImageStore.java#L59) - `UUID`๋ฅผ ์‚ฌ์šฉํ•ด ์ด๋ฏธ์ง€์˜ ์ด๋ฆ„์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. - ์ƒ์„ฑํ•œ ์ด๋ฆ„์œผ๋กœ ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•œ ํ›„ ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
**Service & Repository** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/service/admin/AdminProductService.java#L44) - **ํŒ๋งค ์ƒํƒœ ์ฒดํฌ** - ํŒ๋งค ์ƒํƒœ(`saleStatus`)๋ฅผ ์ž…๋ ฅํ•˜์ง€ ์•Š์œผ๋ฉด ์ž๋™์œผ๋กœ ํŒ๋งค ๋Œ€๊ธฐ(`WAIT`) ์ƒํƒœ๋กœ ์ €์žฅ๋˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
- **DB ์ €์žฅ** - ์ด๋ฏธ์ง€ ์ €์žฅ, ํŒ๋งค ์ƒํƒœ ์ฒดํฌ๊ฐ€ ๋๋‚œ ์ƒํ’ˆ์€ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑํ•œ ํ›„ DB์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. - DB์— ์ €์žฅํ•œ ํ›„ ์ €์žฅ๋œ ์ƒํ’ˆ ์—”ํ‹ฐํ‹ฐ์˜ ID๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
์ƒํ’ˆ ์ฃผ๋ฌธ

> ์ƒํ’ˆ ์ฃผ๋ฌธ์€ ๋‘ ๊ฐ€์ง€ ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. > 1. ์ƒํ’ˆ์„ ๋ฐ”๋กœ ์ฃผ๋ฌธํ•˜๋Š” ๊ฒฝ์šฐ (= ํ•œ ์ƒํ’ˆ์„ ์ฃผ๋ฌธํ•˜๋Š” ๊ฒฝ์šฐ) > 2. ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋‹ด๊ธด ์ƒํ’ˆ์„ ์„ ํƒํ•ด์„œ ํ•œ ์ƒํ’ˆ ๋˜๋Š” ์—ฌ๋Ÿฌ ์ƒํ’ˆ์„ ์ฃผ๋ฌธํ•˜๋Š” ๊ฒฝ์šฐ > > ์ƒํ’ˆ์„ ๋ฐ”๋กœ ์ฃผ๋ฌธํ•˜๋Š” ๊ฒฝ์šฐ๋กœ ์„ค๋ช…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
> ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋‹ด๊ธด ์ƒํ’ˆ์„ ์ฃผ๋ฌธํ•˜๋Š” ๊ฒฝ์šฐ๋Š” ์ฝ”๋“œ ๋งํฌ๋ฅผ ๋‚จ๊ธฐ๊ฒ ์Šต๋‹ˆ๋‹ค.
**Controller** > [์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ƒํ’ˆ ์ฃผ๋ฌธ ์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/controller/CartApiController.java#L53)
- **์š”์ฒญ ์ฒ˜๋ฆฌ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/controller/OrderApiController.java#L52) - ๋กœ๊ทธ์ธํ•œ ์œ ์ €์™€ ์ƒํ’ˆ ์ฃผ๋ฌธ์— ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ์š”์ฒญ์œผ๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
**Service & Repository** > [์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ƒํ’ˆ ์ฃผ๋ฌธ ์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/service/CartProductService.java#L68)
- **ํŒ๋งค ์ค‘์ธ์ง€ ๊ฒ€์ฆ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/service/OrderService.java#L54) - ์ฃผ๋ฌธํ•œ ์ƒํ’ˆ์ด ํ˜„์žฌ ํŒ๋งค ์ค‘์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. - ์ƒํ’ˆ์ด ํ˜„์žฌ ํŒ๋งค ์ค‘์ด๊ณ , ์žฌ๊ณ ๋„ ์ถฉ๋ถ„ํ•˜๋‹ค๋ฉด ์ฃผ๋ฌธ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
- **์ƒํ’ˆ ์žฌ๊ณ  ๊ฐ์†Œ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/domain/OrderProduct.java#L50) - ์ฃผ๋ฌธ ์ƒํ’ˆ ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ ์‹œ ์ƒํ’ˆ ์žฌ๊ณ ๋ฅผ ๊ฐ์†Œ์‹œํ‚ต๋‹ˆ๋‹ค. - ์ƒํ’ˆ์˜ ์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•˜๋ฉด ์ฃผ๋ฌธํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - ์ƒํ’ˆ์˜ ์žฌ๊ณ ๊ฐ€ 0์ด ๋˜๋ฉด ํŒ๋งค ์ƒํƒœ๋ฅผ (`OUT_OF_STOCK`)์œผ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
- **DB ์ €์žฅ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/service/OrderService.java#L54) - ์ƒ์„ฑ๋œ ์ฃผ๋ฌธ & ์ฃผ๋ฌธ ์ƒํ’ˆ ์—”ํ‹ฐํ‹ฐ๋ฅผ DB์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. - ์ €์žฅ๋œ ์ฃผ๋ฌธ ์—”ํ‹ฐํ‹ฐ์˜ ID๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
์ƒํ’ˆ ํŽ˜์ด์ง• ์กฐํšŒ (๊ฒ€์ƒ‰)
- ๊ด€๋ฆฌ์ž๋Š” **์นดํ…Œ๊ณ ๋ฆฌ ID, ์ƒํ’ˆ๋ช…, ์ƒํ’ˆ ์ƒํƒœ**๋กœ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - ์ผ๋ฐ˜ ํšŒ์›์€ **์ƒํ’ˆ๋ช…**์œผ๋กœ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - ๊ด€๋ฆฌ์ž ๊ธฐ์ค€์œผ๋กœ ์„ค๋ช…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
**Controller** - **์š”์ฒญ ์ฒ˜๋ฆฌ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/controller/admin/AdminProductApiController.java#L42) - ์ƒํ’ˆ ๊ฒ€์ƒ‰ ์กฐ๊ฑด๊ณผ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ •๋ณด๋ฅผ ์š”์ฒญ์œผ๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
**Service** - **Repository ํ˜ธ์ถœ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/service/admin/AdminProductService.java#L34) - ๋‹จ์ˆœํžˆ Repository๋ฅผ ํ˜ธ์ถœํ•˜๊ธฐ๋งŒ ํ•ฉ๋‹ˆ๋‹ค.
**Repository** > ์กฐ๊ฑด๋ฌธ ์žฌํ™œ์šฉ ๋ฐ ๊ฐ€๋…์„ฑ์„ ์œ„ํ•ด QueryDSL์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
- **Projection** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/repository/querydsl/product/ProductRepositoryCustomImpl.java#L82) - Projection์„ ์‚ฌ์šฉํ•ด DTO์— ๊ฒฐ๊ณผ๋ฅผ ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค. - ์ƒํ’ˆ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ๋Š” `imageName` ์•ž์— `imagePath`๋ฅผ ๋ถ™์ž…๋‹ˆ๋‹ค.
- **์ƒํ’ˆ ๊ฒ€์ƒ‰** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/repository/querydsl/product/ProductRepositoryCustomImpl.java#L43) - QueryDSL์„ ์‚ฌ์šฉํ•˜์—ฌ ์ƒํ’ˆ์„ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค. - `Pageable`์˜ `Sort`๋ฅผ ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— `OrderSpecifier`๋ฅผ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค. (`OrderSpecifier`์— ๊ด€ํ•œ ๋‚ด์šฉ์€ **5. ํ•ต์‹ฌ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…**์— ์žˆ์Šต๋‹ˆ๋‹ค.)
๋ฐฐ์†ก์ง€ ์ถ”๊ฐ€/์ˆ˜์ •/์‚ญ์ œ
- ๋ฐฐ์†ก์ง€์—” **๊ธฐ๋ณธ ๋ฐฐ์†ก์ง€** ๊ธฐ๋Šฅ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. - ๊ธฐ๋ณธ ๋ฐฐ์†ก์ง€๊ฐ€ ์•„๋‹Œ ๋ฐฐ์†ก์ง€๋ฅผ **์ผ๋ฐ˜ ๋ฐฐ์†ก์ง€**๋ผ๊ณ  ์นญํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
๋ฐฐ์†ก์ง€ ์ถ”๊ฐ€

**Controller** - **์š”์ฒญ ์ฒ˜๋ฆฌ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/controller/AddressApiController.java#L48) - ๋กœ๊ทธ์ธํ•œ ์œ ์ €์™€ ์ถ”๊ฐ€ํ•  ๋ฐฐ์†ก์ง€ ์ •๋ณด๋ฅผ ์š”์ฒญ์œผ๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
**Service & Repository** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/service/AddressService.java#L42) - **๊ธฐ๋ณธ ๋ฐฐ์†ก์ง€ ์ฒ˜๋ฆฌ** - ๊ธฐ๋ณธ ๋ฐฐ์†ก์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒฝ์šฐ: ๊ธฐ์กด์˜ ๊ธฐ๋ณธ ๋ฐฐ์†ก์ง€๋Š” ์ผ๋ฐ˜ ๋ฐฐ์†ก์ง€๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค. - ์ผ๋ฐ˜ ๋ฐฐ์†ก์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒฝ์šฐ: ๊ธฐ๋ณธ ๋ฐฐ์†ก์ง€๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ๊ธฐ๋ณธ ๋ฐฐ์†ก์ง€๋กœ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
- **DB ์ €์žฅ** - ๊ธฐ๋ณธ ๋ฐฐ์†ก์ง€ ์„ค์ •์ด ๋๋‚œ ๋ฐฐ์†ก์ง€๋Š” ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ ํ›„ DB์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. - ์ €์žฅ๋œ ๋ฐฐ์†ก์ง€ ์—”ํ‹ฐํ‹ฐ์˜ ID๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
๋ฐฐ์†ก์ง€ ์ˆ˜์ •

**Controller** - **์š”์ฒญ ์ฒ˜๋ฆฌ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/controller/AddressApiController.java#L57) - ๋กœ๊ทธ์ธํ•œ ์œ ์ €, ์ˆ˜์ •ํ•  ๋ฐฐ์†ก์ง€ ID, ์ˆ˜์ •ํ•  ๋ฐฐ์†ก์ง€ ์ •๋ณด๋ฅผ ์š”์ฒญ์œผ๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
**Service** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/service/AddressService.java#L58) - **๊ธฐ๋ณธ ๋ฐฐ์†ก์ง€ ์ˆ˜์ •** - ํ˜„์žฌ ๋ฐฐ์†ก์ง€๋ฅผ ๊ธฐ๋ณธ ๋ฐฐ์†ก์ง€๋กœ ์ˆ˜์ •ํ•œ๋‹ค๋ฉด ๊ธฐ์กด์˜ ๊ธฐ๋ณธ ๋ฐฐ์†ก์ง€๋Š” ์ผ๋ฐ˜ ๋ฐฐ์†ก์ง€๋กœ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.
- **Dirty Checking** - ์ˆ˜์ •ํ•œ ๋ฐฐ์†ก์ง€๋ฅผ ์ง์ ‘ save ํ•˜์ง€ ์•Š๊ณ , dirty checking์„ ํ†ตํ•ด ์ž๋™์œผ๋กœ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค.
๋ฐฐ์†ก์ง€ ์‚ญ์ œ

**Controller** - **์š”์ฒญ ์ฒ˜๋ฆฌ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/controller/AddressApiController.java#L67) - ๋กœ๊ทธ์ธํ•œ ์œ ์ €, ์‚ญ์ œํ•  ๋ฐฐ์†ก์ง€ ID๋ฅผ ์š”์ฒญ์œผ๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
**Service & Repository** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/service/AddressService.java#L71) - **๊ธฐ๋ณธ ๋ฐฐ์†ก์ง€ ๊ฒ€์ฆ** - ๊ธฐ๋ณธ ๋ฐฐ์†ก์ง€๋Š” ์‚ญ์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
- **DB ์‚ญ์ œ** - ์ผ๋ฐ˜ ๋ฐฐ์†ก์ง€๋ผ๋ฉด DB์—์„œ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.
---
๋ฐฐ์†ก ์ƒํƒœ ๋ณ€๊ฒฝ
๋ฐฐ์†ก ์ƒํƒœ ๋ณ€๊ฒฝ ์ˆœ์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.
๋กœ์ง์˜ ํ๋ฆ„์€ ๋™์ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— **์ƒํ’ˆ ์ค€๋น„ ์ค‘์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒฝ์šฐ**๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์„ค๋ช…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
**Controller** - **์š”์ฒญ ์ฒ˜๋ฆฌ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/controller/admin/AdminOrderApiController.java#L46) - ๋ฐฐ์†ก ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋ ค๋Š” ์ฃผ๋ฌธ์˜ ID๋ฅผ ์š”์ฒญ์œผ๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
**Service** - **์ƒํ’ˆ ํŒ๋งค๋Ÿ‰ ์ฆ๊ฐ€** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/domain/Order.java#L104) - ์ƒํ’ˆ ์ค€๋น„ ์ค‘ ์ฒ˜๋ฆฌ ์‹œ ์ƒํ’ˆ์˜ ํŒ๋งค๋Ÿ‰์„ ์ฆ๊ฐ€์‹œํ‚ต๋‹ˆ๋‹ค. - _cf) ๋ฐฐ์†ก ์™„๋ฃŒ ์ฒ˜๋ฆฌ ์‹œ์—๋Š” ๋ฐฐ์†ก ์—”ํ‹ฐํ‹ฐ(`Delivery`)์˜ `deliveredAt`์— ํ˜„์žฌ ์‹œ๊ฐ„์„ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/domain/Delivery.java#L71)_
- **์ƒํ’ˆ ์ค€๋น„ ์ฒ˜๋ฆฌ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/service/admin/AdminOrderService.java#L39) - ๋ฐฐ์†ก ์ƒํƒœ๊ฐ€ ๊ฒฐ์ œ ์™„๋ฃŒ(`ACCEPT`)์ธ ๊ฒฝ์šฐ์—๋งŒ ์ƒํ’ˆ ์ค€๋น„ ์ค‘์œผ๋กœ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - ์ˆ˜์ •ํ•œ ์ฃผ๋ฌธ์„ ์ง์ ‘ save ํ•˜์ง€ ์•Š๊ณ , dirty checking์„ ํ†ตํ•ด ์ž๋™์œผ๋กœ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค.
์ฃผ๋ฌธ ์ทจ์†Œ
- ๊ด€๋ฆฌ์ž, ์ผ๋ฐ˜ ํšŒ์› ๋ชจ๋‘ ์ฃผ๋ฌธ์„ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - ์ผ๋ฐ˜ ํšŒ์› ๊ธฐ์ค€์œผ๋กœ ์„ค๋ช…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
**Controller** - **์š”์ฒญ ์ฒ˜๋ฆฌ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/controller/OrderApiController.java#L61) - ๋กœ๊ทธ์ธํ•œ ์œ ์ €์™€ ์ทจ์†Œํ•˜๋ ค๋Š” ์ฃผ๋ฌธ์˜ ID๋ฅผ ์š”์ฒญ์œผ๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
**Service** - **์ƒํ’ˆ ์žฌ๊ณ  ์ฆ๊ฐ€** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/domain/Order.java#L96) - ์ฃผ๋ฌธ ์ทจ์†Œ ์‹œ ํ•ด๋‹น ์ฃผ๋ฌธ์˜ ์ƒํ’ˆ ์žฌ๊ณ ๋ฅผ ์ฆ๊ฐ€์‹œํ‚ต๋‹ˆ๋‹ค. - ๋งŒ์•ฝ ํ•ด๋‹น ์ƒํ’ˆ์ด ํ’ˆ์ ˆ ์ƒํƒœ์˜€๋‹ค๋ฉด ํŒ๋งค ์ค‘์œผ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
- **์ฃผ๋ฌธ ์ทจ์†Œ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/service/OrderService.java#L65) - ์ฃผ๋ฌธ ์ƒํƒœ๊ฐ€ `ORDER`์ธ ๊ฒฝ์šฐ์—๋งŒ ์ฃผ๋ฌธ์„ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - ์ทจ์†Œํ•œ ์ฃผ๋ฌธ์€ DB์—์„œ ์‚ญ์ œํ•˜์ง€ ์•Š๊ณ , ์ฃผ๋ฌธ ์ƒํƒœ(`OrderStatus`)๋ฅผ ์ทจ์†Œ(`CANCEL`)๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค. - ์ˆ˜์ •ํ•œ ์ฃผ๋ฌธ์„ ์ง์ ‘ save ํ•˜์ง€ ์•Š๊ณ , dirty checking์„ ํ†ตํ•ด ์ž๋™์œผ๋กœ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค.
API ์‘๋‹ต ํ˜•์‹ ํ†ต์ผ
- ํ˜„์žฌ ์„œ๋น„์Šค์—์„œ๋Š” API ๊ณตํ†ต ์‘๋‹ต ํฌ๋งท์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ“Œ [์„ฑ๊ณต ์‘๋‹ต](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/response/ApiResponse.java#L16) ๐Ÿ“Œ [์—๋Ÿฌ ์‘๋‹ต](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/response/ApiErrorResponse.java#L14) - `Controller`์—์„œ๋Š” `ApiResponse`๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. - ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ `GlobalExceptionHandler`์—์„œ `ApiErrorResponse`๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/exceptionhandler/GlobalExceptionHandler.java#L24)
- ๊ทธ ์™ธ์˜ ์ƒํ™ฉ์—๋„ ๊ณตํ†ต ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด ์•„๋ž˜์™€ ๊ฐ™์ด ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. - **๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/exceptionhandler/security/CustomAuthenticationEntryPoint.java#L17)
- **๋กœ๊ทธ์ธ์€ ํ•˜์˜€์ง€๋งŒ ํ•ด๋‹น ๊ถŒํ•œ์œผ๋กœ๋Š” ์ ‘๊ทผํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/exceptionhandler/security/CustomAccessDeniedHandler.java#L17)
- **๋กœ๊ทธ์•„์›ƒ** ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/src/main/java/com/been/onlinestore/config/security/CustomLogoutSuccessHandler.java#L19) - Security ์„ค์ •์˜ `logoutSuccessHandler`์— ๋“ฑ๋กํ•˜์˜€์Šต๋‹ˆ๋‹ค. ```java http.logout(logout -> logout.logoutSuccessHandler(new CustomLogoutSuccessHandler())) ```


5. ํ•ต์‹ฌ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

5.1. QueryDSL ์ •๋ ฌ ๋ฌธ์ œ

์ฝ”๋“œ
```java private OrderSpecifier[] getOrderSpecifiers(Pageable pageable) { List orderSpecifiers = getOrderSpecifiers(pageable.getSort()); return orderSpecifiers.toArray(OrderSpecifier[]::new); } private List getOrderSpecifiers(Sort sort) { List orderSpecifiers = new ArrayList<>(); sort.stream().forEach(order -> { Order direction = order.isAscending() ? Order.ASC : Order.DESC; String property = order.getProperty(); PathBuilder pathBuilder = new PathBuilder<>(Product.class, "product"); orderSpecifiers.add(new OrderSpecifier(direction, pathBuilder.get(property))); } ); return orderSpecifiers; } ``` ```java @Override public Page searchProducts(ProductSearchCondition cond, Pageable pageable) { List content = queryFactory .select(getAdminProductResponseProjection()) .from(product) .leftJoin(product.category, category) .where( categoryIdEq(cond.categoryId()), productNameContains(cond.name()), saleStatusEq(cond.saleStatus()) ) .orderBy(getOrderSpecifiers(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); //... } ```


5.2. fetch join๊ณผ ํŽ˜์ด์ง•์„ ํ•จ๊ป˜ ์‚ฌ์šฉ ์‹œ count query ์ƒ์„ฑ ์˜ค๋ฅ˜ ๋ฌธ์ œ

๊ธฐ์กด ์ฝ”๋“œ
```java @Query("select p from Product p join fetch p.category where p.saleStatus = 'SALE'") Page findAllOnSale(Pageable pageable); ```

cf) fetch join์ด๋‚˜ ๋ณต์žกํ•œ ์ฟผ๋ฆฌ์˜ ๊ฒฝ์šฐ ๊ผญ countQuery๋ฅผ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

๊ฐœ์„ ๋œ ์ฝ”๋“œ
```java @Query(value = "select p from Product p join fetch p.category where p.saleStatus = 'SALE'", countQuery = "select count(p) from Product p where p.saleStatus = 'SALE'") Page findAllOnSale(Pageable pageable); ```


5.3. Spring REST Docs๋งŒ ์‚ฌ์šฉํ•  ์‹œ API Test๋ฅผ ํ•  ์ˆ˜ ์—†๋Š” ๋ฌธ์ œ


  1. Swagger UI ํŒŒ์ผ์„ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.
    • index.html ํŒŒ์ผ์˜ ๋‚ด๋ถ€ css, js ๊ฒฝ๋กœ๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.
    • swagger-initializer.js ํŒŒ์ผ์˜ SwaggerUIBundle ๊ฒฝ๋กœ๋Š” OpenAPI Specification(OAS) ํŒŒ์ผ ๊ฒฝ๋กœ๋กœ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.
  2. Spring REST Docs + restdocs-api-spec๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
  3. restdocs-api-spec์œผ๋กœ OAS ํŒŒ์ผ ์ƒ์„ฑ ํ›„ static ๋””๋ ‰ํ† ๋ฆฌ๋กœ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.
  4. Swagger UI ์ •์  ํŒŒ์ผ๋กœ ์ƒ์„ฑ๋œ OAS ํŒŒ์ผ์„ ์—ฝ๋‹ˆ๋‹ค.
์ฝ”๋“œ
์˜ˆ์‹œ) [ProductControllerTest](/src/test/java/com/been/onlinestore/controller/ProductApiControllerTest.java) ์ค‘ ์ƒํ’ˆ ์กฐํšŒ ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค.
์˜ˆ์‹œ์ฒ˜๋Ÿผ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค. ```java @DisplayName("[API][GET] ์ƒํ’ˆ ์กฐํšŒ") @Test void test_getProducts() throws Exception { //Given int pageNumber = 0; int pageSize = 20; String sortName = "createdAt"; CategoryProductResponse response = CategoryProductResponse.of( 1L, "์ฑ„์†Œ", "๊น๋Œ€ํŒŒ 500g", 4500, "์‹œ์›ํ•œ ๊ตญ๋ฌผ ๋ง›์˜ ๋น„๋ฐ€", SaleStatus.SALE, 3000, imagePath + "c1b2f2a2-f0b8-403a-b03b-351d1ee0bd05.jpg" ); Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Order.desc(sortName))); Page page = new PageImpl<>(List.of(response), pageable, 1); given(productService.findProductsOnSale(null, pageable)).willReturn(page); //When & Then mvc.perform( get("/api/products") .queryParam("page", String.valueOf(pageNumber)) .queryParam("size", String.valueOf(pageSize)) .queryParam("sort", sortName + ",desc") ) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.status").value("success")) .andExpect(jsonPath("$.data").isArray()) .andExpect(jsonPath("$.data[0].id").value(response.id())) .andExpect(jsonPath("$.data[0].name").value(response.name())) .andExpect(jsonPath("$.data[0].price").value(response.price())) .andExpect(jsonPath("$.page.number").value(page.getNumber())) .andExpect(jsonPath("$.page.size").value(page.getSize())) .andExpect(jsonPath("$.page.totalPages").value(page.getTotalPages())) .andExpect(jsonPath("$.page.totalElements").value(page.getTotalElements())); then(productService).should().findProductsOnSale(null, pageable); } ```
- `openapi3` task๋กœ OAS ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค. - OAS ํŒŒ์ผ์„ ์—ด๊ธฐ ์œ„ํ•ด `copyOpenApiYaml` task๋กœ ์ƒ์„ฑ๋œ OAS ํŒŒ์ผ์„ static ๋””๋ ‰ํ† ๋ฆฌ์— ๋ณต์‚ฌํ•˜์˜€์Šต๋‹ˆ๋‹ค. ```groovy openapi3 { server = 'http://onlinestoreapi.kro.kr' title = '์‡ผํ•‘๋ชฐ API' description = '์‡ผํ•‘๋ชฐ API ์ž…๋‹ˆ๋‹ค' version = '1.0.0' format = 'yaml' } tasks.register('copyOpenApiYaml', Copy) { dependsOn 'processResources' dependsOn 'openapi3' def dir = "src/main/resources/static/docs" new File("${dir}/openapi3.yaml").delete() from("${openapi3.outputDirectory}") into(dir) } bootJar { dependsOn 'copyOpenApiYaml' } ```
๋นŒ๋“œ ํ›„ ์„œ๋ฒ„๋ฅผ ๋„์šฐ๋ฉด API ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•œ Swagger API ๋ฌธ์„œ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.


6. ๊ทธ ์™ธ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

[ํ™ˆ] ์นดํ…Œ๊ณ ๋ฆฌ ์ „์ฒด ์กฐํšŒ API - ํŒ๋งคํ•˜๋Š” ์ƒํ’ˆ์ด ์—†๋Š” ์นดํ…Œ๊ณ ๋ฆฌ๊นŒ์ง€ ์กฐํšŒ๋˜๋Š” ๋ฌธ์ œ
- ํ™ˆ์—์„œ๋Š” ํŒ๋งคํ•˜๋Š” ์ƒํ’ˆ์ด ์žˆ๋Š” ์นดํ…Œ๊ณ ๋ฆฌ๋งŒ ์กฐํšŒ๋˜์–ด์•ผ ํ•จ - ํŒ๋งคํ•˜๋Š” ์ƒํ’ˆ์ด๋ž€? **ํŒ๋งค ์ค‘**์ด๊ฑฐ๋‚˜ **ํ’ˆ์ ˆ**์ธ ์ƒํ’ˆ - ํ•ด๊ฒฐ: `findAll()` ๋Œ€์‹  ์•„๋ž˜ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉ ```java @Query("select distinct c from Category c " + "join c.products p " + "where p.saleStatus = 'SALE' or p.saleStatus = 'OUT_OF_STOCK'") List findAllBySellingProducts(); ```
[ํ™ˆ] ์ƒํ’ˆ ์กฐํšŒ API - ํ’ˆ์ ˆ ์ƒํ’ˆ์€ ์กฐํšŒ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ
- ๋ฌธ์ œ: ํ™ˆ์—์„œ ์ƒํ’ˆ ์กฐํšŒ ์‹œ ํŒ๋งค ์ค‘์ด๊ฑฐ๋‚˜ ํ’ˆ์ ˆ์ธ ์ƒํ’ˆ์ด ์กฐํšŒ๋˜์–ด์•ผ ํ•˜๋Š”๋ฐ ํŒ๋งค ์ค‘์ธ ์ƒํ’ˆ๋งŒ ์กฐํšŒ๋จ - ํ•ด๊ฒฐ: `SaleStatus`๊ฐ€ `SALE`, `OUT_OF_STOCK`์ธ ์ƒํ’ˆ์„ ์กฐํšŒ `where p.saleStatus = 'SALE' or p.saleStatus = 'OUT_OF_STOCK'`
[์–ด๋“œ๋ฏผ] ์ƒํ’ˆ ๋“ฑ๋ก API - sale_status ์นผ๋Ÿผ์— null์ด ๋“ค์–ด๊ฐ€๋Š” ๋ฌธ์ œ
- ๋ฌธ์ œ: ์ƒํ’ˆ ๋“ฑ๋ก ์‹œ ํŒ๋งค ์ƒํƒœ๋ฅผ ์ž…๋ ฅํ•˜์ง€ ์•Š์œผ๋ฉด `sale_status` ์นผ๋Ÿผ์— ๊ธฐ๋ณธ๊ฐ’(`WAIT`)์ด ์•„๋‹Œ `null`์ด ๋“ค์–ด๊ฐ - ํ•ด๊ฒฐ: ์ƒํ’ˆ ๋“ฑ๋ก ์‹œ `SaleStatus`๊ฐ€ `null`์ผ ๊ฒฝ์šฐ `WAIT`์œผ๋กœ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑํ•จ ```java public Long addProduct(ProductServiceRequest.Create serviceRequest, String imageName) { Category category = categoryRepository.getReferenceById(serviceRequest.categoryId()); if (serviceRequest.saleStatus() == null) { //์ถ”๊ฐ€ return productRepository.save(serviceRequest.toEntity(category, SaleStatus.WAIT, imageName)).getId(); } return productRepository.save(serviceRequest.toEntity(category, imageName)).getId(); } ```
[์–ด๋“œ๋ฏผ] ์ฃผ๋ฌธ ํŽ˜์ด์ง• ์กฐํšŒ API - ์ฃผ๋ฌธ์ด ์ค‘๋ณต ๊ฒ€์ƒ‰๋˜๋Š” ๋ฌธ์ œ
- ๋ฌธ์ œ: count query ์‹คํ–‰ ์‹œ ์ฃผ๋ฌธ์ด ์•„๋‹Œ ์ฃผ๋ฌธ ์ƒํ’ˆ์˜ ๊ฐœ์ˆ˜๊ฐ€ ์ถœ๋ ฅ๋จ
`queryFactory.selectDistinct(order.count())` - ํ•ด๊ฒฐ: `countDistinct()` ์‚ฌ์šฉ ```java public Page findOrdersForAdmin(OrderSearchCondition cond, Pageable pageable) { List orders = findOrders(cond, pageable); JPAQuery countQuery = queryFactory .select(order.countDistinct()) // .from(order) .join(order.orderer, user) .join(order.deliveryRequest, deliveryRequest) .join(order.orderProducts, orderProduct) .join(orderProduct.product, product) .where( ordererIdEq(cond.ordererId()), productIdEq(cond.productId()), deliveryStatusEq(cond.deliveryStatus()), orderStatusEq(cond.orderStatus()) ); return PageableExecutionUtils.getPage(orders, pageable, countQuery::fetchOne); } ```
์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ƒํ’ˆ์„ ์ฟ ํ‚ค์— ๋‹ด์„ ๋•Œ ๋ฐœ์ƒํ•œ ๋ฌธ์ œ
- ๋ฌธ์ œ: ์ฟ ํ‚ค๋Š” ๋ธŒ๋ผ์šฐ์ €๋ณ„๋กœ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ์žฅ๋ฐ”๊ตฌ๋‹ˆ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์—†์—ˆ์Œ - ํ•ด๊ฒฐ: ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ƒํ’ˆ์„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ•จ ([ERD ์ฐธ๊ณ ](/document/online-store-erd.png)) - ํ–ฅํ›„ Redis์— ์ €์žฅํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Œ
DB์— ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ƒํ’ˆ์ด ์˜๊ตฌ์ ์œผ๋กœ ์ €์žฅ๋˜๋Š” ๋ฌธ์ œ
- ๋ฌธ์ œ - ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ƒํ’ˆ์€ ์ƒํ’ˆ์„ ์ฃผ๋ฌธํ•˜๋Š” ๊ฒฝ์šฐ์—๋งŒ DB์—์„œ ์‚ญ์ œ๋จ - ๋”ฐ๋ผ์„œ ์ƒํ’ˆ์„ ์ฃผ๋ฌธํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด DB์— ์˜๊ตฌ์ ์œผ๋กœ ์ €์žฅ๋˜๋Š” ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•จ - ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ƒํ’ˆ์ด ์˜๊ตฌ์ ์œผ๋กœ DB์— ์ €์žฅ๋˜๋Š” ๊ฑด DB ๋ฆฌ์†Œ์Šค ๋‚ญ๋น„๋ผ๊ณ  ์ƒ๊ฐ๋จ - ํ•ด๊ฒฐ - `Scheduler`๋ฅผ ์‚ฌ์šฉํ•ด ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ƒํ’ˆ์˜ `modifiedAt` ๊ธฐ์ค€ 30์ผ์ด ์ง€๋‚˜๋ฉด ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ƒํ’ˆ์„ ์‚ญ์ œํ•จ - ํ•ด๋‹น ์ž‘์—…์€ ๋งค์ผ ์ž์ •์— ์‹คํ–‰ ```java @Scheduled(cron = "0 0 0 * * *") public void cleanUpExpiredCartProducts() { LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30); List expiredCartProducts = cartProductRepository.findAllByModifiedAtBefore(thirtyDaysAgo); List expiredCartProductIds = expiredCartProducts.stream() .map(CartProduct::getId) .toList(); cartProductRepository.deleteAllByIdInBatch(expiredCartProductIds); } ```
์•„๋ฌด๋‚˜ ๊ด€๋ฆฌ์ž๋กœ ๊ฐ€์ž…ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฌธ์ œ
- ๋ฌธ์ œ: ํšŒ์›๊ฐ€์ž… ์‹œ ๊ถŒํ•œ์„ ์ž…๋ ฅํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ฌด๋‚˜ ๊ด€๋ฆฌ์ž ๊ถŒํ•œ์œผ๋กœ ๊ฐ€์ž…ํ•  ์ˆ˜ ์žˆ์Œ - ํ•ด๊ฒฐ - ์ตœ์ดˆ ํ•œ ๋ช…์˜ ๊ด€๋ฆฌ์ž ํšŒ์›๋งŒ DB์— ์ง์ ‘ ์ €์žฅ - ํšŒ์› ๊ฐ€์ž… ์š”์ฒญ์œผ๋กœ๋Š” ์ผ๋ฐ˜ ํšŒ์› ๊ถŒํ•œ์œผ๋กœ๋งŒ ๊ฐ€์ž…ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ณ€๊ฒฝ
ํšŒ์›๊ฐ€์ž… ์‹œ ์ต๋ช… ์œ ์ €๊ฐ€ ๋„˜์–ด์˜ค๋Š” ๋ฌธ์ œ
- ํ•ด๊ฒฐ: ํšŒ์›๊ฐ€์ž… ์‹œ `AnonymousAuthenticationToken`์„ ํ•„ํ„ฐ๋งํ•จ ```java @Bean public AuditorAware auditorAware() { return () -> Optional.ofNullable(SecurityContextHolder.getContext()) .map(SecurityContext::getAuthentication) .filter(Authentication::isAuthenticated) .filter(authentication -> !(authentication instanceof AnonymousAuthenticationToken)) //์ถ”๊ฐ€ .map(Authentication::getPrincipal) .map(PrincipalDetails.class::cast) .map(PrincipalDetails::getUsername); } ```
HttpMediaTypeNotSupportedException: Content type 'application/octet-stream' not supported
- ๋ฌธ์ œ: Swagger๋กœ API ํ…Œ์ŠคํŠธ ์‹œ `Multipart` ํƒ€์ž…์œผ๋กœ ๋ฐ›์œผ๋ ค๊ณ  ํ•˜๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ `application/octet-stream` ํƒ€์ž…์œผ๋กœ ๋„˜์–ด์™€์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•จ - ํ•ด๊ฒฐ: `application/octet-stream` ํƒ€์ž…์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก ์ปจ๋ฒ„ํ„ฐ ์ƒ์„ฑ ```java @Component public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter { /** * "Content-Type: multipart/form-data" ํ—ค๋”๋ฅผ ์ง€์›ํ•˜๋Š” HTTP ์š”์ฒญ ๋ณ€ํ™˜๊ธฐ */ public MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) { super(objectMapper, MediaType.APPLICATION_OCTET_STREAM); } @Override public boolean canWrite(Class clazz, MediaType mediaType) { return false; } @Override public boolean canWrite(Type type, Class clazz, MediaType mediaType) { return false; } @Override protected boolean canWrite(MediaType mediaType) { return false; } } ```
OAS ํŒŒ์ผ ์ƒ์„ฑ ์‹œ RequestPart๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ถ€๋ถ„์€ ๋ˆ„๋ฝ๋˜๋Š” ๋ฌธ์ œ
- ๋ฌธ์ œ: restdocs-api-spec์œผ๋กœ OAS ํŒŒ์ผ ์ƒ์„ฑ ์‹œ `@RequestPart`์— ๊ด€๋ จ๋œ ๋ถ€๋ถ„์ด ๋ˆ„๋ฝ๋จ - ํ•ด๊ฒฐ: ๋น ์ง„ ๋ถ€๋ถ„์„ ์ˆ˜๋™์œผ๋กœ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๊ฒŒ build script๋ฅผ ์ž‘์„ฑํ•จ ๐Ÿ“Œ [์ฝ”๋“œ ํ™•์ธ](https://github.com/hbeeni/online-store/blob/df624c3a7faea999576c10ea7fc57642562c6a71/build.gradle#L49) ```groovy tasks.register('insertToOpenApiYaml') { dependsOn 'processResources' dependsOn 'openapi3' doLast { def filePath = "${openapi3.outputDirectory}/openapi3.yaml" def openApiFile = file(filePath) def content = openApiFile.text def dir = "src/main/resources/static/insert-to-yaml" def addProductText = file("${dir}/add-product.txt").text def updateProductImageText = file("${dir}/update-product-image.txt").text def insertionPoint1 = content.indexOf("operationId: admin/product/addProduct") + "operationId: admin/product/addProduct".length() def insertionPoint2 = content.indexOf("operationId: admin/product/updateProductImage") + "operationId: admin/product/updateProductImage".length() def section1 = content.substring(0, insertionPoint1) + "\n" def section2 = content.substring(insertionPoint1, insertionPoint2) + "\n" def section3 = content.substring(insertionPoint2) def newContent = new StringBuilder().append(section1).append(addProductText) .append(section2).append(updateProductImageText) .append(section3) new File(filePath).write(newContent.toString(), "utf-8") } } ```
MySQL connection timed out ๋ฌธ์ œ
- ๋ฌธ์ œ ์ƒํ™ฉ - GCP๋ฅผ ์ด์šฉํ•ด ๋ฐฐํฌ ํ›„ ๋ฐœ์ƒํ•œ ๋ฌธ์ œ์ž„ - Spring Boot Application VM ์ธ์Šคํ„ด์Šค, MySQL VM ์ธ์Šคํ„ด์Šค๋ฅผ ๊ฐ๊ฐ ์ƒ์„ฑํ•จ - MySQL VM ์ธ์Šคํ„ด์Šค์— MySQL ์„ค์น˜ํ•˜๊ณ , localhost ๊ณ„์ • ์ƒ์„ฑ๋„ ํ–ˆ๋Š”๋ฐ ์—ฐ๊ฒฐ์ด ๋˜์ง€ ์•Š์•˜์Œ - ํ•ด๊ฒฐ - MySQL์„ ๋กœ์ปฌ์ด ์•„๋‹Œ ์™ธ๋ถ€์—์„œ IP๋ฅผ ํ†ตํ•ด ์ ‘์†ํ•˜๋ ค๋ฉด `bind-address`๋ฅผ ์ˆ˜์ •ํ•ด์•ผ ํ•จ 1. MySQL VM ์ธ์Šคํ„ด์Šค์— ์ ‘์† 2. `etc/mysql/mysql.conf.d/mysqld.cnf` ํŒŒ์ผ์˜ `bind-address=0.0.0.0`์œผ๋กœ ๋ณ€๊ฒฝ ํ›„ MySQL ์žฌ์‹œ์ž‘ 3. MySQL์— ๋ชจ๋“  IP์—์„œ์˜ ์ ‘๊ทผ์„ ํ—ˆ์šฉํ•˜๊ฒŒ ์ ‘๊ทผ์„ `%`๋กœ ์ง€์ •ํ•œ ์œ ์ € ์ƒ์„ฑ
ssh: connect to host {IP} port 22: Connection timed out
- ๋กœ์ปฌ์—์„œ GCP VM ์ธ์Šคํ„ด์Šค์— SSH ์ ‘์†์„ ์‹œ๋„ํ•  ๋•Œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•จ - ํ•ด๊ฒฐ: GCP ๋ฐฉํ™”๋ฒฝ ๊ทœ์น™์—์„œ 22๋ฒˆ ํฌํŠธ๋ฅผ ์—ด์–ด์คŒ