Scala 소개

홍재민

2019년 6월 4일 0시 36분 (최근 수정: 2019년 6월 9일 20시 31분)


이번 글은 Scala를 한 번도 사용해보지 않은 사람들을 위한 간단한 Scala 소개 글이다. Scala를 조금이라도 사용해 본 적이 있는 사람은 읽지 않고 다음 글로 넘어가도 무방하다.

Scala

Scala는 EPFL의 Mardin Odersky 교수님 및 연구실 LAMP에서 개발하였고, 현재도 계속 개발 중인 언어로 확장 가능한 언어(Scalable Language)라는 뜻을 가지고 있다. 프로그래밍 언어 수업에서도 매 학기 과제로 듣는 Guy Steele의 “Growing a Language” 연설에 영향을 받아 만들어진 언어로, 함수형(functional)과 객체지향(object-oriented) 프로그래밍을 모두 지원하며, 성장 가능한 언어를 지향하고 있다. 함수형 프로그래밍에 대해서는 이후 글에서 더 자세히 다룰 예정이다.


컴파일러와 인터프리터

약간 이야기가 다른 길로 새기는 하지만, 지금보다 이 부분을 다루기에 더 나은 시점이 없는 것 같아서 잠시 컴파일러와 인터프리터(interpreter)에 대해서 언급하도록 하겠다.

두 단어 모두 음차 한 형태가 많이 사용되고, 한국어 Wikipedia에서도 음차 한 형태로 등재되어 있기 때문에 이 글에서도 컴파일러와 인터프리터라고 부르고 있으나, 변역을 해서 보면 두 단어가 뜻하는 바가 명확해진다. 컴파일러는 번역기, 인터프리터는 해석기라고 번역할 수 있다.

이름이 보여주는 바와 같이 컴파일러는 한 언어로 작성된 소스 코드를 다른 언어의 코드로 번역하는 프로그램이다. 다만, 대부분의 경우에는 컴파일러라고 하면 사람이 이해 가능한 고급 언어로 작성된 코드를 실제 물리적 기계에서 바로 실행 가능한 기계어(machine code)나 가상 기계(virtual machine)에서 실행 가능한 바이트코드(bytecode)로 번역하는 프로그램을 의미한다. 대표적인 예로 C와 C++ 등의 코드를 기계어로 번역하는 GCC나 Java 코드를 Java 바이트코드로 번역하는 javac가 있다. 기계어나 바이트코드는 (가상) 기계에서 읽고 실행하기에 최적의 형태로 되어 있기 때문에 빠른 실행을 보장한다는 장점이 있지만, 코드를 수정한 뒤에 컴파일을 다시 해야 실행이 가능하기 때문에 코드를 수정하면서 바로바로 실행 결과를 확인하는 데 오래 걸린다는 단점이 있다.

반대로 인터프리터는 주어진 코드를 번역하는 대신 바로 해석하여 결과를 보여준다. 인터프리터를 사용하는 언어의 예로는 Python이나 JavaScript 등이 있다. 문자열(string) 형태로 된 코드를 바로 해석하기 때문에 파싱(parsing)과 같은 과정을 모두 실행 시에 수행해야 하므로 실행이 느리다는 단점이 있지만, 코드를 수정한 뒤 컴파일 과정 없이 바로 실행해 볼 수 있다는 장점이 있다.


Scala 코드는 Scala 컴파일러에 의해 Java 가상 머신(JVM)에서 실행 가능한 Java 바이트코드로 컴파일된다. 따라서, Java로 작성된 프로그램과의 호환성이 완벽히 보장되어 Java 표준 라이브러리는 물론 Java로 작성된 어느 외부 라이브러리든 Scala 프로젝트에서 함께 사용할 수 있다. 또한, Java 바이트코드 수준에서만 호환 가능한 것이 아니라, 컴파일 단계에서 함께 사용하는 것이 가능하기 때문에, 한 프로젝트에서 상호 의존적인 Scala 코드와 Java 코드를 동시에 작성해서 사용하는 것도 가능하다. 추가적으로, Scala 표준 라이브러리는 Java 컬렉션(collection)과 Scala 컬렉션 사이의 변환을 지원한다. Java와의 호환성은 실제 프로젝트에서 매우 유용한 경우가 많은데, 거대한 Java 생태계의 라이브러리를 그대로 사용할 수 있기 때문에 Scala로 작성된 라이브러리는 수가 상대적으로 많지 않다는 것이 문제가 되지 않는다.

