코틀린을 배우기 위해서 인프런에서 강의를 구매하고 코틀린과 친해지고 기본기를 다지기 위해서 공부하는 중이다. 글 내용은 접근 제어를 다루는 방법이고 최태현님의 자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide) 강의에 소금을 조금 친 내용이다
Kotlin의 접근 제어는(Visibility Modifiers)는 Java와 유사하지만, package의 역할이나 protected, internal 등의 동작 방식에서 중요한 차이가 있다.
가시성 제어의 기본 개념
- 가시성 제어 (Visibility Control): 어떤 선언(클래스, 함수, 프로퍼티 등)이 어디서 접근 가능한지를 정의하는 규칙이다
- Kotlin의 Package: Kotlin에서 package는 Java와 달리 오직 이름 공간(namespace) 관리용으로만 사용된다. 즉, 이름 충돌을 방지하는 목적으로 사용되며, 접근 제어에는 직접적인 영향을 미치지 않는다.
- Kotlin의 기본 접근 지시어: public (Java의 기본은 default)
Java와 Kotlin의 접근 제어자 비교
접근 제어자 | Java 범위 | Kotlin 범위 |
public | 모든 곳에서 접근 가능 | 모든 곳에서 접근 가능 (기본값) |
protected | 같은 패키지 또는 하위 클래스에서 접근 가능 | 선언된 클래스 또는 하위 클래스에서만 접근 가능 |
default | 같은 패키지 내에거만 접근 가능 | 없음 |
internal | 없음 | 같은 모듈 내에서만 접근 가능 |
private | 선언된 클래스 내에서만 접근 가능 | 선언된 클래스 내에서만 접근 가능 |
Kotlin 접근 제어자의 특징
- protected의 차이: Java의 protected는 같은 패키지에서도 접근이 가능하지만, Kotlin의 protected는 오직 클래스 내부와 해당 클래스를 상속받는 하위 클래스에서만 접근할 수 있다. 이는 Kotlin에서 package가 접근 제어에 사용되지 않기 때문이다.
- internal의 등장: internal은 Kotlin에 새로 추가된 접근 제어자로 같은 모듈(module) 내에서만 접근 가능하다
- 모듈: 한 번에 함께 컴파일되는 단위 (예: Gradle 서브 프로젝트, Maven 아티팩트, IntelliJ IDEA 모듈 등)를 의미한다
- internal 멤버는 해당 모듈 외부(다른 모듈)에서는 접근할 수 없다
Namespace 예시
Kotlin의 package는 이름 충돌을 피하기 위한 논리적인 분류로 사용된다
// 서로 다른 패키지의 같은 이름 클래스
package com.app.util
class StringUtils {
fun doSomething() =
println("App StringUtils")
}
package com.lib.util
class StringUtils {
fun doSomething() =
println("Lib StringUtils")
}
fun main() {
// 사용 시 구분 (별칭을 사용하여 이름 충돌 회피)
import com.app.util.StringUtils as AppStringUtils
import com.lib.util.StringUtils as LibStringUtils
AppStringUtils().doSomething() // 출력 App StringUtils
LibStringUtils().doSomething() // 출력 Lib StringUtils
}
코틀린 파일의 최상위(Top-Level) 선언 접근 제어
Kotlin에서는 .kt 파일에 클래스 없어도 변수, 함수 등을 직접 선언할 수 있다. 이를 최상위(Top-Level) 선언이라고 하며, 이들의 접근 제어도 가능하다
- public (기본값): 어디서든 접근 가능하다
- protected: 파일 최상위에는 사용 불가능하다. protected는 클래스 멤버에만 적용되며, 상속 관계를 통해 접근을 제어하기 때문이다. 최상위 선언은 어떤 클래스의 멤버도 아니다.
- internal: 같은 모듈 내에서만 접근 가능하다
- private: 같은 파일 내에서만 접근 가능하다 (Java의 private static과 유사하게 파일 스코프로 제한된다)
// StringUtil.kt 파일 내용
private val privateVal = 10 // 이 파일 내에서만 접근 가능
internal val internalVal = 20 / 같은 모듈 내에서만 접근 가능
val publicVal = // public (기본값) 어디서든 접근 가능
private fun privateFunc() =
println("private function") // 이 파일 내에서만 호출 가능
internal fun internalFunc() =
println("internal function") // 같은 모듈 내에서만 호출 가능
fun publicFunc() =
println("public function") // public (기본값) 어디서든 호출 가능
// 클래스 정의
class MyClass() {
// ...
}
다양한 구성요소의 접근 제어
생성자 접근 제어
- 클래스 생성자에 접근 지시어를 적용하려면 constructor 키워드를 명시적으로 작성해야 한다
// internal 생성자: 같은 모듈 내에서만 이 클래스의 인스턴스 생성 가능
class Bus internal constructor(
val price: Int
)
// protected 생성자: 선언된 클래스 및 하위 클래스에서만 이 클래스의 인스턴스 생성 가능
// 주의: Final 클래스에서 protected 생성자는 사실상 private과 동일
class Car protected constructor(val price: Int) {
// 이 클래스는 기본적으로 final이므로 상속 불가능
// 따라서 이 protected는 실질적으로 private처럼 동작한다
// 컴파일러 경고: 'protected' visibility is effectively 'private' in a final class
}
// 해결 방법
// 클래스를 open으로 선언하여 상속 허용
open class OpenCar protected constructor(
val price: Int
)
// 생성자를 private으로 명시적으로 변경
class PrivateCar private constructor(
val price: Int
)
Java Util 클래스 설계 패턴과 Kotlin의 해법
- Java에서는 유틸리티 클래스 (예: StringUtils)를 정적 메서드만 제공하고 인스턴스화를 막기 위해 abstract 클래스에 private 생성자를 함께 사용했다. 이는 언어적 제약 때문에 발생하는 방어적 설계 패턴이다
// Java의 StringUtils 패턴
public abstract class StringUtils { // 직접 인스턴스화 방지
private StringUtils() { // 상속을 통한 인스턴스화 우회 방지
}
public static boolean isDirectoryPath(String path) {
return path.endWith("/");
}
}
Kotlin에서는 이러한 제약이 없으므로 더 직관적인 방법을 제공한다
- 최상위 함수(Top-level Function): Kotlin은 파일 최상위에 함수를 직접 선언할 수 있어, 별도의 클래스 없이도 유틸리티 함수를 제공할 수 있다
// StringUtil.kt
fun isDirectoryPath(path: String): Boolean {
return path.endWith("/")
}
- object 선언: 싱글턴 패턴을 위한 object 선언을 통해 정적 메서드와 유사하게 동작하는 유틸리티 객체를 만들 수도 있다
object StringUtils {
fun isDirectoryPath(path: String): Boolean {
return path.endWith("/")
}
}
// 사용: StringUtils.isDirectoryPath("/")
- Kotlin은 언어 차원에서 제공하는 도구(최상위 함수, object)를 통해 Java의 방어적 패턴 없이도 유틸리티 기능을 안전하고 자연스럽게 구현할 수 있다
프로퍼티 가시성 제어
Kotlin에서는 프로퍼티 전체뿐만 아니라, getter와 setter를 개별적으로 제어할 수 있어 유연한 캡슐화를 구현할 수 있다
- 프토퍼티 전체 가시성 제어: 프로프티 선언 앞에 접근 지시어를 붙인다. 이는 해당 프로퍼티의 getter와 setter (var인 경우) 모두에 적용된다
class Bar(
internal val name: String, // getter만 internal
private var owner: String // getter, setter 모두 private
)
- Getter / Setter 개별 제어: var 프로퍼티의 경우 set 또는 get 키워드 접근 지시어를 붙여 개별적으로 가시성을 설정할 수 있다
class Bar(_price: Int) {
var price = _price
private set // getter는 public (기본값), setter만 private
}
class User(
val id: String, // public val: 읽기 전용, 모든 곳에서 접근 가능
private var password: String // private var: 완전 비공개 (클래스 내부만 접근 가능)
_balance: Int
) {
var balance = _balance
private set // 읽기는 public, 수정은 클래스 내부에서만 가능 (캡슐화)
fun deposit(amount: Int) {
balance += amount // 내부에서만 balance 수정 가능
}
}
Java와 Kotlin을 함께 사용할 때 주의점
- internal 멤버 접근: Kotlin의 internal 멤버는 컴파일 시 바이트코드 상 public으로 표시된다. 따라서 Java 코드에서는 Kotlin 모듈의 internal 클래스, 함수, 프로퍼티에 접근할 수 있다. 아는 Kotlin의 internal 가시성 제어가 Java 코드에는 적용되지 않는다는 점을 의미하므로, 혼용 프로젝트 시 주의해야 한다
- 예시: 상위 모듈이 Java이고 하위 모듈이 Kotlin일 때, 하위 Kotlin 모듈의 internal 멤버는 상위 Java 모듈에서 접근 가능하다
- protected 멤버 접근: Kotlin의 protected와 Java의 protected는 동작 방식이 다르다. Java의 protected는 같은 패키지에서도 접근 가능하지만, Kotlin의 protected는 오직 상속 관계에서만 접근 가능하다
- 따라서 같은 패키지에 있는 Java 코드는 Kotlin 클래스의 protected 멤버에 접근할 수 있지만, 같은 패키지에 있는 다른 Kotlin 코드는 접근할 수 없다. 이 부분도 혼용 프로젝트에서 의도치 않은 접근 을 허용할 수 있으므로 유의해야 한다
Kotlin의 접근 제어는 Java보다 더 세밀하고 안전한 방식으로 캡슐화를 지원하며, package의 순수한 namespace 역할 분리를 통해 더욱 명확한 코드 구조를 가능하게 한다. 혼용 프로젝트 시에는 Java와의 미묘한 차이점을 이해하고 적절히 활용하는 것이 중요하다
출처 – 인프런 강의 중 자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide)