본문 바로가기
My Image
프로그래밍/kotiln

클래스, 객체, 인터페이스(4장)

by Lim-Ky 2023. 2. 19.
반응형

코틀린 인터페이스

fun main(arg:Array<String>){
    val button = Button()
    button.showOff()
    button.setFocus(true)
    button.click()
}

interface Clickable { //interface 키워드로 선언
    fun click() //추상메서드
    fun showOff() = println("I'm clickable!") //구현이 있는 메서드(디폴트 메서드) *선택
}

interface Focusable {
    fun setFocus(b: Boolean) =
        println("I ${if (b) "got" else "lost"} focus.") //디폴트 메서드 *선택

    fun showOff() = println("I'm focusable!") //Clickable interface와 메서드 명이 동일한 디폴트 메서드
}

//:(콜론)을 통한 implements 여러 interface도 구현 가능
class Button : Clickable, Focusable {
    override fun click() = println("I was clicked") //오바라이드를 통한 재정의 *강제
    override fun showOff() { //Clickable, Focusable 둘다 동일한 이름의 메서드가 있기 때문에 하위 클래스에서 재정의 강제
        super<Clickable>.showOff(); //Clickable의 showOff호출
        super<Focusable>.showOff(); //Focusable의 showOff호출
    }
}

/*
>>I'm clickable!
>>I'm focusable!
>>I got focus.
>>I was clicked
*/
  • 코틀린 인터페이스 안에는 추상 메서드
  • 코틀린 인터페이스 안에는 구현이 있는 메서드도 정의 가능(=자바 디폴트 메서드)
  • 코틀린 인터페이스 상태(필드)도 들어갈 수 없음
  • interface 키워드를 사용하여 정의
  • 콜론(:) 을 통해 인터페이스를 implements 함
  • 코틀린에선 override 반드시 명시해야 함(why? 실수로 상위클래스의 메서드와 동일한 메서드 구현 방지)
  • 디폴트 메서드를 사용할 수도 있고, 재정의 할 수 있다.
  • 여러 인터페이스를 implements 할 수 있음
  • Super<상위타입> 문법을 통해, 원하는 인터페이스의 메서드를 직접 호출 할 수 있음

 

코틀린 상속제어 변경자 (open, final, abstract)

코틀린은 기본적으로 final ~ !

자바에서는 final을 명시적으로 사용해서 상속을 금지하고, final이 아닌 모든 클래스는 다른 클래스가 상속할 수 있게 하였다.

하지만, 이로 인해서 발생되는 문제가 많았다. '취약한 기반 클래스' 라는 문제인데, 쉽게 기반 클래스의 목적가 용도에 맞게끔 하위클래스가 상속하지 않아서 발생되는 문제라고 보면 된다. 이런 문제를 해결 하기 위해 '이펙트 자바'에선 기반 클래스를 올바르게 상속할 수 있는 정확한 방법을 제공하지 못한다면 아예 상속할 수 없도록 상속을 보수적으로 막아버리는 전략을 취한다. (예를 들어 final 선언)

이런 기조를 코틀린도 따른다. 코틀린은 한발 더 나아가 코틀린의 클래스와 메서드는 기본적으로 final로 처리한다. (자바보다 더 보수적)

만약, 상속을 하고 싶다면, final 이 아닌 open 변경자를 붙여야 한다!!

//상속이 가능하게 open을 사용
open class RichButton : Clickable { 
    fun disable() {} //기본적으로 final, 오버라이드 불가능
    open fun animate() {} //다른곳에서 오바라이드 가능
    override fun click() {} // Clickable 추상메서드를 강제로 오버라이드 하였음(오버라이드된 메서드는 기본적으로 open임)
    final override fun click() {} //오버라이드된 메서드는 open이나, final를 선언하여 다른곳에서 오버라이드 불가능
}
  • 코틀린 클래스와 메서드는 기본적으로 final
  • open을 사용해서, 클래스, 메서드를 다른곳에서 상속 및 오버라이드가 가능하도록 하였음
  • 오버라이드된 메서드는 기본적으로 open 열려있으나, 만약 다른 곳에서 오버라이드를 막고 싶다면 final을 선언하면 됨

