Kotlin 서버 개발하기 2
Pomodoro
최근에 알게 된 공부/일 집중 방법으로 '뽀모도로' 기법이 있습니다.
쉽게 말해 25분 공부하고, 5분 쉬는 것인데요.
실제 타이머로 25분을 재면서 어떤 일에 몰두하면, 생각보다 집중이 잘 되서 효과를 볼 수 있습니다.
특히, 25분이 끝나올 때 다급해지는 마음에 더 집중이 잘 되기도 합니다.
뽀모도로 기법은 이 포모도르를 4번 시행하면 1시간이 채워지는데, 이 때는 5분보다 좀 더 긴 휴식 시간을 가지길 권장합니다.
이렇게 한 싸이클이 끝나면, 다음 목표를 향해 다음 싸이클을 반복하면 됩니다.
지금 이 블로그 글쓰기 역시 뽀모도로 기법을 통해 집중해서 진행하고 있습니다.
이번 프로젝트는 뽀모도로 기법을 통한 자기 관리 앱을 만들 것 입니다.
Domain
먼저 할일을 담을 객체(Todo
)가 있어야 합니다.
class Todo(
val id: Long,
val title: String,
val content: String,
) {
}
이 글은 코틀린 자체에 대한 이야기를, 프로젝트를 만들어보면서 하므로 사소한 문법도 간단하게 같이 설명합니다.
변수
- 코틀린에서 val 키워드는 변수가 변경되지 않음을 나타냅니다. (불변)
- 자바의 final 키워드와 비슷합니다.
- 이와 반대로 가변 변수를 만들고 싶다면, var 키워드를 붙입니다.
참고)
- val : value, 불변 값을 뜻함
- var : variable, 변수를 뜻함
클래스
- 코틀린에서 클래스 다음 ( ) 소괄호 안에 필드를 넣으면, 필드와 동시에 생성자를 만들 수 있습니다.
- { } 중괄호 안에는 아래와 같은 요소를 만들 수 있고, 아무 것도 없으면 생략가능합니다.
- 필드
- 다른 생성자
- 메소드
companion object
등 (자바의 static 변수, 메소드를 코틀린에서 사용하는 방법)
위와 같이 생성한 코틀린 파일은 컴파일하여 자바로 역컴파일하면 아래와 같은 결과가 나옵니다.
...
public final class Todo {
private final long id;
@NotNull
private final String title;
@NotNull
private final String content;
public final long getId() {
return this.id;
}
@NotNull
public final String getTitle() {
return this.title;
}
@NotNull
public final String getContent() {
return this.content;
}
public Todo(long id, @NotNull String title, @NotNull String content) {
Intrinsics.checkNotNullParameter(title, "title");
Intrinsics.checkNotNullParameter(content, "content");
super();
this.id = id;
this.title = title;
this.content = content;
}
}
즉 getter 메소드와 모든 필드를 인자로 받는 생성자가 자동으로 만들어집니다.
- 롬복의
@Getter
,@AllArgsConstructor
를 붙인 결과와 같습니다. - 코틀린은 이런 bolierplate code 를 줄여주는 기능을 언어적 차원에서 제공한다는 장점이 있습니다.
Repository
이제 우리는 R2DBC 를 통해서 도메인 엔티티와 DB 테이블을 연결해주려고 합니다.
어노테이션을 통해 매핑해 봅시다.
Todo.kt
@Table(name = "todo")
class Todo(
@Id
var id: Long? = null,
val title: String,
val description: String,
)
- DB 안에 있는 todo 테이블에 대응되는 엔티티가 됩니다.
- id 변수의 타입은 nullable 한 Long 타입으로 설정했습니다. 그 이유는 Todo 객체를 생성할 때 id 없이 만든 상태로 repository 에게 넘겨주기 때문입니다. 이 프로젝트에서는 H2 DB 에서 auto increment 로 id 값을 자동으로 1개씩 올려 생성해주는데, 우리는 이 값을 그대로 받아올 것입니다.
- 코틀린에서는 이렇게
?
문법을 통해서 nullable 타입을 not nullable 타입과 구분해서 선언할 수 있는데, 이는 자바에서 개발자가 생각하지못해 자주 발생하는NPE(NullPointerException)
예외를 컴파일타임에 쉽게 잡아줄 수 있습니다. 실제로 코틀린에서 자바보다 NPE 발생률이 현저하게 감소했다는 통계 결과가 있습니다. - 또한 id 만
val
에서var
로 변경시켜,getId()
와 더불어setId()
메소드도 생기도록 만들었습니다. 레포지토리에서 save를 사용하는 구조상 id 를 나중에 변경해야하는 부분이다보니 id 에 대해서는 가변 변수로 만들어주었습니다.
혹은 클래스 자체를 data class 로 만들면 val 로 만들어도 동작이 잘되기도 합니다. 이 부분은 r2dbc 에서 불변 객체를 사용할 수 있도록 하면서도, 엔티티로서 사용할 수 있게끔 내부적으로 id 변경을 허용하는 로직을 담아 만들어져 있을거라 추측하고 있습니다.
@Table(name = "todo")
data class Todo(
@Id
val id: Long? = null,
val title: String,
val description: String,
)
코틀린은 이렇게 data class 를 사용해서, equals(), hashcode(), toString(), component(), copy() 함수를 컴파일타임에 자동으로 만들어줍니다.
말그대로 data 를 옮겨 주고 받는 DTO(Data Transfer Object) 등에 잘 어울릴 수 있는 기능이라고 생각합니다.
반면, 위와 같이 객체 지향적으로 사용하고 싶은 엔티티에서도 사용할 수 있을 것 같기도 한게 Todo 엔티티를 객체로써 잘 사용하고자 하면, 위 메소드들은 꼭 필요하기 때문입니다.
이 프로젝트에서는 일단 엔티티에서는 data class 를 사용하지 않고 진행하도록 하겠습니다.
이제 repository 를 선언해봅시다.
지금 프로젝트는 Spring Reactive Web + R2DBC 를 사용하고 있으므로, R2dbcRepository
인터페이스를 상속받아 레포지토리를 만듭니다.
interface TodoRepository : R2dbcRepository<Todo, Long>
이렇게 되면 코드에 리액티브 스트림의 Publisher 구현체인 Mono, Flux 가 많이 보이게될 것입니다.
하지만 우리는 코틀린의 코루틴을 통한 리액티브 앱을 만들 계획이므로 CoroutineCrudRepository
를 상속하려고 합니다.
TodoRepository.kt
interface TodoRepository : CoroutineCrudRepository<Todo, Long>
마치 Spring Data JPA 에서 JpaRepository 를 상속받는 것과 형식이 비슷하고, 인터페이스도 거의 똑같으니 기존에 사용하는 것과 큰 괴리 없이 사용할 수 있습니다.
위 레포지토리는 자동으로 스프링 빈으로 등록됩니다.
Service
TodoService.kt
@Service
class TodoService(
val todoRepository: TodoRepository
) {
suspend fun create(todoRequest: TodoRequest): Todo {
return todoRepository.save(todoRequest.toEntity())
}
}
위에서 만들었던 TodoRepository
를 주입해서 사용하려고 합니다. 의존성 주입(DI) 는 스프링에게 맡기고, TodoService
는 생성자에 TodoRepository
빈을 받아오면 됩니다.
필드를 선언하면서 생성자를 만들 수 있다는 코틀린 문법의 장점 덕분에 코드가 깔끔해졌습니다.
- Todo 를 리턴하는데 함수 앞에
suspend
키워드는 이 함수를 언제든 중단할 수 있고, 나중에 다른 곳에서 재개할 수 있도록 만든 것입니다. 코루틴 활용과 연관있습니다.
Controller
TodoController.kt
@RestController
class TodoController(
val todoService: TodoService
) {
@PostMapping("/api/v1/todos")
suspend fun create(@RequestBody todoRequest: TodoRequest) =
TodoResponse(todoService.create(todoRequest))
}
- 컨트롤러에서는 서비스를 주입받습니다.
- 코틀린은 위처럼 함수의 반환 문법으로
{ return value }
대신= value
로 간단하게 표현이 가능합니다.- 코틀린은 타입을 추론할 수 있다면 생략이 가능한데, 개인적으로는 타입이 눈에 보이면 코드를 보기 쉬워서, 리턴 타입이 명확히 보이는 표현을 사용하면 좋을 것 같습니다.
추가로 여기서 활용되는 Dto 에 주목해보겠습니다.
TodoDto.kt
data class TodoRequest(
val title: String,
val description: String = ""
) {
fun toEntity(): Todo {
return Todo(
title = title,
description = description
)
}
}
data class TodoResponse(
val id: Long,
val title: String,
val description: String
) {
companion object {
operator fun invoke(todo: Todo): TodoResponse {
return TodoResponse(
id = todo.id!!,
title = todo.title,
description = todo.description
)
}
}
}
코틀린은 이와 같이 하나의 파일에 여러 클래스를 구성할 수 있습니다. Dto 같이 관련있는 객체를 한 곳에 모아 관리하기 좋습니다.
엔티티와 Dto 를 변환하는 로직을 Dto 안에 넣고 싶다면, 내부 메소드를 생성해 로직을 만들 수도 있습니다.
특히, 인스턴스를 생성하지 않고 클래스를 통해 바로 호출할 수 있는 정적변수와 메소드는 companion object
안에서 만들 수 있습니다.
여기서는 추가적으로 operator fun invoke
를 통해 생성자를 연산자 오버로딩 했습니다.
operator 키워드는 연산자를 오버로딩 한다는 뜻이고, 생성자호출에 대한 메소드이름이 invoke 입니다.
Todo 객체를 파라미터로 받는 생성자를 하나 만들었다고 생각하시면 편합니다. 그러면 호출하는 쪽에서 생성자를 호출하듯이 깔끔하게 사용할 수 있을 것입니다.
아래 코드와 똑같이 동작합니다.
코틀린에서 생성자를 만드는 문법도 간략하게 주석으로 설명해놓았습니다.
data class TodoResponse ( // primary constructor (주생성자) val id: Long, val title: String, val description: String ) { // secondary constructor (부생성자) constructor(todo: Todo) : this( // delegation to the primary constructor (주생성자있는 경우 주생성자에게 위임해야 함) id = todo.id!!, title = todo.title, description = todo.description ) }
자바에서 자주사용하는 static of 메소드 처럼 만들고 싶을 경우 아래와 같이 가능합니다.
data class TodoResponse (
val id: Long,
val title: String,
val description: String
) {
companion object {
fun of(todo: Todo): TodoResponse {
return TodoResponse(
id = todo.id!!,
title = todo.title,
description = todo.description
)
}
}
}
또한 마지막으로 한 가지 다른 방법이 있는데, 확장함수를 이용하는 것입니다. Todo 의 확장함수로 TodoResponse 를 반환하는 함수를 생성했습니다.
data class TodoResponse (
val id: Long,
val title: String,
val description: String
)
fun Todo.toResponse(): TodoResponse {
return TodoResponse(
id = id!!,
title = title,
description = description
)
}
팀에서는 사용하기 편한 방법을 약속해서 사용하면 좋을 것 같습니다. 혹은 다른 Mapper 계층이 있어도 좋겠습니다.
실행
마지막으로, 지금까지 만들어놓은 애플리케이션을 실행해보겠습니다.
인텔리제이에서 HTTP 요청을 편하게 보낼 수 있도록 문법을 지원합니다.
아래와 같이 Todo 생성 요청을 작성해 봅시다.
generated-requests.http
###
# @no-log
POST http://localhost:8080/api/v1/todos
Content-Type: application/json
{
"title": "블로그 작성하기",
"description": "1) 노트북을 편다. 2) 글을 작성한다."
}
성공적으로 Todo 를 생성하여 데이터베이스에 저장한 것을 확인했습니다.
'Java, Kotlin, Spring > kotlin' 카테고리의 다른 글
자바개발자가 보기에 Kotlin 에서 좋아보이는 것들 - 4 (0) | 2022.11.07 |
---|---|
자바개발자가 보기에 Kotlin 에서 좋아보이는 것들 - 3 (0) | 2022.11.03 |
자바개발자가 보기에 Kotlin 에서 좋아보이는 것들 - 2 (1) | 2022.10.15 |
자바개발자가 보기에 Kotlin 에서 좋아보이는 것들 - 1 (1) | 2022.10.13 |
Kotlin 서버 개발하기 1 (0) | 2022.09.28 |
댓글