Developing Myself Everyday

클래스(Class)란?


클래스는 객체지향에서 가장 중요한 도구입니다. 클래스는 유사한 특성과 동작을 가진 객체들을 만들기 위한 템플릿 역할을 합니다. 클래스는 객체의 속성(Attribute)메서드(method)를 정의하는데 사용되며, 이러한 속성과 메서드는 해당 클래스로부터 생성된 객체에 적용됩니다.

 

 

 

생성자 (Constructor)


틀린의 클래스 생성자는 클래스의 인스턴스를 초기화하기 위한 특수한 예약어로, 클래스 정의 시 초기화 작업을 수행하거나 속성 값을 설정하는 역할을 합니다. 코틀린에서는 주 생성자(primary constructor)보조 생성자(secondary constructor)를 사용하여 클래스 생성자를 정의할 수 있습니다.

 

 

주 생성자(primary constructor)

주 생성자(primary constructor)를 사용해 클래스를 정의해 보겠습니다. 

fun main() {
    val a = Person("준형", 26)
}

class Person(val name: String, val age: Int) {
    init {
        println("Person object가 다음과 같이 생성되었습니다.: $name, age: $age")
    }
}

// 출력: Person object가 다음과 같이 생성되었습니다.: 준형, age: 26

`init` 은 코틀린 클래스 내에서 사용되는 특수한 블록으로 객체가 생성될 때 초기화 작업을 수행하기 위해 사용됩니다. `init` 블록은 클래스 몸체부 정의 내에서 선언되며, 객체가 생성될 때 자동으로 실행됩니다.

주 생성자는 클래스 이름 뒤에 `constructor` 키워드와 함께 작성되며 클래스의 주요 속성과 초기화 작업을 함께 정의할 수 있습니다. 이렇게 선언한 속성은 클래스 내에서 어디서나 사용할 수 있습니다.

주 생성자의 접근 제한자를 지정하지 않은 경우에는 `constructor` 를 생략할 수 있습니다.

 

 

 

보조 생성자(secondary constructor)

이번에는 보조 생성자(secondary constructor)를 사용해 클래스를 정의해 보겠습니다. 보조 생성자도 `constructor` 키워드를 사용하여 클래스 몸체부 정의 내에서 선언합니다. 주 생성자와 보조 생성자를 같이 정의한 경우에는 보조 생성자 중 하나에 반드시 주 생성자를 this로 호출하는 위임 호출을 해야 합니다.

fun main() {
    val a = Person("준형", 26)
    val b = Person("영희")
}

class Person(val name: String, val age: Int) {
    init {
        println("Person object가 다음과 같이 생성되었습니다.: $name, age: $age")
    }

    constructor(name: String) : this(name, 0) {
        println("보조 생성자로 초기화되었습니다.: $name, age: $age")
    }
}

// 출력 :
// Person object가 다음과 같이 생성되었습니다.: 준형, age: 26
// Person object가 다음과 같이 생성되었습니다.: 영희, age: 0
// 보조 생성자로 초기화되었습니다.: 영희, age: 0

 

위에 코드에서는 보조 생성자를 추가하여 `name` 만을 인자로 받아 기본값인 `0` 을 사용하여 `age` 를 초기화 한 후에 메시지를 출력하고 있습니다.

 

코틀린에서 클래스의 생성자 초기화 순서는 다음과 같기 때문에 위와 같은 출력이 나오게 됩니다.

 

 1. 주요 생성자(primary constructor)의 초기화 코드가 실행됩니다.

 2. init 블록의 초기화 코드가 실행됩니다.

 3. 보조 생성자(secondary constructor)의 초기화 코드가 실행됩니다.

 

 

 

객체 생성으로 생성자를 구분

객체를 생성할 때, 주 생성자와 보조 생성자의 인자를 통해 구분하여 호출합니다. 그렇기 때문에 인자의 개수와 타입이 동일한 경우에는 어떤 생성자가 호출되는지를 명확히 판단할 수 없습니다. 따라서 보조 생성자를 사용할 때는 주 생성자와 다른 타입의 인자나 다른 개수의 인자를 사용하여 호출하여야 합니다.

fun main() {
    val a = Person("준형", 26)
    val b = Person("영희")
}

