[Scala] 스칼라 시작하기2

Tuples, Pattern matching, 믹스인 클래스 컴포지션, Currying, companion object, Sealed

Posted by Wonyong Jang on February 25, 2021 · 13 mins read

이 글에서는 스칼라를 처음 시작하기 위해서 기본적인 기능들에 대해 간략하게 요약한 내용을 다루려고 한다.

Tuples

Tuples는 불변이며, 몇개의 정해진 요소의 값을 담고 있다. 특히, 여러 개의 값을 리턴 할 때 유용하다.

두개의 요소의 값을 담고 있는 Tuples은 아래와 같이 만들 수 있다.

val ingredient = ("Sugar", 25)   

각각 타입추론이 되어 (String, Int)인걸 알 수 있고 Tuple2(String, Int)를 줄여 쓴 표현이다.

Tuple2, Tuple3 … Tuple22 까지 요소를 포함하여 생성 가능하다.

Tuples의 각 요소를 출력하기 위해서는 위치를 이용하여 가능하다.

println(ingredient._1) // 첫번째 요소 출력 : Sugar
println(ingredient._2) // 두번째 요소 출력 : 25   

또한, 패턴 매칭을 이용으로 추출하여 값을 가져올 수도 있다.

val (name, quantity) = ingredient
println(name) // Sugar
println(quantity) // 25

아래는 Tuples을 가지는 list가 있고 각 Tuples을 패턴 매칭으로 확인을 하며 조건에 맞는 값이 있다면 y값을 출력해 주는 예제이다.

val p = List((10,1), (20, 2), ( 30, 3), ( 40, 4))
  p.foreach{
    case(20, y) => println(y) // 2 출력
    case _ => println("didn't find")
  }

for문을 이용하여 Tuples을 분해하여 사용도 가능하다.

val p = List((10,1), (20, 2), ( 30, 3), ( 40, 4))
  for ((a,b) <- p) {
    println(a*b)
  }

case classes와 tuples 사이에서 어떤 것을 사용할 지에 대해서 어려움이 있을 수 있다.
보통 함수에서 여러 값을 리턴해야 할 경우나 이름이 필요 없는 경우는 Tuples를 사용하고 이름을 붙여서 가독성을 높히는 경우 case classes를 사용하는 것을 권장한다.


믹스인(Mixin) 클래스 컴포지션

단일 상속만을 지원하는 언어들과는 다르게 스칼라 클래스를 재사용을 위한 다른 개념을 가지고 있다.
Scala는 단일 상속을 지원하지만, 다중 상속 같은 느낌을 믹스인으로 해결한다. 믹스인은 매우 간단하다. extends로 단일 상속으로 하게 하고, with로 믹스인을 구현한다.

abstract class A {
  val message: String
}

class B extends A {
  val message = "I'm an instance of class B"
}

trait C extends A {
  def loudMessage = message.toUpperCase()
}

class D extends B with C   

val d = new D
println(d.message) // I'm an instance of class B    
println(d.loudMessage) // I'M AN INSTANCE OF CLASS B   

위의 예제에서 클래스 D는 슈퍼클래스 B를 가지고 있고 mixin C를 가지고 있다.


패턴 매칭

패턴 매칭은 switch와 비슷하지만 훨씬 간결하며, 강력한 기능을 제공한다.
패턴매칭을 사용해서 자료형에 따라 달라지는 표현들을 하나의 표현으로 묶어서 표현할 수도 있고 collection에서 특징을 가지는 collection들을 처리하는 코드를 작성할 수도 있다.

def test(a: Any) = {
    a match {
      case target: Int if(target > 5) => println("five over")
      case 1 => println("One")
      case str: String =>  println(str + " is String")
      case _ => println("Others") // 와일드 카드 매칭 
    }
  }

매칭되는 case문은 선언된 순서대로 Top-Down으로 매칭을 시도하여 매칭되는 것이 있으면 그 아래 케이스는 더이상 확인하지 않는다. 만약 매칭되는 것이 없으면 와일드카드 매칭이 실행된다.


동반 객체(Companion Object)

동반객체(Companion Object)는 class나 trait와 같은 파일에 동일한 이름을 가지는 object를 말한다.

동일한 이름을 가져 서로 동반 관계에 있는 클래스와 Object는 서로 간의 private, protected 필드와 메서드에 접근할 수 있다.

또한, 스칼라는 자바와 다르게 static 키워드가 없다. 대신 비슷한 역할을 할 수 있게 클래스와 같은 이름을 가지는 object를 만들 수 있다. object안에 필드나 메서드를 구현해 자바에서 static 키워드를 사용한 것과 같이 사용할 수 있다.

