educative - kotlin - 7

Sung Jun Jin·2021년 3월 21일
0

Objects and Singletons

코틀린에서 object 키워드와 {} 를 사용하면 바로 anonymous object(익명 객체)를 생성할 수 있다.

val circle = object {
    val x = 10
    val y = 10
    val radius = 30
}

여기서 더 기능을 확장하려면 익명 객체보다 클래스를 설계하는게 더 좋다.

Why
익명 객체는 클래스와 비교해서 다음과 같은 한계점들이 있다

  • 익명 객체의 내부 타입은 함수나 메소드의 리턴 타입이 될 수 없다
  • 익명 객체의 내부 타입은 매개변수로 이용할 수 없다
  • 클래스 내부에 익명객체가 존재한다면 Any 타입으로 간주되며 객체의 프로퍼티나 메소드에 직접적인 접근이 불가능하다

따라서 위와같은 익명 객체 표현식은 x, y radius 처럼 관련있는 지역 변수들을 묶어줄때 유용하다.

익명 객체 표현식으로 인터페이스를 상속받을 수 있다.

object: <상속받고자 하는 인터페이스> {}

fun createRunnable(): Runnable {
  val runnable = object: Runnable {
    override fun run() { println("You called...") }
  }
  
  return runnable
}

val aRunnable = createRunnable() // 변수에 담하서 호출 가능
aRunnable.run() //You called...

추상 메소드가 하나인 인터페이스(함수형 인터페이스 SAM)를 상속받으면 메소드 이름과 return 키워드를 생략해도 된다.

fun createRunnable(): Runnable = Runnable { println("You called...") }

createRunnable().run() // You called...

만약 1개 이상의 인터페이스를 상속받는다고 하면 : 옆에 인스턴스 타입을 명시해야한다.

fun createRunnable(): Runnable = object: Runnable, AutoCloseable { 
    override fun run() { println("You called...") }
    override fun close() { println("closing...") } 
}

val runnable = createRunnable()
runnable.run() // You called...
runnable.close() // error: unresolved reference: close

리턴 타입을 어느 익명객체로 지정해주느냐의 따라서 createRunnable() 함수 밖에서 내부 인터페이스에 대한 접근이 가능하다. 위와 같은 경우에는 Runnable 객체를 리턴 타입으로 지정해두었기 때문에 함수 밖에서 Runnable 객체만 접근 가능하다.

Singletons with object declaration

object {} 표현식을 사용해서 싱글톤을 만들 수 있다. 코틀린에서 싱글톤은 대표적으로 Unit 클래스가 있다.

object Util {
  fun numberOfProcessors() = Runtime.getRuntime().availableProcessors()
}

println(Util.numberOfProcessors()) // 12

또한 싱글톤 내부에서 메소드 뿐만이 아닌 val, var 키워드로 프로퍼티 생성도 가능하다. 인터페이스나 클래스를 상속받는거도 가능하다. 함수의 인자 전달 또한 가능하다.

object Sun : Runnable {
  val radiusInKM = 696000
  var coreTemperatureInC = 15000000
  
  override fun run() { println("spin...") }
}

fun moveIt(runnable: Runnable) {
  runnable.run()
}               

println(Sun.radiusInKM) //696000

moveIt(Sun)  //spin...

하지만 위의 var coreTemperatureInC 처럼 싱글톤 객체에서 mutable한 변수를 사용하는건 딱히 좋은 방법이 아니다 (특히 멀티 쓰레드 어플리케이션에서는)

Top-level functions vs. singletons

코틀린에서 Top-level function은 패키지 내에서 직접적으로 선언된 함수를 의미한다. 코틀린은 java의 static을 지원하지 않는 대신 top-level function을 사용해 같은 효과를 낼 수 있다.

package com.agiledeveloper.util

fun unitsSupported() = listOf("Metric", "Imperial") // Top-level function

fun precision(): Int = throw RuntimeException("Not implemented yet") //Top-level function

object Temperature {
  fun c2f(c: Double) = c * 9.0/5 + 32
  fun f2c(f: Double) = (f - 32) * 5.0/ 9
}

object Distance {
  fun milesToKm(miles: Double) = miles * 1.609344
  fun kmToMiles(km: Double) = km / 1.609344
}

먼저 package 키워드를 사용해 패키지를 정의해준다. 그 다음에 unitsSupported(), precision() 이라는 이름의 top-level function과 밑에 Temperature, Distance라는 싱글톤을 정의해준다.

이제 위에 패키지를 아래와 같은 방식으로 import 해서 사용할 수 있다.

