Kotlin in Action 4챕터-세션 1
4챕터를 열며
코틀린의 인터페이스에는 프로퍼티의 선언이 들어갈 수 있다.
또한 코틀린 클래스는 기본적으로 final public의 속성을 가지고 있다.
따라서 상속을 위해선 open키워드를 꼭 명시해주어야한다.
data클래스 역시 컴파일러가 일부 표준 메서드를 컴파일러가 생성해주고, 위임 기능을 사용하면 위임 처리에 필요한 준비 메서드도 직접 작성할 필요가 없어진다.
이 챕터는 보다 코틀린스럽게 클래스, 인터페이스, 객체를 다루는 여정이 될 것이다.
클래스 계층의 정의
코틀린 인터페이스
코틀린 인터페이스는 자바 8의 디폴트 메서드와 유사하게 구현이 있는 메서드도 정의할 수 있다.
하지만, 아무런 상태(필드)도 들어갈 수 없기 때문에 만약 상태의 표현이 필요하다면 프로퍼티를 통해 간접적으로 명시하는 것만 가능하다.
단순 인터페이스 구현
interface clickable{
fun click()
}
class Button:clickable{
override fun click()=println("testing")
}
fun main() {
Button().click()
}
위 코드는 clickable이라는 인터페이스의 함수를 override를 통해 Button클래스에서 재정의 하고 있다.
참고로 이때 자바와 달리 클래스 이름 뒤에 ` :인터페이스명`을 하면 따로 implements나 extends를 쓰지 않아도 된다.
유의할 것은 인터페이스의 개수에는 제약이 없으나, 인터페이스 하나는 클래스 하나에 의해서 확장될 수 있다.
유연한 인터페이스 구현하기
어떤 인터페이스를 정의한 뒤 새로운 동작을 정의하거나, 그냥 재정의를 생략한 뒤 디폴트 구현을 사용하고 싶을 때가 있을 것이다
단순한 동작의 정의는 우리가 위에서 한 예제와 크게 다르지 않지만, 정의를 생략한 디폴트 구현의 경우 다음과 같이 코드를 작성해야한다.
code
interface Clickable {
fun click()
fun showOff() = println("I'm testing!")
}
interface Focus {
fun setFocus(b: Boolean) =
println("I ${if (b) "true" else "false"} focus")
fun showOff() = println("focusable")
}
class Button : Clickable, Focus {
override fun click() {
println("This is click function")
}
override fun showOff() {
super<Clickable>.showOff()
}
}
fun main() {
val btn = Button()
btn.showOff()
btn.setFocus(true)
btn.click()
}
위에 코드에서 보며 알 수 있듯 인터페이스의 경우 무조건 함수 재정의시 override임을 명시해주어야하며, 여러개의 요소에 동일한 이름을 가진 하위 메서드가 있는 경우 어떤 인터페이스에 소속된 메서드를 불러오려고 하는지를 super<인터페이스명>
을 통하여 알려주어야한다.
open final abstract 변경자
자바에서는 final을 통해 상속을 금지하지 않는 이상 모든 클래스는 상속이 가능하다.
하지만, 상위 클래스에서 가져야 할 규약들을 명확하게 전달해주지 못했을 경우, 하위 클래스에서 잘못된 오버라이드를 하면서 기본적인 가정들이 깨지게 되고 그로 인한 문제들이 생길 수 있다.
그래서 이펙티브 자바에서는 상속을 위한 설계와 문서를 갖추지 않은 클라스라면 상속을 금지하라는 표현까지 하기도 한다.
즉, 상속을 하기 위해 만든 상위 클래스라면, 처음 설계 단계에서부터 상속에 필요한 메서드와 설명 등을 충분히 갖추고 논리적 설계
를 해야한다는 뜻이다.
final유의점
위에 내가 작성한 글에서는 맥락적으로 final이 상속을 금지시키는 역할만을 하는 것처럼 나와있다.
하지만, 이는 클래스에 final을 붙였을 때의 이야기이며 변수에 final을 붙일 경우 변수에 할당된 값을 변경할 수 없도록 하는 역할을 한다.
final은 클래스나 변수의 성질을 지정하는 한정자의 역할을 한다는 것을 알 수 있다.
코틀린에서는
코틀린도 이러한 철학을 계승하였다.
그런데 코틀린은 기본적으로 클래스와 메서드가 final로 규정되어있기 때문에, 상속의 허용을 위해서는 클래스 선언 시 앞에 open변경자를 붙여야만 한다.
유의점은 클래스를 상속한다고 메서드나 프로퍼티도 한꺼번에 상속이 되는 것이 아니기 때문에, 상속이 필요한 메서드나 프로퍼티에 한해서 앞에 open 변경자를 붙여야한다.
override메서드나 프로퍼티는 기본적으로 open의 성격을 가지고 있기 때문에, 하위 클래스에서 오버라이드를 못하게 하려면 final을 꼭
명시해주어야 한다.
코틀린에서의 abstarct
추상 클래스 이기 때문에 이 클래스의 인스턴스를 직접 만드는 것이 불가능하다.
또한 추상함수 역시 이와 유사한데, 추상함수에는 구현에 대한 내용을 담을 수 없으며, 이후 오버라이드 된 함수
를 통해 구현의 내용을 담아주여야한다.
유의할 점은 추상 클래스 내에도 추상 메서드 외에도 일반 메서드나 프로퍼티를 넣을 수 있다는 것이다.
추상 클래스 내부에 있는 일반 함수는 기본적으로 final의 속성을 띄기 때문에 오버라이드를 통한 내용의 변경 혹은 재구성이 불가능하나, 원한다면 open
변경자를 통해 오버라이드를 허용하는 것도 가능하다.
인스턴스 멤버의 경우 본문이 없으면 자동으로 추상 멤버로 인식되기 때문에 선언 앞에 abstaract 키워드를 꼭 부이기 않아도 괜찮다.
상속 제어 변경자 정리 표
|변경자|멤버|부연설명| |——|—|—| |final|오버라이드 불가|기본적인 멤버 변경자| |open|오버라이드 가능|open을 명시해야만 코틀린은 override가능| |abstract|반드시 오버라이드 해야|추상 클래스의 멤버 혹은 추상클래스에만 사용가능. 멤버에는 구현이 들어갈 수 없다| |override|상위 클래스나 인스턴스의 멤버 오버라이드 표식|오버라이드 하는 멤버는 기본적으로 열려있음
가시성 변경자
코드 기반 선언에 대한 클래스 외부 접근을 제어한다.
이름에서도 알 수 있듯 변경에 대한 제한이라는 점에서는 상속 제어 변경자와 비슷하다고 느낄 수 있으나, 가시성 변경자의 경우 코드 내부에서만 볼 수 있도록 가시성을 제한하는 것이고 상속 제어 변경자의 경우 상속에 대해 제어를 한다는 차이가 있다.
기본적으로 코틀린의 가시성 변경자는 자바와 유사하게 private, protected,public이 있다.
그러나 코틀린의 경우 아무 변경자가 없을 경우 기본 가시성이 public이라는 점이 자바와 다르다.
또한 코틀린의 경우 패키지가 네임 스페이스 관리 용도로만 쓰이기 때문에 패키지를 가시성 제어로는 사용하지 않는다.
대신, interanl이라는 변경자를 통해 모듈 단위로 가시성을 변경하는 것이 가능하다.
모듈 가시성 변경자의 이점
자바에서는 패키지 내에서 같은 클래스를 선언하면 어떤 프로젝트 외부의 코드라도 패키지 내부의 패키지 전용 선언에 접근 가능하다.
이러한 특성 때문에 모듈의 캡슐화가 깨지는 경우도 생긴다.
코틀린에서는 최상위 선언에 한해 private가시성을 허용하는 것도 가능하기 때문에, 상위 시스템의 최상위 선언을 하위 시스템 혹은 외부에게 노출하고 싶지 않을 때 유용하게 사용 가능하다.
가시성 규칙
가시성이 더 낮은 (즉 제한사한이 더 빡빡한 경우) 타입을 가시성이 높은 타입에서 참조하는 것은 불가능하다.
그렇기 때문에, 일반적으로는 타입 제네릭 파라미터에 들어있는 타입의 가시성은 그 클래스의 가시성과 같거나 높아야한다.
이러한 규칙들은 코드의 안정성을 높여주어, 어떤 함수를 호출하거나 클래스를 확장할 때 상위에서 선언한 규범들이 깨지지 않도록 도와준다.
protected 차이
자바에서는 같은 패키지 안에만 있다면 protected멤버에 접근이 가능하다.
하지만 코틀린에서는 protected멤버는 오직 그 클래스를 상속한 하위 클래스 내부 혹은 어떤 클래스에서만 보인다.
따라서 클래스를 확장한 하위 클래스 내에 있는 함수의 경우,원래 클래스의 private나 protected 멤버에 접근 불가능하다.
코틀린 코드를 자바로 마이그레이션 시
코틀린의 public protected 변경자의 경우 똑같은 가시성을 유지하며, 자바로 컴파일된 바이트 코드 안에서도 똑같이 작동한다.
그러나, 자바에서는 클래스를 private로는 만들 수 없기 때문에 내부적으로 코틀린은 private클래스를 패키지 전용 클래스로 컴파일 한다.
또한 internal의 경우 자바에는 딱 맞는 가시성이 없으므로, 바이트 코드상으로는 public으로 나타낸다.
내부 클래스와 중첩된 클래스
자바와 코틀린은 모두 클래스 내에 다른 클래스를 선언 가능하다.
이러한 기능은 도우미 클래스를 캡슐화하거나 코드의 정의를 그 코드를 사용하는 곳 가까이에 두어 가독성을 높이고자 할 때에 유용하다.
그러나 코틀린에서는 중첩 클래스가 명시적으로 요청을 하지 않는다면 내부 클래스는 바깥쪽 클래스의 인스턴스에 대한 접근이 불가능하다.
코드로 보는 특징
interface State: Serializable
interface View {
fun getCurrentState(): State
fun restoreState(state: State) { }
}
잘못된 예시
// Java 구현
public class Button implements View {
@Override
public State getCurrentState() {
return new ButtonState();
}
@Override
public void resoreState(State state) { /*...*/ }
public class ButtonState implements State { /*...*/ }
}
자바의 특징
자바에서는 다른 클래스 안에서 정의한 클래스는 자동으로 내부 클래스(inner class)가 된다.
그렇기 때문에 위에서 보여준 잘못된 예시의 경우 State가 바깥쪽 Button과 묵시적으로 관계를 맺고 있고(참조) 그 관계로 인하여 직렬화가 불가능하다는 에러가 뜨게 된다.
이러한 오류를 해결하기 위해서는 ButtonState를 static으로 선언하여 바깥 클래스에 대한 묵시적 참조를 끊어주어야한다.
코틀린에서는
코틀린 중첩 클래스에는 변경자가 없으면 static
중첩 클래스와 같다.
만약 내부 클래스처럼 사용하여 바깥 클래스에 대한 참조를 포함하고 싶다면 inner
변경자를 붙이면 된다.
만약 바깥쪽 클래스의 인스턴스를 가리키는 참조를 표시할 경우 안쪽 클래스에 this @외부 클래스명
을 작성해주어야한다.
용도(inner클래스)
코틀린에서는 클래스의 계층을 만들되, 그 계층에 속한 클래스를 제한하고 싶은 경우 중첩 클래스를 사용할 수 있다.
즉 어떠한 관계성은 가지고 있는 클래스여서 다른 클래스로 내보내기에는 애매한 경우 서로의 관계성을 명시적으로 표현하기 위하여 이너클래스를 사용할 수 있다.
봉인된 클래스
클래스의 계층이 확장 되어야 하는 때가 있다.
이때 만약 모든 분기로 확장이 뻗어나갈 수 있다면, 오류의 지점을 명확하게 알 수 없을 뿐 아니라 새로운 클래스의 처리가 되지 않았는데도 디폴트 분기가 선택되어 버그를 일으킬 수 있다.
코틀린은 봉인된 클래스라고 불리는 sealed
클래스를 통해 이러한 문제를 해결한다.
sealed
는 기본적으로 open된 클래스이므로 따로 open변경자를 붙이지는 않아도 된다.
봉인된 클래스의 경우 클래스 외부에 자신을 상속할 클래스를 둘 수 없으므로 코드의 안정성을 높일 수 있다.
참고로 인터페이스의 경우 sealed로 정의할 수 없으며, 내부적으로 sealed클래스는 private생성자를 가진다.
왜 그럴까
봉인된 클래스가 private생성자를 가지는 이유는 명확하다.
이름 그대로 봉인된 클래스 이므로 외부에 자신을 상속한 클래를 둘 수 없고, 이를 명시적으로 표현하자면 봉인된 클래스의 생성자는 private하게 이루어져야 한다고 말할 수 있다.
또한 인터페이스를 봉인된 클래스르 만들 수 없는 이유는, 자바 쪽에서 그 인터페이스를 오버라이드 하는 것을 막을 수 있는 기능이 컴파일러에 없기 때문이다.
댓글남기기