์ฒ์ ์ ํ๋ ์ธ์ด์ด๊ณ ๋ค๋ฅธ ๊ทธ๋๋ก ์ฝ๋๋ฅผ ๊ฐ๋ค ์ด ๊ฒ์ด ์๋๊ธฐ ๋๋ฌธ์ ์ค๋ฅ๊ฐ ์์ ์ ์์ต๋๋ค.
์ ๋ชฉ, ๋ด์ฉ์ ๊ฐ๋ 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 ์ ๋น์ทํ ๋๋์ด๋ค. ์ด ์ญ์ ๊ต์ฅํ ํธ๋ฆฌํ์ง๋ง ์ ํด๋์ค๊ฐ ์ํฐํฐ๊ฐ ๋๋ค๋ฉด ์ฃผ์ํด์ผ ํ ๊ฒ์ด๋ค.
๋ฉ๋ชจ๋ฆฌ(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์ ์ฌ์ฉํ๋ค.
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("์กด์ฌํ์ง ์๋ ๊ฒ์๊ธ์
๋๋ค.")
}
}
}
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 .. ์ฒซ์ธ์์ด ๋๋ฌด ์ข๋ค.