Skip to content

Scala 치트시트

JVM의 하이브리드 함수형/OO 언어.

01

기초

변수 & 타입

더 안전하고 예측 가능한 코드를 위해 var(가변)보다 val(불변)을 선호하세요. Scala는 타입을 추론하지만, 공개 API의 가독성을 위해 명시적 어노테이션이 도움됩니다. 모든 것이 객체입니다—프리미티브 없음(Int, Boolean은 클래스).

scala
val name = "Alice"   // immutable (preferred)
var age = 30          // mutable
val pi: Double = 3.14159
val isDev: Boolean = true
val nums: List[Int] = List(1, 2, 3)
println(name.getClass)  // class java.lang.String

문자열 보간

s"..."가 ${expr} 보간을 활성화. f"..."가 printf 스타일 포맷팅(%s, %.2f) 추가. raw"..."가 이스케이프 시퀀스 비활성화. 이 접두사들이 문자열 빌드를 타입 안전하고 읽기 쉽게 만듭니다.

scala
val name = "Alice"
val age = 30
println(s"Name: ${name}, Age: ${age}")
println(s"Next year: ${age + 1}")
println(s"Upper: ${name.toUpperCase}")
println(f"${name}%s weighs ${65.5}%.1f kg")  // formatted
println(raw"No \n escape")  // raw string

튜플

튜플이 2-22개의 이질적 값을 그룹화. _1, _2로 접근(1부터 시작). val (a, b) = tuple로 분해. 2개 이상의 요소에는 이름 있는 필드와 더 나은 가독성을 위해 case class를 선호하세요.

scala
val pair = ("Alice", 30)
println(pair._1)  // Alice
println(pair._2)  // 30
val (name, age) = pair  // destructure
println(s"${name}: ${age}")
// Scala 3: val p = ("a", 1, 2.0)
val triple = ("a", 1, 2.0)
println(triple._3)  // 2.0

타입 추론 & 명시

Scala가 지역 변수와 반환 타입을 추론. 공개 API, 재귀 함수, 모호한 경우에는 명시적 타입을 사용하세요. 타입 명시(expr: Type)가 타입을 강제—업캐스팅이나 모호성 해소에 유용.

scala
val x = 42           // Int inferred
val y: Long = 42      // explicit Long
val z = 42: Long      // type ascription
val list = List(1, 2, 3)  // List[Int]
val mixed: List[Any] = List(1, "a", true)
def double(x: Int) = x * 2  // return type inferred

Unit & Nothing

Unit은 void 같습니다(하나의 값: ()). Nothing은 인스턴스가 없는 바닥 타입—반환하지 않는 함수(throw, 무한 루프)에 사용. Nothing은 모든 타입의 하위 타입이어서 유연한 타입 추론을 가능하게 합니다.

scala
def printIt(x: Int): Unit = println(x)  // like void
val u: Unit = ()    // Unit has one value: ()
// Nothing is the bottom type—no instances
def error(msg: String): Nothing =
  throw new RuntimeException(msg)
// Nothing is a subtype of everything
val n: Nothing = error("boom")
02

문자열

일반적인 문자열 메서드

Scala 문자열은 implicit 변환(StringOps)을 통해 추가 메서드를 가진 Java 문자열입니다. 대부분의 메서드가 새 문자열을 반환(불변). 명확성과 정확성을 위해 수동 루프 대신 이를 사용하세요.

scala
val s = "Hello, World"
println(s.length)        // 12
println(s.toUpperCase)   // HELLO, WORLD
println(s.toLowerCase)   // hello, world
println(s.split(", "))   // Array(Hello, World)
println(s.replace("o", "0"))  // Hell0, W0rld
println(s.reverse)       // dlroW ,olleH
println(s.contains("World"))  // true

여러 줄 문자열 (삼중 따옴표)

삼중 따옴표 문자열이 모든 공백과 줄바꿈을 보존. 코드를 깔끔하게 정렬하기 위해 stripMargin과 |를 사용—| 이후의 텍스트만 유지. SQL, JSON, 코드에 내장된 템플릿에 이상적.

scala
val sql = """
  SELECT * FROM users
  WHERE age > 18
  ORDER BY name
"""
println(sql.trim)
// StripMargin for clean indentation
val text = """|Hello
              |World""".stripMargin
println(text)  // Hello
World

문자열 빌딩

mkString이 선택적 접두사/접미사로 컬렉션을 결합—관용적이고 효율적. 루프에서 큰 문자열을 빌드하려면 StringBuilder 사용. 루프에서 반복되는 + 연결은 피하세요(많은 중간 객체 생성).

scala
val parts = List("apple", "banana", "cherry")
println(parts.mkString(", "))     // apple, banana, cherry
println(parts.mkString("[", ", ", "]"))  // [apple, banana, cherry]
val sb = new StringBuilder
for (p <- parts) sb.append(p).append(" ")
println(sb.toString.trim)

문자열을 숫자로

toInt/toDouble은 잘못된 입력에 throw. 안전한 파싱을 위해 toIntOption(Scala 2.13+) 사용하여 Option 반환. 일괄 파싱에는 Try나 Either를 사용하여 예외 없이 함수적으로 오류 처리.

scala
val n = "42".toInt        // 42
val d = "3.14".toDouble    // 3.14
val b = "true".toBoolean   // true
val safe = "abc".toIntOption  // Some(42) or None
// Handling errors
val result = try "x".toInt catch { case _ => 0 }
println(result)  // 0

정규식

.r이 문자열을 Regex로 변환. findFirstIn이 Option 반환, findAllIn이 반복자 반환. 추출을 위해 패턴 매칭에서 case email(e) => 사용. Regex는 내부적으로 Java의 Pattern입니다.

scala
import scala.util.matching.Regex
val email: Regex = "[\w.]+@[\w]+\.[a-z]+".r
val text = "Contact: [email protected]"
email.findFirstIn(text) match {
  case Some(e) => println(s"Found: ${e}")
  case None => println("No email")
}
val replaced = "[0-9]+".r.replaceAllIn("a1b2c3", "#")
println(replaced)  // a#b#c#
03

데이터 구조

List & Seq

List는 불변 단일 연결 리스트—O(1) head/prepend, O(n) 임의 접근. 임의 접근에는 Vector 사용(O(1) 효과적). +:가 앞에 추가, :+가 뒤에 추가. 스레드 안전을 위해 불변 컬렉션을 선호하세요.

scala
val nums = List(1, 2, 3, 4, 5)
println(nums.head)      // 1
println(nums.tail)      // List(2,3,4,5)
println(nums.reverse)   // List(5,4,3,2,1)
println(nums.take(2))   // List(1, 2)
println(nums.drop(2))   // List(3, 4, 5)
println(nums.mkString)  // 12345
val combined = 0 +: nums :+ 6  // List(0,1,2,3,4,5,6)

Map

Map은 불변—작업이 새 Map 반환. Option 접근에는 get(key), 기본값에는 getOrElse 사용. +가 추가/업데이트, -가 제거. 가변 map에는 scala.collection.mutable.Map 사용. 키는 Hashable이어야 합니다.

scala
val ages = Map("Alice" -> 30, "Bob" -> 25)
println(ages("Alice"))            // 30 (throws if missing)
println(ages.getOrElse("Eve", 0)) // 0 (safe)
val updated = ages + ("Eve" -> 28)  // new Map
val removed = ages - "Bob"
ages.foreach { case (k, v) => println(s"${k}: ${v}") }
println(ages.keys)  // Set(Alice, Bob)

Set

Set은 O(1) contains로 불변. 집합 연산에 union(|), intersect(&), diff(~). +가 추가, -가 제거. 중복 제거와 멤버십 테스트에 사용. 가변 variant: scala.collection.mutable.Set.

scala
val a = Set(1, 2, 3)
val b = Set(3, 4, 5)
println(a union b)        // Set(1,2,3,4,5)
println(a intersect b)    // Set(3)
println(a diff b)         // Set(1,2)
println(a subsetOf(Set(1,2,3,4)))  // true
val added = a + 6  // Set(1,2,3,6)

Option (Null 안전)

Option이 null을 대체—Some(value) 또는 None. 변환에는 map/filter/flatMap, 기본값에는 getOrElse 사용. for-comprehension이 Option에서 작동. 부재를 타입에 명시적으로 만들어 NullPointerException을 제거합니다.

scala
def findUser(id: Int): Option[String] =
  if (id == 1) Some("Alice") else None
val name = findUser(1)
println(name.getOrElse("Unknown"))  // Alice
println(name.map(_.toUpperCase))    // Some(ALICE)
println(name.filter(_.startsWith("A")))  // Some(Alice)
val result = for {
  n <- findUser(1)
  if n.startsWith("A")
} yield n.toUpperCase  // Some(ALICE)

Array & Vector

Array는 가변 Java 배열(가장 빠르지만 함수형 업데이트 없음). Vector는 효과적으로 O(1) 임의 접근과 업데이트를 가진 불변—불변 임의 접근 컬렉션에 선호. 순차적에는 List, 인덱스에는 Vector를 사용하세요.

scala
val arr = Array(1, 2, 3, 4)  // mutable, Java array
arr(0) = 10
println(arr(0))  // 10
val vec = Vector(1, 2, 3, 4)  // immutable, fast random access
println(vec(2))  // 3
val updated = vec.updated(0, 10)  // Vector(10,2,3,4)
// Vector: O(1) random access + immutable
// Array: mutable, Java interop, fastest
04

제어 흐름

If / Else (표현식)

Scala에서 if/else는 값을 반환하는 표현식입니다. 이로 인해 삼항 연산자가 필요 없습니다. 양쪽 분기는 호환 가능한 타입이어야 합니다. 간결한 조건부 할당에 사용하세요.

scala
val score = 85
val grade =
  if (score >= 90) "A"
  else if (score >= 80) "B"
  else if (score >= 70) "C"
  else "F"
println(grade)  // B
// if returns a value—no ternary operator needed

For Comprehension

for comprehension이 반복하고 필터(if 가드)와 변환(yield)할 수 있습니다. yield 없으면 루프; yield 있으면 컬렉션을 빌드. to는 끝 포함, until은 끝 제외. flatMap/map/filter 체인과 동일.

scala
for (i <- 1 to 5) println(i)  // 1 2 3 4 5
for (i <- 1 until 5) print(i)  // 1 2 3 4
// With yield (generates collection)
val doubled = for (n <- List(1,2,3)) yield n * 2
println(doubled)  // List(2, 4, 6)
// With filter (guard)
val evens = for (n <- 1 to 10 if n % 2 == 0) yield n
println(evens.toList)  // List(2,4,6,8,10)

Match (패턴 매칭)

match는 Scala의 강력한 패턴 매칭—스테로이드 맞은 switch. 리터럴, OR(|), 가드(if), 타입 매칭, 분해 지원. 완전해야 함(컴파일러가 누락 case 경고). 값을 반환하는 표현식입니다.

scala
val n = 2
val label = n match {
  case 0 => "zero"
  case 1 | 2 | 3 => "small"
  case x if x < 10 => "medium"
  case _ => "large"
}
println(label)  // small
// Match on types
def describe(x: Any): String = x match {
  case i: Int => s"Int: ${i}"
  case s: String => s"String: ${s}"
  case _ => "unknown"
}

While & Do-While

while/do-while은 Unit을 반환하는 명령형 루프입니다. 가변 상태(var)가 필요. 관용적 함수형 Scala를 위해 for-comprehension이나 재귀를 선호. 성능이 요구되거나 부작용에만 while을 사용하세요.

scala
var i = 0
while (i < 3) {
  println(i)
  i += 1
}
var j = 0
do {
  println(j)
  j += 1
} while (j < 3)
// Prefer recursion or for-comprehensions for immutability

Try / Catch / Finally

try/catch/finally는 Java와 비슷하지만 catch에 패턴 매칭을 사용. 함수형 오류 처리를 위해 Try를 선호—예외를 Success/Failure 값으로 감싸 try/catch 보일러플레이트 없이 map/flatMap 체인 가능.

scala
import scala.util.{Try, Success, Failure}
val result = try {
  "abc".toInt
} catch {
  case e: NumberFormatException => 0
} finally {
  println("cleanup")
}
println(result)  // 0
// Functional alternative
val r2 = Try("abc".toInt).getOrElse(0)
println(r2)  // 0
05

함수

메서드 정의

메서드는 def name(params): ReturnType = body 사용. 기본 매개변수와 이름 있는 인자 지원. Unit 반환 = 부작용만. 단일 표현식 본문은 중괄호 생략. =가 필요(없으면 Unit 반환하는 procedure).

scala
def add(a: Int, b: Int): Int = a + b
def greet(name: String, greeting: String = "Hello"): String =
  s"${greeting}, ${name}!"
def log(msg: String): Unit = println(msg)
println(add(3, 4))           // 7
println(greet("Alice"))      // Hello, Alice!
println(greet("Bob", greeting = "Hi"))  // named arg

Lambda (익명 함수)

Lambda: (params) => body. 단일 매개변수 약어로 _ 사용(x => x * 2가 _ * 2로). 여러 _는 다른 매개변수 참조(_ + _). Lambda는 일급—map, filter, reduce 등에 전달.

scala
val square = (x: Int) => x * x
println(square(5))  // 25
val nums = List(1, 2, 3)
println(nums.map(_ * 2))      // List(2, 4, 6)
println(nums.filter(_ > 1))   // List(2, 3)
println(nums.reduce(_ + _))   // 6
// _ is shorthand for the parameter

고차 함수

