Scala의 특징

Scala의 특징

Scala라는 이름은 scalable(확장 가능한)이라는 단어에서 왔어요. 그 이름에 걸맞게 Scala 언어는 바쁘게 돌아가는 웹사이트를 구동하고, 방대한 데이터셋을 분석하는 데 쓰이고 있어요. 이 섹션에서는 Scala를 확장 가능한 언어로 만들어 주는 특징들을 소개할게요.

이 특징들은 크게 세 가지로 나뉘어요.

  • 고수준 언어 특징 (High-level language features)
  • 저수준 언어 특징 (Lower-level language features)
  • Scala 생태계 특징 (Scala ecosystem features)

출처: Scala 3 Book

본문

고수준 특징

Scala를 흔히 말하는 "3만 피트 상공"에서 내려다보면, 이렇게 정리할 수 있어요.

  • 고수준 프로그래밍 언어예요.
  • 간결하고 읽기 쉬운 문법을 가졌어요.
  • 정적 타이핑 언어지만 동적인 느낌이 나요.
  • 표현력이 뛰어난 타입 시스템을 가졌어요.
  • 함수형 프로그래밍(FP) 언어예요.
  • 객체지향 프로그래밍(OOP) 언어예요.
  • FP와 OOP의 융합을 지원해요.
  • 컨텍스트 추상화(contextual abstractions)로 용어 추론(term inference)을 명확하게 구현해요.
  • JVM에서 실행돼요 (브라우저에서도요).
  • Java 코드와 자연스럽게 상호작용해요.
  • 서버 측 애플리케이션(마이크로서비스 포함), 빅데이터 애플리케이션에 쓰이고, Scala.js를 통해 브라우저에서도 쓸 수 있어요.

다음 절에서 이 특징들을 하나씩 살펴볼게요.

고수준 언어

Scala는 적어도 두 가지 면에서 고수준 언어로 여겨져요. 첫째, Java 같은 다른 현대 언어들과 마찬가지로 포인터나 메모리 관리 같은 저수준 개념을 직접 다루지 않아요. 둘째, 람다와 고차 함수를 활용해서 아주 높은 수준에서 코드를 작성해요.

함수형 프로그래밍에서 하는 말처럼, Scala에서는 "어떻게 구현할지"가 아니라 "무엇을 원하는지"를 작성해요. 다시 말해, 아래처럼 명령형 코드를 쓰지 않아요.

Scala 2

import scala.collection.mutable.ListBuffer

def double(ints: List[Int]): List[Int] = {
  val buffer = new ListBuffer[Int]()
  for (i <- ints) {
    buffer += i * 2
  }
  buffer.toList
}

val oldNumbers = List(1, 2, 3)
val newNumbers = double(oldNumbers)

Scala 3

import scala.collection.mutable.ListBuffer

def double(ints: List[Int]): List[Int] =
  val buffer = new ListBuffer[Int]()
  for i <- ints do
    buffer += i * 2
  buffer.toList

val oldNumbers = List(1, 2, 3)
val newNumbers = double(oldNumbers)

이 코드는 컴파일러에게 단계별로 "무엇을 하라"고 지시해요. 대신 우리는 고차 함수와 람다를 쓰는 고수준 함수형 코드로 같은 결과를 계산해요.

Scala 2 and 3

val newNumbers = oldNumbers.map(_ * 2)

보시다시피 이 코드는 훨씬 더 간결하고, 읽기 쉬우며, 유지보수하기도 쉬워요.

간결한 문법

Scala는 간결하고 읽기 쉬운 문법을 가져요. 예를 들어 변수는 간결하게 만들고, 그 타입도 분명하게 드러나요.

Scala 2 and 3

val nums = List(1,2,3)
val p = Person("Martin", "Odersky")

고차 함수와 람다는 간결하면서도 읽을 수 있는 코드를 만들어줘요.

Scala 2 and 3

nums.map(i => i * 2)   // long form
nums.map(_ * 2)        // short form

nums.filter(i => i > 1)
nums.filter(_ > 1)

