728x90
반응형
- 코틀린의 클래스와 인터페이스는 자바 클래스, 인터페이스와는 약간 다름
- 예를 들면, 인터페이스에 프로퍼티 선언이 들어갈 수 있음
- 자바와 달리 코틀린 선언은 기본적으로 final이며 public
- 중첩 클래스는 기본적으로 내부 클래스가 아님(코틀린 중첩 클래스에는 외부 클래스에 대한 참조가 없음)
- 코틀린은 짧은 주 생성자 구문으로도 거의 모든 경우를 잘 처리할 수 있을 뿐더러 복잡한 초기화 로직을 수행하는 경우를 대비한 완전한 문법도 존재
- 프로퍼티도 마찬가지이며, 필요한 경우 접근자를 직접 정의하여 사용할 수 있음
- 코틀린 컴파일러는 번잡스러움을 피하기 위해 유용한 메서드를 자동으로 만들어줌
- 클래스를 data로 선언하면 컴파일러가 일부 표준 메소드를 생성
- 또한, 코틀린 언어가 제공하는 위임(delegation)을 사용하면 위임을 처리하기 위한 준비 메서드를 직접 작성할 필요가 없음
✅ 클래스 계층 정의
- 코틀린에서 클래스 계층을 정의하는 방식과 자바의 방식을 비교함
- 코틀린의 가시성과 접근 변경자에 대해 살펴봄
- 코틀린에서 새로 도입한 sealed 변경자에 대해 설명 (sealed의 경우 클래스 상속을 제한)
📌 코틀린 인터페이스
- 코틀린 인터페이스는 자바 8 인터페이스와 비슷함
- 코틀린 인터페이스 안에는 추상 메서드뿐 아니라 구현이 있는 메소드도 정의할 수 있음
- But, 인터페이스에는 아무런 상태(필드)도 들어갈 수 없음
- 코틀린에서 클래스는 class로 정의하지만 인터페이스는 interface를 사용
// 간단한 인터페이스 선언하기
interface Clickable {
fun click()
}
// 단순한 인터페이스 구현하기
class Button : Clickable {
override fun click() = println("I was clicked") // click에 대한 구현 제공
}
>> Button().click() // I was clicked
- 자바에서는 extends와 implements 키워드를 사용하지만, 코틀린에서는 클래스 이름 뒤에 콜론(:)을 붙이고 인터페이스와 클래스 이름을 적는 것으로 클래스 확장과 인터페이스 구현을 모두 처리함
- 자바와 마찬가지로 클래스는 인터페이스를 개수 제한 없이 마음대로 구현할 수 있지만, 클래스는 오직 하나만 확장할 수 있음
- 자바의 @Override 애노테이션과 비슷한 override 변경자는 상위 클래스나 상위 인터페이스에 있는 프로퍼티, 메서드를 오버라이드한다는 표시
- 하지만 자바와 달리 코틀린에서는 override 변경자를 꼭 사용해야 함
- 해당 변경자는 실수로 상위 클래스의 메서드를 오버라이드하는 경우를 방지해줌
- 하지만 자바와 달리 코틀린에서는 override 변경자를 꼭 사용해야 함
- 인터페이스 메서드도 디폴트 구현을 제공할 수 있음
- 메서드 앞에 default를 붙여야 하는 자바와 달리 특별한 키워드 없이 구현 가능
- 메소드 본문을 메소드 시그니처 뒤에 추가하면 됨
// 인터페이스 안에 본문이 있는 메서드 정의하기
interface Clickable {
fun click() // 일반 메서드 선언
fun showOff() = println("I'm clickable!") // 디폴트 구현이 있는 메서드
}
// 이 인터페이스를 구현하는 클래스는 click에 대한 구현을 제공해야 함
// 반면 showOff 메서드의 경우 새로운 동작을 정의할 수도 있고, 정의를 생략해서 디폴트 구현을 사용할 수도 있음
// 동일한 메서드를 구현하는 다른 인터페이스 정의
interface Focusable {
fun setFocus (b: Boolean) = println("I $[if (b) "got" else "lost"} focus.")
fun showOff() = println("I'm focusable!") // 동일한 메서드 구현
}
- 만약, 두 개의 인터페이스를 상속한 상태에서 showOff를 호출하면 어떻게 될까?
- 어느 쪽 showOff 메서드도 선택되지 않음
- 클래스가 구현하는 두 상위 인터페이스에 정의된 showOff 구현을 대체할 오버라이딩 메서드를 직접 제공하지 않는 경우 컴파일러 오류가 발생함
The class 'button' must override public open fun showOff() because it inherits many implementations of it.
- 코틀린 컴파일러는 두 메서드를 아우르는 구현을 하위 클래스에 직접 구현하게 강제함
// 상속한 인터페이스의 메서드 구현 호출하기
class Button: Clickable, Focusable {
override fun click() = println("I was clicked")
override fun showOff() { // 이름과 시그니처가 같은 멤버 메서드에 대해 둘 이상의 디폴트 구현이 있는 경우
// 인터페이스를 구현하는 하위 클래스에서 명시적으로 새로운 구현을 제공해야 함
super<Clickable>.showOff() // 상위 타입의 이름을 꺾쇠 괄호 사이에 넣어서 super를 지정하면
super<Focusable>.showOff() // 어떤 상위 타입의 멤버 메서드를 호출할지 지정할 수 있음
}
}
📌 open, final, abstract 변경자 : 기본적으로 final
- 자바에서는 상속을 금지하기 위해 final로 명시한 경우를 제외하면 모든 클래스를 다른 클래스가 상속할 수 있음
- 이런 경우 편리한 경우도 많지만 문제가 생기는 경우도 많음
- 취약한 기반 클래스(Fragile Base Class)라는 문제는 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우에 생김
- 어떤 클래스가 자신을 상속하는 방법에 대해 정확한 규칙을 제공하지 않는다면 그 클래스의 클라이언트는 원래 의도와 다른 방식으로 메서드를 오버라이드할 위험이 있음
- 모든 하위 클래스를 분석하는 것은 불가능하므로 기반 클래스를 변경하는 경우 하위 클래스의 동작이 예기치 않게 바뀔 수도 있다는 면에서 기반 클래스는 '취약'함
- 이러한 문제를 해결하기 위해 특별히 하위 클래스에서 오버라이드하게 의도된 클래스와 메서드가 아닌 경우 모두 final로 만드는 것을 권장함
- 코틀린도 마찬가지로 위의 철학을 따름
- 클래스와 메서드는 기본적으로 final
- 어떤 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 함
- 그와 더불어 오버라이드를 허용하고 싶은 메서드나 프로퍼티의 앞에도 open 변경자를 붙여야 함
// 열린 메서드를 포함하는 열린 클래스 정의하기
open class RichButton : Clickable { // 열린 클래스 -> 다른 클래스가 상속 가능
fun disable() {} // final method -> 오버라이드 불가
open fun animate() {} // open method -> 오버라이드 가능
override fun click() {} // 상위 클래스에서 선언된 open된 메서드를 오버라이드
// 오버라이드한 메서드는 기본적으로 open
}
- 오버라이드하는 메소드의 구현을 하위 클래스에서 오버라이드하지 못하게 금지하려면 오버라이드하는 메소드 앞에 final을 명시해줘야 함
open class RichButton : Clickable {
final override fun click() {}
// final은 쓸데 없이 붙은 중복이 아님
// final이 없는 override 메서드나 프로퍼티는 기본적으로 open
}
- 자바처럼 코틀린에서도 클래스를 abstract로 선언할 수 있음
- abstract로 선언한 추상 클래스는 인스턴스화할 수 없으며, 추상 멤버를 오버라이드하여 사용
- 추상 멤버는 항상 open
- 따라서 추상 멤버 앞에 open 변경자를 명시할 필요가 없음
// 추상 클래스 정의하기
abstract class Animated { // 추상클래스 -> 인스턴스 생성 불가
abstract fun animate() // 추상 함수로, 구현이 없음 -> 하위 클래스에서 반드시 오버라이드
open fun stopAnimating() {} // 추상 클래스에 속했더라도 비추상 함수는 기본적으로 파이널이지만
fun animateTwice() {} // 원한다면 open으로 오버라이드를 허용할 수 있음
}
- 아래 표는 코틀린의 상속 제어 변경자를 나열한 것
- 표에 있는 설명은 클래스 멤버에 대해 적용 가능
- 인터페이스 멤버의 경우 final, open, abstract를 사용하지 않음
- 인터페이스 멤버는 항상 열려 있으며 final로 변경할 수 없음
- 인터페이스 멤버에게 본문이 없으면 자동으로 추상 멤버가 되지만, 그렇더라도 따로 멤버 선언 앞에 abstract 키워드를 덧붙일 필요가 없음
변경자 | 이 변경자가 붙은 멤버는... | 설명 |
final | 오버라이드 불가 | 클래스 멤버의 기본 변경자 |
open | 오버라이드 가능 | 반드시 open을 명시해야 오버라이드 가능 |
abstract | 반드시 오버라이드 | 추상 클래스의 멤버에만 이 변경자를 붙일 수 있음 추상 멤버에는 구현이 있으면 안 됨 |
override | 상위 클래스나 상위 인스턴스의 멤버를 오버라이드하는 중 | 오버라이드하는 멤버는 기본적으로 open 클래스의 오버라이드를 금지하려면 final을 명시 |
📌 가시성 변경자 : 기본적으로 공개
- 가시성 변경자(Visibility Modifier)는 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어
- 기본적으로 코틀린 가시성 변경자는 자바와 비슷함
- 자바와 같은 public, protected, private 변경자가 있음
- 하지만 코틀린의 기본 가시성은 아무 변경자도 없는 경우 모두 공개(public)
- 패키지 전용 가시성에 대한 대안으로 코틀린에는 internal이라는 새로운 가시성 변경자를 도입
- internal : 모듈 내부에서만 볼 수 있음 (모듈 : 한 번에 한꺼번에 컴파일되는 코틀린 파일들)
- 모듈 내부 가시성은 모듈의 구현에 대해 진정한 캡슐화를 제공한다는 장점이 있음
- 자바에서는 패키지가 같은 클래스를 선언하기만 하면 어떤 프로젝트의 외부에 있는 코드라도 패키지 내부에 있는 패키지 전용 선언에 쉽게 접근할 수 있음
- 이는 모듈의 캡슐화를 쉽게 깰 수 있음
- 코틀린에서는 최상위 선언에 대해 private 가시성을 허용함
- 그런 최상위 선언에는 클래스, 함수, 프로퍼티 등이 포함됨
- 비공개 가시성인 최상위 선언은 그 선언이 들어있는 파일 내부에서만 사용 가능
- 이 또한 하위 시스템의 자세한 구현 사항을 외부에 감추고 싶을 때 유용한 방법
변경자 | 클래스 멤버 | 최상위 선언 |
public(기본 가시성) | 모든 곳에서 볼 수 있음 | 모든 곳에서 볼 수 있음 |
internal | 같은 모듈 안에서만 볼 수 있음 | 같은 모듈 안에서만 볼 수 있음 |
protected | 하위 클래스 안에서만 볼 수 있음 | (최상위 선언에 적용 불가) |
private | 같은 클래스 안에서만 볼 수 있음 | 같은 파일 안에서만 볼 수 있음 |
internal open class TalkativeButton : Focusable {
private fun yell() = println("Hey!")
protected fun whisper() = println("Let's talk!")
}
fun TalkativeButton.giveSpeesh() { // 오류 : "public" 멤버가 자신의 "internal" 수신 타입인 "TalkativeButton"을 노출함
yell() // 오류 : "yell"에 접근할 수 없음: "yell"은 "TalkativeBuootn"의 "private" 멤버임
whisper() // 오류 : "whisper"에 접근할 수 없음: "whisper"는 "TalkativeButton"의 "protected" 멤버임
}
- 코틀린은 public 함수인 giveSpeech 안에서 그보다 가시성이 더 낮은(이 경우 internal) 타입인 TalkativeButton을 참조하지 못하게 함
- 자바에서는 같은 패키지 안에서 protected 멤버에 접근할 수 있지만, 코틀린에서는 그렇지 않다는 점에서 자바와 코틀린의 protected가 다름
📌 내부 클래스와 중첩된 클래스 : 기본적으로 중첩 클래스
- 자바처럼 코틀린에서도 클래스 안에 다른 클래스를 선언할 수 있음
- 클래스 안에 다른 클래스를 선언하면 도우미 클래스를 캡슐화하거나 코드 정의를 그 코드를 사용하는 곳 가까이에 두고 싶을 때 유용
- 자바와의 차이는 코틀린의 중첩 클래스는 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없다는 점
- View 요소를 하나 만든다고 했을 때, View 인터페이스 안에는 뷰의 상태를 가져와 저장할 때 getCurrentState와 restoreState 메소드 선언이 있다 해보자
// 직렬화할 수 있는 상태가 있는 뷰 선언
interface State : Serializable // 직렬화
interface View {
fun getcurrentState() : State
fun restoreState(state: State) { }
}
- Button 클래스의 상태를 저장하는 클래스는 Button 클래스 내부에 선언하면 편함
- 자바에서는 그런 선언을 어떻게 하는지 확인해보자
// 자바에서 내부 클래스를 사용해 View 구현하기
public class Button implements View {
@Override
public State getCurrentState() {
return new ButtonState();
}
@Override
public void restoreState(State state) { /*...*/ }
public class ButtonState implements State { /*...*/ }
}
- 이 코드의 잘못된 점은 무엇일까?
- 왜 선언한 버튼의 상태를 직렬화하면 java.io.NotSerializableException: Button 이라는 오류가 발생할까?
- 직렬화하려는 변수는 ButtonState 타입의 state 였는데 왜 Button을 직렬화할 수 없다는 예외가 발생할까?
- 이 예제의 ButtonState 클래스는 바깥쪽 Button 클래스에 대한 참조를 묵시적으로 포함함
- 그 참조로 인해 ButtonState를 직렬화할 수 없음
- Button을 직렬화할 수 없으므로 버튼에 대한 참조가 ButtonState의 직렬화를 방해함
- 이 문제를 해결하려면 ButtonState를 static 클래스로 선언해야 함
- 자바에서 중첩 클래스를 static으로 선언하면 그 클래스를 둘러싼 바깥쪽 클래스에 대한 묵시적인 참조가 사라짐
- 그렇다면 코틀린은 어떻게 사용하는지 확인해보자
// 중첩 클래스를 사용해 코틀린에서 View 구현하기
class Button : View {
override fun getcurrentState() : State = ButtonState()
override fun restoreState(state: State) { /*...*/ }
class ButtonState : State { /*...*/ } // 이 클래스는 자바의 정적 중첩 클래스와 대응
}
- 코틀린 중첩 클래스에 아무런 변경자가 붙지 않으면 자바 static 중첩 클래스와 같음
- 이를 내부 클래스로 변경해서 바깥쪽 클래스에 대한 참조를 포함하게 만들고 싶다면 inner 변경자를 붙여야 함
클래스 B 안에 정의된 클래스 A | 자바에서는 | 코틀린에서는 |
중첩 클래스(바깥쪽 클래스에 대한 참조를 저장하지 않음) | static class A | class A |
내부 클래스(바깥쪽 클래스에 대한 참조를 저장함) | class A | inner class A |
- 코틀린에서 바깥쪽 클래스의 인스턴스를 가르키려면 내부 클래스 Inner 안에서 this@Outer라고 써야 함
📌 봉인된 클래스 : 클래스 계층 정의 시 계층 확장 제한
- 상위 클래스인 Expr에는 숫자를 표현하는 Num과 덧셈 연산을 표현하는 Sum이라는 두 하위 클래스가 있음
- when 식에서 이 모든 하위 클래스를 처리하면 편리함
- 하지만 when 식에서 Num과 Sum이 아닌 경우를 처리하는 else 분기를 반드시 넣어줘야 함
// 인터페이스 구현을 통해 식 표현하기
interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
fun eval(e: Expr): Int =
when (e) {
is Num -> e.value
is Sum -> eval(e.right) + eval(e.left)
else -> // "else" 분기가 꼭 있어야 함
throw IllegalArgumentException("Unknown expression")
}
- 항상 디폴트 분기를 추가하는게 편하지는 않음
- 그리고 디폴트 분기가 있으면 이런 클래스 계층에 새로운 하위 클래스를 추가하더라도 컴파일러가 when이 모든 경우를 처리하는지 제대로 검사할 수 없음
- 실수로 새로운 클래스 처리를 잊어버렸더라도 디폴트 분기가 선택되기 때문에 심각한 버그가 발생할 수 있음
- 코틀린은 이런 문제에 대한 해법을 제공함
- sealed 클래스의 활용
- 상위 클래스에 sealed 변경자를 붙이면 그 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있음
- sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야 함
// sealed 클래스로 식 표현하기
sealed class Expr { // 기반 클래스를 sealed로 봉인함
class Num(val value: Int) : Expr() // 기반 클래스의 모든 하위 클래스를 중첩 클래스로 나열
class Sum(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr) : Int =
when (e) { // "when" 식이 모든 하위 클래스를 검사하므로 별도의 "else" 분기가 없어도 됨
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
}
- when 식에서 sealed 클래스의 모든 하위 클래스를 처리한다면 디폴트 분기가 필요 없음
- sealed로 표시된 클래스는 자동으로 open임을 기억
- 따라서 open 변경자를 붙일 필요가 없음
- 따라서 open 변경자를 붙일 필요가 없음
728x90
반응형
'Studying > Kotlin' 카테고리의 다른 글
[Kotlin] 코틀린의 Scope Function(범위 지정 함수) (0) | 2023.05.16 |
---|---|
[Kotlin In Action] 4장. 클래스, 객체, 인터페이스(2) - 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언 (2) | 2023.05.11 |
[Kotlin In Action] 3장. 함수 정의와 호출(5) - 로컬 함수와 확장 (0) | 2023.05.08 |
[Kotlin In Action] 3장. 함수 정의와 호출(4) - 문자열과 정규식 다루기 (0) | 2023.05.08 |
[Kotlin In Action] 3장. 함수 정의와 호출(3) - 컬렉션 처리 (0) | 2023.05.08 |