[Kotlin] Kotlin Study #4

이제일·2023년 8월 12일
0

Kotlin

목록 보기
4/10

Class

  • 클래스도 함수처럼 기본 구조는 머리부, 몸체부로 구분
  • 코틀린의 클래스는 주 생성자를 머리부에 정의

구조

1번 라인(머리부)

  • public final class

    접근 제어자인 public과 상속을 막는 변경자 final, class를 선언하는 키워드 class

    kotlin에서 접근 제어는 public이 default 설정이다. 접근제어자로는 다음과 같다
    private : 파일 내부에서만 사용 가능
    protected : 파일 내부나 상속한 경우에만 사용 가능.
    internal : 프로젝트 내의 컴파일 단위의 모듈에서만 사용
    public : 어디서나 사용 가능

    final이 있는 자리는 상속 관련 변경자로 default 값이 final이다
    final은 상속을 막는 키워드고 open은 상속을 허용, 추상 클래스 설정인 abstract가 있다.

    Kotlin에서 final이 기본 설정인 이유에 대해서 공식적인 답변은 없다.
    하지만 "Effective Java" 책에는 쓰여진 내용을 참고할 수 있다.
    상속은 캡슐화를 깨트리게 되며, 상위 클래스에 의존적이여서 결합도가 높아진다.
    관련 토론 글

  • public constructor(public var argInt: Int)

    Class의 주 생성자이다
    어노테이션이나 접근 제어자를 설정하지 않는다면(public으로 지정한다면) constructor 키워드를 생략할 수 있다.

    주 생성자의 인자 부분인 var argInt: Int의 경우 var 키워드를 생략할 수 있다.
    다만 그럴 경우 생성 메서드의 매개 변수로만 쓰이는, 클래스의 멤버 변수로는 쓰이지 않는다는 뜻이다.
    따라서 init 블럭이나 멤버 변수의 초기화 과정에서는 쓰일 수 있으나 클래스 내의 함수에서는 사용하지 못한다.
    val이나 var을 작성하면 클래스 내에 멤버 변수로써 속성을 선언하게 된다

  class User(val id: Long, email: String) {
      val hasEmail = email.isNotBlank()    //email can be accessed here
      init {
          //email can be accessed here
      }

      fun getEmail(){
          //email can't be accessed here
      }
  }
  • : Any()

    : 콜론 키워드 뒤에는 상속 또는 구현할 클래스를 지정한다
    Any class는 모든 클래스의 가장 상위 클래스로 자바에서의 Object와 같다
    보다 자세한 내용은 아래 참조


2번 ~ 8번 라인

  • var argStr: String

    클래스의 멤버변수(property)로 선언한다
    기본적으로 생성과 같이 초기화를 해야하는데 init 블럭에서 초기화를 하거나 lateinit을 통해 나중에 초기화가 가능하다

    lateinit 변수는 나중에 초기화되므로 val을 사용할 수 없다.
    초기화 이전에 접근하면 런타임 에러가 난다. ::변수명.isInitialized를 사용해 초기화 여부 확인 가능
    Int, Long 등과 같은 기본 유형 속성에는 lateinit을 사용할 수 없다.
    lazy의 경우 val이 가능하다

  • get() { ... } set(value) { ... }

    kotlin에서는 java로 컴파일 시 멤버 변수의 캡슐화를 자동으로 진행한다.
    변수는 private으로 하고, 해당 getter와 setter를 자동으로 만들어 준다.
    val 변수는 값 변경이 불가하니 get만 자동 생성되고, private으로 설정 시 둘 다 생성되지 않는다.

    그래서 get과 set을 추가적인 로직이나 접근 제어를 변경하는 등 커스텀 할 수 있다.

    자주 사용되는 예시는 다음과 같다.

  private val _liveData = MutableLiveData<String>()
  val liveData: LiveData<String> get() = _liveData

10번 ~ 17번 라인

  • init { ... }
    init 블럭은 보통 class 내 상단부분에 넣지만, 중간에 넣어도 되며, 여러개를 넣어도 생성시 전부 호출

  • constructor(argInt: Int, argStr: String): this(argInt) { ... }
    예시와 같이 보조 생성자들은 주 생성자를 호출해야 한다

    class에는 위와 같이 property (argStr), init, constructor를 통해 초기화를 진행한다.
    이때 진행하는 순서는 다음과 같다

    1. 호출한 생성 메서드의 매개변수에 대한 값 할당 수행
    2. 주 생성자가 아니라면 나올 때까지 반복
    3. 상단에서부터 차례대로 property와 init 블록들 실행
    4. 생성자에 대한 코드 블럭 내부를 역순으로(스택처럼) 수행
  class Test{
      constructor(){
          println("set primary constructor")
      }
      constructor(msg: String): this(){
          println("set secondary constructor")
      }

      val a = println("set property 1")
      init{
          println("set init 1")
      }

      val b = println("set property 2")
      init{
          println("set init 2")
      }
  }

  fun main(){
      Test("")
  }
  // prints
  set property 1
  set init 1
  set property 2
  set init 2
  set primary constructor
  set secondary constructor