트레이트, 클래스, 메서드는 깔끔하고 가벼운 문법으로 정의돼요.

Scala 2

trait Animal {
  def speak(): Unit
}

trait HasTail {
  def wagTail(): Unit
}

class Dog extends Animal with HasTail {
  def speak(): Unit = println("Woof")
  def wagTail(): Unit = println("⎞⎜⎛  ⎞⎜⎛")
}

Scala 3

trait Animal:
  def speak(): Unit

trait HasTail:
  def wagTail(): Unit

class Dog extends Animal, HasTail:
  def speak(): Unit = println("Woof")
  def wagTail(): Unit = println("⎞⎜⎛  ⎞⎜⎛")

연구에 따르면 개발자가 코드를 쓰는 시간에 비해 읽는 시간의 비율이 최소 10:1이라고 해요. 그래서 간결하고 읽기 쉬운 코드를 작성하는 게 중요해요.

동적인 느낌

Scala는 정적 타입 언어지만, 타입 추론(type inference) 덕분에 동적으로 느껴져요. 아래 표현들은 전부 Python이나 Ruby 같은 동적 타입 언어처럼 보이지만, 실제로는 전부 Scala 코드예요.

Scala 2

val s = "Hello"
val p = Person("Al", "Pacino")
val sum = nums.reduceLeft(_ + _)
val y = for (i <- nums) yield i * 2
val z = nums
  .filter(_ > 100)
  .filter(_ < 10_000)
  .map(_ * 2)

Scala 3

val s = "Hello"
val p = Person("Al", "Pacino")
val sum = nums.reduceLeft(_ + _)
val y = for i <- nums yield i * 2
val z = nums
  .filter(_ > 100)
  .filter(_ < 10_000)
  .map(_ * 2)

Heather Miller가 말했듯이 Scala는 강력한 정적 타입 언어로 간주되며, 정적 타입이 주는 이점을 모두 얻을 수 있어요.

  • 정확성(Correctness): 대부분의 오류를 컴파일 타임에 잡아요.
  • 훌륭한 IDE 지원: 신뢰할 수 있는 코드 완성이 제공돼요.
  • 컴파일 타임에 오류를 잡는다는 건, 타이핑하는 순간 실수를 발견한다는 뜻이에요.
  • 쉽고 안정적인 리팩터링: 코드를 자신 있게 리팩터링할 수 있어요.
  • 메서드의 타입 선언은 그 메서드가 무엇을 하는지 읽는 사람에게 알려주고, 문서 역할도 해줘요.
  • 확장성과 유지보수성: 타입은 아무리 큰 애플리케이션과 개발팀에서도 정확성을 보장하는 데 도움을 줘요.

강한 타이핑과 뛰어난 추론의 조합은 컨텍스트 추상화 같은 메커니즘을 가능하게 해요. 이를 통해 보일러플레이트 코드를 생략할 수 있는데, 이런 코드는 보통 타입 정의와 주어진 컨텍스트를 바탕으로 컴파일러가 추론할 수 있어요.

표현력이 뛰어난 타입 시스템

Scala의 타입 시스템은 추상화가 안전하고 일관된 방식으로 쓰이도록 컴파일 타임에 강제해요. 특히 타입 시스템이 지원하는 것들은 다음과 같아요.

  • 추론된 타입 (Inferred types)
  • 제네릭 클래스 (Generic classes)
  • 변성(variance) 애너테이션
  • 상한(upper)과 하한(lower) 타입 바운드
  • 다형적 메서드 (Polymorphic methods)
  • 교차 타입 (Intersection types)
  • 합집합 타입 (Union types)
  • 타입 람다 (Type lambdas)
  • given 인스턴스와 using
  • 확장 메서드 (Extension methods)
  • 타입 클래스 (Type classes)
  • 다중 버전 동등성 (Multiversal equality)
  • 불투명 타입 별칭 (Opaque type aliases)
  • 열린(open) 클래스
  • 매치 타입 (Match types)
  • 의존 함수 타입 (Dependent function types)
  • 다형적 함수 타입 (Polymorphic function types)
  • 컨텍스트 바운드 (Context bounds)
  • 컨텍스트 함수 (Context functions)
  • 객체 멤버로서의 내부 클래스(inner classes)와 추상 타입 멤버(abstract type members)

