
๋ค์ด๊ฐ๋ฉฐ
๋๋์ด 1์ฃผ ์ฐจ๊ฐ ๋๋ฌ๋ค..! (์ด์ 10ํผ ํ๋ค..)
์ ๋ง ์ง๋ ํ ์ฃผ๋ ๋ด ๋จธ๋ฆฌ๋ก ๋ค์ด์ค๋ ์ธํ ๋ฐ์ดํฐ๊ฐ ์ด๋ง์ด๋งํ ์ผ์ฃผ์ผ์ด์๋ค.
์ง๋ ํ ์์ผ, ์คํ๋ผ์ธ ์ธ์ ๋ชจ์์์ 10์ฃผ๋์ ํจ๊ป ํ ํ์๋ค๊ณผ ์ฒซ ๋๋ฉด์ ํ๊ณ , 1์ฃผ ์ฐจ ๊ณผ์ ์ ๋ํ ๋ฐ์ ๋ฅผ ๋ค์๋ค. ๊ทธ๋ฅ ์์ด์ค๋ธ๋ ์ดํน ํ๋ฉฐ ์๋ก ๊ถ๊ธํ ๊ฒ๋ค ์๊ธฐ ๋๋๊ณ , ์ฝ์น๋์๊ฒ ๊ณผ์ ๋ฅผ ์ด๋ป๊ฒ ํ์ด๊ฐ ๊ฒ ์ธ๊ฐ์ ๋ํ ์ค๋ช ๋ง ๋ค์์ ๋ฟ์ธ๋ฐ ์ง๊ธ ํ์ฌ์์๋ ๊ฒฝํํด ๋ณด์ง๋, ๋๊ปด๋ณด์ง๋ ๋ชปํ๋ ๋ด์ฉ๋ค์ ์ ์์ ์ถฉ๊ฒฉ๊ณผ ์ค๋ ๊ณผ ๊ธด์ฅ์ด ๋ค์์ฌ์์๋ค.

