Developing Myself Everyday
article thumbnail

사진: UnsplashHardik Pandya

 

 

 

최근에 프로젝트를 진행하면서 ViewModel에서 당연하다는 듯이 SharedFlowStateFlow를 사용해야 할 일이 많았습니다.

private val _errorFlow = MutableSharedFlow<Throwable>()
val errorFlow: SharedFlow<Throwable> get() = _errorFlow

private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState>
    get() = _uiState

 

 

이렇게 아무 생각 없이 사용하다 보니, 왜 이렇게 2개로 나눠서 관리해야 하지? 라는 원초적인 질문이 생겼습니다. 그래서 이번 게시글에서는 이를 이해하기 위한 FieldProperty 그리고 Backing Properties에 대해 알아보고 의문에 대한 답을 하고자 합니다.


 

 

 

 

Field는 코틀린에서는 없다!


저는 안드로이드 개발을 코틀린으로 시작했습니다. 그렇기 때문에 사실 Field에 대해서는 잘 알지 못합니다. 코틀린을 공부하면서 Backing Field를 접하기 전에는 Field에 대해서 감도 못잡은 상태였습니다.

 

다만, Backing Properties를 공부하기 위해서는 먼저 Backing Field를 설명해야하고, 이 전에 Field를 설명하는 것이 올바른 순서인 것 같다는 게 본능적으로 느껴집니다.

 

 

그럼 Field가 뭔가요?

 Field는 클래스내에 선언된 변수를 의미합니다. 위치에 따라 구분될 수 있지만, 여기서는 이렇게만 알아도 괜찮습니다.

 

Property는 바로 필드에 접근하기 위해 사용됩니다. 코틀린에서는 필드에 직접 접근하는 것이 아니라 바로 Property를 통해서 Field에 접근하게 됩니다.

 

코틀린에서 Property를 정의하면 컴파일 과정에서 자동으로 Fieldprivate하게 선언하고 이에 접근할 수 있는 gettersetter 메소드를 정의해 줍니다. 

class Main {
    val word: String = "test"
}

public final class Main {
   @NotNull
   private final String word = "test";

   @NotNull
   public final String getWord() {
      return this.word;
   }
}

 

 

val과 var

Property를 선언할 때 사용되는  valvar은 바로 필드에 접근하는 방식 즉 gettersetter를 자동으로 정의해 줍니다.

 

코틀린을 사용하셨다면 val은 변경할 수 없고 var은 변경할 수 있는 Property를 정의한다는 것을 아실 겁니다.

 

이 말은 valgetter만 정의하고 vargettersetter를 모두 정의해 준다는 것을 알 수 있습니다.

 

 

직접 보면 이해가 더 쉽습니다. 아래와 같이 2개의 클래스에 각각 val과 var로 Property를 정의해 보겠습니다.

class Val {
    val valValue = "test"
}

class Var {
    var varValue = "test"
}

 

 

이 바이트코드를 디컴파일 한 코드를 보겠습니다.

public final class Val {
   @NotNull
   private final String valValue = "test";

   @NotNull
   public final String getValValue() {
      return this.valValue;
   }
}

public final class Var {
   @NotNull
   private String varValue = "test";

   @NotNull
   public final String getVarValue() {
      return this.varValue;
   }

   public final void setVarValue(@NotNull String var1) {
      Intrinsics.checkNotNullParameter(var1, "<set-?>");
      this.varValue = var1;
   }
}

 

코드를 보면 Property에 접근할 수 있는 메서드를 val, var에 맞춰서 생성해 준다는 것을 알 수 있습니다.

 

 

 

 

Backing Field


Backing Field 또한 자바에는 존재하지 않는 개념입니다. 위의 예시에서 Property를 정의하면 자동으로 Field private 하게 선언해 준다고 말했습니다.

 

이때 바로 이 Field가 바로 Backing Field입니다. Backing Field라는 이름도 뒤에서 이를 지원해 준다고 해서 지어졌습니다.

 

 

 

 

get(), set() 메서드


Property를 정의하면 얻을 수 있는 getter setter 메소드를 Property 선언 과정에서 커스텀하게 정의할 수도 있습니다.

 

바로 get(), set() 메서드를 사용하면 됩니다.

 

