본문 바로가기
Data Platform/Scala

Scala를 처음 접하는 초보자를 위한 입문 가이드_01

by 태하팍 2025. 3. 21.
반응형

스칼라 스터디

guide doc : https://docs.scala-lang.org/tour/tour-of-scala.html
scope : Introduction, Basic, Unified Types, Classes 

Introduction

Scala란 무엇인가?

Scala는 모던한 다중 패러다임 프로그래밍 언어로,
일반적인 프로그래밍 패턴을 간결하고 우아하며, 타입 안정성(type-safe)을 유지하면서 표현할 수 있도록 설계되었습니다.
Scala는 객체지향(Object-Oriented)과 함수형(Functional) 프로그래밍의 장점을 결합한 언어입니다.

Scala는 객체지향 언어다

Scala는 완전한 객체지향 언어입니다.
모든 값이 객체이며,
• 객체의 타입과 동작(behavior)은 클래스(Class)와 트레이트(Trait) 로 정의됩니다.

클래스 상속(subclassing) 및 믹스인(mixin) 조합을 활용하여 다중 상속 문제를 해결할 수 있습니다.

Scala는 함수형 언어다

Scala는 완전한 함수형 언어이기도 합니다.
함수도 값(value)으로 취급됩니다.
익명 함수(Anonymous Function)를 간결한 문법으로 정의할 수 있습니다.
고차 함수(Higher-Order Function) 지원 (함수를 인자로 전달하거나, 반환 가능)
함수 중첩(Nested Function) 및 커링(Currying) 지원
케이스 클래스(Case Class)와 패턴 매칭(Pattern Matching) 제공 → Algebraic Data Types처럼 사용 가능

싱글톤 객체(Singleton Object) 지원 → 클래스에 속하지 않는 함수들을 묶어서 관리 가능

Scala는 정적 타입 언어다 (Statically Typed)

Scala의 강력한 타입 시스템은 컴파일 단계에서 코드의 안정성을 보장합니다.
제네릭 클래스(Generic Class)
변형 어노테이션(Variance Annotation)
타입 상한(Upper Bound) 및 하한(Lower Bound) 지원
내부 클래스(Inner Class) 및 객체의 추상 타입(Abstract Type Members)
복합 타입(Compound Type)
명시적 자기 참조(Explicitly Typed Self Reference)
암시적 매개변수(Implicit Parameter) 및 변환(Implicit Conversion)
다형성 메서드(Polymorphic Methods)
Scala는 타입 추론(Type Inference) 을 지원하므로, 개발자가 불필요한 타입 선언을 생략할 수 있어 코드가 더 간결해집니다.
이러한 기능들을 결합하면 안전한 프로그래밍 추상화를 쉽게 재사용하고, 타입 안정성을 유지하면서 소프트웨어를 확장할 수 있습니다.

Scala는 확장성이 뛰어나다

도메인 특화 애플리케이션(Domain-Specific Applications)을 개발할 때,
Scala는 라이브러리 형태로 새로운 언어 기능을 쉽게 추가할 수 있습니다.
이를 위해 메타 프로그래밍(metaprogramming) 기능(예: 매크로) 없이도 확장할 수 있는 기능을 제공합니다.
예를 들면,

암시적 클래스(Implicit Class) 를 활용하여 기존 타입에 확장 메서드를 추가할 수 있음
문자열 보간(String Interpolation)을 커스텀 보간기(Custom Interpolator)로 확장 가능

Scala는 Java와 완벽하게 호환된다

Scala는 Java Runtime Environment(JRE) 와 원활하게 연동되도록 설계되었습니다.
특히, Java의 객체지향 패러다임과 자연스럽게 호환되도록 구현되었습니다.
Java의 SAM(Single Abstract Method), 람다(Lambda), 어노테이션(Annotation), 제네릭(Generic) 등의
   기능과 직접 연결
됩니다.
• Java에는 없는 기본값(default parameter), 명명된 매개변수(named parameter) 같은 기능들도
  Java와 최대한 가깝게 컴파일됩니다.
• Scala는 Java와 동일한 컴파일 모델(Separate Compilation, Dynamic Class Loading) 을 사용하며,
   기존 Java 라이브러리를 그대로 사용할 수 있음.

