[์ธํ”„๋Ÿฐ ์›Œ๋ฐ์—… ํด๋Ÿฝ 2๊ธฐ - BE] 3์ฃผ์ฐจ ๋ฐœ์ž๊ตญ

2024. 10. 20. 23:36ใ†๐Ÿ“ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ/SPRING

๋ฐ˜์‘ํ˜•

[์ธํ”„๋Ÿฐ ์›Œ๋ฐ์—… ํด๋Ÿฝ 2๊ธฐ - BE] 3์ฃผ์ฐจ ๋ฐœ์ž๊ตญ

์ด ๋ธ”๋กœ๊ทธ๋Š” ์ •๋ณด๊ทผ๋‹˜์˜ ์ž…๋ฌธ์ž๋ฅผ ์œ„ํ•œ Spring Boot with Kotlin - ๋‚˜๋งŒ์˜ ํฌํŠธํด๋ฆฌ์˜ค ์‚ฌ์ดํŠธ ๋งŒ๋“ค๊ธฐ ๊ฐ•์˜ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฝ”๋“œ์ž‘์„ฑ๊ณผ ์ฝ”๋“œ์„ค๋ช…์„ ์ ์—ˆ์Šต๋‹ˆ๋‹ค



1. controller test ์ฝ”๋“œ ๋ถ„์„

์• ๋„ˆํ…Œ์ด์…˜ ์กฐ์‚ฌ

  • @AutoConfigureMockMvc
    • MockMvc๋ฅผ ์ž๋™์œผ๋กœ ์„ค์ •ํ•ด ์ฃผ๋Š” ์• ๋„ˆํ…Œ์ด์…˜. ์ด ์• ๋„ˆํ…Œ์ด์…˜์„ ํ†ตํ•ด HTTP ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ์‘๋‹ต์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.
  • MockMvc ๋ž€ ?
    • ์‹ค์ œ๋กœ ์„œ๋ฒ„๋ฅผ ๋„์šฐ์ง€ ์•Š๊ณ  ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋Š” ๋„๊ตฌ
  • @DisplayName("Test")
    • ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค๋ฅผ ์‹œ์ž‘ํ•  ๋•Œ ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค์˜ ์ด๋ฆ„์„ ์ง€์ •ํ•ด์„œ ํ…Œ์ŠคํŠธ ๋ฆฌํฌํŠธ์— ํ‘œ์‹œ๋œ๋‹ค.
    • ์œ„ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰์‹œํ‚ค๋ฉด ๋ฆฌํฌํŠธ์— TEST ๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ํ…Œ์ŠคํŠธ ์„ฑ๊ณต์œ ๋ฌด๊ฐ€ ํ‘œ์‹œ๋จ.
  • @Configuration : Spring์—์„œ Bean์„ ์ˆ˜๋™์œผ๋กœ ๋“ฑ๋กํ•˜๊ธฐ ์œ„ํ•ด์„œ ์‚ฌ์šฉ

๋ฉ”์„œ๋“œ ๋ถ„์„

 

@Test
@DisplayName("Introductions ์กฐํšŒ")
fun testGetIntroductions() {
    val uri = "/api/v1/introductions"

    val mvcResult = performGet(uri)
    val contentAsString = mvcResult.response.getContentAsString(StandardCharsets.UTF_8)
    val jsonArray = JSONArray(contentAsString)

    assertThat(jsonArray.length()).isPositive()
}
  • api/vi/introduction์„ uri ๋ณ€์ˆ˜์— ๋„ฃ์Œ.
  • HTTP GET ์š”์ฒญ์„ ๋ณด๋‚ธํ›„, ์‘๋‹ต์„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•œ ํ›„์— JSONArray๋กœ ๋ณ€ํ™˜
  • JSONArray์˜ ๊ธธ์ด๊ฐ€ 0๋ณด๋‹ค ํฐ์ง€ ๊ฒ€์ฆ, ์‘๋‹ต ๋ฐ์ดํ„ฐ๊ฐ€ ๋น„์–ด ์žˆ์ง€ ์•Š์Œ์„ ํ™•์ธ

 

@Test
@DisplayName("Link ์กฐํšŒ")
fun testGetLinks() {
    val uri = "/api/v1/links"

    val mvcResult = performGet(uri)
    val contentAsString = mvcResult.response.getContentAsString(StandardCharsets.UTF_8)
    val jsonArray = JSONArray(contentAsString)

    assertThat(jsonArray.length()).isPositive()
}
  • api/vi/links์„ uri ๋ณ€์ˆ˜์— ๋„ฃ์Œ
  • HTTP GET ์š”์ฒญ์„ ๋ณด๋‚ธํ›„, ์‘๋‹ต์„ JSONArray๋กœ ๋ณ€ํ™˜
  • ๋ฐฐ์—ด์˜ ํฌ๊ธฐ์™€, ๊ทธ ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ๊ฒ€์ฆ
