Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
3a9b598
chore: ddl-auto update로 변경
codrin2 Aug 31, 2025
0bd53ec
refactor: lua script 에러 처리 추가
codrin2 Sep 1, 2025
8f38934
docs(README): 기술 issue 수정
codrin2 Sep 1, 2025
9626b45
fix: lua script StringCodec 처리 추가
codrin2 Sep 1, 2025
b56ce4e
chore: Hikari CP 풀 증가
codrin2 Sep 2, 2025
d53c4ab
chore: async 풀 증가
codrin2 Sep 2, 2025
90a7b53
chore: Hikari CP 풀 증가
codrin2 Sep 2, 2025
0bc4089
refactor: 비동기 일괄 주문 로직 수정
codrin2 Sep 2, 2025
9e7970a
test: OrderStatus 변경에 따른 테스트 코드 수정
codrin2 Sep 2, 2025
6673afd
chore: 쓰레드 풀 개수 수정
codrin2 Sep 2, 2025
2d331b9
fix: 비동기 처리 로직 수정
codrin2 Sep 3, 2025
73d3e94
chore: 패키지 위치 수정
codrin2 Sep 3, 2025
5349e74
fix: 일괄 주문 AFTER_ROLLBACK 로직 비활성화
codrin2 Sep 3, 2025
b22f233
fix: 재고 감소 lua script 수정
codrin2 Sep 3, 2025
de2b61e
fix: lua script sha 제거
codrin2 Sep 4, 2025
fd779fd
fix: lua script 수정
codrin2 Sep 4, 2025
67be27a
fix: lua script 수정
codrin2 Sep 4, 2025
2fe7d63
fix: lua script 수정
codrin2 Sep 4, 2025
7cc6886
refactor: key가 없을 때 최신화하는 과정에 lua script 적용
codrin2 Sep 4, 2025
64cb18f
docs(README): 기술 이슈 추가
codrin2 Sep 5, 2025
e585ac3
docs(README): 오타 수정
codrin2 Sep 5, 2025
7895dc6
chore: HikariCP 8 -> 4로 감소
codrin2 Sep 5, 2025
f5d0a77
chore: HikariCP 4 -> 16 증가
codrin2 Sep 5, 2025
ef8c53e
chore: HikariCP 16 -> 32 증가
codrin2 Sep 5, 2025
282e1f5
chore: HikariCP 32 -> 2 감소
codrin2 Sep 5, 2025
df666c4
chore: HikariCP 2 -> 4 증가
codrin2 Sep 5, 2025
b0afdd3
chore: HikariCP 4 -> 6 증가
codrin2 Sep 5, 2025
d456e20
chore: HikariCP 6 -> 8 증가
codrin2 Sep 5, 2025
38f5160
chore: HikariCP 8 -> 10 증가
codrin2 Sep 5, 2025
e5539ab
chore: HikariCP 10 -> 12 증가
codrin2 Sep 5, 2025
f914b69
chore: HikariCP 12 -> 14 증가
codrin2 Sep 5, 2025
dcd7f5b
chore: HikariCP 14 -> 16 증가
codrin2 Sep 5, 2025
a34df45
chore: cookie 도메인 추가
codrin2 Sep 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,7 @@ src/main/resources/application.yaml

# P6Spy log
spy.log

### claude ###
/claudedocs
/CLAUDE.md
16 changes: 7 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,7 @@

<br>

# 📋 API spec