Basic

Scala Playground 제공!  

놀이터 이동!

Scala의 버전은 2와 3이 있습니다.
println

println(1) // 1
println(1 + 1) // 2
println("Hello!") // Hello!
println("Hello," + " world!") // Hello, world!

val keyword를 사용해서 표현식의 결과에 이름을 지정할 수 있습니다.

val x = 1 + 1
println(x) // 2

val은 아래처럼 재할당 불가능 합니다.

x = 3 // This does not compile.

값(val)의 타입은 생략할 수 있으며, 컴파일러가 추론(infer)할 수 있습니다.
또는 명시적으로 타입을 지정할 수도 있습니다.
명시적으로 타입을 지정할 땐느 변수명(x) 뒤에 : Int형태 입니다.

val x: Int = 1 + 1

var keyword를 사용하면 재할당이 가능 합니다.

var x = 1 + 1
x = 3 // This compiles because "x" is declared with the "var" keyword.
println(x * x) // 9

var 로 할당한 변수 또한 컴파일러가 추론(infer)할 수 있습니다.

var x: Int = 1 + 1

Blocks
여러 표현식을 {} 로 감싸서 하나로 묶을 수 있으며, 이를 블록(block) 이라고 합니다.
블록 내에서 마지막 표현식의 결과가 블록 전체의 결과가 됩니다.

println({
  val x = 1 + 1
  x + 1
}) // 3

Functions
함수(Function)는 매개변수(parameters)를 가지며, 인자(arguments)를 받는 표현식(expression) 입니다.
익명 함수(anonymous function, 즉 이름이 없는 함수)를 정의하여, 주어진 정수에 1을 더한 값을 반환할 수 있습니다.

(x: Int) => x + 1

=> 연산자의 왼쪽(left) 에는 매개변수 목록이 위치하며,
오른쪽(right) 에는 해당 매개변수를 사용하는 표현식이 옵니다.
또한, 함수에 이름을 지정할 수도 있습니다.

val addOne = (x: Int) => x + 1
println(addOne(1)) // 2

여러개의 파리미터를 받을수 도 있습니다.
(x: Int, y: Int)
(x와 y 매개변수) => x+y (x와y를 사용하는 표현식)

val add = (x: Int, y: Int) => x + y
println(add(1, 2)) // 3

혹은 파마미터가 없을 수도 있습니다.
=> 를 기준 왼쪽 ()안에 파라미터가 오는데 아래처럼 없을 수 있음!

val getTheAnswer = () => 42
println(getTheAnswer()) // 42

Methods

메소드(Method)는 함수(Function)와 매우 유사하게 동작하지만, 몇 가지 중요한 차이점이 있습니다.
메소드는 def 키워드를 사용하여 정의됩니다.
def 뒤에는 이름(name), 매개변수 목록(parameter list), 반환 타입(return type), 본문(body) 이 옵니다.
이름 : add
매개변수 목록 : x:Int, y: Int
반환타입 : Int
본문 : x+y

def add(x: Int, y: Int): Int = x + y
println(add(1, 2)) // 3

반환 타입 Int 는 매개변수 목록 뒤에 : (콜론)과 함께 선언된다는 점에 주목하세요.
메서드는 여러 개의 매개변수 목록(parameter lists)을 가질 수 있습니다.

def addThenMultiply(x: Int, y: Int)(multiplier: Int): Int = (x + y) * multiplier
println(addThenMultiply(1, 2)(3)) // 9

또는 매개변수 목록이 전혀 없는 경우

def name: String = System.getProperty("user.name")
println("Hello, " + name + "!")

메소드와 함수 사이에는 몇 가지 다른 점이 있지만, 일단은 메소드를 함수와 유사한 개념으로 이해하면 됩니다.
또한, 메소드는 여러 줄의 표현식(multi-line expressions)을 가질 수 있습니다.

def getSquareString(input: Double): String =
  val square = input * input
  square.toString

println(getSquareString(2.5)) // 6.25

메소드 본문에서 마지막 표현식의 결과가 메소드의 반환 값이 됩니다.
(Scala에는 return 키워드가 있지만, 거의 사용되지 않습니다.)

Classes

클래스는 class 키워드를 사용하여 정의하며, 이후 클래스 이름과 생성자 매개변수(constructor parameters) 가 옵니다.

