Kotlin

[Kotlin] 가시성 변경자 public, internal, protected, private

sh1mj1 2024. 1. 5. 10:25

Kotlin in Action 을 공부하고 Effective kotlin 의 내용을 조금 참조하여 정리한 글입니다.

 

이미지 출처        https://commons.wikimedia.org/wiki/File:Kotlin_Icon.svg

코틀린에서는 기본적으로 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