TL;DR
- 문법 간소화: Scala 3는 들여쓰기 기반 구문,
then키워드 도입- enum 지원:
sealed trait+case object대신 네이티브enum사용- 암시적 재설계:
implicit→given/using/extension으로 역할 분리- 새 타입 기능: Union Type (
|), Opaque Type 추가- 하위 호환: Scala 3는 대부분의 Scala 2 문법 지원, 점진적 마이그레이션 가능
대상 독자: Scala 2 경험자 또는 두 버전 모두 이해하고 싶은 개발자
선수 지식:
- Scala 기본 문법 이해
- 함수형 프로그래밍 기초 개념
- (선택) Scala 2 코드베이스 경험
동일한 로직을 Scala 2와 Scala 3로 구현하여 차이점을 비교합니다. 두 버전 간의 문법 변화를 이해하면 코드를 마이그레이션하거나 양쪽 버전 모두에서 작업할 때 도움이 됩니다. 이 문서에서는 주요 변경 사항들을 실제 코드 예제로 보여줍니다.
문법 스타일#
Scala 3에서는 문법이 상당히 간소화되었습니다. 가장 눈에 띄는 변화는 들여쓰기 기반 구문의 도입과 일부 키워드의 변경입니다.
블록 구문
Scala 3에서는 중괄호 대신 들여쓰기로 블록을 구분할 수 있습니다. 이는 Python과 유사한 스타일로, 코드가 더 간결해집니다. Scala 2에서는 반드시 중괄호를 사용해야 합니다.
// 들여쓰기 기반 (선택)
def greet(name: String): String =
val greeting = s"Hello, $name!"
greeting
// 여러 줄
def process(x: Int): Int =
val doubled = x * 2
val squared = doubled * doubled
squared// 중괄호 필수
def greet(name: String): String = {
val greeting = s"Hello, $name!"
greeting
}
// 여러 줄
def process(x: Int): Int = {
val doubled = x * 2
val squared = doubled * doubled
squared
}if 표현식
Scala 3에서는 if 조건 주위의 괄호가 선택 사항이 되었고, then 키워드가 도입되었습니다. 이로 인해 조건문이 더 자연스러운 영어 문장처럼 읽힙니다.
val result = if x > 0 then "positive" else "non-positive"
val grade = if score >= 90 then "A"
else if score >= 80 then "B"
else if score >= 70 then "C"
else "F"val result = if (x > 0) "positive" else "non-positive"
val grade = if (score >= 90) "A"
else if (score >= 80) "B"
else if (score >= 70) "C"
else "F"for 표현식
for 표현식도 마찬가지로 들여쓰기 기반 구문을 지원합니다. do 키워드는 부수 효과를 위한 for 루프에서, yield는 새 컬렉션을 생성할 때 사용합니다.
// do 키워드
for x <- list do
println(x)
// 여러 생성자
for
x <- 1 to 3
y <- 1 to 3
do
println(s"$x, $y")
// yield
val result = for
x <- 1 to 5
if x % 2 == 0
yield x * x// 괄호/중괄호
for (x <- list) {
println(x)
}
// 여러 생성자
for {
x <- 1 to 3
y <- 1 to 3
} {
println(s"$x, $y")
}
// yield
val result = for {
x <- 1 to 5
if x % 2 == 0
} yield x * x핵심 포인트
- 블록 구문: Scala 3에서 중괄호 대신 들여쓰기 사용 가능
- if 표현식: Scala 3에서 괄호 생략,
then키워드 도입- for 표현식: Scala 3에서
do키워드로 부수 효과 루프 표현- 새 문법은 코드를 더 간결하고 Python처럼 읽기 쉽게 만듦
열거형 (Enum)#
Scala 3에서 가장 환영받는 변화 중 하나는 네이티브 enum 지원입니다. Scala 2에서는 sealed trait과 case object/class를 조합해야 했던 패턴이 단일 enum 선언으로 간소화되었습니다.
// 단순 열거형
enum Color:
case Red, Green, Blue
// 매개변수 열거형
enum HttpStatus(val code: Int):
case OK extends HttpStatus(200)
case NotFound extends HttpStatus(404)
case ServerError extends HttpStatus(500)
// ADT
enum Shape:
case Circle(radius: Double)
case Rectangle(width: Double, height: Double)
// 사용
val color = Color.Red
val status = HttpStatus.OK
val circle = Shape.Circle(5.0)// sealed trait + case object
sealed trait Color
object Color {
case object Red extends Color
case object Green extends Color
case object Blue extends Color
}
// 매개변수 열거형
sealed abstract class HttpStatus(val code: Int)
object HttpStatus {
case object OK extends HttpStatus(200)
case object NotFound extends HttpStatus(404)
case object ServerError extends HttpStatus(500)
}
// ADT
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
// 사용
val color: Color = Color.Red
val status: HttpStatus = HttpStatus.OK
val circle: Shape = Circle(5.0)핵심 포인트
- Scala 3 enum: 단순 열거형, 매개변수 열거형, ADT 모두 지원
- Scala 2:
sealed trait+case object/class조합 필요enum은 보일러플레이트를 대폭 줄이고 의도를 명확히 표현
암시적 기능#
Scala 3에서 가장 큰 변화는 암시적 기능의 재설계입니다. implicit 키워드는 여러 역할을 했기 때문에 혼란스러웠지만, Scala 3에서는 각 용도에 맞는 별도의 키워드로 분리되었습니다.
암시적 값과 매개변수
암시적 값 정의에는 given, 암시적 매개변수에는 using 키워드를 사용합니다. 이로 인해 코드의 의도가 더 명확해집니다.
// given 인스턴스
given defaultTimeout: Int = 5000
// using 절
def connect(url: String)(using timeout: Int): Unit =
println(s"Connecting to $url with timeout $timeout")
// 호출
connect("localhost") // given 사용
connect("localhost")(using 10000) // 명시적// implicit val
implicit val defaultTimeout: Int = 5000
// implicit 매개변수
def connect(url: String)(implicit timeout: Int): Unit =
println(s"Connecting to $url with timeout $timeout")
// 호출
connect("localhost") // implicit 사용
connect("localhost")(10000) // 명시적확장 메서드
확장 메서드는 기존 타입에 새 메서드를 추가하는 기능입니다. Scala 3에서는 extension 키워드로 이를 명시적으로 표현합니다. Scala 2에서는 implicit class 패턴을 사용했습니다.
extension (s: String)
def exclaim: String = s + "!"
def words: List[String] = s.split(" ").toList
def reverse: String = s.reverse
"Hello".exclaim // "Hello!"
"Hello World".words // List("Hello", "World")implicit class StringOps(s: String) {
def exclaim: String = s + "!"
def words: List[String] = s.split(" ").toList
def reverse: String = s.reverse
}
"Hello".exclaim // "Hello!"
"Hello World".words // List("Hello", "World")타입 클래스
타입 클래스 패턴도 given과 using을 통해 더 간결하게 표현됩니다. implicitly 대신 summon을 사용하여 암시적 인스턴스를 조회합니다.
trait Show[A]:
def show(a: A): String
object Show:
given Show[Int] with
def show(a: Int): String = a.toString
given Show[String] with
def show(a: String): String = s"\"$a\""
def print[A](a: A)(using s: Show[A]): Unit =
println(s.show(a))
// summon
val intShow = summon[Show[Int]]trait Show[A] {
def show(a: A): String
}
object Show {
implicit val intShow: Show[Int] = new Show[Int] {
def show(a: Int): String = a.toString
}
implicit val stringShow: Show[String] = new Show[String] {
def show(a: String): String = s""""$a""""
}
}
def print[A](a: A)(implicit s: Show[A]): Unit =
println(s.show(a))
// implicitly
val intShow = implicitly[Show[Int]]핵심 포인트
- given/using: 암시적 값과 매개변수를 명확히 분리
- extension: 확장 메서드를 위한 전용 키워드 (implicit class 대체)
- summon:
implicitly대체, 타입 클래스 인스턴스 조회- 역할별 키워드 분리로 코드 의도가 더 명확해짐
진입점#
애플리케이션의 진입점 정의 방식도 간소화되었습니다. Scala 3에서는 @main 어노테이션을 사용하여 더 간단하게 메인 함수를 정의할 수 있으며, 명령줄 인자도 자동으로 파싱됩니다.
@main def hello(): Unit =
println("Hello, World!")
// 인자 받기
@main def greet(name: String, count: Int): Unit =
for _ <- 1 to count do
println(s"Hello, $name!")object Hello {
def main(args: Array[String]): Unit = {
println("Hello, World!")
}
}
// 또는 App 트레이트
object Hello extends App {
println("Hello, World!")
}핵심 포인트
- @main: Scala 3의 새로운 진입점 어노테이션, 간결한 선언
- 자동 인자 파싱:
@main def greet(name: String)형태로 명령줄 인자 자동 처리- Scala 2:
def main(args: Array[String])또는extends App사용
새로운 타입 기능 (Scala 3 전용)#
Scala 3에서는 타입 시스템이 크게 확장되었습니다. Union Type과 Opaque Type은 Scala 3에서만 사용할 수 있는 새로운 기능입니다.
Union Types
Union Type은 여러 타입 중 하나일 수 있는 값을 표현합니다. Scala 2에서는 Either를 사용해야 했지만, Scala 3에서는 | 연산자로 간단히 표현할 수 있습니다.
// Scala 3
def process(input: Int | String): String = input match
case i: Int => s"숫자: $i"
case s: String => s"문자열: $s"
// Scala 2에서는 Either 사용
def process(input: Either[Int, String]): String = input match {
case Left(i) => s"숫자: $i"
case Right(s) => s"문자열: $s"
}Opaque Types
Opaque Type은 런타임 오버헤드 없이 타입 안전성을 제공합니다. 외부에서는 다른 타입으로 보이지만 내부에서는 기반 타입과 동일하게 처리됩니다.
// Scala 3
object Money:
opaque type USD = BigDecimal
def usd(amount: BigDecimal): USD = amount
extension (x: USD) def value: BigDecimal = x
// Scala 2에서는 Value Class 사용
case class USD(value: BigDecimal) extends AnyVal핵심 포인트
- Union Type (
|): 여러 타입 중 하나임을 직접 표현 (Either 없이)- Opaque Type: 런타임 오버헤드 없는 타입 래핑 (Value Class 개선)
- 두 기능 모두 Scala 3 전용, Scala 2에서는 대안 패턴 사용
마이그레이션 요약#
Scala 2에서 Scala 3로 마이그레이션할 때 참고할 주요 변경 사항을 정리했습니다. 대부분의 변경은 더 명확하고 일관된 문법을 위한 것입니다.
| Scala 2 | Scala 3 |
|---|---|
implicit val | given |
implicit def (변환) | given Conversion |
(implicit x: T) | (using x: T) |
implicitly[T] | summon[T] |
implicit class | extension |
sealed trait + case object | enum |
_ (와일드카드 import) | * |
핵심 포인트
- 암시적 기능이 가장 큰 변화:
implicit→given,using,extensionenum으로 ADT 정의가 간단해짐- 와일드카드 import:
_→*
호환성#
Scala 3 컴파일러는 하위 호환성을 위해 Scala 2 문법의 대부분을 여전히 지원합니다. 따라서 기존 코드를 점진적으로 마이그레이션할 수 있습니다.
// Scala 3에서도 가능
implicit val x: Int = 42
def f(implicit n: Int): Int = n * 2
// 권장하지 않지만 동작함
for (i <- 1 to 5) { println(i) }이 호환성 덕분에 Scala 2 코드베이스를 Scala 3로 마이그레이션할 때 모든 것을 한 번에 바꿀 필요가 없습니다. 새로운 코드는 Scala 3 스타일로 작성하면서 기존 코드는 점진적으로 업데이트할 수 있습니다.
핵심 포인트
- Scala 3 컴파일러는 대부분의 Scala 2 문법 지원
- 점진적 마이그레이션 가능: 새 코드는 Scala 3 스타일, 기존 코드는 그대로
- 권장하지 않지만 기존
implicit, 중괄호 문법도 동작함
다음 단계#
더 자세한 버전 비교와 마이그레이션 정보는 다음 자료를 참고하세요.
- 버전 비교 — 전체 차이점 요약
- 마이그레이션 가이드