위의 예시에서 객체를 생성할 때, 위와 같이 생성자를 구분하였습니다.

 

이런 방식으로 생성자를 구분하기 때문에 여러개의 보조 생성자를 생성하는 것도 가능합니다.

 

 

 

클래스 속성을 init으로 초기화

클래스에 속성이 있을 때에 이를 주 생성자로부터 받은 값으로 초기화할 수가 있습니다.

 

아래의 코드를 보겠습니다.

fun main() {
    val a = Person("준형", 26)
}

class Person(name: String, age: Int) {
    var name: String = ""
    var age: Int = 0

    init {
        this.name = name
        this.age = age
        println("Person object가 다음과 같이 생성되었습니다.: ${this.name}, age: ${this.age}")
    }
}

// 출력: Person object가 다음과 같이 생성되었습니다.: 준형, age: 26

위의 코드에서 `init` 블록은 객체 생성시 초기화 작업을 수행하기에 주 생성자로부터 전달받은 `name` 과 `age` 를 사용해 `Person` 클래스의 속성인 `name` 과 `age` 를 초기화 합니다. 

 

다만 위의 코드에선 주 생성자로부터 전달받은 매개변수와 클래스의 속성의 이름이 일치합니다. 그렇기 때문에 이를 구분해주기 위해 `this` 키워드를 사용합니다. `this` 키워드는 현재 객체를 가리키는 키워드로 위의 코드에서는 `Person` 클래스를 가리키게 됩니다. 

 

 

 

보조 생성자만 있는 클래스에서의 초기화

클래스의 속성을 주 생성자가 없이 보조 생성자만 사용해 초기화 작업을 수행할 수 있습니다.

fun main() {
    val a = Person("준형", 26)
    val b = Person("영희")
}

class Person {
    var name: String = ""
    var age: Int = 0

    constructor(name: String) {
        this.name = name
        println("Person object가 다음과 같이 생성되었습니다.: ${this.name}, age: ${this.age}")
    }

    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
        println("Person object가 다음과 같이 생성되었습니다.: ${this.name}, age: ${this.age}")
    }
}

// 출력:
// Person object가 다음과 같이 생성되었습니다.: 준형, age: 26
// Person object가 다음과 같이 생성되었습니다.: 영희, age: 0

보조 생성자만 있을 경우에는 위임 호출을 하지 않아도 됩니다.


 

 

 

수정자 (modifier)


클래스를 정의할 때 사용할 수 있는 것이 바로 수정자(modifier)입니다. 수정자는 상속을 지정하는 수정자와 클래스를 사용할 수 있는 범위를 지정하는 수정자로 나뉩니다.

 

상속 수정자 

 - final: 클래스를 상속 불가능하게 만듭니다. 코틀린에서 클래스의 기본은 final입니다.

 - open: 클래스를 상속 가능하게 만듭니다. 

 

가시성 수정자 (접근 제한자)

 - public: 어디에서나 접근할 수 있습니다. 코틀린에서 클래스의 기본은 public입니다.

 - private: 해당 파일 또는 클래스 내에서만 접근 가능합니다.

 - protected: private와 같지만 같은 파일이 아니더라도 자식 클래스에서는 접근이 가능합니다.

 - internal: 같은 모듈 내에서 접근 가능합니다.

 


자바에서는 접근 제한자를 붙이지 않으면 기본값이 default (package-private) 입니다. 코틀린에서는 해당하는 접근 제한자가 없기에 internal이라는 제한자를 추가해 유사한 효과를 내도록 설계하였습니다.

 

 

패키지 단위에서의 접근 제한자

패키지는 관련된 클래스, 함수, 변수 등을 그룹화하여 코드의 가독성을 높이고 유지 보수를 용이하게 만드는 데 도움을 줍니다. 

 

접근 제한자를 사용해 패키지 내부에서만 접근 가능한 요소들을 정의할 수 있습니다. 아래의 코드를 보겠습니다.

 

`MyPackage.kt`

package mypackage

private fun privateFunction() {
    println("Private function in mypackage")
}

internal fun internalFunction() {
    println("Internal function in mypackage")
}

fun publicFunction() {
    println("Public function in mypackage")
}

