코틀린 & Java/코틀린인액션

[Kotlin] 클래스, 객체, 인터페이스에 대해 알아보자(1)

코딩하는후운 2024. 3. 18. 13:48
반응형

코틀린의 클래스와 인터페이스는 자바와는 약간 다르다

  • 인터페이스에 프로퍼티 선언이 들어갈 수 있다.
  • 코틀린 선언은 기본적으로 final이며 public이다.
  • 중첩 클래스는 내부 클래스가 아니다. 즉, 코틀린 중첩 클래스에는 외부 클래스에 대한 참조가 없다.

코틀린 컴파일러는 유용한 메서드를 자동으로 만들어 준다.

  • 클래스를 data로 선언하면 일부 표준 메서드를 생성해준다.
  • object키워드 : 클래스와 인스턴스를 동시에 선언 싱글턴 클래스, 동반객체(companion object), 객체 식(object expression(자바의 무명클래스)
  • 코틀린 언어가 제공하는 위임(delegation)을 사용하면 준비 메서드를 직접 작성할 필요가 없다.

Delegation 이란?

  • by 키워드를 활용한 Properties에서의 활용
private val viewModel: MainViewModel by lazy {
    MainViewModel()
}
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

결국 by 이후에 오는 lazy에게 프로퍼티 생성을 위임하고, lazy의 내부 동작에 따라 코드를 초기화한다.

Lazy의 최상위는 interface로 구성되어 있고, property인 value와 함수인 isInitialized로 구성되어 있다. 결국 value는 Properties를 getter로 구성해 값을 리턴하는데, 이때 lazy 패턴을 활용하는 형태로 구성되어 있다.

내부적으로 값이 호출되기 전에는 temp 값을 담을 수 있도록 만들고, 외부에서 value를 호출하면 value 안에 있는 get()에서 이를 늦은 처리하도록 한다.

결국 by lazy {} 호출 시 lazy에게 위임해 내부 코드의 동작에 따라 delegation 처리를 함을 알 수 있다.

  • interface를 class delegation에서의 활용 (상속 대신 delegation을 활용할 수 있다.)
  1. 외부에서 주입을 통해 Base를 갈아치울 수도 있고,
  2. 바로 초기화해 사용하는 것도 가능
interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

class Derived(b: Base) : Base by b  //1)
class Derived : Base by BaseImpl()  //2)

fun main() {
    val b = BaseImpl(10)
    Derived(b).print()
		//2)
		Derived().print()
}

클래스 계층 정의

코틀린 인터페이스

  • 코틀린 인터페이스 안에는 추상메서드뿐 아니라 구현이 있는 메서드도 정의할 수 있다.(자바 8의 디폴트 메서드와 비슷)
  • 다만 아무런 상태(필드)도 들어갈 수 없다.
  • 코틀린에서는 override 변경자를 꼭 사용해야 한다.

실수로 상위 클래스의 메서드를 오버라이드 하는 경우를 방지. 상위 클래스 메서드와 하위 클래스 메서드가 같은 경우 컴파일이 안되기 때문.

  • 디폴트 구현을 제공할 수 있다. 특별한 키워드 없이 메서드 본문을 메서드 시그니처 뒤에 추가하면 된다.
    • 새로운 동작을 정의할 수도 있고, 정의를 생략해서 디폴트 구현을 사용할 수 있다.
interface Clickable {
	fun click()  <- 일반 메서드
	fun showOff() = println("i`m clickable!") <- 디폴트 구현이 있는 메서드
}

만약, 두 인터페이스 같은 함수의 메서드가 있다면? → 어느쪽도 선택되지 않는다. 대체할 오버라이딩 메서드를 직접 제공하지 않으면 컴파일러 오류 발생.

//하위 클래스에서 명시적으로 새로운 구현을 제공해야 한다.
override fun showOff() {
	super<Clickable>.showOff()
	super<Focusable>.showOff()
}

//하나만 써도 된다면
override fun showOff() = super<Clickable>.showOff()

코틀린은 자바 6과 호환되게 설계됐다.

→ 인터페이스의 디폴트 메서드를 지원하지 않는다.

따라서 코틀린은 디폴트 메서드가 있는 인터페이스를 일반 인터페이스와 디폴트 메서드 구현이 정적 메서드로 들어있는 클래스를 조합해 구현한다.

인터페이스에는 메서드 선언만 들어간다.

함께 생성되는 클래스에는 디폴트 메서드 구현이 정적 메서드로 들어간다.

디폴트 인터페이스가 포함된 코틀린 인터페이스를 자바 클래스에서 상속해 구현하고 싶다면

→ 모든 메서드에 대한 본문을 작성해야 한다.

하지만 코틀린 1.5부터는 컴파일러가 자바 인터페이스의 디폴트 메서드를 생성해준다.

코틀린 클래스

취약한 기반 클래스 문제.

  • 작성한 사람의 의도와 다른 방식으로 메서드를 오버라이드할 위험이 있다.
  • 하위 클래스의 동작이 예기치 않게 바뀔 수도 있다는면에서 기반 클래스는 취약하다.

→ 하위 클래스에서 오버라이드 하게 의도된 클래스와 메서드가 아니라면 모두 final로 만들라는 뜻.

고로~ 코틀린의 클래스와 메서드는 기본적으로 final이다.

open, final 변경자

  • 상속을 허용하려면 클래스(메서드, 프로퍼티) 앞에 open 변경자를 붙여야 한다.
  • 오버라이드한 메서드는 기본적으로 열려있다.
    • 하위 클래스에서 오버라이드 하지 못하게 금지하려면 오버라이드하는 메서드 앞에 final을 명시

열린 클래스와 스마트 캐스트

기본적인 상속상태를 final로 함에 있어 장점은 스마트 캐스트가 가능하다는 점이다.

스마트 캐스트는 타입 검사 뒤에 변경될 수 없는 변수에만 적용 가능하다. 기본적으로 final이기 때문에 대부분의 프로퍼티를 스마트 캐스트에 활용할 수 있다.

abstract 변경자

  • 추상 클래스는 인스턴스화 할 수 없다.
  • 구현이 없는 추상 멤버가 있기 때문에 하위클래스에서 오버라이드해야만 하는게 보통
  • 추상멤버는 항상 열려있다. (따라서 open 변경자를 명시할 필요가 없다.)
  • 추상 클래스에 속했더라도 비추상 함수는 기본적으로 final이지만 원한다면 open으로 오버라이드를 허용할 수 있다.

클래스 내에서 상속 제어 변경자의 의미

변경자 이 변경자가 붙은 멤버는 설명

final 오버라이드 X 기본 변경자
open 오버라이드 O 반드시 open을 명시해야 오버라이드 가능
abstract 반드시 오버라이드 해야함 추상 클래스의 멤버에만 붙일 수 있다.
구현이 있으면 안된다.    
override 오버라이드 중 오버라이드하는 멤버는 열려있다.
금지하려면 final을 명시    

인터페이스 멤버의 경우 final, open, abstract를 사용하지 않는다.

인터페이스 멤버는 항상 열려 있으며 final로 변경할 수 없다.

인터페이스 멤버에게 본문이 없으면 자동으로 추상 멤버가 되지만, 따로 abstract를 덧붙일 필요가 없다.

가시성 변경자

  • 아무 변경자도 없는 경우 모두 public이다.
  • 코틀린은 패키지를 네임스페이스를 관리하기 위한 용도로만 사용하기 때문에 패키지를 가시성 제어에 사용하지 않음.
  • 외부클래스가 내부클래스나 중첩된 클래스의 private멤버에 접근할 수 없다.
  • 패키지 전용 가시성에 대한 대안으로 internal이라는 가시성 변경자 도입 (모듈 내부에서만 볼 수 있음)

모듈(module) : 한 번에 한꺼번에 컴파일 되는 코틀린 파일들

코틀린의 가시성 변경자

변경자 클래스 멤버 최상위 선언

public(기본 가시성) 모든 곳 모든 곳
internal 같은 모듈 안 같은 모듈 안
protected 하위 클래스 안 최상위 선언에 적용할 수 없음
private 같은 클래스 안 같은 파일 안

자바에서는 같은 패키지 안에서 protected 멤버에 접근할 수 있지만,

코틀린에서는 그렇지 않다.(클래스를 상속한 클래스 안에서만 보인다.)

→ 클래스를 확장한 함수는 그 클래스의 private나 protected 멤버에 접근할 수 없다.

컴파일된 코틀린 선언의 가시성은 자바에서 똑같은 가시성을 사용해 선언한 경우와 같다. 유일한 예외는 private 클래스이다. 자바에서는 클래스를 private로 만들수 없다. 내부적으로 코틀린은 private 클래스를 패키지-전용 클래스로 컴파일 한다.

internal변경자는 자바에 맞는 가시성이 없다. 따라서 바이트코드상에서는 public이 된다.

이런 차이가 있기 때문에, 코틀린에서 접근할 수 없는 대상을 자바에서 접근할 수 있는 경우가 생김. 예) internal 클래스나 internal 최상위 선언 또한 코틀린에서 protected로 정의한 멤버를 같은 패키지에 속한 자바코드에서는 접근 가능.

  • 코틀린 컴파일러가 internal 멤버의 이름을 바꿔 버립니다.
    • action()같은 이름의 함수라면 action$AAA_XXX_BBB() 같이 컴파일 됩니다.

