[객체 지향] 프로그래밍 패러다임 흐름: 함수형

개요

  • 기존 객체 지향 프로그래밍에서 가지는 문제를 해결하는 대안으로 함수형 프로그래밍이 주목받고 있다.
    • 함수의 비일관성: 객체의 멤버 변수가 변경될 경우 함수(메소드)가 다른 결과를 반환할 수 있다.
    • 객체간 의존성 문제: 객체 간 상호작용을 위해서 다른 객체의 함수들을 호출하게 되고, 이는 자연스럽게 프로그램 복잡도를 증가시킨다.
    • 객체 내 상태 제어의 어려움: 외부에서 객체의 상태를 변경하는 메소드들이 호출되면, 언제 어디서 객체의 상태를 변경했는지 추적이 어렵다.
  • 함수형(Functional) 프로그래밍은 외부 상태를 갖지 않는 함수들의 연속으로 프로그래밍을 하는 패러다임이다.
  • 외부 상태를 갖지 않는다는 의미는, 같은 입력을 넣었을 때 언제나 같은 출력을 내보낸다는 것이다.
  • 즉 함수의 입/출력에 영향을 주는 외부 요인이 없다.
  • 함수형 프로그래밍 코드에서는 한 번 초기화한 변수는 변하지 않는다.
  • 이런 특성을 불변성이라고 하는데, 이 불변성을 통해 안정성을 얻을 수 있다.

기본 개념

  • 함수형 프로그래밍에서는 함수가 외부 상태를 갖지 않는다는 것이 중요하다.
  • 예를 들면 다음은 함수형 프로그래밍에서 이야기하는 순수 함수가 아니다.
1
2
3
4
c = 1

def func(a: int, b: int) -> int:
    return a + b + c  # c 라는 외부 값, 상태가 포함되어 있기에 함수형이 아니다.
  • 반면 다음은 순수 함수라고 볼 수 있다.
1
2
def func(a: int, b: int, c: int) -> int:
    return a + b + c  # 주어진 파라미터만 사용한다. 별도의 상태가 없다.
  • 수학에서 f(x) = y라는 함수 f가 있을 때, fx를 입력으로 주면 항상 y라는 출력을 얻는다.
  • 이처럼 함수형 프로그래밍은 같은 입력을 주었을 때 항상 같은 값을 출력하도록 상태를 갖지 않는 것이다.
  • 그렇다면 왜 외부 상태를 가지지 않으려고 할까?
  • 일반적으로 통제하지 못하는 외부 상태를 사용한다면 예측하지 못한 결과를 가질 수 있기 때문이다.
  • 예를 들어 다음과 같이 상태를 가지는 Calculator 클래스가 있다.
1
2
3
4
5
6
7
class Calculator:
    def __init__(self, alpha: int) -> None:
        self.alpha = alpha
        self.beta = 0

    def run() -> int
    	return self.alpha + self.beta
  • 위 코드에서 run() 메서드 실행 시 매번 같은 값이 나오리라는 보장은 없다.
1
2
3
4
calculator = Calculator(alpha=3)
calculator.run()  # 3 반환
calculator.beta = 1
calculator.run()  # 4 반환
  • 이렇게 매번 상태를 가지면, 같은 함수를 실행하더라도 실제로 내가 의도하는 결과가 나오는 지 완전히 단언하기 어렵다.
  • 위 코드처럼 상태는 어디서든 바뀔 가능성이 있기 때문이다.
  • 보통 이런 현상을 “사이트 이펙트”라고 한다.
  • 객체 지향 언어들도 이런 상태가 존재함으로써 생기는 문제를 알고 있어 접근 제어자(private, protected)와 같은 기능들을 제공하지만, 완벽한 해법은 아니다.

예시

  • 함수형 프로그래밍은 보통 다음과 같은 순서로 문제를 해결하게 된다.
  1. 문제를 잘개 쪼갠다.
  2. 쪼개진 문제들 중 하나를 해결하는 순수 함수를 만든다.
  3. 순수 함수들을 결합하여 문제들을 해결해 나간다.
  • 보통 함수형 프로그래밍에서는 함수를 조합하는 방식으로 Pipelining, Partial Application, Currying 등이 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Pipelining
def main():
    pipe_func = pipe(read_input_file, parse_input_data, save_data)
    return pipe_func("input_file.txt")


# Partial application
def power(base, exp): #powering
    return base ** exp

def main():
    square = partial(power, exp=2)
    cube = partial(power, exp=3)
    square(2) #2의 제곱인 4 반환
    cube(2) #2의 세제곱인 8 반환
  • 프로그래밍 언어 중에서는 대표적으로 ‘스칼라(Scala)’가 이런 함수형 프로그래밍을 따르는 언어이며 다른 프로그래밍 언어에서도 특정 API를 사용하면 함수형 프로그래밍이 가능하다.

장단점

  • 함수형 프로그래밍은 상태로 인한 사이드 이펙트가 없기 때문에 안정적이다.
  • 따라서 동시성을 가지는 프로그램에 사용하기 적합하다.
  • 특히 대용량 데이터를 병렬적으로 처리할 때 이렇게 사이드 이펙트가 없도록 로직을 설계하는 것은 매우 중요한데, 최근 데이터 처리기술의 발전으로 함수형 프로그래밍이 부상되었다.
  • 하지만 실제로 함수형 프로그래밍을 하기 위해선, 상태를 허용하지 않기에 기존 객체 지향과 같은 기능의 코드를 구현하려면 다양한 함수들을 조합해서 사용해야 한다.
  • 그리고 친숙하지 않은 설계 방식으로 인해 러닝 커브가 높다.

정리

  • 프로그래밍 패러다임이란 프로그래밍을 어떤 기준으로 바라보고 작성할 것인지에 대한 관점이다.
  • 절차지향 프로그래밍은 순차적인 함수 호출을 중심의 관점이다.
    주로 구조가 TOP-DOWN이다. 따라서 이해하기 쉽다.
    하지만 코드를 확장하거나 자주 실행에 따라 로직이 바뀌어야 하는 경우에 수정하기 어렵다.
  • 객체 지향 프로그래밍은 객체들의 책임과 협력 중심의 관점이다.
    다형성과 의존성 주입으로 코드를 확장하기 쉬우며, 실행환경의 다양한 입력에 대응하기 좋다.
    하지만 런타임이 되기 전에 실제로 코드가 어떤 방향으로 흐르는 지 알기 어려우며, 디버깅도 어렵다.
  • 함수형 프로그래밍은 상태를 갖지 않는 함수들의 활용 중심의 관점이다.
    상태를 가지지 않기 때문에 예측에서 벗어나는 결과(사이드 이펙트)가 없다.
    하지만 실제로 상태를 가지지 않는 함수를 작성하고 활용하는 코드를 작성하는 것은 어렵다.

더 읽어보면 좋은 것들

0%