고차 함수가 함수를 받거나 반환. 강력한 추상화 가능: map/filter/reduce, 조합, 부분 적용. makeAdder가 n을 캡처하는 클로저 반환. 함수형 프로그래밍의 핵심입니다.

scala
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
println(applyTwice(_ + 3, 5))  // 11
def makeAdder(n: Int): Int => Int = _ + n
val add5 = makeAdder(5)
println(add5(10))  // 15
// Functions returning functions = currying-like

커링 & 부분 적용

커링이 매개변수를 여러 리스트로 분할: def f(a)(b). _로 부분 적용하여 특화된 함수 생성. 여러 매개변수 리스트가 타입 추론 향상(컴파일러가 리스트에서 f의 타입 추론 가능). 컬렉션 API에서 흔함.

scala
def add(a: Int)(b: Int): Int = a + b  // curried
val add5 = add(5)_  // partially applied
println(add5(3))  // 8
// Multiple parameter lists
def foldLeft[A, B](list: List[A])(z: B)(f: (B, A) => B): B = ???
// Helps type inference
val sum = List(1,2,3).foldLeft(0)(_ + _)

By-Name & Lazy

By-name 매개변수(=> T)는 사용 시마다 지연 평가—로깅(비활성 시 비싼 msg 건너뛰기)과 커스텀 제어 구조에 유용. lazy val이 첫 접근까지 초기화 지연—비싸거나 선택적 값에 사용.

scala
// By-name parameter: evaluated on each use
def debug(msg: => String): Unit =
  if (debugEnabled) println(msg)
// Lazy evaluation
lazy val expensive = computeHeavy()
println(expensive)  // computed now
def computeHeavy(): Int = { println("computing"); 42 }
06

클래스 & OOP

클래스 & 생성자

주 생성자 매개변수는 클래스 시그니처에 있습니다. val 매개변수는 불변 필드(public getter), var는 가변. 클래스 본문이 생성자입니다. 보조 생성자는 def this(...)를 사용하고 다른 생성자를 호출해야 합니다.

scala
class Person(val name: String, val age: Int) {
  // Constructor params with val/var become fields
  def greet: String = s"Hi, I'm ${name}"
  def isAdult: Boolean = age >= 18
}
val p = new Person("Alice", 30)
println(p.greet)   // Hi, I'm Alice
println(p.name)    // Alice (val field)
println(p.isAdult) // true

Case Class

Case class는 auto-generated equals, hashCode, toString, copy, companion object의 apply/unapply를 가진 불변 데이터 클래스. 데이터 모델링과 패턴 매칭에 사용. 'new' 불필요. Scala에서 ADT의 기초입니다.

scala
case class Point(x: Int, y: Int)
val p1 = Point(3, 4)  // no 'new' needed
val p2 = Point(3, 4)
println(p1 == p2)  // true (value equality)
val moved = p1.copy(x = 5)  // Point(5, 4)
println(p1.x, p1.y)  // 3 4 (fields auto-visible)
// Auto: equals, hashCode, toString, copy, companion

Trait (구현이 있는 인터페이스)

Trait은 Java 인터페이스 같지만 구현을 가질 수 있습니다. 클래스는 여러 trait을 mixin할 수 있음(extends/with로). Trait이 동작의 다중 상속을 가능하게. 공유 인터페이스, mixin, linearization을 통한 스택 가능한 수정에 사용.

scala
trait Greetable {
  def name: String  // abstract
  def greet: String = s"Hello, ${name}"  // concrete
}
trait Named {
  val name: String
}
class User(val name: String) extends Greetable
val u = new User("Alice")
println(u.greet)  // Hello, Alice
// Stackable traits via linearization

Object (싱글톤)

object가 싱글톤(인스턴스 하나)을 선언. Companion object(클래스와 같은 이름)가 정적 같은 메서드, 팩토리(apply), 추출자(unapply) 보유. 호출자가 'new'를 생략하도록 팩토리 메서드에 apply 사용. 관용적 Scala입니다.

scala
object Config {
  val version = "1.0"
  def load(): Map[String, String] = Map("key" -> "value")
}
println(Config.version)  // 1.0
// Companion object (same name as class)
class Person(val name: String)
object Person {
  def apply(name: String): Person = new Person(name)
}
val p = Person("Alice")  // uses apply, no 'new'

상속 & 추상 클래스

abstract class는 구현되지 않은 멤버를 가질 수 있습니다. 상속에는 extends, 재정의에는 override 사용. mixin에는 trait을 선호(다중 상속). 생성자 매개변수가 필요하거나 기본 타입을 원할 때 abstract class 사용. 클래스의 단일 상속입니다.

scala
abstract class Shape {
  def area: Double  // abstract
  def describe: String = s"Area: ${area}"
}
class Circle(r: Double) extends Shape {
  def area: Double = math.Pi * r * r
}
val c = new Circle(5)
println(c.describe)  // Area: 78.53...
// override required for concrete members
// abstract class vs trait: use abstract for base, trait for mixins
07

컬렉션 & 함수형

Map / Filter / Fold

map이 변환, filter가 선택, reduce/foldLeft가 집계. 함수형 데이터 변환의 핵심. foldLeft는 시드를 받고 결합적; reduce는 비어있지 않아야 함. 명확성과 불변성을 위해 루프 대신 사용.

scala
val nums = List(1, 2, 3, 4, 5)
println(nums.map(_ * 2))           // List(2,4,6,8,10)
println(nums.filter(_ % 2 == 0))   // List(2,4)
println(nums.reduce(_ + _))        // 15
println(nums.foldLeft(0)(_ + _))   // 15
println(nums.sum)                  // 15
println(nums.mkString(", "))       // 1, 2, 3, 4, 5

FlatMap & For-Comprehension

flatMap이 매핑과 평탄화를 한 단계로—중첩 컬렉션과 모나딕 작업에 필수. for-comprehension이 flatMap/map/filter 체인의 syntactic sugar. 복잡한 중첩 변환에 사용—더 읽기 쉽습니다.

scala
val nested = List(List(1, 2), List(3, 4))
println(nested.flatten)        // List(1,2,3,4)
println(nested.flatMap(_.map(_ * 2)))  // List(2,4,6,8)
// Equivalent for-comprehension:
val result = for {
  inner <- nested
  n <- inner
} yield n * 2
println(result)  // List(2,4,6,8)

그룹화 & 정렬

groupBy가 키별로 Map으로 분할. sorted가 자연스럽게 정렬, sortBy는 키 함수로, sortWith는 비교자로. 새 컬렉션 반환(불변). 데이터 분석, 분류, 순서 지정에 사용.

scala
val words = List("apple", "bat", "cat", "ant")
val byFirst = words.groupBy(_.head)
// Map(a -> List(apple, ant), b -> List(bat), c -> List(cat))
println(byFirst)
val sorted = words.sorted  // List(ant, apple, bat, cat)
val byLen = words.sortBy(_.length)  // List(ant, bat, cat, apple)
val desc = words.sortWith(_ > _)  // descending
println(sorted, byLen)

Either & Try

Try가 예외를 Success/Failure 값으로 감쌈. Either는 Left(오류) 또는 Right(성공)를 나타냄—오류 타입을 구분하려는 도메인 오류에 사용. 둘 다 함수형 오류 전파를 위해 map/flatMap 지원.

scala
import scala.util.{Try, Success, Failure}
def parse(s: String): Try[Int] = Try(s.toInt)
parse("42") match {
  case Success(n) => println(s"OK: ${n}")
  case Failure(e) => println(s"Err: ${e.getMessage}")
}
// Either for domain errors
def divide(a: Int, b: Int): Either[String, Int] =
  if (b == 0) Left("div by zero") else Right(a / b)
println(divide(10, 2))  // Right(5)
println(divide(10, 0))  // Left(div by zero)

지연 컬렉션 (View & Lazy)

.view가 컬렉션을 지연으로—작업이 강제(toList, sum 등)될 때까지 지연. 큰 데이터에서 더 나은 성능을 위해 중간 컬렉션 방지. LazyList(이전 Stream)가 무한 시퀀스 가능—요소가 요청 시 계산.

scala
val nums = (1 to 1000000).view
val result = nums
  .filter(_ % 2 == 0)
  .map(_ * 2)
  .take(5)
  .toList  // forces evaluation
println(result)  // List(4, 8, 12, 16, 20)
// view = lazy, no intermediate collections
// LazyList (Stream) for infinite sequences
val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map(_ + _)
08

패턴 매칭

Case Class 매칭

Sealed trait + case class가 Algebraic Data Type(ADT)을 형성. 컴파일러가 완전성 확인—case 추가 시 매치 업데이트 필요한 곳 경고. 패턴 매칭이 case class를 직접 분해. 관용적 Scala 모델링입니다.

scala
sealed trait Shape
case class Circle(r: Double) extends Shape
case class Square(s: Double) extends Shape
case class Rect(w: Double, h: Double) extends Shape
def area(s: Shape): Double = s match {
  case Circle(r) => math.Pi * r * r
  case Square(s) => s * s
  case Rect(w, h) => w * h
}
println(area(Circle(5)))  // 78.53...

가드 & 조건

가드(if condition)가 패턴에 런타임 검사 추가. 매칭을 더 표현력 있게 만듭니다. 순서가 중요—첫 매치가 이김. 단순 패턴이 표현할 수 없는 범위, 조건, 복잡한 로직에 가드를 사용하세요.

scala
val n = 15
val desc = n match {
  case x if x < 0 => "negative"
  case 0 => "zero"
  case x if x % 2 == 0 => "even"
  case _ => "odd"
}
println(desc)  // odd
// Guards add boolean conditions to patterns

컬렉션 매칭

:: cons 패턴이 리스트를 head와 tail로 분해. _*가 배열/리스트에서 0개 이상 매치. 이 패턴이 재귀적 리스트 처리와 구조적 분해를 가능하게. 파싱과 트리 순회에 강력.

scala
val list = List(1, 2, 3, 4)
list match {
  case Nil => println("empty")
  case head :: Nil => println(s"one: ${head}")
  case head :: tail => println(s"head=${head}, rest=${tail}")
  case _ => println("other")
}
// head :: tail destructures a list
val arr = Array(1, 2, 3)
arr match {
  case Array(1, _*) => println("starts with 1")
  case _ => println("other")
}

Option & Either 매칭

Option/Either에 대한 패턴 매칭이 관용적—Some(x)/None, Right(x)/Left(e). for-comprehension이 match와 함께 flatMap으로 desugar. 명시적 if-else 검사 없이 오류 처리가 자연스럽게 흐름.

scala
def find(id: Int): Option[String] =
  if (id == 1) Some("Alice") else None
find(1) match {
  case Some(name) => println(s"Found: ${name}")
  case None => println("Not found")
}
// In for-comprehensions
val result = for {
  name <- find(1)
  upper = name.toUpperCase
} yield upper
println(result)  // Some(ALICE)

추출자 (unapply)

unapply를 통한 커스텀 추출자가 모든 타입에 대해 패턴 매칭을 가능하게. unapply 메서드가 추출된 값의 Option 반환. 자신만의 패턴 정의 가능—DSL과 파싱에 강력. Case class가 unapply를 auto-generate.

scala
object Email {
  def unapply(s: String): Option[(String, String)] = {
    val parts = s.split("@")
    if (parts.length == 2) Some((parts(0), parts(1))) else None
  }
}
"[email protected]" match {
  case Email(user, domain) =>
    println(s"User: ${user}, Domain: ${domain}")
  case _ => println("Not an email")
}
09

제네릭 & Implicit

제네릭 클래스 & 메서드

제네릭(타입 매개변수 [A])이 타입 안전하고 재사용 가능한 코드 작성. 단일 타입에는 [A], 두 타입에는 [A, B] 사용. 타입 추론이 보통 타입을 알아냄. 제네릭은 런타임에 지워지지만(JVM 제한) 컴파일 타임에 확인.

scala
class Stack[A] {
  private var items: List[A] = Nil
  def push(x: A): Unit = { items = x :: items }
  def pop: Option[A] = items.headOption
}
val s = new Stack[Int]
s.push(1); s.push(2)
println(s.pop)  // Some(2)
def first[A](list: List[A]): Option[A] = list.headOption
println(first(List("a", "b")))  // Some(a)

타입 바운드

<: 상한(A가 하위 타입), >: 하한(A가 상위 타입). 컨텍스트 바운드(A: Ordering)는 해당 타입의 implicit 값 필요. View 바운드(A <% B)는 deprecated—컨텍스트 바운드 대신 사용. 타입 매개변수를 제한.

scala
// Upper bound: A must be Animal or subclass
class Box[A <: Animal](val content: A)
// Lower bound: A must be Dog or superclass
class Kennel[A >: Dog](val occupant: A)
// Context bound: A must have an Ordering
def max[A: Ordering](a: A, b: A): A =
  if (implicitly[Ordering[A]].gt(a, b)) a else b
abstract class Animal { def name: String }
class Dog extends Animal { def name = "Rex" }

Implicit 매개변수

Implicit 매개변수는 스코프에서 컴파일러가 주입. 모든 곳에 전달하고 싶지 않은 구성, 타입 클래스, 의존성에 사용. implicit val/def로 선언. 컴파일러가 둘러싼 스코프와 companion object 검색.

scala
def greet(name: String)(implicit greeting: String): String =
  s"${greeting}, ${name}!"
implicit val defaultGreeting: String = "Hello"
println(greet("Alice"))  // Hello, Alice! (implicit injected)
println(greet("Bob")("Hi"))  // Hi, Bob! (explicit override)
// Compiler finds implicit in scope

타입 클래스 (Implicit 변환)