import com.agiledeveloper.util.*
import come.agiledeveloper.util.Temperature.c2f

fun main() {
  println(unitsSupported()) // [Metric, Imperial]
  println(Temperature.f2c(75.253)) // 24.029444444444444
  println(c2f(24.305)) // 75.749
}

만약 위와 같이 공통 모듈을 만드는데 모듈안에 있는 함수가 공통적으로 넓은 범위에서 사용된다고 하면 top-level function으로 바로 정의해주면 좋다. 반면 2개이상의 서로 연관이 있고 비슷한 역할을 하는 함수는 하나의 싱글톤 내에서 묶어놓고 해당 싱글톤 객체를 import해서 사용하면 직관적이고 편하다.

Creating Classes

class 키워드를 사용해 클래스를 생성해준다.

class Car(val yearOfMake: Int)

여기까지만 해주면 내부적으로 생성자와 함게 yearOfMake라는 int형 프로퍼티와 getter() 까지 정의해주는 코드가 된다.

인스턴스를 생성해주는 방법은 다음과 같다. 마찬가지로 내부 필드 yearOfMake val로 선언되어 있으므로 값의 직접적인 변경이 불가능하다.

val car = Car(2019) 
println(car.yearOfMake) //2019

car.yearOfMake = 2019 //ERROR: val cannot be reassigned

똑같이 var로 선언된 필드는 mutable 하다

class Car(val yearOfMake: Int, var color: String)

val car = Car(2019, "Red")
car.color = "Green"
println(car.color) //GREEN

코틀린에서 클래스 내부의 변수 선언은 자동적으로 property로 선언된다. 위에 있는 car.yearOfMake는 실제로 car.getYearOfMake()이다. Car 클래스를 자바 바이트코드로 컴파일해보면 다음과 같은 구조가 나타난다. 이런식으로 내부 변수들에 대한 접근은 내부에서 간접적으로 constructor, getter와 setter를 통해 이루어진다.

Compiled from "Car.kt" 
public final class Car {
  private final int yearOfMake;
  private java.lang.String color;
  public final int getYearOfMake(); Getter
  public final java.lang.String getColor();  Getter
  public final void setColor(java.lang.String);  Setter
  public Car(int, java.lang.String); Constructor
}

Controlling changes to properties

Getter와 Setter를 특정한 연산을 수행할 수 있게 커스터마이징 할 수 있다. set(), get() 을 통해 사용해준다. color라는 프로퍼티에 empty string("") 값이 세팅되는것을 막아주는 커스텀 setter를 만들어보자. 관례적으로 setter의 매개변수의 이름은 value로 사용된다고 한다.

class Car(val yearOfMake: Int, theColor: String) {
  var fuelLevel = 100
  
  var color = theColor
    set(value) {
      if (value.isBlank()) {
        throw RuntimeException("no empty, please")
      }

      field = value
    }
}

코틀린에서 클래스 내부 필드는 모두 프로퍼티로 선언되기 때문에 필드를 직접적으로 정의할 순 없다. 따라서 커스텀 setter 내부에서 마지막에 field 키워드를 사용해 color 프로퍼티에 접근 해 값을 세팅해준다 이를 뒷받침필드(Backing Field)라고 한다.

이제 커스템 setter를 테스트해보자.

val car = Car(2019, "Red")
car.color = "Green"

println(car.color) //Green

try {
    car.color = ""
} catch(ex: Exception) {
    println(ex.message) //no empty, please
}

Access modifiers

코틀린에서 클래스 내부의 프로퍼티와 메소드는 기본적으로 public이다. 코틀린에서 지원하는 접근 제한자(access modifiers)는 다음과 같다

  • public : 어디에서나 접근 가능 (default)
  • private : 해당 파일, 혹은 클래스 내에서만 접근 가능
  • protected : private과 같지만 같은 파일이 아니더라도 자식 클래스에서 접근 가능
  • internal : 같은 모듈 내에서 접근 가능

사용은 다음과 같이 해주면 된다. set() 메소드의 value 매개변수는 생략가능하다

var fuelLevel = 100 
  private set

Primary, Secondary Constructors

코틀린의 클래스 생성자는 기본 생성자(Primary Constructor)와 보조 생성자(Secondary Constructor)로 구성되어 있다.

class Person(val first: String, val last: String) {
  var fulltime = true
  var location: String = "-"
  
  // Secondary Constructor 1
  constructor(first: String, last: String, fte: Boolean): this(first, last) {
    fulltime = fte
  }
  