class Greeter(prefix: String, suffix: String):
  def greet(name: String): Unit =
    println(prefix + name + suffix)

메소드 greet 의 반환 타입은 Unit 이며, 이는 반환할 의미 있는 값이 없음을 나타냅니다.
이는 Java와 C의 void 와 유사하게 사용됩니다.
(차이점: Scala에서는 모든 표현식이 값을 가져야 하므로, Unit 타입의 값으로 () 라는 싱글톤(singleton) 값이 존재합니다.
하지만, () 자체는 아무런 정보를 담고 있지 않습니다.)

 

클래스 인스턴스 생성 방법
Scala 2: new 키워드를 사용하여 클래스의 인스턴스를 생성
Scala 3: new 키워드 없이도 universal apply method 덕분에 객체를 생성 가능

val greeter = new Greeter("Hello, ", "!")
greeter.greet("Scala developer") // Hello, Scala developer!
val greeter = Greeter("Hello, ", "!")
greeter.greet("Scala developer") // Hello, Scala developer!

Case Classes

Scala에는 “case class” 라고 불리는 특별한 유형의 클래스가 있습니다.
• 기본적으로 불변(Immutable) 입니다.
• 일반 클래스와 달리, 참조(reference)가 아니라 값(value)으로 비교됩니다. 
• 패턴 매칭(pattern matching)에 유용합니다.

case class 키워드를 사용하여 케이스 클래스를 정의할 수 있습니다.

case class Point(x: Int, y: Int)

case class 는 new 키워드 없이 인스턴스를 생성할 수 있습니다.

val point = Point(1, 2)
val anotherPoint = Point(1, 2)
val yetAnotherPoint = Point(2, 2)

Case 클래스의 인스턴스는 참조(reference)가 아니라 값(value)으로 비교됩니다.
즉, 두 개의 case class 인스턴스가 같은 값을 가지면 동일한 것으로 간주됩니다.

if point == anotherPoint then
  println(s"$point and $anotherPoint are the same.")
else
  println(s"$point and $anotherPoint are different.")
// ==> Point(1,2) and Point(1,2) are the same.

if point == yetAnotherPoint then
  println(s"$point and $yetAnotherPoint are the same.")
else
  println(s"$point and $yetAnotherPoint are different.")
// ==> Point(1,2) and Point(2,2) are different.

Objects
객체(Object) 는 자신의 정의에서 단 하나만 존재하는 인스턴스입니다.
즉, 클래스의 싱글톤(Singleton) 형태로 생각할 수 있습니다.

object 키워드를 사용하여 객체를 정의할 수 있습니다.

object IdFactory:
  private var counter = 0
  def create(): Int =
    counter += 1
    counter

객체(Object)는 이름을 통해 직접 접근할 수 있습니다.

val newId: Int = IdFactory.create()
println(newId) // 1
val newerId: Int = IdFactory.create()
println(newerId) // 2

Traits
트레이트(Trait) 는 특정 필드와 메서드를 포함하는 추상 데이터 타입(Abstract Data Type) 입니다.
Scala에서 클래스는 하나의 클래스만 상속(extend)할 수 있지만,
여러 개의 트레이트를 확장(extend)할 수 있습니다.
trait 키워드를 사용하여 트레이트를 정의할 수 있습니다.

// 스칼라2
trait Greeter {
  def greet(name: String): Unit
}

// 스칼라3
trait Greeter:
  def greet(name: String): Unit

트레이트(Trait)에는 기본 구현(Default Implementation) 을 포함할 수도 있습니다.

// 스칼라2
trait Greeter {
  def greet(name: String): Unit =
    println("Hello, " + name + "!")
}

// 스칼라3
trait Greeter:
  def greet(name: String): Unit =
    println("Hello, " + name + "!")

트레이트(Trait)는 extends 키워드를 사용하여 확장할 수 있으며,
기본 구현을 재정의할 때는 override 키워드를 사용합니다.

// 스칼라2
class DefaultGreeter extends Greeter

class CustomizableGreeter(prefix: String, postfix: String) extends Greeter {
  override def greet(name: String): Unit = {
    println(prefix + name + postfix)
  }
}

