KB 03 첫 파일의 모양 — 세미콜론, import, 패키지와 폴더
고친 사람 github-actions[bot]
0. 자바였다면 여섯 번 거절당했을 파일
misc 라는 폴더에 Shapes.kt 파일 하나를 만들었습니다.
package com.example.geo
import java.util.Date
import java.sql.Date as SqlDate
class Circle
class Square
fun now(): Date {
return Date()
}
fun today(): SqlDate {
val millis = System.currentTimeMillis()
return SqlDate(millis)
}
package— 이 파일의 선언이 속할 패키지를 정합니다. 자바의package선언과 같은 뜻인데, 줄 끝에 세미콜론이 없습니다import— 다른 패키지의 이름을 짧게 쓰게 해 주는 줄입니다. 이것도 자바와 같은 뜻입니다import java.sql.Date as SqlDate—as뒤에 적은 이름을 이 파일에서java.sql.Date대신 씁니다. 이런 이름을 별칭(alias)이라고 부릅니다class Circle— 클래스 선언입니다. 본문이 없으면 중괄호를 생략합니다. 코틀린은 수식어가 없으면public으로 봅니다fun now(): Date— 함수 선언이고, 괄호 뒤: Date가 반환 타입입니다. 클래스 밖, 파일 바로 아래에 쓴 이런 선언을 톱레벨(top-level) 선언이라고 부릅니다Date()— 생성자 호출입니다. 코틀린에는new가 없습니다val millis = ...— 다시 대입할 수 없는 변수입니다. 타입을 안 적으면 값에서 알아냅니다
자바 개발자 눈에는 걸리는 데가 다섯 곳입니다.
- 세미콜론이 없습니다
- 자바에 없는
import ... as가 있습니다 public클래스가 둘인데 파일 이름Shapes와 맞는 클래스가 없습니다- 클래스 밖에 함수가 있습니다
- 폴더는
misc인데 패키지는com.example.geo입니다
그런데 코틀린 컴파일러 kotlinc 는 한 번에 통과시킵니다.
$ kotlinc misc/Shapes.kt -d out
$ find out -name '*.class' | sort
out/com/example/geo/Circle.class
out/com/example/geo/ShapesKt.class
out/com/example/geo/Square.class
-d out 은 결과물을 그 폴더에 쓰라는 옵션이고, find out -name '*.class' | sort 는 그 폴더 아래의 클래스 파일을 하위 폴더까지 찾아 이름순으로 늘어놓는 명령입니다. .class 는 컴파일 결과인 클래스 파일로, 클래스 이름과 필드·메서드 선언, 실행할 명령어 목록이 들어 있습니다. 이것을 읽어 실행하는 것이 JVM(Java Virtual Machine), 곧 자바 가상 머신이고, JVM 은 그게 자바에서 왔는지 코틀린에서 왔는지 따지지 않습니다.
소스에 쓴 클래스는 둘인데 파일은 셋입니다. ShapesKt 는 톱레벨 함수 now 와 today 가 들어간 클래스인데, 이 이름이 어디서 오는지는 3절에서 봅니다.
같은 내용을 misc/Shapes.java 로 옮겼습니다. 자바에는 톱레벨 함수가 없으니 두 함수는 Shapes 클래스의 static 메서드로 넣었습니다.
세미콜론까지 코틀린 그대로 한 줄씩 옮겼습니다. javac 가 무엇부터 거절하는지 보려는 것입니다.
package com.example.geo
import java.util.Date
import java.sql.Date as SqlDate
public class Circle {}
public class Square {}
public class Shapes {
public static Date now() {
return new Date()
}
public static SqlDate today() {
long millis = System.currentTimeMillis()
return new SqlDate(millis)
}
}
$ javac -d jout misc/Shapes.java
misc/Shapes.java:1: error: ';' expected
package com.example.geo
^
misc/Shapes.java:3: error: ';' expected
import java.util.Date
^
misc/Shapes.java:4: error: ';' expected
import java.sql.Date as SqlDate
^
misc/Shapes.java:11: error: ';' expected
return new Date()
^
misc/Shapes.java:15: error: ';' expected
long millis = System.currentTimeMillis()
^
misc/Shapes.java:16: error: ';' expected
return new SqlDate(millis)
^
6 errors
에러 여섯 개입니다. 그런데 이게 끝이 아닙니다. 하나씩 고쳐 다시 컴파일하면 숨어 있던 에러가 층층이 올라옵니다.
그래서 에러 수는 줄기만 하지 않습니다. 고칠수록 에러가 늘기도 합니다.
| 고친 것 | javac 에러 수 |
새로 나온 에러 | 다루는 절 |
|---|---|---|---|
| 한 줄씩 옮긴 처음 모양 | 6 | ';' expected |
1절 |
| 세미콜론을 붙임 | 1 | 별칭 줄의 ';' expected |
2절 |
별칭 대신 두 Date 를 모두 import |
7 (1+4+2, 2절에서 가른다) | 같은 이름이 겹친다 · public 클래스와 파일 이름 | 2절 |
java.sql.Date 를 패키지까지 붙인 전체 이름으로 씀 |
2 | public 클래스와 파일 이름 | 3절 |
| 클래스마다 파일을 나눔 | 0 | 없음. 폴더는 여전히 misc |
4절 |
마지막 줄이 이상합니다. 폴더가 misc 인데 자바도 통과합니다. 자바가 폴더를 강제한다고 알고 있었다면, 무엇이 강제이고 무엇이 관례인지 다시 가를 때입니다. 이 편은 이 표를 한 줄씩 풉니다. 출력은 전부 Kotlin 2.4.10, JDK(Java Development Kit) 21 에서 직접 돌린 결과입니다.
1. 세미콜론 — 코틀린에서는 줄바꿈이 문장을 끝낸다
줄 끝에는 필요 없고, 한 줄에 둘이면 필요하다
0절에서 자바가 낸 에러 여섯 개는 전부 ';' expected 였습니다. 자바는 세미콜론이 나와야 문장이 끝납니다. 코틀린은 줄이 끝나면 문장도 끝납니다.
그러면 한 줄에 문장 둘은 어떨까요. Semi.kt 의 main 안에 val 두 개를 한 줄로 붙였습니다.
$ kotlinc Semi.kt -d out
Semi.kt:2:14: error: syntax error: Unexpected tokens (use ';' to separate expressions on the same line).
val a = 1 val b = 2
^
같은 줄에서 문장을 가르려면 ; 를 쓰라는 에러입니다. val a = 1; val b = 2; 로 가운데와 줄 끝에 ; 를 넣고 다음 줄도 자바 버릇대로 println(a + b); 로 쓴 Semi2.kt 는, kotlinc 가 경고 없이 통과시켰고 3 을 찍었습니다. 줄 끝 세미콜론은 코틀린에서 틀린 게 아니라 필요 없는 것입니다.
함정 1 — 다음 줄이 + 로 시작하면
줄바꿈이 문장을 끝낸다는 규칙은 긴 식을 두 줄로 나눌 때 문제를 만듭니다. Wrap.kt 에서 1 + 2 를 두 가지로 나눴습니다. x 는 + 를 다음 줄 앞에, y 는 윗줄 끝에 뒀습니다. 컴파일은 kotlinc 로, 실행은 코틀린 설치에 딸린 kotlin 명령으로 합니다. kotlin 은 java 와 같은 일을 하되 코틀린 표준 라이브러리(kotlin. 으로 시작하는 클래스가 든 jar)를 classpath 에 스스로 넣어 줍니다.
fun main() {
val x = 1
+ 2
val y = 1 +
2
println(x) // 1
println(y) // 3
}
$ kotlinc Wrap.kt -d out
Wrap.kt:3:9: warning: expression is unused.
+ 2
^^^
$ kotlin -cp out WrapKt
x 는 3 이 아니라 1 입니다. val x = 1 은 한 줄로 완결된 문장이라 거기서 끊겼습니다. 다음 줄 + 2 는 「플러스 2」라는 식 하나로 따로 읽혔고, 그 값을 아무도 안 쓰니 expression is unused(쓰이지 않는 식) 경고가 났습니다. y 는 윗줄이 1 + 로 끝나 문장이 될 수 없으니 다음 줄까지 이어 읽었습니다.
같은 모양의 자바 Wrap.java 입니다. return 다음 줄에 값을 쓰는 메서드도 하나 넣었습니다.
public class Wrap {
static int answer() {
return
42;
}
public static void main(String[] args) {
int x = 1
+ 2;
System.out.println(x); // 3
System.out.println(answer()); // 42
}
}
$ javac -d jout Wrap.java
$ java -cp jout Wrap
자바는 ; 까지가 한 문장이라 x 가 3 이고, return 다음 줄의 42 도 돌려줍니다. 한편 코틀린이 늘 윗줄에서 끊는 것은 아닙니다. Chain.kt 는 다음 줄을 . 과 && 로 시작했습니다.
fun main() {
val s = "kotlin"
.uppercase()
val t = true
&& false
println(s) // KOTLIN
println(t) // false
}
$ kotlinc Chain.kt -d out
$ kotlin -cp out ChainKt
. 과 && 로 시작하는 줄은 윗줄에 이어 붙고, + 로 시작하는 줄은 끊깁니다. . 이나 && 로 시작하는 줄은 혼자서 식이 될 수 없고, + 는 +2 처럼 혼자서도 식이 되기 때문입니다. 확인한 연산자는 +·.·&& 셋입니다.
.uppercase() 는 문자열을 대문자로 바꾼 새 문자열을 돌려주는 코틀린 표준 라이브러리 함수입니다.
식을 나눌 때 + 같은 연산자는 윗줄 끝에 둡니다.
함정 2 — return 다음 줄에 값을 쓰면
return 도 혼자서 완결된 문장입니다. 자바에서 42 를 돌려준 answer 를 코틀린으로 옮겨, Answer.kt 에 fun answer(): Int { 와 return, 그 다음 줄에 42 를 썼습니다.
$ kotlinc Answer.kt -d out
Answer.kt:2:5: error: return type mismatch: expected 'Int', actual 'Unit'.
return
^^^^^^
Unit 은 코틀린에서 「돌려줄 값이 없음」을 뜻하는 타입으로, 자바의 void 에 해당합니다. Int 는 자바의 int 에 해당하는 코틀린 타입입니다.
return 한 줄이 「값 없이 돌아간다」로 끝났는데 이 함수는 Int 를 돌려줘야 해서 에러가 났습니다. 이건 컴파일러가 잡아 줘서 다행인 쪽입니다.
돌려줄 값이 없는 함수라면 잡을 근거가 없습니다. Log.kt 는 return 다음 줄에 출력을 썼습니다.
fun log(msg: String) {
return
println(msg) // 안 찍힘
}
fun main() {
log("hello")
println("end") // end
}
$ kotlinc Log.kt -d out
$ kotlin -cp out LogKt
hello 가 안 찍혔고, kotlinc 는 경고도 내지 않았습니다. return 에서 함수가 끝나 println(msg) 는 실행되지 않습니다. 같은 모양으로 쓴 자바 Log.java 는 반대로 컴파일에서 멈춥니다.
$ javac -d jout Log.java
Log.java:4: error: incompatible types: unexpected return value
System.out.println(msg);
^
1 error
자바는 return System.out.println(msg); 를 한 문장으로 읽고, void 메서드가 값을 돌려주려 한다며 거절했습니다. 같은 줄 배치가 자바에서는 에러, 코틀린에서는 경고 없이 한 줄이 사라지는 코드입니다.
2. import — 별칭, *, 기본 import
자바에는 별칭이 없다
0절 표의 둘째 줄입니다. 자바 파일에 세미콜론을 모두 붙였더니 에러가 하나 남았습니다.
$ javac -d jout misc/Shapes.java
misc/Shapes.java:4: error: ';' expected
import java.sql.Date as SqlDate;
^
1 error
자바의 import 에는 as 가 없어서, javac 는 java.sql.Date 에서 문장이 끝났어야 한다고 봤습니다. 별칭이 왜 필요한지는 이름이 겹칠 때 드러납니다. java.util.Date 와 java.sql.Date 는 패키지만 다르고 이름이 같습니다. Clash.kt 는 둘을 별칭 없이 import 하고, Date() 를 돌려주는 함수 now 하나를 둔 파일입니다.
$ kotlinc Clash.kt -d out
Clash.kt:1:18: error: conflicting import: imported name 'Date' is ambiguous.
import java.util.Date
^^^^
Clash.kt:2:17: error: conflicting import: imported name 'Date' is ambiguous.
import java.sql.Date
^^^^
...
코틀린도 거절합니다. 뒤따르는 에러 둘은 함수에서 Date 를 정하지 못했다는 여파라 ... 로 줄였습니다. import 두 줄에 빈 클래스 하나를 둔 Clash.java 도 마찬가지입니다.
$ javac -d jout Clash.java
Clash.java:2: error: a type with the same simple name is already defined by the single-type-import of Date
import java.sql.Date;
^
1 error
0절 표의 셋째 줄에 적은 일곱은 이렇게 나뉩니다.
- 위의 import 에러 하나
Date가 어느 쪽인지 모르겠다는reference to Date is ambiguous넷- 3절에서 볼 public 클래스 에러 둘
- 합쳐서 일곱
그래서 자바에서는 한쪽을 java.sql.Date 처럼 전체 이름으로 씁니다. 코틀린은 한쪽에 별칭을 붙입니다. 공식 문서도 그렇게 적습니다.
If two imported entities have the same name, use the
askeyword to locally rename one of them — Kotlin 문서 「Packages and imports」
별칭은 소스 파일 안에만 있다
locally rename, 곧 그 파일 안에서만 이름을 바꾼다는 말을 결과물에서 확인했습니다. 0절의 ShapesKt 를 디컴파일러 CFR 0.152 로 되돌렸습니다. 디컴파일(decompile)은 클래스 파일을 거꾸로 읽어 소스 코드 모양으로 다시 적는 일이고, CFR 은 jar 하나로 된 도구라 java -jar cfr.jar 클래스파일 로 돌립니다. 결과 맨 위의 머리 주석과 import, 한 줄이 아주 긴 @Metadata(...)(코틀린 컴파일러가 남기는 부가 정보)는 ... 로 줄여 실었습니다.
$ java -jar cfr.jar out/com/example/geo/ShapesKt.class
...
package com.example.geo;
...
public final class ShapesKt {
@NotNull
public static final Date now() {
return new Date();
}
@NotNull
public static final java.sql.Date today() {
long millis = System.currentTimeMillis();
return new java.sql.Date(millis);
}
}
@NotNull 은 이 메서드가 null 을 돌려주지 않는다고 코틀린 컴파일러가 붙인 표시입니다. SqlDate 는 어디에도 없고, 자바 개발자가 전체 이름으로 손수 쓰던 코드가 들어 있습니다. 별칭은 컴파일러가 소스를 읽는 동안만 쓰는 이름이라 클래스 파일에 남지 않습니다.
* 와 겹칠 때, 그리고 함수 import
패키지 안의 이름을 전부 가져오는 * 는 자바와 같습니다. Star.kt 는 이렇게 생겼습니다.
import java.util.*
import java.sql.Date
fun stamp(): Date { return Date(0) }
fun id(): UUID { return UUID.randomUUID() }
java.util 에도 Date 가 있으니 두 import 가 겹칩니다. 어느 쪽이 이겼는지는 결과물을 열어 보면 압니다. JDK 에 딸린 javap 는 클래스 파일의 모양 — 클래스 이름, public·final 같은 수식어, 멤버 선언 — 을 자바 선언문처럼 보여 주고, -p 는 private 멤버까지 보여 달라는 옵션입니다.
$ kotlinc Star.kt -d out
$ javap -p out/StarKt.class
Compiled from "Star.kt"
public final class StarKt {
public static final java.sql.Date stamp();
public static final java.util.UUID id();
}
Date 는 이름을 콕 집어 import 한 java.sql.Date 가 됐고, UUID 는 * 로 들어왔습니다. 이름을 콕 집어 쓴 import 가 * 보다 앞선다는 공식 문서의 규칙 그대로입니다.
If you import an entity with both a star import and an explicit import, the explicit import takes priority during overload resolution. — Kotlin 문서 「Packages and imports」
같은 import 두 줄을 쓴 자바 Star.java 도 stamp() 가 java.sql.Date 를 돌려줬습니다.
자바와 갈리는 점도 하나 있습니다. 코틀린의 import 는 클래스뿐 아니라 톱레벨 함수도 가져옵니다. 다른 패키지에서 0절의 함수를 쓰려면 import com.example.geo.today 로 패키지 뒤에 함수 이름을 바로 쓰고, ShapesKt 는 적지 않습니다. 자바가 static 메서드를 이름만으로 부를 때 쓰는 import static 이 코틀린에는 따로 없습니다.
기본 import — 자바의 java.lang.* 에 해당하는 것
자바에서 String 이나 Math 를 import 없이 쓰는 것은 컴파일러가 java.lang 을 알아서 가져오기 때문입니다. 코틀린에도 모든 파일에 알아서 import 되는 패키지 목록이 있고, 이를 기본 import(default imports)라고 부릅니다.
Def.kt 는 import 한 줄 없이 코틀린 함수 셋과 자바의 Math 를 불렀습니다. listOf(3, 1, 2) 는 원소 셋짜리 읽기 전용 리스트를 만들고, .sorted() 는 정렬한 새 리스트를 돌려줍니다. maxOf(3, 7) 은 둘 중 큰 값을 돌려주는 코틀린 표준 라이브러리 함수입니다.
fun main() {
// ↓ [1, 2, 3]
println(listOf(3, 1, 2).sorted())
println(maxOf(3, 7)) // 7
println(Math.max(3, 7)) // 7
}
$ kotlinc Def.kt -d out
$ kotlin -cp out DefKt
자바였다면 Math 는 통과해도 java.util 의 List 는 import 없이 cannot find symbol 입니다. 코틀린에서 listOf 가 import 없이 된 것은 목록 덕분입니다. 코틀린 공식 문서가 적은 기본 import 목록과, Kotlin 2.4.10 에서 import 없이 불러 본 결과를 나란히 놓았습니다. 출처는 코틀린 문서 의 「Default imports」입니다.
| 문서가 적은 기본 import | 이 편에서 불러 본 것 | 2.4.10 실측 |
|---|---|---|
kotlin.* · kotlin.annotation.* · kotlin.ranges.* · kotlin.sequences.* |
부르지 않음 | |
kotlin.collections.* |
listOf · sorted |
✓ |
kotlin.comparisons.* |
maxOf |
✓ |
kotlin.io.* |
println |
✓ |
kotlin.text.* |
uppercase (1절) |
✓ |
kotlin.math.* |
sqrt · abs · PI |
✗ |
java.lang.* (JVM 에서만) |
Math · System |
✓ |
kotlin.jvm.* (JVM 에서만) |
부르지 않음 |
✗ 가 하나 있습니다. Need.kt 는 import 한 줄 없이 둘을 찍었습니다. sqrt(16.0) 은 kotlin.math 의 제곱근 함수이고, Random.nextInt(1) 은 목록에 없는 kotlin.random 의 난수입니다. 0 이상 1 미만이라 늘 0 입니다.
fun main() {
println(sqrt(16.0))
println(Random.nextInt(1))
}
$ kotlinc Need.kt -d out
Need.kt:2:13: error: unresolved reference 'sqrt'.
println(sqrt(16.0))
^^^^
Need.kt:3:13: error: unresolved reference 'Random'.
println(Random.nextInt(1))
^^^^^^
Random 은 목록대로입니다. 그런데 sqrt 도 안 풀렸습니다. 문서와 언어 명세는 둘 다 kotlin.math 를 기본 import 에 넣고 있지만, 2.4.10 컴파일러는 받지 않았고 절댓값 abs 와 원주율 PI 도 마찬가지였습니다. import kotlin.math.sqrt 와 import kotlin.random.Random 두 줄을 더한 fix/Need.kt 는 4.0 과 0 을 찍었습니다.
3. 한 파일에 선언 여럿 — 자바가 거절하는 것은 public 뿐이다
자바의 규칙
0절 표의 넷째 줄입니다. java.sql.Date 를 전체 이름으로 바꿨더니 에러 둘이 남았습니다.
$ javac -d jout misc/Shapes.java
misc/Shapes.java:5: error: class Circle is public, should be declared in a file named Circle.java
public class Circle {}
^
misc/Shapes.java:6: error: class Square is public, should be declared in a file named Square.java
public class Square {}
^
2 errors
파일 이름과 맞는 Shapes 는 통과했고 Circle 과 Square 가 걸렸습니다. 문구에 조건이 붙어 있습니다. public 클래스라면 제 이름의 파일에 있어야 한다는 것입니다.
그래서 public 을 떼면 한 파일에 여럿이 들어갑니다. Shapes.java 에 package com.example.geo; 와 수식어 없는 class Circle {} · class Square {} 만 넣었습니다.
$ javac -d jout Shapes.java
$ find jout -name '*.class' | sort
jout/com/example/geo/Circle.class
jout/com/example/geo/Square.class
자바가 막는 것은 「파일 하나에 클래스 여럿」이 아니라 「파일 이름과 다른 public 클래스」입니다. 소스 파일이 하나여도 클래스 파일은 클래스마다 생깁니다.
코틀린은 파일 이름을 안 따지나
코틀린은 파일 이름과 클래스 이름을 묶지 않습니다. 0절의 파일 하나에서 Circle.class · Square.class, 그리고 톱레벨 함수 now · today 를 담은 ShapesKt.class 까지 셋이 나왔습니다.
톱레벨 함수가 없으면 ShapesKt 도 없을까요. package com.example.geo 와 class Circle · class Square 만 든 Models.kt 를 컴파일하면 Circle.class 와 Square.class 둘만 나오고 ModelsKt.class 는 생기지 않았습니다.
파일 이름 + Kt 클래스는 톱레벨 함수나 프로퍼티를 담아야 할 때만 생깁니다. JVM 에서는 모든 메서드가 어떤 클래스에 속해야 해서, 클래스 밖에 쓴 함수도 담아 둘 클래스가 있어야 하기 때문입니다.
수식어 없이 쓴 class Circle 은 javap -p 로 열었습니다.
$ javap -p out/com/example/geo/Circle.class
Compiled from "Shapes.kt"
public final class com.example.geo.Circle {
public com.example.geo.Circle();
}
수식어를 안 썼는데 public final class 이고, 쓴 적 없는 public 생성자가 있습니다. 생성자는 자바에서 생성자를 안 쓰면 기본 생성자가 생기는 것과 같습니다. final 은 이 클래스를 상속할 수 없다는 뜻인데, 코틀린이 왜 기본으로 붙이는지와 상속을 허락하는 방법은 이 시리즈의 뒤쪽 편에서 다룹니다.
공식 컨벤션은 반대로 권합니다. 코틀린 코딩 컨벤션은 관련 있는 선언이라면 한 파일에 모으라고 합니다.
Placing multiple declarations (classes, top-level functions or properties) in the same Kotlin source file is encouraged as long as these declarations are closely related to each other semantically, and the file size remains reasonable (not exceeding a few hundred lines). — Kotlin 문서 「Coding conventions」
같은 문서의 「Source file names」는 파일에 클래스가 하나면 그 클래스 이름으로, 여럿이거나 톱레벨 선언뿐이면 내용을 설명하는 이름으로 파일을 지으라고 합니다. 컴파일러가 강제하지 않으니 관례로 맞춥니다.
4. 패키지와 폴더 — 결과물은 package 를 따른다
소스 폴더는 결과물에 흔적이 없다
0절 표의 마지막 줄입니다. 자바 파일을 Circle.java · Square.java · Shapes.java 로 나눴습니다. 셋 다 package com.example.geo; 로 시작하고, 폴더는 여전히 misc 입니다.
$ javac -d jout misc/Circle.java misc/Square.java misc/Shapes.java
$ find jout -name '*.class' | sort
jout/com/example/geo/Circle.class
jout/com/example/geo/Shapes.class
jout/com/example/geo/Square.class
에러 없이 통과했고 결과물은 com/example/geo 에 생겼습니다. 0절의 코틀린 결과물도 out/com/example/geo/ 였습니다. 두 컴파일러 모두 결과물 폴더를 소스 폴더가 아니라 package 선언에서 정합니다.
그러면 폴더가 필요해지는 때는 언제일까요. 컴파일러가 소스를 찾아야 할 때, 그리고 실행기가 클래스 파일을 찾아야 할 때입니다. 실행기(launcher)는 JVM 을 띄우고 첫 클래스를 찾아 그 안의 main 을 불러 주는 java 명령의 앞단입니다.
그럼 폴더는 언제 필요한가 — 안 넘긴 소스를 찾게 할 때
Main.java 가 com.example.geo.Point 를 import 하고, Point.java 는 misc 폴더에 있습니다. Main.java 만 넘기고 필요한 소스는 찾아서 컴파일하라고 했습니다.
$ rm -rf out && javac -sourcepath . -d out Main.java
Main.java:1: error: package com.example.geo does not exist
import com.example.geo.Point;
^
Main.java:5: error: cannot find symbol
System.out.println(new Point());
^
symbol: class Point
location: class Main
2 errors
-sourcepath . 는 넘기지 않은 클래스가 필요하면 현재 폴더(.) 아래에서 소스를 찾으라는 옵션입니다. javac 는 com.example.geo.Point 를 com/example/geo/Point.java 에서 찾으니 misc 에서는 못 찾았습니다. misc/Point.java 를 함께 넘기거나, 파일을 com/example/geo/ 로 옮기면 통과했습니다.
실행할 때도 폴더가 필요한가
package com.example.geo; 로 시작하고 app 을 찍는 App.java 를 컴파일해 돌린 뒤, 클래스 파일만 패키지 폴더 밖 flat 으로 복사해 돌렸습니다.
$ javac -d out App.java
$ java -cp out com.example.geo.App
app
$ mkdir flat && cp out/com/example/geo/App.class flat/
$ java -cp flat App
Error: Could not find or load main class App
Caused by: java.lang.NoClassDefFoundError: App (wrong name: com/example/geo/App)
클래스 파일 안에는 패키지까지 붙은 이름이 적혀 있습니다. flat/App.class 에 적힌 이름은 com/example/geo/App 이라 요청한 App 과 다르다며 거절했습니다(wrong name). 실행기는 classpath 아래 패키지 이름과 같은 폴더에서 클래스 파일을 찾습니다. 컴파일러가 결과물을 com/example/geo 에 쓰는 까닭입니다.
코틀린도 폴더를 따지나
이번엔 폴더를 패키지와 맞춰 com/example/geo/Hi.kt 에 "hi" 를 돌려주는 톱레벨 함수 hi 를 두고, 이 함수를 import 해 찍는 Main.kt 만 kotlinc 에 넘겼습니다.
$ kotlinc Main.kt -d out
Main.kt:1:12: error: unresolved reference 'example'.
import com.example.geo.hi
^^^^^^^
Main.kt:4:13: error: unresolved reference 'hi'.
println(hi())
^^
$ kotlinc com/example/geo/Hi.kt Main.kt -d out
$ kotlin -cp out MainKt
hi
폴더를 맞춰 둬도 kotlinc 는 찾아 주지 않았습니다. 넘긴 파일과 -cp 로 준 결과물만 봅니다. 그러니 kotlinc 에게 소스 폴더는 아무 뜻이 없고, 폴더 구조는 사람과 빌드 도구를 위한 관례입니다. 실행기의 규칙은 코틀린 결과물에도 똑같이 적용되지만 kotlinc 가 package 대로 폴더를 만들어 줍니다.
네 규칙을 한 줄씩 맞대면 이렇습니다.
| 규칙 | 자바에서 강제하는 쪽 | 어기면 | 코틀린 |
|---|---|---|---|
| public 클래스 이름 = 파일 이름 | javac |
class Circle is public, should be declared in a file named Circle.java |
없음 |
| 직접 넘긴 소스의 폴더 = package | 없음 | 통과 | 없음 |
| 찾아서 컴파일할 소스의 폴더 = package | javac -sourcepath |
package com.example.geo does not exist |
소스를 찾지 않는다 |
| 클래스 파일의 폴더 = package | 실행기 | wrong name: com/example/geo/App |
같은 규칙. 컴파일러가 맞춰 쓴다 |
공식 컨벤션 — 공통 루트 패키지는 폴더에서 뺀다
코틀린 코딩 컨벤션의 「Directory structure」는 이렇게 권합니다.
In pure Kotlin projects, the recommended directory structure follows the package structure with the common root package omitted. For example, if all the code in the project is in the
org.example.kotlinpackage and its subpackages, files with theorg.example.kotlinpackage should be placed directly under the source root, and files inorg.example.kotlin.network.socketshould be in thenetwork/socketsubdirectory of the source root. — Kotlin 문서 「Coding conventions」
모든 코드가 com.example.geo 아래라면 package com.example.geo 파일은 소스 루트(소스 파일을 모아 두는 최상위 폴더) 바로 아래에 두고, com/example/geo 세 겹을 만들지 않는다는 뜻입니다.
단 코틀린과 자바를 섞은 프로젝트라면 같은 문서가 자바처럼 패키지마다 폴더를 따르라고 합니다.
each file should be stored in the directory corresponding to each package statement — Kotlin 문서 「Coding conventions」
Gradle 같은 빌드 도구에서는 src/main/kotlin 과 src/main/java 가 소스 루트이고, 이 컨벤션은 그 아래의 폴더 구조를 말합니다.
5. 한 장 요약
코틀린에서는 줄바꿈이 문장을 끝내므로
return이나+앞에서 줄을 나누면 뜻이 바뀐다.import는as별칭으로 겹치는 이름을 풀고 톱레벨 함수까지 가져오며, 별칭은 클래스 파일에 남지 않는다. 파일 이름·public 클래스 수·소스 폴더는kotlinc가 따지지 않고, 결과물 폴더는 두 언어 모두package를 따른다.
| 질문 | 자바 | 코틀린 | 확인한 방법 |
|---|---|---|---|
| 세미콜론 | 필수. 빠지면 ';' expected |
줄 끝은 필요 없음(붙여도 경고 없음). 한 줄에 문장 둘이면 필요 | Semi.kt |
다음 줄이 + 2 로 시작 |
한 문장. x 는 3 |
끊김. x 는 1, expression is unused 경고. . · && 로 시작하면 이어짐 |
Wrap · Chain.kt |
return 다음 줄에 값 |
그 값을 돌려줌. void 면 에러 |
return 에서 끝남. Unit 함수면 경고 없이 다음 줄이 실행 안 됨 |
Answer.kt · Log |
| 같은 이름 두 클래스 | 한쪽을 전체 이름으로 | import ... as 별칭. 클래스 파일에는 전체 이름만 남음 |
Clash · CFR |
* 와 명시 import 가 겹침 |
명시한 쪽 | 명시한 쪽 | Star.kt · javap -p |
| 알아서 import 되는 것 | java.lang.* |
문서상 kotlin.* 외 8개와 JVM 의 java.lang.* · kotlin.jvm.*. 2.4.10 에서 kotlin.math 는 안 됨 |
Def · Need.kt |
| 한 파일에 public 클래스 여럿 | 에러. public 이 아니면 됨 | 된다. 수식어 없으면 public final. 파일 이름 + Kt 는 톱레벨 선언이 있을 때만 |
javac · find · javap -p |
| 소스 폴더 ≠ package | 직접 넘기면 통과. -sourcepath 로 찾을 때만 실패 |
통과. 소스를 찾지 않는다 | javac · kotlinc |
| 결과물 폴더 | package 를 따름. 어긋나면 wrong name |
package 를 따름 | find · java |
| 폴더 관례 | 패키지마다 폴더 | 공통 루트 패키지는 폴더에서 생략. 자바와 섞으면 자바식 | 코딩 컨벤션 |
관련 항목
첫 파일의 모양을 이루는 코틀린 문법 요소
세미콜론 추론 · 패키지 선언 · import 문 · import 별칭 · 와일드카드 import · 기본 import · 톱레벨 함수 · 톱레벨 프로퍼티 · Unit · 단항 연산자 · return 문 · 문장과 식
줄바꿈이 문장을 끊어서 생기는 오류와 경고
도달할 수 없는 코드 · 쓰이지 않는 식 · 컴파일러 경고
코틀린 문법과 맞세워지는 자바의 문법과 규칙
java.lang 패키지 · static import · 전체 이름 · public 클래스와 파일 이름 · 기본 생성자
코틀린 파일이 클래스 파일이 되어 실행되기까지 거치는 처리 단계
kotlinc · javac · 컴파일러 · 클래스 파일 · 자바 실행기 · 클래스 로더 · JVM
패키지 이름을 따라 클래스와 소스를 찾는 경로
클래스패스 · 소스패스 · 소스 루트 · 패키지 디렉터리 구조 · NoClassDefFoundError · 소스 세트
파일과 폴더의 모양을 정하는 관례와 문서와 빌드 도구
코틀린 코딩 컨벤션 · 코틀린 언어 명세 · Gradle
클래스 파일을 열어 보는 도구
javap · 디컴파일 · 디컴파일러 · CFR
코틀린 컴파일러가 결과물에 남기는 수식어와 애노테이션
final · 가시성 수식어 · @NotNull · @Metadata