추상클래스의 abstract

  • 자바처럼 코틀린도 클래스를 abstract로 선언할 수 있다.
  • abstract로 선언한 추상 클래스는 인스터스화 할 수 없다.(구현이 없는 빈 껍데기이니 인스턴스화 할 수 없는게 당연함)
  • 보통 추상 클래스에는 구현이 없는 추상 멤버가 있기 때문에 하위클래스에서 추상 멤버를 오버라이드해야하는게 일반적이다.
  • 따라서, 추상 멤버는 항상 열려 있고, 추상 멤버 앞에 굳이 open 변경자를 명시할 필요가 없다
  • 참고로 인터페이스는 구현이 강제되기 때문에 항상 열려 있어야 하기 때문에 final로 변경할 수 없고, abstract 키워드도 필요 없다.
//abstract 키워드를 이용하여, 추상 클래스를 선언
abstract class Animated {
    abstract fun animate() //추상 함수, 반드시 오버라이드 해야 함
    // 추상 클래스에 속해 있어도 비 추상함수는 기본적으로 파이널이지만,open으로 오버라이드 가능
    open fun stopAnimating(){
      ...
    }
    fun animateTwice(){ //기본적으로 final, 오버라이드 불가능
      ...
    }
}

요약 하면,,,,

변경자 이 변경자가 붙은 멤버는... 설명
final 오버라이드 불가능 클래스 멤버의 기본 변경자이다!
open 오버라이드 가능(선택적) 반드시 open을 명시해야 오버라이드 가능
abstact 반드시 오버라이드 해야 한다(강제성) 추상 클래스의 멤버에만 이 변경자를 사용 할 수 있으며,
추상 멤버에는 구현이 없어야 함.
override 상위 클래스나 상위 인스턴스의 멤버를 오버라이드 한다는 뜻이다. 오버라이드하는 멤버는 기본적으로 open 이어야 하고, 또 다른 하위 클래스에게 오버라이드를 금지 하고 싶으면 final을 명시해야 한다.

 

코틀린의 가시성 변경자 또는 접근 변경자 (public, private, protected, internal)

기본적으로 코틀린 가시성 변경자는 자바와 비슷하다.

코틀린의 기본 가시성은 자바와 달리 아무 변경자도 없는 경우 public 으로 간주함

코틀린은 internal 이라는 변경자를 제공하고, internal은 모듈 내부에서만 볼 수 있다는 뜻이며, 모듈은 한 번에 한꺼번에 컴파일 되는 컴파일 되는 파일들을 의미 한다.

변경자 클래스 멤버 최상위 선언
public(기본 가시성임) 모든 곳에서 볼 수 있다. 모든 곳에서 볼 수 있다.
internal 같은 모듈 안에서만 볼 수 있다. 같은 모듈 안에서만 볼 수 있다.
protected 하위 클래스 안에서만 볼 수 있다. (최상위 선언엔 적용할 수 없음)
private 같은 클래스 안에서만 볼 수 있다. 같은 파일 안에서만 볼 수 있다.

코틀린 최상위 선언 : https://kotlinworld.com/69

 

Kotlin, Java의 최상위 선언 차이점

목표 Java와 Kotlin의 최상위 선언의 차이에 대해 이해한다. 최상위 선언 최상위 선언이란 파일 최상위에 선언되는 클래스, 메서드, 변수를 뜻한다. Java의 최상위 선언 Java에서는 모든 코드가 클래

kotlinworld.com

참고로, 같은 패키지이면 자바에선 protected 멤버에 접근이 가능하지만 코틀린은 오직 어떤 클래스나 그 클래스를 상속한 클래스 안에서만 보인다. 또한, 확장함수라고 할지라도 수신타입의 클래스의 private, protected 멤버에 접근할 수 없다.

internal oepn class TalkativeButton : Focusable {
    private fun yell() = println("Hey!")
    protected fun whisper() = println("Let's talk!")
}

