티스토리 뷰

728x90

 

 

우아한테크코스에서 강의를 듣던 중, "유효성 검증이 필요한 불변 프로퍼티를 가변 프로퍼티로 변경하게 되었을 때, 그 프로퍼티에 대한 유효성 검증은 어떻게 할 것인가"에 대한 주제가 나왔었다.

이해를 위해 실제 코드를 보며 설명하겠다.

 

 


class Car(name: String, position: Int) {
    var name: String = name
    	private set
    
    var position: Int = position
    	private set
    
    init {
        require(name.length <= NAME_MIN_LENGTH)
    }
        
    fun move() { position++ }
    
    fun changeName(name: String) {
        this.name = name
    }
    
    companion object {
        private const val NAME_MIN_LENGTH = 5
    }
}

위 코드에서 name 프로퍼티는 원래 불변 프로퍼티였다. 또한 길이가 5이하여야 한다는 조건이 있다.

그런데 요구사항이 변경되어 name은 가변 프로퍼티가 되었고, changeName()이라는 함수를 통해 name을 변경할 수 있게 되었다.

 

하지만 name의 길이가 5 이하여야 한다는 조건은 init 블록 내에서만 검사한다.

즉, changeName()에서 길이가 5 초과인 name이 들어와도 오류 없이 동작한다.

 

이름을 변경할 때마다 길이에 대한 유효성 검증을 하기 위해 changeName() 안에서도 require()를 호출하는 방법이 있겠다.

이 때 단점은 유효성 검증 로직이 init 블록에서 한 번, changeName()에서 한 번, 총 두 번 중복된다는 것이다.

 

class Car(name: String, position: Int) {
    var name: String = name
    	private set(value) {
            require(value.length <= NAME_MIN_LENGTH)
            field = value
        }
    
    var position: Int = position
    	private set

    fun move() { position++ }
    
    fun changeName(name: String) {
        this.name = name
    }
    
    companion object {
        private const val NAME_MIN_LENGTH = 5
    }
}

fun main() {
    val car = Car("hm", 0)
    car.changeName("hyemin")
}

그래서 나는 커스텀 세터를 구현해서 name의 값을 변경하려고 할 때마다 유효성 검증을 하는 방식을 제안했었다.

하지만 이 방식은 생성자 파라미터를 통해 프로퍼티를 초기화하는 경우에는 커스텀 세터가 호출되지 않는다.

 

fun main() {
    val car1 = Car("hyemin", 0) // 5글자 이상이지만 예외가 발생하지 않는다
    
    val car2 = Car("hm", 0)
    car2.changeName("hyemin") // 5글자 이상이고 예외가 발생한다
}

즉, Car 객체를 생성할 때 5글자가 넘는 name을 전달해도, throw를 던지지 않고 정상적으로 동작한다.

 

이후에 CarName이라는 새로운 일급 컬렉션을 생성하고 그 클래스 내에서 유효성 검증 로직을 추가하는 방식으로 강의가 진행되었지만, 여전히 의문이 들었다.

 

제대로 이해하기 위해 강의가 끝나고 자바로 디컴파일을 해보았다.

public final class Car {
   @NotNull
   private String name;
   private static final int NAME_MIN_LENGTH = 5;

   @NotNull
   public final String getName() {
      return this.name;
   }

   private final void setName(String value) {
      boolean var2 = value.length() <= 5;
      if (!var2) {
         String var3 = "Failed requirement.";
         throw new IllegalArgumentException(var3.toString());
      } else {
         this.name = value;
      }
   }

   public final void changeName(@NotNull String name) {
      this.setName(name);
   }

   public Car(@NotNull String name) {
      super();
      this.name = name;
   }
}

(가독성을 위해 관심사가 아닌 코드들(position, companion object)은 제외했다.)

코틀린의 프로퍼티에는 getter와 setter를 가지고 있기 때문에, 자바로 디컴파일 했을 때 name에 대한 getName(), setName()이 생성된 것을 볼 수 있다.

getName()은 커스텀해주지 않았기 때문에 backing field만을 반환하는 디폴트 getter다.

 

여기서 setName()을 주목하자. 우리가 구현한, 유효성 검증 로직이 포함된 setter임을 알 수 있다.

changeName() 함수 내에 `this.name = name`으로 값을 변경했던 코드는, 이 커스텀 세터를 호출하도록 변경되었다.

 

그리고 Car 클래스 가장 마지막에 있는 생성자에서는 changeName()처럼 setName()을 호출하여 name의 값을 변경하지 않는다.

name의 field에 바로 접근해서 name을 초기화한다.

 

결론적으로, 클래스 내에서 프로퍼티를 접근하더라도 커스텀 게터와 커스텀 세터가 호출된다.

생성자를 통해 프로퍼티를 변경하면 커스텀 세터가 호출되지 않는다.

 

 

 


 

나는 코틀린에 대해 잘 안다고 생각하고 있었는데, 개념만 잘 알고 경우에 따라 어떤 식으로 동작하는지는 정확히 모르는 경우가 많았다.

이번처럼 커스텀 세터 호출 시기를 제대로 알지 못하면 의도에 맞지 않게 구현하게 될 것같다.

코틀린에서 헷갈리는 부분은 자바로 디컴파일 했을 때 더 와닿을 때가 있다.

우테코 크루들이랑 얘기하면서, 사소한 것도 디컴파일 해보며 정확하게 이해하는 것에 중요성을 깨닫는 중이다..

 

 

728x90
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함