이 특징들이 결합되면 프로그래밍 추상화를 안전하게 재사용하고, 소프트웨어를 타입 안전하게 확장할 수 있는 강력한 기반이 돼요.

함수형 프로그래밍 언어

Scala는 함수형 프로그래밍(FP) 언어예요. 그 말은 곧 이렇다는 뜻이에요.

  • 함수는 값이며, 다른 값처럼 전달할 수 있어요.
  • 고차 함수가 직접 지원돼요.
  • 람다가 내장돼 있어요.
  • Scala의 모든 것은 값을 반환하는 표현식(expression)이에요.
  • 문법상 불변 변수를 쓰기 쉽고, 그 사용이 권장돼요.
  • 표준 라이브러리에 풍부한 불변 컬렉션 클래스가 있어요.
  • 그 컬렉션 클래스들은 수십 가지 함수형 메서드를 제공하는데, 이들은 컬렉션을 변경하지 않고 데이터의 갱신된 복사본을 반환해요.

객체지향 언어

Scala는 객체지향 프로그래밍(OOP) 언어예요. 모든 값은 클래스의 인스턴스이고, 모든 "연산자(operator)"는 메서드예요.

Scala에서는 모든 타입이 최상위 클래스 Any에서 상속받아요. Any의 바로 아래 자식은 AnyVal(값 타입, 예: Int, Boolean)과 AnyRef(참조 타입, Java에서와 같음)예요. 이 말은 Java에서 원시 타입과 박싱된 타입을 구분하는 방식(예: int vs Integer)이 Scala에는 없다는 뜻이에요. 박싱과 언박싱은 사용자에게 완전히 투명하게 처리돼요.

FP/OOP 융합 지원

Scala의 정수는 타입이 지정된 환경에서 함수형 프로그래밍과 객체지향 프로그래밍을 융합하는 것이에요.

  • 함수는 로직(logic)을 담당하고,
  • 객체는 모듈성(modularity)을 담당해요.

Martin Odersky가 말했듯이, "Scala는 함수형과 객체지향 프로그래밍의 융합이 가능하고 실용적임을 보여주기 위해 설계되었다"고 해요.

더 명확해진 용어 추론

Haskell에 이어 Scala는 어떤 형태로든 implicit을 가진 두 번째로 인기 있는 언어였어요. Scala 3에서는 이 개념들이 완전히 재설계되고 더 명확하게 구현되었어요.

핵심 아이디어는 **용어 추론(term inference)**이에요. 즉, 타입이 주어지면 컴파일러가 그 타입을 가지는 "정식(canonical)" 용어를 합성해요. Scala에서 컨텍스트 파라미터는 명시적으로 적어줄 수도 있는 추론된 인자 용어로 직접 이어져요.

이 개념의 활용 사례에는 타입 클래스 구현, 컨텍스트 구축, 의존성 주입, 능력(capabilities) 표현, 새 타입 계산, 그리고 타입 간 관계 증명 등이 있어요. Scala 3는 이 과정을 그 어느 때보다 명확하게 만들어요. 컨텍스트 추상화에 대한 자세한 내용은 Reference 문서에서 읽어볼 수 있어요.

클라이언트 & 서버

Scala 코드는 Java 가상 머신(JVM)에서 실행되므로 그 이점을 모두 얻을 수 있어요.

  • 보안 (Security)
  • 성능 (Performance)
  • 메모리 관리 (Memory management)
  • 이식성과 플랫폼 독립성 (Portability and platform independence)
  • 기존의 풍부한 Java 및 JVM 라이브러리를 활용할 수 있는 능력

JVM에서 실행되는 것 외에도 Scala는 Scala.js로 브라우저에서도 실행되며(인기 있는 JavaScript 라이브러리를 통합하는 오픈소스 서드파티 도구도 있어요), Scala Native와 GraalVM으로 네이티브 실행 파일을 만들 수도 있어요.