위의 코드는 `mypackage` 라는 패키지에서 접근 제한자를 사용해 함수를 생성한 예시입니다. 이제 같은 패키지 안에 다른 파일을 생성해서 위의 함수를 호출해 보겠습니다.

 

`Main.kt`

package mypackage

fun main() {
    privateFunction() // 오류: privateFunction은 불가능
    internalFunction() // 출력: Internal function in mypackage
    publicFunction() // 출력: Public function in mypackage
}

defaultpublicInternal 접근 제한자를 사용한 함수를 호출할 수 있는 반면, private으로 선언된 함수를 호출하면 오류가 발생하는 것을 알 수 있습니다.

 

 

클래스 내의 멤버 접근 제한자

클래스 내부의 모든 멤버에 대해서도 가시성을 지정할 수 있습니다. 

class Example {
    private val privateProperty: Int = 10
    internal val internalProperty: String = "Internal"
    protected val protectedProperty: Double = 3.14
    public val publicProperty: Boolean = true
}

 

 

클래스의 주 생성자의 접근 제한 (private)

클래스의 주 생성자에도 private를 지정할 수 있습니다. 주 생성자에 private을 지정하는 것은 클래스의 인스턴스 생성과 관련된 접근을 제어하는 기능입니다. 코틀린에서 주 생성자는 클래스의 인스턴스를 생성할 때 사용되는 매개변수를 정의합니다. 즉, 주 생성자의 접근 제한을 설정하면 해당 생성자를 통한 인스턴스 생성을 제한할 수 있습니다.

 

아래의 간단한 예시를 보겠습니다.

class Person private constructor(val name: String, val age: Int) {
    // 주 생성자를 private으로 설정하여 외부에서 인스턴스 생성을 막음

    fun introduce() {
        println("이름은 $name 이고 나이는 $age 입니다.")
    }
}

fun main() {
    // 외부에서 주 생성자를 사용한 인스턴스 생성을 막아서 아래 코드는 오류를 발생시킴
    val person = Person("준형", 26) // 오류: 주 생성자에 접근할 수 없음
}

위 예시에서 `Person` 클래스의 주 생성자를 private로 설정해 외부에서의 인스턴스 생성을 막았습니다. 따라서 `main` 함수에서 주 생성자를 사용한 인스턴스 생성이 불가능합니다. 


주 생성자의 접근 제한자를 설정하려면 생략했던 `constructor`를 다시 사용해야 합니다. 그리고 접근 제한자를 `constructor` 앞에 사용합니다.

 

이렇게 주 생성자의 접근 제한을 설정함으로써 클래스의 인스턴스 생성을 제어할 수 있습니다.

 

 

 

companion object

코틀린에서 `companion object` 는 클래스 내부에 정의되며, 해당 클래스와 관련된 메서드나 프로퍼티를 그룹화하고 호출할 수 있는 특별한 객체입니다. `companion object` 를 사용하면 해당 클래스의 인스턴스 없이도 클래스 내부에 접근할 수 있습니다. 주로, 싱글톤(Singleton) 패턴이나 팩토리 메서드(Factory Method) 패턴을 구현하는 데 사용됩니다.

 

클래스의 주 생성자에 대한 접근을 제한했을 때 클래스 내부의 메서드에 접근할 때 바로 이 `companion object` 를 사용할 수 있습니다.

class Person private constructor(val name: String, val age: Int) {
    companion object {
        fun introduce(name: String, age: Int) {
            println("이름은 $name 이고 나이는 $age 입니다.")
        }
    }
}

fun main() {
    val person = Person.introduce("준형", 26)
}

 

 

 

 

메서드 (method)


함수와 메서드

함수는 입력값을 받아서 어떤 연산을 수행하고 결과값을 반환하는 코드 블록을 말합니다. 함수의 가장 큰 특징은 일반적으로 어떤 특정 객체에 종속되지 않는다는 것입니다. 메서드특정 클래스나 객체에 속하는 함수입니다.

 

자바에서 메서드를 정의하기 위해서는 클래스 안쪽이어야 합니다. 클래스 바깥, 즉 최상위 레벨에서는 메서드를 정의할 수 없습니다. 그렇기에 자바의 관점에서 함수와 메서드는 거의 차이가 없기에 함수를 메서드라 통칭합니다.

 

