확장 가능한 언어

왜 스칼라 인가?

스칼라(Scala)라는 이름은 확장 가능한 언어라는 뜻입니다. 언어의 이름을 이렇게 지은 이유는 스칼라를 사용자의 요구(?)에 따라 확장 할 수 있는 언어로 설계했기 때문입니다. 스칼라를 처음 접근하는 것은 매우 쉽습니다. 스칼라는 JVM에서 실행할 수 있고, 모든 자바 라이브러리와 매끈하게 연동할 수 있습니다. 혹은 스칼라는 자바 컴포넌트를 한데 묶는 스크립트를 작성할 때 좋은 언어입니다. 기술적으로 말해서 스칼라는 '객체지향과 함수형' 프로그래밍 개념을 정적 타입 언어에 합쳐놓은 언어입니다.

스칼라는 순수한 형태의 객체지향 언어이기 때문에 모든 값이 객체이며, 모든 연산은 메소드 호출입니다. 예를 들어 스칼라에서 1+2라고 쓰면, 실제로는 Int 클래스가 정의한 +라는 이름의 메소드를 호출하는 겁니다. 객체를 조합함에 있어서 스칼라는 다른 대부분의 언어보다 더 뛰어납니다. 대표적인 예로 트레이트(trait)를 들 수 있습니다. 트레이트는 자바의 인터페이스와 비슷하지만 트레이트 내부에서 메소드를 정의할 수 있고, 심지어 필드도 정의할 수 있습니다. 객체는 믹스인(mlxin) 조합을 통해 만들 수 있습니다. 믹스인은 한 클래스의 멤버에 다른 트레이트에서 가져온 멤버들을 추가하는 것입니다. 꼭 상속을 통해야만 하는 클래스와 달리 트레이트는 새로운 기능을 서브클래스에 추가 할 수 있습니다.

스칼라는 완전한 함수형 언어이기도 합니다. 1930년대 알론조 처치가 만든 람다 계산법에 근거를 두고 있습니다. 함수형 프로그래밍은 두 가지 주요 아이디어에 따라 방향이 정해집니다. 첫 번째 아이디어는 함수가 1급 계층(first class)이라는 점입니다. 함수를 다른 함수에 인자로 넘길 수 있고, 함수 안에서 결과로 함수를 반환 할 수 있고, 함수를 변수에 저장 할 수 있다. 심지어는 함수 이름을 지정하지 않고 함수를 정의할 수도 있습니다. 함수형 프로그래밍의 두 번째 주 아이디어는 프로그램은 입력 값을 출력 값으로 변환해야 하며, 데이터를 그 자리에서 변경하지 말아야 한다는 점이다. 이런 함수형 프로그래밍의 두 번째 아이디어는 "메소드에 부수효과(side effect)가 없어야 한다"라고 다르게 표현할 수 있습니다.

스칼라는 정적 타입 언어입니다. 펄 파이썬, 루비, 그루비 같은 동적 언어를 좋아하는 독자라면, 정적 타입 시스템이 없는게 동적 언어의 주요 장점 중 하나라고 많은 사람들이 말하곤 한다. 예를 들어 스몰토크를 만든 앨런 케이는 "난 타입을 반대하지 않는다. 하지만 내가 아는 타입 시스템들은 모두 완전히 골칫거리다. 그래서 나는 아직 동적인 타입을 좋아한다."라고 말했습니다. 스칼라는 이런 우려를 타입 추론과 패턴 매치를 통해 유연성을 확보하여 정적 타입 시스템의 전통적인 이점을 더 많이 누릴 수 있습니다.

스칼라 첫걸음

  • Scala 인터프리터가 필요한 경우 Mac OS X를 사용하면 Ammonite를 추천합니다. Ammonite는 REPL, 스크립트로 그리고 독립형 시스템 쉘(shell)로서 사용할 수 있습니다. brew를 사용해서 손쉽게 설치(brew install ammonite) 가능하며, 문서를 참고하셔도 됩니다.

  • Intellij를 사용하시면 Scala 플러그인을 사용하시면 예제를 실행하는데 전혀 문제가 없으며, sc인 확장자를 사용하면 자동으로 스크립트 모드로 동작합니다.

변수와 함수를 정의해보자

스칼라에는 두 종류의 변수가 있는데, 바로 valvar이다. val은 자바의 final 변수와 비슷하고, varfinal이 아닌 변수와 비슷합니다. 기본적으로 타입 추론이 가능하다면 변수에 타입을 선언하지 않아도 됩니다. 탸입을 선언할 때는 변수명 뒤에 : ClassName으로 가능합니다.


@ val msg = "Hello, World"
msg: String = "Hello, World"

@ msg = "HELLO, WORLD!"
cmd1.sc:1: reassignment to val
val res1 = msg = "HELLO, WORLD!"
               ^
Compilation Failed

@ var msg2 = "Hello, World"
msg2: String = "Hello, World"

