코틀린 입문 스터디 (11) Object-oriented Programming

mook2_y2
27 min readMar 4, 2019

--

스터디파이 코틀린 입문 스터디 (https://studypie.co/ko/course/kotlin_beginner) 관련 자료입니다.

코틀린 입문반은 Kotlin을 직접 개발한 개발자가 진행하는 Coursera 강좌인 “Kotlin for Java Developers” (https://www.coursera.org/learn/kotlin-for-java-developers) 를 기반으로 진행되며 아래는 본 강좌 요약 및 관련 추가 자료 정리입니다.

목차

(1) Introduction

(2) From Java to Kotlin

(3) Basics

(4) Control Structures

(5) Extensions

(6) 실습 : Mastermind game

(7) Nullability

(8) Functional Programming

(9) 실습 : Mastermind in a functional style, Nice String, Taxi Park

(10) Properties

(11) Object-oriented Programming

(12) Conventions

(13) 실습 : Rationals, Board

(14) Inline functions

(15) Sequences

(16) Lambda with Receiver

(17) Types

(18) 실습 : Game 2048 & Game of Fifteen

1. OOP in Kotlin

  • OOP와 관련한 가시성 (visibility), 상속 (Inheritance)과 오버라이딩 (overriding), 디렉토리 구조를 Kotlin에서 어떻게 처리하는지에 대해 다룹니다. Java와 비교해 새롭게 도입된 개념은 없으며 실용적 측면에서 개선점들이 있습니다.
  • 가시성 (visibility)은 클래스 멤버 (필드, 메소드)의 사용 범위를 결정하는 것을 뜻합니다. Java에는 public, protected, package private, private 의 4가지 수준이 존재하며, 디폴트는 package private로 이에 대한 접근 제어자 (access modifier) 키워드는 없습니다. (관련 링크 : Java 가시성의 개념) 한편, Kotlin은 public, protected, internal, private의 4가지 수준이 존재하며, 디폴트는 public입니다. Kotlin에는 기존 Java의 private package 가시성이 없으며, 동일 모듈 내에서 접근 가능한 internal 개념이 추가되었습니다. (관련 링크 : 커니의 코틀린::클래스 및 인터페이스 중 접근 제한자 부분)
  • 상속 (Inheritance)은 OOP에서 “is/kind of” 관계 (ex: 개 (자식클래스)는 동물 (부모클래스)이다.) 를 표현하는 개념입니다. (관련 링크 : 나무위키-상속(프로그래밍) ) 오버라이딩 (overriding)은 자식클래스가 상속받은 부모클래스의 메소드를 특정한 형태로 재구현하는 것을 뜻합니다. (관련 링크 : 위키피디아 — 메소드 오버라이딩) 이에 대해 Java는 디폴트는 open (오버라이딩/상속 가능)이며, final 키워드를 사용하는 경우 오버라이딩/상속 할 수 없도록 제한할 수 있습니다. 한편, Kotlin은 디폴트는 final (오버라이딩/상속 불가) 이며, open 키워드를 사용하는 경우 오버라이딩/상속이 가능하도록 허용할 수 있습니다.
  • Java에서는 오버라이딩시에 @Override 어노테이션을 사용하며 필수는 아니지만, Kotlin에서는 오버라이딩시에 override 키워드를 사용하고 의무적으로 선언하는 것을 강제합니다.
  • 디폴트 설정 (제어자(modifier)를 선언하지 않은 경우의 기본 설정)을 무엇으로 할지는 중요한 문제이며 Kotlin이 가시성과 상속/오버라이딩 가능 여부에 대해 Java와 다른 디폴트 설정을 둔 (package private -> public, open -> final) 의도는 가장 일반적으로 사용되며, 가능한 많은 기능을 사용할 수 있고, 리스크가 적은 설정 수준이 가시성은 public, 상속/오버라이딩은 final이라 판단했기 때문입니다. 특히 상속/오버라이딩 가능 여부의 경우 라이브러리 개발자 입장에서 이미 출시한 코드를 open에서 final로 변경하는 것은 해당 라이브러리 기반으로 작성된 코드에 영향을 주므로 수정하기 어렵지만, final에서 open으로 변경하는 것은 상대적으로 문제가 적습니다.

2. Constructors, Inheritance syntax

Constructors

  • Kotlin은 단순한 프로퍼티와 단순한 로직의 단일 주 생성자 (primary constructor)를 가진 클래스를 정의할 때 아래와 같이 간결한 문법으로 정의할 수 있습니다. Java 에서는 직접 모두 작성해야했던 다소 귀찮은 단순 코드를 생략할 수 있도록 해줍니다.
// name과 age 프로퍼티를 생성시에 초기화하는 클래스에 대한 Kotlin 문법
class Person(val name:String, var age:Int)
// 위 Kotlin 문법에 대응되는 Java 코드
// 위에서 배운 프로퍼티 accessor에 대한 구현과 생성자 구현을 모두 직접해야 함
public final class Person {
private final String name;
private int age;
public final String getName() { return this.name;} public final int getAge() { return this.age; } public final void setAge(int age) { this.age = age; } public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
  • 보다 복잡한 생성자 로직이 필요한 경우, 클래스명 뒤 괄호에 val/var키워드를 빼고, init { } 을 통해 생성자 부분을 커스터마이징할 수 있습니다. 또한, 생성자의 가시성을 디폴트인 public이 아닌 다른 것으로 변경하고자 하는 경우 클래스명과 괄호 사이에 <가시성 접근제어자> constructor 를 써서 변경할 수 있습니다.
class Person(val name:String, var age:Int)// 위 코드와 동일한 의미를 가지며 생성자 로직을 커스터마이징할 수 있는 문법
class Person(name:String, age:Int){
val name:String
var age:Int
init {
this.name = name
this.age = age
}
}
// 위 코드와 동일한 의미를 가지나, 생성자에 대한 가시성을 변경하는 문법
class Person
internal constructor(name: String, age:Int){
val name:String
var age:Int
init {
this.name = name
this.age = age
}
}
  • 생성자 오버로딩 (생성자 매개변수의 유형과 개수를 다르게 하는 여러 생성자를 구현하는 것) 이 필요한 경우 클래스 내부에 constructor 를 통해 Secondary Constructor를 정의할 수 있습니다. 단, secondary constructor 는 반드시 Primary constructor 또는 다른 secondary constructor를 호출해야 합니다.
// 위 코드 블록 3번째 코드에 secondary constructor를 구현한 예시 
class Person
internal constructor(name: String, age:Int){
val name:String
var age:Int
init {
this.name = name
this.age = age
}

constructor(name: String, birthYear:Int, nowYear:Int) : this (name, nowYear-birthYear+1)
}
// 아래와 같이 상황에 따라 다른 매개변수 유형과 개수로 객체 생성 가능
val person1 = Person("kevin", 28)
val person2 = Person("kevin", birthYear = 1992, nowYear = 2001)

Inheritance Syntax

  • 앞서 언급한대로 Kotlin에서 상속 가능 여부에 대한 디폴트 설정은 final 이므로, 부모클래스로 사용하기 위해서는 class 앞에 open 제어자를 선언해야 합니다.
  • Java에서는 부모클래스 상속시에는 extends , 인터페이스 구현시에는 implements 로 서로 다른 문법을 사용합니다. 하지만 Kotlin에서는 부모클래스 상속과 인터페이스 구현 모두 : 라는 동일한 문법을 사용합니다.
  • 자식클래스에서 부모클래스의 멤버에 접근하기 위해 super 키워드를 사용합니다.
interface Base
class BaseImple : Base // 인터페이스 구현하는 경우
open class Parent(val name: String){ // 부모클래스 사용을 위해 open 선언
open fun getAddress():String{ return TODO() }
class Child(name: String): Parent(name) // 부모클래스 상속하는 경우
class Child: Parent {
// 자식클래스에 secondary constructor를 정의하며 부모클래스 생성자 호출
constructor(name: String, param: Int) : super(name)
// 자식클래스에서 부모클래스 멤버 메소드 오버라이딩하며 부모클래스의 것 호출
override fun getAddress(): String { return super.getAddress() }
}

3. NPE during initialization 실습 코드 작성시 고려할 점

  • Java에서 상속관계인 클래스들의 생성자 호출 순서는 자식클래스 생성자 호출시 우선 부모클래스 생성자가 호출되며 부모클래스 생성자 내부 로직 실행 -> 자식 클래스 생성자 내부 로직 실행의 순서로 실행됩니다. (관련 링크 : Java 상속관계 생성자 호출 순서) Kotlin 역시 이와 동일하게 동작합니다.
open class A(open val value: String) {
init {
println(value.length)
}
}
class B(override val value: String) : A(value)fun main(args: Array<String>) {
B("a")
}
  • 한편 부모클래스를 상속하는 과정에서 위 코드와 같이 프로퍼티 부분에 override val/override var 를 선언할 경우, 자식클래스에서 추가적인 필드를 생성하며 (변수명은 같지만 다른 reference) 연관된 accessor를 오버라이드하여 자식클래스에 생성한 필드에 대해 동작하도록 구현됩니다.
// 위 Kotlin 코드가 실제로 동작하는 방식 (Java 코드)
public class A {
private final String value;
public String getValue() { return this.value;}public A(String value) {
this.value = value;
System.out.println(this.getValue().length());
}
}
public final class B extends A {
private final String value;
@Override
public String getValue() { return this.value; }
public B(String value) {
super(value);
this.value = value;
}
}
  • B 객체를 생성하는 과정에서 B 클래스 생성자가 호출되고, super(value); 로 인해 A클래스 생성자가 호출됩니다. 이 때 this.value = value;라인은 오버라이딩 되지 않았으므로 A 클래스 value 필드를 초기화합니다. 한편, 그 이후 System.out.println(this.getValue().length());라인이 수행되는데 이 때 getValue() 는 오버라이딩 되었으므로 오버라이드 메소드가 호출되어 B 클래스 value 필드를 역참조하게 되고, 이 필드는 아직 초기화가 되어 있지 않으므로 NullPointerException이 발생합니다. 이는 Kotlin 상에서 NullPointerException을 컴파일 단계에서 잡지 못하는 사례로 다만 역참조 하는 부분에 Accessing non-final property value in constructor 라는 경고 메시지가 노출됩니다.

4. Class modifiers — I, II

  • Kotlin은 빈번하게 사용되는 유스케이스들을 위한 class modifiers를 제공합니다. enum , data , sealed , inner 이며 modifiers를 선언할 경우 컴파일 되는 과정에서 사용되는 목적을 지원하는 메소드를 자동으로 생성하거나 추가적인 제약을 더해줍니다.

Enum class

  • Enum은 Enumeration (목록)을 의미하며, Java와 동일하게 고정된 갯수의 값들로 구성된 목록이 필요할 때 사용합니다.
import Color.*enum class Color{
BLUE, ORANGE, RED
}
fun getDescription(color : Color) =
when (color) {
BLUE -> "cold" // 맨 윗줄 import가 없을 경우 Color.BLUE로 접근한다.
ORANGE -> "mild"
RED -> "hot"
}
  • 위 코드와 같이 Kotlin 에서 Enum 을 다루는 효과적인 방식은 whenexpression 을 사용하는 것입니다.
import Color.*// 색상 목록의 각 색상 항목마다 서로 다른 r, g, b 속성 정보를 가짐 -> 프로퍼티로 구현
enum class Color(val r:Int, val g:Int, val b:Int){
BLUE(0,0,255),
ORANGE(255,165,0),
RED(255,0,0); // 세미콜론을 통해 Enum list와 member list를 구분
// 색상 목록의 각 색상 항목마다 rgb값을 구하는 연산이 필요 -> 메소드로 구현
fun rgb() = (r * 256 + g) * 256 + b
}
fun main(args: Array<String>){
println(BLUE.r)
println(BLUE.rgb())
}
  • 한편 위 코드와 같이 Enum class 정의시 프로퍼티와 메소드를 함께 정의할 수 있습니다. 프로퍼티는 목록의 각 항목마다 특정한 속성 정보를 담는 경우 유용하며, 메소드는 목록의 각 항목에 대해 빈번하게 수행되는 연산이 있는 경우 유용합니다.

Data class

  • Enum class 가 고정된 목록을 다룬다면, Data class는 특정한 구조로 구성된 데이터들을 다룰 때 사용됩니다. 예를 들어, enum class가 적합한 예시는 고정된 갯수로 존재하며 추가/수정이 없는 “색상 목록”이며, data class가 적합한 예시는 구조는 특정하나 빈번히 추가/수정될 수 있는 “연락처 목록”입니다.
  • Data class는 copy() , equals() , hashCode() , toString() 등의 메소드가 컴파일 단계에서 자동으로 생성되어 데이터를 추가/복사하거나, 비교하거나, 조회하는 등 데이터를 다룰 때 빈번하게 사용되는 기능이 지원됩니다.
data class Contact(val name:String, val address:String)fun main(args: Array<String>){
val contact1 = Contact("Kevin", "Seoul")
val contact2 = contact1.copy()
val contact3 = contact1.copy(adress = "Busan")
}
  • copy() 는 매개변수 없이 호출할 경우 동일한 데이터를 복사하여 반환하며, 매개변수를 넣을 경우 해당 부분만 변경한 데이터를 복사하여 반환합니다.
fun main(args: Array<String>){
val contact1 = Contact("Kevin", "Seoul")
val contact2 = contact1.copy()
val contact3 = contact1.copy(adress = "Busan")
}
  • Java 에서 == 비교 연산자는 reference equality를 의미하므로 Reference type 객체 구성 내용을 비교하기 위해서는 equals() 메소드를 사용해야 합니다. (관련 링크 : Java equals와 ==의 차이점) 이와 달리, Kotlin에서 ==연산자는 컴파일 과정에서 equals() 메소드로 변환되며, === 연산자가 reference equality를 체크합니다. 한편 개발자가 직접 구현하는 클래스의 경우 의도에 적합하도록 equals() 를 재정의해주어야 하는데 data class로 선언하는 경우 자동적으로 구성 요소가 모두 동일한지 비교하는 메소드를 생성해줍니다.

Sealed class

  • Sealed class는 고정된 상속 계층 구조를 가지는 클래스를 다룰 때 사용합니다. 선언된 상속클래스 외에 다른 클래스가 없음을 보장하므로, Enum class 처럼 when expression 을 사용할 때 불필요한 else 분기를 만들지 않아도 되어 코드를 간결하게 만들 수 있습니다.
interface Expr
class Num(val value: Int) : Expr
class Sum(val left:Expr, val right:Expr) : Expr
/*
sealed class를 사용하지 않을 경우 when expression 사용을 위해
불필요한 else 분기 코드를 작성해야 한다.
실제로는 else 상황이 발생하지 않음에도 코드 상에서 이를 보장하지 못하기 때문에
when is not exhaustive 라는 오류 메시지가 뜬다.
*/
fun eval(e: Expr): Int = when (e) {
is Num -> e.value
is Sum -> eval(e.left) + eval(e.right)
else -> throw IllegarArgumentException("Unknown expression")
}
sealed class Expr
class Num(val value: Int) : Expr()
class Sum(val left:Expr, val right:Expr) : Expr()
/*
sealed class를 사용할 경우 해당 파일 내에 sealed class를 상속한 클래스가
전부임을 보장하여 else 분기를 작성하지 않아도 된다.
*/
fun eval(e: Expr): Int = when (e) {
is Num -> e.value
is Sum -> eval(e.left) + eval(e.right)
}
  • 이처럼 Kotlin의 when expression은 smart casts 및 sealed class 등과 함께 사용할 경우 충분히 강력하게 사용할 수 있어서, Kotlin은 Scala 등의 언어가 제공하는 패턴 매칭 기능을 언어 차원에서 지원하지 않습니다. whenexpression으로 충분히 커버되어 굳이 필요하지 않는 것 대비 복잡한 문법과 시스템 추가가 필요한 것에 대한 실용적 측면의 선택이었고, 실제로 추후에 도입할 수 도 있겠지만 현재로서는 굳이 도입할 필요가 없을 것으로 보인다고 합니다. (한편, 커뮤니티 글들을 보면 Kotlin과 Scala를 비교할 때 Scala의 pattern matching이 Kotlin when expression에 비해 강력하다는 글이 많긴 합니다. — 관련 링크 : Scala vs Kotlin)

Inner modifier & inner /nested class

  • 논리적으로 클래스간에 명확한 종속 관계가 있는 상황에서 클래스 내부에 클래스를 선언합니다. Java 에서는 이를 크게 내부 클래스 (Inner class, Non-static nested class)와 정적 중첩 클래스 (Static nested class, 줄여서 nested class로 씀) 로 나눕니다.
  • 내부 클래스는 non-static 이므로 사용할 때마다 외부 클래스 객체를 생성해야하고, 내부 클래스 객체는 외부 클래스 reference를 저장하기 위해 메모리 점유가 발생합니다. 하지만 클래스 내부에 클래스를 선언하는 경우는 일반적으로 논리적 종속 관계를 표현하기 위한 의도기 때문에, 내부에 선언한 클래스가 외부 클래스의 reference를 필요로 하지 않는 경우도 많습니다. 한편, 정적 중첩 클래스 (nested class)는 static 이므로 외부 클래스 reference 저장이 없어 메모리 효율성 측면에서 이점을 가집니다. (관련 링크 : Java 중첩 클래스를 알아보자)
  • Java는 클래스 내부에 클래스를 선언할 때 디폴트로 내부 클래스가 되며, 중첩 클래스를 사용하기 위해서는 static 키워드를 선언해야 합니다. 한편, Kotlin은 디폴트로 정적 중첩 클래스가 되며, 내부 클래스를 사용하기 위해서는 inner 제어자를 선언한 뒤, this@<외부클래스명> 을 통해 외부 클래스에 접근할 수 있습니다.
  • Kotlin이 클래스 내부에 클래스를 선언하는 경우에 대한 디폴트 설정을 Java와 다르게 가져간 이유는 일반적인 유스케이스에서 내부에 선언된 클래스가 외부 클래스에 대한 역참조를 필요로 하지 않는 경우가 많고, 이 경우 불필요한 메모리 누수를 줄이기 위해 정적 중첩 클래스가 낫기 때문입니다. 앞선 가시성, 상속/오버라이딩 가능성 사례와 마찬가지로 보다 일반적으로 사용되며, 상대적으로 리스크가 적은 설정을 디폴트로 둔 것입니다.

Class delegation

  • 인터페이스에 구현되지 않은 메소드들이 있고, 클래스에서 이 인터페이스를 프로퍼티로 사용할 경우 클래스 구현 과정에서 메소드를 구현할 필요 없이 추후 클래스 객체 생성시에 인자로 받을 인터페이스 구현체에게 메소드 구현을 위임할 수 있습니다. (아래 코드 참고) (관련 링크 : 제타위키 — 위임 패턴)
interface Repository{
fun getById(id: Int): Customer
fun getAll() : List<Customer>
}
interface Logger{
fun logAll()
}
class Controller(
val repository: Repository,
val logger: Logger
) : Repository, Logger {
override fun getById(id: Int): Customer = repository.getById(id)
override fun getAll(): List<Customer> = repository.getAll()
override fun logAll() = logger.logAll()
}
  • 한편 이러한 위임 패턴을 사용할 때, 위 코드 처럼 모든 위임 메소드 (delegating methods)를 직접 코딩하는 것은 번거로운 단순 작업입니다. 이 경우에 대해 Kotlin은 Class delegation 을 지원하며 by 키워드를 사용하면 컴파일 과정에서 위임 메소드를 자동으로 생성해주어 코드를 간결하게 만들 수 있습니다. (아래 코드 참고) by 키워드는 해당 인터페이스의 구현을 해당 매개변수에 할당될 객체에게 위임함을 의미합니다.
// 위 코드 블록의 Controller 클래스와 동일하게 동작하는 코드 
class Controller(
val repository: Repository,
val logger: Logger
) : Repository by repository, Logger by logger

5. Objects, object expressions & companion objects

  • Kotlin의 object 키워드를 통한 object declaration, object expression, 그리고 companion object에 대해 다룹니다.

object 키워드와 object declaration

  • 싱글톤 패턴은 오직 1개의 객체만을 가지는 클래스를 사용하는 디자인 패턴입니다. 데이터베이스 커넥션 풀, 로그 기록 객체 등 여러 곳에서 쓰이지만 공통된 1개의 객체로 데이터를 공유하며 사용하는 것이 효과적인 상황에서 사용합니다. (관련 링크 : 싱글톤 패턴을 사용하는 이유와 문제점)
  • Java에서는 싱글톤 패턴을 사용하기 위해 관용적인 단순 코드 작성 작업이 필요합니다. 한편, Kotlin은 object 키워드를 쓰면 컴파일 과정에서 싱글톤 패턴을 위한 코드를 자동으로 생성해주고, 관용적인 INSTANCE 필드 호출을 생략해주어 코드 간결성을 높일 수 있습니다. (아래 코드 참고)
// Java 코드에서 싱글톤 패턴 구현
public class JSingleton {
public static final JSingleton INSTANCE = new JSingleton();
private JSingleton() { }
public void foo() { System.out.println("foo"); }
}
// Java 싱글톤패턴 객체 멤버 호출 방법
JSingleton.INSTANCE.foo();
// 위와 동일하게 동작하는 Kotlin 코드
object KSingleton {
fun foo() { println("foo") }
}
// Kotlin 싱글톤 패턴 객체 멤버 호출 방법
KSingleton.foo()

object expression

  • Java에서 익명 클래스 (annonymous classes)는 인터페이스 구현체를 매개변수로 받는 메소드 호출과 같이 객체화가 한번만 필요한 클래스 구현시 사용됩니다. (관련 링크 : 익명클래스 사용방법)
  • Kotlin에서 구현할 인터페이스가 오직 1개의 추상 메소드 (abstract method)만 가질 경우 lambda로 구현할 수 있습니다. 인터페이스가 여러개의 추상 메소드를 가질 경우 object expression으로 구현할 수 있습니다.
  • object expression도 object : 키워드를 사용하지만 이 때 생성되는 객체는 호출시마다 매번 새롭게 생성되며 앞선 싱글톤 패턴 (object declaration)과 무관합니다.

companion object

  • 싱글톤 패턴 클래스를 의미하는 object declaration을 외부클래스 내부에 사용하여 중첩 오브젝트 (nested object)를 사용할 수 있습니다. 한편 이 때 companion object 키워드를 사용하면, 중첩 오브젝트 멤버에 대한 접근 코드를 간소화할 수 있습니다. 외부클래스 내부에 싱글톤 패턴 클래스를 구현하는 목적이 일반적으로 외부클래스에 대한 공용 유틸리티성 메소드/프로퍼티를 사용하기 위한 것에 적합합니다.
class A {
companion object {
fun foo() = 1
}
}
fun main(args: Arrays<String>){
A.foo()
// companion object를 사용하면 싱글톤 패턴 클래스 이름을 생략하고
// 외부 클래스에서 바로 중첩 오브젝트 멤버를 호출할 수 있다.
}
  • 한편 Java에서는 위와 같은 목적을 위해 static 메소드를 사용하며, 이를 Kotlin에서는 companion object가 대체합니다. static 메소드와 비교하여 companion object의 이점은 1) 클래스이므로 인터페이스 구현 방식으로 코드를 작성할 수 있고, 2) Companion object에 대한 확장함수를 구현하여 외부클래스에 대한 정적 유틸리티성 기능을 현할 수 있다는 점입니다. (아래 코드 참고)
class Animal(val name:String){
companion object { }
}
// 외부 클래스에 대한 확장함수
// 외부 클래스 구현 객체에 대한 유틸리티성 기능 (ex: 외부클래스 객체 프로퍼티 print)
fun Animal.printName(){
println(this.name)
}
// 외부 클래스 내부 companion object에 대한 확장함수
// 외부 클래스에 대한 정적 유틸리성 기능 (ex : json으로 부터 외부클래스 객체 생성)
fun Animal.Companion.fromJSON(json: String) : Animal{
return Animal(convertJsonToMap(json)["name"])
}
// 아래와 같이 다른 용도의 확장함수로서 사용 가능
fun main(args: Arrays<String>){
val animal = Animal.fromJSON("... JSON 스트링 ...")
animal.printName()
}

No static keyword

  • Kotlin은 다양한 문법을 통해 기존 Java static 키워드가 필요한 기능을 구현하며, Kotlin에는 static 키워드가 제공되지 않습니다.
  • Java static 메소드/멤버의 경우 1) top-level에 정의하거나, 2) object 내부 (싱글톤 패턴 클래스)에 정의하거나, 3) 클래스 내부에 companion object를 정의하고 이 내부에 정의하는 방식을 사용합니다. 기본적으로는 1)로 대체 가능하며, static 메소드/멤버가 특정 클래스 내부의 private 멤버에 접근해야할 경우 2), 3)을 사용합니다.
  • 이러한 static 키워드 대체의 의도는 java static 멤버의 경우 클래스 멤버이면서도 클래스 객체와 무관하게 동작하므로 의미적으로 모호하다는 문제를 개선하기 위해서였다고 합니다. 클래스 내부 private 멤버에 대한 접근이 필요없다면 top-level에 정의하는 것을 지향하고, 필요할 경우 클래스 내부에서 정의하되 오직 1개의 객체를 생성하는 싱글톤 패턴의 (object, companion object) 멤버 형태로 구현합니다.