val greeter = new DefaultGreeter()
greeter.greet("Scala developer") // Hello, Scala developer!

val customGreeter = new CustomizableGreeter("How are you, ", "?")
customGreeter.greet("Scala developer") // How are you, Scala developer?

// 스칼라3
class DefaultGreeter extends Greeter

class CustomizableGreeter(prefix: String, postfix: String) extends Greeter:
  override def greet(name: String): Unit =
    println(prefix + name + postfix)

val greeter = DefaultGreeter()
greeter.greet("Scala developer") // Hello, Scala developer!

val customGreeter = CustomizableGreeter("How are you, ", "?")
customGreeter.greet("Scala developer") // How are you, Scala developer?

Program Entry Point
메인 메서드(Main Method) 는 Scala 프로그램의 진입점(entry point) 입니다.
Java 가상 머신(JVM)은 main 이라는 이름의 메서드를 필요로 하며,

이 메서드는 하나의 매개변수로 문자열 배열(Array of Strings) 을 받아야 합니다.

// 스칼라2
object Main {
  def main(args: Array[String]): Unit =
    println("Hello, Scala developer!")
}


// 스칼라3
@main def hello() = println("Hello, Scala developer!")

Unified Types

Scala에서는 모든 값(value) 이 타입(type) 을 갖습니다.
이는 숫자 값(numerical values) 뿐만 아니라 함수(functions)도 포함됩니다.

아래 다이어그램은 타입 계층 구조(Type Hierarchy)의 일부를 보여줍니다.

 

Scala 타입 계층 구조(Type Hierarchy)

• Any 는 모든 타입의 최상위(supertype) 타입이며, 흔히 “Top Type” 이라고 불립니다.
이 타입은 equals, hashCode, toString 같은 공통 메서드들을 정의합니다.

Any 는 두 개의 직접적인 하위 타입을 가집니다: AnyVal 과 AnyRef

AnyVal – 값 타입(Value Types)
• AnyVal 은 값 타입을 나타냅니다.
• 총 9가지 기본 값 타입이 미리 정의되어 있으며, 이들은 null이 될 수 없습니다(non-nullable):
Double, Float, Long, Int, Short, Byte, Char, Unit, Boolean

• Unit 은 의미 있는 정보를 가지지 않는 특수한 값 타입입니다.
• Unit 타입은 정확히 하나의 인스턴스를 가지며, 이는 ()  로 표현됩니다.
• Scala의 모든 함수는 반환값이 반드시 있어야 하므로, 반환할 값이 없을 때 Unit 타입이 유용하게 사용됩니다.

 AnyRef – 참조 타입(Reference Types)
• AnyRef 는 참조 타입을 나타냅니다.
• 값 타입이 아닌 모든 타입은 참조 타입으로 분류됩니다.
• Scala에서 사용자가 정의한 모든 클래스/타입은 AnyRef의 하위 타입입니다.
• Java 환경에서 Scala를 사용할 경우, AnyRef 는 java.lang.Object 와 대응됩니다.

다음은 문자열(String), 정수(Integer), 문자(Character), 불리언(Boolean), 함수(Function) 등이
다른 모든 객체들과 마찬가지로 모두 Any 타입의 하위 타입임을 보여주는 예시입니다.

val list: List[Any] = List(
  "a string",
  732,  // an integer
  'c',  // a character
  true, // a boolean value
  () => "an anonymous function returning a string"
)

list.foreach(element => println(element))

이 예제에서는 List[Any] 타입의 값인 list를 정의합니다.
이 리스트는 다양한 타입의 요소들로 초기화되었지만, 각 요소는 모두 scala.Any의 인스턴스이기 때문에

같은 리스트에 함께 추가할 수 있습니다.

결과

a string
732
c
true
<function>

Type Casting

값 타입(Value types)은 다음과 같은 방식으로 형 변환(casting) 할 수 있습니다:

  Long을 Float으로 변환하는 것은 정밀도 손실(Potential precision loss) 이 발생할 수 있기 때문에,
최신 Scala 버전에서는 사용이 권장되지 않으며(deprecated) 주의가 필요합니다.

val x: Long = 987654321
val y: Float = x.toFloat  // 9.8765434E8 (note that some precision is lost in this case)