아래와 같이 get(), set() 메서드를 정의하면 기본 getter setter 메소드에 해당 사항을 추가해서 생성해 주게 됩니다.

class Var {
    var varValue: String = ""
        get() = "var"
        set(value) {
            field += "text"
        }
}

public final class Var {
   @NotNull
   private String varValue = "";

   @NotNull
   public final String getVarValue() {
      return "var";
   }

   public final void setVarValue(@NotNull String value) {
      Intrinsics.checkNotNullParameter(value, "value");
      String var10001 = this.varValue;
      this.varValue = var10001 + "text";
   }
}

 

 

private set

set() 메서드에 private 생성자를 붙일 수도 있습니다. 이렇게 되면 해당 Property와 같은 클래스가 아닌 곳에서는 이 Property를 수정할 수 없게 됩니다.

 

 

 

 

Backing Properties


Backing PropertiesBacking Field에 적용했던 개념을 조금 더 넓혀서 Property에 적용한 것이라 봐야 합니다.

 

코틀린에서는 Property의 getter와 setter를 구분하기 위해 Property를 2가지로 나눠서 구분 지었습니다.

 

그렇기 때문에 이를 Backing Properties라고 복수형을 붙여서 부르는 이유기도 합니다.

 

 

아래와 같이 Backing Properties를 정의할 수 있습니다.

class Test1 {
    private var _word = "test"
    val word: String
        get() = _word
}

 

 

Backing Properties의 바이트코드를 디컴파일한 결과를 보면 특이한 점을 알 수 있습니다.

public final class Test1 {
   private String _word = "test";

   @NotNull
   public final String getWord() {
      return this._word;
   }
}

 

바로 이전에 정의했던 word의 필드가 사라졌다는 것입니다.

 

사실 그 이유는 단순합니다. word는 오직 _word의 결과를 반환하주고 있습니다. 그렇기 때문에 word의 field가 사용되지가 않기 때문에 컴파일러는 굳이 word의 field를 생성하지 않습니다.

 

 

이는 여러 가지 방면에서 장점이 될 수 있습니다. 만약 아래와 같이 get() 메서드를 사용하지 않고 그냥 처리한다고 가정해 보겠습니다.

class Test1 {
    private var _word = "test"
    val word: String = _word
}

 

 

그럼 코틀린 컴파일러는 자동으로 생성한 get() 메서드에서 Backing Field를 반환해야 하기 때문에 기존에 불필요했던 Field를 이번에는 만들어야 하고 값을 넣어주는 과정을 거쳐야 합니다.

public final class Test1 {
   private String _word = "test";
   @NotNull
   private final String word;

   @NotNull
   public final String getWord() {
      return this.word;
   }

   public Test1() {
      this.word = this._word;
   }
}

 

위 2가지 방식은 동일하게 작동합니다. 다만 디컴파일된 코드만 보더라도 어떤 방식이 보일러 플레이트 코드를 생성하지 않는지 확연하게 알 수 있습니다.

 

 

 

Backing Properties의 목적

Backing Properties의 목적은 그럼  뭘까요?

 

바로 Backing FieldPropertyField에 접근할 때, 캡슐화를 해 줬다면, 다른 클래스에서 클래스의 Property에 접근할 수는 있지만, 수정할 수는 없게 할 수 있습니다.

 

 

 

 

Backing Properties vs private set


다른 클래스에서 현재 클래스의 Property접근할 수는 있지만, 수정할 수는 없게 할 수 있다는 점은 사실 기존의 get(), set() 메서드를 통해 가능하긴 합니다.

 

아래의 예시를 보겠습니다. 아래와 같이 Backing Propertiesprivate set으로 Property를 정의했습니다. 

class Test1 {
    private var _word = "test"
    val word: String
        get() = _word
}

class Test2 {
    var word = "test"
        private set
}

 

 

 

이 바이트 코드를 디컴파일하면 놀라운 결과를 확인할 수 있습니다.

public final class Test1 {
   private String _word = "test";

   @NotNull
   public final String getWord() {
      return this._word;
   }
}

public final class Test2 {
   @NotNull
   private String word = "test";

   @NotNull
   public final String getWord() {
      return this.word;
   }
}

 