    // Secondary Constructor 2
  constructor(
    first: String, last: String, loc: String): this(first, last, false) {
    location = loc
  }
  
  override fun toString() = "$first $last $fulltime $location"
}

기본 생성자는 클래스 이름 오른편 괄호안에 정의해줄 수 있다. 여기서는 문자열의 first, last이다. 여기서 val, var와 같은 어노테이션이나 접근 제한자(public, private)가지고 있지 않다면 위와 같이 constructor 키워드 생략이 가능하다.

보조 생성자는 constructor 키워드를 사용해 정의한다 (생략불가). 첫번째 보조 생성자는 반드시 this()를 사용해 기본 생성자를 호출 받아야 한다.

Initialization code

파이썬의 init()은 기본 생성자(primary constructor) 호출 직후 바로 실행되는 코드블럭이다. 기 본 생성자 매개변수를 초기화 시키는 역할을 한다. 이 메소드 내에서는 위에서 정의한 클래스 내부 프로퍼티에 대한 접근이 가능하다.

class Car(val yearOfMake: Int, theColor: String) {
  var fuelLevel = 100
    private set

  var color = theColor
    set(value) {
      if (value.isBlank()) {
        throw RuntimeException("no empty, please")
      }

      field = value
    }
// init 블록 사용
  init {
    if (yearOfMake < 2020) { fuelLevel = 90 }
  } 
}

이렇게 사용해도 되고 아예 맨 위에서 바로 fuelLevel 프로퍼티 정의를 해줘도 좋다. init 블록을 한개 이상 사용하지 말자. 생성자에서의 복잡도를 최소화 하는것이 프로그램 안정성 측면에서도 좋다.

class Car(val yearOfMake: Int, theColor: String) {
var fuelLevel = if (yearOfMake < 20) 90 else 100
	private set

Defining instance methods

클래스 내부에서 메소드를 정의할때는 똑같이 접근 제한자와 함께 fun 키워드를 사용한다. 접근 제한자의 default는 public이다..

class Person(val first: String, val last: String) {
  
  internal fun fullName() = "$last, $first"
  
  private fun yearsOfService(): Int = 
    throw RuntimeException("Not implemented yet")
}

val jane = Person("Jane", "Doe")
println(jane.fullName()) //Doe, Jane
//jane.yearsOfService() //ERROR: cannot access...private in 'Person'

여기서 정의된 fullName() 메소드는 내부 모듈에서 호출이 가능하지만 private method로 정의된 yearsOfService()는 클래스 외부에서 호출이 불가능하기 때문에 위와 같은 에러가 발생한다.

Inline classes

비즈니스 로직을 작성할때 어떤 타입의 wrapper를 작성할 때가 있다. 예를들어 SSN과 같은 주민등록번호는 String으로 표현할 수 있지만 SSN이라는 클래스를 만들어 관리하는게 좀 더 직관적이다. 하지만 이는 퍼포먼스적인 측면에서 부담스러울 수 있다. 이때 inline class를 사용하면 런타임에서 primitive 타입을 사용하는것과 같은 효과를 줄 수 있다. 실제로 바이트코드로 컴파일될때 inline class는 primitive 타입으로 간주된다.

inline class SSN(val id: String)

fun receiveSSN(ssn: SSN) {
  println("Received $ssn")
}

Class-level members

만약 클래스의 인스턴스없이 어떤 클래스 내부에 접근하고 싶다면 클래스 내부에서 객체를 선언할때 companion 키워드를 붙힌 object를 선언하면 된다. Java의 static 멤버 변수나 함수를 선언하는것과 같은 효과를 준다. 하지만 실제로 static한것은 아니다. companion object는 런타임 환경에서 실제 객체의 인스턴스로 실행된다고 한다

하지만 companion 객체 내부에 mutable한 프로퍼티를 사용한다면 thread-safety 이슈가 생길 수 있다.

class MachineOperator(val name: String) {
  fun checkin() = checkedIn++
  fun checkout() = checkedIn--
  