val face: Char = '☺'
val number: Int = face  // 9786

형 변환(Casting)은 단방향(unidirectional) 입니다.
따라서 역방향으로 변환하려고 하면 컴파일되지 않습니다.

val x: Long = 987654321
val y: Float = x.toFloat  // 9.8765434E8
val z: Long = y  // Does not conform

 

Nothing and Null

 Nothing
• Nothing은 모든 타입의 하위 타입으로, 흔히 “Bottom Type(최하위 타입)” 라고도 불립니다.
•Nothing 타입을 갖는 값은 존재하지 않습니다.

•주로 정상적으로 값을 반환하지 않는 상황을 나타내는 데 사용됩니다.
예를 들어:
• 예외가 던져졌을 때 (throw new Exception)
• 프로그램이 종료될 때 (sys.exit())

• 무한 루프에 빠졌을 때 (while(true){...})
즉, Nothing은 값이 존재하지 않는 표현식의 타입이며,
정상적으로 반환되지 않는 메서드의 반환 타입으로도 사용됩니다.

 Null
• Null은 모든 참조 타입(AnyRef의 하위 타입) 의 하위 타입입니다.
• 단 하나의 값만 가지며, 리터럴 null 로 표현됩니다.

• JVM의 다른 언어(Java 등)와의 호환성을 위해 제공되는 타입이며,
Scala 코드에서는 거의 사용하지 않는 것이 권장됩니다.

(※ null 대신 사용할 수 있는 안전한 대안은 이후 투어에서 다룰 예정입니다.)

Classes

Scala에서 클래스(Class)객체를 생성하기 위한 설계도(blueprint) 입니다.
클래스는 다음과 같은 구성 요소들을 포함할 수 있습니다:
메서드(methods)
값(values)
변수(variables)
타입(types)
객체(objects)
트레이트(traits)
다른 클래스(classes)
이러한 요소들을 통틀어 멤버(members) 라고 부릅니다.
(※ types, objects, traits 는 이 투어의 이후에 자세히 다룰 예정입니다.)

Defining a class

가장 간단한 클래스 정의는 class 키워드와 식별자(identifier) 만으로 이루어집니다.
클래스 이름은 대문자로 시작하는 것이 관례입니다.

// 스칼라2
class User

val user1 = new User


// 스칼라3
class User

val user1 = User()

클래스의 인스턴스를 만들 때는 User()처럼 함수처럼 호출합니다.
new User()처럼 new 키워드를 명시적으로 사용하는 것도 가능하지만,
일반적으로는 생략하는 경우가 많습니다. 스칼라3에서는 생략 가능!

User 클래스는 생성자가 따로 정의되지 않았기 때문에,
인자를 받지 않는 기본 생성자(default constructor) 를 자동으로 가집니다.
하지만 실제로는 생성자와 클래스 본문이 필요한 경우가 많습니다.

다음은 Point 클래스를 정의하는 예시입니다:

// 스칼라2
class Point(var x: Int, var y: Int) {

  def move(dx: Int, dy: Int): Unit = {
    x = x + dx
    y = y + dy
  }

  override def toString: String =
    s"($x, $y)"
}

val point1 = new Point(2, 3)
println(point1.x)  // prints 2
println(point1)    // prints (2, 3)


// 스칼라3
class Point(var x: Int, var y: Int):

  def move(dx: Int, dy: Int): Unit =
    x = x + dx
    y = y + dy

  override def toString: String =
    s"($x, $y)"
end Point

val point1 = Point(2, 3)
println(point1.x)  // prints 2
println(point1)    // prints (2, 3)

Point 클래스에는 총 네 개의 멤버가 있습니다:
• 변수 x와 y
• 메서드 move
• 메서드 toString

다른 많은 언어들과 달리, Scala에서는 주 생성자(primary constructor)가
클래스 선언부에 직접 포함되어 있습니다. → 예: (var x: Int, var y: Int)

move 메소드는 정수 두 개를 인자로 받아서 x와 y를 이동시킵니다.
반환값은 Unit 타입이며, 이는 정보가 없는 값 ()을 의미합니다.
→ Java 계열 언어의 void 와 유사합니다.