타입 클래스(implicit 통해)가 타입을 수정하지 않고 동작 추가—ad-hoc 다형성. trait 정의, implicit instance 제공, implicit 매개변수 사용. Ordering, Numeric, Show가 이렇게 작동. 상속보다 유연.

scala
trait Show[A] { def show(a: A): String }
object Show {
  implicit val intShow: Show[Int] = (a: Int) => a.toString
  implicit val strShow: Show[String] = identity
}
def printIt[A](a: A)(implicit s: Show[A]): Unit =
  println(s.show(a))
printIt(42)       // 42
printIt("hello")  // hello
// Type class: ad-hoc polymorphism

확장 메서드 (Scala 2)

Implicit class가 기존 타입에 확장 메서드 추가. 단일 매개변수로 implicit class 정의, 메서드가 해당 타입에서 사용 가능. Int, String 등에 유틸리티 메서드 추가에 사용. Scala 3은 더 깔끔한 'extension' 구문 사용.

scala
implicit class IntOps(val n: Int) extends AnyVal {
  def times(f: => Unit): Unit = (1 to n).foreach(_ => f)
  def squared: Int = n * n
}
5.times { print("hi") }  // hihihihihi
println(5.squared)  // 25
// Scala 3: extension (n: Int) def squared = n * n
10

동시성 & Future

Future & Async

Future가 비동기 계산을 나타냄. onComplete가 콜백 등록. 프로덕션 웹 서버에서 블록(Await.result)하지 마세요—스레드를 점유. for-comprehension으로 함수형으로 future 체인. ExecutionContext 필요.

scala
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f: Future[Int] = Future {
  Thread.sleep(1000)
  42
}
f.onComplete {
  case scala.util.Success(v) => println(s"Got ${v}")
  case scala.util.Failure(e) => println(s"Err: ${e}")
}
// Don't block in production—use callbacks or for-comprehensions

Future 조합

Future에 대한 for-comprehension이 순차적으로 실행(각각 이전 것 대기). 병렬 실행을 위해 모든 Future를 먼저 시작한 후 Future.sequence로 결합. Future.traverse가 매핑 + 시퀀싱을 한 단계로. 비동기 작업을 조합하는 관용적 방법.

scala
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f1 = Future { 10 }
val f2 = Future { 20 }
val sum = for {
  a <- f1
  b <- f2
} yield a + b  // Future(30)
// Parallel execution
val results = Future.sequence(List(
  Future { 1 }, Future { 2 }, Future { 3 }
))
results.map(_.sum)  // Future(6)

병렬 컬렉션

.par이 컬렉션을 병렬 버전으로 변환—작업이 자동으로 여러 스레드 사용. 큰 컬렉션의 CPU 바운드 작업에 좋음. 주의: 비결합적 연산(뺄셈 같은)은 다른 결과를 줄 수 있음. I/O 바운드 작업에는 부적합.

scala
val nums = (1 to 1000000).toList
val sum = nums.par.sum  // parallel sum
println(sum)
val doubled = nums.par.map(_ * 2).toList
// .par converts to ParCollection
// Operations run on multiple threads
// Use for CPU-bound work on large collections

Promise (수동 Future)

Promise는 Future의 쓰기 가능한 면—success/failure로 수동 완료. 콜백 기반 API를 Future로 브릿지하거나 여러 곳에서 Future를 완료해야 할 때 사용. Future는 읽기 전용; Promise는 한 번 쓰기.

scala
import scala.concurrent.{Promise, Future, ExecutionContext}
import ExecutionContext.Implicits.global
val p = Promise[Int]()
val f = p.future
// Complete the promise from another thread
Future { Thread.sleep(100); p.success(42) }
f.foreach(println)  // 42 (when complete)
// p.failure(new Exception) for errors
// Promise = write side, Future = read side

동기 vs 비동기 (Await)

Await.result가 Future 완료까지 현재 스레드 블록(타임아웃 포함). 테스트나 main 메서드에서만 사용—비동기 코드에서 블로킹은 목적을 상실. 프로덕션에서는 콜백(onComplete, map)이나 for-comprehension으로 non-blocking 유지.

scala
import scala.concurrent.{Future, Await}
import scala.concurrent.duration._
import ExecutionContext.Implicits.global
val f = Future { Thread.sleep(500); 42 }
// Block and wait (use sparingly—mainly in tests)
val result = Await.result(f, 1.second)
println(result)  // 42
// Await.ready returns Try, Await.result returns value
// Avoid in production servers—use callbacks instead
11

Implicit 심화

Implicit 매개변수

Implicit 매개변수는 매칭 타입의 implicit 값이 스코프에 있을 때 컴파일러가 자동으로 전달. 'context' 매개변수(ExecutionContext, 로깅, 구성)에 대한 보일러플레이트 감소. 컴파일러가 검색: 로컬 스코프, companion object, implicit 스코프. 항상 명시적으로 전달하여 재정의 가능. 과용은 코드 추적을 어렵게—진정한 컨텍스트 의존성에 사용.

scala
import scala.concurrent.ExecutionContext

// Method with implicit parameter
def process[A](data: List[A])
    (implicit ec: ExecutionContext): Future[Unit] = {
  Future { data.foreach(println) }
}

// The compiler finds an implicit ExecutionContext in scope
implicit val ec: ExecutionContext = ExecutionContext.global
process(List(1, 2, 3))  // ec passed automatically

// Explicitly providing (overrides implicit)
process(List(1, 2, 3))(myCustomEC)

// Multiple implicit parameters
def log(msg: String)(implicit
    level: Level, logger: Logger): Unit = {
  logger.log(level, msg)
}

Implicit 변환

Implicit 변환이 필요 시 타입 간 자동 변환. implicit class(제로 오버헤드를 위해 AnyVal 확장)가 기존 타입에 확장 메서드 추가—Scala가 Int, String 등에 메서드를 추가하는 방법. 주의: implicit 변환은 코드를 혼란스럽게 만들 수 있음(무엇이 변환되는지?). 원시 implicit def보다 확장용 implicit class 선호. import scala.language.implicitConversions로 활성화.

scala
import scala.language.implicitConversions

// Implicit conversion: one type to another
implicit def intToString(n: Int): String = n.toString
val s: String = 42  // intToString(42) called implicitly

// Extension via implicit class (Scala 2.10+)
implicit class RichInt(val self: Int) extends AnyVal {
  def times(f: => Unit): Unit = (1 to self).foreach(_ => f)
  def squared: Int = self * self
}

5.times { println("hi") }  // prints hi 5 times
3.squared  // 9

// Implicit conversion for type compatibility
implicit def javaToScalaList(jl: java.util.List[Int]): List[Int] =
  import scala.jdk.CollectionConverters._
  jl.asScala.toList

val javaList: java.util.List[Int] = ???
val scalaList: List[Int] = javaList  // converted

Implicit 해석 우선순위

컴파일러가 우선순위로 implicit 해석: 로컬 스코프 > companion object > import > 상속. 같은 타입의 두 implicit이 동등하게 스코프에 있으면 'ambiguous implicit' 오류. LowPriorityImplicits trait 패턴이 더 구체적인 implicit으로 재정의 가능한 기본값 제공. 해석 순서 이해가 라이브러리 설계에 중요—사용자가 재정의할 수 있도록 기본값을 low-priority trait에 배치.

scala
// Priority of implicit resolution (highest to lowest):
// 1. Local implicit (defined in current scope)
implicit val ec1: ExecutionContext = ec1

// 2. Implicit in companion object
object MyService {
  implicit val ec2: ExecutionContext = ec2  // lower priority
}

// 3. Implicit scope (imported)
import somePackage.Implicits._

// 4. Implicit parameter default (inherited trait)
trait DefaultEc {
  implicit val ec: ExecutionContext = ExecutionContext.global
}

// More specific type wins
implicit def ord1: Ordering[Int] = ???
implicit def ord2: Ordering[Int] = ???  // ambiguous error!

// LowPriorityImplicits trait pattern
object MyLib {
  implicit val high: Ordering[Int] = ???
}
object MyLib extends LowPriorityImplicits
trait LowPriorityImplicits {
  implicit val low: Ordering[Int] = ???  // fallback
}

컨텍스트 바운드와 Evidence

컨텍스트 바운드 [A: TypeClass]는 implicit 매개변수의 syntactic sugar—implicit TypeClass[A]가 존재함을 단언. (Scala 2) implicitly[TypeClass[A]]나 (Scala 3) summon[TypeClass[A]]로 검색. 컨텍스트 바운드가 타입 클래스 제약을 읽기 쉽게: def sort[A: Ordering]. 여러 바운드 스택: [A: Ordering: Numeric]. 타입 클래스 요구사항을 표현하는 관용적 방법.

scala
// Context bound: [A: Ordering] means there's an implicit Ordering[A]
def max[A: Ordering](a: A, b: A): A = {
  val ord = implicitly[Ordering[A]]  // retrieve the implicit
  if (ord.gt(a, b)) a else b
}

// Equivalent to:
def max2[A](a: A, b: A)(implicit ord: Ordering[A]): A =
  if (ord.gt(a, b)) a else b

// summon (Scala 3) instead of implicitly
def max3[A: Ordering](a: A, b: A): A = {
  val ord = summon[Ordering[A]]
  if (ord.gt(a, b)) a else b
}

// Type class evidence
def sort[A: Ordering](list: List[A]): List[A] =
  list.sorted  // uses the implicit Ordering

// Multiple context bounds
def process[A: Ordering: Numeric](x: A, y: A): A = ???

Implicit 스코프와 Companion Object

Implicit 스코프는 현재 스코프보다 넓음—관련 타입의 companion object 포함. 이것이 Ordering[Int]를 import할 필요가 없는 이유: Int의 companion에 있음. 이 메커니즘이 타입 클래스를 인체공학적으로 만듦: 타입의 companion에 instance 정의, 자동으로 사용 가능. Package object가 패키지의 공유 implicit 보유. 이 설계가 'zero-import' 타입 클래스 사용 가능.

scala
// Implicit in companion object is found automatically
case class UserId(value: Long)
object UserId {
  implicit val ordering: Ordering[UserId] =
    Ordering.by(_.value)
}

// No import needed—companion object implicits are in scope
List(UserId(3), UserId(1), UserId(2)).sorted
// Works because Ordering[UserId] is in UserId's companion

// Implicit scope includes:
// 1. Companion object of the type (UserId)
// 2. Companion object of the type class (Ordering)
// 3. Companion objects of type parameters

// This is why Int has an Ordering:
// object Int { implicit val ord: Ordering[Int] = ... }

// Package object for shared implicits
package object myapp {
  implicit val ec: ExecutionContext = ExecutionContext.global
  type Id = Long
}
12

타입 클래스

타입 클래스 정의

타입 클래스는 타입으로 매개변수화된 trait이며, instance가 특정 타입에 대한 동작 제공. 상속과 달리 타입 클래스 instance를 소급적으로(소유하지 않은 타입에) 추가 가능. Show[A]가 A를 표시하는 방법 정의. instance는 companion object에 존재(자동 implicit 스코프). 이것이 ad-hoc 다형성—타입 수정 없이 타입별로 다른 동작. 타입 클래스가 Scala의 가장 강력한 추상화.

scala
// Type class: a trait parameterized by type
trait Show[A] {
  def show(a: A): String
}

// Instances for specific types
object Show {
  // Instance for Int
  implicit val showInt: Show[Int] = (a: Int) => a.toString

  // Instance for String
  implicit val showString: Show[String] = (s: String) => s""$s""

  // Instance for List (recursive)
  implicit def showList[A](implicit s: Show[A]): Show[List[A]] =
    (list: List[A]) => list.map(s.show).mkString("[", ", ", "]")
}

// Usage with implicit parameter
def print[A](a: A)(implicit s: Show[A]): Unit =
  println(s.show(a))

print(42)           // 42
print("hello")      // "hello"
print(List(1, 2, 3))  // [1, 2, 3]

타입 클래스 사용 (syntactic sugar)

컨텍스트 바운드 [A: Show] + summon이 타입 클래스 instance 검색. 확장 메서드(implicit class)가 .show 같은 타입 클래스 사용 메서드 추가. 이 조합이 깔끔한 API 제공: Show[Int]가 존재하면 42.show 작동. 표준 라이브러리가 많은 타입 클래스 제공: Numeric, Ordering, Eq, Monoid(Cats). syntax import(Numeric.Implicits._)가 타입 클래스 사용하는 +와 sum 같은 연산자 추가.

scala
// Context bound syntax
def printAll[A: Show](items: List[A]): Unit =
  items.foreach(a => println(summon[Show[A]].show(a)))

// Extension methods via implicit class
implicit class ShowOps[A](val a: A) extends AnyVal {
  def show(implicit s: Show[A]): String = s.show(a)
}

42.show           // "42"
"hi".show         // ""hi""
List(1,2).show    // "[1, 2]"

// Combining: type class + extension methods
def format[A: Show](a: A): String = a.show

// Standard library type classes
def sum[A: Numeric](xs: List[A]): A =
  summon[Numeric[A]].plus(xs.head, xs.tail.foldLeft(
    summon[Numeric[A]].zero)(summon[Numeric[A]].plus))

// Or with syntax:
import Numeric.Implicits._
def sum2[A: Numeric](xs: List[A]): A = xs.sum

흔한 타입 클래스 (Cats/Scalaz)

