Kotlin

Kotlin 클래스와 객체 (4)

smileDeveloper 2024. 3. 1. 13:44
반응형
SMALL

이번 주제는 클래스와 객체입니다. C++ 같은 객체지향 프로그래밍 언어에 익숙하다면 클래스에도 익숙하실 것 같습니다.

기본적으로 클래스 선언은 참조 타입(Referential Type)을 정의합니다. 이런 참조 타입의 값은 클래스 인스턴스(Instance)의 실제 데이터의 위치를 가리키는 참조의 의미를 가집니다. 자바 인스턴스는 명시적으로 생성자 호출을 통해 생성되고 객체를 가리키는 참조가 사라지면 가비지 컬렉터(Garbage Collector)에 의해 자동으로 해제됩니다. 

클래스의 내부 구조

class Person {
    // 프로퍼티 (Property)
    // 프로퍼티에 어떤 계산이 포함될 경우, 호출이 될 때 계산이 되거나, 지연 계산되거나, 맵에서 값을 얻어오는 방식으로 값을 제공한다.
    var firstName:String = ""
    var familyName:String = ""
    var age:Int = 0

    // 멤버 함수, 메서드 (Method)이다.
    // 함수 - 1
    fun fullName() = "$firstName $familyName"
    fun showMe() = println("${fullName()} : $age")

    // 함수 - 2
    // 아래 함수에서 p, person 이 인스턴스이다. 인스턴스는 수신 객체 (Receiver) 라고도 부른다
    fun showAge(person: Person) = println("${person.age}")
    fun readAge(p:Person) {
        p.age = readLine()!!.toInt()
    }

    // 함수 - 3
    // this 식으로 수신 객체를 참조할 수 있다.
    fun fullName2() = "${this.firstName} ${this.familyName}"
    fun showMe2() = println("${this.fullName2()} : ${this.age}")

    // 클래스의 프로퍼티와 메서드 파라미터의 이름이 같을 경우에 필요하다.
    fun setName(firstName:String,familyName:String) {
        this.firstName = firstName
        this.familyName = familyName
    }
 }

코틀린 프로퍼티는 캡슐화(Encapsulation)에 위배되지 않습니다. 여기서 캡슐화란 클래스의 프로퍼티를 외부에서 직접 접근하는 것을 막고 (private) , 메서드를 통해 변수의 값을 설정 (setter) 하거나 변수의 값을 리턴 (getter) 하게 해줘야 하는 과정입니다. 인스턴스를 사용할 땐 명시적으로 생성합니다. 생성자를 호출하면 새 인스턴스에 대한 힙 메모리를 할당하고 생성자 코드를 호출합니다.

 

생성자

// 클래스 헤더의 파라미터 목록을 주생성자 (Primary Constructor) 선언이라고 부릅니다.
// 클래스 정의 내에서 프로퍼티 초기화와 초기화 블록이 등장하는 순서대로 구성됩니다.
class Person2(firstName: String,familyName: String) {
    val fullName = "$firstName $familyName"

    init {
        println("Created Person2 instance : $fullName")
    }
    // init 블록은 여러개가 들어갈 수 있습니다.
    init {
        println("Created Second Person2 instance : $fullName")
    }

}

// init 블록 안에서 프로퍼티를 초기화할 수 있습니다.
class Person3(fullName:String) {
    val firstName:String
    val familyName:String
    init {
        val names = fullName.split(" ")
        if(names.size == 2) {
            firstName = names[0]
            familyName = names[1]
        }
        else {
            throw Exception("Names Size Must be 2")
        }
    }
}

// val이나 var 키워드를 덧붙이면 자동으로 해당 생성자 파라미터로 초기화되는 프로퍼티를 정의합니다.
class Person4(val firstName:String,val familyName: String) {

}

// 부 생성자 (Secondary Constructor)
// 서로 다른 방법으로 초기화하고 싶을 때 사용합니다.
// 기본적으로는 부생성자는 Unit 타입 값을 반환합니다.
// 부 생성자의 파라미터에는 val/var 키워드를 사용할 수 없습니다.
class Person5 {
    val firstName:String
    val familyName:String

    constructor(firstName: String,familyName: String) {
        this.firstName = firstName
        this.familyName = familyName
    }

    constructor(fullName: String) {
        val names = fullName.split(" ")
        if(names.size == 2) {
            firstName = names[0]
            familyName = names[1]
        }
        else {
            throw Exception("Names Size Must be 2")
        }
    }
}

+) vararg 생성자 파라미터도 사용 가능

 

커스텀 접근자

class Person6(var firstName:String,var familyName: String,age:Int) {

    var fullName: String
        // 커스텀 접근자는 프로퍼티 값을 읽거나 쓸 때 호출되는 특별한 함수이다.
        // 프로퍼티를 읽을 때에 자동으로 게터를 호출한다다.
        // 파라미터가 없고 반환 타입은 프로퍼티의 타입과 같아야 한다.
        // fullName엔 뒷받침하는 필드 (Backing Field)가 없어 메모리를 차지하지 않는다.
        get() = "$firstName $familyName"
        // 모두 가변(var) 일때 정의 가능
        set(value) {
            val names = value.split(" ")
            if(names.size == 2) {
                firstName = names[0]
                familyName = names[1]
            }
            else {
                throw Exception("Names Size Must be 2")
            }
        }
    