다만, 코틀린에서는 조금 다른 부분이 있습니다. 코틀린에서는 꼭 클래스 안이 아닌 최상위 레벨에도 함수를 정의할 수 있다는 점입니다. 그래서 모든 함수를 메서드라고 통칭하는 것은 옮지 않습니다.

 

 

 

메서드 참조(method reference)

저번주에 함수 참조에 대해 배웠습니다. 메서드 또한 함수이기에 메서드 참조도 이용 가능합니다.

 

메서드 참조를 사용해보기 전에 바운드(Bound) 언바운드(Unbound) 참조 방식에 대해 알아야 합니다.

 

바운드(Bound) - 메서드와 객체가 연결되어 있는 상태를 나타냅니다. 즉, 객체가 메서드를 호출할 때 해당 메서드 내에서 `this` 키워드는 호출한 객체를 가리킵니다.

 

언바운드(Unbound) - 메서드와 객체가 연결되어 있지 않은 상태를 나타냅니다. 즉, 메서드를 호출한 객체가 없거나, 메서드와 객체 간의 연결이 끊어진 상태를 의미합니다.

 

아래의 예시를 보겠습니다.

class MyClass {
    fun method() {
        println(this.javaClass) // "this"는 호출한 객체에 바운드됨
    }
}

fun main() {
    MyClass().method() // 바운드 메서드 호출 (출력: class MyClass)
    val unboundMethod = MyClass::method // 언바운드 메서드 참조 (출력: X)
    unboundMethod.invoke(MyClass()) // 언바운드 메서드 호출 (출력: class MyClass)
}

 

`MyClass().method()``MyClass`의 인스턴스를 생성한 후, `method()`를 호출한 것입니다. 따라서 `this`는 생성된 객체에 바운드 되며, `method()` 내부에서 해당 객체의 클래스 정보를 출력합니다.

`MyClass::method`method 메서드의 참조를 나타내며, 이는 클래스에 종속되지 않은 언바운드(unbound) 메서드 참조입니다. 클래스의 인스턴스를 생성하지 않고도 해당 메서드를 참조할 수 있습니다.

`unboundMethod.invoke(MyClass())` 은 언바운드 메서드 참조인 unboundMethod를 호출하는 것입니다. 여기서 invoke() 메서드를 사용하여 메서드를 호출할 수 있으며, `MyClass()`를 전달하여 `method()`를 호출한 것과 동일한 결과를 얻을 수 있습니다.

 

 

메서드 참조는 리플렉션을 사용하기에 객체가 없습니다. 그렇기에 이를 사용하려면 객체를 직접 전달해 바운드 처리를 해야 합니다.

 

 리플렉션이란?
리플렉션은 자바의 기능 중 하나로, 런타임에 클래스의 정보를 동적으로 검사하고 조작할 수 있도록 합니다. 리플렉션을 사용하면 클래스의 메서드, 필드, 생성자 등에 접근하고 호출할 수 있습니다.
코드를 작성하는 시점에서는 런타임에서 동작할 코드가 아니라 컴파일 된 바이트 코드가 실제로 실행되기 때문에, 그 코드 내에서 직접적으로 작성한 소스 코드를 찾을 수 없습니다. 이때 리플렉션을 사용하면 런타임에 프로그램의 클래스를 조사할 수 있습니다.

 

 

 

메서드 참조를 사용해 함수의 인자로 전달

메서드 참조를 사용하면 인자로 전달하기가 매우 간단해 집니다. 아래는 더하기와 빼기 메서드를 `Operator` 클래스 멤버로 정의하고, 이를 메서드 참조를 사용해 `calculate` 함수의 인자로 전달한 예시입니다.

class Operator {
    fun add(a: Int, b: Int): Int {
        return a + b
    }

    fun subtract(a: Int, b: Int): Int {
        return a - b
    }
}

fun calculate(operation: (Int, Int) -> Int, x: Int, y: Int): Int {
    return operation(x, y)
}

fun main() {
    val operator = Operator()

    val resultAdd = calculate(operator::add, 10, 5)
    val resultSubtract = calculate(operator::subtract, 10, 5)

    println("더하기: $resultAdd") // 출력: 더하기: 15
    println("빼기: $resultSubtract") // 출력: 빼기: 5
}

 

 

 

 

