[내일하이] Kotlin 웹 마스터:Java에서 함수형까지, 실무형 웹 마이그레이션 완전 정복#04 인터페이스, sealed class, 생성자, 상속

부트챌린지/내일하이:Kotlin 웹마스터
2026.07.23

 

오늘은 Java에서 주로 사용하던 인터페이스나 생성자, 상속과 처음 들어보는 sealed class에 대해서 배웠다. 혹시 Java를 전혀 모르는 사람이라면 아래 글이 도움이 될 것이다. 강의도 그렇고 듣는 나도 Java의 개념이나 실무 사용에 대해서는 좀 익숙해져 있기 때문에, 이에 맞춰 글을 쓰고 있다.

 

[Java 풀스택 개발자] Java의 용어와 개념: 낯선 Java를 친숙하게 만들어보자!

 

[Java 풀스택 개발자] Java의 용어와 개념: 낯선 Java를 친숙하게 만들어보자!

0. 들어가기에 앞서본래는 특별편인 React 실전으로 먼저 만나려고 했다. 그러나 생각보다 만들려던 것의 규모가 커지면서 다음 기회에 포스팅하게 되었다. 따라서 이번 주는 한국의 백엔드 중심

bbbbabbbababababa.tistory.com

 

Interface인터페이스 

기본적인 동작은 Java의 인터페이스와 일치하다. 다만 몇 가지 차이점이 있는데, 대표적으로 구현 키워드의 차이이다. Java는 상속은 `extends`, 구현은 `implements`를 사용했지만, Kotlin에서는 `:`으로 통일한다. 그리고 메서드 오버라이드 시 Java는 시그니처만 일치하면 오버라이딩 키워드(`@Override`)를 적지 않아도 되었지만, Kotlin은 `override`라는 키워드를 적어야 한다. 또한 인터페이스에 프로퍼티(변수)를 선언할 수 있는 것도 Kotlin의 차이점이다. 다만 코틀린 인터페이스는 상태(State)를 가질 수 없기 때문에, `set()`의 구현은 불가능하고, `get()`이나 추상함수, 구현함수 정도만이 가능하다.

 

따라서 Kotlin 인터페이스는 이런 형태로 나타낼 수 있다.

 

interface MyInterface {
    // 1. 추상 프로퍼티: 구현 클래스에서 반드시 override하여 구현해야 함
    val abstractProp: String 

    // 2. val 프로퍼티 + get() 구현: 계산된 커스텀 게터 제공 가능 (field 사용 불가)
    val getterProp: String
        get() = "Hello"

    // 3. var 프로퍼티 선언: get/set의 규격만 정의 (인터페이스 내 구현은 불가)
    var abstractVar: Int 
}

class MyClass : MyInterface {
    
    // 1. 추상 프로퍼티 구현 (반드시 override 필수)
    override val abstractProp: String = "구현체에서 결정한 문자열"

    // 2. val 프로퍼티 + get() 구현 (인터페이스에 이미 get()이 있으므로 override가 선택 사항임)
    // 바꾸고 싶다면 아래처럼 오버라이드할 수 있고, 안 적으면 인터페이스의 "Hello"를 그대로 쓴다.
    override val getterProp: String
        get() = "구현체에서 새로 정의한 가차없는 게터"

    // 3. var 프로퍼티 구현 (인터페이스에 규격만 있었으므로, 실제 field(메모리)를 가지도록 구현)
    override var abstractVar: Int = 0
}

 

그리고 다중 인터페이스 구현이 Java와 마찬가지로 가능한데, 이름이 충돌할 경우 메서드 오버라이드가 필요하다. `super<interface>` 구문으로 명시적으로 상위 인터페이스 구현을 호출해야한다.

 

interface InterfaceA {
    fun printMessage() {
        println("A에서 출력")
    }
}

interface InterfaceB {
    fun printMessage() {
        println("B에서 출력")
    }
}

// 다중 인터페이스 구현 (Java의 implements와 동일)
class MultiClass : InterfaceA, InterfaceB {

    // 이름이 충돌하므로 컴파일러에서 에러가 난다. 반드시 override로 해결해야 함
    override fun printMessage() {
        // super<인터페이스명> 구문으로 원하는 상위 구현을 명시적으로 호출
        super<InterfaceA>.printMessage() 
        super<InterfaceB>.printMessage() 
    }
}

 

 

상속

Kotlin에서는 모든 값이 `final`로 처리되는 걸 과거 포스트에서 언급한 적 있었다. Java를 잘 아는 사람이라면 `final` 키워드가 붙을 경우 상속과 오버라이딩이 불가능하다. 이에 따라 Kotlin은 상속을 거의 사용하지 않는다. 그러므로 상속이 기본적으로 열려있는 Java와 달리 불가능한 것이 원칙이며, 만약에 상속을 원한다면 `open` 키워드를 사용해야 한다.

 