@ msg2 = "HELLO, WORLD!"

@ msg2
res3: String = "HELLO, WORLD!"

@ var msg3: java.lang.String = "Hello, again!"
msg3: String = "Hello, again!"

@ var msg3: String = "Hello, Again!"
msg3: String = "Hello, Again!"

스칼라의 함수 선언은 def로 선업하며, 간단한 형태는 =를 사용하고 복잡한 구문의 경우 {...}를 사용합니다.


@ def max(x: Int, y: Int) = if (x > y) x else y
defined function max

@ max(10,11)
res7: Int = 11

while과 if

기존의 자바의 whileif와 같은 제어문도 스칼라에서 그대로 사용할 수 있으며, 별다른 차이가 없습니다. 당연히 스칼라에서는 while과 같은 명령형 스타일의 제어문 보다는 내부 이터레이션을 사용한 foreach를 사용하기를 권장합니다.


@ var i = 0
i: Int = 0

@ while (i < 10) {
   if (i != 0) print(" ")
   print(i)
   i += 1
  }
0 1 2 3 4 5 6 7 8 9


foreach와 for 그리고 이터레이션

함수형 언어의 주요 특징 중 하나는 함수가 1급 계층 요소이기 때문에 인자로 함수를 넘길 수 있습니다. 함수 리터럴이 인자를 하나만 받는 경우 해당 인자에 이름을 붙일 필요가 없습니다. 당연히 스칼라는 기존의 for에 함수형 인자를 넘길 수 있는 문법을 제공합니다.


args.foreach(arg => println(arg))
args.foreach(println) // 함수 리터럴의 인자가 하나인 경우

for(arg <- args) println(arg)

배열, 리스트, 튜플, 집합, 맵

자바에서 사용하는 대부분의 컬렉션은 스칼라에서 사용 가능하다. 배열의 경우 아래 예제에서 볼 수 있듯이 Array라는 타입을 사용해서 배열을 만들 수 있습니다. 배열의 원소에 접근하는 것은 일반적인 메소드 호출과 같습니다. 어떤 종류의 객체이든 괄호 안에 인자를 넣어서 호출하면 apply 메소드를 호출하는 것과 같습니다.


@ var greetArray = new Array[String](3)
greetArray: Array[String] = Array(null, null, null)

@ greetArray(0) = "Hello"

@ greetArray
res2: Array[String] = Array("Hello", null, null)

함수형 프로그래밍의 가장 큰 착안점 하나는 메소드에 부수 효과가 없어야 한다는 것 입니다. 같은 타입의 객체로 이뤄진 변경 불가능한 시퀸수를 위해서는 스칼라의 List 클래스를 사용할 수 있습니다.


@ val greetList = List("Hello", null, null)
greetList: List[String] = List("Hello", null, null)

@ greetList
res4: List[String] = List("Hello", null, null)

@ val greetTuple = ("Hello", null, null)
greetTuple: (String, Null, Null) = ("Hello", null, null)

튜플은 또 다른 유용한 컨테이너 객체입니다. 리스트와 마찬가지로 변경 불가능하지만 튜플에는 각기 다른 타입의 원소를 넣을 수 있습니다.


@ greetTuple
res6: (String, Null, Null) = ("Hello", null, null)

@ greetTuple._1
res7: String = "Hello"

@ greetTuple._2
res8: Null = null

스칼라의 목적은 프로그래머들이 함수형 스타일과 명령형 스타일의 장점을 모두 취할 수 있게 돕는 것이다. 이를 위해 스칼라 컬렉션 라이브러리에서 변경 가능한 컬렉션과 변경 불기능한 컬렉션을 구분합니다. 예를 들어, 배열은 항상 변경 가능하지만 리스트는 변경 불기능합니다. 하지만 집합이나 맵에 대해서는 변경 가능한 것과 변경 불가능한 것을 모두 제공합니다.

mutable을 사용하면 변경 가능하며, 일반적인 MapSet의 경우 변경 불가능 합니다.


@ val greetSet = Set("Hello", null)
greetSet: Set[String] = Set("Hello", null)

@ greetSet.contains("Hello")
res10: Boolean = true

@ val greetSet = scala.collection.mutable.Set("Hello")
greetSet: collection.mutable.Set[String] = Set("Hello")

@ greetSet += "World"
res12: collection.mutable.Set[String] = Set("World", "Hello")

@ val greetMap = scala.collection.mutable.Map[Int, String]()
greetMap: collection.mutable.Map[Int, String] = Map()

@ greetMap += (1 -> "Hello")
res14: collection.mutable.Map[Int, String] = Map(1 -> "Hello")

@ greetMap += (2 -> "World")
res15: collection.mutable.Map[Int, String] = Map(2 -> "World", 1 -> "Hello")

@ greetMap
res16: collection.mutable.Map[Int, String] = Map(2 -> "World", 1 -> "Hello")