๐Ÿ”ฅ TIL - Day 70 Kotlin & Springboot 01 CRUD Rest API ๊ตฌํ˜„ ๋ฐ Junit ํ…Œ์ŠคํŠธ

Kim Dae Hyunยท2021๋…„ 12์›” 12์ผ
0

TIL

๋ชฉ๋ก ๋ณด๊ธฐ
81/93

์ „์ฒด์†Œ์Šค Github

์ฒ˜์Œ ์ ‘ํ•˜๋Š” ์–ธ์–ด์ด๊ณ  ๋‹ค๋ฅธ ๊ทธ๋Œ€๋กœ ์ฝ”๋“œ๋ฅผ ๊ฐ–๋‹ค ์“ด ๊ฒƒ์ด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ์˜ค๋ฅ˜๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“Œ ๋„๋ฉ”์ธ ์„ค๊ณ„

์ œ๋ชฉ, ๋‚ด์šฉ์„ ๊ฐ–๋Š” Article ํด๋ž˜์Šค๋ฅผ ์ •์˜ํ•œ๋‹ค.
kotlin์˜ ๊ธฐ๋ณธ์ ์ธ ๋ฌธ๋ฒ•์„ ์•Œ์•„๋ณด๊ธฐ ์œ„ํ•จ์ด๋ฏ€๋กœ DB๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  Pojoํ•œ ํด๋ž˜์Šค๋กœ ์ž‘์„ฑํ•œ๋‹ค.

data class Article(
    val title: String,
    val content: String,
)

Kotlin์˜ ์ฒซ๋ฒˆ์งธ ๋งค๋ ฅ ํฌ์ธํŠธ...
์œ„ ํด๋ž˜์Šค๋Š” ์ƒ์„ฑ์ž, Getter, Equals And HashCode, ToString์„ ๋ชจ๋‘ ํฌํ•จํ•œ๋‹ค.
body(์ค‘๊ด„ํ˜ธ)๊ฐ€ ์—†๋Š”๊ฒŒ ๊ต‰์žฅํžˆ ์ธ์ƒ์ ์ด๋‹ค.

primary constructor ํ•„๋“œ์˜ ํƒ€์ž…์„ val๋กœ ์ง€์ •ํ•˜๋ฉด ํ•ด๋‹น ํ•„๋“œ๋ฅผ publicํ•˜๊ฒŒ ์ฝ์„ ์ˆ˜ ์žˆ๋‹ค. ์‹ค์ œ๋กœ Java class ํŒŒ์ผ๋กœ ๋””์ปดํŒŒ์ผ ๋œ ๊ฒฐ๊ณผ ๋˜ํ•œ Getter๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค.

var๋กœ ์ž‘์„ฑํ•˜๋Š” ๊ฒฝ์šฐ ํ•ด๋‹น ํ•„๋“œ๋ฅผ ์™„์ „ํžˆ publicํ•˜๊ฒŒ ์—ด์–ด๋‘๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•˜๋‹ค. ์ฝ๊ธฐ์™€ ์ˆ˜์ • ๋ชจ๋‘ ํ•„๋“œ์— ์ง์ ‘ ์ ‘๊ทผํ•˜์—ฌ ์ˆ˜ํ–‰ ๊ฐ€๋Šฅํ•˜๋‹ค. (์‹ค์ œ Java ์ฝ”๋“œ๋กœ๋Š” getter, setter๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค)

primary constructor์—์„œ var๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ์ชผ๊ธˆ ์œ„ํ—˜ํ•  ์ˆ˜๋„?
์—”ํ‹ฐํ‹ฐ์˜ ๋ณ€๊ฒฝ์€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋ฐ˜์˜ํ•œ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์„ ๋“ฏ ํ•˜๋‹ค.

๋‹ค์Œ์œผ๋กœ class ์•ž์— ๋ถ™์€ data ํ‚ค์›Œ๋“œ๋Š” Equals And HashCode, ToString๋ฅผ ์ œ๊ณตํ•œ๋‹ค. Lombok์˜ @Data ์™€ ๋น„์Šทํ•œ ๋Š๋‚Œ์ด๋‹ค. ์ด ์—ญ์‹œ ๊ต‰์žฅํžˆ ํŽธ๋ฆฌํ•˜์ง€๋งŒ ์œ„ ํด๋ž˜์Šค๊ฐ€ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ๋œ๋‹ค๋ฉด ์ฃผ์˜ํ•ด์•ผ ํ•  ๊ฒƒ์ด๋‹ค.


๐Ÿ“Œ InMemoryRepository ๊ตฌํ˜„

๋ฉ”๋ชจ๋ฆฌ(Map)์„ ์ด์šฉํ•ด์„œ Repository๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด์ž.

@Repository
class ArticleRepositoryImpl : ArticleRepository {
    var sequence: Long = 0L
    var store = mutableMapOf<Long, Article>(
        ++sequence to Article("article1", "content1"),
        ++sequence to Article("article2", "content2"),
        ++sequence to Article("article3", "content3"),
    )
}

