코틀린이 정한 목표 영역은 상당히 광범위하다. 코틀린은 어느 한문제 영역만을 해결하거나 오늘날 소프트웨어 개발이 처한 어려움 중 일부만을 다루기 위한 언어가 아니다. 대신 코틀린은 개발 과정에서 수행해야 하는 모든 과업에 있어 폭넓게 생산성을 향상시켜 준다. 코틀린은 구체적인 영역의 문제를 해결하거나 특정 프로그래밍 패러다임을 지원하는 여러 라이브러리와 아주 잘 융합된다. 다음은 코틀린 언어의 핵심적인 특징이다.
코틀린 언어 특징
1. 정적 타입 지정 언어
자바와 마찬가지로 코틀린도 정적 타입 지정 언어다. 정적 타입 지정이라는 말은 모든 프로그램 구성 요소의 타입을 컴파일 시점에 알 수 있고 프로그램 안에서 객체의 필드나 메소드를 사용할 때마다 컴파일러가 타입을 검증해준다는 뜻이다. JVM에서는 Groovy나 JRuby가 대표적인 동적 타입 지정 언어다. 동적 타입 지정 언어에서는 타입과 관계없이 모든 값을 변수에 넣을 수 있고, 메소드나 필드 접근에 대한 검증이 실행시점에 일어나며, 그에 따라 코드가 더 짧아지고 데이터 구조를 더 유연하고 생서하고 사용할 수 있다. 하지만 반대로 이름을 잘못 입력하는 등의 실수도 커파일 시 걸러내지 못하고 실행 시점에 오류가 발생한다.
한편 자바와 달리 코틀린에서는 모든 변수의 타입을 프로그래머가 직접 명시할 필요가 없다. 대부분의 경우 코틀린 컴파일러가 문맥으로부터 변수 타입을 자동으로 유추할 수 있기 때문에 프포그래머는 타입 선언을 생략해도 된다. 컴파일러가 문맥을 고려해 변수 타입을 결정하는 이런 기능을 타입 추론(type inference)라고 부른다. 정적 타입 지정의 장점은 다음과 같다.
1) 성능
실행 시점에 어떤 메소드를 호출할지 알아내는 과정이 필요 없으므로 메소드 호출이 더 빠르다.
2) 신뢰성
컴파일러가 프로그램의 정확성(correctness)을 검증하기 때문에 실행시 프로그램이 오류로 중단될 가능성이 더 적어진다.
3) 유지보수성
코드에서 다루는 객체가 어떤 타입에 속하는지 알 수 있기 때문에 처음 보는 코드를 다룰때도 더 쉽다.
4) 도구 지원
정적 타입 지정을 활용하면 더 안전하게 리팩토링할 수 있고, 도구는 더 정확한 코드 완성 기능을 제공할 수 있으며, IDE의 다른 지원 기능도 더 잘 만들수 있다.
코틀린은 타입 추론을 지원하므로 정적 타입 지정 언어에서 프로그래머가 직접 타입을 선언해야 함에 따라 생기는 불편함이 대부분 사라진다. 코틀린의 타입 시스템을 더 자세히 살펴보면 이미 잘알고 있는 내용을 많이 발경할 수 있다. 클래스, 인터페이스, 제네릭스는 모두 자바와 비슷하게 작동한다. 하지만 몇가지 새로운 점이 있다.
그 중 가장 중요한 특성은 코틀리이 널이 될수 있는 타입(nullable type)을 지원한다는 점이다. 널이 될 수 있는 타입을 지원함에 따라 컴파일 시점에 null pointer exception이 발생할 수 있는지 여부를 검사할 수 있어서 좀더 프로그램의 신뢰성을 높일 수 있다. 코틀린 타입 시스템은 또한 함수 타입(function type)에 대한 지원을 들 수 있다.
코틀린은 함수형 스타일로 프로그램을 짤 수 있게 지원하지만 함수형 프로그래밍 스타일을 강제하지는 않는다. 명령형 방식이 더 적합한 경우라면 함수형 프로그래밍으로 번거롭게 코드를 작성할 필요없이 직접 변경 가능한 데이터와 부수 효과를 활용하는 함수를 사용해도 된다. 코틀린으로 코드를 작성할때는 객체지향과 함수형 접근 방법을 함께 조합해서 문제에 가장 적합한 도구를 사용하면 된다.
1. 코틀린 서버 프로그래밍
서버 프로그래밍은 상당히 광범위한 개념이다. 다음과 같은 응용 분야를 포함하는 여러분야가 서버 프로그래밍에 포함된다.
- 브라우저에 HTML 페이지를 돌려주는 웹 애플리케이션
- 모바일 애플리케이션에게 HTTP를 통해 JSON API를 제공하는 백엔드 애플리케이션
- RPC(원격 프로시저 호출) 프로토콜을 통해 서로 통신하는 작은 서비스들로 이뤄진 마이크로서비스
개발자들은 이런 애플리케이션을 수년간 자바로 개발해 오면서 이런 종류의 애플리케이션 개발에 도움을 줄 수 있는 기술과 프레임워크를 만들어왔다. 새로운 기술이나 프레임워크는 언제나 기존 프레임워크나 기술을 확장하고 개선하거나 대치하며, 이미 여러 해 동안 쓰여온 기존 시스템과 새로운 코드를 통합해야만 한다.
이런 환경에서 자바 코드와 매끄럽게 상호운용할 수 있다는 점이 코틀린의 큰 장점이다. 코틀린은 새로운 컴포넌트를 작성하거나 기존 서비스 코드를 코틀린으로 이식해야하는 경우에 모두 잘 들어맞는다. 자바 클래스를 코틀린으로 확장해도 아무 문제가 없으며, 코틀린 클래스 안의 메소드나 필드에 특정 자바 어노테이션을 붙여야 하는 경우에도 아무 문제가 없다. 그러면서도 시스템 코드는 더 간결해지고 더 신뢰성이 높아지며, 더 유지보수하기 쉬워질 것이다.
2. 코틀린 안드로이드 프로그래밍
전형적인 모바일 애플리케이션은 전형적인 엔터프라이즈 애플리케이션과 아주 많이 다르다. 모바일 애플리케이션은 엔터프라이즈 애플리케이션보다 더 작고 기존 코드 기반과 새 코드를 통합할 필요도 더 적다. 또 모바일 애플리케이션은 보통 더 다양한 디바이스에 대해 서비스의 신뢰성을 보장하면서 더 빠르게 개발해 배포할 필요가 있다. 코틀린 언어의 특성과 안드로이드 프레임워크의 특별한 컴파일러 플러그인 지원을 조합하면 안드로이드 애플리케이션 개발의 생산성을 더 높일 수 있다.
코틀린을 사용하면 얻을 수 있는 이익으로는 애플리케이션의 신뢰성이 더 높아진다는 점이다. 안드로이드 앱의 'Process Has Stopped' 대화상자는 애플리케이션에서 처리되지 않는 예외가 발생한 경우에 표시된다. 코틀린 타입 시스템은 null 값을 정확히 추적하며 널 포인터로 인해 생기는 문제를 줄여준다. 자바에서 Null Pointer Exception을 일으키는 유형의 코드는 대부분 코틀린에서는 컴파일도 되지 안흔ㄴ다. 따라서 개발 중인 애플리케이션이 릴리즈되기 전에 널 포인터 관련 오류를 수정할 수 있다.
코틀린을 사용하더라도 성능 측면에서 아무 손해가 없다. 코틀린 컴파일러가 생성한 바이트코드는 일반적인 자바 코드와 똑같이 효율적으로 실행된다. 코틀린의 런타임 시스템은 상당히 작기 때문에 컴파일 후 패키징한 애플리케이션 크기도 자바 애플리케이션에 비해 그리 많이 늘어나지 않는다. 대부분의 코틀린 표준 라이브러리 함수는 인자로 받은 람다 함수를 인라이닝한다. 따라서 람다를 사용해도 새로운 객체가 만들어지지 않으므로 객체 증가로 인해 GC가 늘어나서 프로그램이 자주 멈추는 일도 없다.
코틀린의 철학
코틀린이 자바와의 상호운용성에 초점을 맞춘 실용적이고 간결하며 안전한 언어라고 설명하는 경우가 자주 있다. 그렇다면 실용성, 간결성, 안전성, 상호운용성은 각각 어떤 의미인지 살펴본다.
1. 실용성
코틀린은 실제 문제를 해결하기 위해 만들어진 실용적인 언어다. 코틀린 설계는 대규모 시스템을 개발해온 다년간의 IT업계 경험을 바탕으로 이루어졌으며, 수많은 소프트웨어 개발자들의 사용에 잘 들어맞을 수 있게 주의깊게 언어 특성을 선택했다. 더 나아가 젯브레인스나 코틀린 커뮤니티 내부의 개발자들이 다년간 코틀린 초기 버전을 사용하면서 전달한 피드백이 현재 발표된 최종 코틀린 버전에 반영돼 있다. 그런 이유로 실제 프로젝트에서 문제를 해결할때 코틀린이 도움이 될 여지가 많다.
코틀린은 다른 프로그래밍 언어가 채택한 이미 성공적으로 검증된 해법과 기능에 의존한다. 이로 인해 언어의 복잡도가 줄어들고 이미 알고 있는 기존 개념을 통해 코틀린을 더 쉽게 배울 수 있다. 코틀린은 어느 특정 프로그래밍 스타일이나 패러다임을 사용할 것을 강제로 요구하지 않는다. 코틀린을 처음 배우는 사람은 자바에서 사용해 온 익숙한 프로그래밍 스타일이나 기법을 활용할 수 있다. 나중에 코틀린의 더 강력한 특성을 발견하고 그런 특성을 자신의 코드에 적용하는 방법을 배우고 나면 그 특성을 잘 활용해서 간결하게 코드를 작성할 수 있다.
코틀린의 경우 인텔리J 아이디어의 개발과 컴파일러의 개발이 맞물려 이뤄져 왔다. 그리고 코틀린 언어의 특성은 항상 도구의 활용을 염두에 두고 설계돼 왔다. 코틀린의 여러 특성을 배울때도 IDE의 코틀린 언어 지원이 중요한 역할을 한다. 흔히 쓰이지만 더 간결한 구조로 바꿀 수 있는 대부분의 코드 패턴을 도구가 자동으로 감지해서 수정하라고 제안한다. 이런 자동 수정 안내를 살펴보면서 코틀린 언어의 특성을 잘 이해하면 자신의 코드에 그런 특성을 적용하는 방법을 배울 수 있다.
2. 간결성
개발자가 코드를 새로 작성하는 시간보다 기존 코드를 읽는 시간이 더 길다. 코드가 더 간단하고 간결할수록 내용을 파악하기가 더 쉽다. 물론 설계가 좋고 각 부분의 역할을 잘 표현해주는 적절한 이름이 붙어있다면 내용을 파악할 때 큰 도움이 된다. 그러나 어떤 언어를 사용해 코드를 작성했고 그 언어가 얼마나 간결한 언어인지도 중요하다. 어떤 언어가 간결하다는 말은 그 언어로 작성된 코드를 읽을때 의도를 쉽게 파악할 수 있는 구문 구조를 제공하고, 그 의도를 달성하는 방법을 이해할 때 방해가 될 수 있는 부가적인 준비 코드가 적다는 뜻이다.
코틀린은 프로그래머가 작성하는 코드에서 의미가 없는 부분을 줄이고, 언어가 요구하는 구조를 만족시키기 위해 의미없이 프로그램에 꼭 넣어야 하는 부수적인 요소를 줄이기 위해 많은 노력을 기울였다. getter, setter, 생성자 파라미터를 필드에 대입하기 위한 로직 등 자바에 존재하는 여러가지 번거로운 준비 코드를 코틀린은 무시적으로 제공하기때문에 그런 준비 코드로 인해 지저분해지는 일이 없다.
코드가 불필요하게 길어지는 또다른 이유는 컬렉션에서 원소를 찾는 것과 같은 일반적인 작업을 수행하기 위해 명시적으로 작성해야만 하는 코드의 양이 상당하기 때문이다. 코틀린은 기능이 다양한 표준 라이브러리를 제공하기때문에 반복되거나 길어질 수 있는 코드를 라이브러리 함수 호출로 대치할 수 있다. 람다를 지원하기 때문에 작은 코드 블록을 라이브러리 함수에 쉽게 전달할수도 있다. 따라서 일반적인 기능을 라이브러리 안에 캡슐화하고 작업에 따라 달라져야 하는 개별적인 내용을 사용자가 작성한 코드 안에 남겨둘 수 있다.
코드가 더 간결하면 쓰는데 시간이 덜 걸린다. 더 중요한것은 읽는데도 시간이 덜 걸린다는 점이다. 간결성은 생산성을 향상시켜주고 개발을 더 빠르게 진행할 수 있게 해준다.
3. 안전성
일반적으로 프로그래밍 언어가 안전하다는 말은 프로그램에서 발생할 수 있는 오류 중에서 일부 유형의 오류를 프로그램 설계가 원천적으로 방지해준다는 뜻이다. 컴파일러에게 프로그램이 어떻게 작동해야 하는지에 대한 정보를 더 자세히 제공해야만 컴파일러가 프로그램 코드와 프로그램의 작동 의도에 대한 정보가 일치하는지를 검증할 수 있다. 따라서 더 큰 안전성을 얻기 위해서는 프로그램에 더 많은 정보를 덧붙여야 하므로 생산성이 하락하는 것을 감수해야하며 안전성과 생산성 사이에는 트레이드오프 관계가 성립한다.
코틀린을 JVM에서 실행한다는 사실은 이미 상당한 안전성을 보장할 수 있다는 뜻이다. 예를 들어 JVM을 사용하면 메모리 안전성을 보장하고, 버퍼 overflow를 방지하며, 동적으로 할당한 메모리를 잘못 사용함으로 인해 발생할 수 있는 다양한 문제를 예방할 수 있다. JVM에서 실행되는 정적 타입 지정 언어로서 코틀린은 애플리케이션의 타입 안전성을 보장한다. 하지만 자바보다 더 적은 비용으로 타입 안전성을 사용할 수 있다. 대부분의 경우 코틀린 컴파일러가 타입을 자동으로 추론해주기 때문에 직접 타입 정보를 지정할 필요가 없다.
코틀린은 실행 시점에 오류를 발생시키는 대신 컴파일 시점 검사를 통해 오류를 더 많이 방지해준다. 가장 중요한 내용으로 코틀린은 프로그램의 NullPointerException을 없애기 위해 노력한다. 코틀린의 타입 시스템은 null이 될수 없는 값을 추적하며, 실행 시점에 NullPointerException이 발생할 수 있는 연산을 사용하는 코드를 금지한다.
추가로 코틀린은 널이 될 수 있는 값을 다룰 수 있는 편리한 방법을 다양하게 제공한다. 이런 기능은 애플리케이션이 NullPointerException으로 인해 갑자기 중단되는 경우를 많이 줄여준다. 코틀린이 방지해주는 다른 예외로는 ClassCastException이 있다. 어떤 객체를 다른 타입으로 캐스트(cast)하기 전에 타입을 미리 검사하지 않으면 ClassCastException이 발생할 수도 있다. 코틀린에서는 타입 검사와 캐스트가 한 연산자에 의해 이뤄진다. 어떤 객체의 타입을 검사했고 그 객체가 그 타입에 속한다면 해당 타입의 메소드나 필드 등의 멤버를 별도의 캐스트 없이 사용할 수 있다. 따라서 타입 검사를 생략할 이유가 없고, 검사를 생략하지 않으면 검사를 생략해서 생기는 오류가 발생할 일도 없다.
if (value is String) // 타입을 검사한다
println(value.toUpperCase()) // 해당 타입의 메소드를 사용한다.
4. 상호운용성
코틀린은 자바 라이브러리가 어떤 API를 제공하던 간에 코틀린에서 그 API를 활용할 수 있다. 자바 메소드를 호출하거나 자바 클래스를 상속하거나 인터페이슬 구현하거나 자바 어노테이션을 코틀린 코드에 적용하는 등의 일이 모두 가능하다. 다른 일부 JVM 언어와 달리 코틀린은 상호운용성 측면에서 훨씬 더 많은 것을 제공한다. 코틀린의 클래스나 메소드를 일반적인 자바 클래스나 메소드와 똑같이 사용할 수 있다. 이에 따라 자바와 코틀린 코드를 프로젝트에서 원하는대로 섞어 쓸 수 있는 궁극적인 유연성을 발휘할 수 있다. 기존 자바 프로젝트에 코틀린을 도입하는 경우 자바를 코틀린으로 변환하는 도구를 코드베이스 안에 있는 자바 클래스에 대해 실행해서 그 클래스를 코틀린 클래스로 변환할 수 있다. 이렇게 변경한 클래스가 프로젝트 안에서 어떤 역할을 하는지와는 관계없이 코틀린으로 바꾼 클래스가 어떤 것이든 프로젝트의 나머지 부분을 전혀 수정하지 않고도 컴파일 및 실행이 가능하다.
상호운용성 측면에서 코틀린이 집중하는 다른 방향으로는 기존 자바 라이브러리를 가능하면 최대한 활용한다는 점을 들 수 있다. 코틀린은 자체 컬렉션 라이브러리를 제공하지 않는다. 코틀린은 자바 표준 라이브러리 클래스에 의존한다. 다만 코틀린에서 컬렉션을 더 쉽게 활용할 수 있게 몇가지 기능을 더할 뿐이다.
코틀린이 제공하는 도구도 다중 언어 프로젝트를 완전히 지원한다. 코틀린은 자바와 코틀린 소스 파일이 임의로 섞여 있어도 제대로 프로그램을 컴파일할 수 있다. 각 소스 파일 사이의 의존관계가 어떤 식으로 이뤄졌든 관계없이 컴파일할 수 있다. IDE 기능도 언어와 관계없이 제대로 동작하다.
- 자바와 코틀린 소스 파일을 자유롭게 내비게이션할 수 있다.
- 여러 언어로 이뤄진 프로젝트를 디버깅하고 서로 다른 언어로 작성된 코드를 언어와 관계없이 한 단계씩 실행할 수 있다.
- 자바 메소드를 리팩토링해도 그 메소드와 관련있는 코틀린 코드까지 제대로 변경된다. 역으로 코틀린 메소드를 리팩토링해도 자바 코드까지 모두 자동으로 변경된다.
코틀린 도구 사용
코틀린 소스코드를 저장할 때는 보통 .kt라는 확장자를 파일에 붙인다. 코틀린 컴파일러는 자바컴파일러가 자바 소스코드를 컴파일할 때와 마찬가지로 코틀린 소스코드를 분석해서 .class 파일을 만들어낸다. 만들어진 .class 파일은 개발 중인 애플리케이션의 유형에 맞는 표준 패키징 과정을 거쳐 실행될 수 있다. 가장 간단한 방식은 커맨드라인에서 kotlinc 명령을 통해 코틀린 코드를 컴파일한 다음 java 명령으로 그 코드를 실행하는 것이다.
kotlinc <소스파일 또는 디렉토리> -include-runtime -d <jar 이름>
java -jar <jar 이름>
다음은 코틀린 빌드 과정을 간단히 보여준다.
코틀린 컴파일러로 컴파일한 코드는 코틀린 런타임 라이브러리(kotlin runtime library)에 의존한다. 런타임 라이브러리에는 코틀린 자체 표준 라이브러리 클래스와 코틀린에서 자바 API의 기능을 확장한 내용이 들어있다. 코틀린으로 컴파일한 애플리케이션을 배포할때는 런타임 라이브러리도 함께 배포해야 한다.
실제로 개발을 진행한다면 프로젝트를 컴파일하기 위해 메이븐, 그레이들, 앤트 등의 빌드 시스템을 사용하게 될것이며, 코틀린은 그런 빌드 시스템과 호환된다. 이런 빌드 시스템은 모두 코틀린과 자바가 코드베이스에 함께 들어있는 혼합 언어 프로젝트를 지원할 수 있다. 메이븐과 그레이들은 애플리케이션을 패키지할 때 알아서 코틀린 런타임을 포함시켜준다.
자바-코틀린 변환기
새로운 언어를 배워 써먹을 만큼 숙련도를 높이려면 많이 노력해야 한다. 이 도구는 자동으로 자바를 코틀린으로 변환한다. 코틀린을 처음 배웠는데 정확한 코틀린 문법이 기억나지 않는 경우 이 변환기를 유용하게 써먹을 수 있다. 작성하고픈 코드를 자바로 작성해 복사한 후 코틀린 파일에 그 코드를 붙여 넣으면 변환기가 자동으로 같은 뜻의 코틀린 코드를 제안한다. 물론 변환기가 항상 가장 코틀린다운 코드를 제안해주지는 못하지만 잘 작동하는 코틀린 코드를 알려주기 때문에 원하는 바를 코틀린으로 달성할 수 있다.
기존 자바 프로젝트에 코틀린을 도입하고 싶을때 변환기를 사용하면 쓸모가 있다. 새 클래스를 작성할 필요가 있다며 ㄴ처음부터 코틸린으로 그 클래스를 만들면 된다. 기존 클래스를 상당 부분 변경해야 한다면 자바 대신 코틀린을 사용하고 싶을텐데 그런경우 변환기를 사용하면 도움이된다.
intellij에서 변화기를 사용하기는 쉽다. 자바 코드 조각을 변환하고 싶을때는 자바 코드 조각을 복사해 코틀린 파일에 붙여넣는다. 자바 파일 하나를 통째로 코틀린으로 변환하고 싶으면 메뉴에서 [Code] - [Convert Java File to Kotlin File] 을 선택하면 된다.
코틀린 기본 요소: 함수와 변수
모든 프로그램을 구성하는 기본 단위인 함수와 변수를 살펴본다. 코틀린에서 타입 선언을 생략해도 된다는 사실을 보고, 코틀린이 어떻게 변경 가능한 데이터보다 변경할 수 없는 불변 데이터 사용을 장려하는지 배운다.
- 함수를 최상위 수준에서 정의할 수 있다. 자바와 달리 클래스 안에 함수를 넣어야할 필요가 없다.
- 배열도 일반적인 클래스와 마찬가지다. 코틀린에는 자바와 달리 배열 처리를 위한 문법이 따로 존재하지 않는다.
- System.out.println 대신에 println이라고 쓴다. 코틀린 표준 라이브러리는 여러가지 표준 자바 라이브러리 함수를 간결하게 사용할 수 있게 감싼 wrapper를 제공한다. println도 그런 함수 중 하나다.
- 최신 프로그래밍 언어 경향과 마찬가지로 줄 끝에 세미콜론을 붙이지 않아도 좋다.
1. 함수
문(statement)와 식(expression)의 구분
코틀린에서 if는 식이지 문이 아니다. expression은 값을 만들어내며 다른 expression의 하위 요소로 계산에 참여할 수 있는 statement는 자신을 둘러싸고 있는 가장 안쪽 블록의 최상위 요소로 존재하며 아무런 값을 만들어내지 않는다는 차이가 있다. 자바에서는 모든 제어구조가 statement인 반면 코틀린에서는 루프를 제외한 대부분의 제어 구조가 expression이다. 제어 구조를 다른 expression으로 엮어낼 수 있으면 여러 일반적인 패턴을 아주 간결하게 표현할 수 있다.
반면 대입문은 자바에서는 expression이었으나 코틀린에서는 statement가 되었다. 그로인해 자바와 달리 대입식과 비교식을 잘못 바꿔 써서 버그가 생기는 경우가 있다.
식이 본문인 함수
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
위와 같이 본문이 중괄호로 둘러싸인 함수를 블록이 본문인 함수라 부르고,
fun max(a: Int, b: Int): Int = if (a > b) a else b
등호와 식으로 이뤄진 함수를 식이 본문인 함수라고 부른다.
인텔리J IDEA는 이 두 방식의 함수를 서로 변환하는 메뉴가 있다. 각각 'Convert to expression body'와 'Convert to block body'이다.
코틀린에서는 식이 본문인 함수가 자주 쓰인다. 그런 함수의 본문 식에는 단순한 산술식이나 함수 호출 식뿐만 아니라 if, when, try 등의 더 복잡한 식도 자주 쓰인다. 식이 본문인 함수는 반환 타입이 생략 가능하다. 블록이 본문인 함수가 값을 반환한다면 반드시 반환 타입을 지정하고 return문을 사용해 반환 값을 명시해야 한다.
2. 변수
코틀린에서는 타입 지정을 생략하는 경우가 흔하다. 타입으로 변수 선언을 시작하면 타입을 생략할 경우 식과 변수 선언을 구별하 ㄹ수 없다. 그런 이유로 코틀린에서는 키워드로 변수 선언을 시작하는 대신 변수 이름 뒤에 타입을 명시하거나 생략하게 허용한다.
val question = "삶, 우주, 그리고 모든 것에 대한 궁극적인 질문"
val answer = 42
val answer: Int = 42
식이 본문인 함수에서와 마찬가지로 타입을 지정하지 않으면 컴파일러가 초기화 식을 분석해서 초기화 식의 타입을 변수 타입으로 지정한다. 초기화 식을 사용하지 않고 변수를 선언하려면 변수 타입을 반드시 명시해야 한다. 초기화 식이 없다면 변수에 저장될 값에 대해 아무 정보가 없기 때문에 컴파일러가 타입을 추론할 수가 없다. 따라서 그런 경우 타입을 반드시 지정해야 한다.
1) 변경 가능한 변수와 불가능한 변수
변수 선언 시 사용하는 키워드는 다음과 같은 2가지가 있다.
- val (값을 뜻하는 value에서 따옴) : 변경 불가능한 참조를 저장하는 변수다 val로 선언된 변수는 일단 초기화하고 나면 재대입이 불가능하다. 자바로 말하자면 final 변수에 해당한다.
- var (변수를 뜻하는 variable에서 따옴) : 변경 가능한 참조다. 이런 변수의 값은 바꿀 수 있다. 자바의 일반 변수에 해당한다.
기본적으로 모든 변수를 val 키워드를 사용해 불변 변수로 선언하고, 나중에 꼭 필요할때에만 var로 변경하라. 변경 불가능한 참조와 변경 불가능한 객체를 부수 효과가 없는 함수와 조합해 사용하면 코드가 함수형 코드에 가까워진다.
val 변수는 블록을 실행할 때 정확히 한번만 초기화돼야 한다. 하지만 어떤 블록이 실행될때 오직 한 초기화 문장만을 실행됨을 컴파일러가 확인할 수 있다면 조건에 따라 val 값을 다른 여러 값으로 초기화할 수도 있다.
val message: String
if (canPerformOperation()) {
message = "Success"
// ... 연산을 수행한다.
} else {
message = "Failed"
}
val 참조 객체는 불변일지라도 그 참조가 가리키는 객체의 내부 값은 변경될 수 있다.
val languages = arrayListOf("Java") // 불변 참조를 선언한다.
languages.add("Kotlin") // 참조가 가리키는 객체 내부를 변경한다.
var 키워드를 사용하면 변수의 값을 변경할 수 있지만 변수의 타입은 고정돼 바뀌지 않는다. 예를 들어 다음 코드는 컴파일할 수 없다.
var answer = 42
answer = "no answer" // "Error:type mismatch" 컴파일 오류 발생
문자열 리터럴(string literal)에서 컴파일 오류가 발생한다. 이유는 그 타입(String)이 컴파일러가 기대하는 타입(Int)와 다르기 때문이다. 컴파일러는 변수 선언 시점의 초기화 식으로부터 변수의 타입을 추론하며, 변수 선언 이후 변수 재대입이 이뤄질 때는 이미 추론한 변수의 타입을 염두에 두고 대입문의 타입을 검사한다.
어떤 타입의 변수에 다른 타입의 값을 지정하고 싶다면 변환 함수를 써서 값을 변수의 타입으로 반환하거나, 값을 변수에 대입할 수 있는 타입으로 강제 형 변환해야한다.
2) 문자열 형식 지정: 문자열 템플릿
코틀린의 문자열 템플릿은 자바 문자열 접합 연산을 사용한 식과 마찬가지로 효율적이다. 컴파일된 코드는 StringBuilder를 사용하고 문자열 상수와 변수의 값을 append로 문자열 빌더 뒤에 추가한다. 자바에서 + 연산으로 문자열과 변수를 붙여도 컴파일러는 StringBuilder를 사용하는 바이트코드를 생성해준다.
코틀린에서는 자바와 마찬가지로 한글(한글 뿐 아니라 '글자(letter)'로 분류할 수 있는 모든 유니코드 문자)을 식별자에 사용할 수 있으므로 변수 이름에 한글이 들어갈 수 있다. 그런 유니코드 변수 이름으로 인해 문자열 템플릿을 볼때 오해가 생길 수 있다. 문자열 템플릿 안에 $로 변수를 지정할 때 변수명 바로 뒤에 한글을 붙여서 사용하면 코틀린 컴파일러는 영문자와 한글을 한꺼번에 식별자로 인식해서 unresolved reference 오류를 발생시킨다.
이 문제를 해결하는 방법은 '${name}님 반가와요!'처럼 변수 이름을 {}로 감싸는 것이다. 문자열 템플릿 안에서 변수 이름만 사용하는 경우라도 ${name}처럼 중괄호로 변수명을 감싸는 습관을 들이면 더 좋다. 필요할 때 정규식 등을 통해 검색하거나 일괄 변환할 때도 중괄호를 쓴 경우 처리가 더 쉽고, 코드를 사람이 읽을 때도 문자열 템플릿 안에서 변수가 쓰인 부분을 더 쉽게 식별할 수 있다.
문자열 템플릿 안에 사용할 수 있는 대상은 간단한 변수 이름만으로 한정되지 않는다. 복잡한 식도 중괄호({})로 둘러싸서 문자열 템플릿 안에 넣을 수 있다.
fun main(args: Array<String>) {
if (args.size > 0) {
println("Hello, ${args[0]}!") // args 배열의 첫번째 원소를 넣기위해 ${} 구문을 사용.
}
}
중괄호로 둘러싼 식 안에서 큰 따옴표를 사용할 수도 있다.
fun main(args: Array<String>) {
println("Hello, ${if (args.size > 0) args[0] else "someone"}!")
}
심지어 중괄호로 둘러싼 식 안에서 문자열 템플릿을 사용해도 된다.
"${if(s.length > 2) "too short" else "normal string ${s}"}"
클래스와 프로퍼티
코틀린을 활용하면 더 적은 양의 코드로 클래스와 관련있는 대부분의 작업을 수행할 수 있다. 코드가 없이 데이터만 저장하는 클래스를 값 객체(value object)라 부르며, 다양한 언어가 값 객체를 간결하게 기술할 수 있는 구문을 제공한다. 자바를 코틀린으로 변환하게 되면 public 가시성 변경자가 사라지게 되는데, 코틀린의 기본 가시성은 public이므로 이런 경우 변경자를 생략해도 된다.
1. 프로퍼티
클래스라는 개념의 목적은 데이터를 캡슐화하고 캡슐화한 데이터를 다루는 코드를 한 주체 아래 가두는 것이다. 자바에서는 데이털르 필드에 저장하며, 멤버 필드의 가시성은 보통 비공개다. 클래스는 자신을 사용하는 칼리언트가 그 데이터에 접근하는 통로로 쓸수 있는 접근자 메소드(accessor method)를 제공한다. 보통은 필드를 읽기 위한 게터(getter)를 제공하고 필드를 변경하게 허용해야 할 경우 세터(setter)를 추가 제공할 수 있다. 세터는 자신이 받은 값을 검증하거나 필드 변경을 다른 곳에 통지하는 등의 로직을 더 가질 수 있다.
자바에서는 필드와 접근자를 한데 묶어 프로퍼티라고 부르며, 프로퍼티라는 개념을 활용하는 프레임워크가 많다. 코틀린은 프로퍼티를 언어 기본 기능으로 제공하며, 코틀린 프로퍼티는 자바의 필드와 접근자 메소드를 완전히 대신한다. 클래스에서 프로퍼티를 선언할 때는 앞에서 살펴본 변수를 선언하는 방법과 마찬가지로 val이나 var를 사용한다. val로 선언한 프로퍼티는 읽기 전용이며, var로 선언한 프로퍼티는 변경 가능하다.
class Person {
val name: String, // 읽기 전용 프로퍼티로, 코틀린은 (비공개) 필드와 필드를 읽는 단순한 (공개) 게터를 만들어낸다.
var isMarried: Boolean // 쓸 수 있는 프로퍼티로, 코틀린은 (비공개) 필드, (공개) 게터, (공개) 세터를 만들어낸다.
}
기본적으로 코틀린에서 프로퍼티를 선언하는 방식은 프로퍼티와 관련있는 접근자를 선언하는 것이다. 읽기 전용 프로퍼티의 경우 게터만 선언하며 변경할 수 있는 프로퍼티의 경우 게터와 세터를 모두 선언한다. 코틀린은 값을 저장하기 위한 비공개 필드와 그 필드에 값을 저장하기 위한 세터, 필드의 값을 읽기 위한 게터로 이뤄진 간단한 디폴트 접근자 구현을 제공한다.
게터와 세터의 이름을 정하는 규칙에는 예외가 있다. 이름이 is로 시작하는 프로퍼티의 게터에는 get이 붙지 않고 원래 이름을 그대로 사용하며, 세터에는 is를 set으로 바꾼 이름을 사용한다. 따라서 자바에서는 isMerried 프로퍼티의 게터를 호출하려면 isMerried()를 사용해야 한다.
자바에서 선언한 클래스에 대해 코틀린 문법을 사용해도 된다. 코틀린에서는 자바 클래스의 getter를 val 프로퍼티처럼 사용할 수 있고, 게터/세터 쌍이 있는 경우에는 var 프로퍼티처럼 사용할 수 있다. 예를 들어 setName과 getName이라는 접근자를 제공하는 자바 클래스를 코틀린에서 사용할 때는 name이라는 프로퍼티를 사용할 수 있다. 자바 클래스가 isMarried와 setMarried 메소드를 제공한다면 그에 상응하는 코틀린 프로퍼티의 이름은 isMarried다.
대부분의 프로퍼티에는 그 프로퍼티의 값을 저장하기 위한 필드가 있다. 이를 프로퍼티를 뒷받침하는 필드(backing field)라고 부른다. 하지만 원한다면 프로퍼티 값을 그때그때 계산할 수도 있다. 커스텀 게터를 작성하면 그런 프로퍼티를 만들수 있다.
코틀린 소스코드 구조: 디렉토리와 패키지
자바의 경우 모든 클래스를 패키지 단위로 관리한다. 코틀린에도 자바와 비슷한 개념의 패키지가 있다. 모든 코틀린 파일의 맨 앞에 package 문을 넣을 수 있다. 그러면 그 파일 안에 있는 모든 선언(클래스, 함수, 프로퍼티 등)이 해당 패키지에 들어간다. 같은 패키지에 속해 있다면 다른 파일에서 정의한 선언일지라도 직접 사용할 수 있다. 반면 다른 패키지에서 정의한 선언을 사요하려면 임포트를 통해 선언을 불러와야 한다. 자바와 마찬가지로 임포트문은 파일의 맨 앞에 와야하며 import 키워드를 사용한다.
코틀린에서는 클래스 임포트와 함수 임포트에 차이가 없으며, 모든 선언을 Import 키워드로 가져올 수 있다. 최상위 함수는 그 이름을 써서 임포트할 수 있다.
package geometry.example
import geometry.shapes.createRandomRectangle // 이름으로 함수 임포트하기
fun main(args: Array<String>) {
println(createRandomRectangle().isSquare)
}
패키지 이름 뒤에 .*를 추가하면 패키지 안의 모든 선언을 임포트할 수 있다. 이런 스타 임포트(star import)를 사용하면 패키지 안에 있는 모든 클래스뿐 아니라 최상위에 정의된 함수나 프로퍼티까지 모두 불러오게 된다.
자바에서는 패키지의 구조와 일치하는 디렉토리 계층 구조를 만들고 클래스의 소스코드를 그 클래스가 속한 패키지와 같은 디렉토리에 위치시켜야 한다. (자바에서는 디렉토리 구조가 패키지 구조를 그대로 따라야 한다)
코틀린에서는 여러 클래스를 한 파일에 넣을 수 있고, 파일의 이름도 마음대로 정할 수 있다. 코틀린에서는 디스크상의 어느 디렉토리에 소스 코드 파일을 위치시키든 관계없다. 따라서 원하시는대로 소스코드를 구성할 수 있다. 예를 들어 geometry.shapes라는 패키지가 있다면 그 패키지의 모든 내용을 shapes.kt라는 파일에 넣고, 하위 패키지에 해당하는 별도의 디렉토리를 만들지 않고 geometry라는 폴더 안에 shapes.kt를 넣어도 된다.
하지만 대부분의 경우 자바와 같이 패키지별로 디겍토리를 구성하는 편이 낫다. 특히 자바와 코틀린을 함께 사용하는 프로젝트에서는 자바의 방식을 따르는게 중요하다. 자바의 방식을 따르지 않으면 자바 클래스를 코틀린 클래스로 마이그레이션할 때 문제가 생길 수도 있다. 하지만 여러 클래스를 한 파일에 넣는 것을 주저해서는 안된다. 특히 각 클래스를 정의하는 소스코드 크기가 아주 작은 경우 더욱 그렇다.
선택 표현과 처리: enum과 when
when은 자바의 switch를 대치하되 훨씬 더 강력하다. when에 대해 설명하는 과정에서 코틀린에서 enum을 선언하는 방법과 smart cast에 대해서도 살펴본다.
1. enum 클래스 정의
enum은 자바 선언보다 코틀린 선언에 더 많은 키워드를 써야 하는 흔치 않은 예다. 코틀린에서는 enum class를 사용하지만 자바에서는 enum을 사용한다. 코틀린에서 enum은 soft keyword라 부르는 존재다. enum은 class 앞에 있을때는 특별한 의미를 지니지만 다른 곳에서는 이름에 사용할 수 있다. 반면 class는 키워드다. 따라서 class라는 이름을 사용할 수 없으므로 클래스를 표현하는 변수 등을 정의할 때는 clazz나 aClass와 같은 이름을 사용해야 한다.
enum class Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}
자바와 마찬가지로 enum은 단순히 값만 열거하는 존재가 아니다. enum 클래스 안에도 프로퍼티나 메소드를 정의할 수 있다. 다음은 프로퍼티와 메소드를 enum 안에 선언하는 방법을 보여준다.
enum class Color(
val r: Int, val g: Int, val b: Int // 상수의 프로퍼티를 정의한다.
) {
RED(255, 0, 0), ORANGE(255, 165, 0), // 각 상수를 생성할 때 그에 대한 프로퍼티 값을 지정한다.
YELLOW(255, 255, 0), GREEN(0, 255, 0), BLUE(0, 0, 255),
INDIGO(75, 0, 130), VIOLET(238, 130, 238); // 여기 반드시 세미콜론을 사용해야 한다.
fun rgb() = (r * 256 + g) * 256 + b // enum 클래스 안에서 메소드를 정의한다
}
2. when으로 enum 클래스 다루기
자바 switch문에 해당하는 코틀린 구성요소는 when이다. if와 마찬가지로 when도 값을 만들어내는 식이다. 따라서 식이 본문인 함수에 when을 바로 사용할 수 있다. 다음은 식을 본문으로 하는 함수의 예제이기도 하다.
fun getMnemoic(color: Color) = // 함수의 반환 값으로 when 식을 직접 사용한다.
when (color) { // 색이 특정 enum 상수와 같을때 그 상수에 대응하는 문자열을 돌려준다.
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "York"
Color.GREEN -> "Gave"
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}
앞의 코드는 color로 전달된 값과 같은 분기를 찾는다. 자바와 달리 각 분기의 끝에 break를 넣지 않아도 된다. 성공적으로 매치되는 분기를 찾으면 switch는 그 분기를 실행한다. 한 분기 안에서 여러 값을 매치 패턴으로 사용할 수도 있다. 그럴 경우 값 사이를 콤마(,)로 분리한다.
fun getWarmth(color: Color) = when(color) {
Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
Color.GREEN -> "neutral"
Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}
3. when과 임의의 객체를 함께 사용
코틀린에서 when은 자바의 switch 보다 훨씬 더 강력하다. 분기 조건에 상수(enum 상수나 숫자 리터럴)만을 사용할 수 있는 자바 switch와 달리 코틀린 when의 분기 조건은 임의의 객체를 허용한다. 두 색을 혼합했을 때 미리 정해진 팔레트에 들어있는 색이 될 수 있는지 알려주는 함수를 작성해본다.
fun mix(c1: Color, c2: Color) =
/*
* when 식의 인자로 아무 객체나 사용할 수 있다.
* when은 이렇게 인자로 받은 객체가 각 분기 조건에 있는 객체와 같은지 테스트한다.
*/
when (setOf(c1, c2)) {
setOf(RED, YELLOW) -> ORANGE // 두 색을 조합해서 다른 색을 만들 수 있는 경우를 열겨.
setOf(YELLOW, BLUE) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
else -> throw Exception("Dirty color") // 매치되는 분기 조건이 없으면 이 문장을 실행
}
c1과 c2가 RED와 YELLOW라면 그 둘을 혼합한 결과는 ORANGE다. 이를 구현하기 위해 집합 비교를 사용한다. set은 원소가 모여 있는 컬렉션으로, 각 원소의 순서는 중요하지 않다. 모든 분기식에서 만족하는 조건을 찾을수 없다면 else 분기의 문장을 계산한다.
when의 분기 조건 부분에 식을 넣을수 있기 때문에 많은 경우 코드를 더 간결하고 아름답게 작성할 수 있다.
4. 인자없는 when 사용
하지만 위 예제는 약간 비효율적이다. 이 함수는 호출될때마다 함수 인자로 주어진 두 색이 when의 분기조건에 있는 다른 두색과 같은지 비교하기 위해 여러 Set 인스턴스를 생성한다. 보통은 이런 비효율성이 크게 문제가 되지 않는다. 하지만 이 함수가 아주 자주 호출된다면 불필요한 가비지 객체가 늘어나는것을 방지하기 위해 함수를 고쳐 쓰는 편이 낫다. 인자가 없는 when 식을 사용하면 불필요한 객체 생성을 막을 수 있다. 코드는 약간 읽기 어려워지지만 성능을 더 향상시키기 위해 그정도 비용을 감수해야하는 경우도 자주 있다.
fun mixOptimized(c1: Color, c2: Color) =
when {
(c1 == RED && c2 == YELLOW) ||
(c1 == YELLOW && c2 == RED) ->
ORANGE
(c1 == YELLOW && c2 == BLUE) ||
(c1 == BLUE && c2 == YELLOW) ->
GREEN
(c1 == BLUE && c2 == VIOLET) ||
(c1 == VIOLET && c2 == BLUE) ->
INDIGO
else -> throw Exception("Dirty color")
}
when에 아무 인자도 없으려면 각 분기의 조건이 불리언 결과를 계산하는 식이여야 한다. mixOptimized는 추가 객체를 만들지 않는다는 장점이 있지만 가독성은 더 떨어진다.
5. 스마트 캐스트: 타입 검사와 타입 캐스트를 조합
(1 + 2) + 4 와 같은 간단한 산술식을 계산하는 함수를 만들어본다. 함수가 받을 산술식에서는 오직 두 수를 더하는 연산만 가능하다. 다른 연산(뺄셈, 곱셈, 나눗셈)도 비슷한 방식으로 구현할 수 있다.
우선 식을 인코딩하는 방법을 생각해야 한다. 식을 트리 구조로 저장하자. 노드는 합계(Sum)나 수(Num) 중 하나다. Num은 항상 말단(leaf 또는 terminal) 노드지만, Sum은 자식이 둘 있는 중간(non-terminal) 노드이다. Sum 노드의 두 자식은 덧셈의 두 인자다. 다음 리스트는 식을 표현하는 간단한 클래스를 보여준다. 식을 위한 Expr 인터페이스가 있고, Sum과 Num 클래스는 그 Expr 인터페이스를 구현한다. Expr은 아무 메소드도 선언하지 않으며, 단지 여러 타입의 식 객체를 아우르는 공통 타입 역할만 수행한다. 클래스가 구현하는 인터페이스를 지정하기 위해서 콜론(:) 뒤에 인터페이스 이름을 사용한다.
interface Expr
// value라는 프로퍼티만 존재하는 단순한 클래스로 Expr 인터페이스를 구현한다.
class Num(val value: Int) : Expr
// Expr 타입의 객체라면 어떤 것이나 Sum 연산의 인자가 될 수 있다. 따라서 Num이나 다른 Sum이 인자로 올 수 있다.
class Sum(val left: Expr, val right: Expr) : Expr
Sum은 Expr의 왼쪽과 오른쪽 인자에 대한 참조를 left와 right 프로퍼티로 저장한다. (1 + 2) + 4 라는 식을 저장하면 Sum(Sum(Num(1), Num(2)), Num(4)) 라는 구조의 객체가 생긴다.
Expr 인터페이스에는 두 가지 구현 클래스가 존재한다. 따라서 식을 평가하려면 두가지 경우를 고려해야 한다.
- 어떤 식이 수라면 그 값을 반환한다.
- 어떤 식이 합계라면 좌항과 우항의 값을 계산한 다음에 그 두 값을 합한 값을 반환한다.
fun eval(e: Expr): Int {
if (e is Num) {
val n = e as Num // 여기서 Num으로 타입을 변환하는데, 이는 불필요한 중복이다.
return n.value
}
if (e is Sum) {
return eval(e.right) + eval(e.left) // 변수 e에 대한 스마트 캐스트를 사용한다.
}
throw IllegalArgumentException("Unknown expression")
}
코틀린에서는 is를 사용해 변수 타입을 검사한다. is 검사는 자바의 instanceof와 비슷하다. 하지만 자바에서 어떤 변수의 타입을 instanceof로 확인한 다음에 그 타입에 속한 멤버에 접근하기 위해서는 명시적으로 변수 타입을 캐스팅해야 한다. 이런 멤버 접근을 여러번 수행해야 한다면 변수에 따로 캐스팅한 결과를 저장한 후 사용해야 한다. 코틀린에서는 프로그래머 대신 컴파일러가 캐스팅을 해준다. 어떤 변수가 원하는 타입인지 일단 is로 검사하고 나면 굳이 변수를 원하는 타입으로 캐스팅하지 않아도 마치 처음부터 그 변수가 원하는 타입으로 선언된 것처럼 사용할 수 있다. 하지만 실제로는 컴파일러가 캐스팅을 수행해준다. 이를 스마트 캐스트(smart cast)라고 부른다.
eval 함수에서 e의 타입이 Num인지 검사한 다음 부분에서 컴파일러는 e의 타입을 Num으로 해석하다. 스마트 캐스트는 is로 변수에 든 값의 타입을 검사한 다음에 그 값이 바뀔 수 없는 경우에만 작동한다. 클래스의 프로퍼티에 대해 스마트 캐스트를 사용한다면 그 프로퍼티는 반드시 val이어야 하며 커스텀 접근자를 사용한 것이어도 안된다. val이 아니거나 val이지만 커스텀 접근자를 사용하는 경우에는 해당 프로퍼티에 대한 접근이 항상 같은 값을 내놓는다고 확신할 수 없기 때문이다. 원하는 타입으로 명시적으로 타입 캐스팅하려면 as 키워드를 사용한다.
val n = e as Num
코틀린의 if와 자바의 if는 어떻게 다를까? 코틀린의 if (a > b) a else b는 자바의 a >b ? a : b처럼 작동한다. 코틀린에서는 if가 값을 만들어내기 때문에 자바와 달리 3항 연산자가 따로 없다. 이런 특성을 사용하면 eval 함수에서 return문과 중괄호를 없애고 if 식을 본문으로 사용해 더 간단하게 만들 수 있다.
fun eval(e: Expr) : Int =
if (e is Num) {
e.value
} else if (e is Sum) {
eval(e.right) + eval(e.left)
} else {
throw IllegalArgumentException("Unknown expression")
}
if의 분기에 식이 하나밖에 없다면 중괄호를 생략해도 된다. if 분기에 블록을 사용하는 경우 그 블록의 마지막 식이 그 분기의 결과 값이다. 이 코드를 when을 사용해 더 다듬을 수도 있다.
fun eval(e: Expr): Int =
when (e) {
is Num -> // 인자 타입을 검사하는 when 분기
e.value // 이부분에 smart cast가 쓰였다.
is Sum -> // 인자 타입을 검사하는 when 분기
eval(e.right) + eval(e.left) // 이부분에 smart cast가 쓰였다.
else ->
throw IllegalArgumentException("Unknown expression")
}
when 식을 앞에서 살펴본 값 동등성 검사가 아닌 다른 기능에도 쓸 수 있다. if 예제와 마찬가지로 타입을 검사하고 나면 스마트 캐스트가 이뤄진다. 따라서 Num이나 Sum의 멤버에 접근할 때 변수를 강제로 캐스팅할 필요가 없다.
자바에서 익숙하게 사용해 온 개념을 코틀린으로 바꾸면 보통 코틀린이 더 간결하고 읽기 좋은 코드를 만들어낸다. 확장을 통해 자바 라이브러리를 활용하면 코틀린과 자바를 함께 쓰는 프로젝트에서 코틀린의 장점을 최대한 살릴 수 있다.
코틀린에서 컬렉션 만들기
val set = hashSetOf(1, 7, 53)
비슷한 방법으로 리스트와 맵도 만들 수 있다.
val list = arrayListOf(1, 7, 53)
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
여기서 to가 언어가 제공하는 특별한 키워드가 아니라 일반 함수라는 점에 유의해야 한다.
>>> println(set.javaClass)
class java.util.HashSet
>>> println(list.javaClass)
class java.util.ArrayList
>>> println(map.javaClass)
class java.util.HashMap
위 결과는 코틀린이 자신만의 컬렉션 기능을 제공하지 않는다는 의미이다. 코틀린이 자체 컬렉션을 제공하지 않는 이유는 뭘까? 표준 자바 컬렉션을 활용하면 자바 코드와 상호작용하기가 훨씬 더 쉽다. 자바에서 코틀린 함수를 호출하거나 코틀린에서 자바 함수를 호출할 때 자바와 코틀린 컬렉션을 서로 변환할 필요가 없다.
코틀린 컬렉션은 자바 컬렉션과 똑같은 클래스다. 하지만 코틀린에서는 자바보다 더 많은 기능을 쓸 수 있다. 예를 들어 리스트의 마지막 원소를 가져오거나 수로 이뤄진 컬렉션에서 최댓값을 찾을 수 있다.
>>> val strings = listOf("first", "second", "fourteenth")
>>> println(strings.last())
fourteenth
>>> val numbers = setOf(1, 14, 2)
>>> println(numbers.max())
14
함수를 호출하기 쉽게 만들기
자바 컬렉션에는 디폴트 toString 구현이 들어있다. 하지만 그 디폴트 toString의 출력 형식은 고정돼 있고 필요한 형식이 아닐수 있다. 디폴트 구현이 아닌 다른 형식을 출력하고 싶을땐 어떻게 해야할까? 코틀린에서는 이런 요구사항을 처리할 수 있는 함수가 표준 라이브러리에 이미 들어있다.
처음에는 함수 선언을 간단하게 만들수 있게 코틀린이 지원하는 여러 기능을 사용하지 ㅇ낳고 함수를 직접 구현한다. 그 후에 좀더 코틀린답게 같은 함수를 다시 구현한다. 다음 리스트의 joinToString 함수는 컬렉션 원소를 StringBuilder의 뒤에 덧붙인다. 이때 원소 사이에 구분자를 추가하고, StringBuilder의 맨 앞과 맨 뒤에는 prefix와 postfix를 추가한다.
fun <T> joinToString(
collection: Collection<T>,
separator: String,
prefix: String,
postfix: String
): String {
val result = StringBuilder(prefix)
for ((index, element) in collection.withIndex()) {
if (index > 0) result.append(separator) // 첫 원소 앞에는 구분자를 붙이면 안된다.
result.apppend(element)
}
result.append(postfix)
return result.toString()
}
이 함수는 제네릭(generic)하다. 즉, 이 함수는 어떤 타입의 값을 원소로 하는 컬렉션이든 처리할 수 있다. 제네릭 함수의 문법은 자바와 비슷하다. 이 함수를 그대로 써도 좋지만, 선언 부분을 좀더 고민해봐야한다.
코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부(또는 전부)의 이름을 명시할 수 있다. 호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 한다.
함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아니라 함수 선언 쪽에서 지정된다. 따라서 어떤 클래스 안에 정의된 함수의 디폴트 값을 바꾸고 그 클래스가 포함된 파일을 재컴파일하면 그 함수를 호출하는 코드 중에 값을 지정하지 않은 모든 인자는 자동으로 바뀐 디폴트 값을 적용받는다.
디폴트 값과 자바
자바에는 디폴트 파라미터 값이라는 개념이 없어서 코틀린 함수를 자바에서 호출하는 경우에는 그 코틀린 함수가 디폴트 파라미터 값을 제공하더라도 모든 인자를 명시해야 한다. 자바에서 코를니 함수를 자주 호출해야 한다면 자바 쪽에서 좀더 편하게 코틀린 함수를 호출하고 싶을 것이다. 그럴때 @JvmOverloads 어노테이션을 함수에 추가할 수 있다. @JvmOverloads를 함수에 추가하면 코틀린 컴파일러가 자동으로 맨 마지막 파라미터로부터 파라미터를 하나씩 생략한 오버로딩한 자바 메소드를 추가해준다. 각각의 오버로딩한 함수들은 시그니처에서 생략된 파라미터에 대해 코틀린 함수의 디폴트 파라미터 값을 사용한다.
1. 정적인 유틸리티 클래스 없애기: 최상위 함수와 프로퍼티
자바에서는 모든 코드를 클래스의 메소드로 작성해야 한다. 보통 그런 구조는 잘 작동한다. 하지만 실전에서는 어느 한 클래스에 포함시키기 어려운 코드가 많이 생긴다. 일부 연산에는 비슷하게 중요한 역할을 하는 클래스가 둘 이상 있을 수도 있다. 그 결과 다양한 정적 메소드를 모아두는 역할만 담당하며, 특별한 상태나 인스턴스 메소드는 없는 클래스가 생겨난다. JDK의 Collections 클래스가 전형적인 예다. 코틀린에서는 이런 무의미한 클래스가 필요없다. 대신 함수를 직접 소스 파일의 최상위 수준, 모든 다른 클래스의 밖에 위치시키면 된다. 그런 함수들은 여전히 그 파일의 맨 앞에 정의된 패키지의 멤버 함수이므로 다른 패키지에서 그함수를 사용하고 싶을때는 그 함수가 정의된 패키지를 임포트해야만 한다. 하지만 임포트 시 유틸리티 클래스 이름이 추가로 들어갈 필요는 없다.
joinToString 함수를 strings 패키지에 직접 넣어본다. join.kt라는 파일을 다음과 같이 작성한다.
package strings
fun joinToString(...): String { ... }
JVM이 클래스 안에 들어있는 코드만을 실행할 수 있기 때문에 컴파일러는 이 파일을 컴파일할 때 새로운 클래스를 정의해준다. 코틀린만 사용하는 경우에는 그냥 그런 클래스가 생긴다는 사실만 기억하면 된다. 하지만 함수를 자바 등의 다른 JVM 언어에서 호출하고 싶다면 코드가 어떻게 컴파일되는지 알아야 joinToString과 같은 최상위 함수를 사용할 수 있다. 어떻게 코틀린이 join.kt를 컴파일하는지 보여주기 위해 join.kt를 컴파일한 결과와 같은 클래스를 자바 코드로 써보면 다음과 같다.
/* 자바 */
package strings;
public class JoinKt { // join.kt 파일에 해당하는 클래스
public static String joinToSTring(...) { ... }
}
코틀린 컴파일러가 생성하는 클래스의 이름은 최상위 함수가 들어있던 코틀린 소스파일의 이름과 대응한다. 코틀린 파일의 모든 최상위 함수는 이클래스의 정적인 메소드가 된다. 따라서 자바에서 joinToString을 호출하기는 쉽다.
/* 자바 */
import strings.JoinKt;
...
JoinKt.joinToString(list, ", ", "", "");
파일에 대응하는 클래스의 이름 변경하기
코틀린 최상위 함수가 포함되는 클래스의 이름을 바꾸고 싶다면 파일에 @JvmName 어노테이션을 추가한다. @JvmName 어노테이션은 파일의 맨 앞, 패키지 이름 선언 이전에 위치해야 한다.
@file:JvmName("StringFunctions") // 클래스 이름을 지정하는 어노테이션
package strings // @file:JvmName 어노테이션 뒤에 패키지문이 와야한다.
fun joinToString(...): String { ... }
이제 다음과 같이 joinToString 함수를 호출할 수 있다.
/* 자바 */
import strings.StringFunctions;
StringFunctions.joinToString(list, ", ", "", "");
최상위 프로퍼티
함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있다. 어떤 데이터를 클래스 밖에 위치시켜야 하는 경우는 흔하지는 않지만, 그래도 가끔 유용할때가 있다. 예를 들어 어떤 연산을 수행하는 횟수를 저장하는 var 프로퍼티를 만들 수도 있다.
var opCount = 0 // 최상위 프로퍼티를 선언한다.
fun performOperation() {
opCount++ // 최상위 프로퍼티의 값을 변경한다.
}
fun reportOperationCount() {
println("Operation performed ${opCount} times") // 최상위 프로퍼티의 값을 읽는다.
}
이런 프로퍼티의 값은 정적 필드에 저장된다. 최상위 프로퍼티를 활용에 코드에 상수를 추가할 수도 있다.
val UNIX_LINE_SEPARATOR = "\n"
기본적으로 최상위 프로퍼티도 다른 모드 프로퍼티처럼 접근자 메소드를 통해 자바 코드에 노출된다(val의 경우 게터, var의 경우 게터와 세터가 생긴다). 겉으론 상수처럼 보이는데, 실제로는 게터를 사용해야 한다면 자연스럽지 못하다. 더 자연스럽게 사용하려면 이 상수를 public static final 필드로 컴파일해야 한다. const 변경자를 추가하면 프로퍼티를 public static final 필드로 컴파일하게 만들 수 있다. (단, 원시 타입과 String 타입의 프로퍼티만 const로 지정할 수 있다)
const val UNIX_LINE_SEPARATOR = "\n"
앞의 코드는 다음 자바 코드와 동등한 바이트코드를 만들어낸다.
/* 자바 */
public static final String UNIX_LINE_SEPARATOR = "\n";
메소드를 다른 클래스에 추가: 확장 함수와 확장 프로퍼티
기존 코드와 코틀린 코드를 자연스럽게 통합하는 것은 코틀린의 핵심 목표 중 하나다. 완전히 코틀린만으로만 이뤄진 프로젝트조차도 JDK나 안드로이드 프레임워크 또는 다른 서드파티 프레임워크 등의 자바 라이브러리를 기반으로 만들어진다. 또 코틀린은 기존 자바 프로젝트에 통합하는 경우에는 코틀린으로 직접 변환할 수 없거나 미처 변환하지 않은 기존 자바 코드를 처리할 수 있어야 한다. 이런 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 여러 편리한 기능을 사용할 수 있다면 좋을 것이다. 바로 확장 함수(extension function)가 그런 역할을 해줄수 있다.
개념적으로 확장함수는 단순하다. 확장 함수는 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 그 클래스의 밖에서 선언된 함수다. 확장함수를 보여주기 위해 어떤 문자열의 마지막 묵자를 되돌려주는 메소드를 추가해본다.
pckage strings
fun String.lastChar() : Char = this.get(this.length - 1)
확장 함수를 만들려면 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이기만 하면 된다. 클래스 이름을 수신 객체 타입(receiver type)이라 부르며, 확장 함수가 호출되는 대상이 되는 값(객체)을 수신 객체(receiver object)라고 부른다. 수신 객체 타입은 확장이 정의될 클래스의 타입이며, 수신 객체는 그 클래스에 속한 인스턴스 객체다.
이 함수를 호출하는 구문은 다른 일반 클래스 멤버를 호출하는 구문과 똑같다.
println("Kotlin".lastChar())
이 예제에서는 String이 수신 객체 타입이고 "kotlin"이 수신 객체다. 어떤 면에서 이는 String 클래스에 새로운 메소드를 추가하는 것과 같다. String 클래스가 여러분이 직접 작성하 코드가 아니고 심지어 String 클래스의 소스코드를 소유한 것도 아니지만, 원하는 메소드를 String 클래스에 추가할 수 있다. 심지어 String이 자바나 코틀린 등의 언어 중 어느것으로 작성됐는가는 중요하지 않다. 예를 들어 Groovy와 같은 다른 JVM 언어로 작성된 클래스도 확장할 수 있다. 자바 클래스로 컴파일한 클래스 파일이 있는 한 그 클래스에 원하는 대로 확장을 추가할 수 있다.
일반 메소드의 본문에서 this를 사용할 때와 마찬가지로 확장 함수 본문에도 this를 쓸 수 있다. 그리고 일반 메소드와 마찬가지로 확장 함수 본문에서도 this를 생략할 수 있다.
package strings
fun String.lastChar(): Char = get(length - 1) // 수신 객체 멤버에 this 없이 접근할 수 있다
확장 함수 내부에서는 일반적인 인스턴스 메소드의 내부에서와 마찬가지로 수신 객체의 메소드나 프로퍼티를 바로 사용할 수 있다. 하지만 확장 함수가 캡슐화를 깨지는 않는다. 클래스 안에서 정의한 메소드와 달리 확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없다. 이제부터는 클래스의 멤버 메소드와 확장 함수를 모두 메소드라고 부르게 된다. "확장 함수 내부에서는 수신 객체의 모든 메소드를 호출할 수 있다"라고 말하면 확장 함수 내부에서 수신 객체의 멤버 메소드와 확장 함수를 모두 호출할 수 있다는 뜻이다. 호출하는 쪽에서는 확장 함수와 멤버 메소드를 구분할 수 없다. 그리고 호출하는 메소드가 확장 함수인지 멤버 메소드인지 여부가 중요한 경우도 거의 없다. 확장 함수는 코틀린 문법상 반드시 짧은 이름을 써야 한다. 따라서 임포트 시 이름을 바꾸는 것이 확장 함수의 이름 충돌을 해결하는 유일한 방법이다.
1. 자바에서 확장 함수 호출
내부적으로 확장 함수는 수신 객체를 첫번째 인자로 받는 정적 메소드이다. 그래서 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않는다. 이런 설계로 인해 자바에서 확장함수를 사용하기도 편하다. 단지 정적 메소드를 호출하면서 첫번째 인자로 수신 객체를 넘기기만 하면 된다. 다른 최상위 함수와 마찬가지로 확장 함수가 들어있는 자바 클래스 이름도 확장 함수가 들어있는 파일 이름에 따라 결정된다. 따라서 확장 함수를 StringUtil.kt 파일에 정의했다면 다음과 같이 호출할 수 있다.
/* 자바 */
char c = StringUtilKt.lastChar("Java");
확장 함수로 유틸리티 함수 정의
이제 joinToString 함수의 최종 버전을 만든다. 이제 이 함수는 코틀린 라이브러리가 제공하는 함수와 거의 같아졌다.
fun <T> Collection<T>.joinToString( // Collection<T>에 대한 확장 함수를 선언한다.
separator: String = ", ",
prefix: String = "",
postfix: String = ""
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) // this는 수신 객체를 가리킨다. 여기서는 T 타입의 원소로 이뤄진 컬렉션이다.
if (index > 0) result.append(separator)
result.append(element)
}
result.append(postfix)
return result.toString()
}
확장 함수는 단지 정적 메소드 호출에 대한 문법적인 편의(syntatic sugar)일 뿐이다. 그래서 클래스가 아닌 더 구체적인 타입을 수신객체 타입으로 지정할 수도 있다. 그래서 문자열의 컬렉션에 대해서만 호출할 수 있는 join 함수를 지정하고 싶다면 다음과 같이하면 된다.
fun Collection<String>.join(
separator: String = ", ",
prefix: String = "",
postfix: String = ""
) = joinToString(separator, prefix, postfix)
확장 함수가 정적 메소드와 같은 특징을 가지므로, 확장 함수를 하위 클래스에서 오버라이드할 수는 없다.
실행 시점에 객체 타입에 따라 동적으로 호출될 대상 메소드를 결정하는 방식을 동적 디스패치(dynamic dispatch)라고 한다. 반면 컴파일 시점에 알려진 변수 타입에 따라 정해진 메소드를 호출하는 방식을 정적 디스패치(static dispatch)라고 부른다. 프로그래밍 언어 용어에서 '정적'이란느 말은 컴파일 시점을 의미하고, '동적'이라는 말은 실행 시점을 의미한다.
확장 함수는 클래스의 일부가 아니다. 확장 함수는 클래스 밖에 선언된다. 이름과 파라미터가 완전히 같은 확장 함수를 기반 클래스와 하위 클래스에 대해 정의해도 실제로는 확장 함수를 호출할때 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장함수가 호출될지 결정되지, 그 변수에 저장된 객체의 동적인 타입에 의해 확장함수가 결정되지 않는다.
확장 함수를 오버라이드할 수는 없다. 코틀린은 호출될 확장 함수를 정적으로 결정하기 때문이다.
어떤 클래스를 확장한 함수와 그 클래스의 멤버 함수의 이름과 시그니처가 같다면 확장 함수가 아니라 멤버 함수가 호출된다(멤버 함수의 우선순위가 더 높다). 클래스의 API를 변경할 경우 항상 이를 염두에 둬야 한다. 코드 소유권을 가진 클래스에 대한 확장함수를 정의해서 사용하는 외부 클라이언트 프로젝트가 있다고 한다. 그 확장 함수와 이름과 시그니처가 같은 멤버 함수를 클래스 내부에 추가하면 클라이언트 프로젝트를 재컴파일한 순간부터 그 클라이언트는 확장 함수가 아닌 새로 추가된 멤버 함수를 사용하게 된다.
확장 프로퍼티
확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가할 수 있다. 프로퍼티라는 이름으로 불리기는 하지만 상태를 저장할 적절한 방법이 없기 때문에(기존 클래스의 인스턴스 객체에 필드를 추가할 방법은 없다) 실제로 확장 프로퍼티는 아무 상태도 가질 수 없다. 하지만 프로퍼티 문법으로 더 짧게 코드를 작성할 수 있어서 편한 경우가 있다.
val String.lastChar: Char
get() = get(length - 1)
확장 함수의 경우와 마찬가지로 확장 프로퍼티도 일반적인 프로퍼티와 같은데, 단지 수신 객체 클래스가 추가됐을 뿐이다. 뒷받침하는 필드가 없어서 기본 게터 구현을 제공할 수 없으므로 최소한 게터는 꼭 정의를 해야 한다. 마찬가지로 초기화 코드에서 계산한 값을 담을 장소가 전혀 없으므로 초기화 코드도 쓸수 없다.
StringBuilder에 같은 프로퍼티를 정의한다면 StringBuilder의 맨 마지막 문자는 변경 가능하므로 프로퍼티를 var로 만들수 있다.
var StringBuilder.lastChar: Char
get() = get(length - 1) // 프로퍼티 게터
set(value: Char) {
this.setCharAt(length - 1, value) // 프로퍼티 세터
}
코틀린에서 확장 프로퍼티를 사용하는 방법은 멤버 프로퍼티를 사용하는 방법과 같다. 자바에서 확장 프로퍼티를 사용하고 싶다면 항상 StringUtilKt.getLastChar("Java") 처럼 게터나 세터를 명시적으로 호출해야 한다.
가변 인자 함수: 인자의 갯수가 달라질 수 있는 함수 정의
리스트를 생성하는 함수를 호출할 때 원하는 만큼 원소를 전달할 수 있다.
val list = listOf(2, 3, 5, 7, 11)
라이브러리에서 이 함수의 정의를 보면 다음과 같다.
fun listOf<T>(vararg values: T): List<T> { ... }
자바의 가변 길이 인자(varargs)에 보다 익숙할 것이다. 가변 길이 인자는 메소드를 호출할 때 원하는 갯수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값들을 넣어주는 기능이다. 코틀린의 가변 길이 인자도 자바와 비슷하다. 다만 문법이 조금 다르다. 타입 뒤에 ...를 붙이는 대신 코틀린에서는 파라미터 앞에 vararg 변경자를 붙인다.
이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때도 코틀린과 자바 구문이 다르다. 자바에서는 배열을 그냥 넘기면 되지만 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야 한다. 기술적으로는 스프레드(spread) 연산자가 그런 작업을 해준다. 하지만 실제로는 전달하려는 배열 앞에 *를 붙이기만 하면 된다.
fun main(args: Array<String>) {
val list = listOf("args: ", *args) // 스프레드 연산자가 배열의 내용을 펼쳐준다.
println(list)
}
이 예제는 스프레드 연산자를 통하면 배열에 들어있는 값과 다른 여러 값을 함께 써서 함수를 호출할 수 있음을 보여준다. 이런 기능은 자바에서는 사용할 수 없다. 이제 맵으로 대상을 옮겨서 코틀린 함수 호출의 가독성을 향상시킬 수 있는 다른 방법인 중위 호출에 대해 살펴본다.
값의 쌍 다루기: 중위 호출과 구조 분해 선언
맵을 만들려면 mapOf 함수를 사용한다.
val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
여기서 to라는 단어는 코틀린 키워드가 아니다. 이 코드는 중위 호출(infix call)이라는 특별한 방식으로 to라는 일반 메소드를 호출한 것이다.
중위 호출 시에는 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣는다. (이때 객체, 메소드 이름, 유일한 인자 사이에는 공백이 들어가야 한다). 다음 두 호출은 동일하다.
1.to("one") // "to" 메소드를 일반적인 방식으로 호출함
1 to "one" // "to" 메소드를 중위 호출 방식으로 호출함
인자가 하나뿐인 메소드나 인자가 하나뿐인 확장 함수에 중위 호출을 사용할 수 있다. 함수(메소드)를 중위 호출에 사용하게 허용하고 싶으면 infix 변경자를 함수 선언 앞에 추가해야 한다. 다음은 to 함수의 정의를 간략하게 줄인 코드다.
infix fun Any.to(other: Any) = Pair(this, other)
이 to 함수는 Pair의 인스턴스를 반환한다. Pair는 코틀린 표준 라이브러리 클래스로 그 이름대로 두 원소로 이뤄진 순서쌍을 표현한다. 실제로 to는 제네릭 함수지만 여기서는 설명을 위해 그런 세부 사항을 생략했다.
Pair의 내용으로 두 변수를 즉시 초기화할 수 있다.
val (number, name) = 1 to "one"
이런 기능을 구조 분해 선언(destructuring declaration)이라고 부른다. 다음은 Pair에 대해 구조 분해가 어떻게 작동하는지 보여준다.
Pair 인스턴스 외 다른 객체에도 구조 분해를 적용할 수 있다. 예를 들어 key와 value라는 두 변수를 맵의 원소를 사용해 초기화할 수 있다. 루프에서도 구조 분해 선언을 활용할 수 있다. joinToString에서 본 withIndex를 구조 분해 선언과 조합하면 컬렉션 원소의 인덱스와 값을 따로 변수에 담을 수 있다.
for ((index, element) in collection.withIndex()) {
println("${index}: ${element}")
}
to 함수는 확장함수다. to를 사용하면 타입과 관계없이 임의의 순서쌍을 만들 수 있다. 이는 to의 수신 객체가 제네릭하다는 뜻이다. 1 to "one", "one" to 1, list to list.size() 등의 호출이 모두 잘 작동한다. mapOf 함수의 선언을 살펴보자.
fun <K, V> mapOf(vararg values: Pair<K, V>): Map<K, V>
listOf와 마찬가지로 mapOf에도 원하는 갯수만큼 인자를 전달할 수 있다. 하지만 mapOf의 경우에는 각 인자와 키와 값으로 이뤄진 순서쌍이어야 한다.
코틀린을 잘 모르는 사람이 보면 새로운 맵을 만드는 구문은 코틀린이 맵에 대해 제공하는 특별한 문법인 것처럼 느껴진다. 하지만 실제로는 일반적인 함수를 더 간결한 구문으로 호출하는 것뿐이다.
'프로그래밍(TA, AA) > JVM 언어' 카테고리의 다른 글
[kotlin] 스프링 프레임워크 개발 (0) | 2020.04.22 |
---|---|
[kotlin] 코틀린 인 액션(2) - 클래스, 객체, 인터페이스 (0) | 2020.04.21 |
[RxJava] RxJava 프로그래밍(4) - 디버깅과 예외 처리 (0) | 2020.04.16 |
[Spring] Spring WebFlux와 Kotlin으로 만드는 Todo 서비스 (0) | 2020.04.15 |
[RxJava] RxJava 프로그래밍(3) - 스케줄러 (0) | 2020.04.12 |