Cats와 Scalaz가 표준 타입 클래스 제공. Monoid(empty + combine)가 일반적 집계 가능. Functor(map)와 Monad(pure + flatMap)가 컨테이너(List, Option, Future, IO) 추상화. Eq가 타입 안전 동등성 제공(우발적 크로스 타입 비교 없음). 이들이 조합: Monad는 Functor, Monoid는 Semigroup. 타입 클래스가 여러 타입에 작동하는 일반적이고 재사용 가능한 코드 작성 가능.

scala
// Monoid: combine values with empty
trait Monoid[A] {
  def empty: A
  def combine(a: A, b: A): A
}
object Monoid {
  implicit val intAdd: Monoid[Int] = new Monoid[Int] {
    def empty = 0
    def combine(a: Int, b: Int) = a + b
  }
  implicit def listMonoid[A]: Monoid[List[A]] = new Monoid[List[A]] {
    def empty = Nil
    def combine(a: List[A], b: List[A]) = a ++ b
  }
}

// Functor: map over structure
trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

// Monad: chain operations
trait Monad[F[_]] {
  def pure[A](a: A): F[A]
  def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
}

// Eq: type-safe equality
trait Eq[A] {
  def eqv(a: A, b: A): Boolean
}

// Semigroup: combine (no empty)
trait Semigroup[A] {
  def combine(a: A, b: A): A
}

법칙과 타입 클래스 테스트

타입 클래스 법칙은 instance가 만족해야 하는 수학적 속성. Monoid는 결합법칙과 항등원 필요. Functor는 항등과 조합 보존 필요. Cats 같은 라이브러리가 법칙 정의 제공; discipline + ScalaCheck가 자동 테스트. 법칙이 타입 클래스가 강력한 이유: 일반적 코드(foldMap 같은)가 법칙 준수하는 모든 instance에 대해 올바르게 작동. 항상 instance가 법칙을 만족하는지 확인—instance의 버그가 이를 사용하는 모든 일반적 코드를 망가뜨림.

scala
// Type class laws: properties that must hold
// Monoid laws:
// 1. Left identity:  combine(empty, a) == a
// 2. Right identity: combine(a, empty) == a
// 3. Associativity:  combine(a, combine(b, c)) == combine(combine(a, b), c)

// Functor laws:
// 1. Identity:    map(fa)(identity) == fa
// 2. Composition: map(fa)(f andThen g) == map(map(fa)(f))(g)

// Testing laws with ScalaCheck (discipline)
import org.scalacheck.Prop.forAll
import cats.kernel.laws.MonoidLaws

class MonoidSpec extends munit.FunSuite with Discipline {
  checkAll("Int Monoid", MonoidLaws[Int].monoid)
}

// Custom law test
def monoidLeftIdentity[A](implicit m: Monoid[A], arb: Arbitrary[A]) =
  forAll { (a: A) =>
    m.combine(m.empty, a) == a
  }

// Laws make type classes trustworthy:
// if an instance satisfies laws, generic code works correctly

타입 클래스 도출 (Scala 3)

Scala 3이 'derives' 키워드와 Mirror로 타입 클래스 도출 단순화. 컴파일러가 요소 instance 조합으로 case class와 enum에 대한 instance auto-generate 가능. 이것이 모든 case class에 instance 작성 보일러플레이트 제거(Scala 2에서 shapeless로 흔함). Cats와 Circe 같은 라이브러리가 Scala 3 도출 지원. Mirror 타입이 일반 프로그래밍을 위해 타입의 구조(필드 타입, 레이블)에 컴파일 타임 접근 제공.

scala
// Scala 3: derive type class instances automatically
import scala.deriving.Mirror

trait Show[A] {
  def show(a: A): String
}

object Show {
  // Inline given for derivation
  given showInt: Show[Int] with
    def show(a: Int) = a.toString

  given showString: Show[String] with
    def show(s: String) = s""$s""

  // Derive for products (case classes)
  given showProduct[A](using m: Mirror.ProductOf[A])
      (using ev: Show[m.MirroredElemTypes]): Show[A] with
    def show(a: A): String = ???

  // Or use Scala 3's derivation
  inline given derive[A](using m: Mirror.Of[A]): Show[A] = ???
}

// Auto-derive for case classes
case class Person(name: String, age: Int) derives Show
// Show[Person] is generated automatically
13

For Comprehension 심화

기본 For Comprehension

For comprehension은 flatMap/map/withFilter의 syntactic sugar. 각 <-는 flatMap(마지막은 map). if 가드는 withFilter로. yield가 컬렉션 반환; yield 생략 시 명령형(foreach). flatMap/map를 가진 모든 타입(Monad)에서 작동: List, Option, Future, Try, IO. for-comprehension 숙달이 관용적 Scala의 핵심—중첩 map/flatMap을 읽기 쉬운 순차적 구문으로 대체.

scala
// For comprehension: syntactic sugar for flatMap/map
val result = for {
  x <- List(1, 2, 3)
  y <- List(10, 20)
} yield x + y
// List(11, 21, 12, 22, 13, 23)

// Desugared:
List(1, 2, 3).flatMap { x =>
  List(10, 20).map { y => x + y }
}

// With filters (if guards)
val evens = for {
  x <- 1 to 10
  if x % 2 == 0
} yield x
// Vector(2, 4, 6, 8, 10)

// Desugared:
(1 to 10).withFilter(_ % 2 == 0).map(identity)

// Without yield: imperative (foreach)
for (x <- 1 to 3) println(x)  // 1, 2, 3

Option과 Future로 For

For-comprehension이 모든 Monad에서 작동. Option에서 None에 short-circuit(None 반환). Future에서 실패에 short-circuit. 순차적 비동기/실패 가능 코드가 함수형을 유지하면서 직선 명령형 코드처럼 읽힘. 각 <- 라인이 이전 바인딩에 의존 가능. 중첩 flatMap 호출보다 훨씬 깔끔. 같은 구문이 Try, Either, IO, 커스텀 모나드에 작동.

scala
// Option: chain operations that might return None
def getUser(id: Int): Option[User] = ???
def getEmail(user: User): Option[String] = ???

val email: Option[String] = for {
  user <- getUser(42)
  email <- getEmail(user)
} yield email

// Desugared:
getUser(42).flatMap(user => getEmail(user).map(email => email))

// Future: chain async operations
val result: Future[Int] = for {
  user <- fetchUser(1)      // Future[User]
  posts <- fetchPosts(user) // Future[List[Post]]
} yield posts.size

// If any returns None/failed Future, the whole chain short-circuits
// This is the power of monadic composition

Either와 오류 처리로 For

Either가 Scala의 타입 지정 오류 처리. For-comprehension이 Either를 체인, Left(오류)에 short-circuit. 함수형 오류 처리—예외 없이 오류가 값. Left 타입이 오류(보통 String이나 sealed trait). Either는 Scala 2.12+에서 right-biased(map/flatMap이 Right에서 작동). 이 패턴이 try/catch를 조합 가능하고 타입 안전한 오류 전파로 대체. Cats의 Validated가 오류 누적 대안.

scala
// Either for error handling
def parseAge(s: String): Either[String, Int] =
  s.toIntOption.toRight(s"not a number: $s")

def validateAge(age: Int): Either[String, Int] =
  if (age >= 0) Right(age) else Left(s"negative: $age")

val result: Either[String, Int] = for {
  age <- parseAge("30")     // Right(30)
  valid <- validateAge(age) // Right(30)
} yield valid

// With error in chain
val error: Either[String, Int] = for {
  age <- parseAge("abc")    // Left("not a number: abc")
  valid <- validateAge(age) // skipped!
} yield valid
// error == Left("not a number: abc")

// Scala 3: for-comprehensions work with Either directly
// (Scala 2 needed either.map(_.right) or withFilter)

Desugaring과 커스텀 모나드

flatMap과 map을 가진 모든 타입이 for-comprehension 지원—이것이 Monad 패턴. for-syntax를 활성화하기 위해 타입에 이 메서드들을 정의. = (not <-)이 로컬 바인딩 생성(flatMap으로 desugar 안 됨). desugaring 이해가 복잡한 comprehension 디버그와 커스텀 모나드 구현에 도움. 컴파일러가 for-comprehension을 flatMap/map/withFilter 체인으로 변환. 이것이 for가 List, Option, Future, IO 등에서 균일하게 작동하는 이유.

scala
// For-comprehensions require flatMap, map, withFilter
// Define your own monad to use for-comprehensions

case class Box[A](value: A) {
  def map[B](f: A => B): Box[B] = Box(f(value))
  def flatMap[B](f: A => Box[B]): Box[B] = f(value)
  def withFilter(p: A => Boolean): Box[A] =
    if (p(value)) this else throw new NoSuchElementException
}

val result = for {
  x <- Box(10)
  y <- Box(20)
  if x < y
} yield x + y
// Box(30)

// Desugaring steps:
// 1. Box(10).flatMap { x =>
// 2.   Box(20).withFilter(_ > x... wait, order matters
// Actual:
// Box(10).flatMap(x =>
//   Box(20).withFilter(y => x < y).map(y => x + y))

// Assignment within for (= instead of <-)
for {
  x <- List(1, 2, 3)
  doubled = x * 2  // local val
} yield doubled

For vs Map/FlatMap (언제 사용)

2개 이상의 의존적 작업에는 for-comprehension 사용—중첩 flatMap보다 읽기 쉬움. 단일 변환에는 map가 더 명확. 평탄화에는 flatMap 직접. 부작용(결과 없음)에는 yield 없는 for 사용. for-comprehension이 각 단계가 이전에 의존할 때 빛남(모나딕 체인). 비동기/오류 처리 코드를 순차적으로 읽히게. 깊게 중첩된 for(>5 레벨)는 피하세요—가독성을 위해 헬퍼 추출.

scala
// For: sequential, dependent operations
val result = for {
  user <- fetchUser(id)
  profile <- fetchProfile(user.id)
  avatar <- fetchAvatar(profile.avatarId)
} yield avatar

// Equivalent with flatMap (harder to read):
fetchUser(id).flatMap(user =>
  fetchProfile(user.id).flatMap(profile =>
    fetchAvatar(profile.avatarId)))

// Map: single transformation (no chaining)
users.map(_.name)  // simple, use map

// FlatMap: when you need to chain but for is overkill
users.flatMap(_.posts)  // List[Post]

// For without yield: side effects
for (user <- users) {
  saveToDb(user)
  sendEmail(user)
}

// Guidelines:
// - 1 operation: use map/flatMap directly
// - 2+ dependent operations: use for-comprehension
// - Side effects: use for without yield
14

Scala 3: Given & Using

Given Instance (implicit val 대체)

Scala 3이 implicit val/def를 'given'으로 대체. given이 더 명확하고 명시적. 익명 given(given Type = ...)은 컴파일러 생성 이름. 'given T with'가 여러 메서드를 가진 instance 정의. 조건부 given(given [A: Ordering]: Ordering[List[A]])이 implicit def 대체. companion object의 given은 Scala 2와 마찬가지로 자동으로 implicit 스코프에. 키워드 변경이 'implicit' 과부하(Scala 2에서 4가지 의미) 감소.

scala
// Scala 2: implicit val
// implicit val ec: ExecutionContext = ExecutionContext.global

// Scala 3: given
given ec: ExecutionContext = ExecutionContext.global

// Anonymous given (inferred name)
given ExecutionContext = ExecutionContext.global

// Given with 'with' for complex types
given Show[Int] with
  def show(a: Int): String = a.toString

// Given in companion object (automatic scope)
object MyType:
  given Ordering[MyType] = Ordering.by(_.id)

// Conditional given (like implicit def)
given [A: Ordering]: Ordering[List[A]] with
  def compare(a: List[A], b: List[A]): Int =
    a.zip(b).find((x, y) => x != y) match
      case Some((x, y)) => summon[Ordering[A]].compare(x, y)
      case None => a.length - b.length

Using 절 (implicit 매개변수 대체)

Scala 3이 implicit 매개변수를 'using'으로 대체. 'implicit'의 두 의미(매개변수 vs 변환) 분리. using 절은 'using' 키워드로 명시적으로 전달. 컨텍스트 바운드 [A: T]는 동일. summon[T]가 implicitly[T] 대체(더 명확한 이름). using 키워드가 컨텍스트 제공 시 호출 사이트에서 명확. 순수 구문 변경—의미론은 implicit 매개변수와 동일.

scala
// Scala 2: implicit parameter
// def process[A](data: List[A])(implicit ec: ExecutionContext): Unit

// Scala 3: using
def process[A](data: List[A])(using ec: ExecutionContext): Unit =
  data.foreach(println)

// Context bound (unchanged)
def sort[A: Ordering](list: List[A]): List[A] = list.sorted

// Multiple using clauses
def log(msg: String)(using level: Level, logger: Logger): Unit =
  logger.log(level, msg)

// summon replaces implicitly
def max[A: Ordering](a: A, b: A): A =
  val ord = summon[Ordering[A]]
  if ord.gt(a, b) then a else b

// Provide explicitly with 'using'
process(data)(using myEC)
log("hello")(using Level.INFO, myLogger)

확장 메서드 (Scala 3)

Scala 3이 implicit class를 'extension'으로 대체—더 명확하고 집중. extension (self: T)가 T에 메서드 정의. 확장은 제네릭 가능(extension [A]). using과 결합하여 타입 클래스 구문 제공(42.show). 확장은 단순 메서드 추가—래퍼 객체 생성 안 함(Scala 2에서 AnyVal로 제로 오버헤드, Scala 3에서 네이티브). Scala 3에서 기존 타입에 메서드를 추가하는 관용적 방법.

scala
// Scala 2: implicit class
// implicit class RichInt(val self: Int) extends AnyVal {
//   def squared: Int = self * self
// }

