Uknow's Lab.
article thumbnail

 

코틀린 클래스의 생성자와 getter, setter

public class Member {

    private String name;
    private int age;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

 

자바에서 데이터를 담기 위한 클래스를 작성할 때,

name, age 라는 두 개의 필드만 존재하더라도

필드에 대한 생성자, getter, setter 등 보일러 플레이트 코드가 다소 존재합니다.

 

 

class Member(val name: String, val age: Int)

 

반면 코틀린의 경우,

위와 같이 클래스를 선언해놓는다면

생성자와 getter, setter 까지 모두 만들어져

위의 장황한 코드가 단 한 줄로 표현이 가능합니다.

때문에 코틀린의 클래스 내의 변수는 자바의 필드, getter, setter를 통합한 프로퍼티(property)라는 이름으로 불립니다.

 

자바에서도 Lombok을 사용하면 깔끔하게 표현할 수 있으나,

별도 플러그인이 아닌 언어 자체에서 지원한다는 점은 충분한 메리트지요.

 

 

 

getter만 열어두고 setter를 닫아놓아라

객체지향 프로그래밍에 대해 배울 때,

getter만 열어두되 setter를 닫아놓아야 좋다는 말이 있습니다.

 

이는 데이터의 캡슐화와 불변성과 관련이 있는데요.

클래스의 외부에서 객체 내 변수의 값을 마음대로 설정이 가능하면,

객체의 필드의 값의 흐름을 파악하는데 어려움을 줍니다.

 

 

public class Member {

    private String name;
    private int age;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
    
