Kotlin 정보은닉 캡슐화

각 클래스나 메서드, 프로퍼티의 접근 범위를 가시성(Visibility)이라고 합니다. 클래스에서 민감하거나 불필요한 부분은 숨기고 사용하기 위해 필요한 부분만 공개하듯이 각 클래스나 메서드, 프로퍼티에 가시성 지시자에 의해 공개할 부분과 숨길 부분을 정할 수 있습니다.

가시성 지시자

  • private: 이 요소는 외부에서 접근할 수 없습니다.
  • public: 이요소는 어디서든 접근이 가능합니다.(기본값)
  • protected: 외부에서 접근할 수 없으나 하위 상속 요소에서는 가능합니다.
  • internal: 같은 정의의 모듈 내부에서는 접근이 가능합니다.

1. private 지시자

private은 접근 범위가 선언된 요소에 한정하는 가시성 지시자입니다. 만약 클래스를 private와 함께 선언하면 그 클래스 안의 맴버만 접근할 수 있습니다.

private class PrivateClass {
    private var i = 1
    private fun privateFunc() {
        i += 1
    }

    fun access() {
        privateFunc()
    }
}

class OtherClass {
    private val opc = PrivateClass() // private를 생략하면 접근 불가합니다. 
    fun test() {
       val pc = PrivateClass()
    }
}

fun main() {
    val pc = PrivateClass() // 생성 가능
    pc.i // 접근 불가
    pc.privateFunc() // 접근 불가
}

fun TopFunction() {
    val tpc = PrivateClass() // 객체 생성 가능
}

위 예제에서는 PrivateClass 클래스는 private으로 선언되어 있으므로 다른 파일에서 접근할 수 없습니다. 만일 다른 클래스에서 프로퍼티로서 PrivateClass에 접근하려하면 똑같이 private으로 선언해야 합니다.

2. protected 지시자

protected 지시자는 최상위에 선언된 요소에는 지정할 수 없고 클래스나 인터페이스와 같은 요소의 멤버에만 지정할 수 있습니다. 맴버가 클래스인 경우에는 protected로 선언할 수 있습니다.

open class Base {
    protected var i = 1
    protected fun protectedFunc() {
        i += 1
    }

    fun access() {
        protectedFunc()
    }
    protected class Nested // 내부 클래스에는 지시자 허용
}

class Derived: Base() {
    fun test(base: Base): Int {
        protectedFunc()
        return i
    }
}

fun main() {
    val base = Base()
    // base.i // 접근 불가
    // base.protectedFunc() // 접근 불가
    base.access()
}

3. internal

코틀린의 internal은 자바와 다르게 새롭게 정의된 이름입니다. internal 키워드를 사용합니다. 이 지시자는 모듈 내부에서만 접근이 가능하고, 모듈이 달라지면 접근이 불가능합니다.

Java의 package로 지정된 경우 접근 요소가 패키지 내부에 있다면 접근할 수 있습니다. 하지만 프로젝트 단위 묶음의 .jar 파일이 달라져도 패키지 이름이 동일하면 다른 .jar에서도 접근할 수 있었기 때문에 보안 문제가 발생 할 수 있습니다. 코틀린에서는 이것을 막고자 package를 버리고 internal로 프로젝트의 같은 모듈이 아니면 외부에서 접근할 수 없게 했습니다.

internal class InternalClass {
    internal var i = 1

    internal fun icFunc() {
        i += 1
    }

    fun access() {
        icFunc()
    }
}

class Other {
    internal val ic = InternalClass()
    fun test() {
        ic.i // 접근허용
        ic.icFunc() // 접근허용
    }
}

fun main() {
    val mic = InternalClass() // 생성 가능
    mic.i // 접근 허용
    mic.icFunc() // 접근 허용
}

이제 같은 프로젝트 모듈에만 있으면 어디서든 접근이 가능합니다.

최종적으로 자동차와 도둑의 예제를 통해서 가시성에 대해서 더 확실하게 이해할 수 있었습니다.

4. 예제 코드

open class Car protected constructor(_year: Int, model: String, _power: String, _wheel: String) {
    private var year: Int = _year
    public var model: String = model
    protected open var power: String = _power
    internal var wheel: String = _wheel