Kotlin이 상속을 사용하지 않는 다른 이유에서는 취약한 기반 클래스 문제가 있었다. 상속은 알다시피 A 클래스를 B 클래스가 이어받고 이를 오버라이딩해서 사용하게 되는데, A 클래스가 변경된다면 B 클래스에 영향을 준다. 문제는 상속을 받은 게 B 클래스만이 아니라 C, D, E... 같은 여러 클래스일 경우이다. 협업을 하게 될 경우, 부모 클래스를 설계한 사람과 자식 클래스를 설계하는 사람이 다를 수도 있다. 그럴 때 부모 클래스에서 수정이 일어나면, 설계한 사람은 어느 자식 클래스가 이에 영향을 받아 심각한 에러를 일으키는지 알 수 없고, 자식 클래스 설계를 한 사람은 부모의 어느 부분 수정으로 문제가 생겼는지 알 수가 없게 된다.

 

이에 따라 Kotlin은 상속을 사용하기 보단 인터페이스Interface를 구현하는 방식을 주로 채택했다. 실제로 Java 실무에서도 상속 자체의 사용보다는 인터페이스의 구현을 더 자주 채택하기 때문에 매우 실용적인 선택이라고 할 수 있겠다.

 

 

Sealed Class

Sealed Class는 Java를 하던 사람이라면 생소한 개념이다. 간단히 말하면 상속받을 수 있는 자식 클래스의 종류를 컴파일러가 완벽히 제한한 클래스라고 할 수 있다. 말 그대로 sealed(봉인된) 클래스라는 뜻이다. 구조상으로 비교하기 쉬운 건 바로 `Enum`이다. 클래스 계층의 Enum 버전이라고 생각하면 편한데, 클래스의 장점과 Enum의 장점을 합쳤다고 보는 것이 좋다.

 

Sealed Class의 핵심적인 특징으론, 외부에서 sealed Class를 상속할 수 없고, `when` 표현식과 주로 사용되며, 요소들을 관리하기 좋고 확장도 편하다. 다만 `when`을 사용할 때는 가급적 `else`를 사용하지 않고 모든 경우에 대응하면 좋다. 또한 Sealed Class는 상태관리나 결과 타입 모델링에 유용하다고 할 수 있다.

 

길게 글을 쓰기보다는 간단한 예제로 나타내겠다. 아래 코드는 내가 프로젝트 당시 사용한 `Enum`과 `switch-case` 식이다.

 

public enum MetaTagType {
    // 수동 추가 메타태그
    CATEGORY, SEASONAL, PURPOSE, MERCHANDISING, OCCUPATION, AGE_PREFERENCE,

    //자동 추가 메타태그
    POPULARITY, RELEASE_OR_UPDATE, PRICE_RANGE, TIME_OF_DAY
}

 

    private String capExceededMessage(MetaTagType type, int cap) {
        return switch (type) {
            case SEASONAL      -> "계절 태그는 최대 " + cap + "개까지 선택할 수 있습니다.";
            case PURPOSE       -> "목적 태그는 최대 " + cap + "개까지 선택할 수 있습니다.";
            case MERCHANDISING -> "기획 의도 태그는 최대 " + cap + "개까지 선택할 수 있습니다.";
            case OCCUPATION    -> "직종 태그는 최대 " + cap + "개까지 선택할 수 있습니다.";
            case AGE_PREFERENCE -> "나이대는 최대 " + cap + "개까지 선택할 수 있습니다.";
            default            -> "태그는 최대 " + cap + "개까지 선택할 수 있습니다.";
        };
    }

 

Java에서는 ENUM과 ENUM에 따른 제약을 따로 지정해야 하는데, Sealed Class에서는 다음과 같이 표현할 수 있다.

 

// 1. 태그 타입과 각각의 제약 조건(cap)을 데이터와 함께 묶어서 정의
sealed class MetaTagType(val cap: Int) {
    
    // 수동 추가 메타태그 (각각 커스텀한 cap 값을 쥐여줄 수 있음)
    class Category(cap: Int) : MetaTagType(cap)
    class Seasonal(cap: Int) : MetaTagType(cap)
    class Purpose(cap: Int) : MetaTagType(cap)
    class Merchandising(cap: Int) : MetaTagType(cap)
    class Occupation(cap: Int) : MetaTagType(cap)
    class AgePreference(cap: Int) : MetaTagType(cap)

