Developing Myself Everyday

 

이 게시글은 아래의 게시글와 이어집니다.

 

[Kotlin] 클래스 (1) - week 5

클래스(Class)란? 클래스는 객체지향에서 가장 중요한 도구입니다. 클래스는 유사한 특성과 동작을 가진 객체들을 만들기 위한 템플릿 역할을 합니다. 클래스는 객체의 속성(Attribute)과 메서드(meth

everyday-develop-myself.tistory.com


 

 

 

특수한 기능을 수행하는 클래스


내포 클래스(Nested Class)

내포 클래스는 내부의 클래스가 외부의 클래스와 연결되지 않는 정적 클래스입니다. 외부 클래스의 멤버에 접근할 수 없다는 특징이 있습니다.

class Outer {
    private val outerProperty: Int = 10

    class Nested {
        fun nestedMethod() {
            // outerProperty에 접근할 수 없음
            println("Nested method")
        }
    }
}

fun main() {
    val nested = Outer.Nested()
    nested.nestedMethod()
}

// 출력: Nested method

외부 클래스의 인스턴스를 생성하지 않고도 사용할 수 있습니다. 

 

 

 

이너 클래스(Inner Class)

외부 클래스의 인스턴스와 연결되는 클래스입니다. 클래스 내부에 클래스를 정의할 때 `inner` 예약어를 붙여서 이너 클래스를 정의할 수 있습니다. 

class Outer {
    private val outerProperty: Int = 10

    inner class Inner {
        fun innerMethod() {
            println("Inner method: $outerProperty")
        }
    }
}

fun main() {
    val outer = Outer()
    val inner = outer.Inner()
    inner.innerMethod()
}

// 출력: Inner method: 10

이너 클래스의 인스턴스를 생성하려면 외부 클래스의 인스턴스를 생성해야 합니다. 외부 클래스의 상태를 공유할 수 있습니다.

 

이너 클래스는 외부 클래스의 상태를 공유하고자 할 떄 유용하며, 내포 클래스는 외부 클래스와 분리된 기능을 정의하고자 할 때 유용합니다.

 

 

 

지역 클래스(Local Class)

지역 클래스는 특정 범위 내에서만 유효한 클래스로, 메서나 블록 내에 정의되어 해당 영역 내에서만 사용할 수 있습니다. 지역 클래스는 해당 메서드나 블록 내에서 필요한 보조 클래스를 정의하거나 캡슐화할 때 사용합니다.

fun outerFunction() {
    val outerProperty: Int = 10

    class LocalClass {
        fun localMethod() {
            println("Local method: $outerProperty") // 외부 변수에 접근 가능
        }
    }

    val localInstance = LocalClass()
    localInstance.localMethod()
}

fun main() {
    outerFunction()
}

// 출력: Local method: 10

 

 

 

지역 클래스의 활용

다른 객체를 생성하는 팩토리(Factory) 함수를 만들 경우에 지역 클래스를 활용할 수 있습니다. 

팩토리(Factory) 패턴
팩토리 패턴은 객체 생성을  추상화하고 관리하기 위해 사용되는 디자인 패턴 중 하나입니다. 일반적으로 팩토리 패턴 추상화된 인터페이스를 정의하고, 이를 구현하는 여러개의 팩토리 클래스를 만들어 사용합니다. 팩토리 인터페이스를 통해 객체를 생성하고 반환받을 수 있습니다. 팩토리 클래스는 객체 생성에 대한 로직을 구현하고, 실제 객체의 인스턴스를 생성하여 반환합니다.

 

팩토리를 간단하게 함수에 구현한 예시를 보겠습니다.

interface Shape {
    fun draw()
}

fun createShape(): Shape {
    class Circle : Shape {
        override fun draw() {
            println("Drawing a circle")
        }
    }
    return Circle()
}

fun main() {
    val circle = createShape()
    circle.draw()
}

이런 식으로 팩토리 메서드 내에서 지역 클래스를 활용하면 팩토리에서만 사용되는 클래스를 외부에 노출시키지 않고도 객체 생성과 관련된 로직을 캡슐화할 수 있습니다.

 

다만 사실 실제로 팩토리를 구현할 때 지역 클래스를 많이 활용하진 않습니다. 다양한 방식으로 팩토리를 구현할 수 있으며, 적합한 방법을 선택하면 됩니다.

 

 

 

메서드 내부에서 외부 변수 참조

지역 클래스의 예시의 일부분을 다시 가져와 보겠습니다.

val outerProperty: Int = 10

class LocalClass {
    fun localMethod() {
        println("Local method: $outerProperty") // 외부 변수에 접근 가능
    }
}

위와 같이 메서드에서 외부 변수에 접근할 수 있습니다.

 

 

 

함수와 메서드의 변수 탐색

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

fun outerFunction() {
    val property: Int = 10

    class LocalClass {
        val property: Int = 20

        fun localMethod() {
            println("method: $property")
        }
    }

    val localInstance = LocalClass()
    localInstance.localMethod()
}

fun main() {
    outerFunction()
}

위의 코드에는 클래스의 안과 밖에 같은 이름의 변수가 정의되어 있습니다. 일반적으로는 클래스 안에 있는 변수의 값을 가져온다고 생각할 수 있지만, 이는 틀립니다. 

 

일반적으로 변수를 참조할 때, 해당 메서드의 스코프에서 가장 가까운 변수를 참조합니다. 따라서 `localMethod()` 가 실행될 때 출력되는 값은 `LocalClass``property` 값인 20이 출력되는 것이 맞습니다. 

 

그러나 `main()` 함수에서 `outerFunction()`을 호출하게 되면, `outerFunction()`내에서 선언된 `property` 변수를 참조합니다. 따라서 위의 코드를 실행하면 `outerFunction()` 함수의 `property` 변수의 값인 10을 출력하게 됩니다.

 

 

 

 

object


코틀린에서 `object`는 다양한 의미로 사용되는 키워드입니다. `object`는 `익명 객체`, `Singleton 객체`, `companion object`로 주로 사용됩니다.

 

 

 

익명 객체

 `object` 키워드를 사용하여 클래스를 정의하지 않고 익명 객체를 생성할 수 있습니다. 익명 객체를 생성하면 새로운 클래스를 정의하지 않고도 인터페이스나 추상 클래스의 메서드를 구현할 수 있습니다.

 

예를 들어, `Shape` 인터페이스를 구현하는 익명 객체를 생성해보겠습니다:

interface Shape {
    fun area(): Double
}

fun printArea(shape: Shape) {
    println(shape.area())
}

fun main() {
    val rectangle = object : Shape {
        override fun area(): Double {
            return 5.0 * 3.0
        }
    }

    printArea(rectangle) // 출력: 15.0
    
//    바로 사용
//    printArea(object : Shape {
//        override fun area(): Double {
//            return 5.0 * 3.0
//        }
//    }) // 출력: 15.0
}

rectangle `Shape` 인터페이스를 구현한 익명 객체입니다. `printArea()` 함수의 매개변수로 이 익명 객체를 전달할 수 있습니다.

 

 

 

Singleton 객체

싱글톤(Singleton)은 디자인 패턴 중 하나로, 클래스의 인스턴스를 하나만 생성하도록 보장하는 패턴입니다. 이를 통해 어플리케이션 전체에서 하나의 공유된 인스턴스를 사용하여 데이터나 기능을 관리하게 됩니다.

 

`object` 키워드를 사용하여 싱글톤을 구현할 수 있습니다. `object` 키워드를 사용하면 `object` 정의를 처음으로 사용할 때 단일 인스턴스의 객체가 생성되며, 이 객체는 해당 `object`의 유일한 인스턴스입니다.

 

예시를 통해 싱글톤을 알아보겠습니다.

object Logger {
    fun log(message: String) {
        println("[INFO] $message")
    }
}

 

위의 코드에서 `Logger`라는 이름의 싱글톤 객체를 생성하고 있습니다. 이 객체는 `log` 매서드를 가지고 있어 로그 메시지를 출력합니다.

 

fun main() {
    Logger.log("Application started")
    Logger.log("Processing data")
    Logger.log("Application finished")
}

// 출력
// [INFO] Application started
// [INFO] Processing data
// [INFO] Application finished

 `Logger` 싱글톤 객체는 단일 인스턴스이므로 어디서든 동일한 인스턴스를 사용하여 로그 메시지를 출력합니다.

 

object도 클래스와 마찬가지로 다른 클래스를 상속해서 구현하거나 인터페이스를 구현할 수 있습니다.

 

 

 

companion object

클래스 내부에서의 companion object는 실제 클래스와 상관없이 작동하는 객체를 말했습니다. 

class OuterClass {
    companion object {
        fun sayHelloFromCompanion() {
            println("Hello from Companion Object!")
        }
    }
}

fun main() {
    OuterClass.sayHelloFromCompanion() // 출력: Hello from Companion Object!
}

 

클래스와 companion object 하나처럼 움직이도록 구성되었습니다. 그래서 클래스에 companion object를 정의하면 클래스 이름으로 companion object 안의 메서드를 처리할 수 있습니다.

 

 

 

companion object를 이용한 팩토리 메소드 패턴

이전의 게시글에서 클래스의 주 생성자에 대한 접근을 제한했을 때 클래스 내부의 메서드에 접근할 때 `companion object` 를 사용한다고 했습니다. 이를 활용하여 아래와 같은 팩토리 메서드 패턴을 구현할 수 있습니다.

class User private constructor(val username: String) {
    companion object {
        fun create(username: String): User {
            return User(username)
        }
    }
}

fun main() {
    val user = User.create("준형")
    println("Username: ${user.username}") // 출력: Username: 준형
}

 

 

 

 

확장(extension)


코틀린에서는 클래스의 기능을 추가할 때 클래스를 직접 수정하지 않고 기능을 추가하는 방법을 제공합니다. 이를 확장이라고 합니다.

 

속성(프로퍼티, property)

프로퍼티는 클래스, 구조체 및 인터페이스의 이름이 붙은 멤버입니다. 클래스나 구조체 내의 멤버 변수나 메서드는 필드(Field)라고 불립니다. 프로퍼티는 필드의 확장으로, 동일한 구문을 사용하여 접근됩니다. 이들은 접근자를 통해 개인 필드의 값이 읽기, 쓰기 또는 조작될 수 있습니다.

 

프로퍼티는 저장 위치를 지정하지 않습니다. 대신, 값이 읽기, 쓰기 또는 계산되는 접근자(accessor)를 갖습니다.

 

코틀린의 프로퍼티는 Backing field를 제공합니다. 아래의 예시를 보겠습니다.

class Test {
    var size = 0
    var isEmpty = false
}

 

`size`, `isEmpty` 프로퍼티를 가진 `Test` 클래스를 디컴파일 한 java 클래스를 확인해 보겠습니다.

public final class Test {
   private int size;
   private boolean isEmpty;

   public final int getSize() {
      return this.size;
   }

   public final void setSize(int var1) {
      this.size = var1;
   }

   public final boolean isEmpty() {
      return this.isEmpty;
   }

   public final void setEmpty(boolean var1) {
      this.isEmpty = var1;
   }
}

 

getter/setter 메서드가 제공된 것을 확인할 수 있습니다. 또한 코틀린에선 프로퍼티의 접근자 내에서 사용되며, 값을 저장하거나 가져오는 역할을 하는 `size`, `isEmpty` 필드가 정의된 것을 확인할 수 있습니다.

 

`size`, `isEmpty` 필드를  Backing field라고 부르는 것입니다.

 

사용자는  Backing field에 직접적으로 접근하거나 조작할 수 없고 코틀린은 접근자를 통해 자동으로 처리합니다.

 

이것이 변수와 프로퍼티의 가장 큰 차이점입니다. 변수는 단순히 값을 저장하고 변경할 수 있는 메모리 공간을 나타내는 반면, 프로퍼티은 클래나 객체 내부에서 값의 저장과 접근을 더 효율적으로 관리할 수 있게 합니다.

 

 

 

최상위 프로퍼티

만약 전역 변수처럼 동작하는 최상위 수준의 프로퍼티를 원한다면, 이는 최상위 프로퍼티를 사용하여 구현할 수 있습니다.

val person : Int = 0
    get() : Int {
        return field * 2
    }

var man : Int = 0
    get() = field
    set(value) {
        field = value
    }

fun main() {
    println(person) // 출력: 0
    man = 100
    println(man) // 출력: 100
}

 

 

 

프로퍼티 확장

프로퍼티 확장은 기존 클래스에 새로운 프로퍼티를 추가하는 기능입니다. 아래의 예를 보겠습니다.

class Rectangle(val width: Double, val height: Double)

// 프로퍼티 확장으로 넓이 계산
val Rectangle.area: Double
    get() = width * height

fun main() {
    val rectangle = Rectangle(5.0, 10.0)

    val area = rectangle.area
    println("넓이: $area") // 출력: 넓이: 50.0
}

 

넓이를 계산하는 `area` 프로퍼티를 기존 클래스의 내부 구조를 변경하지 않고 추가했습니다.

 

확장할 때 중요한 점은 어떤 클래스를 확장할 것인지를 지정해야 한다는 것입니다. 이런 방식은 `클래스명.속성이름` 의 형식으로 이뤄집니다.

 

objectcompanion object에서도 확장 프로퍼티를 추가할 수 있습니다.

 

 

 

확장 함수


프로퍼티 확장으로 기존 클래스의 프로퍼티를 추가하는 법을 배웠습니다. 메서드 또한 프로퍼티처럼 클래스나 object에 추가할 수 있습니다.

 

 

 

Any 클래스를 대상으로 하는 확장 함수

Any 클래스를 대상으로 하는 확장 함수를 정의할 수도 있습니다. 이렇게 정의하면 모든 클래스에 대해 새로운 동작을 추가할 수 있습니다.

// Any 클래스를 대상으로 하는 확장 함수 정의
fun Any.customToString(): String {
    return "Custom toString: $this"
}

fun main() {
    val number = 42
    val text = "Hello, world!"

    println(number.customToString()) // 출력: Custom toString: 42
    println(text.customToString())   // 출력: Custom toString: Hello, world!
}

 

 

 

사용자 클래스를 대상으로 하는 확장 함수

사용자 클래스에도 확장 함수를 지정할 수 있습니다. 아래의 예시를 보겠습니다.

class Person(val name: String, val age: Int)

fun Person.isAdult(): String {
    return if (age >= 18)
        "${this.name}은 ${this.age}으로 어른입니다."
    else
        "${this.name}은 ${this.age}으로 어른이 아닙니다."

}

fun main() {
    val person1 = Person("준형", 26)
    val person2 = Person("지섭", 15)

    println(person1.isAdult()) // 출력: 준형은 26으로 어른입니다.
    println(person2.isAdult()) // 출력: 지섭은 15으로 어른이 아닙니다.
}

 

위 코드에서 `Person` 클래스를 대상으로 하는 확장 함수 `isAdult()`를 정의하고 있습니다. 이 확장 함수는 해당 객체를 기반으로 성인 여부를 판별합니다.

 

 

 

널러블 확장 함수

클래스에 대해 널이 가능한 확장 함수를 정의하여 추가적인 기능을 부여할 수 있습니다. 아래의 예시를 보겠습니다.

class Person(val name: String, val age: Int)

fun Person?.isAdultOrNull(): String? {
    if (this == null) return null

    return if (age >= 18)
        "${this.name}은 ${this.age}으로 어른입니다."
    else
        "${this.name}은 ${this.age}으로 어른이 아닙니다."
}

fun main() {
    val person1 = Person("준형", 26)
    val person2 = null

    println(person1.isAdultOrNull()) // 출력: 준형은 26으로 어른입니다.
    println(person2.isAdultOrNull()) // 출력: null
}

 

위 코드에서 `Person` 클래스를 대상으로 하는 확장 함수 `isAdultOrNull()`를 정의하고 있습니다. 이 확장 함수는 해당 객체를 기반으로 성인 여부를 판별하고 널이 전달 되었을 때의 처리를 추가적으로 수행하고 있습니다.

 

 

object companion object에서도 확장 함수를 추가할 수 있습니다.

 

 

 

리시버 객체(Receiver Object) 처리 방식

리시버 객체는 확장 함수나 확장 프로퍼티에서 사용되는 개념으로, 해당 함수 또는 프로퍼티가 적용되는 대상 객체를 가리킵니다.

 

이때 확장 함수나 확장 프로퍼티 내에서 리시버 객체인 `this`를 받아서 객체 멤버를 사용할 수 있습니다.

 

다만  `this`는 확장 함수내에서 object 표현식을 사용하는 경우와 같은 상황에 정확히 어떤 객체 멤버를 말하는 것인지 알 수 없기 때문에 문제가 발생할 수 있습니다.

fun String.createObject() {
    val otherObject = object {
        fun printValues() {
            println("this@createObject: ${this@createObject}")
            println("this: $this")
        }
    }

    otherObject.printValues()
}

fun main() {
    val text = "Hello"
    text.createObject()
}

// 출력
// this@createObject: Hello
// this: ProgrammersKt$createObject$otherObject$1@3941a79c

 

그렇게 때문에 `this@확장 함수명` 을 사용해서 이를 구분할 수 있습니다.

profile

Developing Myself Everyday

@배준형

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