    protected fun start(key: Boolean) {
        if (key) println("Start the Engine!")
    }

    class Driver(_name: String, _license: String) {
        private var name: String = _name
        var license: String = _license
        internal fun driving() = println("[Driver] Driving() - $name")
    }
}

class Tico(_year: Int, model: String, _power: String, _wheel: String, var name: String, private var key: Boolean): Car(_year, model, _power, _wheel) {
    override var power: String = "50hp"
    val driver = Driver(name, "first Class")

    constructor(_name: String, _key: Boolean): this(2014, "basic", "100hp", "normal", _name, _key) {
        name = _name
        key = _key
    }

    fun access(password: String) {
        if (password == "gotico") {
            println("------[Tico] access()--------")
            println("super.model = ${super.model}")
            println("super.power = ${super.power}")
            println("super.wheel = ${super.wheel}")
            super.start(key)

            println("Driver().license = ${driver.license}")
            driver.driving()
        } else{
            println("You're a burglar")
        }
    }
}


class Burglar() {

    fun steal(anycar: Any) {
        if (anycar is Tico) {
            println("--------[Burglar] steal()----------")
            println("anycar.name = ${anycar.name}")
            println("anycar.wheel = ${anycar.wheel}")
            println("anycar.model = ${anycar.model}")

            println(anycar.driver.license)
            anycar.driver.driving()
            anycar.access("dontknow")
        } else {
            println("Nothing to steal")
        }
    }
}

fun main() {
    // val car = Car()
    val tico = Tico("kildong", true)
    tico.access("gotico")

    val burglar = Burglar()
    burglar.steal(tico)
}

Car 클래스는 상속이 가능하고, 이 클래스를 상속하는 Tico 클래스를 정의하였습니다. 그리고 도둑을 의미하는 Burglar로 정의하였습니다.

여기서 주의깊게 볼점은 아래와 같습니다.

  1. Car 클래스의 주 생성자는 protected 지시자가 있기 때문에 constructor 키워드를 생략할 수 없으며 Car 클래스를 상속한 클래스만이 Car 클래스의 객체를 생성할 수 있습니다.

  2. Driver 클래스는 Car 클래스 안에 있고, Car 클래스를 상속받는 Tico 클래스에서는 access() 메서드에서 super 키워드를 사용해 상위 클래스에 접근을 시도합니다. 이때 private 지시자가 적용된 year에는 접근이 불가합니다.

  1. Burglar 클래스를 살펴보면 steal() 메서드 하나만 정의하고 있습니다. Any 자료형의 매개변수인 anycar를 받아서 검사하고 있습니다. 이때 자료형 검사 키워드인 is를 사용해 Tico 의 객체인 경우 이 Tico 객체인 anycar를 통해 접근을 시도합니다. 특히 internal의 경우 파일이 달라져도 같은 모듈에 있으면 접근이 가능합니다.

참조 문헌: Do it 코틀린 프로그래밍

'Kotlin' 카테고리의 다른 글

코틀린 기본문법  (0) 2021.05.11

코틀린 기본문법

1. main() 함수

Intellij에서 코틀린 프로젝트를 생성하고, 마치 대학생때 가장 먼저 접하는 프로그래밍 대표 예제 Hello Kotlin!을 콘솔창에 띄우는 예제를 작성하였습니다.

fun main() {
    println("Hello Kotlin!")
}

위의 kotlin 파일에서 main() 함수만 정의하고 실행시키면 콘솔에 정상적으로 Hello Kotlin!이 출력됩니다.

main을 입력 후 Tab키를 누르면 자동으로 생성해줍니다. args를 추가할 경우에는 maina만 누르고 Tab키를 누르면 자동완성이 됩니다.

위의 main 함수는 JVM에서 실행되면 main 함수가 있는 파일 이름을 기준으로 자바 클래스가 자동생성 됩니다.

순서는 코틀린 -> 바이트 코드 순서로 JVM에서 실행됩니다.

위의 메서드를 decompile을 하면 아래와 같은 코드로 해석이됩니다.

public final class HelloKotlinKt {
    public static final void main() {
        String var0 = "Hello Kotlin!";
        boolean var1 = false;
        System.out.println(var0);
    }
}

