유용하(indy.jones) / kakao corp.(톡메시징파트)
---
JVM 기반 언어의 코틀린은 자바의 생태계와 완전히 호환되면서도 간결하고 안전한 코드를 위한 문법을 가진 언어다. 이런 장점으로 최근 안드로이드를 중심으로 점차 자바를 대체해 가고 있지만, 아직 안정성과 성능에 보수적인 서버 분야에서 자바의 지위는 견고해 보인다. 하지만 기존의 자바 기반 프레임웍과도 완벽히 호환되는 코틀린은 서버사이드에서도 도입하지 않을 이유가 없으며 성공적인 도입 사례가 늘어날수록 그 추세는 가속화될 것이다. 현재 카카오톡의 일부 서버들도 코틀린으로 개발되어 대량의 요청을 안정적으로 서비스하고 있고 그 영역은 점점 늘어나고 있다. 이 세션에서는 다양한 서버 분야의 코틀린 도입에 도움이 될 만한 카카오톡 서버의 코틀린 적용 경험을 공유한다.
11. 2017~
카카오톡 서버 구조 개선 프로젝트
개발 언어로 코틀린 (for JVM) 도입
기존 컴포넌트 개선을 위한 대체, 서비스 투입
신규 플랫폼용 컴포넌트 개발, 검증용도 서비스 투입2017
2018
Brewery
12. 코틀린 (for JVM) 선택의 배경
✔ 적정한 성능/안정성, 개발/운영의 편의성
✔ 다양한 목적의 서버 환경에 검증된 자바의 생태계를 이용
✔ 생산성이 좋은 새로운 언어
✔ 적정 수준의 러닝 커브
13. 도입을 고민할 때 할 만한 걱정들
개발 환경은 쓸 만 한가요?
언어로 생산성과 안전함이 향상된다?
잘 돌아가나요?
자바와 어느 정도 호환되나요?
문법
그래서
생태계
개발 환경
14. 개발 환경은 쓸 만 한가요?
언어로 생산성과 안전함이 향상된다?
잘 돌아가나요?
자바와 어느 정도 호환되나요?
문법
그래서
생태계
개발 환경
15. 버전 업데이트 시 변화가 크지 않음
1.0 ~ 1.3(preview)의 문법, API 하위 호환
역사
https://github.com/JetBrains/Kotlin
1.0.0
오픈소스
1.1.0 1.2.0
안드로이드 공식 지원
M1~M14, 1.0 Beta 1~4 1.3-M1
릴리즈
하위 호환
25. 개발 환경은 쓸 만 한가요?
언어로 생산성과 안전함이 향상된다?
잘 돌아가나요?
자바와 어느 정도 호환되나요?
문법
그래서
생태계
개발 환경
26. 중복 최소화
// Java
class Host {
private final String name;
private final int port;
private int weight;
Host(String name, int port, int weight) {
this.name = name;
this.port = port;
this.weight = weight;
}
public String getName() { return name; }
public int getPort() { return port; }
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
@Override
public boolean equals(Object obj) { return false; }
@Override
public int hashCode() { return 0; }
@Override
public String toString() { return ""; }
}
// Java (with Lombok)
import lombok.*;
@AllArgsConstructor
@ToString
@EqualsAndHashCode
class Host {
@Getter @NonNull private final String name;
@Getter private final int port;
@Getter @Setter private int weight;
}
// Java (with Lombok)
import lombok.*;
@AllArgsConstructor
@ToString
@EqualsAndHashCode
class Host {
@Getter @NonNull private final String name;
@Getter private final int port;
@Getter @Setter private int weight;
}
Too many annotations
Lombok
27. 중복 최소화
// Java
class Host {
private final String name;
private final int port;
private int weight;
Host(String name, int port, int weight) {
this.name = name;
this.port = port;
this.weight = weight;
}
public String getName() { return name; }
public int getPort() { return port; }
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
@Override
public boolean equals(Object obj) { return false; }
@Override
public int hashCode() { return 0; }
@Override
public String toString() { return ""; }
}
// Java (with Lombok)
import lombok.*;
@AllArgsConstructor
@ToString
@EqualsAndHashCode
class Host {
@Getter @NonNull private final String name;
@Getter private final int port;
@Getter @Setter private int weight;
}
Lombok
Kotlin
// Kotlin
data class Host(
val name: String,
val port: Int,
var weight: Int
)
28. 중복 최소화
// Java
class Host {
private final String name;
private final int port;
private int weight;
Host(String name, int port, int weight) {
this.name = name;
this.port = port;
this.weight = weight;
}
public String getName() { return name; }
public int getPort() { return port; }
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
@Override
public boolean equals(Object obj) { return false; }
@Override
public int hashCode() { return 0; }
@Override
public String toString() { return ""; }
}
Kotlin
// Kotlin
data class Host(
val name: String,
val port: Int,
var weight: Int
)
val host = Host("kakao.com", 443, 1)
println("${host.name}:${host.port}")
host.weight = 2
29. 안전한 코드
코딩의 실수로 인한 오류를 방지하는 방법
✔ 유효한 패턴 적용, 안티 패턴을 지양
✔ 테스트 작성
✔ 공개된 API의 문서화
✔ 문법에서 실수 가능성을 막아 컴파일 시점에 오류를 방지
30. 안전한 코드
정적 타입: 더 엄격한 타입 체크
Nullable 타입을 문법에서 구분
Primitive 타입, Boxed 타입 구분 없음:
Int, Long, Float, Double, Byte, Short, Char, Boolean 클래스 이용
특수 타입: Any, Nothing, Unit
개선된 타입 시스템
31. 안전한 코드: Nullable 타입
Not-null Type Nullable Type
var str: String
str = null str = null
if (str != null) {
val len: Int = str.length
}
val len: Int? = str?.length
val len: Int = str?.length ?: 0
var str: String?
Smart Cast
“컴파일 시점”에 NPE를 방지한다.
Optional을 문법에서 강제한다.
그래도 자바와 호환 과정에서는 발생할 수 있다.
val len: Int = str.length val len: Int = str.length
32. 안전한 코드: 불변 컬렉션
list.set(0, 100)
list[0] = 100
list.add(100)
list.remove(0)
map.put("key", 1)
map["key"] = 1
map.remove("key")
“컴파일 시점”에 자료의 변경을 막는다.
그래도 자바에 넘겼을 때는 변경될 수 있다.
Immutable List Immutable Map
val list: List<Int>
val i = list.get(0)
val i = list[0]
val map: Map<String, Int>
val i = map.get("key")
val i = map["key"]
변경용 API 변경용 API
33. 안전한 코드: 불변 컬렉션
list.set(0, 100)
list[0] = 100
list.add(100)
list.remove(0)
map.put("key", 1)
map["key"] = 1
map.remove("key")
Mutable List Mutable Map
val list: MutableList<Int>
val i = list.get(0)
val i = list[0]
val map: MutableMap<String, Int>
val i = map.get("key")
val i = map["key"]
변경용 API 변경용 API
* List<T>의 sub-interface * Map<K, V>의 sub-interface
34. 안전한 코드: final
불변 변수 val를 권장 (final 변수)
상속 제한이 디폴트 (final 클래스, 함수)
open class PushServiceImpl(private val pushService: PushService)
: PushService {
open override fun push(request: PushRequest) {
// ...
}
}
val max = 1 // final
max = 100
의외로 변경이 필요한 변수는 많지 않다.
통제되지 않은 상속에서 문제가 발생한다.
단 Spring AOP는 open이 필요. 😳 -> kotlin-spring 플러그인
35. 명확한 코드
val properties: Properties = // ...
val timeout = properties.getInt("timeout")
fun Properties.getInt(key: String): Int =
this.getProperty(key).toInt()
// Java (decompiled - pseudo code)
class PropertiesUtilsKt {
static int getInt(Properties receiver, String key) {
return Integer.parseInt(receiver.getProperty(key));
}
}
// Java
int timeout = PropertiesUtilsKt.getInt(properties, "timeout");
확장함수
38. 명확한 코드
if (StringUtils.isEmpty(str)) return
if (str.isNullOrEmpty()) return
😃 Utility, Helper 클래스 대체 (의존성 제거 가능)
😃 클래스의 핵심 기능과 확장 기능 구현 분리
😃 상속 없이 기존의 클래스의 기능을 추가
😞 기존 클래스에 새 인터페이스 적용은 안 됨
확장함수
39. 간결함
스마트 캐스트 (Smart Cast)
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (msg is HttpRequest) {
if (msg.method() == HttpMethod.GET) {
// ...
}
}
}
HttpRequest req = (HttpRequest) msg;
if (req.method().equals(HttpMethod.GET)) {
// ...
}
40. 간결함
스마트 캐스트 (Smart Cast)
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (msg is HttpRequest) {
if (msg.method() == HttpMethod.GET) {
// ...
}
}
}
41. 간결함
스마트 캐스트 (Smart Cast)
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (msg is HttpRequest) {
if (msg.method() == HttpMethod.GET) {
// ...
}
}
}
fun foo(str: String?) {
if (str == null) throw IllegalArgumentException()
val length = str.length
}
코틀린 방식의 Guard 처리
Not-null 타입 (String)
42. 간결함
val members = if (response.status == 0) {
logger.info("members: {}", response.members)
response.members
} else {
emptyList()
}
조건문도 값을 가지는 표현문 (Expression)
val apiHostName = when(env) {
Environment.PRODUCTION -> "api.dommain.name"
Environment.ALPHA -> "alpha-api.dommain.name"
Environment.BETA -> "beta-api.dommain.name"
else -> throw IllegalArgumentException()
}
‘Nothing’ 타입
선언형 문장으로 임시 변수 사용을 줄일 수 있다.
43. println("[" + index + "] " + request.length)
println("[$index] ${request.length}”)
if (version.compareTo(minVersion) < 0) { }
if (version < minVersion) { }
val member= objectMapper.readValue(json, Member::class.java)
val member = objectMapper.readValue<Member>(json)
val member: Member = objectMapper.readValue(json)
간결함
인라인 제네릭 함수 + reified 타입 파라미터
연산자 오버로드
스트링 템플릿
if (version.equals(minVersion)) { }
if (version == minVersion) { }
==는 equals()
44. 유용한 기능
람다 파라미터
list.forEach {
println(it)
}
val value = map.getOrPut(key) {
calculateValue()
}
list.forEach({ str ->
println(str)
})
val map: ConcurrentHashMap<String, Int>
val value = map.getOrPut(key, {
calculateValue()
})
DSL로 이용하기 좋은 문법
45. 유용한 기능
함수형 유틸리티
val addresses: List<Server> = params
.filter { it.isNotBlank() }
.map {
it.split(Regex(":"), 2)
.let { Server(it[0], it[1].toIntOrNull() ?: 80) }
}
class ServerNode(val address: HostAndPort){
private val logger = LoggerFactory.getLogger(javaClass)
.apply { info("Initializing with {}", address) }
}
apply, let, run, also,
Collection, Iterable의 확장 함수로 제공
Any의 확장 함수로 제공되어 모든 expr에 대해 사용 가능
46. 강력한 기능
suspend fun lastMessages(chatRoomId: ChatRoomId, maxCachedCount: Int): List<Message> {
val cached = async { messageService.recent(chatRoomId, maxCachedCount) }
val fetched = async { requestChatLogs(chatRoomId) }
return (cached.await() + fetched.await())
.distinctBy { it.messageId }
.sortedBy { it.messageId }
}
😃 비동기 구현에서 Future, RxJava 대비 장점
🤔 1.3부터 정식 패키지에 포함되지만 1.3이 아직 베타
😱 StackTrace가 제대로 안 나오는 경우가 있다.
코루틴
비동기 로직을
가독성이 높은 절차적인 코드로 작성
기본적으로 컨텍스트 스위칭 없이 동작하여 성능이 좋음
47. 자바와 코틀린 비교
* 코드의 특성에 따라 달라질 수 있음
* RxJava2 기반으로 작성된 모듈
* Compile Time은 kapt 제외한 순수한 컴파일 시간
인라인 함수
익명 클래스
null 체크
메타 데이터
66% x2.2 x6.5
자바 컴파일이 빠른거 '
코드 라인 수 바이트코드 사이즈 컴파일 시간
자바8 코틀린같은 동작을 하는
...증분 컴파일은 체감상 빠른데..
48. 개발 환경은 쓸 만 한가요?
언어로 생산성과 안전함이 향상된다?
잘 돌아가나요?
자바와 어느 정도 호환되나요?
문법
그래서
생태계
개발 환경
57. OpenJDK
JDK7 부터 기본 기능으로는 OracleJDK와 차이가 없음
기술 지원이 없으며 업데이트를 지속적으로 할 필요
OracleJDK과의 차이점
Brewery 프로젝트에 OpenJDK10 적용
코틀린&자바 빌드: 문제 없음 (코틀린 코드는 kotlinc에서)
성능 및 안정성: JDK10에 준하는 결과
카카오톡 서비스 적용
58. 마이그레이션
⚠ 자바 코드/클래스 연동시 타입 주의 (Nullable 등)
⚠ Lombok 적용된 자바 코드와는 같은 모듈에 사용하기 힘듬
⚠ Spring에서는 특정 클래스/함수를 open해줘야 함
⚠ Immutable 데이터로 리팩토링하는 것은 생각보다 큰 일
✔ 새로운 프로젝트: 처음부터 코틀린
✔수정이 앞으로도 많을 프로젝트: 모델, 유틸리티부터 리팩토링
✔ 그 외 잘 돌아가고 있는 자바 프로젝트: 잘 돌아가게 두자
✔kotlin-spring 플러그인을 쓰자
✔delombok을..
59. 도입을 고민할 때 할 만한 걱정들
개발 환경은 쓸 만 한가요?
언어로 생산성과 안전함이 향상된다?
잘 돌아가나요?
자바와 어느 정도 호환되나요?
문법
그래서
생태계
개발 환경
60. 요약
이미 안정화된 언어와 개발 환경
생산성과 가독성 높은 문법
하위 호환할 레거시 문법이 없음
잘 돌아갑니다. 😃
자바와 완전한 호환으로 생태계 공유
프레임웍의 러닝 커브가 없음
문법
그래서
생태계
개발 환경