//변경자가 없기 때문에 public이며, giveSpeech 확장함수 선언
//public 멤버가 자신의 internal 수신타입인 TalkativeButton을 노출할 수 없음(에러)
fun TalkativeButton.giveSpeech(){
    yell() // private 접근할 수 없음
    whisper() // 하위클래스가 아니라서 접근 불가
}
  • 코틀린 public 함수인 giveSpeech 안에서는 그보다 가시성이 낮은 internal 타입인 TalkativeButton을 참조하지 못한다.
  • 가시성이 같거나 접근하려고 하는 곳의 가시성이 더 높아야 접근이 가능함.

 

내부클래스, 중첩 클래스

코틀린은 기본적으로 클래스 안에 클래스를 선언하면 중첩 클래스로 처리한다.

중첩클래스는 외부 클래스의 멤버(프로퍼티, 메서드)를 참조할 수 없다.

fun main(arg:Array<String>){
    val instance = Outer.Nested()
    instance.hello() // "중첩된 클래스입니다!" 출력
}

class Outer{ // 외부 클래스
    private val num: Int = 10
    class Nested{ // 증첩 클래스 (static 으로 처리)
        fun hello() = println("중첩된 클래스입니다!")
        //fun printMyNum = println(num) //Unresolved reference: num 컴파일 오류
    }
}

내부클래스는 외부 클래스에 대한 참조를 할 수 있으며, 참조시에 'this@외부클래스명'을 사용하여 접근 가능

fun main(args: Array<String>){
    val outerInstance = Outer(610)
    val innerInstance = outerInstance.Inner(40) // 바깥 클래스의 인스턴스로 부터 내부 클래스의 인스턴스를 생성
    innerInstance.print()
}

class Outer(private val outerNum: Int){
    fun print(){
        println(this.outerNum)
    }

    inner class Inner(private val innerNum:Int){
        fun print(){
            this@Outer.print()
            println(this.innerNum + this@Outer.outerNum) //내부 클래스는 this@Outer를 사용하여 외부클래스 참조 가능
        }
    }
}

>>610
>>650
  • 내부 클래스는 inner 키워드를 선언하여 정의 가능
  • 내부 클래스는 외부 클래스 참조 가능
  • 참조시, this@외부클래스명 을 사용하여 접근 가능

 

봉인된 클래스 (클래스 계층 정의 시 계층 확장 제한)

아래 코드를 보면, 상위 클래스인 Expr에는 숫자를 표현하는 Num과 덧셈 연산을 표현하는 Sum 하위 클래스들이 있고

when 식에서 하위 클래스가 무엇인지 체크하여 분기 처리를 한다. 이 과정에서 항상 else 구문이 들어가고 else에서 특별히 리턴할게 없어서 예외를 던지고 있다. 아래 코드는 개발자 실수로 하위클래스 체크를 누락할 가능성이 있다.

interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.right) + eval(e.left)
        else -> // "else" 분기가 꼭 있어야 한다. 
            throw IllegalArgumentException("Unknown expression")
    }

이를 sealed 클래스를 이용해서 해결 할 수 있다.

상위 클래스에 sealed 변경자를 붙이면 그 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있다. sealed를 통해 코드를 고쳐보자.

다시 코드를 보자. 

//기반클래스를 sealed 키워드를 써서 선언함과 동시에 하위 클래스를 중첩클래스로 나열한다.
sealed class Expr {
    class Num(val value: Int) : Expr()
    class Sum(val left: Expr, val right: Expr) : Expr()
}

fun eval(e: Expr): Int =
    //when식이 모든 하위클래스를 검사하므로, 별도의 else 분기가 없어도 됨
    when (e) {
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.right) + eval(e.left)
    }
  • sealed 는 자동으로 open 임
  • sealed 클래스 외부에 하위클래스를 둘 수 없다.
  • 무조건 중첩 클래스 나열을 통해 하위 클래스를 구현하는건가?
  • 새롭게 하위클래스를 선언하면, when식도 수정해야한다는 사실을 알 수 있을까?

 

클래스 초기화 : 주 생성자와 초기화 블록

class User(val nickname: String)

위 클래스 선언을 최대한 명시적으로 선언하면 아래와 같다.

