전체 비유: 건축 자재 선택#

Scala의 기본 문법을 건축 자재 선택에 비유하면 이해하기 쉽습니다:

건축 비유Scala 개념역할
콘크리트 (영구 고정)val (불변)한 번 정하면 변경 불가
이동식 파티션var (가변)필요시 재배치 가능
조립식 자재lazy val필요할 때 조립
건물 설계도타입 시스템구조적 안전성 보장
자동 측량타입 추론컴파일러가 치수 계산
모듈형 템플릿문자열 보간재사용 가능한 형식
건축 표준타입 계층일관된 호환성 규칙

이처럼 좋은 건물이 적절한 자재 선택에서 시작하듯, 좋은 Scala 코드는 올바른 변수 선언과 타입 활용에서 시작합니다.


대상 독자: Java 또는 다른 정적 타입 언어 경험이 있는 개발자 선수 지식: 변수, 함수, 클래스의 기본 개념 소요 시간: 약 25-30분 이 문서를 읽으면: Scala의 val/var 차이를 이해하고, 타입 시스템을 활용하며, 문자열 보간으로 깔끔한 코드를 작성할 수 있습니다

TL;DR
  • val(불변)을 기본으로, var(가변)는 꼭 필요할 때만 사용하세요
  • Scala는 타입 추론이 강력해서 대부분 타입 선언을 생략할 수 있습니다
  • 문자열 보간(s"Hello, $name")으로 문자열 조합이 간편합니다
  • 모든 값은 객체입니다 — 1.toString처럼 메서드를 호출할 수 있습니다

왜 Scala의 기본 문법을 알아야 하나?#

Scala는 Java와 100% 호환되면서도 더 간결하고 표현력 있는 코드를 작성할 수 있게 해줍니다. 같은 로직을 Java보다 적은 코드로 작성할 수 있고, 컴파일 타임에 더 많은 오류를 잡아냅니다.

Scala의 설계 철학

Scala의 기본 문법은 세 가지 핵심 철학을 반영합니다:

graph LR
    P1["불변성 우선<br>(Immutability First)"]
    P2["타입 안전성<br>(Type Safety)"]
    P3["표현력<br>(Expressiveness)"]

    P1 --> R1["예측 가능한 코드"]
    P2 --> R2["컴파일 타임 오류 탐지"]
    P3 --> R3["적은 코드, 명확한 의도"]

Scala의 설계 철학은 불변성, 타입 안전성, 표현력의 균형을 추구합니다. 이 세 가지가 조화를 이루어 안전하면서도 간결한 코드를 가능하게 합니다.

철학적용결과
불변성 우선val을 기본으로 사용상태 추적 불필요, 동시성 안전
타입 안전성정적 타입 + 추론런타임 오류를 컴파일 타임에 발견
표현력문자열 보간, 케이스 클래스보일러플레이트 제거, 의도 중심 코드
// Java: 15줄
public class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    // equals, hashCode, toString 생략...
}
// Scala: 1줄
case class Person(name: String, age: Int)

이런 간결함의 기반이 되는 것이 바로 Scala의 기본 문법입니다.


변수와 상수#

Scala에서는 val(불변)과 var(가변) 두 가지 방식으로 값을 선언합니다.

비유로 이해하기: val영구 마커로 쓴 이름표와 같습니다. 한 번 붙이면 바꿀 수 없습니다. var화이트보드에 쓴 메모와 같습니다. 언제든 지우고 다시 쓸 수 있지만, 누가 언제 바꿨는지 추적하기 어렵습니다.

val - 불변 (권장)

val로 선언한 값은 재할당할 수 없습니다. 함수형 프로그래밍의 핵심 원칙입니다.

val name = "Scala"
val year = 2024
val pi = 3.14159

// 재할당 불가
// name = "Java"  // 컴파일 에러!

왜 불변이 좋은가?

장점설명
예측 가능성값이 변하지 않으므로 코드 흐름을 추적하기 쉽습니다
스레드 안전여러 스레드가 동시에 읽어도 문제가 없습니다
디버깅 용이“이 값이 언제 바뀌었지?“를 고민할 필요가 없습니다
컴파일러 최적화값이 불변임을 알면 컴파일러가 더 효율적인 코드를 생성합니다