// Scala 3: extension
extension (self: Int)
  def squared: Int = self * self
  def times(f: => Unit): Unit = (1 to self).foreach(_ => f)

5.squared   // 25
3.times { println("hi") }

// Extension on generic type
extension [A](self: List[A])
  def takeWhileInclusive(p: A => Boolean): List[A] = ???

// Extension with using (type class syntax)
extension [A](self: A) def show(using s: Show[A]): String = s.show(self)

42.show  // uses given Show[Int]

// Multiple extensions in one block
extension (s: String)
  def isBlank: Boolean = s.trim.isEmpty
  def words: List[String] = s.split(" ").toList

Enum과 ADT (Scala 3)

Scala 3 enum이 ADT용 sealed trait + case object 대체. 더 간결하고 매개변수, 필드, 메서드 지원. Enum case가 매개변수 가질 수 있음(case class처럼). 패턴 매칭은 완전성 확인. Enum은 제네릭 가능(Option[A]). enum과 ADT를 하나의 구조로 통합. 개방 계층(확장 가능)에는 여전히 sealed trait + case class 사용. 닫힌 열거형/ADT에는 enum이 더 깔끔.

scala
// Scala 3 enum (replaces sealed trait + case objects)
enum Color:
  case Red, Green, Blue

enum HttpStatus(val code: Int):
  case Ok extends HttpStatus(200)
  case NotFound extends HttpStatus(404)
  case Error extends HttpStatus(500)

// Pattern matching (exhaustive)
def describe(c: Color): String = c match
  case Color.Red => "red"
  case Color.Green => "green"
  case Color.Blue => "blue"

// Access fields
HttpStatus.Ok.code  // 200

// Parameterized enum cases
enum Option[+A]:
  case Some(value: A)
  case None

// ADT with methods
enum Tree[+A]:
  case Leaf(value: A)
  case Node(left: Tree[A], right: Tree[A])

  def size: Int = this match
    case Leaf(_) => 1
    case Node(l, r) => l.size + r.size + 1

최상위 정의와 들여쓰기

Scala 3이 최상위 정의 허용—모든 것을 object로 감쌀 필요 없음. 파일 구조 단순화(Python/Go처럼). 새 구문은 중괄호와 의미 있는 들여쓰기(선택) 모두 지원. if/then이 괄호와 함께 if/else 대체. match가 중괄호 없이 표현식으로 사용 가능. 이 변경이 Scala 3을 더 접근하기 쉽게 하면서 하위 호환성 유지. 스타일 혼합 가능—명확성이 필요한 곳에 중괄호, 노이즈 감소에 들여쓰기 사용.

scala
// Scala 3: top-level definitions (no class wrapper needed)
// File: MyMath.scala
def add(a: Int, b: Int): Int = a + b  // top-level function

val Pi: Double = 3.14159  // top-level val

type Id = Long  // top-level type alias

given Show[Int] = (a: Int) => a.toString  // top-level given

extension (i: Int) def squared: Int = i * i  // top-level extension

// Indentation-based syntax (optional, braces still work)
def factorial(n: Int): Int =
  if n <= 1 then 1
  else n * factorial(n - 1)

// Or with braces:
def factorial2(n: Int): Int = {
  if (n <= 1) 1
  else n * factorial2(n - 1)
}

// if/then, match/case without braces
val sign = if x > 0 then 1 else -1
val desc = x match
  case 0 => "zero"
  case _ => "nonzero"
15

확장 메서드 & 구문

Implicit Class (Scala 2)

implicit class(Scala 2.10+)가 기존 타입에 확장 메서드 추가. AnyVal 확장 시 제로 할당(컴파일러가 래퍼 지움). 클래스는 단일 생성자 매개변수(확장되는 타입)를 받아야 함. implicit class의 메서드가 확장된 타입에서 사용 가능. 이것이 Scala가 Int, String 등을 풍부하게 하는 방법. 공유를 위해 implicit class를 package object나 utility object에 넣으세요. Scala 3에서는 'extension'을 대신 사용.

scala
// Scala 2: implicit class for extension methods
import scala.language.implicitConversions

implicit class RichString(val s: String) extends AnyVal {
  def wordCount: Int = s.split("\\s+").length
  def slug: String = s.toLowerCase.replaceAll("[^a-z0-9]+", "-")
  def encrypt(key: Int): String = s.map(c => (c + key).toChar)
}

"Hello World".wordCount  // 2
"My Blog Post!".slug     // "my-blog-post-"
"abc".encrypt(1)         // "bcd"

// AnyVal avoids allocation (zero overhead)
// implicit class must be in a trait, class, or object

// For generic extensions
implicit class RichList[A](val list: List[A]) extends AnyVal {
  def middle: Option[A] = list.lift(list.length / 2)
}

List(1, 2, 3, 4, 5).middle  // Some(3)

실전 확장 메서드

확장 메서드가 컬렉션과 다른 타입에 유틸리티를 추가하는 표준 방법. distinctBy, chunked, tap이 흔한 추가. ===와 =!= 연산자(Eq 타입 클래스 통해)가 타입 안전 동등성 제공(크로스 타입을 허용하는 ==와 달리). 확장을 object에 그룹화하고 필요한 곳에 import. 핵심 타입을 깔끔하게 유지하면서 도메인별 메서드 허용. Scala의 컬렉션 라이브러리 자체가 이 패턴을 광범위하게 사용.

scala
// Common pattern: add utility methods to collections
object CollectionExtensions {
  implicit class RichSeq[A](val seq: Seq[A]) extends AnyVal {
    def chunked(size: Int): Seq[Seq[A]] =
      seq.grouped(size).toSeq

    def distinctBy[B](f: A => B): Seq[A] =
      seq.groupBy(f).values.map(_.head).toSeq

    def tap(f: A => Unit): Seq[A] = {
      seq.foreach(f)
      seq
    }
  }
}

import CollectionExtensions._

List(1, 2, 3, 4, 5).chunked(2)  // List(List(1,2), List(3,4), List(5))
List("aa", "bb", "ab").distinctBy(_.head)  // List("aa", "ab")
List(1, 2, 3).tap(println)  // prints 1,2,3, returns List(1,2,3)

// Type class syntax via extension
implicit class EqOps[A](val a: A) extends AnyVal {
  def ===(b: A)(implicit eq: Eq[A]): Boolean = eq.eqv(a, b)
  def =!=(b: A)(implicit eq: Eq[A]): Boolean = !eq.eqv(a, b)
}

Implicit 변환 (신중하게 사용)

Implicit 변환이 타입을 자동으로 변환하지만 위험합니다—코드 동작이 명확하지 않게 됨. Scala 2.10+는 명시적 옵트인 필요(scala.language.implicitConversions). 변환(타입 변경)보다 확장 메서드(타입 변경 없이 메서드 추가)를 선호. 합법적 사용: Java interop(컬렉션 타입 간 변환), DSL 구성. 컴파일러가 implicit 변환에 대해 경고—이 경고를 심각하게 다루세요. Scala 3에서 given Conversion[T, U]가 명시적 메커니즘.

scala
import scala.language.implicitConversions

// Implicit conversion between types
implicit def stringToInt(s: String): Int = s.toInt
val n: Int = "42"  // stringToInt("42")

// Dangerous: can cause surprising behavior
implicit def intToBoolean(n: Int): Boolean = n != 0
if (1) println("yes")  // works! (intToBoolean(1))

// Safer: use extension methods instead
extension (n: Int) def toBool: Boolean = n != 0
if (1.toBool) println("yes")

// When implicit conversions are appropriate:
// 1. Java interop (java.util.List <-> scala.List)
implicit def javaListToScala[A](jl: java.util.List[A]): List[A] =
  import scala.jdk.CollectionConverters._
  jl.asScala.toList

// 2. Backward compatibility layers
// 3. DSL construction (use sparingly)

// Enable per-file: import scala.language.implicitConversions

타입 수준 프로그래밍

타입 수준 프로그래밍이 컴파일러가 확인하는 타입에 정보를 인코딩. Phantom 타입(사용되지 않는 타입 매개변수)이 상태(Open/Closed)를 추적하여 오용 방지(닫힌 파일 읽기 불가). Peano 자연수가 숫자를 타입으로 표현. 컴파일 타임 정확성 보장 가능—버그가 컴파일 오류가 됨. 상태 기계, 측정 단위, 크기 지정 벡터에 사용. 강력하지만 복잡—안전성이 타입 복잡성의 가치가 있을 때 사용. shapeless(Scala 2) 같은 라이브러리가 고급 타입 수준 프로그래밍 가능.

scala
// Phantom types: track state at type level
sealed trait State
trait Open extends State
trait Closed extends State

class File[S <: State] private (val path: String)

object File {
  def open(path: String): File[Open] = new File[Open](path)
}

def close[S <: Open](f: File[S]): File[Closed] =
  new File[Closed](f.path)  // unsafe cast internally

def read[S <: Open](f: File[S]): String = "data"
// def read[S <: Closed](f: File[S]): String  // compile error

val f = File.open("test.txt")
read(f)  // OK: f is File[Open]
val closed = close(f)
// read(closed)  // COMPILE ERROR: closed is File[Closed]

// Type-level natural numbers (Peano)
sealed trait Nat
trait Zero extends Nat
trait Succ[N <: Nat] extends Nat

// Compile-time list length check
type _0 = Zero
type _1 = Succ[_0]
type _2 = Succ[_1]

Opaque 타입 (Scala 3)

Opaque 타입(Scala 3)이 제로 오버헤드 newtype 생성—value class(AnyVal)와 달리 박싱하지 않고 런타임에 진정으로 기저 타입. 타입 구분은 컴파일 타임에만 존재, 혼란 방지(Celsius vs Fahrenheit, UserId vs Long). 정의하는 object 내부에서는 타입과 기저 타입이 상호 교환 가능; 외부에서는 별개. Haskell의 'newtype' 패턴—런타임 비용 없는 도메인 주도 타입. ID, 단위, 도메인 프리미티브에 사용.

scala
// Scala 2: value classes for zero-overhead wrappers
// case class UserId(value: Long) extends AnyVal

// Scala 3: opaque types (true zero overhead, no boxing)
object Types:
  type UserId = Long
  object UserId:
    def apply(value: Long): UserId = value

  type Email = String
  object Email:
    def apply(s: String): Email =
      require(s.contains("@"), "invalid email")
      s

import Types.*
val id: UserId = UserId(42)  // just a Long at runtime
val email: Email = Email("[email protected]")

// UserId and Long are NOT interchangeable outside the object
// def wrong(x: Long): UserId = x  // ERROR
// But inside the object, they're the same

// Newtype pattern: domain types without overhead
type Celsius = Double
type Fahrenheit = Double
// Prevents mixing up Celsius and Fahrenheit
def toF(c: Celsius): Fahrenheit = c * 9 / 5 + 32
16

테스트 (ScalaTest & ScalaCheck)

ScalaTest 스타일

ScalaTest가 여러 스타일 제공. FunSuite가 가장 단순(test("name") { ... }). FlatSpec은 BDD 스타일("A Stack" should "..."). Matchers가 읽기 쉬운 어설션 제공(shouldBe, should contain, should throw). 일관성을 위해 프로젝트당 하나의 스타일 선택. 단위 테스트에는 FunSuite가 인기; 동작 중심 테스트에는 FlatSpec. 모든 스타일이 같은 matcher와 수명 주기 훅 지원. 스타일은 구문에 영향, 능력에는 영향 없음.

scala
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers

// FunSuite: simple test functions
class MyTest extends AnyFunSuite with Matchers {
  test("addition works") {
    1 + 1 should be (2)
    1 + 1 shouldBe 2
    List(1, 2, 3) should contain (2)
  }

  test("string operations") {
    "hello".length shouldBe 5
    "hello" should startWith ("he")
  }
}

// FlatSpec: BDD-style
import org.scalatest.flatspec.AnyFlatSpec
class StackSpec extends AnyFlatSpec with Matchers {
  "A Stack" should "pop values in LIFO order" in {
    val stack = Stack(1, 2, 3)
    stack.pop() shouldBe 3
  }

  it should "throw on empty pop" in {
    val stack = Stack()
    a [NoSuchElementException] shouldBe thrownBy(stack.pop())
  }
}

// WordSpec, FreeSpec, PropSpec also available

어설션과 Matcher

Matcher가 표현력 있는 어설션 제공. 동등성에는 shouldBe/should be. 컬렉션에는 contain, have size, have key. 문자열에는 startWith/endWith/include. 예외에는 thrownBy. 비교용 커스텀 matcher(be >, be <=). 'should' DSL이 영어처럼 읽어 테스트를 자체 문서화. 복잡한 어설션에는 커스텀 matcher나 단순 assert(condition) 사용. matcher 체이닝 과용 피하기—영리함보다 가독성.

scala
import org.scalatest.matchers.should.Matchers._

// Equality
result shouldBe 42
result should be (42)
result should equal (42)

// Collections
list should contain (5)
list should not contain (0)
list shouldBe empty
list should have size 5
list should contain inOrder (1, 2, 3)
map should contain key ("name")
map should contain value ("Alice")

// Strings
s should startWith ("Hello")
s should endWith ("world")
s should include ("lo")
s should fullyMatch regex "H.*d".r

// Exceptions
a [IOException] should be thrownBy riskyOp()
the [IOException] thrownBy riskyOp() should have message "fail"

// Custom matchers
result should be > 0
result should be <= 100

// Type checks
result shouldBe a [List[_]]
result should be an [IllegalArgumentException]

BeforeAndAfter와 Fixture