위의 소스를 보면 main() 메서드는 HelloKotlinKt 클래스 안에 속한 멤버 메서드로 선언된 것을 알 수 있습니다. 이것은 자바 가상머신인 JVM에 실행되기 위해, 문자열은 String var1으로 선언되어 System.out.println()에 의해 콘솔 장치에 출력되는 것입니다.

package

코틀린 프로젝트는 여러 개의 모듈로 구성됩니다. 보통 각각의 기능을 모듈 단위로 분리해서 관리하고, 그 안에 package를 둬서 클래스/파일들을 관리합니다.

2. 변수 만들기

변수 선언 키워드는 2가지가 있습니다.

  • val: immutable 불변, 자바스크립트의 const, 자바의 String 클래스 같은 것입니다. 자바의 final 키워드랑은 살짝 다릅니다.
  • var: mutable 가변, 일반적인 변수로 재할당이 가능합니다. 자바스크립트의 let과 같은 것입니다. 코틀린에서 변수를 선언하려면 반드시 저 둘중에 하나의 키워드를 사용해야 합니다.

왠만하면 val 위주로 적용해서 안전성을 높이는 코딩을 지향해야하고, 꼭 가변적으로 할당되어야만 하는 변수에는 var를 사용하도록 합시다.

val username: String = "junyoung"

변수선언키워드 변수명:타입=값 이렇게 쓰면 됩니다. 코틀린 컴파일러가 타입을 추론할 수 있는 경우에는 타입을 생략해도 됩니다.

val username = "junyoung"

위와 같이 하면 값이 문자열인 것을 보고 코틀린 컴파일러가 username을 String 타입으로 추론합니다.

주의할 점은 변수 선언과 할당을 동시에 하지 않을 경우에는 타입을 생략할 수 없습니다.

val name // 불가능
val name:String // 가능
name = "hello"

위와 같이 선언만 먼저 하는 경우에는 반드시 타입을 적어줘야 합니다.

변수 선언 예제

// val var 차이
val name:String = "immutable"
var age:Int = 20
name = "junyoung"  // val로 설정했으니까 컴파일 시점에 에러가 납니다.
age = 30
println("age: $age")  // $(달러표시)로 변수를 출력할 수 있습니다. 

// 선언과 할당
var number = 10
number = 20
var number2:Int // Int라고 타입을 줬으니까 선언만 해도 됩니다.
number2 = 30
val number4 // 이 변수는 타입을 가져야 하거나 초기화가 되어야된다는 에러가 납니다.

변수 타입 기본형(Primitive type)과 참조형(Reference type)

코틀린에서는 참조형을 사용해서 코딩해야 합니다.
참조형만 사용해도 괜찮은 이유는 컴파일러가 내부적으로 기본형으로 바꿔서 최적화된 바이트 코드로 만들어주기 때문이빈다.
(int, long, double,.. 이런 기본형 타입 대신 Long, Double,.. 같은 참조형을 써야하고 이렇게 해도 내부적으로 잘 바꿔서 씁니다.)

3. 자료형

  • 정수 자료형
    • Long, Int, Short, Byte가 있습니다.
      변수를 선언할 때 타입을 별도로 적지 않으면 코틀린이 타입을 추론해서 지정해줍니다.
      만약 100이라는 값을 주고, 타입을 생략하면 Int로 추론하고 99933322233344455는 Long으로 추론합니다.

val ex1 = 100L 이렇게 접미사 L을 붙이면 Long 타입으로 추론합니다. 정수타입에서는 Int가 기본입니다. Short 범위 내 정수라고 해도 Short로 추론하지 않고 Int로 추론합니다.

val myLong = 99_933_322_233_344_455 // Long으로 추론
val ex1 = 0x0F // 16진수 표기 15
val ex2 = 0b0001111 // 2진수 표기 15
  • 실수 자료형

    • 기본적으로 double로 추론, 값에 F를 붙이면 Float 타입으로 추론합니다.
  • 논리 자료형

    • val isOpen = true 이렇게 하면 자동으로 Boolean으로 추론합니다.
  • 문자 자료형

    • '(single quote)으로 한 문자를 지정하면 Char로 추론합니다.
  • 문자열 자료형

    • 문자열 타입은 다른 것들과 다르게 기본형이 아니라 참조형 타입입니다. 값에 문자를 배열하면 String 타입으로 추론합니다. 특징적인 것은 자바와 마찬가지로 힙에 StringPool을 공유한다는 것입니다.
