개발 공부 기록하기/01. JAVA & Kotlin

[코틀린 탐구생활] when, 그리고 클린 코드

lannstark 2021. 1. 20. 20:20

when

사실 when expression을 처음 보았을때 들었던 생각은 switch case문과 무척 유사하다는 점이었다.

Java에서는 else 사용을 지양해야 한다는 일반적인 clean code 원칙에 따라 switch case 사용도 지양하고 있었는데, Kotlin의 when은 Java의 switch case보다 훨씬 단순하고 강력했다.

비교를 해보자.

Kotlin 숫자가 들어오면 숫자를 3으로 나눈 나머지에 따라 적절한 로직을 수행하고 원하지 않는 형태 이면 exception을 뱉어야 한다고 하자.

이를 Early Return 스타일로 표현하면 다음과 같다. (println에 로직이 들어간다고 생각할 수 있다)

private fun validateNumber(num: Int) {
  if (num % 3 == 0) {
    println("숫자가 3의 배수입니다!")
    throw IllegalArgumentException("잘못된 숫자: $num")
  }

  if (num % 3 == 1) {
    println("숫자가 3n+1 형태입니다!")
    throw IllegalArgumentException("잘못된 숫자: $num")
  }

  println("우리가 원하는 형태!! 3n+2 입니다")
}

프로그램을 작성하다보면 어딘가에서는 분기처리를 해주어야 하고, 그 분기처리의 가독성을 위해 early return을 사용하는 것은 굉장히 자연스럽게 느껴진다.

 

이제 이를 Kotlin의 when 으로 바꿔보자.

private fun validationNumberWhen(num: Int) {
  when (num % 3) {
    0 -> {
      println("숫자가 3의 배수입니다!")
      throw IllegalArgumentException("잘못된 숫자: $num")
    }
    1 -> {
      println("숫자가 3n+1 형태입니다!")
      throw IllegalArgumentException("잘못된 숫자: $num")
    }
    else -> println("우리가 원하는 형태!! 3n+2 입니다")
  }
}

when 의 경우도 가독성이 크게 떨어지지는 않는 느낌이다. 물론 early return이 조금 더 좋아보이기는 하지만 그것이 지금까지의 익숙함 때문인지 정말 좋기 때문인지는 구별할 필요가 있어 보인다.

눈에 보이는 when의 단점이라면 depth가 2개라는것..? 함수에 들어와 when으로 인해 첫 번째 depth가 생기고 나서 when에 들어온 값에 따라 한 번더 depth가 생기게 된다.

 

이제 로직을 처리하거나 exception을 뱉어야 하는 경우가 아닌 조건에 따라 값을 반환해야 하는 경우를 생각해보자.

문자열로 된 파라미터를 받고, 그 값에 따라 다른 정수를 반환해주는 함수이다. early return으로 이루어진 로직.. 익숙하다

private fun isBlogOwner(name: String): Int {
  if (name == "lannstark") {
    return 0
  }

  if (name == "lannstank") {
    return 1
  }

  return 2
}
private fun isBlogOwner(name: String): Int {
  return when (name) {
    "lannstark" -> 0
    "lannstank" -> 1
    else -> 2
  }
}

when expression을 사용하는 로직.. 각각이 early return 임을 생각하고 들여다 보면 가독성이 떨어지지 않는다. (바로 값을 반환하기 때문에 이번에는 depth가 한 번 더 들어가지 않는다)

 

가독성 측면을 살펴보았으니 when의 기능적인 면을 살펴보자

when (value) 구문은 value에 주어지는 값을 switch case 문처럼 바로 사용할 수도 있고, 서로 다른 value에 대해 같은 로직이 필요한 경우 쉽게 표현이 가능하다

when (x) {
  0, 1 -> print("x == 0 or x == 1") // 사실 이 뒤에 return이 존재한다
  else -> print("otherwise")
}

또한 in 예약어와 range를 이용해 between을 판단하는 것 역시 쉽게 가능하다.

when (x) {
  in 1..10 -> print("x is in the range")
  !in 10..20 -> print("x is outside the range")
  else -> print("none of the above")
}