따라서 처음에 나온 18줄의 코드를 아래의 한 줄짜리 코드로 표현할 수 있다.


상속

Kotlin의 Class는 default로 final이라서 open 키워드를 선언해야 상속할 수 있다.
내부 메서드 및 필드도 마찬가지로 open 키워드를 선언해야한다.
final이 default인 이유는 위 내용 참조

is-a, has-a, implements

클래스의 계층 구조를 만드는 기법은 다음이 있다.

  • 상속관계 is-a : 슈퍼 클래스와 서브 클래스가 하나로 묶여서 사용.
  • 연관관계 has-a : 클래스를 사용하는 관계, 클래스 내부에 다른 클래스를 속성으로 처리.
  • 구현관계 implements : 인터페이스, 추상 클래스를 구현

상속관계
open 키워드를 제외하곤 Java와 유사하다

open class Base {
    open fun baseOpenFun() { ... }
    fun baseNotOpenFun() { ... }
}

class Derived() : Base() {
    override fun baseOpenFun() { ... }
}

연관관계, Composition
kotlin의 의도에 맞게 상속보다 연관관계로 계층 구조를 풀어나가는 것이 대부분의 경우 좋다

방법으로는 Composition, 즉 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 메서드를 호출하는 기법

  • 해당 인스턴스의 내부 구현이 바뀌더라도 영향을 받지 않는다.
  • 또한, 다른객체의 인스턴스이므로 인터페이스를 이용하면 Type을 바꿀 수 있다
class Progress {
    fun showProgress() { ... }
    fun hideProgress() { ... }
}

class ImageLoader(private val progress: Progress) {
    fun load() {
        progress.showProgress()
        ...
        progress.hideProgress()
    }
}

구현관계
자바에서는 클래스의 상속과 인터페이스의 구현을 extends 와 implements로 구분하지만, 코틀린에서는 이를 구분하지 않고 콜론(:) 뒤에 상속한 클래스나 구현한 인터페이스를 표기

interface Clickable {
    fun click()
    fun showOff() = println("Clickable showoff") //디폴트 메소드
}
class Button : Clickable { 
    override fun click() { 
    	...
    }
}

확장 (Extension)

기존 클래스를 수정하지 않고 새로운 기능을 추가하는 방법
확장되는 유형을 참조하는 수신자 유형을 해당 이름 앞에 붙인다

fun String.hello() : String {
    return "Hello, $this"
}
fun main(args: Array<String>) {
    val whom = "cwdoh"
    println(whom.hello()) // print: Hello, cwdoh
}

Extension은 다음의 규칙을 가진다

  • 정적으로 처리된다.
  • Extension 보다 멤버가 우선이다.
  • 범위(Scope)를 가진다.

  • 정적으로 처리된다.
    Extension은 확장하는 클래스를 실제로 수정하지 않는다.
    확장을 정의하면 새 멤버를 클래스에 삽입하는 것이 아니라 새 함수를 호출할 수 있게 만들 뿐, 확장 기능은 정적으로 전달된다.
    따라서 어떤 확장 함수가 호출되는지는 수신자 유형에 따라 컴파일 타임에 이미 확정된다.
  open class Shape
  class Rectangle: Shape()

  fun Shape.getName() = "Shape"
  fun Rectangle.getName() = "Rectangle"

  fun printClassName(s: Shape) {
      println(s.getName())
  }

  printClassName(Rectangle()) // print: Shape

      호출된 확장 함수가 매개변수의 선언된 유형에만 의존하기 때문에 Shape 를 print 한다


  • Extension 보다 멤버가 우선이다.
    동일한 유형의 메서드가 Class의 멤버로 있다면 멤버가 우선된다
  class Example {
      fun printFunctionType() { 
          println("Class method") 
      }
  }

  fun Example.printFunctionType() { 
      println("Extension function") 
  }

  Example().printFunctionType() // print: Class method
  • 범위(Scope)를 가진다.
    대부분의 경우 패키지 바로 아래 최상위 수준에서 확장을 정의한다.
  class Host(val hostname: String) {
      fun printHostname() { print(hostname) }
  }

  class Connection(val host: Host, val port: Int) {
      fun printPort() { print(port) }

      // 확장함수
      fun Host.printConnectionString() {
          printHostname()   // calls Host.printHostname()
          print(":")
          printPort()   // calls Connection.printPort()
      }

      fun connect() {
          /*...*/
          host.printConnectionString()   // calls the extension function
      }
  }

  fun main() {
      Connection(Host("kotl.in"), 443).connect() // print: kotl.in:443
      //Host("kotl.in").printConnectionString()  // error, the extension function is unavailable outside Connection
  }

Nullable 확장함수 정의
안전호출연산자로 nullable한 class를 확장할 수 있다

