[Scala] 스칼라 프로그래밍 심화

 

1. 객체 지향 프로그래밍 (OOP)

 

1.1 클래스 (Class)

  • Scala는 객체지향 프로그래밍을 지원하기 위해 클래스를 제공합니다. 타 언어와 비교했을 때 더 간소한 문법을 가지고 있습니다.
  • 기본적으로 Java와 같은 keyword를 사용하는 특징들이 있습니다.
    • new를 이용한 생성
    • extends를 이용한 상속
  • Scala class 
    Scala에서 클래스를 만드는 방법은 아주 간단합니다. 아래 Book 클래스에는 책 제목 title과 저자 author 가 멤버변수로 담겨져 있습니다.
// 클래스 생성
class Book(var title: String, var author: String)

// 인스턴스 생성(클래스로부터 만들어진 각각의 객체)
val myBook1 = new Book("My awesome book 1", "Me")
val myBook2 = new Book("My awesome book 2", "Me")

 

  • Scala에서는 게터(getter)와 세터(setter)가 자동으로 설정됩니다.
println(myBook1.title)
// 출력: "My awesome book 1"

myBook1.title = "My awesome book 1 updated!"
println(myBook1.title)
// 출력: "My awesome book 1 updated!"

 

  • 아래와 같이 Java로 클래스를 만든다고 했다면 Scala보다 훨씬 긴 코드가 필요합니다.
//Java Class 생성

public class Book {
	private String title;
	private String author;
	public Book(String title, String author) {
		this.title = title;
		this.author = author;
	}
    public String getTitle() {
		return this.title
	}
	public String getAuthor() {
		return this.author
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public void setAuthor(String author) {
		this.author = author;
	}
}

 

  • 생성자 Constructor
    이전에 클래스를 만들 때는 (변경 가능한 변수인) var 을 사용했는데요, (변경 불가능한 변수인) val 을 사용하면 필드를 read-only 로 만들 수 있습니다.
class Book(val title: String, val author: String) // var 이 아닌 val!

val myBook1 = new Book("My awesome book 1", "me")
myBook1.title = "My awesome book 1 updated!" // Not allowed!

※ Scala 에서 객체지향 코드를 작성하실 때는 변수를 변경할 수 있도록var 을 사용해주세요. 함수형 프로그래밍을 하실 때는 이와 같은 클래스보다 후에 나올 case class 라는 것을 사용합니다.

 

  • 보조 생성자 Auxiliary Constructors
    보조 생성자는 디폴트 생성자 외의 다른 방법으로 클래스 인스턴스 객체를 만드는 방법을 의미합니다. 보조 생성자를 사용하면 클래스를 유연하게 만들 수 있습니다. this 메서드를 아래 규칙에 따라 정의함으로써 만들 수 있습니다.
val DefaultAuthor = "me"
class Book(var title: String, var author: String) {
	def this(title: String) = {
	this(title, DefaultAuthor)
	}
}

val myBook1 = new Book("My awesome book 1", "me")
val myBook2 = new Book("My awesome book 2") // Works!

println(myBook2.author)
// 출력: "me"

 

  • 기본값 설정과 명명변수 Named Parameters
    이전 보조 생성자가 헷갈리셨다면 염려하실 필요 없습니다. Scala 에서 기본값 설정은 쉬워서 보조 생성자를 사용할 일은 많이 없으니까요. 또한 명명변수를 사용해서 코드를 더 깔끔하고 읽기 편하게 작성하실 수 있습니다.
class Book(
	var title: String = "Default Title",
	var author: String = "me"
) {

}

val myBook = new Book()
val myBook2 = new Book(title="My Title") // 생성자 변수 이름을 지정해 줍니다.
// val myBook2 = new Book("My Title") 보다 더 정확한 의미 파악이 가능해집니다.

println(myBook.title)
// 출력: "Default Title"

println(myBook.author)
// 출력: "me"

println(myBook.title)
// 출력: "My Title"

 

1.2 Traits

trait (특성)는 Scala 의 아주 좋은 기능 중 하나입니다. Java 로 치면 Interface 와 비슷하지만 약간 다릅니다. trait 는 추상 클래스 (Abstract Class) 로도 사용하실 수 있습니다.

  • 특성 (Traits)
    자바의 Interface 처럼 어떻게 사용되는지 간단한 예시를 먼저 봅시다. 우선 trait 을 정의합니다.
trait Car {
	def engineStart(): Unit // Unit 은 Java 의 void 와 비슷하다고 생각하시면 됩니다.
	def engineStop(): Unit
}

자동차는 엔진 시동을 걸 수 있고 끌 수 있습니다. 세단, SUV 모두 자동차이기에 Car trait을 extend 할 수 있습니다.

class Sedan extends Car {
	def engineStart(): Unit = println("Engine Start")

	def engineStop(): Unit = println("Engine Stop")
}

// 리턴타입 없이 아래처럼 작성하셔도 됩니다.
class SUV extends Car {
	def engineStart() = println("Engine Start")
	def engineStop() = println("Engine Stop")
}

 