    // 자동 추가 메타태그 (만약 얘네는 cap이 고정(예: 0)이거나 필요 없다면 싱글톤 object로 처리)
    object Popularity : MetaTagType(0)
    object ReleaseOrUpdate : MetaTagType(0)
    object PriceRange : MetaTagType(0)
    object TimeOfDay : MetaTagType(0)
}

// 2. 메시지를 출력하는 함수
private fun capExceededMessage(type: MetaTagType): String {
    // else가 없으므로 새로운 태그가 추가되면 무조건 컴파일 에러를 내서 실수를 막아줍니다.
    return when (type) {
        is MetaTagType.Category -> "카테고리 태그는 최대 ${type.cap}개까지 선택할 수 있습니다."
        is MetaTagType.Seasonal -> "계절 태그는 최대 ${type.cap}개까지 선택할 수 있습니다."
        is MetaTagType.Purpose -> "목적 태그는 최대 ${type.cap}개까지 선택할 수 있습니다."
        is MetaTagType.Merchandising -> "기획 의도 태그는 최대 ${type.cap}개까지 선택할 수 있습니다."
        is MetaTagType.Occupation -> "직종 태그는 최대 ${type.cap}개까지 선택할 수 있습니다."
        is MetaTagType.AgePreference -> "나이대는 최대 ${type.cap}개까지 선택할 수 있습니다."
        
        // 자동 추가 태그들은 일괄 처리하고 싶다면, 이때는 묶어서 처리 가능
        MetaTagType.Popularity, 
        MetaTagType.ReleaseOrUpdate, 
        MetaTagType.PriceRange, 
        MetaTagType.TimeOfDay -> "자동 생성 태그는 선택할 수 없습니다."
    }
}

 

이렇게 하면 안전성이 올라가고, 그룹화되기 때문에 코드의 의도가 명확해진다. 무엇보다 `ENUM`이라는 파일을 따로 생성하지 않아도 된다는 점이 큰 장점이라고 할 수 있다.

 

생성자

생성자의 경우 주 생성자에 대해서는 지난 번에 설명했었다.

 

[내일하이] Kotlin 웹 마스터:Java에서 함수형까지, 실무형 웹 마이그레이션 완전 정복#02 함수, 클래스, 인스턴스

 

[내일하이] Kotlin 웹 마스터:Java에서 함수형까지, 실무형 웹 마이그레이션 완전 정복#02 함수, 클래

오늘은 kotlin의 함수와 클래스, 인스턴스에 대해서 배웠다. 함수함수는 `fun` 키워드를 통해서 만들 수 있다. `fun 함수명(매개변수){}` 형태로 만들 수 있으며, Java와 달리 오버로딩 없이도 함수 선

bbbbabbbababababa.tistory.com

 

이러한 주 생성자의 축약 과정에 대해서 설명하면 이런 형태가 된다.

 

class User constructor(_userName: String) {
    val userName: String

    init {
        userName = _userName
    }
}

 

이런 식으로 `init` 키워드를 붙여 초기화를 하고, 매개변수 이름 앞에 `_`를 붙여 프로퍼티와 구분했었다. 그러다가 한 차례 더 간소화되었다.

 

class User(_userName: String) {
    val userName: String = _userName
}

 

그래서 최종 간소화된, 현재 자주 사용하는 주 생성자는 이렇게 된다.

 

class User(val userName: String) {}

 

중요한건 이 마지막 축약에서 `val`이나 `var` 키워드를 붙여야 프로퍼티로 취급하여 매개변수와 구분이 된다는 점이다. 주 생성자는 이렇게만 하면 간단하게 종료된다. 부생성자 또한 지난 번에 다룬 적이 있는데, 다시 한번 정리하면 다음과 같다.

 

class User(val name: String) { // 1. 주 생성자 (이름 필!)

    var age: Int = 0

    // 2. 부생성자 (이름과 나이를 한 번에 받고 싶을 때 추가)
    constructor(name: String, age: Int) : this(name) { 
        this.age = age
    }
}

 

부생성자의 생성을 위해서는 ① constructor 키워드로 본문 내에 선언 → ② this() 같은 클래스의 다른 생성자 호출 → ③ super() 부모 클래스 생성자 호출의 과정을 거쳐야 한다. 물론 부생성자의 경우는 자주 사용되지는 않기 때문에(방식을 보다시피 번거롭다), 대부분 주 생성자에 매개변수 기본값을 넣어 해결한다.

 


오늘의 수업은 Java의 핵심 개념들을 Kotlin과 비교를 하며 마이그레이션 하는 과정이다. 강사님께서 Java의 불편한 점을 토로하시면서 이야기해서 더 재미있게 다가왔다.

«   2026/07   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31