위에서도 언급하였듯, Scala는 현재도 활발히 개발 중인 언어이며, 2019년 6월 3일 기준으로 Scala 최신 버전은 2.12.8이고 2.13 공개를 눈앞에 두고 있다. Scala 공식 GitHub 저장소는 https://github.com/scala/scala이다. 또한 Scala 개선과 동시에 새로운 컴파일러, 추가된 많은 기능과 사라진 약간의 기능을 가진 Dotty(Scala 3) 개발 역시 진행 중이다. Dotty는 현재 0.14.0까지 공개되었다. Dotty 공식 GitHub 저장소는 https://github.com/lampepfl/dotty이다.

현업에서의 Scala

Scala는 내가 가장 좋아하고 가장 많이 사용하는 언어지만, 냉정히 말해서 최고로 인기 있는 언어 중 하나라고 보기는 어렵다. 2017년 GitHub Octoverse에 따르면 1년간 발생한 PR의 개수가 약 9만 9천개로 모든 언어 중 13위에 자리하고 있다. 2018년 GitHub Octoverse에서는 10위까지만 공개를 하였으며, 2017년과 마찬가지로 10위 안에 들지 못했음을 볼 수 있다. 2019년 Stackoverflow Developer Survey에 따르면 Scala는 인기 순위 20위를 기록했다. 이러한 조사가 언어가 얼마나 흔하게 사용되는지를 완벽히 보여준다고 할 수는 없으나, 분명히 C++, Java, Python, JavaScript 등 인기 있는 언어에 비해서는 훨씬 드물게 사용되는 것이 사실이다. 내 표현으로는 “인기 있는 언어 중에는 가장 인기 없고, 인기 없는 언어 중에는 가장 인기 있다”. 따라서, 개발자로 일하게 될 많은 학생에게 있어 Scala를 현업에서 사용할 가능성은 높지 않다.

Scala의 인기에 대한 부정적인 이야기를 바로 위에서 늘어놓았지만, 분명히 Scala는 어느 정도 현업에서 사용되는 언어이며, 가게 될 직종에 따라서는 Scala로 많은 개발을 하게 될 가능성도 분명히 존재한다. 지금부터는 Scala가 실제로 어디에 사용되었는지 알아보겠다. 가장 가까이로는 KAIST 류석영 교수님의 PLRG 연구실에서 개발한 JavaScript 분석기 SAFE 2.0이 Scala로 개발되었으며 그 밖에도 여러 프로젝트가 Scala로 개발 중이니, PLRG에 들어가고자 한다면 Scala를 잘 다룰 수 있으면 좋을 것이다. :) 너무 팔이 안으로 굽은 예시인 것 같으니, 다른 곳을 찾아보자면 Scala 컴파일러와 Dotty 컴파일러 모두 Scala로 개발되었다. 사실 여기도 너무 당연한 예시이다. Scala 팀에서조차 Scala를 안 쓰면 누가 쓰겠는가. 조금 더 시야를 넓히면, Scala는 동시성 분산(distributed) 컴퓨팅 분야에서 많이 사용된다. Akka는 Scala로 작성된 동시성 분산 컴퓨팅 라이브러리로 여러 기업에서 사용하고 있다. Apache Spark 역시 Scala로 작성되었으며, 데이터 프로세싱을 위해서 널리 사용되고 있다. Play는 Akka를 기반으로 Scala로 개발된 프레임워크(web framework)로 역시 많은 기업이 사용하고 있다. 각 라이브러리가 활용되고 있는 곳은 각 사이트에서 자세히 확인할 수 있다. Scala가 대규모 데이터를 처리하는 분야에서 주로 사용되는 이유는 이후에 함수형 프로그래밍에 대한 글에서 조금 다루도록 하겠다.

Scala가 가치 있는 또 다른 이유는 함수형 프로그래밍을 처음 접하기 좋은 언어라는 것이다. 함수형 프로그래밍에 대해 다룰 때 자세히 이야기하겠지만, 과거보다 함수형 프로그래밍은 현업에서도 사용이 늘어나고 있다. 따라서, Scala로만 한정하는 경우에는 사용할 가능성이 그리 높지 않을지라도, OCaml, Haskell과 같은 다른 함수형 언어를 사용하는 경우나, 함수형 패러다임이 완전히 주가 되지는 않더라도 중요하게 사용되는 Kotlin 등의 언어를 사용할 가능성까지 고려하면, C, Java, Python과 같은 명령형(imperative) 프로그래밍이 주가 되는 언어로 프로그래밍을 시작한 사람이 Scala를 통해서 함수형 프로그래밍을 경험하는 것은 앞으로 할 수 있는 일의 폭을 넓히는 데 있어서 큰 도움이 될 수 있다.