class User constructor(_nickname: String) { // 파라미터가 하나만 있는 주 생성자
    val nickName: String

    init { // 초기화 블록
        nickName = _nickName
    }
}
  • 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드를 주 생성자 라고 부름
  • 주생성자는 생성자 파라미터를 지정, 그 생성자 파라미터에 의해 초기화 되는 프로퍼티를 정의 함
  • _constructor 키워드는 주 생성자나 부 생성자 정의를 시작할 때 사용
  • init 키워드는 초기화 블록을 시작할 때 사용
  • 초기화 블록은 객체가 생성될 때 실행됨
  • 여러 초기화 블록 선언 가능
  • _(밑줄)은 생성자 파라미터와 프로퍼티를 구분해줌

위 코드를 축약해보자

class User (_nickname: String) { 
    val nickName = _nickname
}
  • 초기화 블록을 _nickname 프로퍼티 선언에 녹일 수 있다
  • 가시성 변경자가 없다면 constructor는 생략할 수 있다

마지막으로 더 축약해보자...

class User(val nickname: String)
  • 주생성자 앞에 val을 쓰면, 프로퍼티 정의와 초기화를 다 알아서 해준다..

이 밖에도, 주생성자에 디폴트 값을 쓸 수 있고, 생성자 인자 중에 일부에 대해 이름을 지정할 수도 있으며, 모든 인자들 파라미터 선언 순서대로 지정할 수 있다.

 

클래스에 기반 클래스 있다면 주 생성자에서 기반 클래스의 생성자를 호출해야 할 필요가 있다. 기반 클래스를 초기화하려면 기반 클래스 이름 뒤에 괄호를 치고 생성자 인자를 넘기면 된다.

open class User(val nickname: String){ ... }
class TwitterUser(nickname: String) : User(nickname) { ... }

 

부 생성자 : 상위 클래스를 다른 방식으로 초기화 (코틀린이 제공하는 생성자가 아닌 직접생성자 정의)

인자가 한개, 두개, 세개..등 다양하게 선언된 생성자를 만들고 싶을 때 부 생성자를 구현하자.

open class view {
    constructor(ctx: Context){
        //코드
    }
    constructor(ctx: Context, attr:AttributeSet){
    	//코드
    }
}

class myButton : view {
    constructor(ctx: Context): super(ctx){
        	//..
    }
    constructor(ctx: Context, attr:AttributeSet): super(ctx, attr){
        	//..
    }
    constructor(ctx: context):this(ctx, MY_STYLE){
    }
    constructor(ctx: context, attr:AttributeSet):super(ctx, attr){
    }
    
}
  • view는 주생성자를 선언하지 않고 부 생성자 2개를 구현했다.
  • 부 생성자는 constructor 키워드를 사용한다.
  • 클래스 헤더 부분에 클래스 이름 뒤에 괄호가 없다. (즉, 주생성자를 사용하지 않겠다는 뜻)
  • super() 키워드를 통해 상위클래스에게 생성자 위임을 처리할 수 있다.
  • this() 를 통해 자신의 다른 생성자를 호출 할 수도 있음
  • 부 생성자의 위임 끝엔 반드시 상위 클래스 생성자를 호출해야 하나?

 

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

interface User {
    val nickName: String //추상 프로퍼티 선언
}

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) // 프로퍼티 초기화 식
}
  • 인터페이스 안에서 추상 프로퍼티 선언가능(구현체쪽에서 오버라이드해서 get 구현 필요)
  • PrivateUsers는 주 생성자 안에 프로퍼티를 직접 선언하는 간결한 구문으로 처리
  • SubscirbingUser는 커스텀 게터로 처리하여, 매번 이메일 주소에서 별명을 계산하여 반환
  • FacebookUser는 초기화 식으로, 초기화 단계에서 한 번만 getFacebookName을 호출하여 값을 저장하고 불러오는 방식을 사용