BeforeAndAfterEach가 각 테스트 주위에 setup/teardown 실행. beforeEach/afterEach 재정의. 리소스 관리를 위해 loan pattern(withDb { conn => ... })이 더 깔끔—try/finally로 정리 보장하고 리소스를 명시적으로. ScalaTest는 fixture 컨텍스트(FixtureContext)와 trait를 통한 공유 fixture도 지원. 가변 beforeEach 상태보다 loan pattern이나 fixture 메서드를 선호—더 함수형이고 테스트 간 공유 상태 버그 방지.

scala
import org.scalatest.BeforeAndAfterEach
import org.scalatest.funsuite.AnyFunSuite

class DbTest extends AnyFunSuite with BeforeAndAfterEach {
  var conn: Connection = _

  override def beforeEach(): Unit = {
    conn = DriverManager.getConnection("jdbc:h2:mem:test")
    conn.execute("CREATE TABLE users (id INT, name VARCHAR)")
  }

  override def afterEach(): Unit = {
    conn.close()
  }

  test("insert works") {
    conn.execute("INSERT INTO users VALUES (1, 'Alice')")
    val count = conn.query("SELECT COUNT(*) FROM users")
    count shouldBe 1
  }
}

// Fixture via loan pattern
class FixtureTest extends AnyFunSuite {
  def withDb(test: Connection => Unit): Unit = {
    val conn = DriverManager.getConnection("jdbc:h2:mem:test")
    try test(conn) finally conn.close()
  }

  test("query works") {
    withDb { conn =>
      conn.execute("INSERT ...")
      // assertions
    }
  }
}

속성 기반 테스트 (ScalaCheck)

ScalaCheck가 임의의 테스트 입력을 생성하여 여러 케이스에 대해 속성 테스트. forAll이 기본적으로 100개의 임의 입력으로 속성 실행. whenever가 입력 필터. 커스텀 Gen 타입이 생성 데이터 제한(Gen.choose, Gen.nonEmptyListOf). 예제 기반 테스트로 놓치는 엣지 케이스(빈 리스트, 음수, 큰 값)를 잡음. Table 기반 테스트(Table)는 특정 케이스용. 속성 기반 테스트는 순수 함수와 데이터 변환에 강력—항상 참이어야 할 것을 정의.

scala
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
import org.scalacheck.Prop.forAll

class ListSpec extends AnyFunSuite with ScalaCheckPropertyChecks {
  // Property: reversing twice = identity
  test("reverse twice is identity") {
    forAll { (xs: List[Int]) =>
      xs.reverse.reverse shouldBe xs
    }
  }

  // Property with conditions
  test("head of sorted list is min") {
    forAll { (xs: List[Int]) =>
      whenever(xs.nonEmpty) {
        xs.sorted.head shouldBe xs.min
      }
    }
  }

  // Custom generators
  import org.scalacheck.Gen
  val smallInt = Gen.choose(1, 100)
  val nonEmptyList = Gen.nonEmptyListOf(smallInt)

  test("custom generator") {
    forAll(nonEmptyList) { (xs: List[Int]) =>
      xs should not be empty
      xs.forall(_ >= 1) shouldBe true
    }
  }

  // Table-driven tests
  test("addition table") {
    val cases = Table(
      ("a", "b", "sum"),
      (1, 2, 3),
      (10, 20, 30),
      (-1, 1, 0)
    )
    forAll(cases) { (a, b, sum) =>
      a + b shouldBe sum
    }
  }
}

목킹과 테스트 더블

Mockito(ScalaTestPlus 통해)가 테스트 더블 생성. mock[T]가 목 생성; when(...).thenReturn(...)이 메서드 스텁; verify가 상호작용 확인. 목으로 테스트 대상을 의존성(데이터베이스, API)에서 격리. 인자 matcher(argThat)가 특정 인자 확인. 과도한 목킹 피하기—모든 것을 목킹 중이라면 실제 통합을 테스트하세요. 복잡한 의존성에는 목보다 fake(인메모리 구현)를 선호—더 견고하고 읽기 쉬움.

scala
import org.scalatest.funsuite.AnyFunSuite
import org.scalatestplus.mockito.MockitoSugar
import org.mockito.Mockito._

class UserServiceSpec extends AnyFunSuite with MockitoSugar {
  test("getUser returns user from repo") {
    // Create mock
    val repo = mock[UserRepository]
    val user = User(1, "Alice")

    // Stub: when X then Y
    when(repo.findById(1)).thenReturn(Some(user))

    val service = new UserService(repo)
    val result = service.getUser(1)

    result shouldBe Some(user)

    // Verify: was X called?
    verify(repo).findById(1)
    verify(repo, never()).findById(2)
  }

  test("createUser saves to repo") {
    val repo = mock[UserRepository]
    val service = new UserService(repo)

    service.createUser("Bob")

    // Verify with argument matcher
    verify(repo).save(argThat((u: User) => u.name == "Bob"))
  }
}

// Stubbing exceptions
when(repo.findById(99)).thenThrow(new RuntimeException("not found"))
17

고차 함수 & FP

값으로서의 함수

Scala에서 함수는 값입니다—저장, 전달, 반환 가능. (A, B) => C가 함수 타입. Eta-expansion(multiply _)이 메서드를 함수 값으로 변환. 고차 함수(함수 받거나 반환)가 강력한 추상화 가능: map, filter, reduce는 HOF. 함수 조합(andThen, compose)이 파이프라인 빌드. 커링(adder(5)가 함수 반환) 부분 적용. Scala 함수형 프로그래밍의 기초.

scala
// Functions are first-class values
val add: (Int, Int) => Int = (a, b) => a + b
val square: Int => Int = x => x * x

// Apply
add(2, 3)  // 5
square(4)  // 16

// Method to function (eta-expansion)
def multiply(a: Int, b: Int): Int = a * b
val mul = multiply _  // or just 'multiply' in Scala 3
mul(3, 4)  // 12

// Higher-order: function taking function
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(square, 2)  // 16

// Higher-order: function returning function
def adder(n: Int): Int => Int = _ + n
val add5 = adder(5)
add5(10)  // 15

// Function composition
val f: Int => Int = _ + 1
val g: Int => Int = _ * 2
val h = f andThen g  // f then g: (x+1)*2
val h2 = f compose g  // g then f: (x*2)+1
h(3)  // 8
h2(3)  // 7

커링과 부분 적용

커링이 함수를 여러 매개변수 리스트로 분할하여 부분 적용 가능(일부 인자 고정, 나머지를 위한 함수 획득). 타입 추론에 도움(이전 매개변수가 이후 것 제한)과 구성 가능한 함수(withDb(config)) 생성. 자리 표시자 _가 부분 적용: sum(1, _)이 함수 생성. 여러 매개변수 리스트가 Scala의 커링 메커니즘. 일부 인자가 '구성'이고 다른 것이 '데이터'일 때 커링 사용.

scala
// Curried function: multiple parameter lists
def add(a: Int)(b: Int): Int = a + b
val add5: Int => Int = add(5)  // partial application
add5(10)  // 15

// Multiple parameter lists for type inference
def map[A, B](list: List[A])(f: A => B): List[B] =
  list.map(f)
map(List(1, 2, 3))(x => x * 2)  // A, B inferred from first list

// Curried form (Function types)
val curriedAdd: Int => Int => Int = a => b => a + b
curriedAdd(5)(10)  // 15

// Uncurrying
def uncurriedAdd(a: Int, b: Int): Int = a + b

// Practical: configuration via currying
def withDb(config: DbConfig)(f: Connection => Result): Result = ???
val withMyDb = withDb(myConfig) _
withMyDb { conn => /* ... */ }

// Partial application with placeholder
val sum: (Int, Int) => Int = _ + _
val addOne: Int => Int = sum(1, _)

순수 함수와 참조 투명성

순수 함수는 같은 입력에 항상 같은 출력을 반환하고 부작용이 없습니다. 테스트, 추론, 병렬화, 조합하기 쉽습니다. 참조 투명성은 함수 호출을 결과로 대체해도 동작이 변경되지 않음을 의미. Scala는 순수성을 강제하지 않지만, Cats Effect(IO 모나드) 같은 라이브러리로 부작용을 격리 가능. 순수 코어 + 엣지에서 IO가 흔한 FP 아키텍처: 비즈니스 로직은 순수, I/O는 IO로 감쌈.

scala
// Pure function: same input → same output, no side effects
def pureAdd(a: Int, b: Int): Int = a + b  // pure

// Impure: depends on external state
var counter = 0
def impureAdd(a: Int): Int = { counter += 1; a + counter }  // impure

// Impure: side effect
def impurePrint(a: Int): Int = { println(a); a }  // side effect

// Referential transparency: can replace call with result
val x = pureAdd(2, 3)
val y = x + x  // same as pureAdd(2, 3) + pureAdd(2, 3)

// Benefits of purity:
// 1. Easy to test (no setup/teardown)
// 2. Easy to reason about (no hidden state)
// 3. Parallelizable (no shared state)
// 4. Memoizable (cache results)
// 5. Composable (predictable)

// IO monad for side effects (Cats Effect)
import cats.effect.IO
val program: IO[Unit] = IO.println("hello")
val mapped: IO[String] = IO.pure("world").map(_.toUpperCase)
// Side effects captured in IO, run at edge of program

불변 데이터와 영속적 컬렉션

불변 데이터 구조는 수정 시 새 복사본을 반환, 효율성을 위해 내부적으로 구조 공유(영속적 데이터 구조). Scala의 컬렉션은 기본적으로 불변. case class copy가 수정된 복사본 생성. 깊은 업데이트를 위해 Lens 라이브러리(Monocle)가 조합 가능한 접근자 제공. 불변성이 전체 클래스의 버그(경쟁 조건, 예상치 못한 변경)를 제거하고 코드를 추론하기 쉽게. 성능 비용은 구조 공유로 인해 종종 허용 가능(O(n)이 아닌 O(log n)).

scala
// Immutable: operations return new collections
val list1 = List(1, 2, 3)
val list2 = list1 :+ 4  // List(1, 2, 3, 4)
// list1 is unchanged: List(1, 2, 3)

// Persistent data structures: share structure (efficient)
val map1 = Map("a" -> 1, "b" -> 2)
val map2 = map1 + ("c" -> 3)  // shares structure with map1
// O(log n) due to structural sharing, not O(n) copy

// Case classes: copy for modification
case class User(name: String, age: Int)
val alice = User("Alice", 30)
val older = alice.copy(age = 31)  // new instance, alice unchanged

// Lens (Monocle library) for nested updates
import monocle.macros.GenLens
val ageLens = GenLens[User](_.age)
val updated = ageLens.modify(_ + 1)(alice)  // User("Alice", 31)

// Benefits:
// - No bugs from shared mutation
// - Easy to reason about (values don't change)
// - Free concurrency (no locks needed)
// - Undo/redo trivial (keep old versions)

재귀와 꼬리 호출

꼬리 재귀(재귀 호출이 마지막 작업)는 Scala 컴파일러에 의해 루프로 최적화—스택 성장 없음. @tailrec가 컴파일 타임에 확인. 누산기 패턴(누적 결과 전달)이 non-tail 재귀를 꼬리 재귀로 변환. foldLeft/foldRight가 흔한 재귀 패턴 캡슐화. 깊은 non-tail 재귀에는 트램폴린(Cats)이나 fold로 재작성. 명확성을 위해 명시적 재귀보다 fold를 선호—꼬리 재귀이고 관용적.

scala
// Regular recursion (can stack overflow)
def factorial(n: Int): Int =
  if (n <= 1) 1 else n * factorial(n - 1)
factorial(10000)  // StackOverflowError!

// Tail recursion: compiler optimizes to loop
import scala.annotation.tailrec
@tailrec
def factorialTail(n: Int, acc: Int = 1): Int =
  if (n <= 1) acc else factorialTail(n - 1, n * acc)
factorialTail(10000)  // works (no stack overflow)

// @tailrec annotation: compiler verifies it's tail-recursive
// Error if not actually tail-recursive

// Trampoline for non-tail recursion
// (Cats' Trampoline or Free monads)
// Converts stack recursion to heap

// Fold as alternative to recursion
def sum(list: List[Int]): Int = list.foldLeft(0)(_ + _)
// foldLeft is tail-recursive internally

// Pattern: accumulator pattern
@tailrec
def reverse[A](list: List[A], acc: List[A] = Nil): List[A] =
  list match
    case Nil => acc
    case head :: tail => reverse(tail, head :: acc)
18

Case Class & ADT

Case Class 기초

Case class는 Scala의 주요 데이터 모델링 도구. 컴파일러가 equals, hashCode, toString, copy, apply, unapply(패턴 매칭용), 접근자 메서드 생성. 기본적으로 불변(val 필드). DTO, 값 객체, 메시지, 도메인 엔티티(불변인 경우)에 사용. companion object의 apply로 'new' 없이 생성. copy가 비파괴적 업데이트 가능. Case class가 ADT(Algebraic Data Type)의 기초.

scala
// Case class: immutable data holder with boilerplate generated
case class Person(name: String, age: Int)

val alice = Person("Alice", 30)
val bob = Person("Bob", 25)

// Auto-generated methods:
alice.toString  // "Person(Alice,30)"
alice == Person("Alice", 30)  // true (structural equality)
alice.hashCode  // based on fields

// Pattern matching
alice match
  case Person(name, age) => s"$name is $age"

// Copy with modifications
val older = alice.copy(age = 31)  // Person("Alice", 31)

// Companion object with apply (no 'new' needed)
val p = Person("Charlie", 40)  // Person.apply called

