Kotlin in Action 을 공부하고 Effective kotlin 의 내용을 조금 참조하여 정리한 글입니다.
코틀린에서는 기본적으로 public (visibility modifier 가시성 변경자)
visibility modifier(가시성 변경자)는 클래스 외부 접근을 제어한다.
코틀린에서도 자바와 같은 가시성 변경자를 가진다.
`public`, `protected`, `private` 변경자가 모두 있다.
하지만 코틀린의 기본 가시성은 `public` 이다.
코틀린에서는 최상위 선언에 대해 `private` 을 허용한다.
클래스, 함수, 프로퍼티 등이 최상위 선언으로 가능하다.
`private` 최상위 선언은 그 선언이 들어있는 파일 내부에서만 사용 가능하다.
참고로 자바에서는 클래스를 `private` 으로 만들 수 없다.
코틀린의 `private` 클래스는 자바에서는 `package-private` 클래스로 컴파일된다.
자바의 기본 가시성인 `package-private`(패키지 전용) 은 코틀린에 없다.
코틀린에서는 패키지를 namespace 를 관리하기 위한 용도로만 사용하기 때문에 패키지를 가시성 제어에 사용하지 않는다.
대신 코틀린에는 `internal` 이 있다.
먼저 코틀린의 가시성 변경자를 표로 정리한 후에 설명을 이어나가겠다.
변경자 | 클래스 멤버 | 최상위 선언 |
`public` (기본 가시성) | 모든 곳에서 볼 수 있다. | 모든 곳에서 볼 수 있다. |
`internal` | 같은 모듈 안에서만 볼 수 있다. | 같은 모듈 안에서만 볼 수 있다. |
`protected` | 하위 클래스 안에서만 볼 수 있다. | 최상위 선언에 적용 불가 |
`private` | 같은 클래스 안에서만 볼 수 있다. | 같은 파일 안에서만 볼 수 있다. |
`internal` 이 너무 낯설 것이다. 더 아래에서 설명한다.
가시성 규칙을 위반하는 예시 코드
가시성을 위반하는 예시 코드를 천천히 보자.
internal open class TalkativeButton : Focusable {
private fun yell() = println("Hey!")
protected fun whisper() = println("Let's talk!")
}
fun TalkativeButton.giveSpeech() { // ERROR 'public' member exposes its 'internal' receiver type TalkativeButton
yell() // ERROR Cannot access 'yell': it is private in 'TalkativeButton'
whisper() // ERROR Cannot access 'whisper': it is protected in 'TalkativeButton'
}
`giveSpeech` 는 `public` 이다.
이 함수 안에서 그보다 가시성이 더 낮은 (`internal`) 타입인 `TalkativeButton` 을 참조하지 못한다.
아래 일반적인 규칙을 따른다.
- 메서드의 시그니처에 사용된 모든 타입의 가시성은 그 메서드의 가시성과 같거나 더 높아야 한다. (위 `giveSpeech()` 의 예시)
참고로 `giveSpeech` 는 결국 `fun giveSpeech(talkativeButton: TalkativeButton){ ...}` 의 정적 메서드 버전과 비슷하다.
- 어떤 클래스 A의 기반 타입들의 가시성은 A 의 가시성과 같거나 A 보다 더 높아야 한다.
internal open class SupClass1()
class SubClass() : SupClass1() // ERROR 'public' subclass exposes its 'internal' supertype SupClass1
- 제네릭 클래스 A의 타입 파라미터에 들어있는 타입의 가시성은 A 의 가시성과 같거나 A 보다 더 높아야 한다.
internal class GenericClass
class NormalClass<T : GenericClass> // ERROR 'public' generic exposes its 'internal' parameter bound type GenericClass
위와 같은 규칙은 어떤 함수를 호출하거나 클래스를 확장할 때 필요한 모든 타입에 접근할 수 있도록 보장해준다.
자바의 protected 와의 차이
자바와 코틀린의 `protected` 는 다르다!
자바에서는 같은 패키지 안에서 `protected` 멤버에 접근할 수 있다.
코틀린에서의 `protected` 멤버는 오직 어떤 클래스나 그 클래스를 상속한 클래스 안에서만 보인다.
즉, 클래스를 확장한 함수는 그 클래스의 `private` 이나 `protected` 멤버에 접근할 수 없다.
이 사실을 아래 코드에서 보여준다.
open class SupClass2(protected val protectedField: Int, private val privateField: Int) {
private fun privateFunc() {}
protected fun protectedFunc() {}
}
class SubClass2(protectedField: Int, privateField: Int) : SupClass2(protectedField, privateField) {
fun accessProperty() {
protectedField
privateField // ERROR Cannot access 'privateField': it is invisible (private in a supertype) in 'SubClass2'
}
}
fun SupClass2.extensionFunc() {
this.privateField // ERROR Cannot access 'privateField': it is private in 'SupClass2'
this.protectedField // ERROR Cannot access 'protectedField': it is protected in 'SupClass2'
}
internal 이 뭔데?
이제 다른 것들은 알겠는데 `internal` 은 감이 잘 안 잡힌다.
`internal` 은 "module(모듈) 내부에서만 볼 수 있음" 이라는 뜻이다.
모듈은 한 번에 한꺼번에 컴파일되는 코틀린 파일들을 의미한다.
`internal` 이라는 모듈 내부 가시성은 우리의 모듈의 구현에 대해 더 좋은 캡슐화를 제공한다.
자바에서는 패키지가 같은 클래스를 선언하기만 하면 어떤 코드라도 패키지 내부에 있는 `package-private` 인 것에 쉽게 접근이 가능하다.
즉, 캡슐화가 쉽게 깨질 수 있다.
- 코틀린 문서(23.01.04) 에서 모듈의 단위는 아래와 같다.
- IntelliJ IDEA 모듈.
- 메이븐 프로젝트.
- Gradle 소스 세트(테스트 소스 세트가 main의 내부 선언에 액세스할 수 있다는 점은 제외)
- <kotlinc> Ant task 가 한 번 실행될 때 함께 컴파일되는 파일의 집합
모듈을 만들어서 internal 을 테스트해보자
나는 보통 Jetbrains 의 IntelliJ 나 Android Studio 를 사용하므로 IntelliJ IDEA 모듈을 생성해서 테스트를 해보겠다.
이와 관련해서는 뾰로롱 님의 글을 참고했다.
IntelliJ IDEA 에서 위처럼 두 개의 module `calle` 와 `caller` 를 추가해주었다.
`callee` 에서 클래스를 선언하고 `caller` 에서 그 클래스를 사용할 것이다.
`MemberInternal` 클래스에는 메서드와 프로퍼티를 `internal` 로 선언해주었다.
`callee` 의 `MemberInternal`
class MemberInternal {
val publicField = "public field"
internal val internalField = "internal field"
fun publicMethod() {
println("public method")
}
internal fun internalMethod() {
println("internal method")
}
}
`caller` 에서 호출
fun main() {
val memberInternal = MemberInternal()
memberInternal.publicMethod()
println(memberInternal.publicField)
// 아래 오류
memberInternal.internalField // Cannot access 'internalField': it is internal in 'MemberInternal'
memberInternal.internalMethod() // Cannot access 'internalMethod': it is internal in 'MemberInternal'
}
`MemberInternal` 의 인스턴스를 만들어서 사용해본 결과 `internal` 프로퍼티는 컴파일 오류를 발생시키는 것을 확인했다.
이제 `ClassInternal` 클래스에서는 클래스 자체를 `internal` 로 선언해주었다.
`callee` 의 `ClassInternal`
internal class ClassInternal {
val publicField = "public field"
internal val internalField = "internal field"
fun publicMethod() {
println("public method")
}
internal fun internalMethod() {
println("internal method")
}
}
`caller` 에서 호출
fun main() {
val classInternal = ClassInternal() // Cannot access 'ClassInternal': it is internal in ''
}
`ClassInternal` 의 인스턴스는 만들 수 없다. 컴파일 오류를 발생시킨다.
자바에서는 코틀린의 `internal` 에 해당하는 가시성은 없다.
그렇다면 자바 코드로 코틀린의 `internal` 에 접근하면 어떻게 될까?
`JavaCaller` 라는 클래스에서 아래와 같이 코드를 작성해보자.
`caller` 에서 `JavaCaller` 를 만들어 호출
public class JavaCaller {
public static void main(String[] args) {
accessToInternalMember();
System.out.println();
accessToInternalClass();
}
private static void accessToInternalMember(){
MemberInternal memberInternal = new MemberInternal();
System.out.println(memberInternal.getInternalField$callee()); // ERROR Usage of Kotlin internal declaration from different module
memberInternal.internalMethod$callee(); // ERROR Usage of Kotlin internal declaration from different module
}
private static void accessToInternalClass(){
ClassInternal classInternal = new ClassInternal(); // ERROR Usage of Kotlin internal declaration from different module
System.out.println(classInternal.getInternalField$callee()); // ERROR Usage of Kotlin internal declaration from different module
classInternal.internalMethod$callee(); // ERROR Usage of Kotlin internal declaration from different module
}
}
오류가 엉망진창으로 뜬다.
코드를 보면 이상한 점이 있다.
분명 코틀린에서 프로퍼티 이름은 `internalField` 이고, 메서드 이름은 `internalMethod` 이다.
그런데 게터를 `getInternalField$callee` 로 호출하도록, 메서드의 이름은 `internalMethod$callee` 로 호출하도록 자동완성이 된다.
그리고 또 다른 이상한 점이 있다. 위 코드를 실행해보면 에러가 발생함에도 불구하고 실행이 된다....
실행 결과는
internal field
internal method
internal field
internal method
위에서의 두 가지 이상한 점의 이유
자바에는 `internal` 이 따로 없다고 했다.
자바의 `package-private` 은 코틀린의 `internal` 과 전혀 다르다.
모듈은 보통 여러 패키지로 이루어진다.
서로 다른 모듈에 같은 패키지에 속한 선언이 들어 있을 수 있다.
즉, 코틀린에서의 `internal` 은 바이트 코드 상에서 `public` 될 수 밖에 없다.
그래서! 자바에서는 다른 모듈에서의 코틀린 `internal` 멤버에 접근을 할 수 있는 것이다!!!
하지만 자바에서 접근을 하는 것이 코틀린으로 모듈을 나누어 개발한 개발자의 의도는 아니다.
즉, 자바 언어로 다른 모듈의 코틀린 `internal` 멤버에 접근을 못 하게 하는 것이 더 좋다.
여기에서 위의 이상한 점의 이유가 보인다!!!
기본적으로는 자바 언어로 접근이 가능하지만 접근을 최대한 막기 위해서
- 컴파일러가 오류를 발생시켜 준다. 오류 내용은 아래와 같이 표시된다.
- Usage of Kotlin internal declaration from different module
- 컴파일러가 `internal` 멤버의 이름을 보기 나쁘게 바꾼다(mangle, mangling 한다).
- `internal` 이 붙은 멤버에는 그 뒤에 "${모듈의 이름}" 을 붙여준다.
- 위의 예제 코드에서는 `getInternalField$callee` 와 `internalMethod$callee`
위처럼 일종의 두 가지 경고를 보임으로써 모듈을 사용하는 클라이언트 개발자가 보았을 때 타 모듈의 `internal` 멤버를 사용하지 않도록 주의해준다.
참조
https://wanjuuuuu.tistory.com/entry/Kotlin-internal-%EC%A0%91%EA%B7%BC%EC%A0%9C%EC%96%B4%EC%9E%90
https://kotlinlang.org/docs/visibility-modifiers.html#modules
'Kotlin' 카테고리의 다른 글
[Kotlin] sealed 로 클래스 계층 확장을 제한하기 + 태그 클래스 VS 상태 패턴 (1) | 2024.01.06 |
---|---|
[Kotlin] inner class & nested class 자바와 비교해서 (feat. 직렬화) (1) | 2024.01.06 |
[Kotlin] 코틀린 인터페이스 & 상속 제어 변경자 (2) | 2024.01.04 |
[Kotlin] 로컬 함수 & 확장 (0) | 2024.01.04 |
[Kotlin] 문자열 & 정규식 (1) | 2024.01.04 |