티스토리 뷰

📢  Java의 enum과 Kotlin의 enum은 비슷하지만 반드시 알아야 하는 차이점이 있습니다. 이와 더불어 코틀린에서 변화가 생긴 if와 새롭게 추가된 분기 처리를 위한 강력한 식(Expression)인 when에 대해 살펴보겠습니다.

 

#1_ 들어가며

Programming을 하다보면 반드시 필요한 게 분기 처리입니다. 조건에 따른 처리를 명시함으로써 여러 예측 가능한 상황들에 유연하게 대처할 수 있기 때문입니다. 전통적인 프로그래밍 언어에서는 if문(statement)이나 switch문(statement)을 사용해서 이를 처리합니다.

if(input == 1) {
  // 구구단 1단 출력
} else if(input ==2) {
  // 구구단 2단 출력
} else {
  // 에러 메시지 출력
}

switch(input) {
  case 1: // 구구단 1단 출력
    break;
  case 2: // 구구단 2단 출력
    break;
  default: // 에러 메세지 출력
}

Kotlin에서는 if식(expression)과 when식(expression)을 통해 분기 처리가 가능합니다.

if(input == 1) {
  // 구구단 1단 출력
} else if(input == 2) {
  // 구구단 2단 출력
} else {
  // 에러 메세지 출력
}

when(input) {
  1 -> // 구구단 1단 출력
  2 -> // 구구단 2단 출력
  eles -> // 에러 메세지 출력
}

여기서 재미있는 점은 if가 자바에서는 문(statement) 이었지만 코틀린에서는 식(expression)이 되었다는 것인데요, 문과 식의 차이는 아래와 같으며 지난 포스팅에서도 설명한 바 있습니다.

💡 문(statement)과 식(expression)의 구분
... 
식은 값을 만들어 내며 다른 식의 하위 요소로 계산에 참여할 수 있는 반면 문은 자신을 둘러싸고 있는 가장 안쪽 블록의 최상위 요소로 존재하며 아무런 값을 만들어내지 않는다는 차이가 있다. 
...
출처 : Kotlin in Action / page. 62

왜 Kotlin은 if에 대해 이런 변화를 주었을까요? 그리고 switch를 대신해 when을 제공하는 이유는 무엇일까요? 먼저 코틀린의 if와 when에 대해서 살펴보겠습니다. 그리고 분기 처리와 짝꿍처럼 붙어 다니는 Kotlin의 enum에 대해 이야기합니다.

 

#2_ Kotlin의 if, 작지만 큰 변화

Kotlin의 if는 Java의 if와 사용 방법이 동일하지만 작은 차이가 있습니다. 그 차이는 바로 Java에서는 상태만 가지는 문(statement)인 반면, Kotlin의 if는 값을 반환하는 식(expression)이라는 것입니다. 때문에 Java에서 불가능했던 분기 처리를 통한 초기화가 Kotlin에서는 가능합니다.

// java
String userLevel;
if(id == 0) {
  userLevel = "Administrator"
} else if(id in 1 .. 10) {
  userLevel = "Normal"
} else {
  userLevel = "Hacker"
}

// kotlin
val userLevel = if(id == 0) {
  "Administrator"
} else if(id in 1 .. 10) {
  "Normal"
} else {
  "Hacker"
}

Java에서라면 String 변수를 먼저 선언하고, if문 안에 3개의 대입문이 들어가야 했지만 Kotlin에서는 if를 통한 초기화가 가능합니다. userLevel이라는 변수가 Java에서는 4번, Kotlin에서는 1번 타이핑 되었습니다. 이 차이가 별것 아닌 것처럼 보이지만 중복을 제거한다는 것은 클린 코드 관점에서 엄청나게 큰 메리트입니다.

 

if식이 반환하는 값은 Block 안의 마지막 줄에 해당하는 값입니다. 따라서 아래와 같은 표현도 틀린 식이 아닙니다.

val userLevel: String = if(id == 0) {
  println("user level is Admin")
  "Administrator" // 최종 리턴값은 블럭의 맨 마지막 줄이다
} else {
  println("Senpai Notice Me!")
  "Unknown"
}

또한 Kotlin에서는 if가 식이 됨에 따라 삼항연산자를 지원하지 않습니다.

 

삼항연산자가 사라져서 속상한 개발자.jpg

대신 if와 else를 통해 동일한 표현이 가능합니다. 개인적으로 삼항연산자의 표현이 귀여워서 즐겨 썼는데 아쉽네요.

// Java
int bigger = (a > b) ? a : b

// kotlin
val bigger = if(a > b) a else b

하지만 Kotlin의 if/else 식의 표현이 삼항연산자보다 더 직관적입니다. Java의 삼항 연산자는 "A가 B보다 크냐? 그럼 A를 대입하고, 아니냐? 그럼 B를 넣어라."처럼 해석되는 반면 Kotlin의 if/else 식은 "더 큰 수의 값은 만약 A가 B보다 크면 A고 아니면 B다."라는 영어문장처럼 해석되기 때문이죠.

 