var - 가변

var로 선언한 값은 재할당할 수 있습니다. 정말 필요한 경우에만 사용하세요.

var count = 0
count = count + 1  // OK
count += 1         // OK (축약형)

var message = "Hello"
message = "World"  // OK

언제 var를 사용하나?

  • 성능이 중요한 루프 내부에서 누적 계산이 필요할 때
  • 외부 라이브러리가 가변 상태를 요구할 때
  • 점진적으로 값을 수집해야 할 때 (Builder 패턴)

⚠️ 주의: 가능하면 var 대신 val과 함수형 연산(map, fold 등)을 사용하세요.

val vs var 선택 가이드

flowchart TD
    Q1{"값을 재할당해야<br>하는가?"}
    Q2{"성능이 중요한<br>루프 내부인가?"}
    Q3{"외부 라이브러리가<br>가변 상태를 요구하는가?"}

    Q1 -->|"아니오"| VAL["✅ val 사용"]
    Q1 -->|"예"| Q2
    Q2 -->|"예"| VAR["⚠️ var 사용<br>(범위 최소화)"]
    Q2 -->|"아니오"| Q3
    Q3 -->|"예"| VAR
    Q3 -->|"아니오"| FP["✅ val + 함수형 연산<br>(map, fold 등)"]

val과 var 중 어떤 것을 선택할지 결정하는 흐름도입니다. 기본적으로 val을 사용하고, 정말 필요한 경우에만 var를 제한적으로 사용합니다.

지연 초기화 (lazy val)

lazy val처음 접근할 때 초기화됩니다. 비용이 큰 계산이나 리소스 로딩을 필요할 때까지 미룰 수 있습니다.

비유로 이해하기: lazy val주문 후 조리하는 음식과 같습니다. 미리 만들어두지 않고, 손님이 주문하면 그때 조리를 시작합니다. 한 번 만들면 같은 주문에는 캐시된 음식을 제공합니다.

lazy val expensiveValue = {
  println("계산 중...")
  Thread.sleep(1000)
  42
}

println("선언됨")          // 즉시 출력
println(expensiveValue)   // 여기서 "계산 중..." 출력, 1초 대기
println(expensiveValue)   // 캐시된 42 즉시 반환, 재계산 없음
핵심 포인트: 변수 선언
  • val: 불변, 기본으로 사용 → val name = "Scala"
  • var: 가변, 꼭 필요할 때만 → var count = 0
  • lazy val: 지연 초기화, 비용 큰 계산에 → lazy val config = loadConfig()

타입 시스템#

Scala는 정적 타입 언어이지만, 강력한 타입 추론 덕분에 Java처럼 매번 타입을 명시하지 않아도 됩니다.

왜 정적 타입인가?

동적 타입 (Python, JS)정적 타입 (Scala, Java)
런타임에 타입 오류 발견컴파일 타임에 타입 오류 발견
빠른 프로토타이핑대규모 리팩토링에 안전
IDE 자동완성 제한적강력한 IDE 지원

Scala는 정적 타입의 안전성과 동적 타입의 간결함을 모두 제공합니다.

기본 타입

Scala의 모든 값은 객체입니다. Java의 원시 타입(primitive)도 Scala에서는 객체로 취급됩니다.

val n = 42
n.toString      // "42" - Int도 객체이므로 메서드 호출 가능
n.to(50)        // Range(42, 43, 44, ..., 50)
1.+(2)          // 3 - 연산자도 메서드!
타입설명예시
Int32비트 정수val i = 42
Long64비트 정수val l = 1234567890L
Double64비트 부동소수점val d = 3.14159
Boolean참/거짓val flag = true
String문자열val s = "Hello"
Unit값 없음 (Java의 void)val u = ()

위 표는 Scala에서 가장 자주 사용하는 기본 타입들을 정리한 것입니다. Java와 달리 모든 타입이 객체이므로 메서드를 호출할 수 있습니다.

타입 계층 구조