Map์˜ ๊ฒฝ์šฐ ์ˆ˜์ •์ด ๊ฐ€๋Šฅํ•˜๋„๋ก mutableMap์„ ์‚ฌ์šฉํ–ˆ๋‹ค.

CRUD ๊ตฌํ˜„

getArticle์˜ ๋ฆฌํ„ดํƒ€์ž…์ด Article?์ด๋‹ค.
Kotlin์˜ ์žฅ์  ์ค‘ ํ•˜๋‚˜์ธ null safety์˜ ํ•œ ๋ถ€๋ถ„์ธ๋ฐ Java์˜ Optional๊ณผ ๋น„์Šทํ•œ ๊ฐœ๋…์œผ๋กœ ์ƒ๊ฐํ•˜๋ฉด ๋  ๊ฒƒ ๊ฐ™๋‹ค.

Java์˜ ๊ฒฝ์šฐ null ๊ฐ์ฒด์— length ํ˜น์€ getter ๋“ฑ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค๋ฉด NullPointException(NPE)์ด ๋ฐœ์ƒ๋œ๋‹ค. ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ๊ฒƒ์€ null-safety ์ด๋‹ค.

๋‚˜์ค‘์— ํ…Œ์ŠคํŠธ ๋ถ€๋ถ„์—์„œ ๋‚˜์˜ค๊ฒ ์ง€๋งŒ Article?๋กœ ๋ฆฌํ„ด๋œ ๊ฐ์ฒด์— NPE๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์ƒํ™ฉ์ด ์ƒ๊ธด๋‹ค๋ฉด ํ•ด๋‹น ๋ถ€๋ถ„์„ ๋ฌด์‹œํ•œ ์ฑ„ ์ง„ํ–‰ํ•œ๋‹ค.

@Repository
class ArticleRepositoryImpl : ArticleRepository {
    var sequence:  Long = 0L
    var store = mutableMapOf<Long, Article>(
        ++sequence to Article("article1", "content1"),
        ++sequence to Article("article2", "content2"),
        ++sequence to Article("article3", "content3"),
    )

    override fun getArticles() = this.store.values

    override fun getArticle(articleId: Long): Article? = this.store.get(articleId)

    override fun saveArticle(articleDto: ArticleRequestDto) {
        val article = Article(articleDto.title, articleDto.content)
        store[++sequence] = article
    }

    override fun deleteArticle(articleId: Long) {
        if (store.containsKey(articleId)) {
            store.remove(articleId)
        } else {
            throw IllegalArgumentException("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒŒ์‹œ๊ธ€์ž…๋‹ˆ๋‹ค.")
        }
    }

    override fun updateArticle(articleId: Long, articleDto: ArticleRequestDto) {
        if (store.containsKey(articleId)) {
            store[articleId] = Article(articleDto.title, articleDto.content)
        } else {
            throw IllegalArgumentException("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒŒ์‹œ๊ธ€์ž…๋‹ˆ๋‹ค.")
        }
    }
}

CURD ํ…Œ์ŠคํŠธ

internal class ArticleRepositoryTest {

    private val articleRepository = ArticleRepositoryImpl()

    @DisplayName("1. Article ์ „์ฒด์กฐํšŒ")
    @Test
    fun getArticles() {
        val articles = articleRepository.getArticles()

        assertEquals(3, articles.size)
    }

    @DisplayName("2. Article ๋‹จ๊ฑด์กฐํšŒ")
    @Test
    fun getArticle() {
        val article = articleRepository.getArticle(1L)

        assertEquals("article1", article?.title)
    }

    @DisplayName("3. Article ๋‹จ๊ฑด์กฐํšŒ (์กด์žฌํ•˜์ง€ ์•Š๋Š” id)")
    @Test
    fun failedGetArticle() {
        val article = articleRepository.getArticle(4L)

        assertEquals(null, article?.title)
    }

    @Test
    @DisplayName("4. Article ์ถ”๊ฐ€")
    fun addArticle() {
        val articleDto = ArticleRequestDto("article4", "content4")

        articleRepository.saveArticle(articleDto)

        assertEquals(4, articleRepository.store.size)
    }

    @Test
    @DisplayName("5. Article ์‚ญ์ œ")
    fun deleteArticle() {
        // Given
        val deleteArticleId = 1L

        // When
        articleRepository.deleteArticle(deleteArticleId)

        // Then
        assertEquals(2, articleRepository.store.size)
    }

    @Test
    @DisplayName("6. Article ์ˆ˜์ •")
    fun updateArticle() {
        // Given
        val updateArticleId = 1L
        val updateArticleDto = ArticleRequestDto("updatedTitle", "updatedContent")

        // When
        articleRepository.updateArticle(updateArticleId, updateArticleDto)

        // Then
        assertEquals(3, articleRepository.store.size)
        assertEquals("updatedTitle", articleRepository.store[updateArticleId]?.title)
    }
}

