TDD, ๋™์‹œ์„ฑ ์ฒ˜๋ฆฌ - ํ•ญํ•ด ํ”Œ๋Ÿฌ์Šค ํšŒ๊ณ  (1์ฃผ์ฐจ)

2024. 9. 29. 14:53ยท๐Ÿ  ์ผ์ƒ/๐Ÿ““ ์ผ์ƒ ์ผ๊ธฐ

 

 

๋“ค์–ด๊ฐ€๋ฉฐ

 

๋“œ๋””์–ด 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์„ ๋ณด๋ฉด์„œ ์–ด๋–ป๊ฒŒ ์ œ์ถœํ•˜๋Š”๊ฒŒ ์ข‹์„๊นŒ ๊ณ ๋ฏผํ•ด๋ด์•ผ๊ฒ ๋‹ค.

 

 

 

 

๋งˆ์น˜๋ฉฐ

 

[์ถœ์ฒ˜] https://pin.it/e2tA7ydBN

 

ํ‡ด๊ทผํ•˜๊ณ ๋„ ๋‹ค๋ฅธ ๊ณณ์— ์‹ ๊ฒฝ์จ์•ผ ํ•  ์ผ์ด ๋งŽ๋‹ค ๋ณด๋‹ˆ ์˜ค๋กœ์ง€ ๊ฐœ๋ฐœ์— ๋ชฐ๋‘ํ•ด ๋ณธ ์ ์ด ์ž…์‚ฌ ์ดํ›„ ์ฒ˜์Œ์ธ ๊ฒƒ ๊ฐ™๋‹ค. ๊ทธ๋งŒํผ ์•ž์œผ๋กœ์˜ ์ธ์ƒ์— ์ค‘์š”ํ•œ ์‹œ๊ธฐ๊ฐ€ ๋  ๊ฒƒ ๊ฐ™์œผ๋‹ˆ ๋‚จ์€ 9์ฃผ๋„ ์ง€๊ธˆ๊ณผ ๊ฐ™์€ ๋งˆ์Œ์œผ๋กœ ์—ด์‹ฌํžˆ ์ž„ํ•ด์•ผ๊ฒ ๋‹ค..!

 

[1์ฃผ์ฐจ ๊ณผ์ œ GitHub] https://github.com/eastshine12/hhplus-tdd-jvm/tree/dev

 

 

์ €์ž‘์žํ‘œ์‹œ (์ƒˆ์ฐฝ์—ด๋ฆผ)

'๐Ÿ  ์ผ์ƒ > ๐Ÿ““ ์ผ์ƒ ์ผ๊ธฐ' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

ํ•ญํ•ด ํ”Œ๋Ÿฌ์Šค ์ตœ์ข… ํšŒ๊ณ  (+์ฃผ์ฐจ๋ณ„ ํ•™์Šต๋‚ด์šฉ ์ •๋ฆฌ)  (1) 2024.12.15
Chapter 2๋ฅผ ๋Œ์•„๋ณด๋ฉฐ - ํ•ญํ•ด ํ”Œ๋Ÿฌ์Šค ํšŒ๊ณ  (5์ฃผ์ฐจ)  (2) 2024.10.25
์š”๊ตฌ์‚ฌํ•ญ ๋ถ„์„(์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ, ERD, API ์„ค๊ณ„) - ํ•ญํ•ด ํ”Œ๋Ÿฌ์Šค ํšŒ๊ณ  (3์ฃผ์ฐจ)  (2) 2024.10.15
Clean Architecture - ํ•ญํ•ด ํ”Œ๋Ÿฌ์Šค ํšŒ๊ณ  (2์ฃผ์ฐจ)  (1) 2024.10.06
ํ•ญํ•ด ํ”Œ๋Ÿฌ์Šค ๋ฐฑ์—”๋“œ 6๊ธฐ - ํšŒ๊ณ  (0์ฃผ์ฐจ)  (1) 2024.09.22
'๐Ÿ  ์ผ์ƒ/๐Ÿ““ ์ผ์ƒ ์ผ๊ธฐ' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€
  • Chapter 2๋ฅผ ๋Œ์•„๋ณด๋ฉฐ - ํ•ญํ•ด ํ”Œ๋Ÿฌ์Šค ํšŒ๊ณ  (5์ฃผ์ฐจ)
  • ์š”๊ตฌ์‚ฌํ•ญ ๋ถ„์„(์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ, ERD, API ์„ค๊ณ„) - ํ•ญํ•ด ํ”Œ๋Ÿฌ์Šค ํšŒ๊ณ  (3์ฃผ์ฐจ)
  • Clean Architecture - ํ•ญํ•ด ํ”Œ๋Ÿฌ์Šค ํšŒ๊ณ  (2์ฃผ์ฐจ)
  • ํ•ญํ•ด ํ”Œ๋Ÿฌ์Šค ๋ฐฑ์—”๋“œ 6๊ธฐ - ํšŒ๊ณ  (0์ฃผ์ฐจ)
