질문 정리

_username과 username 왜 나누는 것일까?(백킹 프로퍼티)

five2week 2024. 4. 16. 13:25

평소 프로젝트를 하며 다음과 같은 형식의 코드를 자주 사용했습니다.

private var _username: String? = null
var username: String?
    get() = _username
    set(value) {
        _username = value
    }

private var _password: String? = null
var password: String?
    get() = _password
    set(value) {
        _password = value
    }

_username와 username를 나누어 사용하는 이유는 코틀린에서 일반적으로 사용되는 "백킹 프로퍼티(backing property)" 패턴 때문입니다.

  1. 캡슐화 강화: 내부적으로 사용되는 _username 필드는 비공개(private)로 설정되어, 클래스 외부에서 직접 접근할 수 없습니다. 이는 클래스의 내부 상태를 보호하고, 외부에서 무분별한 접근을 제한하여 클래스의 무결성을 유지하는 데 도움이 됩니다.
  2. 접근 제어: username 프로퍼티를 통해 _username 필드의 접근을 제어할 수 있습니다. 예를 들어, 비밀번호를 설정할 때 유효성 검사를 추가하거나, 로깅을 수행하거나, 변경 알림을 발생시키는 등의 추가적인 로직을 삽입할 수 있습니다.
  3. 유연성: 프로퍼티에 로직을 추가할 수 있기 때문에, 나중에 요구사항이 변경되었을 때 기존의 필드 접근 방식을 수정하지 않고도 새로운 기능을 쉽게 통합할 수 있습니다. 이는 코드의 유지보수를 용이하게 합니다.
  4. 읽기 전용과 쓰기 전용 분리: 예를 들어, 어떤 프로퍼티는 외부에서 읽을 수만 있어야 하고, 내부적으로만 변경이 가능해야 할 경우, 백킹 프로퍼티를 사용하여 이러한 제한을 구현할 수 있습니다.

이러한 이유로, 비록 _username와 username가 결국 같은 데이터를 참조하고 있지만, 이렇게 분리해서 사용하는 것이 좋은 소프트웨어 설계 관행이며, 코드의 안정성과 확장성을 높인다고 생각합니다.

자바와 코틀린의 Property(해당 정리를 하게 된 이유)

로그인 스크린 부분의 코드를 리팩토링하던 도중, 다른 개발자분이 다음과 같은 형식으로 코드를 작성해둔 것을 보았습니다.

private var userName: String? = null
private var password: String? = null

fun getUserName(): String? {
    return userName
}

fun setUsername(user: String) {
    userName = user
}

fun setPassword(pass: String) {
    password = pass
}

fun getPassword(): String? {
    return password
}

자바의 경우 필드와 필드에 접근하기 위한 메서드를 위와 같이 정의해서 사용하는 경우가 많지만, 다음과 같은 방식이 좀 더 코틀린답다고 생각했습니다. 그래서 다음과 같이 수정했습니다.

private var _username: String? = null
var username: String?
    get() = _username
    set(value) {
        _username = value
    }

private var _password: String? = null
var password: String?
    get() = _password
    set(value) {
        _password = value
    }

 

위의 로직은 로그인의 경우 사용하는 viewmodel에 포함된 부분입니다. 생각해보면 View에서 정보를 ViewModel로 저장할 수는 있지만, 민감 정보인 아이디와 비밀번호를 다시 화면에 보여줄 일이 없었습니다. 하지만 다음과 같은 코드는 불가능했습니다. 코틀린에서는 프로퍼티의 getter 가시성이 프로퍼티 자체의 가시성보다 더 제한적인 수 없습니다. 따라서 프로퍼티의 가시성이 public이라면 getter의 가시성도 public이어야합니다.

// 올바르지 않은 코드
class LoginViewModel {
    private var _username: String? = null
    var username: String?
        private get() = _username
        set(value) {
            _username = value
        }

    private var _password: String? = null
    var password: String?
        private get() = _password
        set(value) {
            _password = value
        }
}

로그인 스크린 리팩토링에서 변수 최종 코드는 다음과 같아졌습니다.

private var _userName: String? = null
private var _password: String? = null

fun updateUserName(name: String) {
    _userName = name
}

fun updatePassword(pass: String) {
    _password = pass
}

+) 그 이후, SplashScreen을 리팩토링하며, 해당 코드는 사라졌습니다. 이전 개발자가 userName과 password 변수를 뷰모델에 만들어뒀던 이유가 뭐였을지 개인적으로 추측해봤습니다. 첫째로는 로그인 api를 요청하기 위해서이고, 둘째로는 형식 체크를 위해서입니다. 제 생각에는 로그인 api를 요청할 때, 당시에 형식 체크를 하고, 요청을 보내기 때문에 로그인 api를 호출하는 함수의 파라미터로 username과 password를 넣고, 변수에서 없애는 것이 더 깔끔한 방식입니다. 따라서 위의 코드를 제거했습니다.

사용하는 경우에 따른 예제 코드

1. 지연 초기화

class User {
    private var _email: String? = null
    val email: String
        get() {
            if (_email == null) {
                _email = loadEmailFromDatabase()
            }
            return _email!!
        }

    private fun loadEmailFromDatabase(): String {
        return "user@example.com"
    }
}

2. 값의 변경 감지

class Product {
    private var _price: Double = 0.0
    var price: Double
        get() = _price
        set(value) {
            if (value != _price) {
                println("Price changed from $_price to $value")
                _price = value
            }
        }
}

3. 조건부 접근 제어

class Account {
    private var _balance: Double = 0.0
    var balance: Double
        get() = _balance
        set(value) {
            if (value >= 0) {
                _balance = value
            } else {
                throw IllegalArgumentException("Balance cannot be negative.")
            }
        }
}

4. 읽기 전용 및 쓰기 전용 프로퍼티 구분

class Settings {
    private var _darkMode: Boolean = false
    val isDarkModeEnabled: Boolean
        get() = _darkMode

    fun enableDarkMode() {
        _darkMode = true
    }

    fun disableDarkMode() {
        _darkMode = false
    }
}

5. 값의 검증 및 변환

class Person {
    private var _age: Int = 0
    var age: Int
        get() = _age
        set(value) {
            if (value < 0 || value > 120) {
                throw IllegalArgumentException("Invalid age value: $value. Age must be between 0 and 120.")
            }
            _age = value
        }
}