자바 빈
- 최초에는 GUI 개발을 지원하기 위해 태어났다.
- GUI 에서의 많은 프로퍼티 (패딩, 높이, 너비 등) 를 적절하게 컨트롤하기 위해
Getter
와Setter
가 기본으로 장착됐다. - 이 당시엔 UI 툴킷과 일반 객체 모두 가변 컴포넌트 모델이 옳다는 사고관을 가지고 있었다.
- 근래엔 객체로 표현하고 싶은 대부분의 대상에는 '자바빈즈' 보다는 '값(불변)' 이 더 낫다고 여겨진다.
- 자바에서의 가변 객체들은 최근 복잡도를 높이는 문제가 되어가고 있다.
값 혹은 불변을 선호해야 하는 이유
- 가변 객체보다 추론이 쉽다.
- 맵의 키, 집합 원소로 활용 가능하다. (의미론적 동치 판단이 쉽다.)
- 컬렉션으로 사용하는 경우, 원소가 달라질지 염려할 필요가 없다.
- 초기 상태를 복사하지 않고도 다양한 시나리오를 탐험할 수 있다.
- 되돌리기, 다시하기 등을 쉽게 구현 가능하다. (그 때의 객체만 기억해두면 된다.)
- 여러 스레드에서 안전하게 공유하며 사용할 수 있다. (다른 스레드에서 변경될 걱정이 없다.)
자바 빈 살펴보기
UserPreferences
객체
public class UserPreferences {
private String greeting;
private Locale locale;
private Currency currency;
public UserPreferences() {
this("Hello", Locale.UK, Currency.getInstance(Locale.UK));
}
public UserPreferences(String greeting, Locale locale, Currency currency) {
this.greeting = greeting;
this.locale = locale;
this.currency = currency;
}
public String getGreeting() {
return greeting;
}
public void setGreeting(String greeting) {
this.greeting = greeting;
}
public Locale getLocale() {
return locale;
}
public void setLocale(Locale locale) {
this.locale = locale;
}
public Currency getCurrency() {
return currency;
}
public void setCurrency(Currency currency) {
this.currency = currency;
}
}
- 유저의 설정에 따라 인사 메시지, 언어 설정, 통화 설정을 변화시키는 클래스로 보인다.
Application
객체
public class Application {
private final UserPreferences preferences;
public Application(UserPreferences preferences) {
this.preferences = preferences;
}
public void showWelcome() {
new WelcomeView(preferences).show();
}
public void editPreferences() {
new PreferencesView(preferences).show();
}
}
- 인자로 받은
preferences
에 따라 환영 메시지를 보여주거나preferences
를 편집할 수 있는 화면으로 안내하는 것으로 보인다.
WelcomeView
객체
public class WelcomeView extends View {
private final UserPreferences preferences;
public WelcomeView(UserPreferences preferences) {
this.preferences = preferences;
}
}
- 단순히 환영 메세지를 띄우는 뷰로 추정된다.
PerferencesView
객체
public class PreferencesView extends View {
private final UserPreferences preferences;
private final GreetingPicker greetingPicker = new GreetingPicker();
private final LocalePicker localePicker = new LocalePicker();
private final CurrencyPicker currencyPicker = new CurrencyPicker();
public PreferencesView(UserPreferences preferences) {
this.preferences = preferences;
}
public void show() {
greetingPicker.setGreeting(preferences.getGreeting());
localePicker.setLocale(preferences.getLocale());
currencyPicker.setCurrency(preferences.getCurrency());
super.show();
}
protected void onGreetingChange() {
preferences.setGreeting(greetingPicker.getGreeting());
}
protected void onLocaleChange() {
preferences.setLocale(localePicker.getLocale());
}
protected void onCurrencyChange() {
preferences.setCurrency(currencyPicker.getCurrency());
}
}
class GreetingPicker {
private String greeting;
public String getGreeting() {
return greeting;
}
public void setGreeting(String greeting) {
this.greeting = greeting;
}
}
class LocalePicker {
private Locale locale;
public Locale getLocale() {
return locale;
}
public void setLocale(Locale locale) {
this.locale = locale;
}
}
class CurrencyPicker {
private Currency currency;
public Currency getCurrency() {
return currency;
}
public void setCurrency(Currency currency) {
this.currency = currency;
}
}
- 실제로
Preferences
를 변경할 수 있는 화면으로 보인다. - 각
XxxPicker
패턴의 내부 클래스에setXxx()
메서드를 통해 필드 값을 변경하도록 구성해두었다. - 얼핏 보면, 각 필드에
final
선언을 통해 불변을 지킨것처럼 보이지만, 각 필드 타입이 값이 아닌 가변 객체여서 가변 객체의 문제점을 그대로 안고 있다.PreferencesView
와WelcomeView
가 모두 모바일 화면 스택에 쌓여있다고 했을 때,WelcomeView
의 상태가 현재 값과 달라질 수 있다. (초기Application
객체 생성 시 받은 값을 이용했기 때문에)UserPreferences
는 동등성과 해시코드가 가변 프로퍼티 값에 따라 결정되어, 집합에 넣거나 맵의 키로 사용할 수 없다.WelcomeView
가 사용자 설정만을 읽는다는 사실을 알려주는 정보가 없다.- 읽기와 쓰기가 다른 스레드에서 발생하는 경우, 설정 프로퍼티 수준에서 동기화를 해야 한다.
자바 빈 -> 코틀린으로 변환하기
- 불변 객체로 변환하기 전에 먼저 언어부터 변환해보자.
Application.kt
class Application(private val preferences: UserPreferences) {
fun showWelcome() {
WelcomeView(preferences).show()
}
fun editPreferences() {
PreferencesView(preferences).show()
}
}
- 생성자만 짤막해졌다.
UserPreferences.kt
class UserPreferences @JvmOverloads constructor(
var greeting: String = "Hello",
var locale: Locale = Locale.UK,
var currency: Currency = Currency.getInstance(Locale.UK)
)
@JvmOverloads
는 프로퍼티를 이용하여 경우의 수에 따른 다양한 디폴트 생성자를 만들어주는데, 이로 인해 이전 코드보다 대단히 짧아졌다.- 기존엔 2개의 생성자가 존재했다. (기본 값을 넣은 생성자, 객체만 생성하는 빈 생성자)
- 원래의 자바코드는 이런 일을 하지 않는다.
Getter
,Setter
도 사라졌다.- 각 필드는
final
이 아니었으므로var
로 변환되었다.- 이로 인해 비공개 필드,
Getter
,Setter
가 만들어진다.
- 이로 인해 비공개 필드,
UserPreferences
를 불변으로 만드는 방법
- 추후에
UserPreferences
클래스의 불변을 유지하기 위해 이 클래스의 필드 값을 바꾸는 것이 아닌Application
에서 가리키는UserPreferences
를 변경하도록 해보자. PreferencesView
가 새로운UserPreferences
를 반환하도록 하여Application
이 반환된 새로운 객체를 바라보게 하자.- '가변 객체에 대한 불변 참조' -> '불변 객체에 대한 가변 참조' 로 형태가 바뀐다.
- 결과적으로 일어날 수 있는 변화의 범위를 제한하여, 문제가 일어났을 때 어느 부분을 봐야 할지가 명확해질 것이다.
PreferencesView.kt
class PreferencesView(
private val preferences: UserPreferences
) : View() {
private val greetingPicker = GreetingPicker()
private val localePicker = LocalePicker()
private val currencyPicker = CurrencyPicker()
fun showModal(): UserPreferences {
greetingPicker.greeting = preferences.greeting
localePicker.locale = preferences.locale
currencyPicker.currency = preferences.currency
show()
return preferences
}
protected fun onGreetingChange() {
preferences.greeting = greetingPicker.greeting
}
protected fun onLocaleChange() {
preferences.locale = localePicker.locale
}
protected fun onCurrencyChange() {
preferences.currency = currencyPicker.currency
}
}
internal class GreetingPicker {
var greeting: String = ""
}
internal class LocalePicker {
var locale: Locale = Locale.UK
}
internal class CurrencyPicker {
var currency: Currency = Currency.getInstance(Locale.UK)
}
show()
는View
에 있는 메서드를 오버라이드 한 것으로 뷰를 눈에 보이게 만들고 뷰가 끝날 때까지 호출하는 쪽 스레드를 블록시킨다고 하자.showModal()
은 단순히show()
를 하는 것이 아니라, 이젠UserPreferences
타입을 반환하는 메서드가 되었다.
Application.kt
class Application(
private var preferences: UserPreferences // <1>
) {
fun showWelcome() {
WelcomeView(preferences).show()
}
fun editPreferences() {
preferences = PreferencesView(preferences).showModal()
}
}
preferences
가 가변 필드가 되었다.- 이제
editPreferences()
는showModal()
에서 반환한UserPreferences
를 새로 할당한다.
PreferencesView.kt
class PreferencesView(
private var preferences: UserPreferences
) : View() {
private val greetingPicker = GreetingPicker()
private val localePicker = LocalePicker()
private val currencyPicker = CurrencyPicker()
fun showModal(): UserPreferences {
greetingPicker.greeting = preferences.greeting
localePicker.locale = preferences.locale
currencyPicker.currency = preferences.currency
show()
return preferences
}
protected fun onGreetingChange() {
preferences = UserPreferences(
greetingPicker.greeting,
preferences.locale,
preferences.currency
)
}
protected fun onLocaleChange() {
preferences = UserPreferences(
preferences.greeting,
localePicker.locale,
preferences.currency
)
}
protected fun onCurrencyChange() {
preferences = UserPreferences(
preferences.greeting,
preferences.locale,
currencyPicker.currency
)
}
}
internal class GreetingPicker {
var greeting: String = ""
}
internal class LocalePicker {
var locale: Locale = Locale.UK
}
internal class CurrencyPicker {
var currency: Currency = Currency.getInstance(Locale.UK)
}
- 프로퍼티 값에 변화가 있을 때마다 새로운
UserPreferences
를 생성한다. - 이제
UserPreferences
에 있는Setter
들은 전부 필요 없어진다.UserPreferences
에 변화를 주자.
UserPreferences.kt
data class UserPreferences(
val greeting: String,
val locale: Locale,
val currency: Currency
)
- 필드 값이 불변으로 변했다.
data class
가 되어, 이젠 값이 갖는 장점들을 마찬가지로 가질 수 있을 것이다.Locale
과Currency
가 잘 구현되어 있다면.
data class
를 이용할 때는copy()
메서드가 자동으로 생기는 것도 잊지 말자. 생성자를 통한 유효성 검증 시에copy()
메서드 노출이 심각한 취약점이 될 수도 있다.
리팩토링으로써 얻은 것
- 공유 가변 데이터에 대한 불변 참조 두개를 불변 값에 대한 가변 참조 두개로 바꾸었다.
- 가변 영역을 위 (
Application
단) 로 끌어올렸다. - 가변 영역을 위로 끌어 올려 애플리케이션 단에서 상태 변경을 관리할 수 있게 되었다.
가변 객체의 동등성이나 해시코드에 의존하지 않도록 항상 주의하자.
PreferencesView.kt
마지막 리팩토링
class PreferencesView : View() {
private val greetingPicker = GreetingPicker()
private val localePicker = LocalePicker()
private val currencyPicker = CurrencyPicker()
fun showModal(preferences: UserPreferences): UserPreferences {
greetingPicker.greeting = preferences.greeting
localePicker.locale = preferences.locale
currencyPicker.currency = preferences.currency
show()
return UserPreferences(
greeting = greetingPicker.greeting,
locale = localePicker.locale,
currency = currencyPicker.currency
)
}
}
internal class GreetingPicker {
var greeting: String = ""
}
internal class LocalePicker {
var locale: Locale = Locale.UK
}
internal class CurrencyPicker {
var currency: Currency = Currency.getInstance(Locale.UK)
}
- 생성자 인자로 받은
UserPreferences
를 가변적으로 변화시키는 것이 아니라, 아예 새로운UserPreferences
를 만드는 방식으로 코드를 변경했다. showModal()
메서드에서 파라미터로UserPreferences
를 받는다.
Application.kt
마지막 리팩토링
class Application(
private var preferences: UserPreferences
) {
fun showWelcome() {
WelcomeView(preferences).show()
}
fun editPreferences() {
preferences = PreferencesView().showModal(preferences)
}
}
- 이제는 사용자 설정이 변경될 수 있는 지점은
editPreferences()
단 한군데밖에 존재하지 않는다. - 의미 없는
editPreferences()
를 수행할 때마다 새로운 객체가 생성되어 낭비가 일어날 수 있다고 생각할 수 있지만, 실제로는 값이 된UserPreferences
는 동등성을 판단하기 매우 쉬우므로, 변화가 있을 때만 객체를 만들도록 하는 것도 매우 쉽다.
소감
- 가변 객체의 위험성을 깨닫고, 가변 영역을 위로 끌어올리고 나머지를 불변으로 만드는 과정을 통해 앱의 복잡도가 크게 줄었다.
- UI 영역이 아닌 곳에서는 대부분 불변 객체가 정답이다.
- 그래도 여전히 UI 에서는 엄격한 생명주기 요구사항이 있는 경우, 가변 객체와 변경 이벤트를 사용하는 쪽을 선호할 수도 있다.
반응형
'코틀린 (Kotlin) > 자바에서 코틀린으로' 카테고리의 다른 글
자바에서 코틀린으로 6장 - 자바에서 코틀린 컬렉션으로 요약 (0) | 2022.12.06 |
---|---|
자바에서 코틀린으로 4장 - 옵셔널에서 널이 될 수 있는 타입으로 요약 (0) | 2022.11.30 |
자바에서 코틀린으로 3장 - 자바클래스에서 코틀린 클래스로 요약 (0) | 2022.11.30 |