    var fullName2:String = "TEST"
        private set // 클래스 밖에서는 변경 불가능합니다.

    // 뒷받침 필드에 접근, field 라는 키워드를 사용하여 접근자 본문 안에서 사용할 수 있습니다.
    var age: Int = age
        get() {
            println("LOG Example")
            return field
        }
        set(value) {
            field = value
        }

}

멤버 가시성

가시성은 클래스 멤버마다 다르게 지정할 수 있습니다. 가시성을 사용해 구현과 관련한 세부 사항을 캡슐화함으로써 외부 코드로 부터 구현 세부 사항을 격리시킬 수 있습니다.

내포된 클래스 ( Nested Class )

  • 내포된 클래스에 inner를 붙이면 자신을 둘러싼 외부 클래스의 현재 인스턴스에 접근 가능
  • 내부 클래스를 가리킬 때 this를 생략 가능
  • 외부 클래스 인스턴스를 가리킬 땐 한정시킨(qualified) this 식을 사용

널 (Null) 가능성 

코틀린의 참조 값에는 아무것도 참조하지 않는 경우를 나타내는 특별한 값인 널(null)이란 값이 있습니다. 널인 참조타입의 프로퍼티나 메소드를 사용할 때 NullPointerException(NPE) 오류가 발생하는데 이 오류는 런타임에서만 오류를 잡을 수 있기 때문에 처리하기가 까다롭습니다.

  • 널이 될 수도 있는 값은 타입 뒤에 물음표(?) 를 붙여 널이 될 수 있는 타입으로 지정 ex) String? 은 널이 될 수 있는 타입 (Nullable Type)이라합니다.
  • 모든 널이 될 수 있는 타입은 원래 타입의 상위 타입
  • 원시 타입도 널이 될 수 있는 타입이 존재하지만, 원시 타입의 널이 될 수 있는 타입은 항상 박싱한 값만 표현
  • 가장 작은 널이 될 수 있는 타입은 Nothing?, 가장 큰 타입은 Any?
 fun checkEmpty(s:String?):Boolean {
        if(s == null) return false
        if(s.isEmpty()) return false
        return true
    }

널이 될 수 있는 값을 처리하는 가장 직접적인 방법은 해당 값을 조건문을 사용해 null과 비교하는 것입니다. 위 함수는 스마트 캐스트 (Smart Cast)라고 불리는 코틀린 기능이 위 일을 가능하게 해줍니다.

 

널 관련 연산자를 살펴보겠습니다.

  • !! : 널 아님 단언 연산자(Not-Null Assertion) : 추천하지는 않는 방식 ex) readline()!!.toInt()
  • ?. : 안전한 호출 연산자(Safe Call) : 수신 객체가 널이 아닌 경우엔 실행하고 아닌 경우에 null을 반환 ex) readLine()?.toInt()
  • ?: : 널 복합 연산자(Null Coalescing Operator) : 널을 대신할 디폴트 값을 지정 가능. 엘비스 연산자라고도 부름  ex) readLine()?.toInt() ?: 0

특별한 키워드

  • lateinit 
    • 가변 프로퍼티로 정의
    • 널 타입과 원시 타입은 정의 X
  • lazy
    • 사용자가 적절한 명령으로 프로퍼티 값을 읽기 전까지 프로그램은 lazy 프로퍼티의 값을 계산하지 않음
    • 초기화가 된 이후 프로퍼티 값은 필드에 저장되고 그 후엔 저장된 값을 읽음
    • 초기화 된 이후에 변경되지 않음
    • 스레드 안전(Thread-Safe) : 한 스레드에서만 계산하기 때문에 궁극적으로는 같은 값을 얻음
    •  

* 위임 객체(Delegate Object) 를 통해 프로퍼티를 구현하게 해주는 위임 프로퍼티(Delegate Property) 라는 기능의 특별한 경우입니다. 위임 객체는 by 라는 키워드 다음에 위치합니다. 

 

객체 ( Object )

object 라는 키워드를 사용하여 싱글톤 (Singleton) 패턴을 내장하고 있는 객체를 만들 수 있습니다. 싱글톤이란 객체의 인스턴스를 1개만 생성하는 패턴입니다. 객체 정의도 스레드 안전합니다. 한 인스턴스만 공유되고 초기화 코드도 단 한 번만 실행되도록 보장합니다.

동반 객체 ( Companion Object )

팩토리 디자인 패턴을 쉽게 구현하는 경우 유용하게 활용할 수 있습니다.

반응형
LIST

'Kotlin' 카테고리의 다른 글

Kotlin 함수 정의하기 (3)  (1) 2024.02.27
Kotlin 언어 기초 (2)  (1) 2024.02.25
Kotlin 개념 (1)  (0) 2024.02.21
Kotlin 시작  (1) 2024.02.15