[**[바로(BARO) Swagger]**](https://api.s-baro.shop/swagger-ui/index.html#/)

<br>

# 📈 기술적 목표
# 📈 목표 성능
MAU 5만명, DAU 5,000명(DAU/MAU 비율 10%) 기준

동시 접속자 수 : 500명(DAU/10)
Expand All @@ -45,7 +39,7 @@ MAU 5만명, DAU 5,000명(DAU/MAU 비율 10%) 기준

<br>

# 🤔 기술적 Issue
# 🤔 Technical Issue
[**[Part 1] JWT는 정말 괜찮은 방법일까? (Ft. 세션저장소 선택 이유)**](https://chobo-backend.tistory.com/50)

[**[Part 2] 확장성과 성능을 고려한 ERD 설계하기**](https://chobo-backend.tistory.com/51)
Expand All @@ -54,6 +48,10 @@ MAU 5만명, DAU 5,000명(DAU/MAU 비율 10%) 기준

[**[Part 4] 반복되는 인증,인가 처리 없애버리기(Ft. AOP & ArgumentResolver)**](https://chobo-backend.tistory.com/53)

[**[Part 5] 주문 API 성능 개선 삽질기 (Ft. 목표 TPS 1666 vs 현실 187.4)**](https://chobo-backend.tistory.com/54)
[**[Part 5] 단일 주문 기능 개선 삽질기 (Ft. TPS 54.9% 개선)**](https://chobo-backend.tistory.com/54)

[**[Part 6] DeadLock 범인 찾기 (Ft. 위험한 FK?)**](https://chobo-backend.tistory.com/55)

[**[Part 7] 일괄 주문 기능 74.6% 개선 (Ft. Eventual Consistency)**](https://chobo-backend.tistory.com/56)

[**[Part 8] Redis의 Lua Script는 Atomic 하지 않다..?**](https://chobo-backend.tistory.com/57)
5 changes: 4 additions & 1 deletion src/main/kotlin/com/dh/baro/core/ErrorMessage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ enum class ErrorMessage(val message: String) {
URL_PARAMETER_ERROR("요청 URL 파라미터 검증에 실패했습니다."),
MISSING_REQUEST_HEADER("필수 요청 헤더가 누락되었습니다"),
METHOD_ARGUMENT_TYPE_MISMATCH("요청 파라미터 타입이 올바르지 않습니다."),
ALREADY_DISCONNECTED("클라이언트 연결이 중단되었습니다."),
ALREADY_DISCONNECTED("클라이언트 연결이 중단되었습니다.") ,
NO_RESOURCE_FOUND("요청한 리소스를 찾을 수 없습니다."),
METHOD_NOT_SUPPORTED("허용되지 않은 메서드입니다."),
MEDIA_TYPE_NOT_SUPPORTED("허용되지 않은 미디어 타입입니다."),
Expand All @@ -27,6 +27,7 @@ enum class ErrorMessage(val message: String) {
INVALID_POPULAR_PRODUCT_CURSOR("cursorLikes와 cursorId는 함께 지정하거나 함께 생략해야 합니다."),
OUT_OF_STOCK("재고가 모두 소진되었습니다.[id = %d]"),
INSUFFICIENT_STOCK("일부 상품의 재고가 부족합니다."),
INVALID_STOCK_AMOUNT("잘못된 형식의 재고 수량입니다."),
PRODUCT_NOT_FOUND_FOR_DEDUCTION("재고 차감할 상품을 찾을 수 없습니다: %d"),
INVENTORY_RESTORE_ERROR("Redis 재고 복원 중 오류 발생: orderId=%d"),
INVENTORY_RETRY_EXCEEDED("재고 초기화 재시도 횟수 초과: maxRetry=%d"),
Expand All @@ -41,6 +42,8 @@ enum class ErrorMessage(val message: String) {

// Order
ORDER_NOT_FOUND("주문을 찾을 수 없습니다: %d"),
ORDER_CONFIRM_INVALID_STATUS("주문 확정은 PENDING 상태에서만 가능합니다. 현재 상태: %s"),
ORDER_CONFIRM_NO_ITEMS("주문 항목이 없어 주문을 확정할 수 없습니다."),

// Look
LOOK_NOT_FOUND("룩을 찾을 수 없습니다: %d"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class OutboxMessageScheduler(
private val outboxBatchJob: OutboxBatchJob,
) {

@Scheduled(fixedDelay = 60_000) // 1분
// @Scheduled(fixedDelay = 60_000) // 1분
fun runOutboxJob() {
outboxBatchJob.run()
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package com.dh.baro.order.application

import com.dh.baro.identity.domain.service.UserService
import com.dh.baro.order.domain.Order
import com.dh.baro.order.domain.OrderQueryService
import com.dh.baro.order.domain.OrderService
import com.dh.baro.order.domain.service.OrderQueryService
import com.dh.baro.order.domain.service.OrderService
import com.dh.baro.order.domain.service.OrderServiceV2
import com.dh.baro.order.presentation.dto.OrderCreateRequest
import com.dh.baro.product.domain.InventoryItem
Expand Down
17 changes: 15 additions & 2 deletions src/main/kotlin/com/dh/baro/order/domain/Order.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dh.baro.order.domain

import com.dh.baro.core.BaseTimeEntity
import com.dh.baro.core.ErrorMessage
import com.dh.baro.core.IdGenerator
import com.dh.baro.core.annotation.AggregateRoot
import jakarta.persistence.*
Expand All @@ -26,7 +27,7 @@ class Order(

@Enumerated(EnumType.STRING)
@Column(name = "order_status", nullable = false, length = 20)
var status: OrderStatus = OrderStatus.ORDERED,
var status: OrderStatus = OrderStatus.PENDING,

@OneToMany(
mappedBy = "order",
Expand All @@ -49,6 +50,18 @@ class Order(
.setScale(0, RoundingMode.HALF_UP)
}

fun confirmOrder() {
if (status != OrderStatus.PENDING) {
throw IllegalStateException(ErrorMessage.ORDER_CONFIRM_INVALID_STATUS.format(status))
}

if (items.isEmpty()) {
throw IllegalStateException(ErrorMessage.ORDER_CONFIRM_NO_ITEMS.message)
}

status = OrderStatus.ORDERED
}

companion object {
fun newOrder(
userId: Long,
Expand All @@ -59,7 +72,7 @@ class Order(
userId = userId,
totalPrice = BigDecimal.ZERO,
shippingAddress = shippingAddress,
status = OrderStatus.ORDERED,
status = OrderStatus.PENDING,
)
}
}
1 change: 1 addition & 0 deletions src/main/kotlin/com/dh/baro/order/domain/OrderStatus.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dh.baro.order.domain

enum class OrderStatus {
PENDING,
ORDERED,
PAID,
SHIPPED,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.dh.baro.order.domain
package com.dh.baro.order.domain.service

import com.dh.baro.core.ErrorMessage
import com.dh.baro.order.domain.Order
import com.dh.baro.order.domain.OrderRepository
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Slice
import org.springframework.stereotype.Service
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.dh.baro.order.domain
package com.dh.baro.order.domain.service

import com.dh.baro.core.ErrorMessage
import com.dh.baro.core.exception.ConflictException
import com.dh.baro.order.application.OrderCreateCommand
import com.dh.baro.order.domain.Order
import com.dh.baro.order.domain.OrderItem
import com.dh.baro.order.domain.OrderRepository
import com.dh.baro.product.domain.repository.ProductRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

Expand Down Expand Up @@ -54,4 +58,10 @@ class OrderService(
)
order.addItem(orderItem)
}

fun confirmOrder(orderId: Long) {
val order = orderRepository.findByIdOrNull(orderId)
?: throw IllegalArgumentException(ErrorMessage.ORDER_NOT_FOUND.format(orderId))
order.confirmOrder()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.dh.baro.order.infra

import com.dh.baro.core.ErrorMessage
import com.dh.baro.product.infra.event.InventoryDeductionRequestedEvent
import com.dh.baro.product.infra.redis.InventoryRedisRepository
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import org.springframework.transaction.event.TransactionPhase
import org.springframework.transaction.event.TransactionalEventListener

@Component
class InventoryEventBeforeListener(
private val inventoryRedisRepository: InventoryRedisRepository,
) {

private val log = LoggerFactory.getLogger(javaClass)

// @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
// fun restoreInventoryOnRollback(event: InventoryDeductionRequestedEvent) {
// runCatching {
// inventoryRedisRepository.restoreStocks(event.items)
// }.onFailure { ex ->
// log.error(ErrorMessage.INVENTORY_RESTORE_ERROR.format(event.orderId), ex)
// }
// }
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dh.baro.product.domain.service

import com.dh.baro.core.ErrorMessage
import com.dh.baro.core.exception.ConflictException
import com.dh.baro.product.domain.InventoryItem
import com.dh.baro.product.domain.repository.ProductRepository
import com.dh.baro.product.infra.redis.InventoryRedisRepository
Expand All @@ -22,10 +23,10 @@ class InventoryService(
*/
@Transactional
fun deductStockFromDB(productId: Long, quantity: Int) {
val product = productRepository.findById(productId)
.orElseThrow { IllegalArgumentException(ErrorMessage.PRODUCT_NOT_FOUND_FOR_DEDUCTION.format(productId)) }
val updated = productRepository.deductStock(productId, quantity)

product.deductStock(quantity)
productRepository.save(product)
if (updated == 0) {
throw ConflictException(ErrorMessage.OUT_OF_STOCK.format(productId))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.dh.baro.product.infra.event

import com.dh.baro.core.config.AsyncConfig.Companion.EVENT_ASYNC_TASK_EXECUTOR
import com.dh.baro.order.domain.service.OrderService
import com.dh.baro.product.domain.service.InventoryService
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Component
import org.springframework.transaction.event.TransactionPhase
import org.springframework.transaction.event.TransactionalEventListener

@Component
class InventoryEventAfterListener(
private val orderService: OrderService,
private val inventoryService: InventoryService,
) {

@Async(EVENT_ASYNC_TASK_EXECUTOR)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun handleInventoryDeductionEvent(event: InventoryDeductionRequestedEvent) {
event.items.forEach { item ->
inventoryService.deductStockFromDB(item.productId, item.quantity)
}
orderService.confirmOrder(event.orderId)
}
}

This file was deleted.

Loading