[Swift] 함수형 프로그래밍(Functional Programming)

민경준·2024년 1월 5일
1
post-thumbnail

함수형 프로그래밍(Functional Programming) 이란?

함수형 프로그래밍(FP)은 프로그래밍 패러다임의 한 종류로 코딩의 관점이자 접근 방식이다. 자료처리를 수학적인 함수의 계산으로 간주하고, 상태 변경 및 가변 데이터를 멀리하는 프로그래밍 패러다임이다.

자료처리를 수학적인 함수의 계산으로 간주한다고?

사실 이 문장만 두고 보면 이해가 잘 가지 않았는데, 위의 사진을 통해 짧게나마 이해 할 수 있었다. 오른쪽의 수학의 함수처럼 함수끼리의 결합을 통해 계산을 하는것이 바로 함수형 프로그래밍이다.

무슨 말이냐고? 수학의 함수는 입력 값을 받아 특정한 규칙에 따라 출력 값을 생성하는 규칙적인 변환을 나타내는데, 함수들의 결합을 통해 계산하는 방식의 아이디어를 가져와 프로그램에 적용한것이 함수형 프로그래밍이다.

왼쪽 파이프들을 각각 한개의 함수라고 생각해보자. 1번 파이프 함수에서 파라미터를 받아 리턴값을 생성하고 2번 파이프 함수로 바로 넘겨준다. 그럼 리턴값을 받은 2번 파이프 함수는 처리를 통해 리턴값을 생성하게 된다. 일련의 명령문으로 처리하는것이 아닌 여러 함수의 결합으로 복잡한 계산을 표현하는 것을 바로 함수형 프로그래밍이라고 할 수 있다.

상태 변경(State Mutation)은 뭔데?

상태 변경에 대한 정의를 보자면 프로그램이 실행 중에 특정 변수의 값을 수정하거나 객체의 속성을 갱신하는 행위를 뜻한다고 한다. 말로는 잘 와닿지 않을 수 있으니 예시 코드를 살펴보자

상태 변경을 포함하는 코드:

// 상태 변경이 포함된 코드
var count = 0

func increment() {
    count += 1
}

increment()
print(count)  // 출력: 1

increment 함수를 호출할 때 마다 count의 값이 1씩 증가한다.


불변성을 유지하는 코드:

// 불변성을 유지하는 코드
var count = 0

func increment(value: Int) -> Int {
    return value + 1
}

let newCount = increment(value: count)
print(count)      // 출력: 0 (원래의 변수는 변경되지 않음)
print(newCount)   // 출력: 1 (새로운 값은 증가한 결과)

이번에는 increment 함수가 count를 변경하지 않고, 대신에 새로운 값을 반환한다. newCount 변수를 통해 원래의 변수는 그대로 유지되고, 함수의 결과를 사용할 수 있다.

아~ 그러니까 상태 변경이라는 것은 변수의 값이 바뀌는것을 말하고 상태 변경을 멀리한다는건 함수형 프로그래밍의 특징인 불변성과 관련이 있다는 말이라고 이해 하면 되겠구나!

그럼 함수형 프로그래밍은 왜 배워야 할까?

함수형 프로그래밍? 그거 가독성 높이려고 쓰는거 아니였어?
나는 처음에 이렇게 단순하게만 이해하고 있었다. 물론 틀린 말은 아니다. 함수형 프로그래밍의 장점 중 하나이니까. 하지만 이것 하나만으로 함수형 프로그래밍을 왜 사용해야 하고, 어떻게 사용해야 하는지는 모른다.

잠시 당신이 벌목꾼이라고 생각해보자. 당신은 숲에서 가장 좋은 도끼를 가지고 있고, 가장 일을 잘하는 나무꾼이다. 그런데 어느날 누가 나타나서 새로운 벌목 패러다임인 전기톱을 알리고 다니는 걸 보게 됐고 이 사람의 미친 설득력에 전기톱을 구매하게 됐다. 당신은 여태껏 해왔던 방식대로 시동을 걸지도 않고 전기톱으로 나무를 그저 두들겨 댈 뿐이다. 결국, 당신은 이 전기톱은 일시적 유행이라고 생각하고 다시 도끼를 쥐고 벌목을 시작한다. 그런데 그 때 누군가 나타나서 전기톱의 시동거는 법을 가르쳐준다.
— 닐포드 , “함수형 사고”

그러니까 나는 지금까지 전기톱의 시동거는 법은 모르면서 톱날이 많네? 좋아보이는데? 하면서 전기톱을 사용하고 있었던 셈이다. 주변에서 아무리 전기톱이 좋다고 한 들 어떻게 사용하는지? 왜 사용하는지? 모르고 남들이 좋다고 해서, 있어 보여서 쓴다면 손에 익은 숲에서 가장 좋은 도끼를 쓰는것만 못하다고 생각한다.

