'sealed'에 해당되는 글 1건

  1. 2020.09.17 :: Kotlin - 코틀린의 클래스, 객체, 인터페이스 1
프로그래밍언어/Kotlin 2020. 9. 17. 10:09

 

이전까지 간단하게 코틀린에 대한 간략한 문법들을 다루어봤는데, 이번 포스팅은 코틀린의 클래스, 객체, 인터페이스에 대해 다루어본다.

 

인터페이스

자바의 인터페이스와 크게 다르지 않다.

 

interface SampleInterface1 {
    val property: String
    fun method1()
    fun method3() = println("method2")
}

interface SampleInterface2 {
    fun method2()
    fun method3() = println("method2")
}

class SampleImpl(): SampleInterface1, SampleInterface2 {
    override val property: String
        get() = TODO("Not yet implemented")

    override fun method1() {
        TODO("Not yet implemented")
    }

    override fun method3() {
        super<SampleInterface2>.method3()
    }

    override fun method2() {
        TODO("Not yet implemented")
    }
}

 

자바처럼 여러 인터페이스를 한 클래스에서 구현할 수도 있다. 또한 자바의 default 메서드처럼 코틀린에서는 그냥 함수 구현체를 정의하면 그게 default 함수가 된다. 하지만 유의할점은, 구현하고 있는 인터페이스 두개에서 같은 이름의 함수가 있다면, 구현하고 있는 하위 함수에서 오버라이드 해주어야한다.(혹은 특정 상위 인터페이스의 구현을 사용한다고 명시할 수도 있다.) 

 

자바는 @Override 어노테이션을 사용하지만 코틀린은 override 키워드를 사용한다. 마지막으로 자바와는 다르게 코틀린은 인터페이스에서 프로퍼티를 선언할 수 있다. 만약 인터페이스에 프로퍼티가 선언되어 있다면, 구현체에서 Getter를 오버라이드 해주면 된다.

 

코틀린의 열린 클래스(클래스상속)

자바에서는 기본적으로 클래스를 상속하면 다른 클래스에서 상속을 받을 수 있는 구조이다(public, not final) 하지만, 코틀린은 기본적으로 클래스가 모두 final 이기때문에 상속을 받을 수 없다. 하지만, 일반 클래스도 상속을 허용하기 위해 열린 클래스를 정의할 수 있다.

 

open class SampleOpen(val value: String): SampleInterface1 {
    override val property: String
        get() = TODO("Not yet implemented")

    fun disable() {}
    open fun animate() {}
    override fun method1() {
        TODO("Not yet implemented")
    }
}

class SampleOpenImpl(val key: String, value: String): SampleOpen(value) {
    override fun animate() {
        super.animate()
    }
}

 

open이라는 키워드를 붙여 상속이 가능한 형태의 클래스를 만들 수 있다. 하지만 자바와는 조금 문법이 다르게 하위 클래스에서 상위 클래스의 생성자 인자만큼 괄호안에 프로퍼티를 넣어줘야하는데, 그 프로퍼티를 생성자 시점에 넘겨줘야한다.(자바와 비슷) 하지만 자바와는 다르게 하위클래스의 생성자 프로퍼티 명을 그대로 상위 클래스 생성자 아규먼트로 넣어주면 된다. 또한 open class는 구현체 함수도 들어갈 수 있고, 상속을 강제하기 위해 open 키워드를 붙인 함수를 정의할 수 있다.

 

확장함수의 접근제한자

코틀린에서는 확장함수라는 아주 편한 문법이 존재한다. 하지만 하나 유의해야할 점이 확장함수를 정의할때 수신 객체 타입의 접근제한자의 레벨과 같거나 낮아야한다. 그말은 코틀린의 internal(같은 모듈에서만 사용가능)으로 수신객체의 접근제한자로 정의해놓았다면, 확장 함수는 반드시 internal 제한과 같거나 낮아야한다.(public으로는 internal 수신 객체의 확장함수를 정의할 수 없다.)

 

internal class SampleInternal {
    
}

internal fun SampleInternal.expendFunction() {} -> ok
fun SampleInternal.expendFunction() {} -> 컴파일 에러

 

sealed 클래스

아래와 같은 코드가 있다고 가정하자.

 

interface InterfaceExam {}
class InterfaceImpl1: InterfaceExam
class InterfaceImpl2: InterfaceExam
class InterfaceImpl3: InterfaceExam

fun whenExam(obj: InterfaceExam) = when (obj) {
    is InterfaceImpl1 -> println("interfaceImpl1")
    is InterfaceImpl2 -> println("interfaceImpl2")
    is InterfaceImpl3 -> println("interfaceImpl3")
    else -> println("els")
}

 

when 식에서 인터페이스 구현체의 타입을 분기하는 로직인데, 실제로 모든 구현체를 다 명시했음에도 else 구문이 들어가야한다. 하지만 코틀린에서 sealed 타입을 구현함으로써 else문을 명시하지 않아도된다.(sealed로 상위 타입을 명시함으로써 when 식에 모든 타입이 들어왔으니 else 구문이 오지 않을 것이라라고 강제할 수 있어 else 구문이 필요없다.)

 

sealed class SampleSealed {
    open fun method() {}
}

class SampleSealedImpl1: SampleSealed() {
    override fun method() {
        super.method()
    }
}
class SampleSealedImpl2: SampleSealed() {
    override fun method() {
        super.method()
    }
}

class SampleSealedImpl3: SampleSealed() {
    override fun method() {
        super.method()
    }
}

 

클래스 생성자

코틀린에서 주 생성자는 자바보다 간결하게 정의가능하다.

 

class User1(val name: String)

#java
public final class User1 {
	private final String name;
    