Scala의 타입은 명확한 계층 구조를 가집니다.

graph TB
    Any["Any<br>(최상위)"]
    AnyVal["AnyVal<br>(값 타입)"]
    AnyRef["AnyRef<br>(참조 타입)"]

    Any --> AnyVal
    Any --> AnyRef

    Int["Int"]
    Double["Double"]
    Boolean["Boolean"]
    Unit["Unit"]

    AnyVal --> Int
    AnyVal --> Double
    AnyVal --> Boolean
    AnyVal --> Unit

    String["String"]
    List["List[T]"]
    UserClass["사용자 클래스"]

    AnyRef --> String
    AnyRef --> List
    AnyRef --> UserClass

    Null["Null"]
    Nothing["Nothing<br>(최하위)"]

    String --> Null
    List --> Null
    UserClass --> Null

    Null --> Nothing
    Int --> Nothing
    Double --> Nothing
    Boolean --> Nothing
    Unit --> Nothing

위 다이어그램은 Scala 타입 계층을 보여줍니다. Any가 모든 타입의 최상위이고, Nothing이 모든 타입의 최하위입니다. AnyVal은 값 타입(Int, Double 등)의 부모이고, AnyRef는 참조 타입(String, List, 사용자 클래스)의 부모입니다.

비유로 이해하기: 타입 계층은 가족 족보와 같습니다. Any는 모든 타입의 조상이고, Nothing은 모든 타입의 후손입니다. 조상의 자리에는 후손을 넣을 수 있습니다 (다형성).

타입역할언제 만나나?
Any모든 타입의 조상여러 타입을 하나로 다룰 때
AnyVal값 타입의 부모Int, Double 등의 공통 조상
AnyRef참조 타입의 부모Java의 Object와 동일
Nullnull의 타입(가능하면 사용하지 마세요)
Nothing모든 타입의 후손예외, 빈 컬렉션, Option.None

Nothing은 언제 사용될까?

비유로 이해하기: Nothing어디에나 들어갈 수 있는 만능 어댑터와 같습니다. 빈 박스(Nil)는 어떤 물건을 담는 박스 자리에도 놓을 수 있고, “물건 없음”(None)은 어떤 물건이든 있을 수 있는 자리에 놓을 수 있습니다. Nothing이 모든 타입의 하위 타입이기 때문에 이런 유연성이 가능합니다.

Nothing정상적으로 값을 반환하지 않는 경우에 사용됩니다.

// 1. 예외를 던지는 함수
def fail(message: String): Nothing =
  throw new RuntimeException(message)

// Nothing은 모든 타입의 하위 타입이므로 어디서나 사용 가능
val result: Int = if (true) 42 else fail("error")

// 2. 빈 컬렉션의 타입
val empty: List[Nothing] = Nil  // List[Int], List[String] 등에 할당 가능

// 3. Option.None의 타입
val none: Option[Nothing] = None  // Option[Int], Option[String] 등에 할당 가능

왜 유용한가? Nothing이 모든 타입의 하위 타입이기 때문에, Nil이나 None을 어떤 타입의 리스트나 Option에도 사용할 수 있습니다.

핵심 포인트: 타입 시스템
  • 모든 값은 객체 — 1.toString, true.&&(false) 가능
  • AnyAnyVal(값) / AnyRef(참조) → 구체적 타입 → Nothing
  • Nothing은 빈 컬렉션, None, 예외 등에서 타입 호환성을 제공

타입 추론#

Scala 컴파일러는 대부분의 경우 타입을 자동으로 추론합니다.

Java vs Scala 비교

// Java: 타입 명시 필수
Map<String, List<Integer>> map = new HashMap<String, List<Integer>>();
List<String> names = Arrays.asList("Alice", "Bob");
// Scala: 타입 추론
val map = Map("a" -> List(1, 2), "b" -> List(3, 4))  // Map[String, List[Int]]
val names = List("Alice", "Bob")                      // List[String]

추론되는 경우

val name = "Scala"     // String으로 추론
val count = 42         // Int로 추론
val pi = 3.14          // Double로 추론
val flag = true        // Boolean으로 추론
val numbers = List(1, 2, 3)  // List[Int] 추론

