제어 구조

제어 구조 (Control Structures)

조건 분기, 반복, 패턴 매칭 같은 제어 구조는 어떤 언어를 배우든 가장 먼저 만나는 주제예요. Scala 3에는 다른 언어에서 흔히 보는 제어 구조들이 있는데, 특히 for 표현식과 match 표현식이 매우 강력해요. 그리고 결정적으로, Scala의 제어 구조는 전부 *식(expression)*으로 동작한다는 점이 핵심이에요.

출처: Scala 3 Book

본문

Scala에는 다른 프로그래밍 언어에서 볼 수 있는 제어 구조들이 있고, 강력한 for 표현식과 match 표현식도 있어요:

  • if/else

  • for 루프와 표현식

  • match 표현식

  • while 루프

  • try/catch

이 구조들을 아래 예시로 하나씩 살펴볼게요.

if/else

Scala의 if/else 제어 구조는 다른 언어들과 비슷하게 생겼어요.

if x < 0 then
  println("negative")
else if x == 0 then
  println("zero")
else
  println("positive")

여기서 중요한 점은, 이것이 정말로 *식(expression)*이지 *문(statement)*이 아니라는 점이에요. 즉 값을 반환하므로, 결과를 변수에 할당할 수 있어요:

val x = if a < b then a else b

이 책 전체에서 보게 되겠지만, Scala의 모든 제어 구조는 식으로 사용할 수 있어요.

식(expression)은 결과를 반환하지만, 문(statement)은 그렇지 않아요. 문은 보통 println을 사용해 콘솔에 출력하는 것 같은 부수 효과를 위해 사용돼요.

for 루프와 표현식

for 키워드는 for 루프를 만드는 데 사용돼요. 이 예시는 List의 모든 요소를 출력하는 방법을 보여줘요:

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

for i <- ints do println(i)

i <- ints라는 코드는 generator라고 불러요. 모든 generator p <- e에서 식 e는 패턴 p에 대한 바인딩을 0개 또는 여러 개 만들어낼 수 있어요. generator의 닫는 괄호(여기서는 do) 뒤에 오는 코드가 루프의 body예요.

Guards

for 루프 안에서 if 표현식을 하나 이상 사용할 수도 있어요. 이것을 guards라고 해요. 이 예시는 ints에서 2보다 큰 모든 숫자를 출력해요:

for
  i <- ints
  if i > 2
do
  println(i)

여러 개의 generator와 guard를 사용할 수도 있어요. 이 루프는 숫자 1부터 3을 반복하면서 각 숫자에 대해 문자 a부터 c도 반복해요. 그런데 guard가 두 개 있어서, i2이고 j가 문자 b일 때만 출력문이 호출돼요:

for
  i <- 1 to 3
  j <- 'a' to 'c'
  if i == 2
  if j == 'b'
do
  println(s"i = $i, j = $j")   // prints: "i = 2, j = b"

for 표현식

for 키워드는 훨씬 더 강력해요. do 대신 yield 키워드를 사용하면, 결과를 계산하고 산출(yield)하는 for 표현식을 만들게 돼요.

몇 가지 예시로 확인해 볼게요. 앞선 예시와 같은 ints 리스트를 사용해, 이 코드는 새 리스트를 만들어요. 새 리스트의 각 요소 값은 원본 리스트 요소 값의 두 배예요:

scala> val doubles = for i <- ints yield i * 2
val doubles: List[Int] = List(2, 4, 6, 8, 10)

Scala의 제어 구조 문법은 유연해서, 그 for 표현식은 취향에 따라 여러 다른 방식으로도 쓸 수 있어요:

val doubles = for i <- ints yield i * 2     // style shown above
val doubles = for (i <- ints) yield i * 2
val doubles = for (i <- ints) yield (i * 2)
val doubles = for { i <- ints } yield (i * 2)

이 예시는 리스트에 있는 각 문자열의 첫 글자를 대문자로 바꾸는 방법을 보여줘요:

val names = List("chris", "ed", "maurice")
val capNames = for name <- names yield name.capitalize

