IT_Programming/Kotlin

[펌] Java와 함께 사용하는 Kotlin

JJun ™ 2017. 8. 28. 08:41



 출처

 : https://academy.realm.io/kr/posts/kotlin-with-java/




Java와 함께 사용하는 Kotlin

by Taehwan Kwon
Aug 28 2017


Java와 함께 사용하는 Kotlin


Droid Knights 팀에서 준비한 올어바웃 코틀린 (AAK: All About Kotlin) 행사에서 “Java와 함께 사용하는 Kotlin”이라는 주제를 다룬 세션입니다.



소개

Java와 함께 사용하는 Kotlin이라는, 어쩌면 당연히 그렇게 써야 하는 내용을 말씀드리겠습니다. 회사에서 Kotlin을 두 달간 사용하면서 Kotlin에서 null을 안전하게 사용할 수 있다는 얘기 때문에 nullable을 사용하지 않고 앱을 올렸다가 앱이 크래시났던 상황 등을 포함해서 느낀 내용을 정리했습니다.


null 주의

// class name String.kt
fun merge(a: String, b: String) = "$a $b"
public class Sample {
   @Test
   public void sample() {
     System.out.println("merge " + StringKt.merge("A", "B"));
   }
}

간단하게 코드를 작성해봤는데, 보시다시피 위 변수들은 nullable이 아닙니다. 첫 번째 코드처럼 Kotlin에서는 클래스가 없어도 String.kt처럼 만들 수 있고, Java에서 호출할 때는 두 번째 코드처럼 StringKT처럼 Kotlin이라는 이름이 붙은 상태로 호출합니다.


Java에서 부를 때 - value

이제 Java에서 변수를 선언해서 만들어보겠습니다.

public class Sample {
  private String a = "A";
  private String b = "B";
  @Test
  public void sample() {
    System.out.println("merge " + StringKt.merge("a", "b"));
  }
}

앞선 테스트와 마찬가지로 결과는 A와 B가 나올 겁니다.


Java에서 부를 때 - null value

이제 문제가 되는 상황을 보여드리겠습니다.

// class name String.kt
fun merge(a: String, b: String) = "$a $b"
public class Sample {
  private String a = "A";
  private String b = "B";
  @Before
  public void setUp() {
    a = null;
  }
  @Test
  public void sample() {
    System.out.println("merge " + StringKt.merge("a", "b"));
  }
}
 

제가 겪었던 문제가 이런 내용입니다. 긴 코드 중간에서 변수를 null로 바꾸면 추적하기가 쉽지 않습니다.
일일이 null인 것을 확인해야 하거나, 직접 사용 전에 null을 if로 점검해줘야 하는데 솔직히 귀찮습니다.
위 코드를 실행하면 바로 예외가 발생하게 되죠.

// java.lang.IllegalArgumentException:
// Parameter specified as non-null is null:
// method tech.thdev.myapplication.StringKt.merge

IllegalArgumentException이 발생하는 이유는 Kotlin이 null을 허용하지 않으므로 null을 허용하는 코드를 추가해주지 않았기 때문입니다. 도구에서 안내를 해주지 않을까 했지만, 위와 같은 코드에서는 안내해주지 않았습니다.


public class Sample {
  // 이전 코드 생략
  @Test
  public void sample() {
    a = null;
    System.out.println("merge " + StringKt.merge("a", "b"));
  }
}
// warning message: Argument 'a' might be null less...

물론 위 코드처럼 바로 직전에 변수를 null로 바꾸면 경고 메시지가 나옵니다. 하지만 현실적으로는 이전 케이스처럼 API에서 값을 받아와서 처리하는데 null인지 아닌지 알 수 없는 상황이 발생하거나, 누군가 다른 사람이 null로 바꾸거나 할 가능성이 높습니다. 생각보다 많이 마주치게 되는 경우이므로 if 문으로 null point exception 처리를 해주게 됩니다.


null 처리