Scala 코딩 시작하기

Scala 설치에 대해서는 Scala 공식 웹 페이지에서 정보를 얻을 수 있다. 이 글에서 설치에 대해서 자세히 설명하더라도 시간이 지나면서 방법이 달라질 수도 있고, 웹 페이지에 어렵지 않게 설명이 되어 있으며, Scala는 설치에 어려움을 겪는 경우는 거의 없는 언어이기 때문에, 자세한 설치 방법에 관해서는 설명하지 않겠다.

웹 페이지에서 사용 가능한 통합 개발 환경(integrated development environment; IDE)에 대해서도 소개하고 있으며, 사용할 IDE는 전적으로 개인 취향일 뿐 아니라 나는 대부분의 경우 IDE 사용 없이 Vim을 사용하는 편이기 때문에 IDE 설치 및 환경 설정에 대해서도 다루지 않겠다. IDE의 강력한 기능을 활용하는 편을 선호한다면 IntelliJ IDEA를 추천한다.

Scala REPL

Scala 설치가 성공적으로 되었다면, Windows에서는 명령 프롬프트, UNIX 계열에서는 터미널에 scala를 입력하여 Scala REPL을 실행할 수 있다. REPL은 read-eval-print-loop의 두문자어(acronym)로, 사용자가 코드 한 줄을 입력한 것을 읽어(read), 해당 코드를 실행(eval)하고, 실행 결과를 출력(print)하는 것을 계속 반복(loop)하는 환경을 의미한다. 인터프리터는 그 자체가 코드 문자열을 그대로 한 줄씩 읽어서 바로 실행하는 프로그램이기에 일반적으로 Python 같은 인터프리터 언어에서 REPL을 지원한다. 그러나, Scala는 컴파일 언어임에도 REPL을 지원하는데, 매 줄을 입력할 때마다 입력한 코드를 컴파일한 뒤에 실행하여 결과를 보여준다.

scala> 0
res0: Int = 0

위는 REPL에 0을 입력하면 일어나는 일이다. 0이라는 (expression)을 계산하여 나온 (value)은 0이며, 0이 가지는 타입은 Int라는 것을 알려준다.

scala> 1 * 1
res1: Int = 1

조금 더 복잡한 수식을 입력해도 잘 작동한다.

변수 선언

scala> val x = 2
x: Int = 2

변수 선언(declaration)은 val 키워드(keyword)를 사용한다. val [변수 이름] = [식] 형태를 사용하며, 우변의 식을 계산한 값이 변수 이름이 가리키는 값이 된다. 변수의 타입을 지정해야 하는 C나 Java와는 달리 타입을 명시하지 않아도 자동으로 x의 타입이 Int라고 유추(inference)된 것을 볼 수 있다.

scala> x = 3
<console>:12: error: reassignment to val
       x = 3

val 키워드는 수정 불가능한(immutable) 변수를 정의한다. 따라서, 한 번 정의된 변수의 값은 절대 바뀌지 않으며, 변수에 등호를 사용해서 어떤 식을 대입하는 것은 불가능하며, 컴파일 오류(error)가 일어난다. 불변성(immutability)은 함수형 프로그래밍의 가장 중요한 특징이다.

scala> val x = 4
x: Int = 4

반면, 이름이 같은 변수 x를 다시 선언하는 것은 허용되는데, 이는 REPL의 특성상 두 변수 x가 서로 다른 영역(scope)에 존재하기 때문이다. 실제로 코딩을 하는 경우에는 당연히 한 영역 안에서, 예를 들면 한 함수 안에서, 이름이 같은 변수를 다시 정의하는 것은 불가능하다.

scala> val y: Int = x + 1
y: Int = 5

