๐ ์จ๋ผ์ธ ์ผํ๋ชฐ API (online store API)
์ผํ๋ชฐ ์น ์ฌ์ดํธ๋ฅผ ์ํ REST API
1. ์ ์ ๊ธฐ๊ฐ & ์ฐธ์ฌ ์ธ์
- v1.0 : 2023๋
11์ 1์ผ ~ 2024๋
1์ 14์ผ
- v1.1 : 2024๋
3์ 23์ผ ~ 2023๋
3์ 25์ผ
- ๊ฐ์ธ ํ๋ก์ ํธ
2. ์ฌ์ฉ ๊ธฐ์
- Java 17
- Spring Boot 2.7.17
- Gradle 8.3
- Spring Data JPA
- QueryDSL
- MySQL
- H2
- Spring Security
- Spring Web
- Spring REST Docs
- restdocs-api-apec
- Google Cloud Platform
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 ์ ๋ ฌ ๋ฌธ์
-
QueryDSL์ ์ฌ์ฉ ์ ์ ๋ ฌํ ๋๋ Pageable
์ Sort
๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค.
-
orderBy
๋ฉ์๋์ ํ๋ผ๋ฏธํฐ๋ก OrderSpecifier
ํ์
์ด ํ์ํ๊ธฐ ๋๋ฌธ์
๋๋ค.
-
๋ฐ๋ผ์ Pageable
์ Sort
์ ๋ณด๋ฅผ ํ ๋๋ก OrderSpecifier
๋ฅผ ์์ฑํด์ฃผ์์ต๋๋ค.
์ฝ๋
```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 ์์ฑ ์ค๋ฅ ๋ฌธ์
- ํ์ด์ง์ ํ๊ธฐ ์ํด์๋ ์ ์ฒด ์นด์ดํธ๊ฐ ๊ผญ ์์ด์ผ ๋ช ํ์ด์ง๊น์ง ์๋์ง ์ ์ ์์ต๋๋ค.
- ๊ทธ๋์
countQuery
๊ฐ ์์ผ๋ฉด Spring Data JPA๊ฐ ์๋ณธ ์ฟผ๋ฆฌ๋ฅผ ๋ณด๊ณ ์์๋ก countQuery
๋ฅผ ์์ฑํฉ๋๋ค.
- ๊ทธ๋ฐ๋ฐ ๊ธฐ์กด ์ฝ๋์์
QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list
์๋ฌ๊ฐ ๋ฐ์ํ์ต๋๋ค.
๊ธฐ์กด ์ฝ๋
```java
@Query("select p from Product p join fetch p.category where p.saleStatus = 'SALE'")
Page
findAllOnSale(Pageable pageable);
```
- ์๋๋
countQuery
๋ฅผ ์ ๋๋ก ๋ง๋ค์ด ์ฃผ๋๋ฐ, ํ์ด์ง ์์๋ fetch join
์ด ํฌํจ๋๊ฒ ๋ง๋ค์ด์ฃผ๊ธฐ ๋๋ฌธ์ ์ค๋ฅ๊ฐ ๋ฐ์ํฉ๋๋ค.
fetch join
์ ์ํฐํฐ ์ํ์์ ์ํฐํฐ ๊ทธ๋ํ๋ฅผ ์ฐธ์กฐํ๊ธฐ ์ํด์ ์ฌ์ฉํ๋๋ฐ, count()
๋ก ์กฐํ ๊ฒฐ๊ณผ๊ฐ ๋ณ๊ฒฝ๋์ด๋ฒ๋ ธ๊ธฐ ๋๋ฌธ์
๋๋ค.
countQuery
๋ฅผ ์ง์ ์์ฑํจ์ผ๋ก์จ ์ค๋ฅ๋ฅผ ํด๊ฒฐํ์ต๋๋ค.
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๋ฅผ ํ ์ ์๋ ๋ฌธ์
- API ๋ฌธ์๋ฅผ ๋ง๋ค๊ธฐ ์ํด Spring REST Docs๋ฅผ ์ฌ์ฉํ์์ต๋๋ค.
- ๋น์ฆ๋์ค ์ฝ๋์ ์ํฅ์ ์ฃผ์ง ์๊ณ , ํ
์คํธ๋ ๊ฐ์ ํ์ฌ ์ ๋ขฐ์ฑ ๋์ API ๋ฌธ์๋ฅผ ๋ง๋ค ์ ์๊ธฐ ๋๋ฌธ์ ํด๋น ๊ธฐ์ ์ ์ ํํ์์ต๋๋ค.
- ๊ทธ๋ฐ๋ฐ Spring REST Docs๋ฅผ ์ฌ์ฉํ๋ฉด API Test๊ฐ ๋ถ๊ฐ๋ฅํ์ต๋๋ค.
- ์ด ์๋น์ค๋ API ๋ฌธ์๋ง์ ์ ๊ณตํ๋ฏ๋ก Swagger๋ฅผ ์ฌ์ฉํ ๋์ฒ๋ผ API๋ฅผ ํ
์คํธํ ์ ์๋ ๊ธฐ๋ฅ์ ๊ฐ์ถ์์ผ๋ฉด ํ์ต๋๋ค.
- ๋ฐ๋ผ์ API Test๋ ๊ฐ๋ฅํ๊ฒ๋ restdocs-api-spec๊ณผ Swagger UI ์ ์ ํ์ผ์ ์ฌ์ฉํ์์ต๋๋ค.
- Swagger UI ํ์ผ์ ์์ ํฉ๋๋ค.
index.html
ํ์ผ์ ๋ด๋ถ css, js ๊ฒฝ๋ก๋ฅผ ์์ ํฉ๋๋ค.
swagger-initializer.js
ํ์ผ์ SwaggerUIBundle ๊ฒฝ๋ก๋ OpenAPI Specification(OAS) ํ์ผ ๊ฒฝ๋ก๋ก ์์ ํฉ๋๋ค.
- Spring REST Docs + restdocs-api-spec๋ก ํ
์คํธ๋ฅผ ์์ฑํฉ๋๋ค.
- restdocs-api-spec์ผ๋ก OAS ํ์ผ ์์ฑ ํ static ๋๋ ํ ๋ฆฌ๋ก ๋ณต์ฌํฉ๋๋ค.
- 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๋ฒ ํฌํธ๋ฅผ ์ด์ด์ค