if/else 식을 보고 납득한 개발자.jpg

프로그래밍을 하다보면 새로운 코드를 짜는 시간보다 기존 코드를 읽는 시간이 더 많습니다. 이럴 때 영어문장을 읽는 것처럼 코드가 읽힌다면 가독성이 올라가고 이는 결국 코드 생산성에 영향을 미치게 됩니다. Kotlin 개발자들은 이런 점을 염두에 두고 if에 변화를 준 것이 아닐까 생각됩니다.

 

 

#3_ Kotlin의 when, switch보다 더 똑똑한

Java와 완벽한 호환을 보장한다더니 switch는 어디갔냐구요? 걱정하지 마세요 더 좋은 친구가 생겼습니다. Kotlin에서는 switch 대신 when을 지원하는데요, 이 녀석도 if처럼 식(expression)으로서 값을 반환합니다.

// java의 switch
String userLevel;
switch(id){
  case 0:
    userLevel = "administrator";
    break;
  case 1:
  case 2:
  ...
  case 10:
    userLevel = "Normal"; 	
    break;
  default:
    userLevel = "Hacker";
}

// Kotlin의 when
val userLevel = when(id) {
  0 -> "administrator"
  in 1..20 -> "Normal"
  else -> "Hacker"
}

사용법은 switch와 비슷하지만 매번 case/break를 입력해야 했던 switch에 비해 간결합니다. 위에서 보여진 when의 형태는 아래와 같습니다.

when(param) {
  [condition] -> [expression]
  [condition] -> [expression]
  [condition] -> [expression]
  else -> [expression]
}
  • [param] 이 있는 경우 아래 [condition]은 [param]에 대한 조건식이어야 합니다.
  • when 내부 블럭 안에서 [condition] -> [expression] 형태로 분기 처리를 합니다.
  • [expression] 이 1줄인 경우 Block을 생략할 수 있으며, 2줄 이상인 경우 반드시 Block을 사용해야 합니다.
  • when 앞에 대입 연산자 (=)가 있는 경우 반드시 대입되는 데이터 타입에 맞는 값을 [expression]의 마지막 줄에 명시해야 합니다.

따라서 아래 코드는 위에서 작성한 when의 코드와 동일한 동작을 수행합니다.

val userLevel = when {
  id == 0 -> { "administrator" }
  id in 1..10 -> {
    println("user level is Normal!")
    "Normal"
  }
  else -> "Hacker"
}

뿐만아니라 아래와 같은 특징들도 가지고 있습니다.

  • [condition]이 여러 개인 경우 콤마로 구분해 다수의 [condition]에 하나의 [expression]을 적용할 수 있습니다.
  • 분기 조건에 상수만 사용할 수 있는 switch와 달리 객체를 허용합니다.
  • 스마트 캐스팅(Smart Casting)을 지원합니다.

아래 코드와 함께 위 특성에 대해 살펴보겠습니다.

// [condition]이 여러개인 경우 콤마로 구분해 다수의 [condition]에 하나의 [expression]을 매칭 할 수 있다.
val userLevel = when (id) {
  0 -> "administrator"
  1,2,3,4,5,6..10 -> "Normal"
  else -> "Hacker"
}

// 분기 조건에 상수만 사용 할 수 있는 switch와 달리 객체를 허용한다. (1)
val adminSet = setOf(0)
val normalSet = setOf(1,2,3,4,5,6,7,8,9,10)
val userLevel = when {
  adminSet.contains(id) -> "administrator"
  normalSet.contains(id) -> "Normal"
  else -> "Hacker"
}

// 분기 조건에 상수만 사용 할 수 있는 switch와 달리 객체를 허용한다. (2)
val setA = setOf(0,1)
val setB = setOf(1,0)
when(setA) {
  setB -> println("setA와 setB는 구성이 같다.")
  else -> println("setA와 setB는 구성이 다르다.")
}

분기 조건에 객체를 사용한 2번째 예제의 경우 어떤 텍스트가 출력될까요? 정답은 바로 "setA와 setB의 구성이 같다." 입니다. 순서가 다른데 어째서 setA == setB가 true냐구요? 그 이유는 바로 "같냐"가 아닌 "동등하냐"를 비교하기 때문입니다. set의 경우 내부 element들의 순서가 달라도 데이터만 같다면 동등하다고 판단합니다.

 

스마트 캐스팅은 이해가 어려우실 수도 있을 것 같아 동일한 역할을 하는 Java 코드와 함께 설명드리겠습니다.

// 스마트 캐스팅을 지원한다
fun smartCasting(n: Any) {
  when (n) {
    is Int -> println("${n + 1}")
    is String -> println(n.split("\n"))
    is Main -> n.finish()
  }
}

// java 
if(n instanceof Integer) {
  System.out.println("" + (((int)n) + 1));
} else if(n instanceof String) {
  System.out.println(((String)n).split("\n"));
} else if(n instanceof Main) {
  ((Main)n).finish()
}