class LearnScala {
}
object LearnScala {   // 동반 객체
}
trait LearnScala {
}
object LearnScala {   // 동반 객체
}
class LearnScala {
   private val privateValue: Int = 1
}
object LearnScala {
  val learnScala = new LearnScala
  def hardStudy: Int = {
    learnScala.privateValue // 접근 가능    
  }
}

위에서 private한 변수나 함수에 접근할 수 있다고 언급했는데 접근할 수 없는 예외가 몇가지 있다.

class TestClass(b: Int) {
  private[this] def a = 1
  private val c = 3
}
object TestClass {
  val test = new TestClass(1)
  def aaa: Int = test.b     // 생성자의 파라미터는 접근할 수 없다. "Cannot resolve symbol b" 라는 에러 발생
  def aaaa: Int = test.a   // private[this] 한 변수는 접근할 수 없다. "Symbol a is inaccessible from this place" 라는 에러발생
  def aaaaa: Int = test.c // 접근 가능하다.
}

아래와 같이 apply 메서드를 구현해 new 키워드 없이 객체를 만들거나 unapply를 구현해 패턴 매칭에 사용할 수도 있다.

class Dog(name: String) {
  def bark = println("bark! bark!")
  def getName = name
}

object Dog {
  def apply(name: String) = new Dog(name)
}

아래와 같이 자바 리플렉션으로 확인 한 결과 런타임 코드에서 static으로 되어 있음을 확인할 수 있다.

class Dog(name: String) {
  def bark = println("bark! bark!")
}

object Dog {
  val age = 2
  def barkable = true
  def apply(name: String) = new Dog(name)
}

object Test extends App {
  val dog = Dog("dog1")
  val dogClass = dog.getClass
  dogClass.getDeclaredFields.foreach(println)
  dogClass.getDeclaredMethods.foreach(println)

  /* 결과
      public static pack.Dog pack.Dog.apply(java.lang.String)
      public static boolean pack.Dog.barkable()
      public static int pack.Dog.age()
      public void pack.Dog.bark()
   */
}

결국 companion object - companion class는 같은 클래스를 인스턴스 부분과 static 부분으로 분리해둔 것이라고도 볼 수 있다.

동반 클래스라는 용어도 있는데 용어 정리를 해보면 아래와 같다.

  • 동반 객체 : 어떤 클래스 이름과 동일한 이름의 싱글톤 객체
  • 동반 클래스 : 어떤 싱글톤 객체와 동일한 이름의 클래스
  • 독립 객체(Standalone Object) : 동반 클래스가 없는 싱글톤 객체
  • 클래스와 동반 객체는 서로 비공개 멤버에 접근할 수 있다.

sealed 키워드

sealed, final 키워드 모두 자바에서 사용하고 있고 클래스에 쓰게 되면 더 이상 상속하지 못하게 할 때 사용한다.
하지만 sealed 키워드는 final 키워드와 달리 같은 파일에서는 상속할 수 있다. 하나의 파일에 하나의 구현만 가능한 자바와 달리 스칼라는 그런 제한이 없기 때문에 존재하는 키워드라고 볼 수 있다.

아래는 같은 파일에서의 sealed class를 상속한 예제이다.
다른 파일에서 sealed class를 상속했을 때는 에러메시지가 출력된다

sealed class Fruit(color: String) {
  def printColor = println(color)
}

class Apple extends Fruit("Red") {
  def print = "Apple"
}

StackOverFlow를 보면 sealed 키워드 사용한 예로 Option의 구현을 예로 들고 있다.

sealed abstract class Option[+A] extends Product with Serializable {    
final case class Some[+A](x: A) extends Option[A] {
  def isEmpty = false
  def get = x
}
case object None extends Option[Nothing] {
  def isEmpty = true
  def get = throw new NoSuchElementException("None.get")
}

Option과 마찬가지로 Try도 Success와 Failure 2개의 자식을 가지고 있고 같은 구조로 이루어져있다.

Option과 Try는 sealed로 선언되어 같은 파일에서 선언한 Some, None과 Success, Failure 이외에는 자식을 가지지 못하게 하고, Some, Success, Failure는 final class로 None은 상속 할 수 없는 object로 선언하여 사용자가 추가로 상속받을 방법을 막아 놓았다.

또한, sealed 키워드는 class 뿐만 아니라 trait 에도 사용이 가능하다.


Reference

https://partnerjun.tistory.com/56
https://groups.google.com/g/scala-korea/c/dfkcfM5yM9M
https://docs.scala-lang.org/ko/tour/tour-of-scala.html
https://docs.scala-lang.org/ko/tutorials/scala-for-java-programmers.html#%EC%8B%9C%EC%9E%91%ED%95%98%EB%A9%B4%EC%84%9C
https://partnerjun.tistory.com/11
https://sung-studynote.tistory.com/73