컨텐츠 바로가기

11.26 (화)

자바 개발자를 위한 코틀린 입문서 : 클래스 및 코루틴 활용 가이드

댓글 첫 댓글을 작성해보세요
주소복사가 완료되었습니다
자바는 고전적인 객체 지향 언어 중 하나인 만큼 클래스와 객체는 자바 개발자들에게 특별한 관심사다. 객체 지형 프로그래밍은 특히 프로그램의 크기가 커짐에 따라 프로그램을 구조화, 체계화하는 측면에서 매우 강력한 기능을 제공한다. 대부분의 현대 언어가 어떤 형태로든 객체를 제공하는 이유가 여기에 있다.
ITWorld

ⓒ Louis Tsai / Unsplash

<이미지를 클릭하시면 크게 보실 수 있습니다>



다른 한편으로 클래스와 클래스의 계층 구조를 정의하고 유지하는 작업은 복잡해질 수 있으며 이로 인해 프로그래밍 속도가 느려질 수 있다. 여기서 타협이 필요하다. 코틀린은 비교적 새로운 언어 중 하나로, 배후에서 작동하면서 프로그래밍을 간소화하는 동시에 필요할 때는 여전히 복잡성에 액세스할 수 있게 해준다.

코틀린을 통한 클래스 간소화

‘“자바의 현대적인 대안” 코틀린 입문 가이드’에서 코틀린으로 작성한 클래스 예제를 살펴봤는데, 이번에는 다른 예제를 보자.
data class StarWarsMovie(val title: String, val episode_id: Int, val release_date: String)

믿기 어렵겠지만 이 한 줄이 완전한 클래스를 제공한다. 이는 데이터 클래스로, equals toString과 같은 일반적인 메서드를 자동으로 제공한다. 그 외에는 다른 클래스와 동일하게 작동한다. 이 클래스는 영화 제목, ID, 개봉일을 사용해 스타워즈 영화를 모델링한다.

여기서 눈여겨볼 점은 코틀린이 클래스 생성을 본질적인 요소로 축소한다는 것이다. 이 예제에서 우리가 제공한 것은 클래스 이름과 생성자 인수가 전부다. 필요한 다른 모든 요소는 코틀린이 인수의 유형과 이름을 기반으로 추론한다. 코틀린 클래스는 자바 클래스와 동일하게 작동하지만 멤버를 명시적으로 작성할 것을 요구하지 않는다.

퍼블릭 멤버와 메서드

코틀린에서 멤버와 메서드는 기본적으로 퍼블릭이므로 클래스 인스턴스를 만들고 그 속성에 직접 액세스할 수 있다.
val newHope = StarWarsMovie("A New Hope", 4, "1977-05-25")
println(newHope.id) // outputs “4”

가시성 선언을 필요로 하는 자바와 퍼블릭을 전제하는 코틀린 사이에는 흥미로운 설계상의 차이점이 있다. 이와 같은 효율화는 더 간소하고 빠른 언어 표면으로 이어진다. 반면 프라이빗 멤버의 안전을 원한다면 코틀린에서는 해당 제어자(modifier)를 명시적으로 설정해야 한다.

필드 중 하나를 프라이빗으로 설정하려면 다음과 같이 하면 된다.
class StarWarsMovie(val title: String, private val episode_id: Int, val release_date: String)
println("test: " + newHope.episode_id) // Error


함수 확장

코틀린의 역동성을 보여주는 또 다른 예제는 함수 확장이다(간단히 확장이라고도 함). 함수 확장을 사용하면 기존 클래스 또는 인터페이스에 함수를 추가할 수 있다. 자바스크립트 세계에는 이미 잘 알려진 기능으로, 자바와 비슷한 클래스 맥락에서 돋보인다. 여기서는 기존 StarWarsMovie 클래스에 새 메서드를 추가한다.
fun StarWarsMovie.releaseYear(): Int {
val year = release_date.substring(0, 4)
return year.toInt()
}
val newHope = StarWarsMovie("A New Hope", 4, "1977-05-25")
val releaseYear = newHope.releaseYear()
println("The release year of A New Hope is $releaseYear")