class Person(val name: String)

fun Person?.getName() {
    this?.let { println(it.name) } ?: println("null")
}

fun main() {
    val p1: Person? = null
    p1.getName() // print: null
}

확장 속성
확장 함수를 지원하는 것처럼 확장 속성을 지원한다.

val <T> List<T>.lastIndex: Int
    get() = size - 1

클래스 종류

Data Class

class 앞에 data 를 붙여 데이터 보관 목적으로 클래스를 생성할 수 있다.

data class User(val name: String, val age: Int)

특징은 다음과 같다

  • 주 생성자(primary constructor)는 1개 이상의 프로퍼티를 선언되어야 한다.
  • 생성자 프로퍼티는 val 또는 var으로 선언해야 한다.
  • 데이터 클래스에 abstract open sealed inner 를 붙일 수 없다.
  • 데이터 클래스는 상속받을 수 없다.
  • equals() hashCode() toString() componentN() copy()를 자동으로 구성해준다.
    해당 메서드들은 주 생성자에 의존한다

Data Class가 상속이 안되는 이유로는 equals()를 제대로 정의할 수 없기 때문이다

Sealed Class

sealed 클래스는 추상 클래스로, 상속받는 서브 클래스의 종류를 제한할 수 있다.
특징으로는 다음과 같다

  • sealed 클래스의 서브 클래스들은 반드시 같은 파일 내에 선언되어야 함
    단, sealed 클래스의 서브 클래스를 상속한 클래스들은 같은 파일 내에 없어도 됨
  • 기본적으로 abstract 클래스임
  • protected(default) 또는 private 생성자만 갖게 됨
sealed interface Error

sealed class IOError(): Error

class FileReadError(val file: File): IOError()
class DatabaseError(val source: DataSource): IOError()

object RuntimeError : Error

상속하는 타입이 정해져 있기 때문에 다음과 같은 이점을 가진다.

fun log(e: Error) = when(e) {
    is FileReadError -> { println("Error while reading file ${e.file}") }
    is DatabaseError -> { println("Error while reading from database ${e.source}") }
    is RuntimeError ->  { println("Runtime error") }
    // the `else` clause is not required because all the cases are covered
}

Inner and Nested Class

Class 안에 Class를 정의한 형태
inner 키워드를 선언하지 않으면 기본이 중첩 클래스
이너 클래스 내부에서는 외부 클래스의 속성에 접근 가능.

class Outer {
    private val bar: Int = 1
    class Nested {
        fun foo() = 2
    }
}
val demo = Outer.Nested().foo() // == 2



class Outer {
    private val bar: Int = 1
    inner class Inner {
        fun foo() = bar
    }
}
val demo = Outer().Inner().foo() // == 1

이펙티브 자바와 코틀린 인 액션 책을 참고하면, 자바의 Inner Classes에는 크게 3 가지 문제가 있음을 알 수 있다.

  • Inner classes를 사용할 경우 직렬화에 문제가 있다.
  • Inner classes 내부에 숨겨진 Outer class 정보를 보관하게 되고, 결국 참조를 해지하지 못하는 경우가 생기면 메모리 누수가 생길 수도 있고, 코드를 분석하더라도 이를 찾기 쉽지 않아 해결하지 못하는 경우도 생긴다.
  • Inner classes를 허용하는 자바는 Outer를 참조하지 않아도 기본 inner classes이기 때문에 불필요한 메모리 낭비와 성능 이슈를 야기한다.

Object class

object 정의는 하나의 싱글턴 패턴을 만드는 방법이다

  • object 정의를 처음으로 사용될 때 메모리에 적재
  • 생성자가 필요 없음.
  • 별도의 객체를 생성하지 않고, 정의한 이름으로 멤버들을 접근해서 사용.
  • 클래스 상속, 인터페이스 구현이 가능함.
fun main(args: Array<String>) {
    Counter.increment()
    println(Counter.count)
}

object Counter {
    var count: Int = 0
        private set
    
    fun increment() = ++count
}

companion object

  • 클래스와 동반 객체는 하나처럼 움직이도록 구성되어 있음.
    • 다양한 기능을 클래스 이름으로 처리 가능.
    • 동반객체는 클래스 내 하나만 생성 가능.
  • 이너 클래스나, 중첩 클래스에서도 동반객체를 사용시 해당 멤버를 참조해서 사용 가능.
fun main() {
    CompanionClass.test() // 동반 객체 선언 : 2
    ObjectClass.ObjectTest.test() // object 선언 : 1
}

class ObjectClass {
    object ObjectTest { // 싱글턴 객체
        const val CONST_STRING = "1"
        fun test() { println("object 선언 : $CONST_STRING") }
    }
}

class CompanionClass {
    companion object { // 동반
        const val CONST_TEST = 2
        fun test() { println("동반 객체 선언 $CONST_TEST") }
    }
}
profile
세상 제일 이제일

0개의 댓글