interface User {
    val email: String //하위 클래스에서 반드시 오버라이드 해야 함
    val nickName: String
    get() = email.substringBefore('@') // 프로퍼티에 뒷받침하는 필드가 없다. 대신 매번 결과를 계산해 돌려준다. 
    //뒷받침하는 필드가 없다는 뜻은, 따로 값을 저장하는 변수가 없다는 뜻 같음.
}
  • 인터페이스에서는 위와 같이 게터, 세터가 있는 프로퍼티를 선언할 수도 있다.
  • 하위 클래스는 반드시 email을 오버라이드 해야 함
  • 반면 nickname은 오버라이드 없이 상속 가능

 

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

값을 저장하는 프로퍼티와 커스텀 접근자를 통해 매번 값을 계산하는 프로퍼티를 조합하여 값을 조회하거나 변경할 때 특정 로직을 수행하게 만들 수 있다. 이를 위해 field 라는 특별한 식별자를 통해 뒷받침하는 필드(값을 가지고 있는 필드)에 접근할 수 있다.

class User(val name: String) {
    var address: String = "unspecified"
        set(value: String) {
            println("""Address was changed for $name:
            "$field" -> "$value".""".trimIndent())
            field = value
	}
}

>> val user = User("Alice")
>> user.address = "Elsenheimerstrasse 47, 80687 Muenchen"
Address was changed for Alice:
"unspecified" -> "Elsenheimerstrasse 47, 80687 Muenchen".
  • 위 코드에서 field는 address 값을 의미 함
  • trimIndent() 를 통해 뒷받침 하는 필드 값 읽기 (필드 값을 읽기 위해선 해당 함수를 사용해야 하나봄...)
  • field = value 를 통해 값 변경 (field 키워드를 통해 바로 값을 변경할 수도 있음)
  • user.address = "Elsenheimerstrasse 47, 80687 Muenchen" 자동으로 set()호출..

 

접근자의 가시성 변경

쉽게 말해, var 를 선언한 변수는 외부에서 set을 통해 값을 변경할 수 있게 해준다는 뜻이다.

하지만 원한다면 set 앞에 가시성 변경자를 추가해서 접근자의 가시성을 컨트롤(변경) 할 수 있다.

즉, 코틀린에서는 외부에서 변경가능한 변수를 외부에서 수정 못하게 막고, 내부에서만 수정할 수 있게끔 처리가 가능함

class LengthCounter {
    var counter: Int = 0
        private set // 이 클래스 밖에서 이 프로퍼티의 값을 바꿀 수 없다. 

    fun addWord(word: String) {
        counter += word.length
    }
}

>>> val lengthCounter = LengthCounter()
>>> lengthCounter.addWord("Hi!")
>>> println(lengthCounter.counter)
>>> 3 출력됨..
  • 외부 코드에서 단어 길이의 합을 마음대로 바꾸지 못하게 클래스 내부에서만 변경할 수 있게끔 set 앞에 private를 선언했다.

 

모든 클래스가 정의해야 하는 메서드

모든 클래스가 정의해야 하는 메서드는 대표적으로, toString(), equals(), hashcode() 등이 있다.

  • 인스턴스 간 비교를 위한 euqals
  • HashMap과 같은 해시 기반 컨테이너에서 키로 사용할 수 있는 hashCode
  • 클래스의 각 필드를 각 순서대로 표시하는 문자열 표현을 만들어주는 toString

해당 메서드를 오버라이드 해서 우리의 입맛에 맞게 (유용하게) 바꿔 보자..

class Client(val name: String, val postalCode, Int){
  //Any는 Java에서 java.lang.Object와 대응
  override fun equals(other: Any?) : Boolean {
    if(other == null || other !is Client)
      return false
    return (name == other.name && postalcode == other.postalCode)
  }
  //toString() 오버라이드
  override fun toString() = "client(name=$name, postalCode=$postalCode)"
  
  //hashCode() 오버라이드
  override fun hashCode() : Int = name.hashCode() * 31 + postalCode
}
  • toString을 그냥 쓰면, Client@53dl344 같은 식으로 출력되기 때문에 우리가 원하는 클래스 내부 값 비교가 되지 않는다. 따라서 우리 입맛에 맞게 값을 보여주도록 오버라이드 한다.
  • equals()를 오버라이드하여, 객체간 값을 비교하여 동일한 값인지 판단하도록 오버라이드 한다. (참조 비교를 하고 싶으면 === 사용)