  companion object {
    var checkedIn = 0
    
    fun minimumBreak() = "15 minutes every 2 hours"
  }
}

// 인스턴스 없이 바로 호출 가능
MachineOperator("Mater").checkin()
println(MachineOperator.minimumBreak()) //15 minutes every 2 hours
println(MachineOperator.checkedIn) //1

위처럼 companion object 옆에 이름이 따로 없다면 Companion 키워드를 사용해 바로 접근이 가능하다.

// companion.kts
val ref = MachineOperator.Companion

ref.minimumBreak() // 15 minutes every 2 hours

하지만 아래처럼 companion object 옆에 따로 이름이 있다면 다음과 같이 사용해준다.

//companion object {
companion object MachineOperatorFactory { 
  var checkedIn = 0

val ref = MachineOperator.MachineOperatorFactory //직관성을 위해 Factory suffix를 사용해주는게 좋다 

Companion objects factories

companion object를 객체 생성을 대신 수행해주는 팩토리로 활용할 수 있다.

class MachineOperator private constructor(val name: String) { 
    //...
    companion object { 
        //...
        fun create(name: String): MachineOperator { 
            val instance = MachineOperator(name) 
            instance.checkin()
            return instance
        } 
    }
}

MachineOperator 클래스의 생성자를 private로 정의해 클래스 내부에서만 인스턴스가 생성되게 해준다. 이후 create() 메소드 내에서 checkin() 메소드를 실행시킨 이후에 생성된 인스턴스를 return 해주면 factory의 역할을 수행할 수 있다.

따라서 인스턴스를 생성하려면 위에서 정의한 create() 통해서만 생성되게 할 수 있다.

val operator = MachineOperator.create("Mater")
println(MachineOperator.checkedIn) //1

Generic classes

PriorityPair라는 제네릭 클래스를 만들어보자. feature는 다음과 같다

  • 코틀린 Pair와 비슷한 한 쌍의 object를 가지고 있다
  • compareTo() 메소드를 통해 첫번째 객체는 두번째 객체보다 크도록 정렬한다.
class PriorityPair<T: Comparable<T>>(member1: T, member2: T) {
  val first: T
  val second: T
  
  init {
    if (member1 >= member2) {
      first = member1
      second = member2
    } else {
      first = member2
      second = member1
    }
  }                                               
  
  override fun toString() = "${first}, ${second}"
}

Data classes

데이터 클래스는 데이터 보관을 목적으로 만든 클래스이다. 코틀린 데이터 클래스는 자동적으로 equals(), hashCode(), toString(), copy() 메소드를 생성해 boilerplate code를 만들지 않아도 된다. 데이터 클래스는 클래스 앞에 data를 붙여준다.

특징은 다음과 같다

  • 데이터 클래스의 생성자(primary constructor)는 1개 이상의 프로퍼티를 선언되어야 합니다.
  • 데이터 클래스의 생성자 프로퍼티는 val 또는 var으로 선언해야 합니다.
  • 데이터 클래스에 abstract, open, sealed, inner 를 붙일 수 없습니다.
  • 클래스에서 toString(), hashCode(), equals(), copy()를 override하면, 그 함수는 직접 구현된 코드를 사용합니다.
  • 데이터 클래스는 상속받을 수 없습니다.
data class Task(val id: Int, val name: String, 
  val completed: Boolean, val assigned: Boolean)
  
val task1 = Task(1, "Create Project", false, true)

println(task1)
//Task(id=1, name=Create Project, completed=false, assigned=true)
println("Name: ${task1.name}") //Name: Create Project

println()을 통해 알아서 생성된 toString()을 호출보면 기본 생성자에 있는 프로퍼티들을 (id, name, completed, assigned) 리스팅해주는 식으로 구현된걸 확인할 수 있다.

데이터 클래스에서 생성해주는 copy() 메소드는 객체의 복사본을 만들어 리턴한다. 인자로 생성자에 정의된 프로퍼티를 넘길 수 있다. 그 외에 나머지 값은 동일한 객체가 생성된다.

val task1 = Task(1, "Create Project", false, true)

val task1Completed = task1.copy(completed = true, assigned = false)
println(task1Completed) //Task(id=1, name=Create Project, completed=true, assigned=false)

위 처럼 인자로 넘겨진 completed, assigned 이외에는 모두 동일한 값이 복사된걸 확인할 수 있다.

또한 아래와 같은 식으로 프로퍼티 순서대로 destructuring도 가능하다.

val (id, _, _, isAssigned) = task1
println("Id: $id Assigned: $isAssigned") //Id: 1 Assigned: true

When to use data classes?

  • 데이터를 모델링할때
  • equals(), hashcode(), toString(), copy() 메소드를 오버라이딩 하거나 필요할때
  • 기본 생성자에 하나 이상의 프로퍼티를 필요로 할때, 혹은 프로퍼티만 있어도 될 때
  • 객체로부터 쉽게 데이터를 가져오고 싶을 때 (destructuring 활용)
profile
주니어 개발쟈🤦‍♂️

0개의 댓글