    public User1(String name) {
    	this.name = name
    }
}

 

위 정의는 모든 필드가 final이고, 그 필드를 모든 필드를 인자로 받는 생성자로 초기화하는 것과 같은 것이다. 위와 같은 생성자는 코틀린에서 펼친 표현으로는 아래와 같다.

 

class User2 constructor(name: String){
    val name: String
    init {
        this.name = name
    }
}

class User3(name: String){
    val name: String = name
}

 

복잡한 초기화 로직을 적용해야하는 클래스는 주 생성자말고 부 생성자를 여러개 정의할 수 있다.

 

class Secretive private constructor() {}

open class View {
    constructor(title: String): this(title, "")
    constructor(title: String, content: String) {}
}

class MyButton: View {
    constructor(title: String): super(title){

    }

    constructor(title: String, content: String): super(title, content) {

    }
}

 

인터페이스 프로퍼티 오버라이드

코틀린의 인터페이스에서는 프로퍼티를 선언할 수 있는데, 그 예로 아래와 같다.

 

interface Book {
    val name: String
}

class EBook1(override val name: String): Book
class EBook2: Book {
    override val name: String
        get() = TODO("Not yet implemented")
}
class EBook3(val category: String): Book {
    override val name = getBookName(category)
    private fun getBookName(category: String) = "ebook"
}

 

데이터 클래스

자바의 레코드 클래스와 비슷하게 코틀린에는 데이터 클래스가 있다. 데이터 클래스는 코틀린에서 필요한 함수를 모두 숨겨서 구현해주고 있다.( equal(), hashCode(), toString() , copy())

 

data class BookDto(val name: String, val price: Int) {}

 

위임클래스

코틀린에서는 delegate pattern을 아주 쉽게 구현할 수 있는 수단이 존재한다. 만약 자바에서 HashMap을 상속하여 커스텀한 로직을 넣은 클래스를 만든다면 여러 불필요한 코드가 많고, 실제 상위 클래스의 메서드에 의존해야한다라는 단점이 있는데, 코틀린은 이를 아주 우하하게 해결해주었다.

 

/**
 * 자바와 달리 데코레이터 패턴을 아주 쉽게 구현할 수 있다.
 * 보통 특정 클래스를 상속할때 상위 클래스의 기능이 변경되면, 하위 클래스의 동작에도 문제가 생기는데
 * 아래처럼 위임 관계를 만들어주면 실제 상위 클래스의 구현에 의존하지 않고, 위임하기때문에 상위클래스 변경에
 * 안전하다.
 */
class DelegatingCollection<T>(
        private val innerList: Collection<T> = ArrayList<T>()
): Collection<T> by innerList {

}

class BookNamePriceMap(
        private val map: Map<String, Int> = hashMapOf()
): Map<String, Int> by map {

}

 

만약 오버라이드해야하는 함수가 있다면 오버라이드하면 되고, 나머지는 "by map"이라는 키워드로 인해 자동으로 상위 클래스의 함수로 위임해준다. 

 

객체의 선언과 생성을 동시에 해주는 Object

코틀린에는 자바와는 다르게 object라는 타입이 존재한다. 간단하게는 싱글톤을 만들어주고, 마치 static한 유틸클래스처럼 이용가능한 타입이다.

 

object Payroll {
    const val companyName = "company"
    init {
    }
}

 

선언과 생성이 동시에 이루어지기 때문에, 상태값을 담은 프로퍼티 선언을 하지 않고 보통은 상수정도의 프로퍼티를 갖는다. object 타입에 함수를 정의하면 Payroll.method() 처럼 정적인 클래스처럼 이용가능하다.

 

동반객체(companion object)

특정 클래스의 생성을 담당할 팩토리 클래스 등을 만들때 아주 좋은 코틀린의 기능이 있다. 또한 마치 확장함수와 같은 기능을 제공하기도 하는데 아래 예제를 살펴보자.

 

class OuterClass private constructor(val name: String) {
    //내부 객체선언이 동반 객체선언이라면, 내부 객체 선언의 private 멤버 혹은 함수 접근이 가능하다.
    private fun method2() = method()
    private fun method3() = value
    
    companion object OuterClassFactory {
        private const val value = ""
        
        fun newOuterClass() = OuterClass("outerName")
        
        private fun method() = println("")
        
        fun companionMethod() = println("")
    }
}

OuterClass.newOuterClass()
OuterClass.companionMethod()

 

자바와는 달리 코틀린의 내부클래스는 외부 클래스를 참조하지 않는다. 또한 companion 키워드를 넣어주면, 바깥 클래스에서 내부 객체의 private한 멤버도 참조가능하다. 그렇다면 그냥 object로 선언된 내부 객체는 어떠할까

 

class OuterClass private constructor(val name: String) {
    private fun method() = println("private method")
    //내부 객체선언이 일반 객체선언이라면, 내부 객체 선언의 private 멤버 혹은 함수 접근이 안된다.
    private fun method2() = OuterClassFactory.method() //컴파일에러
    
    object OuterClassFactory {
        fun newOuterClass() = OuterClass("outerName")
        
        private fun method() = println("")
    }
}

 

일반 object 내부 객체로 정의되어있다면, 외부 클래스에서 내부 객체의 private 멤버에 접근할 수 없다. 이를 잘 활용하면 팩토리 클래스를 따로 분리할 필요없이 클래스 내부에 정의가 가능해지는 것이다. 또한 이와 비슷한 느낌의 로직도 한곳에 다모이게 되어 관리가 쉬워질 것이다.

 

여기까지 코틀린의 클래스, 인터페이스, 객체에 대해 간단히 다루어보았다.

posted by 여성게
: