🔥 TIL-Day 70 KotlinとSpringboot 01 CRUD Rest API実装とJunitテスト


完全なソースGithub
これは、コードをそのまま書き込むのではなく、初めて接触した言語であるため、エラーかもしれません.

📌 ドメイン設計


タイトル、コンテンツのArticleクラスを定義します.
kotlinの基本構文を理解するためにDBではなくPojoでクラスを記述します.
data class Article(
    val title: String,
    val content: String,
)
Kotlinの最初の魅力点...
以上のカテゴリは、생성자GetterEquals And HashCodeToStringを含む.
body(中括弧)がないのが印象的でしたprimary constructorのフィールドタイプがvalであることを指定した場合、このフィールドpublicを読み取ることができる.実際、Javaクラスファイルにコンパイルした結果もGetterを呼び出します.varと記載されている場合は、このフィールドをpublicに完全に開くことに相当します.読み取りと変更は、フィールドに直接アクセスできます.(実際のJavaコード呼び出しgetter、setter)
primary constructorでvarを使うのは少し危険かもしれませんか? エンティティの変更には、ビジネスロジックを反映する方法が望ましい。

次に、classの前のデータキーワードはEquals And HashCodeおよびToStringを提供する.Lombokの@Dataと似ています.これも便利ですが、上のクラスがエンティティである場合は注意が必要です.

📌 メモリレポートの実装


メモリ(Map)を使用してレポートを実装します.
@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については,可変Mapを用いて修正を行った.

CRUDの実装

getArticleのリターンタイプはArticle?です.
これはKotlinの長所の一つnull safetyの一部であり、JavaのOptionに似た概念であればよい.
Javaの場合、nullオブジェクトに対してlengthやgetterなどのメソッドを呼び出すと、NullPointException(NPE)が生成されます。これを防ぐ方法はnull-securityです。

以降はテスト部分で発生するが、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
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)
}

📌 コントローラ層実装

@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)
}

コントローラテスト

@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 .. 第一印象よかったです.