@Test
@DisplayName("Resume ์กฐํšŒ")
fun testGetResume() {
    val uri = "/api/v1/resume"

    val mvcResult = performGet(uri)
    val contentAsString = mvcResult.response.getContentAsString(StandardCharsets.UTF_8)
    val jsonObject = JSONObject(contentAsString)

    assertThat(jsonObject.optJSONArray("experiences").length()).isPositive()
    assertThat(jsonObject.optJSONArray("achievements").length()).isPositive()
    assertThat(jsonObject.optJSONArray("skills").length()).isPositive()
}
  • /api/vi/resume๋ฅผ uri๋ณ€์ˆ˜์— ๋„ฃ์Œ
  • GET์š”์ฒญ์„ ๋ณด๋‚ธํ›„, ์‘๋‹ต์„ JSONObject๋กœ ๋ณ€ํ™˜
  • experiences, achievements,skills๋ฅผ JSONArray๋กœ ๋ณ€ํ™˜ ํ›„ ๊ทธ ๊ฐ’์ด ์–‘์ˆ˜์ธ์ง€์™€ ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ๊ฒ€์ฆ
@Test
@DisplayName("Projects ์กฐํšŒ")
fun testProjects() {
    val uri = "/api/v1/projects"

    val mvcResult = performGet(uri)
    val contentAsString = mvcResult.response.getContentAsString(StandardCharsets.UTF_8)
    val jsonArray = JSONArray(contentAsString)

    assertThat(jsonArray.length()).isPositive()
}
  • /api/vi/projects๋ฅผ uri ๋ณ€์ˆ˜์— ๋„ฃ์Œ
  • GET์š”์ฒญ์„ ๋ณด๋‚ธํ›„ ์‘๋‹ต ๋ณธ๋ฌธ์„ JSONArray๋กœ ๋ณ€ํ™˜ํ•˜๊ณ , ๋ฐฐ์—ด์˜ ๊ธธ์ด๊ฐ€ ์–‘์ˆ˜์ธ์ง€์™€ ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ๊ฒ€์ฆ
private fun performGet(uri: String): MvcResult {
    return mockMvc
        .perform(MockMvcRequestBuilders.get(uri))
        .andDo(MockMvcResultHandlers.print())
        .andReturn()
}
  • perform(MockMvcRequestBuilders.get(uri)) : ํŠน์ • uri์— GET ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ์ฝ”๋“œ. ์„œ๋ฒ„๋ฅผ ๋„์šฐ์ง€ ์•Š๊ณ  API ์—”๋“œํฌ์ธํŠธ์˜ ๋™์ž‘์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Œ
  • andDo(MockMvcResultHandlers.print()) : ์š”์ฒญ๊ณผ ์‘๋‹ต์„ ์ฝ˜์†”์— ์ถœ๋ ฅ
  • andReturn() : MvcResult๋ฅผ ๋ฐ˜ํ™˜

2. ๋ถ€๋ถ„ ์ฝ”๋“œ ๋ถ„์„


  • Thymeleaf ํ…œํ”Œ๋ฆฟ ์—”์ง„์—์„œ ๋ ˆ์ด์•„์›ƒ์„ ์ •์˜ํ•˜๊ณ  ํ•ด๋‹น ๋ ˆ์ด์•„์›ƒ์„ ์žฌ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹, ํ…œํ”Œ๋ฆฟ ์ฝ”๋“œ์˜ ์ค‘๋ณต์„ ์ค„์ด๊ณ , HTML ํŒŒ์ผ ๊ฐ„์— ๊ณตํ†ต ์š”์†Œ๋ฅผ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ๊ฒƒ.

 