  • 다중 특성 (Traits Mixin)
    Scala 에서는 with 키워드를 사용해 클래스에서 여러 특성을 사용할 수 있습니다. 예를 들어 버스와 택시같은 차들은 결제 기능도 있을 수 있는데요, 아래 예시 코드처럼 작성하실 수 있습니다.
trait PaymentModule {
	def collectPayment(amount: Int): Boolean
}

class Bus extends Car with PaymentModule {
	// Car
	def engineStart(): Unit = println("Engine Start")
	def engineStop(): Unit = println("Engine Stop")
	
    	// PaymentModule
	def collectPayment(amount: Int): Boolean = {
		// 결제를 위한 기능을 여기서 수행합니다.
        return true
	}
}

 

1.3 추상 클래스 (Abstract Class)

  • Scala 추상 클래스
    Scala 의 추상 클래스는 Java 의 추상 클래스와 비슷합니다. 하지만 대부분의 경우 특성(Trait)으로 충분해서 추상 클래스는 많이 사용할 일이 없습니다. 추상 클래스는 아래와 같은 경우에만 사용합니다.

1. 생성자 매개변수 (constructor parameters)가 필요한 경우

trait Car(name: String) // Not allowed! 컴파일 에러

abstract class Car(name: String) // OK

 

2. Java 코드1.에서 호출이 되어야 하는 경우

Java에서는 Scala trait을 몰라서 Java와의 호환성을 생각해야하면 abstract class를 사용해야 합니다.

 

  • 사용법 (Syntax)

추상 클래스는 특성과 비슷한 문법을 가지고 있습니다. 이전에 봤던 자동차 특성을 추상 클래스로 만들어 봅시다.

abstract class Car (name: String) {
	def engineStart(): Unit = println("Engine Start")
	def engineStop(): Unit = println("Engine Stop")

	def accelerate(): Unit // 추상 메소드
	def brake(): Unit = println(s"$name braking!")
}

 

추상 메소드는 Car 추상 클래스를 상속받은 객체에서 (예를 들면 Bus 클래스와 같은) 직접 정의를 할 수 있습니다. 추상 메소드가 아닌 경우에도 override 키워드를 통해 구현을 재정의 할 수 있습니다.

class Bus(name: String) extends Car(name) {
	override def engineStart() = println("Bus Engine Start")
	
    def accelerate() = println("Bus accelerating!")
}

val myBus = new Bus("402")
myBus.engineStart
// 출력: "Bus Engine Start"
myBus.accelerate
// 출력: "Bus accelerating!"
myBus.brake
// 출력: "402 braking!"

 

 

2. 함수형 프로그래밍 (Functional Programming)

함수형 프로그래밍은 순수 함수(pure functions) 와 불변값(immutable values)을 위주로사용해서 어플리케이션을 만드는 프로그래밍 스타일입니다. 수학을 하는 것처럼 코딩을 하는 느낌이라고 생각하시면 됩니다.

 

2.1 순수 함수

아래와 같은 조건을 가진 함수를 순수함수라고 합니다.

  • 함수의 리턴값이 자체 입력값에만 의존
  • 어떠한 숨겨진 상태도 변경하지 않음
  • 어떠한 백도어도 없음 (데이터베이스, 인터넷, 파일 등으로부터 읽어오는 데이터가 없음)

이러한 정의에 따라 순수 함수는 어느 경우에도 같은 값을 입력하면 항상 같은 값을 출력합니다.

val abs: Int = (-5).abs // 절대값 함수 (순수 함수)
val ceil: Double = (2.1).ceil // 올림 함수 (순수 함수)
val isEmpty: Boolean = "Hello world!".isEmpty // 순수 함수

println(abs)
// 출력: 5
println(ceil)
// 출력: 3
println(isEmpty)
// 출력: false

 

 

역으로 불순수 함수(impure functions)는 위 조건을 충족시키지 않습니다. 예시를 살펴봅시다.

val nums = List(1,2,3)
val foreach = nums.foreach{println} // foreach 불순수 함수

println(foreach)
// 출력: ()

Unit "()" 이 출력되었습니다. 따라서 foreach 는 출력값이 없습니다. 이처럼 Unit 을 리턴하는 함수는 불순수 함수입니다.

 

이번에는 간단하게 순수 함수를 한 번 만들어 봅시다.

def square(num: Int): Int = num * num // 제곱 순수 함수

val square = square(2)
println(square)
// 출력: 4

 

2.2 함수의 변수화

Scala 에서는 함수를 변수로 사용할 수 있습니다. 이는 map 이나 filter 같은 함수를 사용하면서 코드를 더 간결하게 짤 수 있게 합니다. 예시를 보면서 이해해 보시죠.

val nums = List(1,2,3,4,5)
def double(i: Int): Int = i * 2 // double 함수 정의

val doubles = nums.map(double) // 함수를 변수처럼 사용
println(doubles)
// 출력: List(2,4,6,8,10)

 

 

2.3 케이스 클래스

케이스 클래스는 일반 클래스에서 기능들을 더한 클래스입니다. 케이스 클래스를 사용하면 아래와 같은 차이점과 장점들이 있습니다.

 