๋๋ ์ง๊ธ ํ์ฌ์์ ๊ทธ๋๋ ๋์์ง ์์ ์ฑ๊ณผ๋ฅผ ๋ด๊ณค ์์ง๋ง, ์ง๋ 20๋ ์์ ๋ค๋ฅธ ๋ถ์ผ์ ์ผ์ ํ๋ฉด์ โ์ฐ๋ฌผ ์ ๊ฐ๊ตฌ๋ฆฌโ๊ฐ ๋ญ์ง๋ฅผ ํ๋ฒ ์๊ฒ ๋๊ปด๋ณด์๊ธฐ ๋๋ฌธ์, ์ด๋ฒ์๋ ๊ทธ๋ด ๊ฒ์ด๋ผ๋ ๊ฑธ ๊ฐ์คํ๊ณ ์์๋ค.
์๋๋ ๋ค๋ฅผ๊น, ์ญ์ ์ธ์์ ์ํ๋ ์ฌ๋์ ๋ง๊ณ , ๋ ์ด์ฌํ ํด๋ณด๊ณ ์ ๋์ ํ๋ ์ฌ๋๋ ๋ง์์ ๋ด๊ฐ ์ฌ๊ธฐ์ ์ ๋ฐฐ์ฐ๊ณ ๋ฒํธ ์ ์์๊น ๋ด์ฌ ๊ฑฑ์ ์ ๋์๋ค.
ํ์ง๋ง ํ ๊ฐ์ง ์ข์๋ ์ ๋ ์์๋ค. ๊ทธ๋๋ ๋๋ฆ ์ง์ฅ๋ค๋๋ฉด์ ํํ์ด CS ๊ณต๋ถ, ์๋น์ค ํ์ฌ์์ ์ฌ์ฉํ๋ ๊ธฐ์ ์คํ์ ๊ณต๋ถํด ์์๋ค๊ณ ์ฌ๋๋ค์ด ๋ฌด์จ ์๊ธฐ๋ฅผ ํ๋์ง ์ดํด๊ฐ ๋๋ ๊ฒ์ ์ค์ค๋ก ์ ๊ธฐํ๋ค. ๋ถ๋ช 1๋ ์ ๋ง ํด๋ ์ดํดํ์ง ๋ชปํ ๊ทธ๋ฐ ์ฃผ์ ์๋ค. ๊ทธ๋ฐ ๋ถ๋ถ์์ ์ค์ค๋ก ์์ ๊ฐ์ ๊ฐ์ง๊ณ ํด์ผ๊ฒ ๋ค๊ณ ์๊ฐํ๋ ๊ฒ ๊ฐ๋ค.
์ฐ๋ฆฌ ํ์ ๋ชจ๊ฐ์ฝ๋ฅผ ์ํ ์ฝ์ด ํ์์ ๋งค์ฃผ ์~๋ชฉ 21~23์๋ก ์ ํ๋ค. ๋ค๋ฅธ ์๊ฐ์ ์์จ์ ์ผ๋ก ๊ณต๋ถํ๊ณ , ์ด ์๊ฐ์๋ ์๋ก ๊ถ๊ธํ ์ ์ ๋ฌผ์ด๋ณด๊ณ , ์ฝ๋ ๋ฆฌ๋ทฐํ๋ ๋ฑ์ ์๊ฐ์ผ๋ก ํ ์ ํ๋ค.
๊ทธ์ค์ ์ ๋ง ์ข์๋ ๊ฑด, ์ฃผ์ค์ ์ผ๊ณฑ ๋ถ์ ์ฝ์น๋๋ค์ด ๊ฐ ํ๋ง๋ค ํ ์๊ฐ์ฉ ๊ถ๊ธํ ์ ์ ๋ํ ๋ฉํ ๋ง์ ํด์ฃผ์๋๋ฐ ํด๋น ์ธ์ ์ ๊ฐ์ ์ฒญ๊ฐ(์ด๋ผ ์ฝ๊ณ ๋๊ฐ)ํ ์ ์๋๋ก ํด์ฃผ์๋ค. ๋ค๋ฅธ ์๊ฐ์๋ค์ ์ง๋ฌธ๋ ๋์ ๋น์ทํ ๊ณ ๋ฏผ์ด๊ณ , ๊ถ๊ธํดํ ๋งํ ๊ฒ๋ค์ด์ด์ ์ฝ์น๋๋ง๋ค์ ์๊ฐ, ์ฑํฅ๊ณผ ์คํ์ผ, ์ฝ๋ ์์ฑ ์๋ น ๋ฑ์ ๋ผ์ด๋ธ ์ฝ๋ฉ์์ผ๋ก ๋ณผ ์๊ฐ ์์๋ค.

