[Scala] 스칼라 시작하기

스칼라 구조 및 특징 / method, function, class, trait, 싱글턴 오브젝트(object)

Posted by Wonyong Jang on February 24, 2021 · 21 mins read

이 글에서는 스칼라를 처음 시작하기 위해서 기본적인 기능들에 대해 간략하게 요약한 내용을 다루려고 합니다.

스칼라란?

스칼라는 일반적인 프로그래밍 패턴을 간결하고 type-safe한 방식으로 표현할 수 있게 설계된 프로그래밍 언어이다.

type-safe하다는 말은 병렬 프로그래밍 처리에 강하다는 말이며, 그 이유는 변경 불가능한 Immutable변수를 많이 가지고 있기 때문이다.

또한, 스칼라는 아래와 같은 특징을 가진다.

JVM 위에서 동작한다.

스칼라는 소스코드를 컴파일 하면 그 결과로 JVM위에서 동작하는 바이트 코드가 나온다.
즉, JVM위에서 위에서 동작한다.

스크린샷 2021-02-25 오후 9 10 32

이것은 두 가지 중요한 의미를 갖는다. 하나는 컴파일 된 스칼라 코드는 이미 10년 이상 최적화가 되어 있는 JVM 상에서 동작하기 때문에, 성능과 안정성을 어느정도 보장한다.

또, 기존에 Java로 작성된 소스와 호환이 되기 때문에, 엄청나게 많은 Java 라이브러리들을 Scala에서 그대로 사용할 수 있다.

import java.util.{Date, Locale}
import java.text.DateFormat._

object Test {
  def main(args: Array[String]): Unit = {

    val now = new Date();
    val df = getDateInstance(0,Locale.KOREA) // DateFormat.getDateInstance(..)
    println(df format now)
    // 출력 : 2021년 2월 25일 (목)    
  }
}

스칼라의 import 구문은 Java와 매우 비슷해 보이지만 좀 더 강력하다. 위 예제의 첫번째 줄과 같이 중괄호를 사용하면 같은 패키지에서 여러개의 클래스를 선택적으로 불러 올 수 있다. 즉 배열처럼 한번에 표현이 가능하다.

또한가지 특징은 패키지나 클래스에 속한 모든 이름들을 불러 올 경우 별표( * ) 대신 ( _ ) 을 사용 한다는 것이다.

마지막으로 스칼라에서 println을 할때 재밌는 표현을 볼 수 있다. 오직 하나의 인자를 갖는 함수는 마치 이항연산자 같은 문법으로 호출이 가능하다.

df format now   // 스칼라에서 표현 가능 ! 
df.format(now)  // 같은 표현     

Functional, object-oriented language

스칼라는 람다를 비롯하여 여러 functional language 문법을 지원하여 자바에 비하여 코드 길이가 짧다. getter, setter, 생성자를 제거하고, 표현식을 간소화하여 자바대비 짧은 코드로 동일한 내용을 작성할 수 있다.

모든 값이 객체이며, 동시에 object-oriented적인 요소도 지원한다.

모든 것이 객체이다.

스칼라는 순수한 객제지향적 언어이다. 이 말은 숫자와 함수를 포함한 모든 것이 객체라는 것이다. 이러한 면에서 스칼라는 Java와 다르다.

함수 또한 객체이다. 따라서, 함수에 함수를 인자로 넘기거나 함수를 변수에 저장하거나 함수가 함수를 리턴하는 것도 가능하다. 이처럼 함수를 값과 동일하게 다루는 것은 함수형 프로그래밍의 핵심 요소 중 하나이다.


1.표현식

표현식은 연산 가능한 명령문이다. println 표현식을 사용해 결과를 출력할 수 있다.
즉, 평가(Evaluation)을 통해서 값을 바뀌는 식, 혹은 결과값이 있는 식을 뜻한다.

object Expression extends App {
  println({
    5 * 7
    4 + 3
  })
}

위에서 숫자값들이 모드 표현식이다. 이 표현식은 여러 줄로 쓸 수도 있고 println()과 같은 함수를 실행할 수도 있다.
이 표현식이 결과는 언제나 제일 마지막 표현식의 평가 결과와 같다.
즉, 위 예제의 경우는 4 + 3 의 값인 7이 전체 표현식의 값이다.

val 키워드로 표현식의 결과에 이름을 붙인다.
x 같이 이름이 붙여진 결과를 값이라고 부른다. 참조된 값은 재연산하지 않으며 값을 재할당 할 수 없다.

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

x = 3  // This does not compile.   

값의 타입을 추론할 수 있지만 명시적으로 타입을 지정할 수도 있다.

val x: Int = 1+1

변수

변수는 재할당이 가능한 것 이외에는 값과 같다 var 키워드로 변수를 정의한다.

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