위 예제에서는 releaseYear()라는 클래스에 새 메서드를 정의했다. 기존 클래스 StarWarsMovie.releaseYear()에 직접 정의했다는 점에 주목하라. 자체 클래스로 할 수 있지만 서드 파티 라이브러리에서 가져온 클래스도 가능하다. 코틀린 설명서에는 표준 라이브러리에 메서드를 추가하는 예제가 나와 있다. (필자는 이 같은 종류의 몽키패치에 대해 조심하는 편이지만 코틀린의 유연성은 확실히 볼 수 있다.)

이제 StarWarsMovieMovie 슈퍼클래스의 서브클래스로 만들려는 경우를 생각해 보자. 코틀린에서는 다음과 같이 할 수 있다.
open class Movie(val title: String, val releaseDate: String) {
open fun releaseYear(): Int {
val year = releaseDate.substring(0, 4) return year.toInt()
}
}

class StarWarsMovie(title: String, episodeId: Int, releaseDate: String) : Movie(title, releaseDate) {
val episodeId: Int = episodeId
}

open 키워드는 클래스 또는 함수를 서브클래스 또는 오버라이드에 사용할 수 있음을 나타낸다. 자바 용어로 말하자면 코틀린 클래스는 기본적으로 final이다. 기본 public 멤버와 기본 final 클래스는 상속보다는 구성을 선호하도록 독려하는 것으로 해석이 가능하다. 앞 예제에서는 StarWarsMovie Movie에 생성자 기반 선언을 사용했다. : movie에서 콜론은 확장을 나타내며, 자바의 extends 키워드와 비슷하게 작동한다.

명시적인 클래스 선언

코틀린에서는 자바와 비슷하게 더 명시적으로 선언하는 옵션도 있다.
class Movie {
var title: String = ""
var releaseDate: String = ""

constructor(title: String, releaseDate: String) {
this.title = title
this.releaseDate = releaseDate
}

fun releaseYear(): Int {
val year = releaseDate.substring(0, 4)
return year.toInt()
}
}

class StarWarsMovie(title: String, releaseDate: String, val episodeId: Int) : Movie(title, releaseDate) {
// ...
}

여기서 보는 클래스는 자바 개발자에게는 딱히 설명이 필요 없는 클래스다. 생성자는 클래스 이름이 아닌 constructor 키워드를 사용해 정의된다. 대체 생성자가 필요한 경우 이것을 앞서 살펴본 기본 생성자 스타일과 결합할 수 있다.
class Movie(title: String, releaseDate: String) {
var title: String = title
var releaseDate: String = releaseDate

constructor(title: String) : this(title, "") {
println("Secondary constructor called")
}
}

일반적으로 코틀린의 객체 지향 구문은 혼합하기가 매우 쉽다. 가능한 경우 더 간단한 구문을 선택하고, 꼭 필요할 때 더 세부적인 구문으로 확장할 수 있다.

생성자 개념과 관련해서 코틀린이 제공하는 init 키워드를 사용하면 객체 생성 중 코드를 실행할 수 있다. Init 외에는 속성 선언만 허용된다. 코틀린이 생성 중에 로직과 속성을 구분한다는 것은 자바와 다른 점이다.
class StarWarsMovie(title: String, episodeId: Int, releaseDate: String) : Movie(title, releaseDate) {
val episodeId: Int = episodeId
init { println("Made a new star wars movie: $episodeId") } // un-Java-like init block
}

임시 클래스 선언

코틀린의 개선 사항 중에서 특히 자유로운 부분 중 하나는 코드의 어느 위치에서나 유형, 클래스, 함수를 임시로 선언할 수 있는 기능이다. 따라서 “이 부분에 클래스가 필요하다”는 생각이 들면 간단히 바로 클래스를 쓸 수 있다. 이후 같은 지점에서 전체 작업으로 돌아가서 새 클래스를 사용할 수 있다. 나중에 돌아와서 다시 클래스를 다듬고, 자바에서 하듯이 자체 파일로 추출할 수도 있다.

싱글톤 스타일 객체

코틀린에서는 다음과 같이 object 키워드로 싱글톤 스타일 객체를 선언할 수 있다.
object MovieDatabase {
private val movies = mutableListOf<Movie>()

fun addMovie(movie: Movie) {
movies.add(movie)
}

fun getMovies(): List<Movie> {
return movies.toList()
}
}

이와 같은 객체는 다른 블록 내에 중첩되지 않고 최상위에서만 선언이 가능하다.