이 2가지의 방식은 많은 부분에 차이가 거의 없다는 것입니다!

 

 

그럼 이 2가지 방식은 서로 어떤 상황이든 대체할 수 있을까요? 

 

 

이건 또 그렇지많은 않습니다.

 

 

MutableListList의 경우를 예를 들어 보겠습니다.

 

우리가 원하는 것은 MutableList에 원소를 추가하고 싶습니다. 다만, 여기서 MutableList 객체 자체를 교체해서는 안됩니다.

 

이 경우에는 Backing Properties를 사용한다면 아주 쉽게 요구사항을 충족시킬 수 있습니다.

class Test1 {
    private val _words = mutableListOf<String>()
    val words: List<String>
        get() = _words
}

 

 

반면에 private set을 사용한다면 MutableList 객체 자체를 교체할 수 있게 만들 수밖에 없습니다.

class Test2 {
    var words = mutableListOf<String>()
        private set
}

 

 

이 2개의 디컴파일된 자바 코드는 아래와 같은 차이가 발생하게 됩니다.

public final class Test1 {
   private final List _words = (List)(new ArrayList());

   @NotNull
   public final List getWords() {
      return this._words;
   }
}

public final class Test2 {
   @NotNull
   private List words = (List)(new ArrayList());

   @NotNull
   public final List getWords() {
      return this.words;
   }
}

 

 

 

 

Compose에서 State를 사용할 때 By 사용


만약 Compose를 사용하고있고, 거기서 State를 사용한다면 다른 선택지가 존재합니다.

 

이전과 똑같이 Backing Properties를 사용하면 아래와 같이 작성할 수 있습니다.

class Test1 {
    private val _test = mutableStateOf(false)
    val test : State<Boolean> get() = _test
}

 

 

Compose에서는 State에 `by`를 붙이면 setValuegetValue 함수를 생성해 줍니다. 만약 이렇게 생성한 setValue에 private를 붙일 수만 있다면, Backing Properties로 얻을 수 있는 이점을 똑같이 얻을 수 있게 됩니다. 

 

바로 아래와 같이 할 수 있습니다.

class Test2 {
    var test: Boolean by mutableStateOf(false)
        private set
}

 

 

이 바이트 코드를 디컴파일해보면 아래와 같은 코드를 확인할 수 있습니다.

public final class Test2 {
   @NotNull
   private final MutableState test$delegate = SnapshotStateKt.mutableStateOf$default(false, (SnapshotMutationPolicy)null, 2, (Object)null);

   public final boolean getTest() {
      State $this$getValue$iv = (State)this.test$delegate;
      KProperty property$iv = null;
      int $i$f$getValue = false;
      return (Boolean)$this$getValue$iv.getValue();
   }

   private final void setTest(boolean var1) {
      MutableState $this$setValue$iv = this.test$delegate;
      Object var4 = null;
      Object value$iv = var1;
      int $i$f$setValue = false;
      $this$setValue$iv.setValue(value$iv);
   }
}

 

위의 코드를 보게되면 우리가 원하는 대로 field를 private하게 그리고 변경할 수 없게 final로 선언되어 있습니다. 그리고 이 field를 수정할 수 있는 함수는 private하게 선언되어 있기 때문에 우리가 원하는 대로 작동하는 것을 알 수 있습니다.

 

 

 

 

결론


이렇게 해서 Backing Properties를 이해하기 위한 여러 가지 것들을 공부하였고, Backing Properties를 왜 사용해야 하며, 대체할 수 있는 상황과 그럴 수 없는 상황도 알아보았습니다. 

이젠 누가 Backing Properties를 왜 사용했냐고 물어본다면, 자신 있게 답할 수 있을 것 같습니다.

 

 

 

 

Reference

 

I have a hard time understanding the purpose of a "backing property"

I am learning Kotlin right now. For context, I am a Java developer for +10 years. I stumbled upon the concept of backing properties. As I understand it, the problem to be solved is this: I have a

stackoverflow.com

 

Private setter vs Backing Property in Kotlin

I'm an old Java fan and trying to understand Kotlin basics. Can someone tell me what is the difference between these codes: private val _users = mutableListOf<User>() val users: List<User&...

stackoverflow.com

 

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!