Spring Boot ๊ธฐ๋ฐ์ ์ด์ปค๋จธ์ค API ์ ๋๋ค. ํ์ ๊ด๋ฆฌ, ์ํ ์กฐํ, ์ฅ๋ฐ๊ตฌ๋, ์ฃผ๋ฌธ/๊ฒฐ์ , ์ฟ ํฐ ๋ฐ๊ธ ๋ฑ ์ด์ปค๋จธ์ค์ ํต์ฌ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
- ์ํคํ ์ฒ ๋ฐ ํ๋ก์ ํธ ๊ตฌ์กฐ
- ๋์์ฑ ์ ์ด
- ๊ธฐ์ ์คํ
- ์ค์น ๋ฐ ์คํ
- API ๋ฌธ์
- ์ค๊ณ & ๋ณด๊ณ ์ ๋ฌธ์
๋ ์ด์ด๋ ์ํคํ ์ฒ (Layered Architecture)
์ด ํ๋ก์ ํธ๋ ๋ ์ด์ด๋ ์ํคํ ์ฒ๋ฅผ ์ฑํํ์ฌ ๊ด์ฌ์ฌ์ ๋ถ๋ฆฌ์ ์ ์ง๋ณด์์ฑ์ ํ๋ณดํฉ๋๋ค.
โโโ presentation (ํ๋ ์ ํ
์ด์
๋ ์ด์ด)
โ โโโ controller
โ โโโ dto // request, response DTO
โโโ application (์ ํ๋ฆฌ์ผ์ด์
๋ ์ด์ด)
โ โโโ service // ์ฌ๋ฌ entity, repository ์กฐํฉํ ๋น์ฆ๋์ค ๋ก์ง
โ โโโ dto // application layer DTO
โ โโโ enums // service ๋ด ์ฌ์ฉ enum
โ โโโ validator // ํด๋น ๋๋ฉ์ธ์ ๊ฒ์ฆ ๋ก์ง (๋ค๋ฅธ ๋๋ฉ์ธ์์ ์ฌ์ฉํ ์ ์๋๋ก ๋ถ๋ฆฌ)
โโโ domain (๋๋ฉ์ธ ๋ ์ด์ด)
โ โโโ entity // ๋๋ฉ์ธ ๊ฐ์ฒด, ๋๋ฉ์ธ์ ํด๋นํ๋ ๋น์ฆ๋์ค ๋ก์ง
โโโ infrastructure (์ธํ๋ผ์คํธ๋ญ์ฒ ๋ ์ด์ด)
โโโ InMemoryXXRepository // ์ธ๋ฉ๋ชจ๋ฆฌ ์ ์ฅ์
- Presentation Layer: HTTP ์์ฒญ/์๋ต ์ฒ๋ฆฌ, DTO ๋ณํ
- Application Layer: ๋น์ฆ๋์ค ์ ์ค์ผ์ด์ค ์กฐ์จ, ํธ๋์ญ์ ๊ด๋ฆฌ
- Domain Layer: ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง ๋ฐ ๋๋ฉ์ธ ๋ชจ๋ธ
- Infrastructure Layer: ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ๊ทผ, ์ธ๋ถ ์์คํ ์ฐ๋
- ๋ช ํํ ์ฑ ์ ๋ถ๋ฆฌ: ๊ฐ ๋ ์ด์ด๊ฐ ๋ช ํํ ์ญํ ์ ๊ฐ์ง๊ณ ์์ด ์ฝ๋์ ์์ง๋๊ฐ ๋๊ณ ๊ฒฐํฉ๋๊ฐ ๋ฎ์
- ํ ์คํธ ์ฉ์ด์ฑ: ๋ ์ด์ด๋ณ๋ก ๋ ๋ฆฝ์ ์ธ ํ ์คํธ ์์ฑ์ด ๊ฐ๋ฅํ๋ฉฐ, ๋ชจํน์ด ์ฉ์ดํจ
- ์ ์ง๋ณด์์ฑ: ๋ณ๊ฒฝ ์ฌํญ์ด ํน์ ๋ ์ด์ด์ ๊ตญํ๋์ด ์ํฅ ๋ฒ์๊ฐ ์ ํ์ ์
- ํ์ฅ์ฑ: ์๋ก์ด ๊ธฐ๋ฅ ์ถ๊ฐ ์ ๊ธฐ์กด ๋ ์ด์ด ๊ตฌ์กฐ๋ฅผ ๋ฐ๋ผ ์ผ๊ด๋๊ฒ ๊ตฌํ ๊ฐ๋ฅ
- ํ ํ์ : ๋ ์ด์ด๋ณ๋ก ์์ ์ ๋ถ๋ดํ๊ธฐ ์ฉ์ดํ์ฌ ๋ณ๋ ฌ ๊ฐ๋ฐ์ ์ ๋ฆฌํจ
์ด์ปค๋จธ์ค ์์คํ ์์ ๋ฐ์ํ ์ ์๋ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์ปค์คํ ๋ฝ(Lock) ๋ฉ์ปค๋์ฆ์ ๊ตฌํํ์ต๋๋ค.
@WithLock ์ด๋ ธํ ์ด์ + AOP (Aspect)
@WithLock(key = "'issueCoupon:' + #command.couponId")
public IssueCouponResult issueCoupon(IssueCouponCommand command) {
// ์ฟ ํฐ ๋ฐ๊ธ ๋ก์ง
}-
@WithLock ์ด๋ ธํ ์ด์
- SpEL ํํ์์ ์ฌ์ฉํ์ฌ ๋ฝ ํค๋ฅผ ๋์ ์ผ๋ก ์ง์
timeout: ๋ฝ ํ๋ ๋๊ธฐ ์๊ฐ ์ค์ (๊ธฐ๋ณธ 3์ด)ignoreIfLocked: ๋ฝ ํ๋ ์คํจ ์ ์ฆ์ ๋ฐํ ์ฌ๋ถ (๊ธฐ๋ณธ false)
-
LockAspect
ConcurrentHashMap<Object, ReentrantLock>๊ธฐ๋ฐ ๋ฝ ๊ด๋ฆฌ- ๊ณต์ ํ ๋ฝ ํ๋์ ์ํด
ReentrantLock(true)์ฌ์ฉ - ๋ฝ ํค๋ณ๋ก ๋ ๋ฆฝ์ ์ธ ๋ฝ ๊ฐ์ฒด ์์ฑ ๋ฐ ๊ด๋ฆฌ
- ์ธ๋ฐํ ๋ฝ ์ ์ด: ์ ์ญ ๋ฝ์ด ์๋ ํค ๊ธฐ๋ฐ ๋ฝ์ผ๋ก ์ฑ๋ฅ ์ต์ ํ
- ํ์์์ ์ค์ : ๋ฐ๋๋ฝ ๋ฐฉ์ง ๋ฐ ์๋ต ์๊ฐ ๋ณด์ฅ
- SpEL ์ง์: ๋ฉ์๋ ํ๋ผ๋ฏธํฐ๋ฅผ ํ์ฉํ ๋์ ๋ฝ ํค ์์ฑ
๋ฌธ์ ์ํฉ
- ๋์ผ ์ฌ์ฉ์๊ฐ ๊ฐ์ ์ฟ ํฐ์ ๋์์ ์ฌ๋ฌ ๋ฒ ์์ฒญ
- ์ ํ๋ ์๋์ ์ฟ ํฐ์ ์ฌ๋ฌ ์ฌ์ฉ์๊ฐ ๋์์ ์์ฒญ
ํด๊ฒฐ ๋ฐฉ๋ฒ
@WithLock(key = "'issueCoupon:' + #command.couponId")
public IssueCouponResult issueCoupon(IssueCouponCommand command)- ์ฟ ํฐ ID๋ฅผ ๊ธฐ์ค์ผ๋ก ๋ฝ ํ๋
- ๋์ผ ์ฟ ํฐ์ ๋ํ ๋์ ๋ฐ๊ธ ์์ฒญ์ ์์ฐจ ์ฒ๋ฆฌํ์ฌ ์ค๋ณต ๋ฐ๊ธ ๋ฐฉ์ง
๊ฒ์ฆ
- 20๊ฐ ์ค๋ ๋๊ฐ ๋์์ ๋์ผ ์ฌ์ฉ์๋ก ๋ฐ๊ธ ์๋ โ 1๊ฐ๋ง ๋ฐ๊ธ ์ฑ๊ณต
- 20๊ฐ ์ฌ์ฉ์๊ฐ ๋ง์ง๋ง 1๊ฐ ์ฟ ํฐ ๋ฐ๊ธ ์๋ โ 1๋ช ๋ง ์ฑ๊ณต
๋ฌธ์ ์ํฉ
- ๋์ผ ์ฃผ๋ฌธ์ ๋ํด ์ฌ๋ฌ ๋ฒ ๊ฒฐ์ ์๋ (์ค๋ณต ๊ฒฐ์ )
- ์ฌ๊ณ ์ฐจ๊ฐ, ํฌ์ธํธ ์ฐจ๊ฐ ์ ๊ฒฝ์ ์กฐ๊ฑด ๋ฐ์
ํด๊ฒฐ ๋ฐฉ๋ฒ
@WithLock(key = "'processPayment:' + #userId")
public PaymentResult processPayment(Integer orderId, Integer userId)- ์ฌ์ฉ์ ID๋ฅผ ๊ธฐ์ค์ผ๋ก ๋ฝ ํ๋
- ๋์ผ ์ฌ์ฉ์์ ๋์ ๊ฒฐ์ ๋ฅผ ์์ฐจ ์ฒ๋ฆฌํ์ฌ ์ค๋ณต ๊ฒฐ์ ๋ฐฉ์ง
- ๋ณด์ ํธ๋์ญ์ ์ ํตํด ๊ฒฐ์ ์คํจ ์ ์ฌ๊ณ /ํฌ์ธํธ ์์๋ณต๊ตฌ
๊ฒ์ฆ
- 10๊ฐ ์ค๋ ๋๊ฐ ๋์ผ ์ฃผ๋ฌธ ๊ฒฐ์ ์๋ โ 1๋ฒ๋ง ์ฑ๊ณต, ํฌ์ธํธ 1ํ๋ง ์ฐจ๊ฐ
- ์๋ก ๋ค๋ฅธ ์ฌ์ฉ์์ ์๋ก ๋ค๋ฅธ ์ฃผ๋ฌธ โ ๋ชจ๋ ์ ์ ์ฒ๋ฆฌ (๋ฝ ๊ฒฝํฉ ์์)
๋ฌธ์ ์ํฉ
- ๋์ผ ์ฌ์ฉ์๊ฐ ๋์์ ์ฌ๋ฌ ๋ฒ ํฌ์ธํธ ์ถฉ์
ํด๊ฒฐ ๋ฐฉ๋ฒ
@WithLock(key = "'chargePoint:' + #userId", ignoreIfLocked = true)
public PointResult chargePoint(Integer userId, Integer amount)- ์ฌ์ฉ์ ID๋ฅผ ๊ธฐ์ค์ผ๋ก ๋ฝ ํ๋
ignoreIfLocked = true: ๋ฝ ํ๋ ์คํจ ์ ์ฆ์ null ๋ฐํ (ํ์์์ ์์)
๋ชจ๋ ๋์์ฑ ์ ์ด ๋ก์ง์ ํตํฉ ํ ์คํธ๋ก ๊ฒ์ฆ๋์์ต๋๋ค.
ํ ์คํธ ๋ฐฉ๋ฒ
ExecutorService+CountDownLatch๋ฅผ ์ฌ์ฉํ ๋ฉํฐ์ค๋ ๋ ํ๊ฒฝ ์๋ฎฌ๋ ์ด์ - ์ผ๋ฐ์ ์ผ๋ก 10~20๊ฐ ์ค๋ ๋๋ก ๋์ ์์ฒญ ํ ์คํธ
AtomicInteger๋ก ์ฑ๊ณต/์คํจ ์นด์ดํธ ์ถ์
ํ ์คํธ ์ผ์ด์ค
CouponServiceConcurrencyIntegrationTest: ์ฟ ํฐ ์ค๋ณต ๋ฐ๊ธ ๋ฐฉ์ง, ์ ์ฐฉ์ ์ฒ๋ฆฌOrderServiceConcurrencyIntegrationTest: ์ค๋ณต ๊ฒฐ์ ๋ฐฉ์ง, ๋ณด์ ํธ๋์ญ์ PointServiceConcurrencyIntegrationTest: ํฌ์ธํธ ์ถฉ์ ๋์์ฑ ์ ์ด
๋ค๋ฅธ ๋ฐฉ์ ๋๋น ์ฅ์
-
synchronized ๋๋น
- ์ธ๋ฐํ ๋ฝ ์ ์ด ๊ฐ๋ฅ (ํค ๊ธฐ๋ฐ)
- ํ์์์ ์ค์ ๊ฐ๋ฅ
- ๊ณต์ ์ฑ ๋ณด์ฅ (fair lock)
-
@Transactional + DB ๋ฝ ๋๋น
- ์ธ๋ฉ๋ชจ๋ฆฌ ์ ์ฅ์ ์ฌ์ฉ ์์๋ ๋์
- DB ๋ถํ ๊ฐ์
- ์๋ต ์๋ ํฅ์
-
๋ถ์ฐ ๋ฝ(Redis) ๋๋น
- ๋จ์ผ ์ธ์คํด์ค ํ๊ฒฝ์ ์ ํฉ
- ์ธ๋ถ ์์กด์ฑ ์์
- ๊ตฌํ ๋ฐ ํ ์คํธ ์ฉ์ด
ํ๊ณ์
- ๋จ์ผ ์ ํ๋ฆฌ์ผ์ด์ ์ธ์คํด์ค์์๋ง ๋์
- ๋ค์ค ์ธ์คํด์ค ํ๊ฒฝ์์๋ ๋ถ์ฐ ๋ฝ(Redis, Zookeeper ๋ฑ) ํ์
- Java 17
- Spring Boot 3.3.1
- Gradle
- SpringDoc OpenAPI 3 (Swagger UI)
git clone <repository-url>
cd ecommerce-api# Gradle Wrapper ์ฌ์ฉ (๊ถ์ฅ)
./gradlew build
# ๋๋ ์์คํ
Gradle ์ฌ์ฉ
gradle build# Gradle Wrapper ์ฌ์ฉ
./gradlew bootRun
# ๋๋ ๋น๋๋ JAR ํ์ผ ์ง์ ์คํ
java -jar build/libs/ecommerce-api-0.0.1-SNAPSHOT.jar์๋ฒ๊ฐ ์ ์์ ์ผ๋ก ์์๋๋ฉด ๋ค์ ์ฃผ์๋ก ์ ์ํ ์ ์์ต๋๋ค:
- API ์๋ฒ: http://localhost:8080
- Swagger UI: http://localhost:8080/swagger-ui.html
# ์ ์ฒด ํ
์คํธ ์คํ
./gradlew test
# ํน์ ํ
์คํธ ํด๋์ค๋ง ์คํ
./gradlew test --tests com.example.ecommerceapi.YourTestClass
# ํน์ ํ
์คํธ ๋ฉ์๋๋ง ์คํ
./gradlew test --tests com.example.ecommerceapi.YourTestClass.testMethod์ ํ๋ฆฌ์ผ์ด์ ์คํ ํ Swagger UI๋ฅผ ํตํด ๋ชจ๋ API๋ฅผ ํ ์คํธํ ์ ์์ต๋๋ค:
URL: http://localhost:8080/swagger-ui.html
curl -X POST http://localhost:8080/api/cart \
-H "Content-Type: application/json" \
-d '{
"userId": 1,
"productId": 1,
"quantity": 2
}'์์ธํ ์ค๊ณ & ๋ณด๊ณ ์ ๋ฌธ์๋ docs/ ๋๋ ํ ๋ฆฌ์์ ํ์ธํ ์ ์์ต๋๋ค:
- ์๊ตฌ์ฌํญ ๋ช ์ธ์ - ๋น์ฆ๋์ค ์๊ตฌ์ฌํญ ๋ฐ ์ ์ฝ์กฐ๊ฑด
- ERD - ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ตฌ์กฐ ๋ฐ ์ํฐํฐ ๊ด๊ณ
- API ๋ช ์ธ์ - API ์๋ํฌ์ธํธ ์์ธ ์ค๋ช
- Sequence Diagram - ์ฃผ์ ๊ธฐ๋ฅ๋ณ ์ํ์ค ๋ค์ด์ด๊ทธ๋จ
- ์ฟผ๋ฆฌ ์ต์ ํ ๋ณด๊ณ ์ - ์ฟผ๋ฆฌ ์ต์ ํ ๋ฐฉ์ ์ค๋ช
- ๋์์ฑ ๋ฌธ์ ์ฒ๋ฆฌ ๋ฐฉ์ ๋ณด๊ณ ์ - ๋์์ฑ ์ ์ด ์ค๊ณ ๋ฐ ๊ตฌํ
- ๋ถ์ฐ ๋ฝ ์ ํ ๋ณด๊ณ ์ - Redis ๋ถ์ฐ ๋ฝ ์ ํ ๊ณผ์ ๋ฐ ์ฑ๋ฅ ๋ถ์
- ์ฑ๋ฅ ๊ฐ์ ๋ณด๊ณ ์ - Redis ์บ์ฑ ์ ์ฉ ๋ฐ ์ฑ๋ฅ ๊ฐ์ ๊ฒฐ๊ณผ
- Kafka ์ ์ฉ ๊ฐ์ ์ ๋ณด๊ณ ์ - Kafka ์ ์ฉ ๊ฐ์ ์ ๊ณผ์ ๋ฐ ๊ฒฐ๊ณผ
This project is licensed under the MIT License.