-
12-2. 자바 상호 운용성 : 코틀린 코드를 자바에서 사용하기Study(종료)/Kotlin 22.09.13 ~ 12.18 2022. 12. 4. 17:42
12장
12-1. 자바 상호 운용성 : 자바 코드를 코틀린에서 사용하기12-2. 자바 상호 운용성 : 코틀린 코드를 자바에서 사용하기
12-2. 코틀린 코드를 자바에서 사용하기
1) 프로퍼티 접근
자바나 JVM에는 프로퍼티라는 개념이 없으므로 코틀린 프로퍼티를 자바에서 직접 접근할 수는 없지만,
자바 클라이언트는 접근자 메서드를 통해 프로퍼티에 접근할 수 있다.
접근자의 시그니처는 아래와 같은 규칙에 따라 프로퍼티의 정의에서 만들어진다.
● 게터는 파라미터가 없는 메서드이며 게터의 반환 타입은 원래의 프로퍼티 타입과 같다.
게터의 이름은 프로퍼티 이름의 첫 번째 글자를 대문자로 바꾼 다음, 앞에 get을 붙여서 생성된다.
● 세터는 새로운 값에 해당하는 파라미터를 하나만 받는 메서드다.
세터의 이름은 프로퍼티 이름의 첫 번째 글자를 대문자를 바꾼 다음, 앞에 set을 붙여서 생성된다.
// Person.kt class Person(var name: String, val age: Int)
위 클래스는 자바 코드로 변환하면 아래와 같은 코드가 된다.
// Person.java public class Person{ @NotNull public String getName(){ ... } public void setName(@NotNull String name){ ... } public int getAge() { ... } }
그래서 자바 클라이언트 코드는 접근자 메서드를 통해 프로퍼티에 접근할 수 있다.
// main.java public class Main { public static void main(String[] args) { Person person = new Person("John", 25); System.out.println(person.getAge()); //25 person.setName("Harry"); System.out.println(person.getName()); // Harry } }
프로퍼티 이름이 is로 시작하는 경우,
코틀린 컴파일러는 이름을 붙이는 다른 규칙을 사용하는데 그 규칙은 아래와 같다.
● 게터 이름은 프로퍼티 프로퍼티와 같다.
● 세터 이름은 맨 앞의 is를 set으로 바꾼 이름이다.
그래서 다시 예시를 보기 위해 Person 클래스에 isEmployed 프로퍼티를 추가했다고 가정하면,
// Person.kt class Person(var name: String, val age: Int, var isEmployed: Boolean)
이 프로퍼티에 접근하는 자바 코드는 아래와 같다.
// main.java public class Main { public static void main(String[] args) { Person person = new Person("John", 25, false); person.setEmployed(true); System.out.println(person.isEmployed()); // true } }
이 관습은 이름만 살펴보고, Boolean 타입과는 아무 관계가 없다.
(하지만 혼동을 막기 위해 is로 시작하는 이름은 Boolean 프로퍼티에만 사용할 것을 권장한다.)
코틀린 프로퍼티에 뒷받침하는 필드가 필요한 경우,
컴파일러가 접근자 메서드와 함께 필드도 만들어준다.
하지만 기본적으로 이 필드는 비공개이므로 게터 / 세터 코드 밖에서 필드에 접근할 수 없다.
경우에 따라 자바 클라이언트 쪽에 이 프로퍼티 필드를 노출시켜야 할 때가 있는데,
이 경우 @JvmField 어노테이션을 프로퍼티 앞에 넣으면 된다.
// Person.kt class Person(@JvmField var name: String, @JvmField val age: Int)
// Main.java public class Main { public static void main(String[] args) { Person person = new Person("John", 25); System.out.println(person.age); // 25 person.name = "Harry"; System.out.println(person.name); // Harry } }
만약 @JvmField를 넣지 않을 경우 자바코드에서 컴파일 에러가 발생한다.
(ERROR : 'name' has private access in 'Person')
이 경우 접근자 메서드는 생성되지 않고 뒷받침하는 필드가 프로퍼티 자체와 동일한 가시성으로 만들어진다.
프로퍼티 접근자가 명확하지 않다면 @JvmField를 사용할 수 없다.
(게터는 필드 값을 그대로 돌려주기만하고,
세터는 받은 값을 그대로 필드에 저장하기만 하며 다른 처리를 하지 않는 경우를 명확하다 표현)
class Person(val firstName: String, val familyName: String) { @JvmField val fullname get() = "$firstName $familyNmae" // ERROR: Thhis annotation is not applicable to target // 'member property without backing field or delegate'
@JvmField를 추상 프로퍼티나 open 프로퍼티에 적용할 수도 없는데,
그 이유는 이 프로퍼티를 오버라이드 하는 쪽에서 커스텀 접근자를 만들 수 있기 떄문이다.
open class Person(var name: String, val age: Int){ @JvmField open var description: String = "HELLO!" // ERROR : JvmField can only be applied to final property }
이름 붙은 객체의 프로퍼티에 대해 @JvmField를 적용하면
인스턴스 필드가 아니라 정적 필드를 만들어낸다.
const 변경자가 붙은 프로퍼티도 마찬가지이다.
object Application{ @JvmField val name = "MyApp" const val package = "package" }
그래서 자바 코드는 Application.name이라는 정적 필드를 통해 name, version 프로퍼티에 직접 접근 가능하다.
public class Main { public static void main(String[] args) { System.out.println(Application.name); System.out.println(Application.version); } }
2) 파일 퍼사드와 최상위 선언
코틀린에서 다른 선언 내부가 아닌, 패키지 바로 아래에 두는 최상위 선언을 자주 사용하는데,
자바와 JVM 플랫폼에서 일반적으로 모든 메서드는 어떤 클래스에 속해야ㅁ나 한다.
이런 요구사항을 만족하기 위해 코틀린 컴파일러는 최상위 함수와 프로퍼티를
자동으로 생성된 파일 퍼사드(file facade)클래스에 넣는다.
퍼사드 클래스에 생성된 메서드가 정적 메서드이므로,
자바 코드에서 최상위 메서드에 접근할 때 퍼사드 클래스를 인스턴스화할 필요가 없다.
코틀린 컴파일러는 @JvmName 어노테이션을 통해 퍼사드 클래스 이름을 지정할 수 있으며
자바 클래이언트는 변경한 이름을 통해 파일에 선언된 최상위 함수와 프로퍼티에 접근할 수 있다.
여러 파일에 있는 최상위 선언을 한 클래스로 모으는 것도 가능하다.
이 방식을 사용하려면 합치려는 파일마다 @JvmMultifileClass 어노테이션을 붙이고
@JvmName으로 클래스 이름을 지정해야 한다.
그러면 코틀린 컴파일러가 자동으로 퍼사드 클래스 이름이 같은 파일들의 선언을 한데 모아준다.
코틀린 코드에서는 퍼사드 클래스에 접근할 수 없고 JVM쪽 클라이언트에서만 퍼사드에 접근할 수 있다.
3) 객체와 정적 멤버
JVM에서 코틀린 객체 선언은 정적인 INSTANCE 필드가 있는 일반적인 클래스로 컴파일된다.
예를들어 아래와 같은 코틀린 선언이 있을 경우
object Application{ val name = "My App" fun exit() { } }
자바 코드에서는 Application.INSTANCE 필드를 통해 이 객체의 멤버에 접근할 수 있다.
public class Main { public static void main(String[] args) { System.out.println(Application.INSTANCE.getName()); Application.INSTANCE.exit(); } }
4) 노출된 선언 이름 변경하기
12.2.2에서 @JvmName을 사용해 최상위 선언이 들어가는 퍼사드 클래스의 이름을
바꿀 수 있다는 내용이 있었는데, @JvmName 어노테이션은 파일에만 적용할 수 있는 것이 아니라
함수나 프로퍼티 접근자에서 적용할 수 있고, 이를 통해 함수나 프로퍼티 접근자 이름을 변경할 수 있다.
위와 같은 기능의 주된 용도는 코틀린에서는 올바른 선언이지만 자바에서는 금지된
선언이 되는 시그니처 충돌을 막는 것 이다. 예시를 들면 아래와 같다.
class Person(val firstName: String, val familyName: String) val Person.fullName get() = "$firstName $familyName" // ERROR : Platform declaration clash: The following declarations have the same JVM signature fun getFullName(person: Person): String{ // ERROR : Platform declaration clash: The following declarations have the same JVM signature return "${person.familyName}, ${person.firstName}" }
코틀린에서는 프로퍼티와 함수를 쉽게 구분할 수 있음에도 불구하고 위 코드는 컴파일 에러가 발생한다.
JVM상에서 프로퍼티와 함수가 똑같은 시그니처와 메서드를 만들어내고,
이로 인해 모호성이 생기기 때문에 컴파일러는 오류를 발생시킬 수 밖에 없다.
이러한 문제를 @JvmName을 사용해 충돌이 일어나는 이름을 바꿔 해결할 수 있다.
class Person(val firstName: String, val familyName: String) val Person.fullName get() = "$firstName $familyName" @JvmName("getFullNameFamilyFirst") fun getFullName(person: Person): String{ return "${person.familyName}, ${person.firstName}" }
이제 자바 클라이언트는 이 함수를 getFullNameFamilyFirst라는 이름으로 호출할 수 있지만,
코틀린 코드는 원래의 getFullName을 사용해 호출할 수 있다.
마찬가지로 프로퍼티의 접근자에 대해 @JvmName 어노테이션을 붙이면
JVM의 접근자 이름을 지정하거나, 프로퍼티 자체에 어노테이션을 붙일 수도 있다.
5) 오버로딩한 메서드 생성하기
코틀린 함수에 디폴트 값이 지정된 경우, 함수 인자 중 일부를 생략할 수 있기 때문에
함수를 호출할 때 인자의 수가 달라질 수 있다.
fun restrictToRange( what: Int, from: Int = Int.MIN_VALUE, to: Int = Int.MAX_VALUE ): Int{ return Math.max(from, Math.min(to, what)) } fun main() { println(restrictToRange(100, 1, 10 )) // 10 println(restrictToRange(100, 1)) // 100 println(restrictToRange(100)) // 100 }
하지만 자바에는 디폴트 값이라는 개념이 없으므로 위 예제 코드는 아래와 같이 보인다.
public int restrictToRange(int what, int from, int to){ ... }
결과적으로 자바 클라이언트는 디폴트 값을 사용하고 싶더라도 항상 모든 인자를 넘겨야만 한다.
public class Main { public static void main(String[] args) { System.out.println(UtilKt.restrictToRange(100, 1, 10)); System.out.println(UtilKt.restrictToRange(100, 1)); // ERROR System.out.println(UtilKt.restrictToRange(100)); // ERROR } }
코틀린은 이에 대한 해법으로 @JvmOverloads 어노테이션을 제공한다.
@JvmOverloads fun restrictToRange( what: Int, from: Int = Int.MIN_VALUE, to: Int = Int.MAX_VALUE ): Int{ return Math.max(from, Math.min(to, what)) }
@JvmOverloads를 적용하면 원래 코틀린 함수 외에 오버로딩된 함수를 추가로 생성해준다.
● 오버로딩된 첫 번째 함수는 마지막 파라미터를 제외한 나머지 인자를 받는 함수이며,
이 함수는 원래 함수의 마지막 파라미터를 디폴트 값으로 지정해준다.
● 오버로딩된 두 번쨰 함수는 마지막 두 파라미터를 제외한 나머지 인자를 받는 함수이며
이 함수는 원래 함수의 마지막 두 파라미터를 디폴트 값으로 지정해준다.
3, 4번째 함수도 이런 식으로 점점 파라미터 수를 줄여나가며 디폴트 값을 적용해준다.
● 오버로딩된 마지막 함수는 파라미터를 하나만 받고,
나머지 파라미터를 디폴트 값으로 적용해준다.
위 내용들을 종합해 앞의 restrictToRange 함수는 자바에서 볼 때 아래 세 가지 오버로딩 함수가 생긴다.
public int restrictToRange(int what, int from, int to){ ... } public int restrictToRange(int what, int from){ ... } public int restrictToRange(int what){ ... }
추가된 오버로딩 함수들은 원래 함수를 호출하면서 생략된 파라미터에 대해 디폴트 값을 전달해준다.
이제 오버로딩이 제대로 해결되므로 처음의 자바 코드에서 오류가 발생했던 함수 호출 부분이
모두 제대로 컴파일된다.
public class Main { public static void main(String[] args) { System.out.println(UtilKt.restrictToRange(100, 1, 10)); System.out.println(UtilKt.restrictToRange(100, 1)); // OK System.out.println(UtilKt.restrictToRange(100)); // OK } }
@JvmOverloads 어노테이션에 의해 생성되는 오버로딩된 함수들이
컴파일된 바이너리상에 존재하기는 하지만, 코틀린에서 이런 함수를 호출할 수는 없고,
오직 자바와 상호 운용하려는 목적으로 추가된 것 이다.
6) 예외 선언하기
코틀린은 예외와 비검사 예외를 구분하지 않는다.
함수나 프로퍼티는 예외의 유형과 무관하게 코드를 추가할 필요 없이 예외를 그냥 던지면 된다.
반면 자바에서는 명시적으로 함수 본문에서 처리하지 않고
외부로 던져지는 검사 예외 목록을 함수에 추가해야 하는데,
이로 인해 자바 코드가 코틀린 선언을 호출하면서
코틀린 선언에서 발생하는 검사 예외를 처리하고 싶을 때 문제가 생긴다.
예를 들기 위해 아래의 코드를 작성하며 설명하자면,
// util.kt fun loadData() = File("data.txt").readLines()
// main.java public class Main { public static void main(String[] args) { for (String line: UtilKt.loadData()){ System.out.println(line); } } }
이 때 data.txt 파일을 읽을 수 없으면, 이 Exception은 main 스레드를 중단시킨다.
그래서 main()에 예외 처리를 추가하려고 시도하면 다른 문제가 생긴다.
// main.java import java.io.IOException; public class Main { public static void main(String[] args) { try{ for (String line: UtilKt.loadData()){ System.out.println(line); } }catch (IOException e){ // ERROR System.out.println("Can't load data"); } } }
자바는 try 블록 안의 코드에서 발생하는 것으로
선언되지 않은 검사 예외를 catch로 처리하는것을 금지하므로 위 코드는 컴파일 에러가 난다.
자바 관점에서 loadData() 함수가 아래와 같이 보인다는 점이고,
@notNull public List<String>loadData() { ... }
이 함수는 자신이 던지는 예외에 대해 아무 정보도 제공하지 않는다는 점이 문제이다.
이 때 @Throws 어노테이션을 사용해 예외 클래스를 지정하면 해결할 수 있다.
// util.kt import java.io.File import java.io.IOException @Throws(IOException::class) fun loadData() = File("data.txt").readLines()
// main.java import java.io.IOException; public class Main { public static void main(String[] args) { try{ for (String line: UtilKt.loadData()){ System.out.println(line); } }catch (IOException e){ // OK System.out.println("Can't read Data"); } } }
7) 인라인 함수
자바에서는 인라인 함수가 없기 때문에 코틀린에서 inline 변경자가 붙은 함수는
일반 메서드로 자바 쪽에 노출된다. 자바 코드에서 이런 메서드를 호출할 수 있지만,
이 경우 인라인 함수의 본문이 호출하는 자바 코드로 인라인되지는 않는다.
특별한 경우로 구체화된 타입 파라미터가 있는 제네릭 인라인 함수를 들 수 있다.
현재는 인라인을 사용하지 않고 타입 구체화를 구현할 방법이 없으므로,
자바 코드에서 이런 함수를 호출하는 것이 불가능하다.
예를들어 아래 코드의 cast() 함수는 자바 클라이언트에서 쓸모가 없다.
inline fun <reified T : Any> Any.cast(): T? = this as? T
위 함수는 퍼사드 클래스의 비공개 멤버로 노출되고,
따라서 외부에서는 이 함수를 호출할 방법이 없다.
public class Main { public static void main(String[] args) { UtilKt.<Integer>cast(""); // ERROR : Cannot resolve method 'cast' in 'UtilKt' } }
8) 타입 별명
코틀린 타입 별명은 자바 코드에서 쓸 수 없다.
자바에서 볼 때 타입 별명을 참조하는 선언은 모두 원래 타입을 가리키는 것으로 보이기 때문이다.
typealias Name = String class Person(val firstName: Name, val familyName: Name)
JVM 관점에서 보면, 위 코드의 Person 정의는
Name이라는 별명을 String으로 치환한 Person 클래스로 보인다.
자바 코드에서 Person 클래스의 인스턴스를 생성하고 사용하면 이 사실을 쉽게 알 수 있다.
public class Main { public static void main(String[] args) { Person person = new Person("John", "Doe"); System.out.printf(person.getFamilyName()); // Doe } }
사실 글을 적기 전에는 코틀린과 자바 코드가 섞여 있는 프로젝트가 꽤나 많다고 들었고,
과연 어떻게 사용할까?에 대한 의문점만 가지고 있었는데 이번 장을 통해 나름 자세히 알게 되었다.
사실 자바와 코들린 코드가 섞여있다는건 오히려
원래 자바 코드로 된 프로젝트가 있었고,이를 코틀린 코드로 변경하고 있는데,
개인적으로 코틀린 코드가 괜찮은 방향이라 생각해서 괜찮은 방향으로 나아가고 있는 회사라는 생각이 들어
안좋다는 생각은 들지 않지만,한편으론 정말 복잡하겠다는 생각은 많이 들었고, 이번 장을 정리하며 확신을 얻었다.
그래도 코틀린과 자바가 거의 1:1 대응이 된다는 내용도 알고는 있었지만 이번 장을 정리하며
어떻게 대응되는지 알 수 있어서 좋았다.
다음장은 13장 동시성이다. 여기서 아마 코루틴, 스레드 관련 내용이 나올 것 같은데
개인적으로 실무에서 아직 둘 다 사용해본 적이 없어서 무지에서 오는 무서움이 있다.
그래도 다음 장을 정리하며 좀 더 잘 알게 되면 좋을 것 같다.
반응형'Study(종료) > Kotlin 22.09.13 ~ 12.18' 카테고리의 다른 글
13-2. 동시성 : 코루틴 흐름 제어와 잡 생명 주기 (2) 2022.12.10 13-1. 동시성 : 코루틴 (2) 2022.12.09 12-1. 자바 상호 운용성 : 자바 코드를 코틀린에서 사용하기 (2) 2022.12.03 11-3. 도메인 특화 언어 : 고차 함수와 DSL (2) 2022.12.02 11-2. 도메인 특화 언어 : 위임 프로퍼티 (0) 2022.12.01