th:replace="~{presentation/layouts/layout-main :: layout(~{::#content})}"
  • th:replace : ๋‹ค๋ฅธ ํŒŒ์ผ์„ ๊ฐ€์ ธ์™€ ํ˜„์žฌ ์œ„์น˜์— ์‚ฝ์ž…ํ•˜๋Š” ๊ธฐ๋Šฅ ์ˆ˜ํ–‰
  • ~{presentation/layouts/layout-main :: layout(~{::#content})} :
    • ~{presentation/layouts/layout-main} : layout-main.html ํŒŒ์ผ ์ฐธ์กฐ
    • :: layout : layout ์ด๋ผ๋Š” ์ด๋ฆ„์˜ fragment๋ฅผ ์‚ฌ์šฉํ•˜๊ฒ ๋‹ค๋Š” ์˜๋ฏธ, ์œ„ ์ฝ”๋“œ์—์„œ๋Š” layout fragment๋ฅผ ๊ฐ€์ ธ์˜ค๊ฒ ๋‹ค๋Š” ๋œป
    • (~{::#content}) : id="content" ๋กœ ์ง€์ •๋œ ๋ถ€๋ถ„์„ layout fragment์˜ ํŠน์ • ์œ„์น˜์— ๋Œ€์ฒดํ•  ๊ฒƒ์ด๋ผ๋Š” ์˜๋ฏธ

3. interceptor ์ฝ”๋“œ ๋ถ„์„

@Component
class PresentationInterceptor(
        private val httpInterfaceRepository: HttpInterfaceRepository
) : HandlerInterceptor {
    override fun afterCompletion(request: HttpServletRequest, response: HttpServletResponse, handler: Any, ex: Exception?) {
        val httpInterface = HttpInterface(request)
        httpInterfaceRepository.save(httpInterface)
    }
}
  • private val httpInterfaceRepository: HttpInterfaceRepository
    • ์˜์กด์„ฑ ์ฃผ์ž…์„ ํ†ตํ•ด์„œ Repository๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
  • override fun afterCompletion(request: HttpServletRequest, response: HttpServletResponse, handler: Any, ex: Exception?) {
    • afterCompletion : HTTP ์š”์ฒญ ์ฒ˜๋ฆฌ ํ›„์— ํ˜ธ์ถœ๋˜๋Š” ๋ฉ”์„œ๋“œ
  • val httpInterface = HttpInterface(request)
    • request ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” httpInterface ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑ ํ›„, httpInterfaceRepository์— ์ €์žฅ.
  • httpInterfaceRepository.save(httpInterface)

 

@Configuration
class PresentationInterceptorConfiguration(
        private val presentationInterceptor: PresentationInterceptor
) : WebMvcConfigurer {
    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(presentationInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/assets/**", "/css/**", "/js/**", "/admin/**", "h2**",
                        "/favicon.ico", "/error")
    }
}
  • addInterceptors
    • ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ๋“ฑ๋กํ•˜๋Š” ๋ฉ”์„œ๋“œ
  • registry.addInterceptor(presentationInterceptor)
    • addInterceptor ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด presentationInterceptor๋ฅผ ๋“ฑ๋ก
  • addPathPatterns("/**")
    • ๋ชจ๋“  ์š”์ฒญ ๊ฒฝ๋กœ์— ๋Œ€ํ•ด ์ธํ„ฐ์…‰ํ„ฐ ์ ์šฉ
  • excludePathPatterns(...) : ์ธํ„ฐ์…‰ํ„ฐ๊ฐ€ ๋™์ž‘ํ•˜์ง€ ์•Š๋„๋ก ์„ค์ •
    • /assets/**: ์ •์  ์ž์›(์˜ˆ: ์ด๋ฏธ์ง€, ํฐํŠธ ๋“ฑ).
    • /css/**: CSS ํŒŒ์ผ.
    • /js/**: JavaScript ํŒŒ์ผ.
    • /admin/**: ๊ด€๋ฆฌ ํŽ˜์ด์ง€.
    • h2**: H2 ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฝ˜์†”.
    • /favicon.ico: ์‚ฌ์ดํŠธ ์•„์ด์ฝ˜.
    • /error: ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๊ฒฝ๋กœ.

[๋ฏธ์…˜4] ์กฐํšŒ REST API ๋งŒ๋“ค๊ธฐ-ํšŒ๊ณ 

์ด๋ฒˆ ๋ฏธ์…˜์„ ํ•˜๋ฉด์„œ @GetMapping ์–ด๋…ธํ…Œ์ด์…˜์— ๋Œ€ํ•ด์„œ๋Š” ์–ด๋А์ •๋„ ์ดํ•ด๋ฅผ ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ์ €๋ฒˆ ๋ฐœ์ž๊ตญ์—์„œ @Id, @GeneratedValue ์–ด๋…ธํ…Œ์ด์…˜์— ๋Œ€ํ•ด์„œ ์ ์—ˆ์ง€๋งŒ ์•„์ง ๋ถ€์กฑํ•˜๋‹ค๋Š” ๊ฑธ ์•Œ ์ˆ˜ ์žˆ์—ˆ๊ณ , @ManyToOne ์–ด๋…ธํ…Œ์ด์…˜์— ๋Œ€ํ•ด ๋” ๊นŠ์ด ๊ณต๋ถ€ํ•ด์•ผ๊ฒ ๋‹ค๋Š”๊ฑธ ๊นจ๋‹ฌ์•˜๋‹ค. ์•„์ง Spring์— ๋ณธ์งˆ์ ์ธ๊ฑธ ์ดํ•ด๋ฅผ ๋ชปํ•œ๊ฑธ์ˆ˜๋„ ์žˆ๋Š”๊ฑฐ ๊ฐ™๋‹ค. ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด ํ• ์ˆ˜๋ก ๋” ๊นŠ๊ฒŒ ๊ณต๋ถ€๋ฅผ ํ•ด์•ผํ–ˆ๊ณ , JAVA ๋ฌธ๋ฒ•๋„ ๋‹ค์‹œ ํ•ด์•ผ๊ฒ ๋‹ค๋Š”๊ฑธ ๋ผˆ์ €๋ฆฌ๊ฒŒ ๋А๋ผ๋ฉด์„œ ํ•œ๊ฒƒ ๊ฐ™๋‹ค...


3์ฃผ์ฐจ ํšŒ๊ณ 

Spring์€ ํ•˜๋ฉด ํ• ์ˆ˜๋ก ์žฌ๋ฐŒ๋Š”๊ฑด ๋งž๋‹ค. Test ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ๋„ ์ด๋ž˜์„œ ์ด์ฝ”๋“œ๊ฐ€ ์ž‘๋™์ด ๋˜๋Š”๊ฒƒ๋„ ์•Œ ์ˆ˜ ์žˆ๊ณ , ๋ถ€๋ถ„์ ์ธ ๊ฑธ ๋”ฐ๋กœ ๋ชจ๋“ˆํ™”? ํ•ด์„œ ๋งŒ๋“œ๋Š”๊ฒƒ๋„ ์žฌ๋ฐŒ๋‹ค. ์ด๋ฒˆ ์›Œ๋ฐ์—… ํด๋Ÿฝ๋„ ๊ณง ์ข…๋ฃŒ๊ฐ€ ๋˜๋Š”๋ฐ java์˜ ์ค‘์š”์„ฑ๋„ ๊นจ๋‹ฌ์•„์„œ java๊ณต๋ถ€๋„ ์—ด์‹ฌํžˆ ํ•˜๊ณ , Spring์— ๋Œ€ํ•ด์„œ ๋” ๊นŠ๊ฒŒ ๊ณต๋ถ€ํ• ๊ฑฐ๋‹ค!

์ฐธ๊ณ ๋กœ ์ค‘๊ฐ„์ ๊ฒ€ ๋•Œ ์ •๋ณด๊ทผ๋‹˜๊ป˜์„œ ๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ์งˆ๋ฌธ์— ๋Œ€ํ•ด ๋‹ต๋ณ€์„ ํ•ด์ฃผ์‹ ๊ฑฐ ๊ฐ™๋‹ค. ๋‚˜์˜ ์งˆ๋ฌธ์€ ์ด๋ฒˆ ์›Œ๋ฐ์—… ํด๋Ÿฝ์ด ์ข…๋ฃŒ๊ฐ€ ๋˜๋ฉด java์˜ ์ •์„, spring๊ณต๋ถ€๋ฅผ ํ• ๊ฑด๋ฐ ์ถ”์ฒœํ•ด์ค„ ๊ฐ•์˜๊ฐ€ ์žˆ๋ƒ ๋ผ๋Š” ์งˆ๋ฌธ์ด์—ˆ๋‹ค. ๋‹ต๋ณ€์€ ์—ญ์‹œ ๊น€์˜ํ•œ๋‹˜์˜ ๊ฐ•์˜์˜€๊ณ  ๋‹ค๋ฅธ ๊ฐ•์˜๋„ ์ถ”์ฒœํ•ด์ฃผ์…จ๋Š”๋ฐ ์ด๊ฑฐ๋Š” ํ™•์ธํ•ด๋ณด๊ณ  ๊ธ€์„ ์ˆ˜์ •ํ•ด์•ผ๊ฒ ๋‹ค...! ๋ฌดํŠผ, spring์„ ์„ ํƒํ•œ๊ฑด ์ž˜ํ•œ ์ผ ๊ฐ™๋‹ค...!

๋ฐ˜์‘ํ˜•