2. 함수(function)

함수는 parameter를 가지는 표현식이다.
=> 을 기준으로 왼쪽에는 매개변수 목록이고 오른쪽은 매개변수를 포함한 표현식이다.

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

또는 매개변수를 가지지 않을 수도 있다.

val fun = () => 45
print(fun())   // 45

3. 메소드(method)

메소드는 함수와 비슷하게 보이고 동작하는거 같지만 몇 가지 중요한 차이가 있다.
def 키워드로 메소드를 정의하고 이름, 매개변수 목록, 반환 타입 그리고 본문이 뒤따른다.

def add(x: Int, y: Int): Int = x + y
print(add(5,4))   // 9 

메소드는 여러 매개변수 목록을 가질 수 있다.

def add(x: Int, y: Int)(z: Int): Int = (x + y) * z
print(add(5,4)(2))

매개변수 목록을 가지지 않을 수도 있으며, 하나의 표현식만 계산하는 경우 {} 중괄호를 없앨 수 있다.

def name: String = "call"
print(name)   // call

메소드는 여러 줄의 표현식을 가질 수 있다.

def getSquareString(input: Double): String = {
  val square = input * input
  square.toString
}
println(getSquareString(2.5)) // 6.25

본문의 마지막 표현식은 메소드의 반환 값이다 (스칼라는 return 키워드가 있지만 거의 사용하지 않고 생략한다.)

함수 VS 메소드

스칼라에서는 함수와 메소드를 구분하여 사용하는데 차이점에 대해 알아 보자.

다음과 같은 스칼라 클래스 파일이 있고, foo()라는 메소드가 정의되어 있다.

class Foo {
  def foo() = "foo"   // method
}

또한 다음과 같은 클래스 파일이 있고, bar라는 함수를 정의 했다.

class Bar {
  val bar = () => "bar" // function
}

스칼라에서 메소드는 클래스의 멤버 로써 존재하는 것이고, 함수는 독립적인 객체처럼 취급된다.

다른 예제로 메서드의 파라미터로 함수가 들어온 예제를 볼 수 있다.
아래 예제는 doSomething이라는 메서드안의 파라미터로 f 함수가 들어온다는 것이고 f함수는 매개변수 없이 int 타입의 값을 리턴한다고 이해하면 된다.

def doSomething(a: Int, f: () => Int) : Int = {
    a + f()
  }


val result = doSomething(1, () => 3)
println(result) // 출력 : 4

f함수가 매개변수를 하나 넣어야 된다면 아래와 같이 구현해 볼 수 있다.

def doSomething(a: Int, f: Int => Int) : Int = {
    f(a)
  }


val result = doSomething(3, (x) => x*x)
println(result) // 출력 : 9

4. 클래스

class 키워드로 클래스를 정의하고 이름과 생성자 매개변수가 뒤따른다.

class Greeter(prefix: String, suffix: String) {
  def greet(name: String): Unit = {
    print(prefix + name + suffix)
  }
}

val greeter = new Greeter("Hello, ", "!") // new 키워드로 클래스의 인스턴스를 생성    
greeter.greet("Scala developer") // Hello, Scala developer!

greet 메소드의 반환 타입은 Unit 으로 자바의 void와 유사하다.

또한, 생성자의 경우는 default value를 지정해줄 수 있다. 아래 예제를 살펴보자.

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: 1, y: 0   

val point2 = new Point(y=2)  // x:0, y:2

private Members and Getter/Setter Syntax

아래 Point 클래스의 x, y는 0으로 초기값을 설정해주고 외부에서 사용하지 못하도록 private으로 지정하였다.
bound도 역시 private으로 설정하였고 Point클래스를 인스턴스화 하여 x, y의 값을 수정할 때 bound 값과 비교하여 검사를 하게 된다.

여기서 눈여겨 봐야할 문법은 메서드 뒤에 '_='을 붙여서 사용하게 되면 setter를 나타나게 된다.

class Point {
  private var _x = 0
  private var _y = 0
  private var bound = 100

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

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

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

object Main extends App {

  val point = new Point
  point.x = 99  // 99로 setter   

