본문 바로가기
Engineering/Kotlin

Companion object

by 쿨쥰 2023. 5. 18.

Companion object

 

object

Companion object 를 알아보기 전에 object 를 먼저 살펴보면,

kotlin 에서는 object 키워드를 통해 객체를 별다른 정의 없이 싱글톤으로 구현을 지원한다.

즉, JAVA 에서는 클래스 내부에서 Static 객체로 한번만 할당해주는 코드가 Kotlin 에서는 class 키워드 대신 object 키워드를 사용 함으로서 마치 static 객체에 할당 하는 것처럼 자동으로 생성 해주는 것이다.

 

object ServiceRepo {
    val allService = arrayListOf<Service>()
    
    fun getService() {
    	// ...
    }
}

// 호출부
ServiceRepo.getService()
ServiceRepo.allService.add(Service(.....)))

ServiceRepo 인스턴스를 생성하지 않았는데도 바로 getService 에 접근하여 사용할 수 있다는 점을 알 수 있다.

이 Object 의 특징으로는 primary constructor, secondary constructor 모두를 사용할 수 없다는 점이다.

Object 자체가 싱글돈 객체이기 때문에 생성자 자체를 사용할 수 없기 때문이며,

따라서 object ServiceRepo(val id: Int) 따위와 같은 생성자를 쓸 수 없다.

 

 

 

Companion Object

kotlin 에는 static 이 없지만, 대신 package 수준의 최상위 함수와 객체 선언을 사용할 수 있다.

최상위 함수란 static 메서드를 대신할 수 있는 함수를 뜻하며,

객체 선언이란 최상위 함수가 접근할 수 없는 클래스 내에 private 멤버 변수에 접근할때 사용하는 객체를 의미한다.

이 객체선언 방법으로서 companion object {…} 를 사용 할 수 있고,

동반 객체라 불리우는 이 companion object는 클래스 내에 하나만 생성 할 수 있다.

companion object 에 대해 심층적으로 JAVA static 과 비교하여 설명해둔 자료는 아래에서 찾을 수 있다.

https://www.bsidesoft.com/8187

나는 실제 설계 패턴을 퀵하게 훑어 보고 있는 중이므로 실전 사용 패턴에만 주목한다.

 

 

Companion object 의 일반적인 사용 형태

class User private constructor(val name: String) { // User 클래스 주 생성자는 private로 접근 제한 됨

    companion object {
        fun getUserName() : User {
            return User("사용자 이름")	// companion object의 getUserName()을 통해 private 생성자에 접근 가능
        }
    }
}

// 호출부
var a = User("이거 접근되나?")	// 접근 불가능. 주 생성자는 private 접근제한자로 설정되어있으므로 호출 불가능
var b = User.getUserName()		// 성공, 동반 객체의 getUserName() 메서드를 통해 private 주생성자에 접근이 가능

위 예제는 User 클래스에 주 생성자에 private 접근 제한자를 사용했으므로 외부에서 생성자 접근이 불가능하게 선언되어 있다.

따라서 위 상황에서는 companion object 내 존재하는 getUserName() 메서드를 통해서만 private 로 설정된 주 생성자를 호출할 수 있게 되었다.

추가로, 이 Companion Object 에 이름을 붙일 수 도 있다.

예를들어 위 예제에서 Companion object ForPublic 이라는 이름을 붙인다면,

호출부에서는 var c = User.ForPublic.getUserName(). 이런 식으로 동반 객체의 이름을 지정해 접근 할 수 있게 되는 것이다.

물론, 이 동반 객체 이름은 생략도 가능 하므로, 동반 객체에 이름을 붙였다고 무조건 이름을 사용할 필요는 없다.

 

 

Companion object + Factory pattern

companion object 는 팩토리 패턴을 구현하는데 효과적이다.

보통 클래스를 생성 할 때는 여러 constructor 를 만들어서 객체를 생성할 수는 있지만, 생성자가 많아지면 복잡해지고 헷갈릴 때가 많아지기 마련이다.