var string1 = "hello"
var string2 = "hello"
println(${string1 === string2}) // true
// string1과 string2가 참조하는 주소값이 같습니다.

문자열 내에 "(쌍따옴표)나 $(달러표시)를 쓰고 싶으면 백슬래시(\)를 앞에 붙여서 사용하면 됩니다.

val expression = "\"hello\" this is \$10"

백슬래시 대신 ${""}이렇게 써도 쌍따옴표를 적용할 수 있습니다.

""" 문자열 사용 법(문자열 그 자체 그대로 표현하는 방법)

val num = 10
val formattedString = """
    var a = 11
    var b = "hello kotlin"
    """
println(formattedString)

실행 결과

var a = 11
var b = "hello kotlin"
  • 자료형에 별명 붙이기
typealias Username = String
val user:Username = "junyoung"

typealias 키워드를 이용해서 String 타입에 Username이라는 별명을 붙여서 사용했습니다.

4. 자료형 검사

  • 코틀린 null

코틀린은 변수를 사용할 때 반드시 값이 할당되어 있어야 한다! 라는 원칙이 있습니다. (null을 싫어함) 지금까지 위에서 작성한 코드는 전부 다 변수에 null을 허용하지 않는 방식이 있습니다. 만약 null을 허용하고 싶으면 물음표(?) 기호를 타입 뒤에 붙여서 변수를 선언하면 됩니다.

// null 문제
val str:String = "hello_world"
str = null // 컴파일러에서 이미 에러 발생함
// 타입 뒤에 ? 붙이면 null이 할당될 수 있는 변수라는 의미입니다.
var str2:String? = "hello_world" // val은 immutable이라 ?를 붙여도 컴파일 에러가 발생합니다.
str2 = null
println(str2) // null 출력 
// non-null 단정 기호와 세이프 콜
var nullableString:String? = "hello_kotlin"
nullableString = null
// println("nullableString: $nullableString.length: ${nullableString.length}") // .에서 컴파일 에러 발생

// String? 타입에서는 ?.(세이프콜) 또는 !!.(non-null 단정기호)만 쓸 수 있다고 나온다. 
// safe call이란 말 그대로 안전하게 호출하도록 도와주는 기법이다. println("nullableString : $nullableString , length : ${nullableString?.length}") //출력 값 : nullableString : null , length : null
  • 엘비스 연산자: (?:)

위에서 배운 세이프 콜과 엘비스 연산자를 같이 사용하면 아주 아름다운 null 처리가 가능합니다.

var testString:String? = "hello_kotlin"
testString = null
println("testString length: ${testString?.length ?: -1}")

testString이 세이프 콜로 length에 접근하지 않고 null을 리턴한다고 하였으니 엘비스 연산자의 앞의 값은 null 입니다.

엘비스 연산자는 값이 null이면, 뒤에 미리 지정해 놓은 값을 리턴하는 연산자입니다. 따라서 출력은 -1로 지정해놓았으니까 -1이 나올 것입니다.

if 문으로 null 검사를 일일이 할 필요가 없어지고 처리까지 한 줄에 되므로 가독성도 좋아집니다.

5. 자료형 변환

자바에서는 int 값을 double 변수에 할당하면 자동으로 타입이 변환되었습니다.

int a = 3;
double d = a; // 3.0으로 자동 변환

코틀린은 자동으로 형 변환을 시켜주지 않습니다. 오히려 타입 미스매치 에러를 내면서 실수를 철저하게 방지합니다. 그래서 코틀린은 형변환 메서드를 제공합니다.

val intValue:Int = 3
val doubleValue:Double = intValue.toDouble()
val result = 1L + 3 // Long으로 형변환 -> 표현범위가 더 큰쪽으로 변환됩니다.

기본형과 참조형 타입 비교 원리

값 비교: ==, 값이 같으면 true 다르면 false
참조 주소 비교: ===, 값과 상관없이 참조 자체가 동일하면 true 다르면 false(값과 상관없지만 참조 자체가 동일하면 값도 같습니다...)

기본형과 참조형 타입 비교 연산예제

val one:Int = 128
val two:Int = 128

println(one == two) // true
println(one === two) // true

val three:Int = 128
val four:Int? = 128

println(three == four) // true
println(three === four) // false
// Int 형으로 선언된 three는 128 값 자체가 스택에 저장, Int? 형으로 서언된 four는 128이 힙 영역에 저장되어 있고, 그것의 주소로 저장합니다.

val data1:Int = 128 
val data2 = data1; 
println(data1 == data2)//기본형으로 int로 변환되어 값이 동일하므로 true 

val data3:Int? = data1  //128이라는 값을 힙에 할당하고 그것을 가리킵니다. 
val data4:Int? = data1  //128이라는 값을 힙에 할당하고 그것을 가리킵니다. 
val data5:Int? = data3 
println(data3 == data4) //true 값만 비교했는데 두 개의 값은 128로 같습니다.

//각각 값은 128로 동일하지만 힙에 생긴 128은 각각 생기기 때문에 다른 주소를 가리키므로 주소비교는 false 
println(data3 === data4) //false
/**
* data5는 data3를 가리키는데 data3가 힙의 128의 주소를 가리키고 있으므로 
* 복사되었을 때 같은 힙에 있는 128을 가리킵니다.
**/
println(data3 === data5) // true

메모리 영역 중 하나인 힙에 생성되느냐 스택에 생성되느냐를 일단 알아야 하는데 간략하게 설명하면 null을 허용하지 않는 변수는 참조형을 사용하더라도 내부적으로 기본형으로 값을 갖게 되는데 그것이 스택에 직접 할당됩니다.

그러나 null을 허용하는 변수는 기본형으로 변환되는 참조형일지라도 자기만의 값을 힙에 할당하고 그것을 가리키는 주소를 스택공간에 할당합니다.

  • 기본적으로 null을 허용한다는 것 그 자체로 그 변수를 객체로 본다는 뜻입니다. 기본형에는 null이란 것을 할당할 수 조차 없습니다.
  • -128 ~ 127의 정수 값은 캐시에 저장되어 참조됩니다.

따라서 위의 예제에서 값을 만약 10으로 했다면 10이라는 값 자체가 캐시에 저장되고 모든 변수들은 그 캐시의 주소값을 저장하고 있을 것입니다.

6. 스마트 캐스트

어떤 값이 정수일 때도 있고 실수일 때도 있으면 어떻게 해야할까요?
Number 타입이라는 것도 있습니다. (특수한 객체)
Number 타입으로 정의된 변수에는 정수, 실수 둘다 들어갈 수 있습니다.

var sc:Number = 3.14
println(sc) // 3.14 Double

sc = 3
println(sc) // 3 Int

sc = 30L
println(sc) // 30 Long

sc = 10.1F
println(sc) // 10.1 Float

자료형 검사할 때 is 키워드

val cup = 3
if(cup is Int) {
    println("Int Type")
}
if(cup !is Int) {
    println("Not a Int Type")
}
if(!(cup is Int)) {
    println("Not a Int Type")
}

자료형 스마트 캐스트

val x:Any // Any는 자바의 Object와 유사합니다. 모든 타입의 부모 타입입니다. Any를 이용하면 자료형을 결정하지 않은 채로 변수를 선언할 수 있습니다.
x = "jdk"
if(x is String) { // is 키워드로 검사될 때 자동으로 스마트 캐스팅 되었으므로 해당 조건문을 탑니다.
    println(x)
}

as 키워드에 의한 스마트 캐스트

var y:Int = 3
val t:String = y as String 
val tt:String? = y as? String

y가 null이 아니면 String으로 형변환되어 t에 할당됩니다. 만약 null이라면 형변환 할 수 없으므로 exception이 발생합니다.

null이 발생할 가능성이 있다면 예외를 피하기 위해 아래와 같이 물음표를 붙여서 해결 가능합니다.

y가 null이면 as 키워드에 접근하지 않고 null을 리턴합니다. 그러면 tt는 null이 가능하니까 null을 저장합니다.

참조 문헌: Do it! 코틀린 프로그래밍

'Kotlin' 카테고리의 다른 글

코틀린 정보은닉 캡슐화  (0) 2021.05.11

+ Recent posts