따라서 이런 예외 처리를 피하려면 ?로 nullable 처리를 해주시는 것이 좋습니다.

// class name String.kt
fun merge(a: String?, b: String?) = "$a $b"

이렇게 바꾸면 반환 값이 “merge null B”라고 나옵니다.
제가 원하는 결과는 아니므로 이제부터 차근차근 원하는 결과를 찾아보겠습니다.


null 처리는? - if != null

// class name String.kt
fun merge(a: String?, b: String?) = {
  if (a != null && b != null) {
    "$a $b"
  }
}

Java처럼 써보면 이렇습니다. 하지만 Java에서 호출해보면 String으로 반환 값을 정의해 주지 않았기 때문에 Function 자체가 반환되면서 “merge Function0"이라는 엉뚱한 결과가 나옵니다. 람다 축약식이므로 메서드 자체가 반환되는 것이죠.

// class name String.kt
fun merge(a: String?, b: String?) = {
  if (a != null && b != null) {
    "$a $b"
  } else {
    ""
  }
}

원하는 결과는 아직 아니지만, else로 공백을 반환했습니다.


3항식 도입

String merge(String a, String b) = {
  return (a != null ? a :  "a") + " " + (b != null ? b : "b");
}
// class name String.kt
fun merge(a: String?, b: String?)
  = "${a = null ? a : "a"} ${b != null ? b : "b"}"
}

Java식 3항식은 위 코드와 같은 모습입니다. 하지만 Kotlin에 아래 코드처럼 적용하면 바로 문법 오류가 납니다. Kotlin은 3항식이 없기 때문이죠. 따라서 3항식을 if else로 처리하려면 다음과 같은 코드를 사용합니다.

// class name String.kt
fun merge(a: String?, b: String?)
  = "${if (a != null) a else "a"} "${if (b != null) b else "b"}""
}
// class name String.kt
fun merge(a: String?, b: String?)
  = "${a ?: "a"} ${b ?: "b"}"
}

길이가 긴 첫 번째 코드를 두 번째 코드처럼 줄여서 쓸 수 있고, 도구에서도 축약을 잘 해줍니다. a가 null이 아니면 자기 자신이, null이면 “a”로 출력해줍니다.

위 코드를 앞서 예외가 발생하던 Java 코드로 호출해보면 “merge a B”와 같은 결과가 나옵니다.
드디어 제가 원하던 결과가 나왔습니다.


Kotlin에서 Java 코드 사용할 때는?

반대로 사용할 경우도 생각해볼까요? Annotation을 잘 적용해야 합니다. @Nullable/@NotNull을 사용하면 자동으로 상속을 받으며 ?가 포함되거나 되지 않는 형태로 내려옵니다. 저는 기존의 Java 코드의 일부를 Kotlin으로 변경하는 작업을 하면서 이런 문법 오류를 자주 발견했습니다.


JvmOverloads

class Sample(
  val name: String,
  val age: Int = 0,
  val birthday: String = "2000-01-01") {
  init {
    println("name $name age $age birthday $birthday")
  }
}

일반적으로 Kotlin에서 오버라이드를 구현하면 위와 같은 모습입니다. 디폴트가 지원되므로 나이를 0, 생일을 2000-01-01로 바꿀 수 있습니다.



하지만 이 코드를 Java에서 호출하면 위 그림과 같은 결과가 나옵니다.

class Sample(
  val name: String,
  val age: Int = 0,
  val birthday: String = "2000-01-01") {
  constructor(name: String, age: Int): this(name, age, "2000-01-01")
  constructor(name: String): this(name, 0)
  init {
    println("name $name age $age birthday $birthday")
  }
}