이때, 팩토리 패턴을 사용하면 클래스를 생성할 때 어떤 목적으로 만들지 생성자를 선택하는데에 도움이 될 수 있다.

아래는 역시 private 생성자 클래스를 만들고, companion object 블럭에서 Service() 를 생성하는 팩토리를 구현한 예시이다.

class Service private constructor(val name: String) { //private 주 생성자

    companion object {
        // 이메일(email)을 기준으로 아이디를 분리해서 Service 인스턴스 생성
        fun newLodgingService(email: String) = Service(email.substringBefore("@"))
        // ID를 사용해서 Servicwe 인스턴스 생성
        fun newFlightService(id: Long) = Service("${id}")
    }
}

// 호출부
User.newLodgingService("skidrow6122@gmail.com")
User.newFlightService(500100071)

이러한 팩토리 패턴으로 사용하면 Service 인스턴스 생성시 서비스 타입에 따른 클래스가 각각 존재하는 경우에도 필요에 맞는 클래스를 생성해서 리턴 할 수 있다.

다만, 위 예제에서 Service class 를 class Service2: Service {…} 와 같이 상속이 필요한 경우라면, 동반 객체는 오버라이드가 불가능하므로 여러개의 secondary constructor를 쓰는것이 효율적이다.

 

 

사용예시 1)

사용자의 키를 암호화해서 주고 받는 api 를 가정해본다.

컨트롤러에서는 사용자의 key 를 암호화 된 형태로 받아 들이게 되고, 암호화 된 키를 풀어서 실제 DB 의 사용자 key 와 대조해서 인증한다.

UserIdentity 클래스에서는 암복호화에 필요한 Crypto 클래스에서 요구하는 iv를 static 하게 세팅하며, 내부 함수에서 기본적으로 디크립션 하게 된다.

data class UserIdentity(
    val userId: Long,
    val createdAt: String = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
) {

    fun makeIdentity(cryptoKey: String): String {
        return Crypto(cryptoKey, iv).enc(this)
    }

    companion object {
        val iv = IvParameterSpec("xxxxxxxxxxx".toByteArray())

        fun from(userIdentity: String, cryptoKey: String): UserIdentity {
            return Crypto(cryptoKey, iv).dec(userIdentity, UserIdentity::class.java)
        }
    }
}

컨트롤러에서는 UserIdentity 내 companion object 내 세팅된 iv로 파라미터 key를 디크립션 하며,

외부로 다시 리턴 해줄때 역시 UserIdentity 클래스를 호출하여, 해당 클래스의 내부에서 수행하는 암호화된 형태로 리턴한다.

@Operation(summary = "사용자 조회", description = "CI를 이용하여 사용자를 조회한다.")
    @PostMapping("/token")
    fun createToken(
        @RequestBody request: UserResources.Request.Identity
    ): Reply<String> {

        val ci = Crypto(openUserIdentityCryptoKey, UserIdentity.iv).dec(request.encryptedCi, String::class.java)
        val userId = userInteraction.findByCi(ci).id

        return UserIdentity(userId).makeIdentity(openUserIdentityCryptoKey)
            .toReply()
    }

 

사용 예시 2)

간단하게 redis 를 사용하는 예에서 디폴트 값을 주는 예제를 가정한다.

companion object 는 interface class 에서도 활용될 수 있으며, companion object 안에서 특정 변수를 세팅할 수 있다.

interface RedisKey {
    fun getKey(): String

    companion object {
        const val PREFIX = "api"
    }
}

 

사용 예시 3)

구조화 된 데이터 클래스에서 연산, 조회를 통해 값을 채워 넣는 패턴에서 사용한다.

@Schema(name = "Item.option")
            data class Option(
                val id: Long,
                val name: String,
                var description: String? = null,
                val price: Long? = null
            ) {
                companion object {
                    fun from(itemOption: ItemOption): Option {
                        return itemOption.run {
                            Option(id!!, name, description, price)
                        }
                    }
                }
            }

'Engineering > Kotlin' 카테고리의 다른 글

Constructor  (0) 2023.05.16
Interface VS Abstract?  (0) 2023.05.14

댓글