1์ฃผ ์ฐจ ๊ณผ์ ์์
์ด๋ฒ 1์ฃผ ์ฐจ ํ๋ก์ ํธ ๊ตฌํ ๊ณผ์ ๋ ์ฃผ๋ก TDD
, ๋์์ฑ ์ฒ๋ฆฌ
์ ๊ดํ ๋ฌธ์ ์๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก ์ ์ ๊ฐ ํฌ์ธํธ, ํฌ์ธํธ ์ด๋ ฅ ์กฐํ, ํฌ์ธํธ ์ฌ์ฉ ๋ฐ ์ถฉ์ ํ๋ API๋ฅผ ์์ฑํ๋ ๊ณผ์ ์๋ค.
ํ์ง๋ง ํ ์คํธ ์ฝ๋ ์์ฑ๊ณผ ๋์์ฑ ์ฒ๋ฆฌ๋ฅผ ์ฒ์ ํด๋ณธ(์ฝํ๋ฆฐ๋) ๋๋ก์ ์ด๋ค ์์ผ๋ก ํด์ผ ํ ์ง ๊ณ ๋ฏผ์ด ๋ง์ด ๋์๋ค.
์ฐธ ๋คํํ๋ ๋ง์นจ ์ต๊ทผ์ ์ธํ๋ฐ์์ ๋ค์๋ ๊ฐ์ข 2๊ฐ๊ฐ ๋์๊ฒ ํฐ ๋ณดํฌ์ด ๋์๋ค.
์ธํ๋ฐ Practical Testing: ์ค์ฉ์ ์ธ ํ ์คํธ ๊ฐ์ด๋ ๊ฐ์ | ๋ฐ์ฐ๋น - ์ธํ๋ฐ
โ์ธํ๋ฐ ๊น์ํ์ ์ค์ ์๋ฐ - ๊ณ ๊ธ 1ํธ, ๋ฉํฐ์ค๋ ๋์ ๋์์ฑ ๊ฐ์ | ๊น์ํ - ์ธํ๋ฐ
์ฐ๋น๋์ด ๊ฐ์ํด ์ค ํ ์คํธ ๊ฐ์๋ JUnit5 ๊ธฐ๋ฐ ๋จ์ํ ์คํธ, ํตํฉํ ์คํธ์ ๋ํ ์์ฑ ์๋ น์ ์์ ์ฝ๋์ ํจ๊ป ์ ์ค๋ช ํด ์ฃผ์๊ณ , ์ํ๋์ ๋ฉํฐ์ค๋ ๋ ๊ฐ์๋ ์ฐ๋ฆฌ๊ฐ Java ์ง์์์ ์ฃผ๋ก ์ฌ์ฉํ๋ Executor๊ฐ ์ด๋ค ๊ณผ์ ๊ณผ ์๋ฆฌ๋ก ํ์ํ๋์ง์ ๋ํด ๋จ๊ณ ๋ณ๋ก ์์ ์ฝ๋๋ฅผ ๋ง๋ค๋ฉฐ ์ฝ๊ฒ ์ดํด ๊ฐ๋๋ก ์ค๋ช ํด ์ฃผ์๋ค. ๊ทธ ๋์ ๋ฌธ์ ๋ฅผ ์ด๋ป๊ฒ ํ์ด๊ฐ ๊ฒ์ธ๊ฐ์ ๋ํ ๊ธฐ๋ณธ์ ์ธ ๋ฐฉํฅ์ ์ก์ ์ ์์๋ค.