is 예약어를 이용해 타입 체크도 가능하다.

fun hasPrefix(x: Any) = when(x) {
  is String -> x.startsWith("prefix")
  else -> false
}

결론적으로 when 사용에 대해 부정적이었던 처음과 달리, 어느정도 익숙해지고 각 경우에 자동으로 return이 있음에 적응이 된다면 early return과 비슷한 수준의 가독성을 가질 수 있을 것으로 기대된다.

또한 in 이나 is 를 이용해 다양한 조건을 한 번에 확인할 수 있는 것도 장점이라고 생각된다.

 

단, early return을 이용하기 위해서는 대부분의 경우 method 분리를 해야 하고, 그로 인해 method가 쪼개지며 함수 단위 코드 가독성이 좋아지는 장점도 함께 있다고 생각한다. 하지만 when expression을 사용하게 되면 함수를 쪼갤 필요 없이 상위 함수에서 바로 값을 넣어줄 수 있기 때문에 무작정 when expression을 사용한다면 method가 함수 단위의 가독성이 떨어질 수 있어 보인다. 때문에 when 을 사용하더라도 다른 clean code 원칙들과 종합적인 고려를 통해 사용해야 한다고 생각한다.

 

(최초 작성 후 추가)

추가적인 쓰임새

when이 일반적인 switch case와 다른 점 하나가 더 존재한다.

Enum class나 sealed class를 사용하는 경우 모든 경우가 처리되었는지 컴파일 단에서 처리된다는 점이다.

예를 들어 Enum 타입별로 다른 로직을 수행해야 하고 그 로직이 Enum 내부에 있으면 안된다고 가정하자 (로직 처리를 위해 Bean이 필요하거나, 책임/역할 측면에서 적절하지 않거나 등등)

그 경우 java 였다면 early return을 활용해 다음과 같이 작성했을 것이다.

java

enum YAY {
  A, B, C, D
}

if (enum == A) {
  // logic
  return 1;
}

if (enum == B) {
  // logic
  return 2;
}

if (enum == C) {
  // logic
  return 3;
}

// Enum D가 여기까지 내려올 수 없다
throw new IllegalStateException("적절한 에러 메시지");

이 로직을 그냥 Kotlin의 when으로 바꿔보면 이렇다.

kotlin when

return when (enum) {
  A -> {
    // logic
    1
  }
  B -> {
    // logic
    2
  }
  C -> {
    // logic
    3
  }
  else -> throw IllegalStateException("적절한 에러 메시지")
}

이 코드는 다음과 같이 작성할 수도 있는데, 이게 가능한 이유는 Kotlin에서의 When은 enum 또는 sealed class에 대해 모든 경우가 처리되었는지 커버해주기 때문이다. (// logic은 생략하였다)

return when (enum) {
  A -> 1
  B -> 2
  C -> 3
  D -> throw IllegalStateException("적절한 에러 메시지")
}

이런 코드가 장점을 발휘하는 순간은 새로운 Enum이 추가되었을 때이다. 기존의 자바 코드 혹은 else를 사용한 Kotlin when 코드 라면 새로운 Enum이 추가되었을때 개발자가 이를 인지하지 못하고 놓칠 수 있다.

 

만약 새로운 Enum에 대해 해당 로직이 수행될 일이 없다면 천운이겠지만 만약 적절한 로직이 수행되어야 한다면 아주아주 큰 일이 발생하게 된다.

하지만 모든 타입을 명시한 Kotlin when 코드라면 다음과 같은 컴파일 에러가 나게 된다.

'when' expression must be exhaustive, add necessary 'NEW_TYPE' branch or 'else' branch instead

때문에 개발자는 해당 enum으로 인해 분기가 쳐진 곳을 빠지지 않고 개발할 수 있게 된다.

 

물론 첫 번째 경우에 테스트 코드를 (당연히) 작성함으로써 확인할 수 있긴 하지만 컴파일 레벨로 확인되는 것과는 엄연히 다른 느낌이다.

이런 방법은 Enum 외에 sealed class에도 적용 가능하다.

끗!