이름을 바꾸는 이유

  1. 모듈 밖에서 상속한 경우 하위/상위 클래스 내부의 메서드 이름이 내부메서드를 오버라이드 하는 경우를 방지
  2. 모듈 외부에서 사용하는 일을 막기 위함

내부 클래스와 중첩된 클래스

  • 자바와의 차이는 코틀린 중첩클래스는 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없다.
  • 자바에서는 다른 클래스 안에 정의한 클래스는 내부 클래스가 된다. (바깥쪽 클래스에 대한 참조를 포함) 참조로 인해 직렬화 문제 생김. 해결법 : static 붙여준다.
  • 자바에서 중첩 클래스를 static으로 선언하면 클래스를 둘러싼 바깥쪽 클래스에 대한 묵시적인 참조가 사라진다.
  • 코틀린 중첩 클래스에 아무런 변경자가 붙지 않으면 자바 static 중첩 클래스와 같다. 이를 내부 클래스로 변경해서 바깥쪽 클래스에 대한 참조를 포함하게 만들고 싶다면 inner 변경자를 붙여야 한다.

P.155

  • 코틀린에서 Inner안에서 Outer의 참조에 접근하려면 this@Outer라고 써야한다.

봉인된 클래스 (sealed 클래스)

실드 클래스는 추상 클래스처럼 계층을 나타낼 수 있으며 하위 클래스는 데이터 클래스, Object, 일반 클래스, 또 다른 실드 클래스 등 모든 타입의 클래스가 될 수 있습니다.

