스칼라 함수형 프로그래밍 - 불변성과 순수 함수의 매력
View: 333
0
0
작성자: 달빛제이크
카테고리: Scala Language
발행: 2024-06-04
수정
2024-06-21
안녕하세요. 달빛제이크입니다.
스칼라 함수형 프로그래밍 스타일에 대해서 말씀드리겠습니다.
스칼라 언어로 프로그래밍을 하기 위해서는 함수형 프로그래밍 스타일로 코딩을 해야 하는데, 기본적인 문법 체계가 함수형 프로그래밍을 근간으로 하고 있기 때문이에요.
스칼라 기본 문법에서 설명 드렸다시피, 스칼라가 문법 상으로 지원하는 제어 구조가 많지 않습니다.
스칼라가 지원하는 제어 구조는 if, while, for, try, match의 5개 이고, 주로 함수 리터럴을 사용해서 함수 자체를 인수로 넘기기 때문이라고 설명을 드렸습니다.
다시 말하면, 스칼라로 코딩을 한다는 것은 절차형 프로그래밍처럼 제어문을 사용해서 일일이 할 일을 컴퓨터에 알려 주는 방식이 아닌 미리 정의된 함수를 이용하거나 새로운 함수를 만들어서 짜임새 있고 가독성이 좋은 프로그램을 작성한다는 의미입니다.
그렇다면 함수형 스타일이라는 것은 구체적으로 어떤 방식으로 프로그래밍을 하는 것일까요?
Functional Style (함수형 스타일)
함수형 프로그래밍에 대한 특징을 한마디로 표현하자면 변하지 않는다는 것입니다. 불변성!! 이것이 함수형 프로그래밍의 시작이자 전부 입니다.
값이 변하지 않기 때문에 가독성이 좋아지고, 프로그램 동작을 좀 더 쉽게 예측할 수 있고, 스레드 안정성을 확보할 수 있고, 디버깅이 쉬워 집니다.
불변성을 확보하기 위해 순수 함수를 활용해서 프로그래밍을 하는 방식, 이것이 함수형 프로그래밍입니다.
순수 함수는 불변성의 맥락에서 2가지 특징을 가집니다.
-
참조 투명성 (Referential Transparency) : 동일한 입력이 주어지면 항상 동일한 출력을 반환합니다. 입력 값에 따라 출력이 결정되며, 다른 외부 상태나 변수에 의존하지 않습니다.
-
부작용 없음 (No Side Effect) : 함수의 실행이 외부 상태를 변경하지 않습니다. 즉, 전역 변수나 입력 파라미터를 변경하지 않고, 함수 외부의 상태를 읽거나 쓰지 않습니다.
프로그램을 작성할 때 이 두 가지 특징을 만족하기 위해서 가장 기본이 되는 방법은 다음과 같습니다.
1) val을 사용하는 것
보통 C, C++, JAVA를 사용할 경우에는 초기화된 변수에 대해서 다른 값을 재할당 하는 것이 지극히 당연하게 여겨 질 것입니다. Scala에서는 이런 성격의 변수를 var로 선언하는 데요. var을 사용하면 코드 내에서 값을 쉽게 바꿀 수 있기 때문에 어디서 어떤 값으로 변경했는지 추적이 어려울 수 있고 참조 투명성에 위배되는 함수를 만들 가능성이 커집니다. 따라서 Scala에서는 val의 사용을 권장하는 데요. var에서 val로 변수 형식을 바꾸는 것 만으로 프로그래밍 스타일이 함수형 방식을 따라가게 됩니다.
// 절차형 프로그래밍 방식
def printArgs(args: List[String]): Unit =
var i = 0
while i < args.length do
println(args(i))
i += 1
// 함수형 프로그래밍 방식 첫 번째. args는 val 형식을 취합니다.
def printArgs(args: List[String]): Unit =
for arg <- args do
println(arg)
// 함수형 프로그래밍 방식 두 번째. args는 val 형식을 취합니다.
def printArgs(args: List[String]): Unit =
args.foreach(println)
예제에 사용된 함수는 순수 함수가 아닙니다. 그 이유는 함수를 실행하면 터미널에 출력(외부 상태를 변경)하는 결과를 만들기 때문입니다. 다만, var을 사용하지 않고 함수형 스타일로 어떻게 구조를 만들었는지를 확인하시면 좋을 것 같습니다.
2) 동일한 입력에 대해 항상 동일한 출력을 반환하는 함수를 작성하는 것
전역 변수 또는 외부 상태를 참조하지 않고 입력 값에만 의존해서 항상 동일한 출력을 반환하는 함수를 작성합니다.
// 덧셈 함수
def add(a: Int, b: Int): Int = a + b
// 리스트의 평균 계산
def average(numbers: List[Int]) =
if numbers.length == 0 then 0
else numbers.sum / numbers.length
// 불순 함수 예제
var counter = 0
def incrementCounter(): Int =
counter += 1
counter
3) Side Effect(부작용, 외부 상태를 변경하는 것) 없는 함수를 작성하는 것
프로그래밍을 한다는 것은 결국 외부에 어떤 변화를 만드는 것이지만, 프로그램 안에서 좀 더 명확하고 간결하고 에러 발생 가능성을 줄이면서 테스트를 쉽게 하기 위해서는 최대한 Side Effect를 최소화하고, Side Effect를 프로그램의 가장 끝 단에 위치 시킬 필요가 있습니다. 나중에 설명할 Cats Effect, Akka, Zio와 같은 함수형 프로그래밍 프레임워크도 모두 Side Effect를 다루는 기술이 본질입니다.
앞에서 보여주었던 터미널에 출력하는 예제를 좀 더 함수형 프로그래밍에 가깝게 수정하면 다음과 같습니다.
// 터미널에 출력하는 함수를 순수 함수로 변형
def formatArgs(args: List[String]) = args.mkString("\n")
// formatArgs 활용
val res =formatArgs(List("zero", "one", "two"))
assert(res == "zero\none\ntwo")
// 터미널에 출력
args = List("zero", "one", "two")
println(formatArgs(args))
assert 함수는 디버깅에 많이 사용하는 함수로 변수의 값과 기대 값을 비교해서 같으면 반환 값이 없고 다르면 Assertion Error를 발생 시킵니다. 프로그램의 본문을 순수 함수로 작성을 하면 테스트에 매우 효과적이고 에러를 줄일 수 있습니다. 예제에서 println(formatArgs(args))는 프로그램과 외부가 맞닿는 지점에서 Side Effect를 발생 시킨 것으로 이해하면 좋겠습니다.
지금까지 함수형 프로그래밍 스타일의 특징 및 형식에 대해서 이야기 드렸습니다.
다음 글에서는 함수에 대해서 좀 더 깊게 들어가 보겠습니다.
감사합니다.