๐Ÿ“Œ Service ๊ณ„์ธต ๊ตฌํ˜„

๊น”___๋”
๋ฌธ๋ฒ•์ ์œผ๋กœ ์ •๋ง ๋„ˆ๋ฌด ์ข‹๋‹ค.

@Service
class ArticleService(private val articleRepository : ArticleRepositoryImpl) {

    /**
     * ์ „์ฒด Article ์กฐํšŒ
     */
    fun getArticles() = articleRepository.getArticles()

    /**
     * Article ๋‹จ๊ฑด์กฐํšŒ
     */
    fun getArticle(articleId: Long) = articleRepository.getArticle(articleId)

    /**
     * Article ์ถ”๊ฐ€
     */
    fun saveArticle(articleDto: ArticleRequestDto) = articleRepository.saveArticle(articleDto)


    /**
     * Article ์‚ญ์ œ
     */
    fun deleteArticle(articleId: Long) = articleRepository.deleteArticle(articleId)

    /**
     * Article ์ˆ˜์ •
     */
    fun updateArticle(articleId: Long, articleDto: ArticleRequestDto) = articleRepository.updateArticle(articleId, articleDto)
}

๐Ÿ“Œ Controller ๊ณ„์ธต ๊ตฌํ˜„

@RequestMapping("/api/articles")
@RestController
class ArticleController(
    private val articleService: ArticleService
) {

    @GetMapping
    fun getArticles() = articleService.getArticles()

    @GetMapping("/{articleId}")
    fun getArticle(@PathVariable articleId: Long) = articleService.getArticle(articleId)

    @PostMapping
    fun postArticle(@RequestBody articleDto : ArticleRequestDto) : ResponseEntity<Any> {
        articleService.saveArticle(articleDto)
        return ResponseEntity(HttpStatus.CREATED)
    }

    @DeleteMapping("/{articleId}")
    fun deleteArticle(@PathVariable articleId: Long) = articleService.deleteArticle(articleId)

    @PutMapping("/{articleId}")
    fun updateArticle(
        @PathVariable articleId: Long,
        @RequestBody articleDto: ArticleRequestDto
    ) = articleService.updateArticle(articleId, articleDto)
}

Controller ํ…Œ์ŠคํŠธ

@AutoConfigureMockMvc
@SpringBootTest
internal class ArticleControllerTest {

    @Autowired
    lateinit var mockMvc: MockMvc

    @DisplayName("Article ์ „์ฒด์กฐํšŒ API")
    @Test
    fun ์ „์ฒด์กฐํšŒ() {
        mockMvc.get("/api/articles")
            .andExpect {
                content { contentType(MediaType.APPLICATION_JSON) }
                status { isOk() }
            }
            .andDo {
                print()
            }
    }

    @DisplayName("Article ๋‹จ๊ฑด์กฐํšŒ API")
    @Test
    fun ๋‹จ๊ฑด์กฐํšŒ() {
        mockMvc.get("/api/articles/{articleId}", 1L)
            .andExpect {
                status { isOk()}
                content {contentType(MediaType.APPLICATION_JSON)}
                jsonPath("$.title") { "article1" }
            }.andDo {
                print()
            }
    }

    @DisplayName("Article ์ €์žฅ API")
    @Test
    fun ์ถ”๊ฐ€() {
        val articleDto = ArticleRequestDto("article4", "content4")
        val articleDtoJson:String = Gson().toJson(articleDto)

        mockMvc.post("/api/articles") {
            content = articleDtoJson
            contentType = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isCreated() }
        }.andDo {
            print()
        }
    }

    @DisplayName("Article ์‚ญ์ œ API")
    @Test
    fun ์‚ญ์ œ() {
        mockMvc.delete("/api/articles/{articleId}", 3L)
            .andExpect {
                status { isOk() }
            }
            .andDo { print() }
    }

    @Test
    @DisplayName("Article ์ˆ˜์ • API")
    fun ์ˆ˜์ •() {
        val updateRequestDto = ArticleRequestDto("updatedTitle", "updatedContent")
        val updateRequestDtoJosn = Gson().toJson(updateRequestDto)
        mockMvc.put("/api/articles/{articleId}", 1L)
            {
                contentType = MediaType.APPLICATION_JSON
                content = updateRequestDtoJosn
            }
            .andDo { print() }
            .andExpect {
                status { isOk() }
            }
    }
}

Kotlin .. ์ฒซ์ธ์ƒ์ด ๋„ˆ๋ฌด ์ข‹๋‹ค.

profile
์ข€ ๋” ์ฒœ์ฒœํžˆ ๊นŒ๋จน๊ธฐ ์œ„ํ•ด ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. ๐Ÿง

0๊ฐœ์˜ ๋Œ“๊ธ€