자연스러운 Java 상호작용

Scala 애플리케이션에서 Java 클래스와 라이브러리를 사용할 수 있고, Java 애플리케이션에서 Scala 코드를 사용할 수도 있어요. 두 번째에 관해서는 Akka, Play Framework 같은 대형 라이브러리가 Scala로 작성됐는데 Java 애플리케이션에서 사용할 수 있어요. 첫 번째에 관해서는 Java 클래스와 라이브러리가 Scala 애플리케이션에서 매일 사용돼요.

예를 들어 Scala에서 Java의 BufferedReaderFileReader로 파일을 읽을 수 있어요.

Scala 2 and 3

import java.io.*
val br = BufferedReader(FileReader(filename))
// read the file with `br` ...

Scala에서 Java 코드를 쓰는 건 일반적으로 자연스러워요. Java 컬렉션도 Scala에서 쓸 수 있고, Scala의 풍부한 컬렉션 클래스 메서드를 Java 컬렉션에 적용하려면 몇 줄의 코드만으로 변환할 수 있어요.

Scala 2 and 3

import scala.jdk.CollectionConverters.*
val scalaList: Seq[Integer] = JavaClass.getJavaList().asScala.toSeq

풍부한 라이브러리

이 페이지의 세 번째 섹션에서 보겠지만, Scala 라이브러리와 프레임워크들은 바쁜 웹사이트를 구동하고 방대한 데이터셋을 다루기 위해 만들어졌어요.

  • Play Framework는 확장성이 뛰어난 애플리케이션을 만들기 위한 가볍고, 상태가 없고, 개발자 친화적이며, 웹 친화적인 아키텍처예요.
  • Apache Spark는 스트리밍, SQL, 머신러닝, 그래프 처리용 내장 모듈을 갖춘 빅데이터 처리를 위한 통합 분석 엔진이에요.

Awesome Scala 목록에는 개발자들이 Scala 애플리케이션을 만들기 위해 만든 수십 개의 추가 오픈소스 도구가 있어요. 서버 측 프로그래밍 외에도 Scala.js는 JavaScript 대신 쓰는 강한 타입의 대안이며, Facebook의 React 라이브러리, jQuery 등을 통합하는 오픈소스 서드파티 라이브러리가 있어요.

저수준 언어 특징

앞선 섹션은 Scala의 고수준 특징을 다뤘는데, 흥미롭게도 고수준에서는 Scala 2와 Scala 3 모두에 대해 같은 말을 할 수 있어요. 10년 전 Scala는 매력적인 특징들의 강력한 기반 위에서 시작했고, 이 섹션에서 보겠지만 그 이점들은 Scala 3에서 더욱 개선됐어요. 개발자들이 매일 쓰는 언어 특징, 즉 "해수면(sea level)" 관점에서 보면 Scala 3는 Scala 2에 비해 상당한 이점이 있어요.

  • enum으로 대수적 데이터 타입(ADT)을 더 간결하게 만들 수 있어요.
  • 더욱 간결하고 읽기 쉬운 문법:
    • "조용한(quiet)" 제어 구조 문법이 읽기 쉬워요.
    • 선택적인 중괄호(optional braces).
    • 코드의 기호가 줄어 시각적 노이즈가 줄어들어 읽기 쉬워요.
    • 클래스 인스턴스를 만들 때 new 키워드가 일반적으로 더 이상 필요 없어요.
    • 패키지 객체의 형식적임이 사라지고 더 단순한 "최상위(top level)" 정의로 대체됐어요.
  • 더 명확한 문법:
    • implicit 키워드의 여러 용도가 제거되고, given, using, extension처럼 더 명확한 키워드로 대체됐어요 (자세한 내용은 Givens 섹션 참고). 이는 메커니즘보다 의도(intent)에 초점을 맞춘 거예요.
    • 확장 메서드(extension methods)가 implicit 클래스를 더 명확하고 단순한 메커니즘으로 대체해요.
    • 클래스에 open 수정자를 추가해 개발자가 의도적으로 "이 클래스는 수정을 위해 열려 있다"고 선언하게 하여, 코드베이스에 대한 임시 확장을 제한해요.
    • 다중 버전 동등성==!=로 말도 안 되는 비교(예: PersonPlanet을 비교하려는 시도)를 배제해요.
    • 매크로가 훨씬 쉽게 구현돼요.
    • 합집합(union)과 교차(intersection)는 타입을 유연하게 모델링하는 방법을 제공해요.
    • 트레이트 파라미터가 초기 초기화자(early initializer)를 대체하고 단순화해요.
    • 불투명 타입 별칭이 값 클래스(value class)의 대부분 용도를 대체하면서 박싱이 없음을 보장해요.
    • export 절이 집계(aggregation)를 표현하는 단순하고 일반적인 방법을 제공하며, 클래스에서 상속하는 패키지 객체의 이전 파사드 패턴을 대체해요.
    • procedure 문법이 제거되고 varargs 문법이 변경됐는데, 둘 다 언어를 더 일관성 있게 만들기 위함이에요.
    • @infix 애너테이션은 메서드를 어떻게 적용하고 싶은지 명확하게 만든다.
    • @targetName 메서드 애너테이션은 메서드의 대체 이름을 정의해서 Java 상호운용성을 개선하고, 기호 연산자에 별칭을 제공할 수 있게 해줘요.

