After ADS ’19: Java❤️ Kotlin, 행복하자 🎵
AndroidDevSummit 세션 내용을 정리해봅니다.
아무래도 안드로이드는 이제 10년이 지난 플랫폼이어서 Kotlin이 first language로 소개된지는 좀 지났지만, 아직도 프로젝트에서 Java와 Kotlin을 함께 사용하는 경우가 많습니다. 😆
혹시 놓치고 있던 내용은 없었는지 내용을 정리해봅니다. 🗒
Calling into the Java from Kotlin
Kotlin에서 Java를 호출할 때!
Nullability?
Java는 기본적으로 값들이 null이 될 수 있고, Kotlin은 될 수 없습니다. 그래서 Kotlin에서 Java API를 호출할 때, 함수의 매개변수 / 반환값이 Nullable인지 판단할 수 없습니다.
간혹 런타임의 안정성을 고려해서 모든 Kotlin 코드에 ? 을 붙이는 경우도 있는데요. 좋지 않은 방법이라고 생각됩니다.
Nullability annotations
그렇다면 Kotlin에서 사용할 때, Null이 되지 않는 것을 알려줄 방법은 없을까요? Nullability Annotation(@Nullable, @NonNull)을 사용하면 됩니다.
아래는 Kotlin에서 인식할 수 있는 Annotation 목록입니다.
- JetBrains (
org.jetbrains.annotations) - Android (
com.android.annotations,android.support.annotations,androidx.annotations) - JSR-305 (
javax.annotation) - FindBugs (
edu.umd.cs.findbugs.annotations) - Eclipse (
org.eclipse.jdt.annotation) - Lombok (
lombok.NonNull)
아래처럼 Java 코드를 변경하면, Kotlin 코드가 더 괜찮아집니다.
// Without annotationSet<String> toSet(Collection<String> elements) { ... }
fun toSet(elements: (Mutable)Collection<String!>) :
(Mutable)Set<String!> { ... }
// With annotation@NotNull
Set<@NotNull String> toSet(
@NotNull Collection<@NotNull String> elements) { ... }
fun toSet(elements: (Mutable)Collection<String>) :
(Mutable)Set<String> { ... }
Property prefixes (get / set / is)
Java bean 스타일을 따르면, Kotlin에서 Property처럼 사용할 수 있습니다.
Getters/Setters
- get:
get-으로 시작하는 매개변수 없는 메소드 - set:
set-으로 시작하는 단일 매개변수 메소드 - is:
is-로 시작하면서 setter와 이름이 같은 getter 메소드
public final class User {
public String getName() { ... }
public void setName(String name) { ... }
public boolean isActive() { ... }
}// Invokes user.getName()
val name = user.name// Invokes user.setName(String)
user.name = "Murat"// Invokes user.isActive()
if (user.active) {
// ...
}
Keywords
Java는 버전이 업데이트될 때, 새로운 키워드가 거의 추가되지 않았고 이전 코드가 잘 동작하는 것도 보장하고 있습니다. 하지만 Kotlin은 새로운 언어라서 고유한 키워드를 사용합니다. 이로 인해, 키워드 중복이 있을 수 있습니다.
fun, is, in, object, typealias, typeof, val, var, when …
위의 키워드들은 Kotlin에서 사용하는 것들로, Java에서는 다른 용도로 사용할 수 있었지만 Kotlin 코드에서는 그렇지 않습니다. 즉, Java의 함수나 매개변수에서 이런 키워드를 사용하고 있으면, Kotlin에서 호출할 때 문제가 됩니다.
public boolean is(SomeObject input) { ... }// Kotlin
something.is(input) // Error!
아래는 해결하는 방법입니다.
- Option 1: (Recommended) 이름을 변경한다.
public boolean isSame(SomeObject input) { ... }- Option 2: 중복되는 부분을 ``로 감싼다.
public boolean is(SomeObject input) { ... }// Kotlin
something.`is`(input) // Success!
Any
Kotlin Extension Function / Property에서 아래 이름을 사용하지 마세요.
also, apply, asDynamic, ensureNeverFroze, n, freeze, hashCode, Iterator, let, objcPtr, pin, run, runCatching, takeIf, takeUnless, to, unsaveCast, usePinned, isFrozen, javaClass, jsClass
Operator Overloading
Java는 연산자 오버로딩 기능이 없지만, Kotlin은 있습니다.
Kotlin에서는 연산자를 함수로 인식합니다.
자세한 것은 아래 표를 참고하세요.