// Fields accessed directly
alice.name  // "Alice"
alice.age   // 30

// Case class with default values
case class Point(x: Double = 0, y: Double = 0)
Point()  // Point(0.0, 0.0)
Point(y = 5)  // Point(0.0, 5.0)

Sealed Trait과 ADT

ADT(Algebraic Data Type)가 데이터를 닫힌 case 집합으로 모델링(sealed trait + case class/object). 'sealed'는 모든 하위 타입이 같은 파일에 있음을 의미, 완전한 패턴 매칭 가능—case 누락 시 컴파일러 경고. 새 case 추가 시 업데이트 필요한 모든 곳 표시(안전한 리팩토링). Case object는 싱글톤(매개변수 없음). Option, List, Either가 모두 ADT. 도메인을 함수적으로 모델링하는 방법—각 변형이 case, 동작은 패턴 매칭에.

scala
// ADT: sealed trait + case classes/objects
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(a: Double, b: Double, c: Double) extends Shape

// Exhaustive pattern matching (compiler checks all cases)
def area(shape: Shape): Double = shape match
  case Circle(r) => math.Pi * r * r
  case Rectangle(w, h) => w * h
  case Triangle(a, b, c) =>
    val s = (a + b + c) / 2
    math.sqrt(s * (s-a) * (s-b) * (s-c))

// Adding a new case: compiler warns about non-exhaustive matches
case class Square(side: Double) extends Shape
// area now needs a Square case!

// Case objects for singletons (no parameters)
sealed trait Status
case object Active extends Status
case object Inactive extends Status
case object Pending extends Status

// Option and List are ADTs:
// sealed trait Option[+A]
// case class Some[A](value: A) extends Option[A]
// case object None extends Option[Nothing]

Case Class 기능

Case class는 메서드, 커스텀 apply(검증용), trait 구현, 제네릭 가능. companion object의 커스텀 apply는 생성 전 검증 가능(불법 상태를 표현 불가능하게). Case class는 다형성을 위해 trait 구현 가능. private 생성자(case class Email private)는 검증을 위해 companion의 apply 사용 강제. 데이터 모델링과 캡슐화 결합—조용히 실패하지 않는 생성자.

scala
// Case class with methods
case class Vec2(x: Double, y: Double) {
  def +(other: Vec2): Vec2 = Vec2(x + other.x, y + other.y)
  def magnitude: Double = math.sqrt(x*x + y*y)
  def dot(other: Vec2): Double = x*other.x + y*other.y
}

Vec2(1, 2) + Vec2(3, 4)  // Vec2(4.0, 6.0)
Vec2(3, 4).magnitude  // 5.0

// Case class with custom apply (validation)
case class Email private (value: String)
object Email {
  def apply(value: String): Email = {
    require(value.contains("@"), "invalid email")
    new Email(value)  // bypass public apply
  }
}
// Email("notanemail")  // IllegalArgumentException
Email("[email protected]")  // works

// Case class implementing trait
trait Jsonable {
  def toJson: String
}
case class User(name: String, age: Int) extends Jsonable {
  def toJson: String = s"""{"name":"$name","age":$age}"""
}

// Case class with type parameters
case class Box[A](value: A) {
  def map[B](f: A => B): Box[B] = Box(f(value))
}

패턴 매칭 심화

패턴 매칭은 Scala의 강력한 분해 도구. 지원: 리터럴 패턴(0, "hello"), 타입 패턴(s: String), case class 패턴(User(name, age)), 가드(if condition), 중첩 패턴, 바인딩(@). 컴파일러가 sealed 타입에 완전성 확인. 패턴은 위에서 아래로 시도. 부분 추출하면서 전체 값 바인딩에 @ 사용. 패턴 매칭이 if-else 체인, 타입 확인, 분해를 하나의 통일되고 읽기 쉬운 구문으로 대체.

scala
// Various pattern types
val x: Any = (1, "hello")

x match
  case (a: Int, b: String) => s"int $a, string $b"
  case (a, b) => s"pair: $a, $b"
  case _ => "other"

// Guards
def classify(n: Int): String = n match
  case n if n < 0 => "negative"
  case 0 => "zero"
  case n if n % 2 == 0 => "even positive"
  case _ => "odd positive"

// Case class patterns (nested)
case class Point(x: Int, y: Int)
case class Shape(center: Point, size: Int)

def isOrigin(s: Shape): Boolean = s match
  case Shape(Point(0, 0), _) => true
  case _ => false

// Named patterns (bind the whole while extracting)
case class User(name: String, age: Int)
def describe(u: User): String = u match
  case u @ User(name, age) if age < 18 => s"$name is a minor"
  case User(name, _) => s"$name is an adult"

// Type patterns
def handle(x: Any): String = x match
  case s: String => s"string: $s"
  case n: Int => s"int: $n"
  case list: List[_] => s"list of size ${list.size}"
  case None => "none"
  case _ => "unknown"

추출자와 커스텀 패턴

추출자(unapply)가 case class뿐 아닌 모든 타입에 패턴 매칭 가능. 매칭 패턴을 데이터 표현에서 분리. 정규식이 추출자(그룹이 바인딩). 외부 타입(JSON, URL)에 대한 추출자 작성 가능(수정 없이). unapply가 추출용으로 Option[(T1, T2, ...)] 반환, 또는 단순 매칭용으로 Boolean. 추출자가 패턴 매칭을 모든 데이터 소스로 확장 가능. Case class가 unapply auto-generate; 커스텀 추출자가 non-case-class 타입에 매칭 추가.

scala
// Custom extractor via unapply
object Email {
  def unapply(str: String): Option[(String, String)] = {
    val parts = str.split("@")
    if (parts.length == 2) Some((parts(0), parts(1)))
    else None
  }
}

// Use in pattern matching
"[email protected]" match
  case Email(user, domain) => s"user: $user, domain: $domain"
  case _ => "not an email"

// Boolean extractor (no extracted values)
object Even {
  def unapply(n: Int): Boolean = n % 2 == 0
}

5 match
  case Even() => "even"
  case _ => "odd"

// Extractor with variable arity
object Pair {
  def unapply[A, B](t: (A, B)): Option[(A, B)] = Some(t._1, t._2)
}

// Regex as extractor
val Date = "(\\d{4})-(\\d{2})-(\\d{2})".r
"2024-01-15" match
  case Date(year, month, day) => s"$year/$month/$day"
  case _ => "not a date"

// Practical: parse without case classes
object Json {
  def unapply(s: String): Option[Any] =
    scala.util.Try(ujson.read(s)).toOption
}
19

컬렉션 심화

불변 컬렉션

Scala 컬렉션은 기본적으로 불변. List는 연결 리스트(임의 접근 O(n)). Vector는 O(log n) 접근의 트리. Map과 Set은 해시 기반. 작업이 새 컬렉션 반환, 효율성을 위해 구조 공유.

scala
val list = List(1, 2, 3)
val vector = Vector(1, 2, 3)  // Fast random access
val set = Set(1, 2, 3)
val map = Map("a" -> 1, "b" -> 2)
// All immutable: operations return new collections
val updated = map + ("c" -> 3)  // New map

컬렉션 작업

map이 요소 변환. filter가 선택. reduce가 결합. grouped가 청크. flatten이 중첩 컬렉션 병합. flatMap이 매핑과 평탄화. 모두 새 컬렉션 반환. 지연 컬렉션은 .view나 .iterator 사용.

scala
val nums = (1 to 10).toList
val doubled = nums.map(_ * 2)
val evens = nums.filter(_ % 2 == 0)
val sum = nums.reduce(_ + _)
val grouped = nums.grouped(3).toList  // List(List(1,2,3), List(4,5,6), ...)
val flat = List(List(1,2), List(3,4)).flatten  // List(1,2,3,4)

패턴 매칭 컬렉션

패턴 매칭이 컬렉션에서 작동. :: (cons)가 head와 tail 매칭. List(a, b, c)가 정확히 3개 요소 매칭. Nil이 빈 리스트 매칭. _는 와일드카드. 파싱과 분해에 유용. 완전한 매칭이 버그 방지.

scala
val list = List(1, 2, 3)
list match {
    case head :: tail => println(s"Head: $head")
    case Nil => println("Empty")
}
List(1, 2, 3) match {
    case List(a, b, c) => println(s"$a, $b, $c")
    case _ => println("Other")
}

가변 컬렉션

가변 컬렉션(ArrayBuffer, mutable.Set, mutable.Map)이 제자리에서 수정. 빈번한 업데이트에 더 빠르지만 스레드 안전하지 않음. 성능이 중요하고 불변성이 필요 없을 때 사용. .toMap, .toSet으로 불변으로 변환.

scala
import scala.collection.mutable
val buffer = mutable.ArrayBuffer(1, 2, 3)
buffer += 4  // Add element
buffer -= 2  // Remove element
buffer(0) = 10  // Update
val mset = mutable.Set(1, 2, 3)
val mmap = mutable.Map("a" -> 1)
mmap("b") = 2

지연 컬렉션

view가 지연 뷰 생성: 작업이 강제될 때까지 지연. 큰 컬렉션의 체인 작업에 유용. LazyList(Scala 2.13+)가 지연 시퀀스. 지연 평가로 무한 시퀀스 가능. .toList, .toArray로 강제.

scala
val lazyView = (1 to 1000000).view.map(_ * 2).filter(_ > 100)
// No computation yet
val first = lazyView.head  // Only computes first
val result = lazyView.take(10).toList  // Only 10 elements processed
// LazyList (formerly Stream)
val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map(_ + _)
20

Implicit

Implicit 매개변수

Implicit 매개변수는 스코프에 있을 때 자동으로 전달. 컴파일러가 매칭 implicit 값 검색. 구성, 타입 클래스, 컨텍스트에 사용. 명시적으로 재정의 가능. 여러 implicit은 고유한 타입이어야 함.

scala
def connect(url: String)(implicit timeout: Int): Unit = {
    println(s"Connecting to $url with timeout $timeout")
}
implicit val defaultTimeout: Int = 5000
connect("http://example.com")  // Uses 5000
connect("http://example.com")(3000)  // Explicit override

Implicit 변환

Implicit 변환이 타입을 자동으로 변환. 위험할 수 있음(예상치 못한 변환). implicit class가 확장 메서드 추가. Scala 3은 명확성을 위해 given/using과 extension 사용. 원시 변환보다 확장 메서드 선호.

scala
import scala.language.implicitConversions
implicit def intToString(n: Int): String = n.toString
val s: String = 42  // Converts via intToString
// Extension methods (Scala 3 preferred)
implicit class RichInt(val n: Int) extends AnyVal {
    def squared: Int = n * n
}
5.squared  // 25

타입 클래스

타입 클래스는 ad-hoc 다형성을 위한 패턴. trait이 동작 정의, instance가 특정 타입에 대한 구현 제공. Implicit 해석이 instance 검색. 상속보다 유연. Cats와 Shapeless에서 흔함.

scala
trait Show[A] {
    def show(a: A): String
}
object Show {
    implicit val intShow: Show[Int] = (a: Int) => a.toString
    implicit val stringShow: Show[String] = (a: String) => a
    def apply[A](a: A)(implicit s: Show[A]): String = s.show(a)
}
Show(42)  // "42"
Show("hello")  // "hello"

컨텍스트 바운드

컨텍스트 바운드 [A: TypeClass]는 implicit 매개변수의 syntactic sugar. 타입 클래스 instance는 implicitly로 사용 가능. 타입 클래스 제약을 위한 더 깔끔한 구문. Ordering, Numeric, 커스텀 타입 클래스에서 흔함.

scala
def max[A: Ordering](a: A, b: A): A = {
    val ord = implicitly[Ordering[A]]
    if (ord.gt(a, b)) a else b
}
// Equivalent to:
def max2[A](a: A, b: A)(implicit ord: Ordering[A]): A =
    if (ord.gt(a, b)) a else b

Given/Using (Scala 3)

Scala 3이 명확성을 위해 implicit을 given/using으로 대체. given이 instance 정의, using이 매개변수 선언. extension이 implicit class 대체. 더 명시적이고 읽기 쉬움. 마이그레이션 도구가 Scala 2 implicit 변환.

scala
// Scala 3 syntax
given defaultTimeout: Int = 5000
def connect(url: String)(using timeout: Int): Unit =
    println(s"Connecting with $timeout")
connect("http://example.com")  // Uses given
// Extension methods
extension (n: Int)
    def squared: Int = n * n
21

동시성

Future

Future가 비동기 계산 나타냄. onComplete가 완료 처리. map/flatMap이 작업 체인. ExecutionContext가 스레드 제공. Future는 불변이고 일회성. 여러 future에 for-comprehension 사용.

scala
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f: Future[Int] = Future {
    Thread.sleep(1000)
    42
}
f.onComplete {
    case Success(v) => println(v)
    case Failure(e) => println(e)
}
// Map/flatMap for chaining
val f2 = f.map(_ * 2).flatMap(x => Future(x + 1))

Future로 for-comprehension

for-comprehension이 flatMap/map으로 desugar. 순차적: 각 단계가 이전 것 대기. 병렬 실행을 위해 for 전에 future 시작. 중첩 콜백보다 훨씬 읽기 쉬움. 모든 모나드(Future, Option, List)에 작동.

scala
def getUser(id: Int): Future[User] = ...
def getOrders(user: User): Future[List[Order]] = ...
val result: Future[List[Order]] = for {
    user <- getUser(1)
    orders <- getOrders(user)
} yield orders
// Equivalent to flatMap/map chaining

병렬 Future