또, 함수형 프로그래밍은 위에서 말 했듯이 코딩의 관점이다. 내가 코딩하는데 있어서 아예 다른 관점, 다른 방식으로 사고하는 방법을 하나 더 가지고 시작한다는 장점을 가질 수 있다는 의미이다.

다양한 패러다임을 아는것이 왜 중요한데?

물론, 패러다임의 다양성면에서도 중요하지만 내가 쓰고 있는 언어는 Swift이다. Swift는 멀티 패러다임 언어로, 객체지향 프로그래밍(OOP)과 함수형 프로그래밍(FP)을 모두 지원한다. 이렇다는것은 Swift를 가장 잘 쓰기 위해서는 객체지향과 함수형 프로그래밍 두 가지 패러다임을 적절히 섞어서 사용 할 줄 알아야 한다는 의미이고, 더 나아가 좋은 설계를 바탕으로 좋은 코드를 작성할 수 있게 된다는 의미이다.

좋은 설계가 뭐길래 패러다임과 관련이 있지?

좋은 설계를 한다는 것은 코드 전반에 걸쳐 일관적인 원칙과 규칙으로 작성 한다는 의미이고, 이러한 원칙과 방법이 되는 관점을 우리는 패러다임이라고 부른다. 즉, 패러다임을 정확히 알고 있어야 좋은 설계가 가능하고 그것을 바탕으로 좋은 코드를 작성할 수 있다.

함수형 프로그래밍(FP)의 특징

자, 이제 우리는 함수형 프로그래밍이 프로그래밍 패러다임의 한 종류라는것도 알았고 왜 배워둬야 하는지도 알게 되었으니 다음 단계인 왜 써야하지? 를 알아 볼 차례이다. 대체 특징이 뭐길래? 장/단점은 뭐가있지? 이 두가지에 대해서 알아보도록 하자.

1. 순수 함수(Pure Functions)

순수 함수는 매개변수에만 의존하며, 외부 변수나 상태에는 의존하지 않는 함수를 일컫는다. 또한 외부의 상태를 변경하거나 다른 부작용을 일으키지 않고, 동일한 값을 입력하면 항상 동일한 결과를 리턴한다.

순수 함수의 예시 코드:

// 순수 함수의 예시
func add(a: Int, b: Int) -> Int {
    return a + b
}

let result = add(a: 3, b: 4)
print(result)  // 출력: 7

// 부작용이 없고, 입력에만 의존하며, 동일한 입력에 대해 동일한 결과를 반환하는 순수 함수

위의 코드에서 add 함수는 두 개의 정수를 받아 더한 값을 반환하는 순수 함수이다. 이 함수는 입력에만 의존하며, 어떠한 외부 상태도 변경하지 않는다.

부작용이 있는 함수와의 비교:

// 외부 상태(total)를 변경하는 부작용이 있는 함수의 예시
var total = 0

func addToTotal(value: Int) {
    total += value
}

addToTotal(value: 5)
print(total)  // 출력: 5


// 동일한 입력에 대해 다른 출력을 반환하는 함수의 예시
var counter = 0

func unpredictableFunction(value: Int) -> Int {
    counter += 1
    return value + counter
}

let result1 = unpredictableFunction(value: 5)
print(result1)  // 출력: 6

let result2 = unpredictableFunction(value: 5)
print(result2)  // 출력: 8 (다른 출력을 반환)

반면에 위의 코드에서 addToTotal 함수는 외부 변수 total의 값을 변경하는 부작용이 있는 함수이고, unpredictableFunction은 외부 변수 counter를 변경하고, 이를 결과에 반영하고 있다. 따라서 동일한 입력 5를 주더라도 함수를 호출할 때마다 counter 값이 증가하여 다른 출력을 반환하므로 부작용이 있는 함수이다.

순수 함수는 왜 필요해?

순수 함수를 사용하게 되면 코드의 예측 가능성과 안정성이 올라가고, 외부 상태에 의존하지 않기 때문에 단위 테스트에 용이 하며, 테스트 코드의 작성이 간단해진다. 또, 부작용이 없어 다중 스레드 환경에서 병렬 처리가 가능하고 결합도가 낮아 코드 리팩토링에도 용이하다.

2. 불변성 (Immutability)