명시적 타입 선언이 필요한 경우

상황예시이유
특정 타입 원할 때val n: Long = 42Int 대신 Long 필요
빈 컬렉션val list: List[Int] = List()요소가 없어 추론 불가
함수 매개변수def greet(name: String)항상 명시 필요
재귀 함수 반환def fact(n: Int): Int = ...자기 참조로 추론 불가
API 문서화def process(data: Data): Result가독성 향상
// 1. 특정 타입을 원할 때
val longNum: Long = 42        // Int 대신 Long
val floatNum: Float = 3.14f   // Double 대신 Float

// 2. 빈 컬렉션
val emptyList: List[Int] = List()
val emptyMap: Map[String, Int] = Map()

// 3. 함수 매개변수 (항상 필요)
def greet(name: String): String = s"Hello, $name"

// 4. 재귀 함수의 반환 타입
def factorial(n: Int): Int =
  if (n <= 1) 1 else n * factorial(n - 1)
핵심 포인트: 타입 추론
  • 대부분의 경우 타입 생략 가능 — 컴파일러가 추론
  • 빈 컬렉션, 함수 매개변수, 재귀 함수에서는 명시 필요
  • 공개 API에서는 가독성을 위해 명시하는 것이 좋음

문자열#

Scala는 강력한 문자열 보간(String Interpolation) 기능을 제공합니다.

비유로 이해하기: 문자열 보간은 우편물 양식과 같습니다. “안녕하세요 ___님, 주문번호 ___의 배송이 완료되었습니다"처럼 빈칸만 채우면 완성되는 템플릿입니다. Java의 + 연결은 각 조각을 풀로 붙이는 것과 같고, Scala의 보간은 미리 디자인된 양식에 도장을 찍는 것과 같습니다.

Java vs Scala 비교

// Java: 문자열 연결
String msg = "Hello, " + name + "! You are " + age + " years old.";
String formatted = String.format("Pi is %.2f", pi);
// Scala: 문자열 보간
val msg = s"Hello, $name! You are $age years old."
val formatted = f"Pi is $pi%.2f"

s-보간: 변수 삽입

가장 기본적인 형태로, $ 기호로 변수를 삽입합니다.

val name = "Scala"
val version = 3

println(s"$name $version")           // Scala 3
println(s"${name.toUpperCase}")      // SCALA (표현식은 ${} 사용)
println(s"1 + 1 = ${1 + 1}")         // 1 + 1 = 2

f-보간: 포맷팅

printf 스타일의 포맷팅을 지원합니다.

val pi = 3.14159
val count = 42

println(f"pi = $pi%.2f")          // pi = 3.14 (소수점 2자리)
println(f"count = $count%05d")    // count = 00042 (5자리, 0 채움)
println(f"hex = $count%x")        // hex = 2a (16진수)

raw-보간: 이스케이프 무시

정규식이나 파일 경로에서 유용합니다.

println(raw"Hello\nWorld")  // Hello\nWorld (줄바꿈 안 됨)
println(s"Hello\nWorld")    // Hello
                            // World (줄바꿈 됨)

val regex = raw"\d+\.\d+"   // 이스케이프 없이 정규식 작성

여러 줄 문자열

