Scala 프로젝트를 생성하고 SBT를 설정하는 방법을 알아봅니다.

참고 자료

프로젝트 생성

IntelliJ IDEA에서 Scala 프로젝트를 생성할 수 있습니다.

메인 진입점 추가

object HelloWorld extends App {
  println("Hello, World!")
}

SBT Assembly 플러그인

Fat JAR을 생성하기 위한 sbt-assembly 플러그인 설정입니다.

project/plugin.sbt

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0")

build.sbt 설정

name := "my-project"
version := "1.0"
scalaVersion := "2.13.12"

// 의존성 추가
libraryDependencies ++= Seq(
  "org.scalatest" %% "scalatest" % "3.2.17" % Test,
  "com.typesafe" % "config" % "1.4.3"
)

// assembly 설정 (Fat JAR 생성 시)
assembly / assemblyMergeStrategy := {
  case PathList("META-INF", _*) => MergeStrategy.discard
  case _ => MergeStrategy.first
}

Fat JAR을 생성하려면 터미널에서 sbt assembly를 실행하면 target/scala-{version}/ 디렉토리에 JAR 파일이 생성됩니다.


Scala 기본 문법

변수 선언

Scala에서는 val(불변)과 var(가변) 두 가지 방식으로 변수를 선언합니다. 함수형 프로그래밍 스타일을 따르려면 가능한 한 val을 사용하는 것이 좋습니다.

val name: String = "Scala"   // 불변 (재할당 불가)
var count: Int = 0           // 가변 (재할당 가능)

// 타입 추론
val message = "Hello"        // String으로 추론
val number = 42              // Int로 추론

함수 정의

// 기본 함수
def greet(name: String): String = {
  s"Hello, $name!"
}

// 한 줄 함수 (중괄호 생략)
def square(x: Int): Int = x * x

// 기본값이 있는 파라미터
def connect(host: String = "localhost", port: Int = 8080): String = {
  s"$host:$port"
}

connect()                    // "localhost:8080"
connect("example.com", 443)  // "example.com:443"
connect(port = 3000)         // "localhost:3000"

Case Class

Case class는 데이터를 담는 불변 클래스를 간결하게 정의하는 방법입니다. equals, hashCode, toString, copy 메서드가 자동으로 생성됩니다.

case class Person(name: String, age: Int)

val p1 = Person("Kim", 30)
val p2 = p1.copy(age = 31)      // 불변이므로 copy로 새 객체 생성
println(p1 == p2)                // false (값 비교)

Pattern Matching

Scala의 패턴 매칭은 Java의 switch보다 훨씬 강력합니다.

def describe(x: Any): String = x match {
  case 0 => "zero"
  case i: Int if i > 0 => "positive integer"
  case s: String => s"string: $s"
  case (a, b) => s"tuple: ($a, $b)"
  case _ => "something else"
}

// case class와 함께 사용
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape

def area(shape: Shape): Double = shape match {
  case Circle(r) => math.Pi * r * r
  case Rectangle(w, h) => w * h
}

컬렉션

Scala의 컬렉션은 불변(immutable)이 기본이며, 풍부한 함수형 메서드를 제공합니다.

val numbers = List(1, 2, 3, 4, 5)

numbers.map(_ * 2)           // List(2, 4, 6, 8, 10)
numbers.filter(_ % 2 == 0)   // List(2, 4)
numbers.reduce(_ + _)        // 15
numbers.flatMap(n => List(n, n * 10))  // List(1, 10, 2, 20, ...)

// Map
val scores = Map("Kim" -> 95, "Lee" -> 87, "Park" -> 92)
scores.getOrElse("Kim", 0)   // 95
scores + ("Choi" -> 88)      // 새 Map 반환

SBT 주요 명령어

명령어 설명
sbt compile 소스 코드 컴파일
sbt run 메인 클래스 실행
sbt test 테스트 실행
sbt console Scala REPL 시작 (프로젝트 클래스 사용 가능)
sbt assembly Fat JAR 생성
sbt clean 빌드 결과물 삭제
sbt ~compile 파일 변경 시 자동 컴파일

SBT 셸 모드에서는 sbt를 실행한 뒤 명령어를 직접 입력할 수 있어 JVM 시작 시간을 절약할 수 있습니다.


Option, Either, Try: 안전한 에러 처리

Scala는 null 대신 Option, Either, Try를 사용하여 안전하게 값의 부재나 에러를 처리합니다.

Option