변수 선언 시, 변수 이름 다음에 쌍점(colon)을 적고 타입을 명시할 수 있다. 우변의 식이 명시한 타입을 가지지 않는다면 컴파일 시에 타입 오류가 발생한다. 대부분의 경우, 타입을 명시하는 것과 하지 않는 것은 동일한 의미를 가지며, 지역 변수(local variable)의 경우 대개 타입을 생략한다. 반면, 아직 다루지 않았지만, 클래스(class)의 필드(field)는 타입을 명시하는 것이 가독성에 도움이 될 수 있다. 지역 변수의 경우에도 타입 유추가 불가능하면 타입을 명시해야 하지만 이런 경우는 드물게 발생한다.

scala> var z = 6
z: Int = 6

scala> z = 7
z: Int = 7

var 키워드를 사용하면 수정 가능한(mutable) 변수를 선언할 수 있다. 수정 가능한 변수 역시 수정 불가능한 변수와 마찬가지로 필요하다면 변수 선언 시 쌍점을 사용해서 타입을 명시할 수 있다.

scala> z = "8"
<console>:12: error: type mismatch;
 found   : String("8")
 required: Int
       z = "8"
           ^

식을 수정 가능한 변수에 대입하는 경우에는 변수 선언 시에 정해진 타입을 따라야 한다. 함수형 프로그래밍에서는 수정 가능한 변수의 사용을 지양하며, 수정 불가능한 변수만 사용하여 많은 (사실은 모든) 일을 할 수 있음을 앞으로 보게 될 것이다. 물론, 수정 가능한 변수가 코드를 더 읽기 쉽고 효율적으로 만드는 경우도 많이 존재하는 만큼, 기본적으로는 val을 통해서 변수를 선언하되 필요에 따라서는 적절히 var을 사용하는 것도 중요하다. 그러나, 프로그래밍 언어 수업에서 작성하는 거의 모든 코드는 수정 가능한 변수가 필요하지 않으며, 특별한 언급이 없다면 수정 가능한 변수는 과제에서 사용할 수 없다.

함수 선언

scala> def add(x: Int, y: Int) = x + y
add: (x: Int, y: Int)Int

함수는 def 키워드를 사용해서 선언한다. def [함수 이름]([매개변수 이름]: [매개변수 타입], ...) = [식] 형태를 사용한다. 수학에서 함수를 정의하듯, return 키워드는 필요하지 않다. return 키워드를 사용하는 것도 가능하나, 대부분의 경우 필요하지 않으며, 꼭 필요한 경우가 아니면 사용을 지양해야 한다. 함수를 호출했을 때 결괏값(return value)은 우변의 식을 계산한 결과이다. 결괏값이 Int 타입을 가지므로 함수의 결과 타입이 Int라는 것이 유추된 것을 확인할 수 있다.

scala> add(4, 5)
res2: Int = 9

결괏값이 잘 계산된 것을 볼 수 있다.

scala> def add(x: Int, y: Int): Int = x + y
add: (x: Int, y: Int)Int

변수 선언 시 타입을 명시할 수 있는 것처럼, 함수의 결과 타입도 쌍점을 사용해 명시할 수 있다. 우변의 식이 명시된 결과 타입을 따라야 한다. 변수와 마찬가지로, 이름이 같은 함수를 한 영역 안에 여러 번 정의할 수는 없으나 (단, 클래스의 메서드(method)는 오버로딩(overloading)이 가능하다), REPL의 특성상 영역이 분리되어 있어 같은 이름의 함수가 다시 정의되었다.

scala> def add(x, y): Int = x + y
<console>:1: error: ':' expected but ',' found.
       def add(x, y): Int = x + y
                ^

OCaml 등과 달리 매개변수(parameter)의 타입을 생략하는 것은 불가능하다. 이는 Scala가 왼쪽에서 오른쪽으로 지역 유추를 하기 때문으로, 매개변수의 타입을 알고 있는 상태에서 우변의 식의 타입을 유추할 수는 있지만, 반대로 우변의 식으로부터 매개변수의 타입을 유추할 수 없다.

scala> def f(x: Int) = {
     |   val y = x + 1
     |   val z = x + 2
     |   y * y + z * z
     | }
f: (x: Int)Int

위에서 본 함수 add는 하나의 식으로 결과를 표현할 수 있는 매우 단순한 함수이다. 지역 변수 선언이 필요해서 여러 식으로 결과를 표현해야 한다면 f처럼 중괄호를 사용해서 여러 식을 하나의 식으로 묶을 수 있다. 이 경우, 중괄호로 묶인 전체 식을 계산한 결과는 위부터 순서대로 식을 계산한 후, 마지막 식을 계산한 결과가 전체 식의 결과가 된다. f에서는 차례대로 변수 yz를 선언한 뒤 마지막 식을 계산하게 된다.