불변성은 데이터의 변경 불가능성을 의미하며, 데이터가 한 번 생성되면 그 값을 변경할 수 없게 만드는것을 의미한다.
Swift에서는 상수(let)를 사용하여 불변성을 확립하는 것이 일반적이다.

불변성의 예시:

let constantValue = 10
// constantValue = 20  // 에러: 상수 'constantValue'는 변경할 수 없습니다.

let immutableArray = [1, 2, 3]
// immutableArray.append(4)  // 에러: 'immutableArray'는 변경할 수 없습니다.

struct Point {
    let x: Int
    let y: Int
}

var mutablePoint = Point(x: 1, y: 2)
// mutablePoint.x = 3  // 에러: 'x'는 상수 속성이기 때문에 변경할 수 없습니다.

불변성은 왜 필요해?

불변성을 유지하는 것은 코드의 안정성을 높이고, 예측 가능성을 높여준다. 또한 다중 스레드 환경에서 불변성을 유지하면 동시성 문제를 방지할 수 있고, Swift에서 불변성을 적극적으로 활용하면 코드의 유지보수성과 확장성을 향상시킬 수 있다.

3. 고차 함수(Higher-Order Functions)

여기서 한 가지 추가로 알아야 할 것이 있다. 함수형 프로그래밍에서 함수는 1급 객체가 된다는 것인데, 1급 객체는 변수나 데이터 구조 안에 담을 수 있으며 파라미터로 전달도 가능하고 리턴값으로도 사용 가능하다.

Swift에는 대표적인 고차함수가 몇 개 있는데 map, filter, reduce 그 주인공이다. 하지만, 우리는 익숙한 3가지는 넘어가고 위에서 언급한 함수가 1급 객체가 되는것에 호기심을 가져보자.

1급 객체의 예시:

// 클로저를 파라미터로 받는 함수의 예시
func performOperation(_ a: Int, _ b: Int, operation: (Int, Int) -> Int) -> Int {
    return operation(a, b)
}

let addition = performOperation(5, 3) { $0 + $1 }
let multiplication = performOperation(5, 3) { $0 * $1 }
// 결과: addition = 8, multiplication = 15

// 함수를 반환하는 함수 및 함수를 변수에 담아 사용하는 예시
func getMathFunction(operation: String) -> (Int, Int) -> Int {
    switch operation {
    case "add":
        return { $0 + $1 }
    case "multiply":
        return { $0 * $1 }
    default:
        return { _, _ in 0 }
    }
}

let addFunction = getMathFunction(operation: "add")
let multiplyFunction = getMathFunction(operation: "multiply")
let result1 = addFunction(3, 4)  // 결과: 7
let result2 = multiplyFunction(3, 4)  // 결과: 12

고차 함수는 왜 필요해?

고차 함수를 사용하게 되면 코드가 간결해져 이해하기가 쉬워지고, 재사용성이 높아지며 함수형 프로그래밍의 장점을 최대한 발휘 할 수 있다. 그리고 Swift에서는 map, filter, reduce와 같은 고차 함수를 사용하는것을 적극 권장하고 있으니 가능한 고차함수를 사용해보도록 하자.

4. 선언적 프로그래밍 (Declarative Programming)

함수형 프로그래밍은 선언적 프로그래밍의 하나로 꼽힌다. 명령형 프로그래밍이 어떻게(How) 할 것인가에 집중한다면, 선언전 프로그래밍은 무엇을(What) 할 것인가에 대해 집중한다. 우선 예시로 차이를 확인해 보자.

명령형 프로그래밍 예시:

// 명령형 프로그래밍의 예시
var sum = 0
for i in 1...5 {
    sum += i
}
print(sum)  // 출력: 15

위의 코드는 반복문을 사용하여 1부터 5까지의 숫자를 더하는 명령형 프로그래밍이고, 루프와 변수를 사용하여 명시적으로 계산의 흐름을 제어하고 있다.

선언형 프로그래밍 예시:

// 선언형 프로그래밍의 예시
let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0, +)
print(sum)  // 출력: 15

반면에 선언형 프로그래밍은 루프나 명시적인 변수 조작 없이, 간단히 reduce 함수를 사용하여 결과값을 얻어낸다.

위 두 개의 예시를 통해 우리가 알 수 있는 사실은 선언형 프로그래밍을 사용하게 되면 어떻게 계산할지에 대한 세부 사항을 숨겨 추상화 수준을 높이고 코드의 가독성과 유지보수성을 높일 수 있다는 점이다. 또, 한가지 차이는 명령형은 대입을 사용하지만 선언형은 대입을 사용하지 않는다는 점. 이러한 방식을 두고 클린 코드(Clean Code)의 저자 Rob은 대입문이 없는 프로그래밍이라고 정의 했다.