for-comprehension 전에 future 시작 시 병렬 실행. Future.sequence가 List[Future[T]]를 Future[List[T]]로 변환. Future.traverse가 매핑과 시퀀싱을 한 단계로. zip이 두 future 결합. 가장 느린 것이 완료될 때 모두 완료.

scala
val f1 = Future { compute1() }
val f2 = Future { compute2() }
val combined: Future[(Int, Int)] = for {
    r1 <- f1
    r2 <- f2
} yield (r1, r2)
// Or: Future.sequence(List(f1, f2))
// Or: Future.traverse(list)(compute)

Actor (Akka)

Akka actor가 상태를 캡슐화하고 메시지로 통신. 공유 가변 상태 없음. 각 actor가 한 번에 하나의 메시지 처리. ! (tell)이 fire-and-forget 전송. ? (ask)가 Future 반환. Supervision이 실패 처리. 동시 상태 저장 시스템에 이상적.

scala
import akka.actor.*
class Counter extends Actor {
    var count = 0
    def receive = {
        case "inc" => count += 1
        case "get" => sender() ! count
    }
}
val system = ActorSystem("mySystem")
val counter = system.actorOf(Props[Counter], "counter")
counter ! "inc"
counter ! "get"

Cats Effect IO

Cats Effect IO는 순수 함수형 IO 모나드. 참조 투명: IO(println("x"))가 값. for-comprehension으로 조합. 취소 가능하고 리소스 안전. unsafeRunSync가 프로그램 엣지에서 실행. 더 나은 의미론으로 Future의 대안.

scala
import cats.effect.IO
val program: IO[Int] = for {
    _ <- IO(println("Start"))
    result <- IO.pure(42)
    _ <- IO(println(s"Got $result"))
} yield result
// Run at end of world
program.unsafeRunSync()
// Referentially transparent, cancellable
22

패턴 매칭 심화

Case Class

Case class가 equals, hashCode, toString, apply/unapply auto-generate. 패턴 매칭에 완벽. 패턴 매칭이 분해. 기본적으로 불변. 대수적 데이터 타입에 사용. copy()가 수정된 복사본 생성.

scala
case class Point(x: Int, y: Int)
val p = Point(1, 2)
p match {
    case Point(0, 0) => "origin"
    case Point(0, _) => "on y-axis"
    case Point(x, 0) => s"on x-axis at $x"
    case Point(x, y) => s"at ($x, $y)"
}

Sealed Trait

sealed trait이 하위 타입을 같은 파일로 제한. 컴파일러가 패턴 매칭의 완전성 확인. 새 하위 타입 추가 시 모든 매치에서 경고. 닫힌 타입 계층에 이상적. ADT를 위해 case class와 결합.

scala
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Square(side: Double) extends Shape
case class Rectangle(w: Double, h: Double) extends Shape
def area(s: Shape): Double = s match {
    case Circle(r) => math.Pi * r * r
    case Square(s) => s * s
    case Rectangle(w, h) => w * h
}  // Compiler warns if not exhaustive

가드 & 추출자

가드(if)가 패턴에 조건 추가. 커스텀 추출자(unapply)가 모든 타입에 패턴 매칭 가능. unapply가 매치를 나타내기 위해 Option 반환. 추출자가 매칭을 타입에서 분리. DSL과 파싱에 강력.

scala
def classify(n: Int): String = n match {
    case x if x < 0 => "negative"
    case 0 => "zero"
    case x if x % 2 == 0 => "even"
    case _ => "odd"
}
// Custom extractor
object Even {
    def unapply(n: Int): Option[Int] =
        if (n % 2 == 0) Some(n / 2) else None
}
4 match { case Even(half) => s"half is $half" }

부분 함수

부분 함수는 일부 입력에만 정의. isDefinedAt이 확인. collect가 정의된 곳에만 적용. 콜백과 라우팅에 유용. orElse가 부분 함수 결합. lift가 Option 반환하는 전체 함수로 변환.

scala
val pf: PartialFunction[Int, String] = {
    case 1 => "one"
    case 2 => "two"
}
pf.isDefinedAt(1)  // true
pf.isDefinedAt(3)  // false
// Collect = filter + map
List(1, 2, 3, 1).collect(pf)  // List("one", "two", "one")

패턴 매칭 타입

패턴 매칭이 타입에서 작동하지만, 타입 소거가 제네릭에 영향. List(a, b)가 2-요소 리스트 매치. List(_*)가 모든 리스트 매치. List[Int] 같은 제네릭 타입 매칭 피하기(소거됨). 런타임 타입 정보를 위해 타입 태그 사용.

scala
def describe(x: Any): String = x match {
    case i: Int => s"Int: $i"
    case s: String => s"String: $s"
    case List(a, b) => s"Two-element list: $a, $b"
    case List(_*) => "List with elements"
    case Some(v) => s"Some: $v"
    case None => "None"
    case _ => "Unknown"
}
23

함수형 프로그래밍

고차 함수

고차 함수가 함수를 받거나 반환. 커링이 다중 인자 함수를 단일 인자 체인으로 분할. 부분 적용이 일부 인자 고정. 함수 조합과 재사용 가능. _가 부분 적용 함수 생성.

scala
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(_ + 3, 5)  // 11
// Currying
def add(a: Int)(b: Int): Int = a + b
val add5 = add(5) _  // Partial application
add5(3)  // 8

Option & Either

Option이 선택적 값 나타냄: Some 또는 None. null 회피. getOrElse가 기본값 제공. Either가 성공(Right) 또는 실패(Left) 나타냄. 예상된 오류에 예외보다 나음. 둘 다 모나드: map, flatMap, for-comprehension.

scala
def find(id: Int): Option[String] =
    if (id > 0) Some("Alice") else None
find(1).getOrElse("Unknown")  // "Alice"
find(-1).getOrElse("Unknown")  // "Unknown"
// Either for error handling
def parse(s: String): Either[String, Int] =
    try Right(s.toInt)
    catch { case _: Exception => Left(s"Not a number: $s") }

함수 조합

compose가 함수를 오른쪽에서 왼쪽으로 체인(수학처럼). andThen이 왼쪽에서 오른쪽으로 체인(더 읽기 쉬움). 둘 다 새 함수 생성. 파이프라인 빌드에 유용. Scala에서 함수는 일급 값.

scala
val addOne: Int => Int = _ + 1
val double: Int => Int = _ * 2
// Compose (right to left)
val f = addOne compose double  // double then addOne
f(3)  // 7
// AndThen (left to right)
val g = addOne andThen double  // addOne then double
g(3)  // 8

재귀 & 꼬리 재귀

꼬리 재귀는 컴파일러에 의해 루프로 최적화. 재귀 호출이 마지막 작업이어야 함. @tailrec 어노테이션이 컴파일 타임에 확인. 누산기 패턴이 상태 전달. 깊은 재귀의 스택 오버플로 방지.

scala
// Not tail-recursive: stack overflow for large n
def factorial(n: Int): Int =
    if (n <= 1) 1 else n * factorial(n - 1)
// Tail-recursive: optimized to loop
import scala.annotation.tailrec
@tailrec
def factorial(n: Int, acc: Int = 1): Int =
    if (n <= 1) acc else factorial(n - 1, n * acc)

모나드

모나드는 flatMap과 unit(pure/pure) 가짐. 컨텍스트와 함께 작업 체인(Option: 부재, List: 비결정성, Future: 비동기). for-comprehension이 flatMap/map으로 desugar. 모나드 법칙이 올바른 조합 보장. Cats가 Monad 타입 클래스 제공.

scala
// Monad laws: left identity, right identity, associativity
// Option is a monad:
Some(5).flatMap(x => Some(x + 1))  // Some(6)
None.flatMap(x => Some(x + 1))  // None
// List is a monad:
List(1, 2).flatMap(x => List(x, x * 10))  // List(1, 10, 2, 20)
// for-comprehension is monadic sugar:
for {
    x <- Some(5)
    y <- Some(x + 1)
} yield y  // Some(6)
24

흔한 함정

Null vs Option

Scala는 Java 호환성을 위해 null이 있지만 권장되지 않음. Option이 부재를 명시적으로 나타냄. 패턴 매칭이 None 처리 강제. 예외에 .toOption 사용. Scala 코드에서 null 피하기; Java interop에만 예약.

scala
// BAD: null
def find(id: Int): String =
    if (id > 0) "Alice" else null
// GOOD: Option
def find(id: Int): Option[String] =
    if (id > 0) Some("Alice") else None
// Scala avoids null; use Option
// NullPointer exceptions are rare in idiomatic Scala

Var vs Val

val은 불변(값), var는 가변(변수). 더 안전하고 예측 가능한 코드를 위해 val 선호. 가변 상태는 추론과 동시성을 복잡하게. 로컬 성능이나 진정으로 필요할 때만 var 사용. 컬렉션은 기본적으로 불변.

scala
var x = 1  // Mutable
x = 2  // OK
val y = 1  // Immutable
// y = 2  // Error
// Prefer val for immutability
// Use var only when necessary
// Mutable state causes bugs in concurrent code

동등성

Scala ==는 equals 호출(값 동등성), Java와 달리. eq가 참조 동등성 확인. ne는 eq의 부정. 값 비교에는 항상 == 사용. Case class가 올바른 equals 가짐. 커스텀 클래스의 경우 equals와 hashCode 재정의.

scala
val a = List(1, 2)
val b = List(1, 2)
a == b  // true (value equality)
a eq b  // false (reference equality)
// In Java: a == b compares references
// In Scala: == calls equals (value)
// Use eq for reference equality (rarely needed)

Implicit 모호성

같은 타입의 여러 implicit이 모호성 오류 발생. implicit 스코프를 깔끔하게 유지. 구분을 위해 특정 타입(newtype) 사용. Scala 3 given/using이 더 명확. 예상치 못한 동작을 일으키는 implicit 변환 피하기.

scala
implicit val s1: String = "hello"
implicit val s2: String = "world"
def greet(implicit s: String) = println(s)
greet  // Error: ambiguous implicit values
// Fix: only one implicit in scope
// Or: pass explicitly
greet(s1)

By-Name vs By-Value

By-name 매개변수(=> T)는 사용 시마다 지연 평가. By-value 매개변수는 호출 전 한 번 평가. By-name이 커스텀 제어 구조(unless, while) 가능. 여러 번 평가될 수 있음; 필요시 lazy val로 캐시.

scala
// By-value: evaluated once
def log(msg: String) = println(msg)
log(expensiveComputation())  // Evaluated before call
// By-name: evaluated each use
def log(msg: => String) = println(msg)
log(expensiveComputation())  // Evaluated only if called
// Useful for lazy evaluation and control flow
def unless(cond: Boolean)(body: => Unit): Unit =
    if (!cond) body
25

Akka Actor

Actor System

ActorSystem이 actor를 위한 컨테이너. actorOf가 이름으로 actor 생성. ! (tell)이 비동기로 메시지 전송. ? (ask)가 전송하고 Future 반환. Actor는 경로로 식별. terminate가 시스템을 우아하게 종료.

scala
import akka.actor.*
val system = ActorSystem("MySystem")
val actor = system.actorOf(Props[MyActor], "myActor")
actor ! "Hello"  // Fire-and-forget
val future = actor ? "Query"  // Ask pattern
system.terminate()

Actor 수명 주기

preStart가 생성 시, postStop이 종료 시 실행. preRestart/postRestart가 supervision 처리. actor는 재시작 전 중지, 새로 시작. 내부 상태는 재시작 시 손실. 초기화에는 preStart, 정리에는 postStop 사용.

scala
class MyActor extends Actor {
    override def preStart(): Unit = println("Starting")
    override def postStop(): Unit = println("Stopped")
    override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
        println("Restarting")
        super.preRestart(reason, message)
    }
    def receive = {
        case "ping" => sender() ! "pong"
    }
}

Supervision

Supervision이 실패 처리 방법 정의. Resume: 같은 상태로 계속. Restart: 새로운 상태로 재생성. Stop: 영구 종료. Escalate: 부모가 처리. OneForOneStrategy는 실패한 자식만 영향. AllForOneStrategy는 모든 자식 영향.

scala
class Supervisor extends Actor {
    override val supervisorStrategy = OneForOneStrategy() {
        case _: ArithmeticException => Resume  // Continue
        case _: NullPointerException => Restart  // Restart actor
        case _: Exception => Stop  // Stop actor
    }
    def receive = { case _ => }
}

Router

Router가 메시지를 여러 actor에 분배. Pool이 worker 생성 및 관리. Group이 기존 actor 사용. 전략: RoundRobin, Random, SmallestMailbox, ConsistentHashing, ScatterGatherFirst. Router가 작업 병렬화로 처리량 향상.

scala
// Pool router: creates workers
val router = system.actorOf(
    RoundRobinPool(5).props(Props[Worker]),
    "router"
)
router ! "work"  // Distributed to one of 5 workers
// Group router: uses existing actors
val group = system.actorOf(
    RoundRobinGroup(paths).props(),
    "group"
)

Persistence

PersistentActor가 이벤트를 저널에 저장(이벤트 소싱). Command가 검증 후 이벤트로 저장. 재시작 시 receiveRecover로 이벤트 재생. 상태가 이벤트에서 재구축. 크래시에서 복구 가능. Akka Typed에는 EventSourcedBehavior 사용.

scala
class Counter extends PersistentActor {
    var count = 0
    override def persistenceId = "counter-1"
    override def receiveCommand = {
        case "inc" => persist(Incremented) { event =>
            count += 1
        }
    }
    override def receiveRecover = {
        case Incremented => count += 1
    }
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.