상속


상속이란?

Kotlin에서 상속은 클래스 간의 계층 구조를 형성하며, 한 클래스가 다른 클래스의 특성과 동작을 상속 받을 수 있도록 해줍니다.

상속 대상이 되는 클래스를 슈퍼 클래스(부모 클래스)라고 하고 이를 상속한 클래스를 서브 클래스(자식 클래스)라고 합니다.

 

 

위에서 수정자를 이야기 할때 상속 수정자에 대한 이야기도 있었습니다. 상속 수정자에는 finalopen이 있고 각 역할은 다음과 같습니다.

 

 - final: 클래스를 상속 불가능하게 만듭니다. 코틀린에서 클래스의 기본은 final입니다.

 - open: 클래스를 상속 가능하게 만듭니다. 

 

코틀린에서 클래스의 기본은 final이기에 상속이 필요한 경우에는 open 상속 수정자를 사용해야 합니다.

 

 

아래는 `Parent` 부모 클래스를 상속한 `Child` 자식 클래스의 예시입니다.

open class Parent {
    open fun printMessage() {
        println("This is from the Parent class")
    }
}

class Child : Parent() {
    override fun printMessage() {
        super.printMessage()
        println("This is from the Child class")
    }
}

fun main() {
    val child = Child()
    child.printMessage()
}

// 출력
// This is from the Parent class
// This is from the Child class

위의 코드에서 `Child` 클래스는 `Parent` 클래스를 상속하고 있습니다. `Child` 클래스는 Parent` 클래스를 상속받았기에 부모 클래스에 있는 특성과 동작을 상속받을 수 있으며, `Child` 클래스는 Parent` 클래스에 있는 `someFunction` 메서드를 재정의(override)할 수 있게 됩니다.

 

 `Child` 클래스에서는 `super` 키워드를 사용해 부모 클래스의 멤버를 호출할 수 있습니다. 위의 코드에서는 `super.printMessage()`를 통해 부모 클래스의 멤버 메서드를 호출하고 있습니다.

 

이런 동작을 통해 상속이 이뤄지고 메서드가 실행됩니다.

 

 

 

부모 클래스와 자식 클래스의 생성자 호출

아래의 코드에 주 생명자만 있는 부모 클래스를 정의하고 이를 상속하는 자식 클래스에도 주 생성자를 정의하였습니다.

open class Parent(val name: String) {
    init {
        println("Parent class init block. 이름: $name")
    }

    open fun greet() {
        println("From Parent. 이름: $name")
    }
}

class Child(name: String, val age: Int) : Parent(name) {
    init {
        println("Child class init block. 나이: $age")
    }

    override fun greet() {
        println("From Child. 이름:$age")
    }
}

fun main() {
    val child = Child("준형", 26)
    child.greet()
}

// 출력
// Parent class init block. 이름: 준형
// Child class init block. 나이: 26
// From Child. 이름:26

위의 코드는 `Child` 클래스의 객체를 생성하고 greet() 메서드를 실행한 코드입니다.  `Child` 클래스의 객체를 생성하면 자동으로 `Parent` 클래스의 객체가 먼저 생성됩니다. 그리고 나서 `Child` 클래스의 객체가 생성됩니다. 

그 후 `Child` 클래스의 greet() 메서드를 호출하면 `Child` 클래스의 재생성된 버전이 호출됩니다.

 

 

 

보조 생성자로 연결하여 상속

Parent 클래스와 Child 클래스 간에 생성자를 보조 생성자로 연결하여 상속 관계를 나타냅니다. Parent 클래스는 주 생성자와 보조 생성자를 가지며, Child 클래스는 Parent 클래스를 상속받고 보조 생성자를 사용하여 연결합니다.

Parent 클래스의 보조 생성자는 `this(name)` 을 통해 주 생성자를 호출하며, Child 클래스의 보조 생성자는 `super(name, age)` 를 통해 Parent 클래스의 보조 생성자를 호출합니다. 이를 통해 생성자 간의 연결과 상속 관계를 확인할 수 있습니다.

 

 

다음

https://everyday-develop-myself.tistory.com/269

 

 

 

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!