๋ฌธ์
1. ํ ์คํธ ์ฝ๋์ ์์ฑ ๋ฒ์๋?
์ฒ์ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ค ๋ณด๋ ๋จ์ํ ์คํธ์ ํตํฉํ ์คํธ์ ์์ฑ ๋ฒ์๋ฅผ ์ ํ๋ ๊ฒ์ด ์ด๋ ค์ ๋ค.
๋ชจ๋ ๋ ์ด์ด์ ๋จ์ ํ ์คํธ๋ฅผ ๋ค ์์ฑํด์ผ ํ๋?
ํตํฉ ํ ์คํธ์ range๋ Service โ Persistance Layer ๊น์ง ์์ฑํ ๊น Presentation Layer ๊น์ง ํฌํจํ์ฌ ์์ฑํ ๊น?
Request ๊ฐ์ฒด ์์ ์๋ Validate ๋ก์ง๋ ํ ์คํธํ๊ธฐ ์ํด ๋จ์ ํ ์คํธ๋ฅผ ์์ฑํด์ผ ํ๋?
2. ๋์์ฑ ์ฒ๋ฆฌ๋ฅผ ์ํ ๋ฝ์ ์ด๋ป๊ฒ?
๋จ์ํ ์๊ณ์์ญ์ ์ง์ ํ๊ธฐ ์ํด Synchronized๋ฅผ ์ฌ์ฉํ๋ฉด ๋ ๊น? ์๋๋ฉด ๋ค๋ฅธ ๋ฐฉ๋ฒ์ ์ฌ์ฉํด์ผ ํ ๊น?
์๋
์ฐ์ ์ ํด๋ต์ ์ฐพ๊ธฐ ์ํด์๋ ๊ตฌ์ฒด์ ์ธ ๋ฐฉํฅ์ฑ์ด ํ์ํ๋ค. ํ์๋ค๊ณผ ๋ฌธ์ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ๋ํ ๊ฐ์์ ์๊ฒฌ์ ๋ฌผ์ด๋ณด๊ณ , ์กฐ์ธ์ ๋ค์ผ๋ฉฐ ๊ธฐ๋ณธ์ ์ธ ๋ฐฉํฅ์ ์ก๊ณ , ์์์ ๋งํ๋ ์ฝ์น๋๋ค์ ๋ฉํ ๋ง์ ์ฒญ๊ฐํ๋ฉด์ ์ฌ๋ฌ ์ฝ์น๋๋ค์ด ์ฃผ๋ ํํธ๋ค์ ์ ๋ง ๋ง์ด ์ฐธ๊ณ ํ๋ค.
ํด๊ฒฐ
1. ํ ์คํธ ์ฝ๋์ ์์ฑ ๋ฒ์
1) ๋จ์ ํ ์คํธ
- Presentation Layer : @WebMvcTest
์ MockMvc
์ ์ฌ์ฉํ HTTP ์์ฒญ ์๋ต ๊ฒ์ฆ
@WebMvcTest(PointController::class)
class PointControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@MockBean
private lateinit var pointService: PointService
@Test
fun `ํน์ ์ ์ ์ ํฌ์ธํธ๋ฅผ ์กฐํํ ์ ์์ด์ผ ํ๋ค`() {
// given
val userId = 1L
val expectedResponse = UserPointResponse(1L, 100L)
// when
`when`(pointService.getUserPoint(userId)).thenReturn(expectedResponse)
// then
mockMvc.perform(
get("/point/$userId"),
)
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.id", `is`(expectedResponse.id.toInt())))
.andExpect(jsonPath("$.point", `is`(expectedResponse.point.toInt())))
verify(pointService).getUserPoint(userId)
}
// ...
}
- Business Layer : ์ธ๋ถ ์์กด ๊ฐ์ฒด์ ๋ํด Fake
๋ฅผ ๋ง๋ค์ด ํด๋น Service ํด๋์ค์ ๊ธฐ๋ฅ๋ง ๊ฒ์ฆ
class PointServiceTest {
private lateinit var pointHistoryRepository: PointHistoryRepository
private lateinit var userPointRepository: UserPointRepository
private lateinit var userLockManager: IUserLockManager
private lateinit var pointValidateHelper: IPointValidateHelper
private lateinit var pointConverter: IPointConverter
private lateinit var pointHistoryQueueManager: IPointHistoryQueueManager
private lateinit var pointService: PointService
@BeforeEach
fun setup() {
pointHistoryRepository = FakePointHistoryRepository()
userPointRepository = FakeUserPointRepository()
userLockManager = FakeUserLockManager()
pointValidateHelper = FakePointValidateHelper()
pointConverter = FakePointConverter()
pointConverter = FakePointConverter()
pointHistoryQueueManager = FakePointHistoryQueueManager(pointHistoryRepository)
pointService =
PointServiceImpl(
pointHistoryRepository,
userPointRepository,
userLockManager,
pointValidateHelper,
pointConverter,
pointHistoryQueueManager,
)
}
@Test
fun `ํฌ์ธํธ๋ฅผ ์กฐํํ๋ฉด ํด๋น ์ ์ ์ ํฌ์ธํธ๋ฅผ ๋ฐํํด์ผ ํ๋ค`() {
// given
val userId = 1L
val point = 1000L
val expectedUserPoint = userPointRepository.insertOrUpdate(userId, point)
// when
val actualUserPoint = pointService.getUserPoint(userId)
// then
assertEquals(expectedUserPoint.id, actualUserPoint.id)
assertEquals(expectedUserPoint.point, actualUserPoint.point)
}
// ...
}
- ๊ทธ ์ธ : Validation ๊ธฐ๋ฅ์ ํ๋ Request ๊ฐ์ฒด, ๋์์ฑ ์ฒ๋ฆฌ๋ฅผ ์ํ LockManager
, ์์ฐจ ์ฒ๋ฆฌ๋ฅผ ์ํ QueueManager
์ ๋ํ ๊ธฐ๋ฅ ๊ฒ์ฆ
class UserLockManagerTest {
private lateinit var userLockManager: IUserLockManager
@BeforeEach
fun setup() {
userLockManager = UserLockManager()
}
@Test
fun `executeWithLock()์ ํธ์ถํ์ฌ ๋ฝ์ ์ป์์ ๋ ๋ด๋ถ ๋ก์ง์ด ์ํ๋๋ค`() {
// given
val userId = 1L
val expectedValue = true
var isExecuted = false
// when
userLockManager.executeWithLock(userId) {
isExecuted = true
}
// then
assertEquals(expectedValue, isExecuted)
}
// ...
}
2) ํตํฉ ํ ์คํธ
- Business Layer์์ @SpringBootTest
๋ฅผ ์ฌ์ฉํ์ฌ Persistance Layer ๊น์ง์ ๋์์ฑ ์ฒ๋ฆฌ ๊ฒ์ฆ
@SpringBootTest
class PointServiceIntegrationTest
@Autowired
constructor(
val pointService: PointService,
val userPointRepository: UserPointRepository,
val pointHistoryRepository: PointHistoryRepository,
) {
@Nested
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
@DisplayName("ํ๋ช
์ ์ ์ ์๊ฒ ํฌ์ธํธ ์ถฉ์ ๊ณผ ์ฌ์ฉ์ ๋์์ ํ ๋")
inner class OneUserPointTest {
private val threadPools = Executors.newFixedThreadPool(1000)
private val chargeExceptionCount = AtomicInteger(0)
private val useExceptionCount = AtomicInteger(0)
private val initPoint = 100_000L
private val operationCount = 50
@BeforeEach
fun setup() {
chargeExceptionCount.set(0)
useExceptionCount.set(0)
}
@Test
@Order(1)
@DisplayName("๋จ์ ํฌ์ธํธ๊ฐ ์ ์์ ์ผ๋ก ๋ฐํ๋๊ณ ")
fun remainingPointsMustBeCorrect() {
// given
val userId = 1L
val chargeAmount = 100L
val useAmount = 50L
userPointRepository.insertOrUpdate(userId, initPoint)
// when
repeat(operationCount) {
threadPools.execute {
pointService.chargePoint(PointRequest(userId, chargeAmount))
}
threadPools.execute {
pointService.usePoint(PointRequest(userId, useAmount))
}
}
threadPools.shutdown()
threadPools.awaitTermination(1, TimeUnit.MINUTES)
// then
val finalUserPoint = userPointRepository.selectById(userId)
val expectedPoint = initPoint + (chargeAmount * operationCount) - (useAmount * operationCount)
assertEquals(expectedPoint, finalUserPoint.point)
}
@Test
@Order(2)
@DisplayName("ํฌ์ธํธ ๋ฒ์ ์ด์ธ์ ์์ฒญ์ ์ทจ์ํ๋ฉฐ")
fun outsideRangeShouldBeCancelled() {
// given
val userId = 2L
val chargeAmount = 200_003L
val useAmount = 200_007L
userPointRepository.insertOrUpdate(userId, initPoint)
// when
repeat(operationCount) {
threadPools.execute {
try {
pointService.chargePoint(PointRequest(userId, chargeAmount))
} catch (e: Exception) {
chargeExceptionCount.incrementAndGet()
}
}
threadPools.execute {
try {
pointService.usePoint(PointRequest(userId, useAmount))
} catch (e: Exception) {
useExceptionCount.incrementAndGet()
}
}
}
threadPools.shutdown()
threadPools.awaitTermination(1, TimeUnit.MINUTES)
// then
val finalUserPoint = userPointRepository.selectById(userId)
val expectedPoint =
initPoint +
(chargeAmount * operationCount) -
(useAmount * operationCount) -
(chargeAmount * chargeExceptionCount.get()) +
(useAmount * useExceptionCount.get())
assertEquals(expectedPoint, finalUserPoint.point)
}
@Test
@Order(3)
@DisplayName("ํฌ์ธํธ ์ด๋ ฅ์๋ ์ ์์ ์ผ๋ก ์ํ๋ ์์ฒญ๋ง ๋จ๊ฒจ์ผ ํ๋ค")
fun pointHistoryMustOnlyContainSuccessfully() {
// given
val userId = 3L
val chargeAmount = 700_003L
val useAmount = 700_007L
userPointRepository.insertOrUpdate(userId, initPoint)
// when
repeat(operationCount) {
threadPools.execute {
try {
pointService.chargePoint(PointRequest(userId, chargeAmount))
} catch (e: Exception) {
chargeExceptionCount.incrementAndGet()
}
}
threadPools.execute {
try {
pointService.usePoint(PointRequest(userId, useAmount))
} catch (e: Exception) {
useExceptionCount.incrementAndGet()
}
}
}
threadPools.shutdown()
threadPools.awaitTermination(1, TimeUnit.MINUTES)
// then
val history = pointHistoryRepository.selectAllByUserId(userId)
for (h in history) {
println(h)
}
assertEquals(operationCount * 2 - chargeExceptionCount.get() - useExceptionCount.get(), history.size)
assertEquals(history.count { it.type == TransactionType.CHARGE }, operationCount - chargeExceptionCount.get())
assertEquals(history.count { it.type == TransactionType.USE }, operationCount - useExceptionCount.get())
}
}
@Nested
@DisplayName("๋๋ช
์ ์ ์ ๊ฐ ๋์์ ํฌ์ธํธ ์ถฉ์ ์ ํ ๋")
inner class TwoUserPointsTest {
private val threadPools = Executors.newFixedThreadPool(1000)
private val initPoint = 100_000L
private val operationCount = 10_000
@Test
@DisplayName("ํฌ์ธํธ ์์ก๊ณผ ์ด๋ ฅ์ด ์ ์์ ์ผ๋ก ์ ์ฅ๋์ด์ผ ํ๋ค.")
fun mustNeverBeAffectedByOtherUsersLock() {
// given
val userId1 = 4L
val userId2 = 5L
userPointRepository.insertOrUpdate(userId1, initPoint)
userPointRepository.insertOrUpdate(userId2, initPoint)
// when
repeat(operationCount) {
threadPools.execute {
pointService.chargePoint(PointRequest(userId1, 10L))
}
threadPools.execute {
pointService.chargePoint(PointRequest(userId2, 10L))
}
}
threadPools.shutdown()
threadPools.awaitTermination(1, TimeUnit.MINUTES)
// then
val userPoint1 = pointService.getUserPoint(userId1)
val userPoint2 = pointService.getUserPoint(userId2)
assertEquals(200_000, userPoint1.point)
assertEquals(200_000, userPoint2.point)
val pointHistory1 = pointService.getPointHistory(userId1)
val pointHistory2 = pointService.getPointHistory(userId2)
assertEquals(10_000, pointHistory1.size)
assertEquals(10_000, pointHistory2.size)
}
}
}
2. ๋์์ฑ ์ฒ๋ฆฌ
1. ์ ์ ๋ณ๋ก ๋ฝ์ ๊ด๋ฆฌ
- ์ด์ : ์๊ณ ์์ญ์ ํ๋์ ๋ฝ์ผ๋ก ๊ด๋ฆฌํ๊ฒ ๋๋ฉด ๋ค๋ฅธ ์ ์ ์ ์ถฉ์ , ์ฌ์ฉ ์์ ๋ฝ ํ๋ ๋๊ธฐ ๋ฐ์
- ํด๊ฒฐ๋ฐฉ๋ฒ : UserLockManager ์์ ์ ์ ๋ณ
ReentrantLock
์ ๊ด๋ฆฌํ๋ConcurrentHashMap
์์ฑ
@Component
class UserLockManager : IUserLockManager {
private val lockMap: ConcurrentHashMap<Long, ReentrantLock> = ConcurrentHashMap()
override fun <T> executeWithLock(
userId: Long,
action: () -> T,
): T {
val lock = lockMap.computeIfAbsent(userId) { ReentrantLock() }
if (!lock.tryLock(10, TimeUnit.SECONDS)) {
throw LockAcquisitionException("๋ฝ ํ๋์ ์คํจํ์ต๋๋ค. userId: $userId")
}
return try {
action()
} finally {
lock.unlock()
}
}
}
2. ํฌ์ธํธ ์ด๋ ฅ Insert๋ฅผ ์ํ ๋ณ๋ Queue ๊ด๋ฆฌ
- ์ด์ : ๋ ๋ช ์ ์ ์ ๊ฐ ํฌ์ธํธ ์ด๋ ฅ์ Insert ํ ๋, ์์์ฑ ๋ณด์ฅ์ด ์๋๋ ์ด์ (๋์์ ์กฐํํ๋ฉด ๊ฒฐ๊ด๊ฐ์ ๋ํ ๋ณด์ฅ์ด ๋์ง ์๋๋ค.)
- ํด๊ฒฐ ๋ฐฉ๋ฒ: PointHistoryQueueManager ์
BlockingQueue
๋ฅผ ํ์ฉํ์ฌ Producer/Consumer ํจํด์ผ๋ก ๊ตฌํ
@Component
class PointHistoryQueueManager(
private val pointHistoryRepository: PointHistoryRepository,
) : IPointHistoryQueueManager {
private val queue: BlockingQueue<PointHistoryQueueItem> = ArrayBlockingQueue(10_000)
init {
Thread { processQueue() }.start()
}
override fun put(history: PointHistoryQueueItem) {
queue.put(history)
}
override fun processQueue() {
while (true) {
try {
val pointRequest = queue.take()
processPointRequest(pointRequest)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
override fun processPointRequest(history: PointHistoryQueueItem) {
pointHistoryRepository.insert(history.userId, history.amount, history.type, System.currentTimeMillis())
}
override fun isQueueEmpty(): Boolean {
return queue.isEmpty()
}
}
์๊ฒ ๋ ๊ฒ
- ํ
์คํธ ์ฝ๋
- ๋จ์ ํ ์คํธ์ ๋ํ ์ธ๋ถ ์์กด์ฑ ํด๊ฒฐ์ ์ด๋ค ๋ฐฉ๋ฒ์ผ๋ก ํด์ผ ํ๋์ง์ ๋ํ ๋ฐฉํฅ์ฑ
- Test Double์ ๋ํ ๊ณต๋ถ๋ฅผ ํ๋ฉด์ ์ด๋ฒ์ Fake ๊ฐ์ฒด๋ฅผ ์ฒ์ ์ฌ์ฉํด ๋ณด์๋๋ฐ, ์ด๋ป๊ฒ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ ํ ์คํธ ์์ฑ์ผ๋ก ์ด์ด์ง๊น์ ๋ํ ์๊ฐ์ ๋ง์ด ํ๋ ๊ฒ ๊ฐ๋ค.
- ๋์์ฑ ์ฒ๋ฆฌ
- Synchronized ์ ReentrantLock์ ๋ํ ์ฐจ์ด
- ๋น์ฆ๋์ค ๋ก์ง์ ์๊ณ ์์ญ์ ์ค์ ํ ๋์ ์ฃผ์์ ์ ๋ํด ํ ์คํธ ์์ฑ์ ํตํด ๋ง์ด ์๊ฒ ๋์๋ค.
Keep
- ์ด๋ฒ ๊ณผ์ ๋ก ๋ง๋ค์ด๋ณธ ํ ์คํธ ์ฝ๋ ์์ฑ ์๋ น์ ํ ๋๋ก ํ ํ ์คํธ ์ฝ๋ ์์ฑ ์ต๊ด
- ๋น์ฆ๋์ค ๋ก์ง์์ ๋์์ฑ ๋ฌธ์ ์ ๋ํ ๊ธฐ๋ฅ, ์ฑ๋ฅ์ ๊ณ ๋ฏผ
Problem
- TDD ์ ๋ํ ์ค์ฒ ๋ถ์กฑ
- ํ ์คํธ ์ฝ๋๊ฐ ์ต์ํ์ง ์์ ๊ธฐ๋ฅ ๊ตฌํ โ ํ ์คํธ ์ฝ๋ ์์ฑ์ผ๋ก ์ด์ด์ง ๊ฒฝ์ฐ๊ฐ ๋ง์๋ค.
- ๋ถ์์ ํ๋ ๋น์ฆ๋์ค ๋ก์ง ์ปดํฌ๋ํธ ๋ณ ์ฑ ์ ๋ถ๋ฆฌ
- GitHub ๋ธ๋์น ๋ณ PR ์์ฑ ์๋ น
Try
๋ค์ ์ฃผ์ฐจ์ ๊ณผ์ ๋ถํฐ๋ TDD์ ์ฅ์ ์ ์ด๋ ค์ ๊ธฐ๋ฅ ๊ตฌํ์ ํด๋ณด๋๊ฑธ ์ค์ ์ผ๋ก ๊ฐ์ ธ๊ฐ์ผ๊ฒ ๋ค. ๋ํ ํ์ฌ์์ GitHub๋ Git์ ์ ๋๋ก ์ฌ์ฉํ์ง ์๋ค๋ณด๋ ๊ณผ์ ์ ์ถ์ ์์ด์๋ ๋ง์ด ํค๋งธ๋ ๊ฒ ๊ฐ๋ค. ๋ค๋ฅธ ์ฌ๋๋ค์ PR์ ๋ณด๋ฉด์ ์ด๋ป๊ฒ ์ ์ถํ๋๊ฒ ์ข์๊น ๊ณ ๋ฏผํด๋ด์ผ๊ฒ ๋ค.
๋ง์น๋ฉฐ

ํด๊ทผํ๊ณ ๋ ๋ค๋ฅธ ๊ณณ์ ์ ๊ฒฝ์จ์ผ ํ ์ผ์ด ๋ง๋ค ๋ณด๋ ์ค๋ก์ง ๊ฐ๋ฐ์ ๋ชฐ๋ํด ๋ณธ ์ ์ด ์ ์ฌ ์ดํ ์ฒ์์ธ ๊ฒ ๊ฐ๋ค. ๊ทธ๋งํผ ์์ผ๋ก์ ์ธ์์ ์ค์ํ ์๊ธฐ๊ฐ ๋ ๊ฒ ๊ฐ์ผ๋ ๋จ์ 9์ฃผ๋ ์ง๊ธ๊ณผ ๊ฐ์ ๋ง์์ผ๋ก ์ด์ฌํ ์ํด์ผ๊ฒ ๋ค..!
[1์ฃผ์ฐจ ๊ณผ์ GitHub] https://github.com/eastshine12/hhplus-tdd-jvm/tree/dev