@JvmStatic

  • object를 통한 싱글톤 패턴 클래스는 사실 <클래스명>.INSTANCE.<멤버> 를 Kotlin 컴파일러가 <클래스명>.<멤버> 로 간소화하여 사용할 수 있도록 하는 것이고, companion object를 통한 외부클래스 내부의 싱글톤 패턴 클래스는 사실 <외부 클래스명>.Companion.<멤버> 를 Kotlin 컴파일러가 <외부 클래스명>.<멤버>로 간소화하여 사용할 수 있도록 하는 것입니다. 따라서 해당 Kotlin 코드를 Java에서 사용할 경우 Kotlin 코드와 달리 .INSTANCE , .Companion 작성이 필요합니다.
  • 이는 Java 상호운용성 측면에서 혼동의 여지가 있으며, object/companion object 내부의 멤버에 대해 @JvmStatic 어노테이션을 붙이면 Java에서도 Kotlin과 동일한 코드로 사용할 수 있습니다.

Inner object (Inner modifier with object)

  • 외부클래스 내부 object에 대해서는 inner 제어자를 사용할 수 없습니다. 왜냐하면 inner 제어자는 non-static nested class를 의미하는데, object는 싱글톤 패턴 클래스로 static 이어서 상충되기 때문입니다. (Modifier ‘inner’ is not applicable to ‘object’ 오류 메시지가 뜸)

6. Constants

  • 상수 (constants)로서 클래스 멤버 변수란 해당클래스에 대해 고정된 값을 가지는 속성을 의미합니다. (ex: 원주율) 상수는 각 객체에 종속되지 않고 동일한 값을 가져야 하며, 한번만 초기화 되어야 하므로 Java에서는 static과 fianl을 통해 구현합니다. (관련 링크 : 왜 자바에서 final 멤버 변수는 관례적으로 static을 붙일까?)
  • Kotlin 에서는 const 제어자 또는 @JvmField 어노테이션을 통해 상수를 구현합니다. const 제어자를 사용하면 컴파일 과정에서 상수 변수를 해당 값으로 치환하며 (단, Primitive type 또는 String에 대해서만 동작), @JvmField 어노테이션을 사용하면 프로퍼티를 accessor를 통해서가 아니라 직접 필드에 접근할 수 있도록 하는 방식으로 mutability를 차단하여 상수를 구현합니다.

7. OOP design choices

  • <OOP in Kotlin>, <Class modifiers — I, II>, <Objects, object expression & companion objects> 요약에서 다루어 생략

--

--