val processed = hashSetOf(Client("임경호", 4122))
println(prcessed.contains(Client("임경호", 4122)))
>> hashCode를 구현하지 않으면 false, 구현하면, true
  • 하지만 더 복잡한 작업을 수행하면 equals를 재정의 했다고 하더라도, 반드시 equals가 우리가 원하는 대로 동작하지 않을 수 있다.
  • 따라서, hashCode()를 함께 오버라이드 해줘야 우리가 100% 원하는 동작을 보장할 수 있다.
  • 예를 들어 hashset은 원소를 비교할 때 비용을 줄이기 위해 해시코드를 비교하고 해시코드가 같은 경우에 실제 값을 비교한다.
  • 따라서,hashCode() 메서드를 함께 오버라이드를 해줘야 한다.

 

데이터 클래스 : 모든 클래스가 정의 해야 하는 메서드 자동 생성...

앞서 직접 구현한 toString() / equals() / hashCode() 오버라이딩을 컴파일러에게 위임하는 클래스...

// data키워드를 통해서 데이터 클래스로 생성
data class Client(val name: String, val postalCode, postalCode)
  • data 변경자를 클래스 이름 앞에 붙혀 선언
  • euqals()와 hashCode()는 주 생성자에 나열된 모든 프로퍼티(property)를 고려해 자동 생성 됨
  • 주 생성자 밖에 선언된 프로퍼티는 대상에서 제외된다
  • 데이터 클래스는 불변 객체인 것을 권장
  • 불변 객체인 경우 프로그램을 추론하기 쉬움
  • 스레드가 사용중인 데이터를 변경할 수 없어서, 스레드 동기화 필요가 줄어든다
  • copy() 메서드 제공(원본과 다른 생명주기를 가지며, 복사를 하면서 일부 값을 변경할 수 있음)

 

클래스 위임 사용하기

어쩔 수 없이 상속을 허용하지 않는 클래스에 새로운 기능을 추가해야할 때 데코레이터 패턴을 사용함.

이 패턴의 핵심은 상속을 허용하지 않는 클래스 대신 사용할 수 있는 새로운 클래스(데코레이터)를 만들되 기존 클래스와 같은 인터페이스를 데코레이터가 제공하게 만들고, 기존 클래스를 데코레이터 내부에 필드로 유지하는 것이다. 이 때 새로 정의해야 하는 기능은 데코레이터의 메서드에 새로 정의하고, 기존 기능이 필요한 부분은 기존 클래스에게 요청하여 처리한다. 

데코레이터 페턴 참고 : https://limkydev.tistory.com/80

 

[Design_Pattern] 데코레이터 패턴(Decorator Pattern)

안녕하세요. Limky 입니다. 이번 시간에는 데코레이터 패턴(Decorator Pattern)에 대해서 알아보겠습니다. Decorator 는 우리말로 장식자라는 뜻을 가지고 있습니다. 말 그래도 원본에 대해서 무언가를 더

limkydev.tistory.com

하지만, 해당 접근 방법의 단점은 너무 많은 준비 코드가 필요하단 점이다. 이런 복잡한 준비 과정을 코틀린에서는 by키워드를 통해 인터페이스에 대한 구현을 다른 객체에 위임 중이란 사실을 명시할 수 있다.

예를 들어 collection 같이 비교적 단순한 인터페이스를 구현하기 위해선 아래와 같은 코드들이 필요하다..

class DelegatingCollection<T> : Collection<T> {
    private val innerList = arrayListOf<T>()
    override val size: Int get() = innerList.size
    override val isEmpty() : Boolean = innerList.isEmpty()
    override val contains(element: T): Boolean = innerList.contains(element)
    override val iterator(): Iterator<T> = innerList.iterator()
    override val containsAll(elements): Boolean = innerList.containsAll(elements)
}

이를 by 키워드로 처리하면...? 

class DelegatingCollection<T>(
	innerList:Collection<T> = arrayList<T>()
    ) : Collection<T> by innerList{}
  • 일일히 기능을 위임하는 메소드를 작성할 필요가 없어진다
  • 만약 위임하는 기능 중 변경이 필요하면 override를 통해 대체할 수 있다

 