함수형 프로그래밍(FP)의 장/단점

장점

  • 코드의 간결성 (Conciseness):
    고차 함수 등의 개념을 통해 코드를 간결하게 작성할 수 있고, 이로 인해 더 적은 코드로 같은 기능을 구현 할 수 있게 된다.

  • 디버깅 용이성 (Debuggability):
    함수는 입력에만 의존하고 외부 상태에 영향을 주지 않아 부작용이 최소화되기 때문에 함수 간의 의존성이 낮아져 디버깅이 쉬워진다.

  • 테스트 용이성 (Testability):
    불변성과 순수 함수의 특성으로 인해 함수형 코드는 단위 테스트 작성이 용이해진다.

  • 병렬 프로그래밍 (Parallel Programming):
    부작용이 적고 상태 변이가 없는 특성 때문에 병렬 및 분산 환경에서 작업하기가 쉽고, 이로 인해 멀티코어 환경에서 더 효과적인 프로그래밍이 가능해진다.

  • 재사용성 (Reusability):
    높은 수준의 추상화와 모듈화를 제공하므로 코드를 재사용하기가 편리해진다. 각 함수는 독립적이며, 작은 함수들을 결합하여 더 큰 기능을 구성할 수 있다.

단점

  • 러닝 커브 (Learning Curve):
    함수형 프로그래밍은 명령형 프로그래밍과는 다른 개념을 도입하고 있어 처음에는 학습 곡선이 높을 수 있다.
    또, 순수 함수를 사용하는 것은 쉬울 수 있지만 조합하는것은 어려울 수 있다.

  • 성능 문제 (Performance Overhead):
    일부 함수형 언어에서는 불변성을 유지하기 위해 데이터를 복사하는 경우가 있어 성능 문제가 발생할 수 있다. 그러나 최적화 기술과 새로운 언어의 개발로 이 문제는 감소하고 있다고 한다. 그리고 함수형 프로그래밍의 경우 for loop 대신 재귀함수를 통해 반복문을 작성하게 되는데, 재귀적 스타일의 코드는 무한 루프에 빠져 스택 오버플로우의 문제가 발생 할 수도 있다.

그럼 함수형 프로그래밍은 언제 쓰는게 좋을까?

우리가 함수형 프로그래밍에 대해서 공부를 했지만 이걸 언제? 어떻게? 써야 하는게 좋을지 잘 모를 수 있다. 함수형 프로그래밍이 만병통치약 처럼 작동하는게 아닌 이상 일련의 상황에서 유리할 수 있다. 예를 들면, 병렬 및 비동기 코드를 작성할 때 함수형 프로그래밍의 특징인 불변성과 순수 함수가 도움이 될 수 있고, 이러한 특징은 테스트 코드를 작성하는데도 도움이 된다.

객체지향 프로그래밍(OOP) 하고는 어떻게 조합해야 하지?

사실 함수형 프로그래밍 공부의 출발은 여기서 부터 시작했다. Swift라는 멀티 패러다임 언어에서 객체지향과 함수형을 적절히 섞어서 사용해야 하는데 함수형 프로그래밍은 대체 뭐지? 하며 알아보기 시작한것이 이번 공부의 시작이였다.

그렇다면 정말 객체지향 프로그래밍과는 어떻게 적절히 조합해야 잘 썼다고 할 수 있을까? 우선은 객체지향에 쉽고 자연스럽게 도입할 수 있는 함수형 프로그래밍의 특징들은 불변성, 순수함수, 고차함수 이 세 가지다. 불변성 개념을 적용해서 객체의 상태 변경을 피하고, 순수 함수를 통해 코드의 예측 가능성과 테스트 용이성을 올려준다. 그리고 고차 함수와 함수 합성을 도입 하여, 작은 함수들을 조합하여 큰 기능을 만들도록 한다. 그렇게 되면 코드의 가독성과 재사용성이 올라갈 수 있다.

이 외에도 다양한 방식의 조합법이 있겠지만, 우선 간단하게 도입할 수 있는 것들부터 차근차근 실 사용에 적용해보는것이 좋을듯 하다.

마치며

사실, 중간 함수형 프로그래밍의 특징에 2~3가지(모나드, 커링) 정도 더 있었지만.. 그 개념이 정말 이해하기 쉽지도 않은 난이도이고, 핵심적인 내용은 아니라 제외 했다. 나중에 기회가 되면 그 부분에 대해서 다시 한번 글을 작성 해보겠지만... 정말 쉽지 않은 일이 될듯..?

Reference

profile
iOS Developer 💻

0개의 댓글