싱글톤은 주어진 프로그램 실행에서 전역적으로 하나의 인스턴스만 있음을 의미한다. 자바와 자바 기반 프레임워크(스프링 등)에서 일반적인 패턴이다. 여러 참조가 있지만 모두 동일한 MovieDatabase를 참조한다.
val db = MovieDatabase;
db.addMovie(movie)
db.addMovie(starWarsMovie)

// some other remote part of your app:
println("Got the same instance: ${db.getMovies()}")

여기서 핵심은 모두 같은 인스턴스에 대한 핸들을 받는다는 것이다. 이 코드를 이대로 실행하면 다음과 같이 보기 흉한 출력이 나온다.
Got the same instance: Movie@3b764bce StarWarsMovie@759ebb3d

가장 먼저 드는 생각은 Movie를 데이터 클래스로 만들고 toString() 메서드를 공짜로 얻는 것이겠지만 데이터 클래스는 final이다. 더 나은 방법은 간단한 toString을 직접 추가하는 것이다.
open class Movie(val title: String, val releaseDate: String) {
override fun toString(): String {
return "Movie(title='$title', releaseYear=${releaseYear()})"
}
}

이제 더 나은 출력을 얻을 수 있다.
Got the same instance: Movie(title='The Shawshank Redemption', releaseYear=1994) Movie(title='A New Hope', releaseYear=1977)

코틀린의 동시성과 코루틴

동시성은 코틀린이 흥미로운 옵션을 제공하는 또 다른 부분이다. 자바는 최근 매우 적극적으로 동시성 모델을 개선하고 있는데 그 중 일부 개선은 코틀린의 코루틴에 착안한 것이다. 코루틴은 스레딩에 대한 다른 종류의 액세스를 제공한다. 여기에 사용되는 메커니즘 중 하나인 구조적 동시성은 지금 자바에 추가되고 있다.

코루틴과 구조적 동시성은 스레드 관리를 동기적, 선언적 스타일로 유지한다. 코틀린 문서에서 가져온 간단한 예를 보자.
import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
launch { doWorld() }
println("Hello")
}

// this is your first suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}

suspend 키워드는 doWorld 함수가 동시성 엔진에 의해 일시 중지될 수 있음을 나타낸다. 즉, 메서드는 논블로킹 메서드다. main 함수가 runBlocking 함수로 설정된 것을 볼 수 있는데 이는 블로킹 CoroutineScope를 설정한다. 모든 동시 코드는 CoroutineScope 내에서 선언되어야 한다.

스레드를 실행하기 위한 실제 메커니즘은 launch 키워드다. 이 키워드는 main 메서드에서 doWorld를 호출하는 데 사용된다.

물론 여기서 다루는 코틀린의 동시성과 코루틴은 표면적인 수준에 불과하지만 예제를 통해 자바와 관련해서 코루틴의 동작 방식에 대한 감을 잡을 수 있을 것이다. 앞서 언급한 바와 같이 자바는 이 영역에서 실제로 코틀린에 가까운 쪽으로 변화하고 있다. 코틀린 동시성에 대한 또 한 가지 중요한 점은 JVM의 가상 스레드 도입이다. 이 기능은 코틀린의 코루틴을 JVM의 가상 스레드와 함께 사용하게 될 가능성이 있음을 시사한다.

결론

코틀린은 유연성을 높이고 코딩이 지연될 수 잇는 부분을 개선하기 위해 자바의 몇 가지 규칙을 수정한다. 자바 코드가 엄격하고 명시적인 쪽으로 치우치는 경향이 있다면 코틀린은 흐름과 관습적인 쪽에 가깝다. 좋은 점은 코틀린이 JVM에서 자바와 함께 사용할 수 있는 표현성이 뛰어난 현대적 언어를 제공한다는 것이다. 필자는 대부분의 자바 개발자가 코틀린으로 할 수 있는 일을 긍정적으로 받아들일 것이라고 생각한다.
editor@itworld.co.kr

Matthew Tyson editor@itworld.co.kr
저작권자 한국IDG & ITWorld, 무단 전재 및 재배포 금지
기사가 속한 카테고리는 언론사가 분류합니다.
언론사는 한 기사를 두 개 이상의 카테고리로 분류할 수 있습니다.