Kotlin의 is는 Java의 instanceof와 동일한 역할을 합니다. 단, is를 통해 Object의 타입이 파악된 경우 그 뒤에 실행되는 코드에서 타입이 파악된 Object에 대해 자동으로 캐스팅을 해줍니다. 이렇게 영리하게 자동으로 캐스팅을 해주는 기능을 스마트 캐스팅(Smart Casting)이라고 부릅니다. 때문에 같은 역할을 하는 Kotlin 코드에서는 ((String) n)과 같은 형태의 기계적으로 달아야 하는 상용구가 사라졌습니다.

 

대충 when이 강력해서 좋다는 짤.jpg

지금까지 Kotlin의 if와 when에 대해 간략하게 알아봤습니다. if는 문에서 식으로 변화했고, switch는 사라지고 강력한 when이 등장했습니다. 제가 중간중간 이런 변화로 인해 발생하는 이점에 대해 말씀드렸지만 직접 사용해보시면 생각보다 훨씬 편리하다는 것을 느끼실 수 있을 겁니다. 이런 작지만 큰 변화들로 인해 개발자는 보다 더 "기능" 개발에만 집중 할 수 있게 되었습니다.

 

마지막으로 분기처리와의 환상의 호흡을 자랑하는 enum에 대해 살펴보겠습니다.

 

#4_ Kotlin의 enum, Java의 유산(;)을 간직한

코틀린의 enum(열거형)은 Java의 enum과 비교했을 때 언어의 문법을 제외하고는 완전히 동일합니다. 때문에 깊게 다루지는 않을 예정이니 더 많은 내용이 궁금하신 분들은 이전에 포스팅한 내용을 참고해주세요.

 

근데 이녀석 조금 특이합니다. 코틀린에서 유일하게 세미콜론(;)이 필요한 녀석이기 때문이죠. 간단한 예제와 함께 사용법을 살펴보겠습니다.

enum class WEEK {
  SUN, MON, TUE, WED, THU, FRI, SAT
}

세미콜론이 보이지 않습니다. 이렇게 enum class의 element만 선언한 경우에는 말이죠. 하지만 아래 추가적인 코드가 들어가는 경우 "여기까지가 element 선언 끝이야! 이제부터 로직을 짤 거야!!"라고 세미콜론을 통해 알려줘야 합니다.

enum class WEEK(val korWord: String) {
  SUN("일"), 
  MON("월"),
  TUE("화"),
  WED("수"),
  THU("목"),
  FRI("금"),
  SAT("토"); // 세미콜론을 통해 element선언의 끝을 알림
  
  companion object {
    fun parseTo(korWord: String) : WEEK {
      return values().find{it.korWord == korWord} ?: SUN
    }
  }
}

// 실제 사용코드
fun main() {
  val mon = WEEK.parseTo("월") // mon == WEEK.MON
  val tue = WEEK.parseTo("화") // mon == WEEK.TUE
  val unknown = WEEK.parseTo("??") // mon == WEEK.SUN
}

WEEK enum class의 파싱 메서드에 사용된 find는 correction 내에서 람다의 조건과 일치하는 첫번째 element를 리턴하는 메서드이며, 엘비스 연산자라는 이름을 가진 ?: 로 표현된 연산자는 좌측 식의 리턴 값이 null인 경우 우측식의 값을 리턴하라는 명령을 내리는 연산자입니다. 즉, find의 람다식과 일치하는 element가 없는 경우 default로 WEEK.SUN을 리턴하라는 의미입니다.

💡 ?: 연산자에 엘비스라는 이름이 붙은 이유는 우측으로 90도 회전시켜보면 미국 로큰롤의 제왕으로 알려진 엘비스 프레슬리의 머리를 한 이모티콘 같다고 해서 그렇게 이름 지었다고 합니다. 코와 입까지 표현하면 아래와 같은 모습일 것 같네요.

?:^)

엘비스 프레슬리.jpg / 출처 : https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Ft1.daumcdn.net%2Fcfile%2Ftistory%2F99D9A54A5BD77D181C

 

#5_ 마치며

오늘 살펴본 코틀린의 분기 식이 가진 장점들이 증명하듯, 코틀린은 개발자가 "기능" 개발에만 집중할 수 있도록 여러 장치들을 마련해 놓았습니다. 단순히 Java코드를 Kotlin문법으로만 바꿔서 사용하기보다는 코틀린스러운 코딩 방식을 접하게 된다면 생산성이 놀랍도록 향상되는 경험을 하실 수 있을 겁니다.

가장 빠른 길은 제대로 가는 것뿐이다.
- 로버트 C. 마틴

클린 코드, 클린 아키텍처의 저자 엉클 밥이 이야기한 것처럼 사소한 문법들도 제대로 이해해서 독자분들이 코틀린스러운 코틀린 코드를 작성하는 개발자가 되었으면 좋겠습니다.

 

끝까지 읽어주셔서 감사합니다. 좋은 하루 보내세요.

 

광고 클릭, 하트, 댓글은 필자에게 큰 힘이 됩니다!
댓글