아래처럼 Java에 정의한 함수도 Kotlin에서 연산자로 사용할 수 있습니다. 😆
public final class RomanNumeral {
private final String value;
public RomanNumaral(String value) {
this.value = value;
}
public RomanNumeral plus(RomanNumeral other) {
// convert roman numeral to decimal
// add
// convert result to roman numeral
return result
}
}// Kotlin
val result = RomanNumeral("III") + RomanNumeral("IX")
SAM (Single Abstract Method)
Kotlin은 SAM conversion을 지원합니다.
public interface Runnable {
void run();
}// Java
Runnable runnable = () -> System.out.println("Run!");// Kotlin
val runnable = Runnable { println("Run!!") }
함수 매개변수에 SAM을 사용하려면, 마지막 매개변수에 선언되어야 합니다.
// Java
public static int calculate(Operation operation,
int firstNumber, int secondNumber) { ... }
// Kotlin
calculate({ ... }, 6, 7)아래처럼 순서를 변경하면, SAM이 적용됩니다.
// Java
public static int calculate(int firstNumber, int secondNumber,
Operation operation) { ... }
// Kotlin
calculate(6, 7) { ... }하지만 SAM conversion은 Java interop에서만 동작합니다.
Kotlin에서는 함수를 Type으로 지원하기 때문에 SAM이 불필요합니다. 🤔
interface MyListener {
fun onCheckedChanged(radioGroup: RadioGroup, resId: Int)
}fun setMyListener(listener: MyListener) { ... }setMyListener { radioGroup, resId -> ... } // Compile time errorsetMyListener(object: MyListener {
override fun onCheckedChanged(
radioGroup: RadioGroup, resId: Int) {...}
})
Kotlin에서는 함수 타입을 사용하여 SAM과 비슷하게 사용할 수 있습니다.
fun setMyListener(listener: (RadioGroup, Int) -> Unit) { ... }setMyListener { radioGroup, resId -> ... } // Correct
Java Interface를 사용하더라도 Kotlin 코드에서는 SAM이 동작하지 않습니다.
// RadioGroup.java
public interface OnCheckedChangeListener {
void onCheckedChanged(RadioGroup var1, int var2);
}// SamDemo.kt
fun setOnCheckedChangeListener(
listener: RadioGroup.OnCheckedChangeListener) {
...
}setOnCheckedChangeListener { // Compile time error
radioGroup, resId -> ...
}
Calling Kotlin from the Java
mData = KotlinCode.call
@JvmName, @JvmMultifileClass
Java Util 코드를 Kotlin 코드로 변경할 때, 참고할 만한 내용입니다.
public class Utils {
public static String timeSince(Date date) {...}
private Utils() {}
}// Dates.kt
fun Date.timeSince(): String {...}// DatesKt.java (Generated)
public final class DatesKt {
@NotNull
public static final String timeSince(
@NotNull Date $this$timeSince) {
Instrinsics.checkParameterIsNotNull(
$this$timeSince, "$this$timeSince");
// ...
}
}
Annotation을 이용하여 DatesKt 대신 다른 이름으로 생성할 수 있습니다.
@JvmMultifileClass // 다수의 파일을 동일한 클래스로 병합
@file:JvmName("Utils") // 클래스명 지정
package com.example.myapplicationfun Date.timeSince(): String {
...
}
호출하는 Java 코드는 기존과 동일하게 사용 가능합니다.
// App.java
mTimeSinceView.setText(Utils.timeSince(date));@JvmField
Data 클래스는 기본적으로 getter/setter를 갖습니다.
// Message.kt
data class Message(
val user: User,
val text: String,
val received: Date
)// Message.java (Generated)
public final class Message {
@NotNull private final User user;
@NotNull private final String text;
@NotNull private final Date received;
@NotNull public final User getUser() { /* ... */ }
@NotNull public final String getText() { /* ... */ }
@NotNull public final Date getReceived() { /* ... */ }
}
@JvmField Annotation을 이용하여, 변수로 노출할 수 있다.
// Message.kt
data class Message(
@JvmField val user: User,
@JvmField val text: String,
@JvmField val received: Date
) {
@JvmField // X, 함수로 다뤄져 동작하지 않습니다.
val nameWithId: String get() = ...
// lateinit는 필드로 노출됩니다.
lateinit var attachment: Any
}// UserPicCache.kt
object UserPicCache {
// 상수도 필드로 노출됩니다.
const val DEFAULT_CACHE_SIZE = 100
}
@JvmName
// User.kt
data class User(
val id: Long,
var name: String,
val isVerified: Boolean,
var likesPink: Boolean
)// User.java (Generated)
public final class User {
public final long getId() { ... }
@NotNull
public final String getName() { ... }
public final void setName(@NotNull String name) { ... }
public final boolean isVerified() { ... }
public final boolean getLikesPink() { ... }
public final void setLikesPink(boolean likes) { ... }
}
@JvmName Annotation을 이용하여, getter/setter 이름을 변경할 수 있다.
// User.kt
data class User(
val id: Long,
@set:JvmName("changeName")
var name: String,
val isVerified: Boolean,
@get:JvmName("likesPink")
var likesPink: Boolean
)// User.java (Generated)
public final class User {
public final long getId() {...}
@NotNull
public final String getName() {...}
public final void changeName(@NotNull String name) {...}
public final boolean isVerified() {...}
public final boolean likesPink() {...}
public final void setLikesPink(boolean likes) {...}
}
@JvmStatic
아래처럼 Service 코드를 Kotlin으로 전환했다고 가정합시다.
// MyService.java
public class MyService {
void doWork() { ... } public static void schedule(Context context) {...}
}// MyService.kt
class MyService {
internal fun doWork() { ... }
companion object {
fun schedule(context: Context) {...}
}
}
Java에서 schedule(context) 함수를 호출하면, Companion이 붙습니다.
// Java
MyService.Companion.schedule(this);Annotation을 추가하여 기존 코드를 유지할 수 있습니다.
// MyService.kt
class MyService {
internal fun doWork() {...}
companion object {
@JvmStatic
fun schedule(context: Context) {...}
}
}// Java
MyService.schedule(this);
@JvmStatic은 아래 부분에 사용할 수 있습니다.
- companion object의 함수와 속성
- named object의 함수와 속성
- interface의 companion object (Kotlin 1.3 ⬆️ / JVM target 1.8 ⬆️)
@JvmOverloads
Default Parameters는 Java 언어에서 기본으로 지원되지 않습니다.
@JvmMultifileClass
@file:JvmName("Utils")// Bitmaps.kt
fun Bitmap.resize(
scale: Float,
interpolation: Interpolation = Interpolation.None
): Bitmap {
...
}// App.java
Utils.resize(bitmap, 1.5f); // Compile Error!
@JvmOverloads Annotation을 이용하면, Java 언어에서 사용할 수 있고, @JvmName 을 이용하여 이름을 변경할 수도 있습니다.
@JvmMultifileClass
@file:JvmName("Utils")// Bitmaps.kt
@JvmOverloads
@JvmName("resizeBitmap")
fun Bitmap.resize(
scale: Float,
interpolation: Interpolation = Interpolation.None
): Bitmap {
...
}// App.java
Utils.resizeBitmap(bitmap, 1.5f);
@Throws
Kotlin은 checked exceptions이 없습니다. 따라서 Java 언어에서 호출할 때는 try-catch 문이 불필요하다는 warning이 보이게 됩니다.
Checked exceptions이란?
Java에서 Compile time에 Exception Handling 여부를 확인하는 장치
// ChatStream.kt
class ChatStream {
fun sendMessage(message: Message) {...}
}// App.java
try {
chatStream.sendMessage(message);
} catch (IOException ioException) { // Warning!
...
}
이 때, @Throws 를 이용하여 Checked exception을 사용할 수 있습니다.
// ChatStream.kt
class ChatStream {
@Throws(IOException::class)
fun sendMessage(message: Message) {...}
}// ChatStream.java (Generated)
public final class ChatStream {
public final void sendMessage(@NotNull Message message)
throws IOException {...}
}
One More Thing…
Feature leak prevention
안드로이드 앱의 feature 유출에 대한 내용입니다.
- Function Parameters
함수 매개변수의 NonNull을 확인하는 목적으로 매개변수 이름을String으로Intrinsics함수를 호출하고 있습니다. 이String은 Proguard로 난독화되지 않는 부분이므로, 민감한 이름은 함수 매개변수에 사용하지 않을 것을 권장합니다.
// Repository.kt
class Repository {
fun fetchDataRemotely(
secretFeatureDataSource: SecretFeatureDataSource) {...}
}// Generated Java file
public final class Repository {
public final void fetchDataRemotely(@NotNull
SecretFeatureDataSource secretFeatureDataSource) {
Intrinsics.checkParameterIsNotNull(
someData, "secretFeatureDataSource")
// ...
}
}
- Variable Names
변수를 사용하는 경우에도 변수 이름이String으로 노출될 수 있습니다.
// Repository.kt
class Repository {
@Inject
lateinit var secretFeatureDataSource: SecretFeatureDataSource
fun fetchDataRemotely() {
secretFeatureDataSource.fetchDataFromServer()
// ...
}
}// Generated Java file
public final void fetchDataRemotely() {
String var10000 = this.secretFeatureDataSource;
if (var10000 == null) {
Intrinsics.throwUninitializedPropertyAccessException(
"secretFeatureDataSource")
}
// ...
}
- Extension function name
코틀린 확장함수를 사용하는 경우에도 함수 이름이 노출될 수 있습니다.
// Utils.kt
@file:JvmName("Utils")
package com.example.myapplication.newfeatureinkotlinfun Model.secretFeature(): Boolean {...}// Generated Java file
public final class Utils {
@NotNull
public static final boolean secretFeature(
@NotNull Model $this$secretFeature) {
Intrinsics.checkParameterIsNotNull(
$this$timeSince, "$this$secretFeature");
// ...
}
}
- Proguard Rules for this
위의 경우에 변수 명을 개발자가 직접 난독화할 수도 있지만, 개발 생산성이 크게 저하될 겁니다. 아래처럼 Proguard Rule을 추가하여, Release 버전에 포함되지 않게 할 수 있습니다.
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
public static void checkParameterIsNotNull(...);
public static void throwUninitializedPropertyAccessException(...);
# More checks
}지금까지 Kotlin과 Java를 함께 사용할 때, 주의해야 할 점에 대한 세션 내용을 살펴봤습니다. 많은 분들이 이미 알고 있을 내용이 많지만, 마지막의 Feature Leak Prevention 내용은 도움이 될거라 생각합니다.
다음 번에는 다른 영상에 대한 내용을 정리해보겠습니다.