sealed class UiState {
    data class Success<T>(val data: T): UiState()
    data class Error(val error: Throwable?): UiState() 
    object Loading: UiState()
}
  • when식에서 Num과 Sum이 아닌 경우 처리하는 else 분기를 반드시 넣어줘야 했다.(이전 발표) 컴파일러가 when이 모든 경우를 처리하는지 제대로 검사할 수 없다.
  • sealed클래스 (자동으로 open) 상위 클래스에 sealed 변경자를 붙이면 모든 하위 클래스를 검사.
  • sealed 인터페이스를 정의할 수는 없다. (1.5미만) → 자바쪽에서 구현하지 못하게 막을 수 있는 수단이 코틀린 컴파일러에게 없다.

코틀린 1.0에서는 제약이 심했다. 하위 클래스는 중첩 클래스여야 하고, 데이터 클래스로 sealed클래스를 상속할 수도 없다.

1.5부터는 아무위치에 선언할 수 있게 됐고, 봉인된 인터페이스도 추가됐다.

내부 동작

실제 변환된 자바 파일을 보면

UiState 클래스는 추상클래스로 바뀌었고,

@Metadata 어노테이션에 해당 클래스의 정보, 하위 클래스 정보가 들어있어서 컴파일러가 이를 인지하게 됩니다.

