KB 04 코틀린에서 자바 부르기 — 게터가 프로퍼티로 보인다
고친 사람 github-actions[bot]
0. 첫 예제 — 쓴 적 없는 이름 name
코틀린으로 프로젝트를 시작해도 쓰는 라이브러리는 대부분 자바로 쓰여 있습니다. 그래서 코틀린 첫날부터 자바 클래스를 부르게 됩니다. 평범한 자바 클래스 하나로 시작하겠습니다.
public class Member {
private String name;
public Member(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
필드 name 은 private 입니다. 밖에서 쓸 수 있는 멤버는 게터 getName() 과 세터 setName() 뿐입니다. 이 클래스를 코틀린에서 불렀습니다.
fun main() {
val m = Member("kim")
println(m.name) // kim
m.name = "lee"
println(m.name) // lee
println(m.getName()) // lee
}
fun main()— 프로그램이 시작하는 함수입니다. 코틀린은 클래스 없이 파일에 바로 씁니다val m = Member("kim")—val은 다시 대입할 수 없는 변수입니다. 코틀린에는new가 없어서 생성자를 함수처럼 부릅니다. 타입은 값에서 알아냅니다println(...)— 한 줄 출력입니다. 줄 끝에 세미콜론이 없습니다m.name— 이 편의 주인공입니다.Member에는name이라는 공개 멤버가 없습니다
자바 클래스를 javac 로 먼저 컴파일했습니다. 그다음 코틀린 컴파일러 kotlinc 에 그 결과 폴더를 -cp(classpath, 클래스를 찾을 경로)로 알려 줬습니다. -d out 은 두 컴파일러 모두 결과물을 out 폴더에 쓰라는 옵션입니다.
$ javac -d out Member.java
$ kotlinc -cp out Main.kt -d out
$ kotlin -cp out MainKt
kotlin— 코틀린 설치에 딸린 실행 명령입니다.java와 같은 일을 하되 코틀린 표준 라이브러리를 classpath 에 스스로 넣습니다MainKt— 컴파일러가Main.kt파일로 지은 클래스입니다. 파일 바로 아래에 쓴 함수는파일 이름 + Kt클래스의 static 메서드가 됩니다
돌았습니다. m.name 이 kim 을 읽었습니다. m.name = "lee" 는 값을 lee 로 바꾸기까지 했습니다. 마지막 줄의 m.getName() 도 문제없이 불렸습니다.
같은 일을 자바로 해 봤습니다. UseJ.java 의 main 은 두 줄입니다.
Member m = new Member("kim");
System.out.println(m.name);
$ javac -cp out -d jout UseJ.java
UseJ.java:4: error: name has private access in Member
System.out.println(m.name);
^
1 error
자바는 name 이 private 이라며 거절합니다. 그런데 코틀린은 같은 m.name 을 받아 줬습니다. 코틀린이 private 필드를 몰래 연 걸까요.
답부터 보기 — m.name 은 필드가 아니라 게터다
두 소스를 나란히 놓고 보겠습니다. 자바 Member 에서 밖으로 열린 멤버는 getName() 과 setName(String) 둘뿐입니다. name 이라는 이름을 가진 것은 private 필드 하나인데 그 필드는 밖에서 못 씁니다. 바로 앞의 javac 에러가 그것을 말해 줍니다.
그런데 코틀린 쪽은 같은 m.name 으로 읽고 쓰기까지 했습니다. 자바가 막은 private 필드를 코틀린이 열었다고 보면 앞뒤가 안 맞습니다. 남는 설명은 하나뿐입니다. 코틀린이 읽고 쓴 name 은 필드가 아니라 게터와 세터를 부르는 또 다른 이름이라는 것입니다.
마지막 줄에 일부러 넣어 둔 m.getName() 이 그 앞줄의 m.name 과 똑같이 lee 를 찍은 것도 같은 이야기를 합니다. 두 줄은 쓰는 모양만 다를 뿐 같은 메서드에 가 닿았습니다.
코틀린에서는 필드와 게터·세터를 한 이름으로 묶은 선언을 프로퍼티(property)라고 부릅니다. 코틀린 클래스를 쓰면 m.name 처럼 프로퍼티 이름으로 읽고 씁니다. 코틀린은 자바 클래스의 게터와 세터도 프로퍼티처럼 보이게 해 줍니다.
컴파일 결과로 확인하기
읽어 낸 대로인지 컴파일 결과를 직접 열어 보겠습니다. .class 파일을 거꾸로 읽어 소스 코드 모양으로 다시 적는 일을 디컴파일(decompile)이라고 하는데, CFR 0.152 는 그 일을 하는 도구입니다. jar 하나로 된 도구라 java -jar cfr.jar 클래스파일 로 돌립니다.
$ java -jar cfr.jar out/MainKt.class
...
public final class MainKt {
public static final void main() {
Member m = new Member("kim");
System.out.println((Object)m.getName());
m.setName("lee");
System.out.println((Object)m.getName());
System.out.println((Object)m.getName());
}
...
}
m.name 을 읽은 두 줄이 m.getName() 이 됐습니다. 우리가 직접 쓴 m.getName() 과 구별이 안 됩니다. m.name = "lee" 는 m.setName("lee") 가 됐습니다. 필드를 연 게 아니었습니다. 코틀린 컴파일러가 m.name 을 게터 호출로, 대입을 세터 호출로 바꿔 적었습니다.
- 디컴파일 결과는 원래 소스를 되살린 것이 아니라 클래스 파일에 적힌 내용을 자바 문법으로 다시 적은 것입니다. 맨 위의 머리 주석,
import, 한 줄이 아주 긴@Metadata(...)(코틀린 컴파일러가 클래스에 남기는 부가 정보)는...로 줄였습니다 (Object)캐스트는println(Object)를 부른다는 CFR 의 표시입니다. 이 편의 주제와는 상관없습니다- 아래쪽
...에는 컴파일러가 옛 자바 실행기에 맞춰 만들어 넣은main(String[] args)가 있습니다. 이 편의 주제와는 상관없어 뺐습니다
이 편은 코틀린에서 자바를 부를 때 자바 개발자가 걸려 넘어지는 다섯 가지를 차례로 봅니다. 게터 규칙, static, 키워드와 겹치는 이름, 체크 예외, 널입니다. 출력은 전부 Kotlin 2.4.10, JDK 21 에서 직접 돌린 결과입니다.
1. 게터·세터가 프로퍼티로 보이는 규칙
왜 헷갈리나. m.name 이 게터 호출이라는 표시가 코틀린 코드 어디에도 없습니다. 어떤 자바 메서드가 프로퍼티로 보이는지는 메서드 이름으로 정해집니다. 규칙을 모르면 a.active 는 되는지, a.URL 은 되는지 매번 컴파일러에게 물어봐야 합니다.
코틀린 공식 문서는 규칙을 이렇게 적습니다.
Methods that follow the Java conventions for getters and setters (no-argument methods with names starting with
getand single-argument methods with names starting withset) are represented as properties in Kotlin. Such properties are also called synthetic properties. — 코틀린 문서
인자 없이 get 으로 시작하는 메서드와 인자 하나로 set 으로 시작하는 메서드가 프로퍼티로 보인다는 뜻입니다. 이렇게 컴파일러가 만들어 보여 주는 프로퍼티를 합성 프로퍼티(synthetic property)라고 부릅니다. 자바 클래스에는 그런 선언이 없습니다. 코틀린 컴파일러의 눈에만 있는 프로퍼티입니다.
되는 경우 — get, set, is, 대문자 이름, 세터 없음
규칙의 갈래를 한 클래스에 모았습니다. 게터·세터 짝, is 로 시작하는 불리언 게터, 대문자가 이어진 이름, 세터가 없는 게터가 들어 있습니다.
public class Account {
private String owner = "kim";
private boolean active = true;
public String getOwner() { return owner; }
public void setOwner(String o) { owner = o; }
public boolean isActive() { return active; }
public void setActive(boolean a) { active = a; }
public String getURL() { return "https://example.com"; }
public int getId() { return 7; }
public int isCount() { return 3; }
}
코틀린 쪽은 이 게터들을 전부 프로퍼티 이름으로 불렀습니다. 자바라면 a.setOwner("lee"), a.getOwner() 처럼 메서드 이름으로 부를 줄들입니다.
fun main() {
val a = Account()
a.owner = "lee"
a.isActive = false
println(a.owner) // lee
println(a.isActive) // false
println(a.url) // https://example.com
println(a.id) // 7
println(a.isCount) // 3
}
$ javac -d out Account.java
$ kotlinc -cp out Main.kt -d out
$ kotlin -cp out MainKt
값이 다섯 개 다 찍혔습니다. 줄 오른쪽 주석이 그 줄이 찍은 값입니다. 어느 이름이 어느 메서드로 갔는지 짐작해 보면 좋습니다. 같은 값을 메서드 이름으로 부른 자바 UseJ.java 도 돌렸습니다. a.setOwner("lee")·a.getOwner() 처럼 아래 표의 왼쪽 칸을 그대로 쓴 파일입니다.
$ javac -cp out -d out UseJ.java
$ java -cp out UseJ
자바 쪽도 lee·false·https://example.com·7·3 다섯 줄을 같은 차례로 찍었습니다. 출력이 같다는 것만으로는 어느 이름이 어느 메서드로 갔는지 모릅니다. 그래서 MainKt.class 를 0절과 같은 방법으로 CFR 로 열었습니다. 이름마다 아래 표의 메서드 호출이 들어 있었습니다.
| 자바 메서드 | 코틀린에서 부른 이름 | 읽을 거리 |
|---|---|---|
getOwner() · setOwner(String) |
a.owner |
get 을 떼고 첫 글자를 소문자로. 세터가 있어 대입도 된다 |
isActive() · setActive(boolean) |
a.isActive |
is 가 떨어지지 않는다. 대입도 a.isActive = false |
getURL() |
a.url |
이어진 대문자 URL 이 전부 소문자가 됐다 |
getId() |
a.id |
세터가 없어도 읽기는 된다 |
isCount() 가 int 를 돌려줌 |
a.isCount |
불리언이 아니어도 is 규칙으로 읽혔다 |
마지막 줄은 문서와 조금 다릅니다. 문서는 is 규칙을 불리언 게터(Boolean accessor methods)의 규칙으로 소개합니다. 그런데 Kotlin 2.4.10 은 int 를 돌려주는 isCount() 도 a.isCount 로 받아 줬습니다.
안 되는 경우 — 규칙에서 빠지는 이름과 모양
이번에는 규칙에 안 맞는 이름을 일부러 모았습니다. 앞의 Account 에 더해 Odd 클래스를 하나 더 만들었습니다.
public class Odd {
public void setPassword(String p) { }
public String fetchTitle() { return "t"; }
public String getname() { return "n"; }
public String getLabel(int i) { return "l"; }
}
세터만 있는 password, get 이 아닌 fetch 로 시작하는 이름, get 바로 뒤가 소문자인 이름, 인자를 받는 게터입니다. 코틀린에서 이것들을 프로퍼티 이름으로 불렀습니다.
fun main() {
val a = Account()
a.id = 8
println(a.active)
println(a.URL)
val o = Odd()
o.password = "secret"
println(o.title)
println(o.name)
println(o.label)
}
일곱 줄 모두 에러가 났습니다. 에러마다 따라 나오는 소스 줄과 ^ 표시는 빼고 에러 줄만 싣습니다. 3:7 은 3번째 줄 7번째 글자라는 뜻입니다.
$ javac -d out Account.java Odd.java
$ kotlinc -cp out Main.kt -d out
Main.kt:3:7: error: 'val' cannot be reassigned.
Main.kt:4:15: error: cannot access 'field active: Boolean': it is private in 'Account'.
Main.kt:5:15: error: unresolved reference 'URL'.
Main.kt:8:7: error: unresolved reference 'password'.
Main.kt:9:15: error: unresolved reference 'title'.
Main.kt:10:15: error: unresolved reference 'name'.
Main.kt:11:15: error: unresolved reference 'label'.
unresolved reference 는 그런 이름을 못 찾았다는 뜻입니다. 에러는 세 종류로 갈립니다.
첫째는 a.id = 8 의 'val' cannot be reassigned 입니다. 세터 없는 게터는 val 프로퍼티, 곧 읽기만 되는 프로퍼티로 보입니다. var 는 val 과 달리 다시 대입할 수 있는 변수를 선언하는 키워드입니다. 세터가 짝으로 있으면 게터는 이 var 프로퍼티가 됩니다.
둘째는 a.active 의 cannot access 'field active: Boolean' 입니다. 코틀린이 찾은 것은 프로퍼티가 아니라 자바의 private 필드 active 였습니다. 합성 프로퍼티의 이름은 isActive 라서, active 라는 이름으로는 필드밖에 안 잡힙니다.
셋째는 나머지 다섯 줄의 unresolved reference 입니다. 프로퍼티가 아예 만들어지지 않았습니다. 세터만 있으면 안 되는 이유는 코틀린에 세터만 있는 프로퍼티가 없기 때문입니다(「Kotlin doesn't support set-only properties」, 같은 문서).
이런 메서드는 프로퍼티로 못 쓸 뿐, 메서드로는 그대로 부를 수 있습니다. o.setPassword("secret"), o.fetchTitle() 처럼 자바와 똑같이 쓰면 됩니다.
| 자바 메서드 | 코틀린에서 | 에러 | 이유 |
|---|---|---|---|
getId() 만 있음 |
a.id = 8 |
'val' cannot be reassigned |
세터가 없으면 읽기 전용 |
isActive() |
a.active |
cannot access 'field active: Boolean' |
프로퍼티 이름은 isActive. active 는 private 필드 |
getURL() |
a.URL |
unresolved reference 'URL' |
프로퍼티 이름은 url |
setPassword(String) 만 있음 |
o.password |
unresolved reference |
세터만으로는 프로퍼티가 안 생긴다 |
fetchTitle() |
o.title |
unresolved reference |
get · is 로 시작하지 않는다 |
getname() |
o.name |
unresolved reference |
get 바로 뒤가 소문자다 |
getLabel(int) |
o.label |
unresolved reference |
인자가 있는 게터다 |
정리하면, 코틀린은 인자 없는 get·is 게터를 프로퍼티로 보여 주고 set 짝이 있으면 대입까지 열어 줍니다. 프로퍼티 이름은 get 을 떼고 소문자로 바꾸거나 is 이름을 그대로 씁니다. 이 모양에서 벗어난 메서드는 메서드로만 부를 수 있습니다.
2. 자바의 static 메서드와 필드 부르기
왜 헷갈리나. 코틀린에는 static 이라는 키워드가 없습니다. 파일 바로 아래에 쓴 함수, 곧 톱레벨(top-level) 함수가 static 메서드 역할을 대신합니다(KB 01 첫 파일 — fun main 한 줄이 JVM 에서 도는 법 3절). 그러면 자바 쪽에 이미 있는 Math.max 나 System.out 은 어떻게 부를까요.
자바와 똑같이 클래스 이름.멤버 로 부릅니다. Math.max 는 static 메서드, Math.PI 와 System.out 은 static 필드입니다. Thread.currentThread() 로 static 메서드가 돌려준 객체의 게터까지 섞었습니다.
fun main() {
println(Math.max(3, 7)) // 7
// ↓ 3.141592653589793
System.out.println(Math.PI)
val t = Thread.currentThread()
println(t.name) // main
}
$ kotlinc Main.kt -d out
$ kotlin -cp out MainKt
찍힌 값은 줄 오른쪽에, 긴 줄은 그 위에 붙여 뒀습니다. main 은 프로그램을 시작한 스레드의 이름입니다.
System.out.println(...)— 코틀린에도 자바의System.out.println을 손대지 않고 쓸 수 있습니다. 코틀린의println과 결과가 같습니다t.name— 1절의 규칙대로Thread의getName()을 부릅니다
같은 일을 자바로 쓴 UseJ.java 입니다.
public class UseJ {
public static void main(String[] args) {
System.out.println(Math.max(3, 7));
System.out.println(Math.PI);
Thread t = Thread.currentThread();
System.out.println(t.getName());
}
}
$ javac -d jout UseJ.java
$ java -cp jout UseJ
찍힌 것도 코틀린과 같은 7·3.141592653589793·main 세 줄입니다. static 멤버를 부르는 모양은 두 언어가 같고, 게터를 부르는 곳만 t.name 과 t.getName() 으로 갈립니다. 코틀린에 static 이라는 키워드가 없다고 해서 자바의 static 멤버를 부르는 방법까지 달라지지는 않습니다. 클래스 이름에 점을 찍고 멤버 이름을 쓰면 그만이고, 1절에서 본 게터 규칙만 그 위에 더해집니다.
static 게터는 프로퍼티가 되지 않는다
한 가지는 1절과 다릅니다. Runtime.getRuntime() 은 인자 없이 get 으로 시작하는데, 인스턴스 메서드가 아니라 static 메서드입니다. 이걸 Runtime.runtime 으로 불러 봤습니다.
fun main() {
println(Runtime.getRuntime().availableProcessors() > 0)
println(Runtime.runtime) // 컴파일 오류
}
$ kotlinc Rt.kt -d rout
Rt.kt:3:21: error: unresolved reference 'runtime'.
println(Runtime.runtime)
^^^^^^^
에러는 3번째 줄 하나뿐입니다. 2번째 줄의 Runtime.getRuntime() 은 통과했습니다. 이 실측에서 합성 프로퍼티는 객체에서 부르는 게터에만 생겼고, 클래스 이름으로 부르는 static 게터에는 생기지 않았습니다.
| 자바 쪽 멤버 | 자바에서 | 코틀린에서 |
|---|---|---|
| static 메서드 | Math.max(3, 7) |
Math.max(3, 7) |
| static 필드 | Math.PI · System.out |
Math.PI · System.out |
| static 게터 | Runtime.getRuntime() |
Runtime.getRuntime(). Runtime.runtime 은 에러 |
| 인스턴스 게터 | t.getName() |
t.name 또는 t.getName() |
정리하면, 자바의 static 메서드와 필드는 코틀린에서도 자바와 같은 모양으로 부릅니다. 다만 static 게터는 프로퍼티로 바뀌지 않으니 getXxx() 로 부릅니다.
3. 자바 메서드 이름이 코틀린 키워드일 때
왜 헷갈리나. 자바에서 멀쩡한 이름이 코틀린에서는 키워드일 수 있습니다. 키워드는 언어가 문법에 쓰려고 잡아 둔 낱말입니다. 그중 하드 키워드(hard keyword)는 어느 문맥에서든 키워드라서 이름으로 못 씁니다. 게다가 이때 나는 에러 메시지에는 「키워드」라는 말이 한 번도 안 나옵니다.
코틀린 문서가 꼽는 대표는 in, object, is 입니다. 셋 다 자바에서는 키워드가 아니지만 코틀린에서는 하드 키워드입니다. 아래 표의 다섯 낱말은 모두 하드 키워드입니다.
| 낱말 | 코틀린에서 하는 일 | 자바로 치면 |
|---|---|---|
is |
값의 타입 검사 | instanceof |
in |
for (x in xs) 의 반복 대상, 포함 검사 |
for-each 의 : |
object |
클래스 선언과 그 객체 하나를 한 번에 만든다 | 없음 |
when |
값에 따라 갈래를 고르는 식 | switch |
fun |
함수 선언 | 없음 |
키워드가 모두 막히는 것은 아닙니다. data·open·value 는 data class 처럼 선언 앞에 붙을 때만 키워드로 읽히는 소프트 키워드(soft keyword)라, 이런 이름의 자바 메서드 data()·open()·value() 는 코틀린에서 w.data() 처럼 아무 표시 없이 불러도 경고 없이 컴파일되고 data, open, value 가 찍혔습니다.
하드 키워드 이름들을 메서드 이름으로 가진 자바 클래스를 만들었습니다. static 메서드 when 은 테스트 라이브러리 Mockito 의 Mockito.when(...) 과 같은 모양입니다. Mockito 없이 같은 이름만 흉내 낸 것입니다.
public class Stubber {
public static <T> T when(T value) {
System.out.println("when: " + value);
return value;
}
public boolean is(String s) { return s.isEmpty(); }
public String in() { return "in"; }
public String object() { return "object"; }
public String fun() { return "fun"; }
}
자바에서는 아무 문제가 없습니다. 코틀린에서는 자바처럼 Stubber.when("x") 와 s.is("") 로 불렀습니다.
fun main() {
val s = Stubber()
Stubber.when("x") // 컴파일 오류
println(s.is("")) // 컴파일 오류
}
자바 쪽 대조는 UseJ.java 로 했습니다. 이 파일의 main 은 Stubber.when("x"); 와 System.out.println(new Stubber().is("")); 두 줄입니다.
$ javac -d out Stubber.java UseJ.java
$ java -cp out UseJ
when: x
true
$ kotlinc -cp out Main.kt -d out
Main.kt:3:5: error: classifier 'class Stubber : Any' does not have a companion object, so it cannot be used as an expression.
Main.kt:3:13: error: the expression cannot be a selector (cannot occur after a dot).
Main.kt:3:22: error: syntax error: Expecting '{'.
Main.kt:4:13: error: syntax error: Call has no callee.
Main.kt:4:15: error: syntax error: Expecting an element.
자바 쪽은 when: x 와 true 를 찍으며 잘 돌았습니다. 코틀린 쪽은 두 줄에서 에러 다섯 개가 났습니다.
앞 두 명령이 자바 파일을 컴파일하고 돌린 것이고, 코틀린 에러는 소스 줄과 ^ 표시를 빼고 에러 줄만 실었습니다.
컴파일러가 when 과 is 를 메서드 이름이 아니라 문법으로 읽었습니다.
Expecting '{'—when을switch같은when식의 시작으로 읽었습니다.when식은 뒤에 중괄호 블록이 와야 합니다does not have a companion object—Stubber뒤의when을 메서드로 못 읽어Stubber만 떨어진 값이 됐다는 말입니다. 문구 속Any는 코틀린에서 자바의Object에 해당하는 타입입니다. companion object 는 클래스에 static 비슷한 멤버를 붙이는 문법인데, 여기서는 몰라도 됩니다Call has no callee·Expecting an element—.뒤에 멤버 이름이 와야 하는데 타입 검사 연산자is가 왔습니다. 그래서 무엇을 부르는지 모르는 호출이 됐습니다
백틱으로 감싸면 이름이 된다
하드 키워드를 이름으로 쓰려면 백틱(`, 키보드 숫자 1 왼쪽의 역따옴표)으로 감쌉니다. 공식 문서의 예는 foo.`is`(bar) 입니다.
fun main() {
val s = Stubber()
Stubber.`when`("x") // when: x
println(s.`is`("")) // true
println(s.`in`()) // in
println(s.`object`()) // object
println(s.`fun`()) // fun
}
Ok.kt 는 경고 없이 컴파일됐고, kotlin -cp out OkKt 로 돌리니 줄 오른쪽에 적어 둔 다섯 줄이 그대로 찍혔습니다. 다섯 메서드가 모두 불린 것입니다.
자바 쪽 Stubber 의 메서드 이름은 when, is, in, object, fun 그대로입니다. 그 이름에 백틱을 감싸 부른 코틀린 호출이 다섯 메서드에 모두 가 닿았다는 것은, 백틱이 이름의 일부가 아니라는 뜻입니다. 백틱은 코틀린 컴파일러에게 「이건 이름이다」라고 알리는 표시일 뿐입니다.
테스트 코드에서 만나는 모양
이 문제를 가장 자주 만나는 곳은 테스트입니다. Mockito 의 when(...).thenReturn(...) 은 가짜 객체가 돌려줄 값을 정해 두는 메서드 호출입니다. 언어 문법이 아니라 스텁을 만드는 라이브러리 메서드라, 자바 테스트의 Mockito.when(repo.find(1)).thenReturn("kim"); 은 코틀린에서 Mockito.`when`(repo.find(1)).thenReturn("kim") 이 됩니다. 위 실측과 같은 모양을 Mockito 에 옮겨 적은 것이고, Mockito 자체로는 돌리지 않았습니다.
백틱이 거슬리는 사람을 위해 mockito-kotlin 라이브러리는 whenever(repo.find(1)) 처럼 쓰는 whenever 함수를 따로 둡니다. 그 함수의 소스를 열면 본문은 백틱으로 Mockito 의 when 을 부르는 한 줄입니다. 백틱 호출을 이름만 바꿔 감싼 함수입니다.
정리하면, 자바 메서드 이름이 코틀린 하드 키워드면 컴파일러는 그 이름을 문법으로 읽고 엉뚱한 에러를 냅니다. 백틱으로 감싸면 이름으로 읽힙니다. 백틱은 코틀린 소스에만 있는 표시라, 불리는 것은 자바에 적힌 원래 이름 그대로입니다.
4. 체크 예외 — try 없이 불린다
왜 헷갈리나. 자바 개발자는 throws IOException 이 붙은 메서드를 부르면 try 나 throws 를 쓰는 게 몸에 배어 있습니다. 코틀린은 그걸 요구하지 않습니다. 컴파일이 통과하니 예외가 사라진 것처럼 보이지만, 사라진 것은 검사뿐입니다.
throws IOException 이 붙은 static 메서드 하나를 준비했습니다. 없는 파일을 읽게 해서 예외가 나게 합니다.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Loader {
public static String load(String file) throws IOException {
return Files.readString(Path.of(file));
}
}
먼저 자바에서 try 없이 불렀습니다. UseJ.java 의 main 은 System.out.println(Loader.load("none.txt")); 한 줄입니다.
$ javac -d out Loader.java
$ javac -cp out -d jout UseJ.java
UseJ.java:3: error: unreported exception IOException; must be caught or declared to be thrown
System.out.println(Loader.load("none.txt"));
^
1 error
익숙한 에러입니다. 잡거나(catch) 던진다고 선언하라(throws)는 말입니다. 같은 호출을 코틀린에 썼습니다.
fun main() {
println("start")
println(Loader.load("none.txt"))
}
try 도 없고 throws 도 없습니다. 자바에서 방금 에러를 받은 그 모양 그대로입니다.
$ kotlinc -cp out Main.kt -d out
컴파일은 경고 하나 없이 통과했습니다. 코틀린 컴파일러는 IOException 을 잡으라는 말도, 던진다고 선언하라는 말도 하지 않았습니다.
실행하면 예외는 그대로 던져진다
그러면 실행하면 어떻게 될까요.
$ kotlin -cp out MainKt
start
Exception in thread "main" java.nio.file.NoSuchFileException: none.txt
at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92)
...
at Loader.load(Loader.java:7)
at MainKt.main(Main.kt:3)
at MainKt.main(Main.kt)
JDK 안쪽을 지나는 줄 여덟 개를 ... 로 줄였습니다. start 가 찍힌 뒤 NoSuchFileException 이 main 밖으로 빠져나가 프로그램이 멈췄습니다. NoSuchFileException 은 IOException 의 하위 클래스입니다. 이 프로그램을 실제로 돌리는 것은 JVM(Java Virtual Machine, 자바 가상 머신)입니다.
throws 선언이 없는 MainKt.main 이 체크 예외를 밖으로 내보냈는데 JVM 은 막지 않았습니다. JVM 은 실행할 때 throws 선언을 따지지 않습니다. 체크 예외 검사는 javac 가 소스를 컴파일할 때만 하는 일입니다.
코틀린 컴파일러는 그 검사를 하지 않습니다. 코틀린에서는 모든 예외가 언체크 예외처럼 다뤄집니다(「In Kotlin, all exceptions are unchecked, meaning that the compiler does not force you to catch any of them」, 코틀린 문서).
잡으려면 try 를 쓴다
잡고 싶으면 자바처럼 try 를 씁니다. 같은 호출을 감싼 Catch.kt 입니다.
import java.io.IOException
fun main() {
try {
println(Loader.load("none.txt"))
} catch (e: IOException) {
println("caught: " + e)
}
}
kotlin -cp out CatchKt 로 돌리니 caught: java.nio.file.NoSuchFileException: none.txt 한 줄을 찍고 정상 종료했습니다.
이 예제에서 보이는 차이는 catch 괄호 안의 순서입니다. 자바의 catch (IOException e) 를 코틀린은 catch (e: IOException) 로, 이름을 먼저 쓰고 콜론 뒤에 타입을 씁니다.
코틀린에는 예외 여럿을 한 번에 잡는 자바의 멀티 catch catch (A | B e) 가 없습니다. 괄호 안에서 연 자원을 저절로 닫아 주는 try-with-resources 도 없습니다. 이 둘을 대신하는 방법은 이 편에서 다루지 않습니다.
| 자바 | 코틀린 | |
|---|---|---|
throws IOException 메서드를 try 없이 부르기 |
unreported exception IOException 컴파일 에러 |
경고 없이 통과 |
| 부르는 쪽에 써야 하는 것 | throws 를 붙이거나 잡아야 한다 |
아무것도 안 써도 컴파일된다 |
| 실행 중 예외가 나면 | 잡지 않으면 스레드가 멈춘다 | 같다. 잡지 않으면 멈춘다 |
정리하면, 코틀린은 자바의 체크 예외를 잡으라고 강제하지 않을 뿐입니다. 예외는 자바에서와 똑같이 던져집니다. 컴파일러가 알려 주지 않으니, 잡아야 할 예외는 부르는 쪽이 스스로 챙겨야 합니다.
5. 자바 컬렉션과 널
왜 헷갈리나. 코틀린은 타입에 널 허용 여부를 적습니다. String 에는 null 이 못 들어가고, 뒤에 ? 를 붙인 String? 에만 들어갑니다. 그런데 자바 메서드의 List<String> 에는 그 표시가 없습니다.
Repo 라는 자바 클래스에, null 을 하나 섞은 컬렉션을 돌려주는 static 메서드 names() 를 만들었습니다.
import java.util.ArrayList;
import java.util.List;
public class Repo {
public static List<String> names() {
List<String> list = new ArrayList<>();
list.add("kim");
list.add(null);
return list;
}
}
코틀린에서 이 리스트를 돌며 문자열 길이를 찍었습니다.
fun main() {
val names = Repo.names()
for (n in names) {
println(n.length)
}
}
for (n in names)— 자바의for (String n : names)입니다. 3절에서 본 키워드in이 이렇게 쓰입니다n.length— 코틀린의String에서 길이는length()메서드가 아니라length프로퍼티입니다
$ kotlinc -cp out Main.kt -d out
$ kotlin -cp out MainKt
3
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "n" is null
at MainKt.main(Main.kt:4)
at MainKt.main(Main.kt)
코틀린은 경고 없이 컴파일을 통과시켰습니다. 그리고 두 번째 원소에서 NullPointerException 이 났습니다.
자바 쪽은 UseJ.java 의 main 에서 같은 반복문을 for (String n : names) 로 쓰고 System.out.println(n.length()); 로 길이를 찍었습니다. 이것도 컴파일을 통과했습니다.
$ java -cp out UseJ
3
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local3>" is null
at UseJ.main(UseJ.java:7)
"<local3>"— 코틀린 메시지의"n"에 해당합니다.javac는 옵션을 따로 주지 않으면 지역 변수 이름을 클래스 파일에 남기지 않아서, 이름 대신 번호가 찍혔습니다
두 언어가 같은 원소에서 같은 NullPointerException 을 냈습니다. 널을 막아 준다는 코틀린이 여기서는 자바만큼만 안전합니다.
에러 메시지에 찍힌 타입 — String!
컴파일러가 이 리스트를 어떤 타입으로 보는지 알아보려고 일부러 틀린 타입 Int 를 적었습니다. val names: Int = ... 는 변수 이름 뒤 콜론에 타입을 적는 코틀린 문법입니다.
$ kotlinc -cp out Type.kt -d tout
Type.kt:2:20: error: initializer type mismatch: expected 'Int', actual '(Mutable)List<String!>!'.
val names: Int = Repo.names()
^
에러 메시지 속 타입 (Mutable)List<String!>! 에 답이 있습니다. ! 는 「널일 수도 있고 아닐 수도 있다」는 표시입니다. 이렇게 널 여부를 모르는 채로 들어온 자바 쪽 타입을 플랫폼 타입(platform type)이라고 부릅니다. String! 은 널 여부를 모르는 String 입니다.
(Mutable)은 「바꿀 수 있는 리스트인지 모른다」는 표시입니다!는 컴파일러가 메시지에 쓰는 표기일 뿐입니다. 플랫폼 타입은 코틀린 코드에 직접 적을 수 없습니다(코틀린 문서)
String 에 담으면 대입하는 줄에서 멈춘다
앞의 반복문은 n 에 타입을 적지 않았습니다. 이번에는 두 번째 원소를, null 이 못 들어가는 널 불가 타입 String 으로 적은 변수에 담았습니다. names[1] 은 자바의 names.get(1) 입니다.
fun main() {
val names = Repo.names()
val n: String = names[1]
println("assigned")
println(n.length)
}
$ kotlinc -cp out Strict.kt -d out
$ kotlin -cp out StrictKt
Exception in thread "main" java.lang.NullPointerException: get(...) must not be null
at StrictKt.main(Strict.kt:3)
at StrictKt.main(Strict.kt)
assigned 가 찍히지 않았습니다. 예외는 n.length 를 부른 5번째 줄이 아니라 대입한 3번째 줄에서 났고, 메시지도 get(...) must not be null 로 바뀌었습니다. 같은 공식 문서는 「If you choose a non-nullable type, the compiler will emit an assertion upon assignment」라고 적습니다. 널 불가 타입에 담는 순간 컴파일러가 널 검사를 넣는다는 뜻입니다.
플랫폼 타입을 어떻게 다루는지, 자바 쪽 널 애노테이션이 무엇을 바꾸는지는 뒤에 나올 널 안전성 편에서 자세히 봅니다.
정리하면, 자바가 돌려준 값은 코틀린에서 널 여부를 모르는 String! 같은 플랫폼 타입이 됩니다. 타입을 적지 않고 그대로 쓰면 널 검사가 없어 자바와 같은 곳에서 NullPointerException 이 납니다. String 같은 널 불가 타입에 담으면 그 대입 줄에서 ... must not be null 예외가 먼저 납니다.
6. 한 장 요약
코틀린에서 자바를 부를 때 알아 둘 것이 다섯이다. 인자 없는
get·is게터는 프로퍼티로 보인다. static 멤버는 자바와 같은 모양으로 부르되, static 게터는 프로퍼티가 되지 않는다. 하드 키워드와 겹치는 이름은 백틱으로 감싼다. 체크 예외는 검사하지 않는다. 자바가 돌려준 값은 널 여부를 모르는 플랫폼 타입이 된다.
| 질문 | 자바 | 코틀린 | 확인한 방법 |
|---|---|---|---|
| 게터·세터 | m.getName() · m.setName("lee") |
m.name · m.name = "lee". 메서드로 불러도 된다 |
kotlin 실행 · CFR |
| 프로퍼티 이름 | 없음 | getOwner → owner, getURL → url, isActive → isActive. 세터가 없으면 읽기 전용 |
Account 실행 · 'val' cannot be reassigned |
| 프로퍼티가 안 되는 것 | 없음 | 세터만 있음, fetch 같은 다른 접두사, getname, 인자 있는 게터 |
unresolved reference |
| static 멤버 | Math.max(3, 7) · System.out |
같은 모양. static 게터는 프로퍼티가 안 된다 | 양쪽 실행 대조 · Runtime.runtime 에러 |
| 하드 키워드와 겹치는 이름 | Stubber.when("x") · s.is("") |
Stubber.`when`("x") · s.`is`(""). 백틱은 코틀린 소스에만 있다 |
kotlinc 에러 · 백틱 실행 |
| 체크 예외 | 안 잡으면 unreported exception 컴파일 에러 |
통과. 실행 중에는 똑같이 던져진다 | javac 에러 · kotlinc 통과 · 실행 |
자바가 돌려준 List<String> |
널 표시 없음 | (Mutable)List<String!>!. 타입을 안 적고 쓰면 널 검사가 없고, String 에 담으면 대입 줄에서 NullPointerException |
타입 불일치 에러 · 실행 |
관련 항목
자바 메서드가 코틀린에서 바뀌어 보이는 멤버
합성 프로퍼티 · 프로퍼티 · 게터 · 세터 · val · var · JavaBeans
자바의 static 과 맞세워지는 코틀린의 수단
정적 메서드 · 정적 필드 · 톱레벨 함수 · companion object
자바 메서드 이름과 부딪히는 코틀린 문법 요소
하드 키워드 · 백틱 식별자 · when 식 · is 연산자 · object 선언
백틱 호출이 흔히 나오는 테스트 라이브러리와 개념
Mockito · mockito-kotlin · 스텁 · 목 객체
코틀린 컴파일러가 따지지 않는 자바의 예외 규칙
체크 예외 · 언체크 예외 · throws 절 · try-catch · IOException · NoSuchFileException · 스택 트레이스
자바가 돌려준 컬렉션과 값의 널을 다루는 개념
플랫폼 타입 · 널 안전성 · 널 가능 타입 · NullPointerException · 널 애노테이션 · 컬렉션
클래스 파일을 만들고 여는 언어와 개발 도구
javap · 디컴파일 · 디컴파일러 · CFR · 클래스 파일 · 코틀린 · 자바 · kotlinc · javac · 컴파일러 · JDK · JVM · 가상 머신 · 코틀린 표준 라이브러리