  point.y = 101  // print: Warning: Out of bounds   
}

5. 케이스 클래스

스칼라는 케이스 클래스라고 불리는 특별한 타입의 클래스를 가지고 있다.

케이스 클래스의 멤버변수는 기본적으로 불변 변수로 선언되며 값으로 비교한다.

케이스 클래스 멤버 변수를 var로 선언하여 사용할 수도 있지만 케이스 클래스 자체가 불변 객체를 사용하기 위함이므로 권장되진 않는다.

case class 키워드로 케이스 클래스를 정의하며 일반 클래스와 달리 인스턴스를 생성할 때 new를 사용하지 않는다.

// 케이스 클래스 선언    
case class Point(x: Int, y: Int)
val test1 = Point(1,1)
val test2 = Point(1,2)
val test3 = Point(1,2)

if(test1 != test2) println("1과 2는 다르다")
if(test2 == test3) println("2와 3 같다")

또한, 케이스 클래스는 컴파일러가 toString, hashCode, equals를 자동으로 생성해 준다.

케이스 클래스 생성자의 파라미터는 public으로 취급되며 직접 접근이 가능하다. 케이스 클래스에서는 필드를 직접 변경할 수 없지만 대신에 copy 메소드를 통해 새로운 객체를 생성할 수 있다.

val test = Point(1,1)
val test2 = test.copy(1,2)  // shallow copy

케이스 클래스에서는 패턴 매칭을 활용해 데이터를 처리 할 수 있고 아래와 같이 if 문을 같이 사용도 가능하다.

val p = Point(1,2)

p match {
   case Point(_,y) if y == 1 => print("success")
   case _ => println("not valid")
}

기본적인 패턴매칭은 일반적인 클래스에선 동작하지 않는다.
스칼라 프로그래밍에 있어서 케이스 클래스는 유지보수 가능한 코드를 작성할 때 도움이 된다.


6. Object (싱글턴 오브젝트)

스칼라와 자바의 가장 큰 차이점 중 하나는 스칼라는 정적 멤버가 없다는 것이다. 대신 전용으로 사용할 수 있는 싱글톤 객체를 제공한다.

하나 이상의 인스턴스를 가질 수 없는 형태의 클래스이다.
object 키워드로 객체를 정의하며 클래스 이름으로 객체에 접근한다.

class와 object의 큰 차이점 중 하나는 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

7. 트레이트(trait)

트레이트는 자바의 인터페이스와 유사하다. 메소드를 정의만 해놓을 수도 있고, 기본 구현을 할 수도 있다.

추상 클래스와 달리 생성자 파라미터는 가질 수 없다. 생성자 파라미터를 사용하면 아래와 같이 컴파일 에러를 발생 시킨다.

아래와 같이 자바에서 이용할 코드이거나, 생성자 argument를 요구하는 base class를 만드는 경우에는 추상 클래스를 사용하고 그렇지 않다면 trait 사용하는 것을 권장한다.

trait Manager(val name: String) { // 컴파일 에러
  ...
}

또한 가변 변수, 불변 변수 모두 선언 가능하고 트레이트를 구현하는 클래스에서 가변 변수는 수정이 가능하지만 불변 변수는 수정할 수 없다.

트레이트의 기본 메서드는 상속되고, override 키워드를 이용하여 메서드를 재정의 할 수도 있다.

extends로 상속하고 여러개의 트레잇을 with 키워드로 동시에 구현할 수 있다.

트레이트는 특정 필드와 메소드를 가지는 타입이고 다양한 트레이트와 결합할 수 있다.

trait Greeter {
  def greet(name: String): Unit
}

또한 트레이트는 기본 구현도 가질 수 있다.

trait Greeter {
  def greet(name: String): Unit =
    println("Hello, " + name + "!")
}

extends 키워드로 트레이트를 상속할 수 있고 override 키워드로 구현을 오버라이드 할 수 있다.

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?

DefaultGreeter는 트레이트 하나만 상속하고 있지만 다중 상속도 가능하다.
즉, 하나의 클래스는 여러개의 trait를 상속 가능하지만 추상 클래스는 하나만 상속 가능하다.


8. 메인 메소드

메인 메소드는 프로그램의 진입 지점이다. JVM에선 main 이라는 메인 메소드가 필요하며 문자열 배열 하나를 인자(argument)로 가진다.

object 키워드를 사용하여 메인 메소드를 정의할 수 있다.

자바 프로그래머들에게 object 는 어색할 수 있다.
이 선언은 싱글턴 객체를 생성하는데, 이는 하나의 인스턴스만을 가지는 클래스라 할 수 있다.

따라서 아래 object 선언은 Hello 라는 클래스와 인스턴스를 함께 정의 하는 것이다.

또한, 자바와는 다르게 main이 static이 아니다. 스칼라에서는 static 키워드가 없다. 따라서, 스칼라에서 정적 멤버(함수든 필드든)를 만들기 위해서는 싱글턴 객체(object) 안에 선언한다.

object Hello {
  def main(args: Array[String]): Unit =
    println("Hello, Scala developer!")
}

Reference

https://groups.google.com/g/scala-korea/c/dfkcfM5yM9M
https://docs.scala-lang.org/ko/tour/tour-of-scala.html
https://docs.scala-lang.org/ko/tutorials/scala-for-java-programmers.html#%EC%8B%9C%EC%9E%91%ED%95%98%EB%A9%B4%EC%84%9C