삼중 따옴표(""")를 사용하면 여러 줄 문자열을 작성할 수 있습니다.

val sql = """
  SELECT *
  FROM users
  WHERE age > 18
"""

// stripMargin으로 앞쪽 공백 제거
val formatted = """
  |SELECT *
  |FROM users
  |WHERE age > 18
  """.stripMargin

stripMargin 메서드는 각 줄 앞의 | 문자까지를 제거합니다. 이를 통해 코드의 들여쓰기를 유지하면서 깔끔한 문자열을 만들 수 있습니다.

핵심 포인트: 문자열
  • s-보간: 변수 삽입 → s"Hello, $name"
  • f-보간: 포맷팅 → f"$price%.2f원"
  • raw-보간: 이스케이프 무시 → raw"\d+"
  • 삼중 따옴표: 여러 줄 → """...""".stripMargin

Scala 2 vs Scala 3 차이점#

대부분의 기본 문법은 동일합니다. 가장 큰 차이점은 들여쓰기 기반 구문과 진입점 정의 방식입니다.

기능Scala 2Scala 3
진입점object Main { def main(...) }@main def hello()
블록 구분중괄호 필수들여쓰기 기반 (선택)
와일드카드 importimport pkg._import pkg.*
타입 교차with&
타입 합집합없음`A

기본 문법

// 들여쓰기 기반 문법 (선택)
@main def hello() =
  val name = "World"
  println(s"Hello, $name!")

// 중괄호도 여전히 사용 가능
@main def hello2(): Unit = {
  val name = "World"
  println(s"Hello, $name!")
}
// 중괄호 필수
object Main {
  def main(args: Array[String]): Unit = {
    val name = "World"
    println(s"Hello, $name!")
  }
}

와일드카드 import

import scala.collection.mutable.*
import scala.collection.mutable._

흔한 실수와 해결책#

Scala 초보자들이 자주 하는 실수와 올바른 해결 방법입니다.

실수문제점해결책
var 남용상태 추적 어려움val + 함수형 연산 사용
null 사용NPE 위험Option 사용
타입 추론 과신Any로 추론됨복잡한 표현식은 타입 명시
Unit 무시의도치 않은 버그반환값 확인

피해야 할 것

// 1. 무분별한 var 사용
var list = List(1, 2, 3)
list = list :+ 4  // 매번 새 리스트 생성 - 비효율적!

// 2. null 사용
val name: String = null  // NullPointerException 위험!

// 3. 타입 추론에 과도한 의존
val x = if (condition) 1 else "error"  // Any로 추론됨 - 타입 안전성 손실

// 4. Unit을 반환하는 표현식 무시
val result = list.foreach(println)  // result는 Unit - 의도한 것인가?

올바른 방법

// 1. val과 불변 연산 사용
val list = List(1, 2, 3)
val newList = list :+ 4  // 새 리스트를 새 val에 할당

// 2. Option 사용
val name: Option[String] = None
name.foreach(n => println(s"Hello, $n"))  // 안전한 접근

// 3. 복잡한 표현식은 타입 명시
val x: Either[String, Int] = if (condition) Right(1) else Left("error")

// 4. Unit 반환 함수는 명확히 표시
def printAll(list: List[Int]): Unit = list.foreach(println)
Anti-Pattern 요약
  • var → ✅ val + 함수형 연산
  • null → ✅ Option[T]
  • Any 추론 → ✅ Either[L, R] 또는 타입 명시

연습 문제#

다음 연습 문제들을 통해 기본 문법을 복습해보세요.

1. 변수 선언

다음 코드의 출력 결과를 예측하세요.

val x = 10
var y = 20
y = y + x
println(s"x = $x, y = $y")
정답 보기
x = 10, y = 30

xval이므로 10으로 고정, yvar이므로 20 + 10 = 30으로 변경됩니다.

2. 타입 추론

다음 변수들의 타입을 추론하세요.

val a = 42
val b = 3.14
val c = "Hello"
val d = List(1, 2, 3)
val e = Map("a" -> 1, "b" -> 2)
정답 보기
  • a: Int
  • b: Double
  • c: String
  • d: List[Int]
  • e: Map[String, Int]

3. 문자열 보간

이름과 나이를 받아 “홍길동님은 25세입니다.” 형식으로 출력하는 코드를 작성하세요.

정답 보기
val name = "홍길동"
val age = 25
println(s"${name}님은 ${age}세입니다.")

관련 개념#

개념연관성설명
제어 구조다음 학습if, for, while도 표현식으로 값을 반환
함수와 메서드다음 학습함수 정의와 고급 기능
케이스 클래스불변성 적용val 철학을 데이터 모델에 적용
패턴 매칭타입 활용타입 계층을 활용한 분기 처리

다음 단계#

기본 문법을 익혔다면 다음 주제로 진행하세요.

추천 순서문서배우는 것
1제어 구조if, for, while, match 표현식
2함수와 메서드함수 정의와 고급 기능