    public void update(String name, int age) {
        this.name = name;
        this.age = age;
    }

}

 

때문에 자바에서는 위와 같이 setter를 닫아두고

업데이트가 필요할 경우 객체에 메시지를 던지는(위 코드에서는 update 메서드)

방향으로 객체를 캡슐화하여 불변성을 구현하였습니다.

 

 

 

 

setter를 닫아두는 방법 1 : read-only, val 변수

프로그램의 안정성을 높이고 예측하지 못한 행동을 방지하기 위해

변수의 흐름을 예측하고, 추적하기 위한 방법은 꽤나 여러 방법으로 연구되어왔습니다.

 

그 중 하나가 바로 불변성과 가변성인데,

코틀린에서는 val과 var라는 두 가지 변수 타입을 두는 것으로

불변과 가변을 좀 더 명확히 구분하여 변수의 흐름을 파악하고

가독성을 높이는데 도움을 주기 위해 고안되었습니다.

 

class Member(val name: String, val age: Int)

fun main() {
    val member = Member("Kotlin", 1)
    member.age = 2 // 컴파일 에러 발생
}

 

val로 선언된 변수는 한 번 선언한 이후 바꿀 수 없는 불변성을 가집니다.

때문에 member.age = 2와 같이 다시 값을 재할당하려 하면 컴파일 에러가 발생합니다.

getter만 열어둔 채 setter는 닫혀있는 형태지요.

 

그런데 이 때, 나이(age)와 같은 변수는 시간의 흐름에 따라 수정이 필요할 수도 있습니다.

하지만 val로 선언되어 있어 클래스 외부에서 뿐 아니라 클래스 내부에서 까지 업데이트가 불가능한 상황입니다.

이 때, 어떻게 getter만 열어두고, setter를 닫아놓는 형태로 만들 수 있을까요?

(실제 서비스에서는 생년월일을 입력받아 나이를 계산하겠지만,

예시를 위해 나이를 직접 입력 / 업데이트 한다고 가정하겠습니다)

 

 

 

 

setter를 닫아두는 방법 2 : backing properties

class Member(
    private var _name: String,
    private var _age: Int
) {
    val name: String
        get() = _name

    val age: Int
        get() = _age

    fun update(name: String, age: Int) {
        _name = name
        _age = age
    }
}

fun main() {
    val member = Member("Kotlin", 1)
    
    println(member.name)
    println(member.age)
    
    member.update("Java", 2)
    
    println(member.name)
    println(member.age)
}

======================================

// 출력
Kotlin
1
Java
2

 

 

두 번째 방법은 뒷받침 프로퍼티(backing propertiy)를 쓰는 것입니다.

 

클래스의 프로퍼티의 접근제어자를 private로 만들고,

변수명의 앞에 _를 붙입니다.

 

이후, 클래스의 바디 부분에 _를 뗀 프로퍼티를 추가하고,

커스텀 getter를 만들어 해당 프로퍼티를 getter로써 접근하면

_를 뗀 프로퍼티의 값을 반환해줍니다.

말 그대로, 뒷받침해주는 프로퍼티(backing property)인 것이죠.

 

변수명 앞에 언더스코어(_)를 붙이는 게 다소 어색하긴 하지만,

코틀린 공식 문서에서도 소개하고 있는 공식 컨벤션입니다.

https://kotlinlang.org/docs/properties.html#overriding-properties

 

Properties | Kotlin

 

kotlinlang.org

 

 

 

 

setter를 닫아두는 방법 3 : private setter

class Member(
    name: String,
    age: Int
) {
    var name: String = name
        private set

    var age: Int = age
        private set

    fun update(name: String, age: Int) {
        this.name = name
        this.age = age
    }
}

fun main() {
    val member = Member("Kotlin", 1)
    member.name = "Python" // 컴파일 에러
}

 

세 번째 방법은 private set을 이용하는 방법입니다.

2번 방법과 비슷하게 클래스 바디 부분에 프로퍼티를 선언하고,

setter를 private으로 설정하여

외부로부터 setter에 접근하지 못하게 막는 방법입니다.

 

 

 

setter를 열어두되, 사용하지 않도록 규칙을 정하기

사실 앞의 두 방법은 필드가 많아지면 다소 번거로워질 수 있습니다.

자바와 비교했을 때, 코틀린의 핵심적인 장점 중 하나가 바로 깔끔한 문법인데, 

1줄로 끝낼 수 있었던 클래스 선언 및 생성자, getter, setter 정의가

자바만큼은 아니지만 조금 길고 번거로워졌습니다.

 

마지막 방법은 사실 가장 단순한 방법인데요.

setter가 열려있는 채 납두면서, 팀원들끼리 setter를 사용하지 말자고 규칙을 정하는 것입니다.

 

아주 간단하고 깔끔한 방법이긴 하나,

setter를 닫아놓는 이유 중 하나가 실수로 값을 변경해

프로그램의 예기치 못한 동작을 처음부터 방지하는 것인데,

안전장치 없이 그냥 규칙만으로 괜찮을까..? 라는 의문이 남긴 합니다만

팀의 구성원이 setter의 리스크와 setter를 사용하지 말자는 규칙을 잘 숙지한다면 괜찮을 것 같네요.

 

비록 setter가 열려있어 setter로 인한 예기치 못한 작동을 원천차단 할 수는 없으나,

trade - off 관계라 볼 수 있을 것 같습니다.

 

 

 

 

마치며

코틀린을 하면서 가장 편했던 점 중 하나가 바로

단 한 줄로 생성자 + getter + setter를 모두 만들 수 있다는 점이였습니다.

 

하지만 객체지향을 공부해보며 setter의 리스크와 setter를 닫아두는 컨벤션들을 보며

그럼 코틀린에서는 어떻게 하지...? 라는 궁금중으로

코틀린에서 setter를 다루는 방법에 대해 알아보았습니다.

 

자바가 오랜 세월에 걸쳐 많은 발전을 이루었듯이,

추후에 코틀린도 더 깔끔한 방법으로 setter를 다룰 수 있는 날이 올 것이라 기대하며

오늘의 글 마치겠습니다.

 

 

p.s

setter를 닫아놓는게 좋다는 것을 전제로 글을 작성하긴 했지만,

그렇다고 setter를 쓰는것이 꼭 나쁜 것 만은 아닙니다.

객체의 설정값을 일일히 하나 하나씩 수정해야 할 일이 잦을 경우,

객체에 메시지를 던지는 것 보다 setter를 사용하는게 훨씬 간단할 수 있습니다.

 

특히 웹 프론트엔드나 안드로이드, Swing 등 UI 쪽 개발에 관해서는

MVVM의 등장으로 setter를 쓸 일이 줄어들었다곤 하나,

아직까지 setter를 써야 하거나, setter를 쓰는게 편한 경우가 종종 있거든요.

 

세상에 무조건 좋다 / 무조건 안좋다는 없듯이

setter를 쓰는게 무조건 안좋다기 보다는

이 역시 편의성과 안전성의 trade-off 관계로 보는게 좋다고 생각합니다.

 

 

같이 볼만한 글

https://uknowblog.tistory.com/229

 

[코파기 2부] 2. 코틀린과 프로퍼티와 setter/getter. 여기에 data class를 곁들인.

POJO (Plain Old Java Object) class Person { String name; int age; int height; double weight; public Person(String name, int age, int height, double weight) { this.name = name; this.age = age; this.height = height; this.weight = weight; } public String getN

uknowblog.tistory.com

 

 

 

 

참고했던 글

https://kotlinlang.org/docs/properties.html

https://www.baeldung.com/kotlin/getters-setters

https://stackoverflow.com/questions/37906607/getters-and-setters-in-kotlin

profile

Uknow's Lab.

@유노 Uknow

인생은 Byte와 Double 사이 Char다. 아무말이나 해봤습니다.