값이 있거나 없을 수 있는 경우에 사용합니다. Some(value) 또는 None으로 표현됩니다.

// null 대신 Option 사용
def findUser(id: Int): Option[String] = {
  val users = Map(1 -> "Kim", 2 -> "Lee")
  users.get(id)  // Option[String] 반환
}

findUser(1)  // Some("Kim")
findUser(99) // None

// 안전한 값 추출
val name = findUser(1).getOrElse("Unknown")  // "Kim"

// map, flatMap과 함께 사용
val greeting = findUser(1).map(n => s"Hello, $n")  // Some("Hello, Kim")

// for comprehension으로 여러 Option 조합
val result = for {
  user <- findUser(1)
  if user.length > 2
} yield s"Found: $user"

Either

성공 또는 실패를 명시적으로 표현할 때 사용합니다. Right가 성공, Left가 실패를 나타냅니다.

def divide(a: Int, b: Int): Either[String, Double] = {
  if (b == 0) Left("Cannot divide by zero")
  else Right(a.toDouble / b)
}

divide(10, 3) match {
  case Right(result) => println(s"Result: $result")
  case Left(error)   => println(s"Error: $error")
}

Try

예외가 발생할 수 있는 코드를 안전하게 감싸는 용도로 사용합니다.

import scala.util.{Try, Success, Failure}

val result = Try("123".toInt)  // Success(123)
val failed = Try("abc".toInt)  // Failure(NumberFormatException)

result match {
  case Success(value) => println(s"Parsed: $value")
  case Failure(ex)    => println(s"Failed: ${ex.getMessage}")
}

// getOrElse로 기본값 지정
val num = Try("abc".toInt).getOrElse(0)  // 0

Implicit과 Type Class

Scala의 implicit은 강력하지만 남용하면 코드를 이해하기 어렵게 만들 수 있습니다. 대표적인 사용 사례를 알아봅니다.

Implicit Parameters

함수 호출 시 자동으로 전달되는 매개변수입니다. ExecutionContext나 설정 객체 전달에 자주 사용됩니다.

implicit val defaultGreeting: String = "Hello"

def greet(name: String)(implicit greeting: String): String = {
  s"$greeting, $name!"
}

greet("Kim")  // "Hello, Kim!" (implicit 값 자동 전달)

Implicit Conversions (Scala 2)

Scala 3에서는 given/using으로 대체되었지만, Scala 2 코드에서는 여전히 많이 사용됩니다.

// Scala 3의 given/using 문법
given defaultGreeting: String = "Hello"

def greet(name: String)(using greeting: String): String = {
  s"$greeting, $name!"
}

Futures와 비동기 프로그래밍

Scala의 Future는 비동기 연산을 표현합니다.

import scala.concurrent.{Future, Await}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._

// 비동기 작업 정의
val futureResult: Future[Int] = Future {
  Thread.sleep(1000)  // 시뮬레이션용
  42
}

// 콜백으로 결과 처리
futureResult.onComplete {
  case Success(value) => println(s"Got: $value")
  case Failure(ex)    => println(s"Failed: ${ex.getMessage}")
}

// 여러 Future 조합
val combined = for {
  a <- Future(10)
  b <- Future(20)
} yield a + b  // Future(30)

// 결과 대기 (테스트용, 프로덕션에서는 피할 것)
val result = Await.result(combined, 5.seconds)

Scala와 Java 상호운용

Scala는 JVM 위에서 동작하므로 Java 라이브러리를 직접 사용할 수 있습니다. 다만 컬렉션 변환에 주의가 필요합니다.

import scala.jdk.CollectionConverters._

// Java 컬렉션 → Scala 컬렉션
val javaList = new java.util.ArrayList[String]()
javaList.add("hello")
val scalaList: Seq[String] = javaList.asScala.toSeq

// Scala 컬렉션 → Java 컬렉션
val scalaMap = Map("key" -> "value")
val javaMap: java.util.Map[String, String] = scalaMap.asJava

Scala 버전 선택 가이드

버전 특징 추천 대상
Scala 2.12 Spark 3.x와 호환 Spark 프로젝트
Scala 2.13 최신 라이브러리 지원 일반 프로젝트
Scala 3 새로운 문법, 개선된 타입 시스템 신규 프로젝트

Apache Spark를 사용하는 경우 Spark 버전에 맞는 Scala 버전을 선택해야 합니다. Spark 3.x는 주로 Scala 2.12를, Spark 3.4+는 Scala 2.13도 지원합니다.