toString 메소드는 인자를 받지 않고, 문자열 String 값을 반환합니다.
이 메서드는 상위 타입인 AnyRef의 toString을 오버라이드 하기 때문에
override 키워드를 붙여야 합니다.

Constructors

생성자(Constructor)는 기본값(default value) 을 지정함으로써 선택적(optional) 매개변수를 가질 수 있습니다.
예를 들면 다음과 같이 작성할 수 있습니다:

// 스칼라2
class Point(var x: Int = 0, var y: Int = 0)

val origin = new Point    // x and y are both set to 0
val point1 = new Point(1) // x is set to 1 and y is set to 0
println(point1)           // prints (1, 0)

// 스칼라3
class Point(var x: Int = 0, var y: Int = 0)

val origin = Point()  // x and y are both set to 0
val point1 = Point(1) // x is set to 1 and y is set to 0
println(point1)       // prints (1, 0)

이 버전의 Point 클래스에서는 x와 y가 기본값 0을 가지므로,
인자를 생략하고도 인스턴스를 생성할 수 있습니다.
하지만 생성자는 왼쪽에서 오른쪽으로 인자를 읽기 때문에,
만약 y 값만 전달하고 싶다면, 해당 매개변수의 이름을 명시해야 합니다.

// 스칼라2
class Point(var x: Int = 0, var y: Int = 0)
val point2 = new Point(y = 2)
println(point2)               // prints (0, 2)

// 스칼라3
class Point(var x: Int = 0, var y: Int = 0)
val point2 = Point(y = 2)
println(point2)           // prints (0, 2)

이처럼 매개변수 이름을 명시하는 방식은 코드의 명확성을 높이는 좋은 습관입니다.

Private Members and Getter/Setter Syntax

멤버(변수, 메서드 등)는 기본적으로 public(공개) 입니다.
외부에서 접근하지 못하도록 하려면,
private 접근 제어자(access modifier) 를 사용하여 숨길 수 있습니다.

// 스칼라2
class Point {
  private var _x = 0
  private var _y = 0
  private val bound = 100

  def x: Int = _x
  def x_=(newValue: Int): Unit = {
    if (newValue < bound)
      _x = newValue
    else
      printWarning()
  }

  def y: Int = _y
  def y_=(newValue: Int): Unit = {
    if (newValue < bound)
      _y = newValue
    else
      printWarning()
  }

  private def printWarning(): Unit =
    println("WARNING: Out of bounds")
}

val point1 = new Point
point1.x = 99
point1.y = 101 // prints the warning

// 스칼라3
class Point:
  private var _x = 0
  private var _y = 0
  private val bound = 100

  def x: Int = _x
  def x_=(newValue: Int): Unit =
    if newValue < bound then
      _x = newValue
    else
      printWarning()

  def y: Int = _y
  def y_=(newValue: Int): Unit =
    if newValue < bound then
      _y = newValue
    else
      printWarning()

  private def printWarning(): Unit =
    println("WARNING: Out of bounds")
end Point

val point1 = Point()
point1.x = 99
point1.y = 101 // prints the warning

이 버전의 Point 클래스에서는 데이터를 _x와 _y라는 private 변수에 저장합니다.
그리고 해당 값을 외부에서 읽을 수 있도록 def x와 def y 메서드가 제공됩니다.
값을 설정할 때는 def x_=와 def y_= 메서드를 사용하며,
이들은 값을 검증하고 설정하는 역할을 합니다.
여기서 중요한 점은 setter 메서드의 특별한 문법(syntax) 입니다:
• getter 이름 뒤에 _=를 붙여 정의

• 매개변수는 메서드 이름 뒤에 소괄호 없이 작성

// 스칼라2
class Point(val x: Int, val y: Int)
val point = new Point(1, 2)
point.x = 3  // <-- does not compile

// 스칼라3
class Point(val x: Int, val y: Int)
val point = Point(1, 2)
point.x = 3  // <-- does not compile

val이나 var 없이 선언된 생성자 매개변수는 private 값으로, 클래스 내부에서만 접근 가능합니다.

// 스칼라2
class Point(x: Int, y: Int)
val point = new Point(1, 2)
point.x  // <-- does not compile

// 스칼라3
class Point(x: Int, y: Int)
val point = Point(1, 2)
point.x  // <-- does not compile
반응형