오버라이드된 결과를 원하므로 Java에서 사용하려면 위 코드처럼 Secondary 생성자를 원하는 결과가 나올 때까지 추가해서 상위 생성자로 보낼 수도 있을 겁니다. 하지만 Primary 생성자가 아니면 앞에 변수 타입을 정의할 수 없기 때문에 곤란합니다. 또한, 생성자 호출 이전에 init이 먼저 호출되고 Primary 생성자가 불린 후 Secondary 생성자들이 불리게 됩니다. 전역 변수로 Java처럼 사용할 수도 있겠지만, 이상적이지 않으니 차라리 위 코드가 나을 수 있습니다.



이제 이 코드를 Java에서 호출하면 위 그림과 같은 결과가 될 테지만 코드가 너무 길다는 단점이 있습니다.
이를 해결하기 위해 JvmOverloads를 지원합니다.


JvmOverloads 추가하기

class Sample
  @JvmOverloads constructor(
    val name: String,
    val age: Int = 0,
    val birthday: String = "2000-01-01") {
  init {
    println("name $name age $age birthday $birthday")
  }
}

물론 Java에서 상속을 받지 않으면 이런 코드를 만들 필요가 없긴 합니다. Java를 버릴 수 있다면 좋겠죠.


JvmOverloads - function


@JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") {
  //
}

void f(String a, int b, String c) { }
void f(String a, int b) { }
void f(String a) { }

function을 오버로드할 때도 첫 번째 코드와 같이 @JvmOverloads를 붙이고 두 번째 코드처럼 Java에서 호출하면 됩니다.


Static Field


class Key {
  companion object {
    const val TYPE_ZERO = 0
    @JvmField val TYPE_ONE = 1
  }
}

final static을 정의하려면 const, @JvmField를 붙여 줍니다.


class SampleTest {
  @Test
  public void sample() {
    System.out.println(Key.TYPE_ZERO);
    System.out.println(Key.TYPE_ONE);
  }
}

두 코드 모두 Java에서의 호출 결과는 같습니다. 단, companion object 안에 들어가 있어야 한다는 점을 주의하세요. 이렇게 사용하면 직접 접근이 가능한 static 변수처럼 사용할 수 있습니다.


Class 상속

앞에서 클래스 상속을 언급했는데, 언젠가 Kotlin에서 Java, Java에서 Kotlin을 상속받을 경우를 대비해서 설명하겠습니다.


public class TestOne {
  public void print() {
    System.out.println("TestOne");
  }
}
class TestTwo : TestOne() {
  override fun print() {
    super.print()
    print("TestTwo")
  }
}

먼저 Java로 최상위 상속자를 첫 번째 코드의 TestOne처럼 만듭니다. 그런 다음 두 번째 Kotlin에서 상속을 받습니다.


public class TestThree extends TestTwo {
  override fun print() {
    super.print()
    print("TestThree")
  }
}

하지만 그 다음 Java에서 TestTwo를 확장해서 TestThree를 만들면 문제가 생깁니다.



Java에서는 명시적으로 final을 붙여야 하지만 Kotlin에서는 생략된 final이 있기 때문입니다. 그러므로 open이라는 키워드를 명시적으로 적어줘야 합니다. 물론 추상 클래스와 인터페이스는 상속할 수 있습니다. 단, 추상 클래스에서 메서드를 정의할 때는 open을 넣어줘야 합니다.


open class TestTwo : TestOne() {
  override fun print() {
    super.print()
    print("TestTwo")
  }
}

이제 Java > Kotlin > Java로 상속을 진행할 수 있고 TestThree도 제대로 동작합니다.


결론

이번에 회사에서 Kotlin을 처음 적용하는 분과 처음 협업 코딩을 하면서 이런 내용을 정리하게 됐습니다.
도구에서 알려주지 않는 3항식 처리 등을 알 수 있었습니다.
제 사례를 통해 오류 없는 Kotlin 사용에 도움이 되시면 좋겠습니다.

발표자료 ( presentation.pdf)


본 영상과 글은 올어바웃 코틀린 (AAK: All About Kotlin)의 비디오 스폰서인 Realm에서 제공합니다.


presentation.pdf
3.0MB