object 키워드 : 클래스 선언과 인스턴스 생성

클래스를 정의함과 동시에 인스턴스를 생성한다고 보면 된다.

object 키워드는 아래 3가지 상황에서 사용된다.

  • 객체 선언을 통한 싱글턴(singleton) 정의
  • 동반 객체 인스턴스
  • 객체 식 (Java의 무명 내부 클래스)

객체 선언을 통한 싱글턴 정의

// 회사에 급여 대장은 하나만 필요하니 객체 선언으로 싱글톤으로 생성
object Payroll {
  val allEmployees = arrayListOf<Person>()
  fun calculateSalary() {
  ...
}

//변수와 마찬가지로 객체 선언에 사용한 이름 뒤에 마침표를 통해 객체 메서드나 프로퍼티 접근 가능
Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()
  • 클래스 선언과 동시에 알아서 인스턴스까지 생성해줌
  • 일반 클래스 인스턴스와 달리 생성자 호출 없이 즉시 생성되며,
  • 별도의 생성자 정의가 필요없음

 

동반 객체 인스턴스

코틀린 클래스는 static 키워드를 지원하지 않아 정적인 멤버가 없다. 대신 코틀린은 패키지 수준의 최상위 함수와 객체 선언을 제공한다.

하지만, 최상위 함수는 클래스의 private 멤버에 접근할 수 없다는 한계가 있다. 따라서, 클래스 내부에 객체를 만들어서 이용하게 된다. 이 때, 클래스 내부에 객체를 동반 객체로 만들면, 클래스의 이름으로 바로 접근할 수 있다!

팩토리 패턴 참고 : https://limkydev.tistory.com/83?category=957882

 

[Design_Pattern] 팩토리 메서드 패턴(Factory Method Pattern)

안녕하세요 Limky입니다. 이번시간은 팩토리 메서드 패턴(Factory Method Pattern)에 대해서 알아보겠습니다. 팩토리 메서드 패턴(Factory Method Pattern)의 팩토리 메소드는 객체를 생성해서 반환하는 것을

limkydev.tistory.com

class User private constructor(val nickname: String) {
    companion object {
        fun newSubscribingUser(email: String) =
            User(email.substringBefore('@'))

        fun newFacebookUser(accountId: Int) =
            User(getFacebookName(accountId))
    }
}

fun main(args: Array<String>) {
    val subscribingUser = User.newSubscribingUser("bob@gmail.com")
    val facebookUser = User.newFacebookUser(4)
    println(subscribingUser.nickname)
}
  • 동반 객체는 companion 와 object 키워드를 통해, 클래스 내부에 동반 객체를 생성해서 클래스로 접근하는 것을 뜻한다.
  • 필요하다면 companion object {이름} 을 통해 이름을 부여할 수도 있음 (기본으로 Companion 이라는 이름으로 지정됨)
  • 즉, Java에서 정적 메소드 호출이나 필드 사용과 동일하게 사용이 가능해 진다.
  • 팩토리 메서드 패턴을 구현하기에 유용하다
    => 불필요하게 객체를 생성하지 않고 바로 접근해서 사용할 수 있기 때문. (객체의 생성을 다른 인스턴스를 통해 하는 것)
  • 이 밖에도 동반객체에서 인터페이스를 구현할 수 있고, 확장 함수를 정의할 수 있다.

객체 식 (Java의 무명 내부 클래스)

window.addMouseListener(
    object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) { ... }
        override fun mouseEntered(e: MouseEvent) { ... }
    }
)

val listener = object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { ... }
    override fun mouseEntered(e: MouseEvent) { ... }
}
  • 무명 객체를 정의할 때에도 object 키워드를 사용할 수 있다.
  • 기본적으로 인스턴스 이름을 붙이지 않지만, 붙이고 싶다면 변수에 무명 객체를 대입하면 된다.
  • 무명 객체는 싱글톤이 아니다 (객체 식이 쓰일 때 마다 새로운 인스턴스가 생성)
반응형

댓글