private 생성자가 생겼으며, 컴파일러가 접근할 수 있는 synthetic 생성자가 만들어졌습니다.

4.2 생성자

  • 코틀린은 주 생성자와 부 생성자를 구분한다.
  • 초기화 블록을 통해 초기화 로직을 추가할 수 있다.
class User (val nickname: String)

클래스 이름 뒤에 오는 괄호로 둘러싸인 코드를 주 생성자라고 부른다.

  • 생성자 파라미터를 지정
  • 파라미터에 의해 초기화되는 프로퍼티를 정의
class User constructor (_nickname: String) { //파라미터가 하나만 있는 주 생성자
	val nickname: String
	
	init {  // 초기화 블록
		nickname = _nickname
	}
}

constructor 키워드 : 주 생성자나 부 생성자 정의를 시작할 때 사용

init 키워드 : 객체가 만들어질 때 초기화 블록 시작 (주 생성자와 함께 사용)

  • 필요하다면 클래스 안에 여러 초기화 블록을 선언할 수 있다.
  • 주 생성자 앞에 별 다른 애노테이션이나 가시성 변경자가 없다면 constructor를 생략해도 된다.

모든 생성자 파라미터에 디폴트 값을 지정하면 컴파일러가 자동으로 파라미터가 없는 생성자를 만들어 준다.

  • 주 생성자에 private 변경자를 붙일 수 있다. 주 생성자밖에 없고, 비공개이면 외부에서 인스턴스화 할 수 없다.

부 생성자

  • 왠만하면 디폴트 파라미터 값과 이름 붙인 인자 문법을 사용해 부 생성자를 여러개 만들지 말라.
  • 가끔 필요한 경우가 있다. 프레임워크 클래스를 확장해야 하는데 여러가지 방법으로 초기화 할 수 있게 다양한 생성자를 지원해야 하는 경우다. 예) View의 super() 키워드를 통해 상위 클래스 생성자를 호출 (생성자가 상위 클래스 생성자에게 객체 생성을 위임한다.)
  • 클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화 하거나 다른 생성자에게 생성을 위임해야 한다.

부생성자가 필요한 주된 이유는

  • 자바 상호 운용성이다.

인터페이스에 선언된 프로퍼티 구현

interface User {
	val nickname: String
}

User인터페이스를 구현하는 클래스에서 nickname의 값을 얻을 수 있는 방법을 제공해야 한다.

class PrivateUser(override val nickname: String) : User // 주 생성자에 있는 프로퍼티

class ~~~ {
	override val nickname: String
		get() = ~~~  // 커스텀 게터
}

class ~~ {
	override val nickname = getFaceBookName() //프로퍼티 초기화 식
}
  • 추상 프로퍼티를 구현하고 있으므로 override를 표시해야 한다.
  • 뒷받침하는 필드는 매번 계산(커스텀 게터)해 반환한다.
  • 프로퍼티 초기화 식은 객체를 초기화 하는 단계에 한 번만 호출

게터와 세터에서 뒷받침하는 필드에 접근

  • 컴파일러는 디폴트 접근자 구현을 사용하건 직접 게터 세터를 정의하건 관계없이 게터나 세터에서 field를 사용하는 프로퍼티에 대해 뒷받침하는 필드를 생성해 준다.
  • 다만 field를 사용하지 않는 커스텀 접근자 구현을 정의한다면 뒷받침하는 필드는 존재하지 않는다.

뒷받침하는 필드(Backing Fields)

field를 통해 프로퍼티의 값을 리턴

val name = "Kim"
  get() {
    return field
  }

접근자 메서드(accessor method) 커스터마이징

게터와 세터에 원하는 로직을 추가

private var getNameCount = 0
val name = "Kim"
  get() {
    println("name getter call count : ${++getNameCount}")
    return field
  }

접근자의 가시성 변경

  • get이나 set앞에 가시성 변경자를 추가해서 변경할 수 있다.
var counter: Int = 0
private set // 이 클래스 밖에서 값을 바꿀 수 없다.
반응형