-
[Kotlin in action] 클래스, 객체, 인터페이스프로그래밍 언어/kotlin 2021. 7. 25. 16:03728x90반응형
인터페이스
[자바와 다른 점]
- 코틀린은 인터페이스에 프로퍼티를 선언할 수 있다.
- 코틀린 인터페이스 선언은 기본적으로 final, public 이다.
- 코틀린 중첩 클래스는 내부클래스가 아니어서 외부클래스에 대한 참조가 없다.
4.1 클래스 계층의 정의
자바는 final로 명시적으로 상속을 금지하는 클래스 외에, 모든 클래스는 다른 클래스가 상속할 수 있다. 그래서 문제가 발생할 여지가 있다.
취약 기반 클래스: 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우.
기반 클래스를 변경하는 경우 하위 클래스의 동작이 예기치 않게 바뀔 수 있다.
따라서, 코틀린에서는 기본적으로 클래스와 메소드는 final 이다. 어떤 클래스에 대해 상속을 허용하려면 앞에 open 변경자를 붙이면 된다.
메소드나 프로퍼티를 오버라이드 하려면 open 변경자를 붙여야한다. 오버라이드한 메소는 기본적으로 열려있다.
추상클래스의 추상 멤버는 항상 열려있다. open 변경자는 필요없다.
추상 함수에 속한 비추상 함수는 기본적으로 final 이지만, 원한다면 open으로 오버라이드를 허용할 수 있다.
인터페이스 멤버의 경우 항상 open 이며, final로 변경할 수 없다. (open, final, abstract 키워드 사용 X)
상속 제어 변경자
final : 오버라이드 X
open : 오버라이드 O
abstract : 반드시 오버라이드
override : 상위 클래스나 인스턴스의 멤버를 오버라이드 하는 중
가시성 변경자 : 기본적으로 공개
변경자 클래스 멤버 최상위 선언 public 모든 곳 모든 곳 internal 같은 모듈 안 같은 모듈 안 protected 하위 클래스 안 최상위 선언에 적용 X private 같은 클래스 안 같은 파일 안 어떤 클래스의 구현에 대한 접근을 제한
자바에서는 같은 패키지 안에서 protected 멤버에 접근할 수 있지만, 코틀린에서는 그렇지 않다.
코틀린에서 protected 멤버는 오직 어떤 클래스나 그 클래스를 상속한 클래스 안에서만 보인다.
클래스를 확장한 함수는 그 클래스의 private 이나 protected 멤버에 접근할 수 없다.
코틀린에서는 외부 클래스가 내부 클래스나 중첩된 클래스의 private 멤버에 접근할 수 없다.
내부 클래스와 중첩 클래스: 기본적으로 중첩 클래스
도우미 클래스를 캡슐화 하거나 코드 정의를 그 코드를 사용하는 곳 가까이에 두고 싶을 때 유용하다.
코틀린의 중첩 클래스는 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없다.
View의 상태를 직렬화?
public interface Serializable { }
interface State : Serializable interface View{ fun getCurrentState() : State fun restoreState(state : State) {} } class Button : View { override fun getCurrentState() : State = ButtonState() override fun restoreState(state : State) {} class ButtonState : State {} }
자바에서 중첩 클래스 사용
View의 상태를 직렬화해야 한다고 할 때, 이를 위해 State 인터페이스를 선언하고, Serializable을 구현한다.
Button 클래스의 상태를 저장하는 클래스를 Button 클래스 내부에 선언한다.
State 인터페이스를 구현한 ButtonState 클래스를 정의해서 Button에 대한 구체적인 정보를 저장한다.
public class Button implements View { @Override public State getCurrentState() { return new ButtonState(); } @Override public void restoreState(State state) { /* ... */ } public void ButtonState implements State { /* ... */ } }
이 코드는 java.io.NotSerializableException: Button 오류 발생.
왜 직렬화 하려는건 ButtonState 타입의 state 였는데, 왜 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 변경자를 붙인다.
내부 클래스 Inner 안에서 바깥쪽 클래스 Outer의 참조에 접근하려면 this@Outer 라고 쓴다.
class Outer { inner class Inner { fun getOuterReference(): Outer = this@Outer } }
중첩 클래스를 유용하게 사용하는 용례를 보자.
봉인된 클래스: 클래스 계층 정의 시 계층 확장 제한
클래스 계층을 만들되, 계층에 속한 클래스 수를 제한하고 싶을 경우 사용
상위 클래스에 sealed 변경자를 붙이면 하위 클래스의 정의를 제안할 수 있다.
sealed 클래스는 자동으로 open 이다. 내부적으로는 private 생성자를 가지고 그 생성자는 클래스 내부에서만 호출이 가능하다.
sealed class Expr { // 기반 클래스. 하위 클래스 정의 제한 가능. class Num(val value: Int) : Expr() // 중첩 클래스 class Sum(val left: Expr, val right: Expr) : Expr() // 중첩 클래스 } // 나중에 새로운 하위클래스가 추가되면, 컴파일 에러가 나므로 when 식을 고쳐야하는 것을 알 수 있음. fun eval(e: Expr): Int = when (e) { // When 식에서 모든 하위 클래스 검사 -> else 필요없음 is Expr.Num -> e.value is Expr.Sum -> eval(e.right) + eval(e.left) } fun main(args: Array<String>) { println(eval(Expr.Sum(Expr.Sum(Expr.Num(1), Expr.Num(2)), Expr.Num(4)))) }
4.2 뻔하지 않은 생성자와 프로퍼티
주 생성자와 부 생성자를 구분한다.
주 생성자 - 클래스를 초기화할 때 주로 사용하는 간략한 생성자로, 클래스 본문 밖에서 정의한다.
부 생성자 - 클래스 본문 안에서 정의한다.
초기화 블록 - 초기화 로직 추가
클래스 초기화: 주 생성자와 초기화 블록
주 생성자는 (1)생성자 파라미터를 지정, (2)그 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의
주 생성자 파라미터 이름 앞에 val을 추가하는 방식으로 프로퍼티 정의와 초기화를 간략히 쓸 수 있다.
class User(val nickname: String)
constructor 키워드 - 주 생성자나 부 생성자 정의를 시작할 때 사용한다.
초기화 블록 - 주로 주생성자와 함께 사용된다. 클래스가 인스턴스화될 때 초기화 코드가 들어간다. init 키워드가 초기화 블록을 시작한다.
(1)프로퍼티를 초기화 하는 식이나, (2)초기화 블록 안에서만 주 생성자의 파라미터를 참조할 수 있다.
생성자 파라미터도 디폴트값을 정의할 수 있다.
class User(val nickname: String, val isSubscribed: Boolean = true)
클래스에 기반 클래스가 있다면 주 생성자에서 기반 클래스의 생성자를 호출해야 할 필요가 있다.
기반 클래스를 초기화하려면 기반 클래스 이름 뒤에 괄호를 치고 생성자 인자를 넘긴다.
open class User(val nickname: String) { ... } class TwitterUser(nickname: String) : User(nickname) { ... }
Button의 생성자는 아무 인자도 받지 않지만, Button클래스를 상속한 하위 클래스는 반드시 Button 클래스의 생성자를 호출하여야 한다. 따라서, 기반 클래스 이름 뒤에 반드시 빈 괄호가 들어간다. 생성자 인자가 있다면, 괄호 안에 인자가 들어간다.
class RadioButton: Button()
인터페이스는 생성자가 없기 때문에 어떤 클래스가 인터페이스를 구현하는 경우 그 클래스의 상위 클래스 목록에 있는 인터페이스 이름 뒤에는 괄호가 없다.
주생성자를 비공개로 하면 인스턴스화되는 것을 막을 수 있다.
class Secretive private constructor() {}
비공개 생성자의 대안?
- 정적 유틸리티 함수 -> 최상위 함수 사용
- 싱글턴 패턴 -> 객체 선언
생성자를 만드는 여러가지 방법
생성자가 여럿 필요한 경우
open class View { constructor(ctx: Context) { // 코드 } constructor(ctx: Context, attr: AttributeSet) { // 코드 } }
this를 통해서 클래스 자신의 다른 생성자를 호출할 수 있다.
class MyButton: View { constructor(ctx: Context): this(ctx, MY_STYLE) { // 이 클래스의 다른 생성자에게 위임 // ... } constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) { // ... } }
클래스에 주 생성자가 없다면, 모든 부 생성자는 반드시 상위 클래스를 초기화 하거나 다른 생성자에게 생성을 위임해야 한다.
부 생성자가 필요한 이유?
- 자바의 상호 운용성
- 클래스 인스턴스를 생성할 때 파라미터 목록이 다른 생성 방법이 여럿 존재하는 경우에는 부 생성자를 여럿 둘 수 밖에 없다.
인터페이스에 선언된 프로퍼티 구현
인터페이스에 추상 프로퍼티 선언을 넣을 수 있다.
interface User { val nickname: String }
이는 User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야 한다는 뜻이다.
인터페이스에 있는 프로퍼티 선언에는 뒷받침하는 필드나 게터 등의 정보가 들어있지 않다.
인터페이스 구현 방법을 보자. 추상프로퍼티를 구현하고 있으므로 override 표시를 해줘야 한다.
class PrivateUser(override val nickname: String) : User // 주 생성자에 있는 프로퍼티 class SubscribingUser(val email: String) : User { override val nickname: String get() = email.substringBefore('@') // 커스텀 게터. 뒷받침필드에 저장하지 않고 매번 이메일 주소에서 별명을 계산해 반환한다. } class FacebookUser(val accountId: Int) : User { override val nickname = getFacebookName(accountId) // 프로퍼티 초기화 식. 객체를 초기화하는 단계에 한번만 호출하여 뒷받침 필드에 저장. }
PrivateUser는 User의 추상 프로퍼티 구현.
SubsribingUser 에서는 커스텀 게터 접근자를 이용해서, nickname이 사용될 때마다 매번 이메일 주소에서 별명을 계산한다.
FacebookUser 에서는 객체를 초기화하는 단계에 한번만 호출하여 뒷받침필드에 저장한다.
interface User { val email: String, val nickname: String get() = email.substringBefore('@') // 뒷받침 필드 없고, 매번 결과를 계산해 돌려줌 }
하위 클래스는 email은 반드시 오버라이드 해야 하지만, nickname은 오버라이드하지 않고 상속할 수 있다.
인터페이스에 선언된 프로퍼티와 달리 클래스에 구현된 프로퍼티는 뒷받침하는 필드를 원하는 대로 사용할 수 있다.
접근자(게터/세터)에서 뒷받침하는 필드를 가리키는 방법
class User(val name: String) { var address: String = "unspecified" set(value: String) { println(""" Address was changed for $name: "$field" -> "$value".""".trimIndent()) field = value // 뒷받침하는 필드 값 변경 } }
접근자의 가시성 변경
기본적으로 프로퍼티의 가시성과 같다.
필요히 get, set 앞에 가시성 변경자를 추가해서 접근자의 가시성을 변경할 수 있다.
컴파일러가 생성한 메소드: 데이터 클래스와 클래스 위임
코틀린 컴파일러는, 데이터 클래스의 유용한 메서드를 자동으로 만들어주고, 예와 클래스 위임 패턴을 아주 간단하게 쓸 수 있게 해준다.
모든 클래스가 정의해야 하는 메소드
- toString()
- equals()
- hashCode()
equals()를 오버라이드 할 때, 조금 더 객체간 복잡한 비교를 할 때 잘 작동하지 않을 수 있다. 대비해서 hashCode()도 오버라이드 해야함. (ex. equals 가 true를 반환하는 객체는 같은 hashcode 반환해야 할 때)
data class : 모든 클래스가 정의해야 하는 메소드 자동생성
- equals
- toString
- hashCode
- copy 메소드 -> 객체의 불변성을 지키기 위해, 객체를 복사하면서 일부 프로퍼티를 바꿀 수 있게 해주는 copy 메서드
by키워드를 통한 클래스 위임
상속을 허용하지 않는 클래스에 새로운 동작을 추가하여야 할 때 -> 데코레이터 패턴
상속을 허용하지 않는 클래스 대신, 사용할 수 있는 새로운 클래스를 만들 되, 기존 클래스와 같은 인터페이스를 데코레이터가 제공하게 만들고, 기존 클래스를 데코레이터 내부에 필드로 유지한다.
class CountingSet<T>( val innerSet: MutableCollection<T> = HashSet<T>() ) : MutableCollection<T> by innerSet { var objectsAdded = 0 override fun add(element: T): Boolean { objectsAdded++ return innerSet.add(element) } override fun addAll(c: Collection<T>): Boolean { objectsAdded += c.size return innerSet.addAll(c) } }
새로운 클래스 CountingSet을 만들어서, MutableCollection 인터페이스의 구현을 innerList 객체에 위임중이다. add, addAll이라는 새로운 함수를 추가하여 MutableCollection의 동작을 확장하고있다.
object 키워드 사용
- 객체 선언 : 클래스 선언 + 단일 인스턴스 선언 = 싱글턴 패턴
- 동반 객체 : 팩토리 메서드(인스턴스와 관계 없이 클래스 내부 private 멤버에 접근)와 정적 멤버가 들어가는 객체
코틀린 클래스 안에서는 정적 멤버가 없다(static 키워드 없음). 그 대신, 코틀린에서는 패키지 수준의 최상위 함수(자바의 정적 메소드 역할 대체)와 객체 선언(자바의 정적 메소드 역할 중 코틀린 최상위 함수가 대신할 수 없는 역할이나, 정적 필드를 대신)을 활용한다.
하지만 최상위 함수는 클래스의 비공개 맴버의 접근할 수 없다. 따라서, 클래스의 인스턴스와 관계 없이 호출해야 하지만, 클래스 내부 정보에 접근해야 하는 함수가 필요한 경우, 클래스에 중첩된 객체 선언의 멤버 함수로 정의해야 한다.
동반 객체를 사용하여 안에 메소드나 프로퍼티를 선언하면, 해당 객체가 정의된 클래스의 이름을 사용하여 호출할 수 있어 자바의 정적 메소드, 정적 필드 사용 구문과 같다.
동반 객체 안이 private 생성자를 호출하기 좋은 위치이다. 자신을 둘러싼 클래스의 모든 private 멤버에 접근이 가능하기 때문이다.
동반 객체 안은 팩토리 패턴을 구현하기 아주 적합한 위치다.
팩토리 패턴 = 팩토리 메서드를 통해서만 해당 클래스의 인스턴스를 만들도록 강제. (생성자를 통해 인스턴스 만들 수 없음)
팩토리 메서드는 팩토리 메서드가 선언된 클래스의 하위 클래스 객체를 반환할 수도 있다.
- 무명 내부 클래스 : 무명 객체를 정의할 때
window.addMouseListener ( object : MouseAdapter() { // MouseAdapter를 확장하는 무명객체 override fun mouseClicked(e: MouseEvent) { } override fun mouseEntered(e: MouseEvent) { } } )
함수를 호출하면서 인자로 무명 객체를 넘겨주면 된다.
final이 아닌 변수도 객체 식 안에서 사용할 수 있다.
728x90'프로그래밍 언어 > kotlin' 카테고리의 다른 글
[Kotlin In Action] 코틀린 타입 시스템 (0) 2021.08.06 [Kotlin in Action] 람다로 프로그래밍 (0) 2021.07.26 [Kotlin in Action] 함수의 정의와 호출 (0) 2021.07.13 [코틀린 인 액션] 코틀린 기초 (0) 2021.07.06 [Kotlin] 코루틴을 이해해보자 (0) 2021.07.04