scala> def g(x: Int) = {
     |   println(x)
     |   x * x
     |   x
     | }
g: (x: Int)Int

scala> g(10)
10
res3: Int = 10

함수 g를 통해서 여러 식을 묶으면 어떻게 되는지 조금 더 명확히 볼 수 있다. g를 인자 10을 사용해서 호출한 결과, 위부터 식이 차례대로 계산되어, 10이 출력되고, x * x가 계산되지만 해당 값은 사용되지 않으며, 함수의 결괏값은 마지막 x를 계산한 10이다. 절차형 관점에서 해석하면, 단순히 return 키워드가 생략 가능하며, 마지막 식 앞에 자동으로 return 키워드가 추가된다고 생각할 수도 있으나, 함수형 관점으로 보는 것이 일관적인 이해를 돕는다.

scala> val w = {
     |   val a = 5
     |   a + a + 1
     | }
w: Int = 11

scala> add({
     |   val x = 6
     |   x + x
     | }, 0)
res4: Int = 12

위와 같이, 중괄호로 식을 묶은 것도 식이기 때문에, 함수 선언 시의 우변뿐 아니라 식이 올 것으로 기대되는 아무 곳에서 사용될 수 있다.

조건식

scala> if (true) 13 else 14
res5: Int = 13

다른 언어와 유사하게 ifelse 키워드를 사용해서 조건식(conditional expression)을 작성할 수 있다. 조건식 역시 하나의 값으로 계산되는 식이다. 이는 절차형 언어에서의 if-else 조건문(conditional statement)과 다른 점으로, 조건문은 값으로 계산되지 않고, 조건으로 주어진 값에 따라 두 가지(branch) 중 하나를 실행한다.

scala> val a = if (false) 13 else 14
a: Int = 14

scala> var b = 0
b: Int = 0

scala> if (false) b = 14 else b = 15

scala> b
res6: Int = 15

따라서, Scala와 같은 함수형 언어에서는 a를 바로 수정 불가능하게 정의하는 것이 가능하지만, 절차형 언어에서는 b를 수정 가능하게 정의한 뒤 원하는 식을 대입해야 한다. 절차형 언어에 존재하는 삼항 연산자(ternary operator) ? : 와 비슷한 역할을 한다고 생각할 수 있으나, 함수형 언어의 조건식이 더 일반적이다.

scala> if (true) {
     |   val x = 8
     |   x + x
     | } else {
     |   val x = 9
     |   x * x
     | }
res7: Int = 16

식을 묶은 것도 식이기 때문에 조건식을 만들 때도 중괄호를 자유롭게 사용하여 복잡한 계산을 수행할 수 있다. 절차형 언어에서는 삼항 연산자를 사용해도 식을 묶을 수 없기 때문에 불가능한 형태이다.

Scala 코드 컴파일

Scala 코드를 Java 바이트코드로 컴파일하기 위해서는 Scala 컴파일러 scalac를 사용한다.

object App {
  def main(args: Array[String]) = println("Hello world!")
}

위 코드를 App.scala 파일로 저장한 다음, 차례로 scalac App.scalascala App을 입력하면, 콘솔에 “Hello world!”가 출력되는 것을 확인할 수 있다. scalac를 통해 App.scala를 컴파일해 Java 바이트코드로 작성된 클래스 파일 App.class를 만들고 JVM을 실행시키는 scala를 통해서 App.class를 실행한다.

JVM을 통해 실행되기 때문에, Java와 마찬가지로 Scala 프로그램의 시작 지점은 main 메서드이며, 실행하고자 하는 코드는 main 메서드 안에 있어야 한다. object 키워드에 대해서는 이후 글에서 설명할 예정이다.

실제로 프로젝트를 진행할 때는 다른 언어와 마찬가지로 빌드 도구(build tool)를 사용한다. C, C++은 CMake, Java는 Ant, Maven, Gradle을 사용하는 것처럼, Scala는 일반적으로 SBT를 사용한다. SBT의 설치 및 사용 방법은 여기서 다루지 않겠다.


감수: 류석영 교수님 (sryu.cs@kaist.ac.kr)

글에 대한 질문과 의견은 jaemin.hong@kaist.ac.kr로 부탁드립니다.