이 모든 특징을 여기서 보여주기에는 공간이 부족하니, 위 항목의 링크를 따라가면 그 특징들이 실제로 동작하는 모습을 볼 수 있어요. 이 특징들은 전부 Overview 문서의 New, Changed, Dropped 특징 페이지에서 자세히 다뤄져요.

Scala 생태계

Scala는 활기찬 생태계를 가지고 있고, 모든 요구에 맞는 라이브러리와 프레임워크가 있어요. Awesome Scala 목록은 Scala 개발자에게 제공되는 수백 개의 오픈소스 프로젝트 목록을 제공하고, Scaladex는 Scala 라이브러리의 검색 가능한 인덱스를 제공해요. 주목할 만한 라이브러리 몇 가지는 아래에 나열돼 있어요.

웹 개발

  • Play Framework: Ruby on Rails 모델을 따라 확장성이 뛰어난 애플리케이션을 위한 가볍고, 상태가 없고, 개발자 친화적이며, 웹 친화적인 아키텍처가 됐어요.
  • Scalatra: Sinatra에서 영감을 받은 작고 고성능인 비동기 웹 프레임워크예요.
  • Finatra: X를 위해 만든 Scala 서비스.
  • Scala.js: 더 안전한 방식으로 견고한 프론트엔드 웹 애플리케이션을 만들 수 있는 강한 타입의 JavaScript 대안.
  • ScalaJs-React: Facebook의 React 라이브러리를 Scala.js로 격상시켜 가능한 한 타입 안전하고 Scala 친화적으로 만들려는 노력.
  • HTTP(S) 라이브러리: Akka-http, Finch, Http4s, Sttp
  • JSON 라이브러리: Argonaut, Circe, Json4s, Play-JSON
  • 직렬화: ScalaPB

과학과 데이터 분석

  • Algebird
  • Spire
  • Squants

빅데이터

  • Apache Spark
  • Apache Flink

AI, 머신러닝

  • BigDL (Apache Spark용 분산 딥러닝 프레임워크)
  • TensorFlow Scala

함수형 프로그래밍 & 함수형 반응형 프로그래밍

  • FP: Cats, Zio
  • 함수형 반응형 프로그래밍(FRP): fs2, monix

빌드 도구

  • sbt
  • Gradle
  • Mill

요약

이 페이지에서 봤듯이 Scala는 고수준, 매일 쓰는 프로그래밍 수준, 그리고 개발자 생태계를 통해 훌륭한 프로그래밍 언어 특징을 많이 가지고 있어요.

더 알아보기 (Learn more)