EastShine_
EastShine_
๋” ๋‚˜์€ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜๊ธฐ ์œ„ํ•œ ๋‚˜์˜ ๊ธฐ๋ก ๐Ÿ“
  • EastShine_
    ๊ฐœ๋ฐœ.LOG ๐Ÿ’ป
    EastShine_
  • ์ „์ฒด
    ์˜ค๋Š˜
    ์–ด์ œ
  • 05-28 05:44
    • ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ (27)
      • ๐Ÿ’ป ๊ฐœ๋ฐœ (21)
        • ๐Ÿ–ฅ๏ธ ์šด์˜์ฒด์ œ (3)
        • ๐ŸŒ ๋„คํŠธ์›Œํฌ (0)
        • ๐Ÿ’พ Database (3)
        • ๐ŸŽ› Java (0)
        • ๐Ÿ–ฒ Javascript (0)
        • ๐Ÿ€ Spring (5)
        • ๐ŸŽธ ETC (4)
        • ๐Ÿ“ˆ ์•Œ๊ณ ๋ฆฌ์ฆ˜ (3)
        • ๐Ÿ“– TIL (Today I Learned) (3)
      • ๐Ÿ  ์ผ์ƒ (6)
        • ๐Ÿ““ ์ผ์ƒ ์ผ๊ธฐ (6)
  • ์ธ๊ธฐ ๊ธ€

  • ํƒœ๊ทธ

    ํ•ญํ•ดํ”Œ๋Ÿฌ์Šค
    ๋™์‹œ์„ฑ์ฒ˜๋ฆฌ
    ๋‚™๊ด€์ ๋ฝ
    ํšŒ๊ณ 
    ์ฝ”๋”ฉํ…Œ์ŠคํŠธ
    ํ”„๋กœ๊ทธ๋ž˜๋จธ์Šค
    ๋Œ€๊ธฐ์—ด
    6๊ธฐ
    ํŠธ๋žœ์žญ์…˜ ๋ถ„๋ฆฌ
    Python
    spring
    redis
    ์ฝ˜์„œํŠธ์˜ˆ์•ฝ์„œ๋น„์Šค
    ๋ฐฑ์—”๋“œ
    transactionaleventlistener
    ๋น„๊ด€์ ๋ฝ
    Whisper API
    e-book pdf ๋ณ€ํ™˜
    ์•Œ๊ณ ๋ฆฌ์ฆ˜
    e-book pdf ์ถ”์ถœ
  • ์ตœ๊ทผ ๋Œ“๊ธ€

  • ์ตœ๊ทผ ๊ธ€

  • hELLOยท Designed By์ •์ƒ์šฐ.v4.10.1
EastShine_
TDD, ๋™์‹œ์„ฑ ์ฒ˜๋ฆฌ - ํ•ญํ•ด ํ”Œ๋Ÿฌ์Šค ํšŒ๊ณ  (1์ฃผ์ฐจ)
์ƒ๋‹จ์œผ๋กœ

ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”