ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스칼라 문법편] CH06 패턴 매칭
    레거시/레거시-누구나 쉽게 스칼라+플레이 2019. 1. 29. 22:19
    반응형

    * 이 포스팅은 책 "누구나 쉽게 스칼라 + 플레이 - 고락윤 한빛미디어" 를 읽고 정리한 것입니다.

    CH06 패턴 매칭

    01. 패턴 매칭이란?


    기존, Java, C, C++ 유저라면, 3장 제어문 파트에서 한 가지 빼먹었구나라고 생각하실 수 있겠습니다. 바로 switch문 때문인데요, switch 문은 특정 변수의 값에 따라 분기하고 싶을 때 쓰는 제어문이죠.

    switch(a){
        case 1 : /*1 logic*/ break;
        case 2 : /*2 logic*/ break;
        default: /*default locgic*/ break;
    }


    switch 문은 if/else 구문으로 구성을 달리 할 수 있습니다. 또한, if/else에 비해 조건 처리문을 짜는게 제한적이기 때문에 근래에 들어서는 잘 쓰이지 않는 추세입니다. 스칼라의 패턴 매칭은 이러한 switch문을 훨씬 업그레이드 시킨 버전이라고 생각하시면 됩니다. 책에서는 크게 2가지 면에서 편의를 제공한다고 정리되어 있습니다.

    1. 객체와 객체의 구조를 파악하여 그에 따른 분기 처리
    2. 변수가 어떤 객체인지 따라 분기 처리
    


    대표적인 예제로, JSON 처리가 있습니다. 이는 공부 범위에 한참 벗어나기 때문에 이는 생략하도록 하겠습니다. 자, 이제 패턴 매칭에 대해서 하나 하나 알아보도록 합시다.

    02. 기본 사용법


    가장 먼저, switch 처럼 쓰는 방법이 존재합니다. 다음의 예제 코드를 살펴보도록 합시다.


    val x = 2
    
    val res = x match {
        case 1 => "1"
        case 2 => "2"
        case 3 => "3"
        case _ => "etc"
    }
    
    println(res)


    패턴 매칭 역시 식으로 평가되기 때문에, 해당 값을 변수에 집어 넣을 수 있습니다. 간략히 살펴보자면, x == 1 이라면 1, 2라면 2, 3이라면 3 그외는 etc를 res에 저장하는 코드입니다. 이번에는 패턴 매칭의 조건문을 넣어보겠습니다.

    val x = 2
    
    val res = x match {
        case x if (x > 2) => "Bigger"
        case x if (x < 2) => "Smaller"
        case _ => "Equal"
    }
    
    println(res)


    => 뒤에 if 식을 쓸 수 있지만, => 이전에 식을 사용할 수 있습니다. 보다 깔끔하죠. 위 코드는 x가 2보다 크면 "Bigger"를 작다면 "Smaller"를 그 외에는 "Equal"을 반환합니다. 이번에는 변수의 값에 따라서 패턴 매칭 하는 것을 알아보죠.

    val x = 5
    val y = 2
    
    val o = x
    val res = o match {
        case `x` => println("x")
        case `y` => println("y")
        case _ => println("_")
    }


    이 경우 xy에는 각각 x, y의 값이 위치하게 됩니다. 따라서 o가 그 값에 일치해야 분기문을 타게 되는 것이죠. 이번에는 패턴 매칭을 활용해 조금 더 유연한 코드를 작성해봅시다.

    def matchFunc(input: Any): Any = input match {
        case "" => 100
        case 100 => ""
        case n: Int => s"100이 아닌 ${n}입니다."
        case _ => "ETC"
    }


    이렇게 해두면, 어떠한 타입을 받아도 어떠한 반환형을 처리하든 개발자 마음대로 정의할 수 있습니다. 무엇보다 타입 캐스팅이 필요가 없죠. 이것만 보더라도 패턴 매칭은 아주 강력하지요.

    03. 객체를 패턴 매칭?!


    그래도 욕심 많은 개발자라면, 아직 부족함을 느낄 것입니다. 패턴 매칭은 이러한 개발자들을 위해서, 객체와 객체간 패턴 매칭도 지원합니다. 바로 case class를 이용해셔 말이죠. 사실, 객체를 판별할 때, 보통은 그 참조값의 주소를 보지만 case class는 equals를 각 필드의 값이 일치하냐 여부로 보기 떄문에 가능한 이야기입니다. 바로 코드로 살펴보시죠.


    case class Person(name: String, age: Int, rank: String)
    
    def matchPerson(p: Person) = p match {
        case Person("K", 50, "President") => println("President come!")
        case Person("L", 40, "CTO") => println("CTO come!")
        case Person("G", 30, "Developer") => println("G come!")
        case _ => println("who are you")
    }
    
    //main
    val p1 = Person("K", 50, "President")
    val p2 = Person("L", 40, "CTO")
    val p3 = Person("G", 30, "Artist")
    
    matchPerson(p1)
    matchPerson(p2)
    matchPerson(p3)


    과연 이 코드의 출력은 무엇일까요? 실제로 출력은 다음과 같습니다.


    President come!
    CTO come!
    who are you
    


    왜냐하면, p3 rank 필드가 다르기 때문에 패턴 매칭에서 다르게 인식하는 것이죠. 여기서 와일드 카드를 쓰면 보다, 더 넓은 범위로 객체를 판단할 수 있습니다. matchPerson을 다음과 같이 변경해보죠

    def matchPerson(p: Person) = p match {
        case Person("K", 50, "President") => println("President come!")
        case Person("L", 40, "CTO") => println("CTO come!")
        case Person("G", 30, _) => println("G come!")
        case _ => println("who are you")
    }


    그리고 다시 돌려보면 출력은 다음과 같이 됩니다.

    President come!
    CTO come!
    G come!
    


    3번째 case에서 rank를 와일드카드 _ 로 값을 넣었기 때문에 이 때는, name, age가 같은지 판별합니다. 한 가지 팁을 드리자면 어지간하면 마지막에 있는 case _ => 코드는 꼭 넣어주시기 바랍니다. 그래야 코드가 런타임 에러 없이 돌 수 있습니다. 

    만약 마지막 case가 없고, Person("W", 10, "Student")를 매칭 시켜보면 런타임에러가 나는 것을 확인하실 수 있습니다. 왜 에러가 나냐면 그 매칭되는 값에 대해 로직을 처리하지 않았기 때문입니다. 그러니까 switch 문의 default 처럼 그냥 박아주신다고 생각하면 됩니다.

    04. Extractor와 unapply


    case class를 이용한 패턴매칭은 분명히 강력합니다. 다만, 상황에 따라서 이러한 패턴매칭 이용이 어려울 때가 있습니다. 만약 단순한 문자열 혹은 숫자를 받아서 이것들을 가공해서 패턴매칭을 하고 싶다면, 추출자 Extractor를 기능을 써야 합니다. 이 때 사용하는 것이 unapply 메소드입니다. 자 예제 코드를 살펴보도록 합시다.

    object Emergency{
      def unapply(number: String): Boolean = {
        "119" == number
      }
    }
    
    object Normal {
      def unapply(number: String): Option[Int] = {
        try {
          Some(number.replaceAll("-", "").toInt)
        } catch {
          case _: Throwable => None
        }
      }
    }
    
    //main
    val e = "119"
    val p = "010-123-1234"
    val n = "hey"
    val l = List(e, p, n)
    
    for( number <- l ) {
        number match {
            case Emergency() => println("Emergency!!")
            case Normal(number) => println(s"number : ${number}")
            case _ => println("I don't know")
        }
    }


    이렇게 하면, 먼저 처음 객체 e가 들어갔을 때, Emergency의 unapply()에 의해서 문자열이 "119"와 같다면 Emergency 오브젝트에 패턴매칭 시켜줍니다. 그 후, p가 들어왔을 때는 들어온 문자열에 "-"를 ""로 변환한 후 숫자로 바꿔줍니다. 만약 이 처리 시에 에러가 나면 None 객체를 처리가 완료되었다면 Some객체의 그 변환된 정수형을 넣어줍니다. 그 외의 경우에는 case _ 에 걸리게 됩니다. 여기서 Option, Some 에 대해서는 추후 설명하도록 하겠습니다. 지금은 값을 Option은 값을 가지고 있다면 Some, 아니라면 None이 되는 타입이라고 생각하시면 됩니다.


    이렇게 해서 패턴 매칭에 대해서 알아보았습니다. 어떠신가요? 어렵나요..? ㅎㅎ 그럼에도 불구하고 스칼라를 저와 함께 계속 공부하고 싶다면 다음 장, 컬렉션 파트에서 만나도록 합시다!

Designed by Tistory.