마지막으로, 이 for 표현식은 문자열 리스트를 반복하면서 각 문자열의 길이를 반환하는데, 길이가 4보다 큰 경우에만 그렇게 해요:

val fruits = List("apple", "banana", "lime", "orange")

val fruitLengths = for
  f <- fruits
  if f.length > 4
yield
  // you can use multiple lines
  // of code here
  f.length

// fruitLengths: List[Int] = List(5, 6, 6)

for 루프와 표현식은 이 책의 Control Structures 섹션참조 문서에서 더 자세히 다뤄져요.

match 표현식

Scala에는 match 표현식이 있어요. 가장 기본적인 사용은 Java의 switch 문과 비슷해요:

val i = 1

// later in the code ...
i match
  case 1 => println("one")
  case 2 => println("two")
  case _ => println("other")

하지만 match는 정말로 *식(expression)*이에요. 즉 패턴 매칭에 기반한 결과를 반환하고, 그 결과를 변수에 바인딩할 수 있다는 뜻이에요:

val result = i match
  case 1 => "one"
  case 2 => "two"
  case _ => "other"

match는 정수 값에만 국한되지 않고 어떤 데이터 타입에도 사용할 수 있어요:

val p = Person("Fred")

// later in the code
p match
  case Person(name) if name == "Fred" =>
    println(s"$name says, Yubba dubba doo")

  case Person(name) if name == "Bam Bam" =>
    println(s"$name says, Bam bam!")

  case _ => println("Watch the Flintstones!")

실제로 match 표현식은 변수를 아주 다양한 종류의 패턴에 대해 테스트하는 데 쓸 수 있어요. 이 예시는 (a) match 표현식을 메서드의 본문으로 사용하는 방법을, (b) 보여지는 모든 타입을 매칭하는 방법을 보여줘요:

// getClassAsString is a method that takes a single argument of any type.
def getClassAsString(x: Matchable): String = x match
  case s: String => s"'$s' is a String"
  case i: Int => "Int"
  case d: Double => "Double"
  case l: List[?] => "List"
  case _ => "Unknown"

// examples
getClassAsString(1)               // Int
getClassAsString("hello")         // 'hello' is a String
getClassAsString(List(1, 2, 3))   // List

getClassAsString 메서드는 Matchable 타입의 값을 매개변수로 받아요. Matchable은 패턴 매칭을 지원하는 어떤 타입이든 될 수 있어요(일부 타입은 캡슐화를 깨뜨릴 수 있기 때문에 패턴 매칭을 지원하지 않아요).

Scala의 패턴 매칭에는 훨씬 많은 것들이 있어요. 패턴은 중첩될 수 있고, 패턴의 결과는 바인딩될 수 있으며, 패턴 매칭은 심지어 사용자가 정의할 수도 있어요. 자세한 내용은 이 책의 Control Structures 챕터에 있는 패턴 매칭 예시를 참고하세요.

try/catch/finally

Scala의 try/catch/finally 제어 구조는 예외를 잡을 수 있게 해줘요. Java와 비슷하지만, 문법이 match 표현식과 일관돼요:

try
  writeTextToFile(text)
catch
  case ioe: IOException => println("Got an IOException.")
  case nfe: NumberFormatException => println("Got a NumberFormatException.")
finally
  println("Clean up your resources here.")

while 루프

Scala에는 while 루프 구조도 있어요. 한 줄 문법은 이렇게 생겼어요:

while x >= 0 do x = f(x)

Scala 3는 호환성을 위해 Scala 2 문법도 여전히 지원해요.

while 루프의 여러 줄 문법은 이렇게 생겼어요:

var x = 1

while
  x < 3
do
  println(x)
  x += 1

커스텀 제어 구조

by-name 매개변수, 중위 표기법(infix notation), 플루언트 인터페이스(fluent interface), 선택적 괄호, 확장 메서드(extension methods), 고차 함수 같은 기능 덕분에, 마치 제어 구조처럼 동작하는 여러분만의 코드를 만들 수도 있어요. 이에 대해서는 Control Structures 섹션에서 더 배울 수 있어요.

더 알아보기