  • 게터 메소드 자동생성 / 세터 메소드 없음
    함수형 프로그래밍에서는 자료형을 업데이트하면 안됩니다. 필요할때 마다 새로 객체를 생성하면서 코드를 짜야 합니다. 그래서 케이스 클래스 생성자 변수는 val필드로 만들어집니다. 따라서 accessor(getField 같은) 메소드는 만들어지지만, mutator(setField같은) 메소드는 만들어지지 않습니다.
case class Car(name: String, engineType: String)

// 참고) case class 에서는 "new" 키워드가 필요하지 않습니다!
val porsche911 = Car("Porsche 911", "3.0L")

println(porsche911.name)
// 출력: "Porsche 911"

porsche911.name = "Porsche 912" // Not allowed!

 

  • 복제(Copy) 메서드
    케이스 클래스는 객체를 복제하거나 하나 또는 그 이상의 필드를 업데이터해야 할 때 유용합니다.
case class Car(brand: String, model: String, firstManufacturedYear: Int)
val porsche911 = Car("Porsche", "911", 1963)
val porsche992 = porsche911.copy(model="992", firstManufacturedYear=2019)
  • Equals & hashCode
    케이스 클래스는 equals와 hashCode메소드가 자동으로 생성되어 두 객체를 비교할 수 있습니다. equals를 사용하면 두 객체가 같은지 체크할 수 있습니다. hashCode메소드는 인스턴스를 map의 키로도 사용할 수 있게 됩니다. 이는 Scala 기초 포스팅에서 배웠던 Pattern Matching을 사용할 수 있게 되어 아주 큰 장점이 됩니다.
val avante = Car("Hyundai", "Avante", 1990)

println(porsche911 == avante)
// 출력: false

 

 

3. 예외처리 (Exception Handling)

3.1 try-catch-finally block

Scala 예외 처리의 기본적인 구조는 Python이나 Java와 비슷하지만 사용하는 키워드가 약간 다르고 케이스 클래스와 패턴매칭을 사용합니다.

var text = ""
try {
	text = ... // 파일을 읽어오는 코드 호출
} catch {
    case e: FileNotFoundException => println("File not found")
	case e: IOException => println("IO exception occurred")
	case _: Throwable => println("default")
} finally {
	// 에러 캐치를 하고 나서 실행시키고 싶은 코드
	// 예) 데이터베이스 연결 종료
}

 

 

3.2 함수형 예외처리 타입

Scala는 functuinal 프로그래밍으로 예외처리를 할 수 있는 수단을 제공합니다. 이 기능을 이용해서 type안정성 뿐만 아니라 null safe 한 코드를 작성할 수 있습니다.

  • Option
    nullable value를 표현할 때, Option[Type]을 쓸 수 있습니다.
def upperString(value: String): Option[String] = {
	if (value.isEmpty) None
	else Some(value.upper)
}
  1. 타입을 Option[Type]으로 선언하고
  2. 값이 없다면 None을 할당하고
  3. 값이 있다면 Some(Value)에 넣어서 할당한다.

 

3.3 Either

Either 는 “이거 아니면 저거”를 타입으로 만든 것입니다. 경우의 수 처리를 안전하게 처리할수 있는 타입입니다. 주로 에러를 던질 때, throw로 던지지 않고 처리할 때 사용합니다. (left를 에러로 둔다)

def upperString(value: String): Either[String, String] = {
	if (value.isEmpty) Left("Value cannot be empty")
	else Right(value.upper)
}
  1. 타입을 Either[$type_of_error, $type_of_value] 로 선언한다.
  2. 에러인경우 Left() 에 넣어서 값을 할당한다.
  3. 정상인 경우 Right() 에 넣어서 값을 할당한다.

3.4 Try

Try 는 scala 2.10 부터 도입된 새로운 유틸입니다. Either 와 같은 기능을 하면서도 Monadic하기 때문에 함수형 연산을 바로 이어서 할 수 있다는 점이 특징입니다.

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

def divide: Try[Int] = {
	val dividend = Try(StdIn.readLine("Enter an Int that you'd like to divide:\n").toInt)
	val divisor = Try(StdIn.readLine("Enter an Int that you'd like to divide by:\n").toInt)
	val problem = dividend.flatMap(x => divisor.map(y => x/y))
	problem match {
		case Success(v) =>
			println("Result of " + dividend.get + "/"+ divisor.get +" is: " + v)
			Success(v)
		case Failure(e) =>
			println("You must've divided by zero or entered something that's not an Int. Try again!")
			println("Info from the exception: " + e.getMessage)
			divide
	}
}
  • 이 예제와 같이 Try 의 결과를 평가(evaluation)해서 사용하는 방식을 Monad transformer 라고 한다.
  • map, flatMap 을 사용할 수 있다.
  • for yield 로 가독성이 좋고 case match 코드도 줄어든다.
println(divide)
println(divide.map(r=>{r*10}))

'ETC > Scala' 카테고리의 다른 글

[Scala] 스칼라 프로그래밍 기초  (0) 2025.05.27