*이 포스트는 Robert C. Martin 님의 허락을 받아 blog.cleancoder.com 의 글 “FP vs. OO“을 번역한 것입니다. 저작권에 유의하시기 바랍니다.
서론
지난 몇 년간 저는, 함수형 프로그래밍을 배우면서 “음.. 그건 너무 객체지향인데요.”라는 식으로 객체지향 프로그래밍에 반감을 표현하는 사람들을 봐왔습니다.
왜인지는 몰라도 함수형 프로그래밍과 객체지향 프로그래밍이 상호 배제 관계에 있다는 생각 때문으로 보입니다. 많은 사람들이 “객체지향적이지 않은 프로그램이 함수형 프로그램이다.”라고도 생각하는 것 같아요. 아마도 이런 생각들은 무언가 새로운 것을 배웠을때 자연스럽게 생겨나는 것 같습니다.
우리는 새로운 기술을 배우면, 이제까지 써왔던 기술은 버리려고 하는 경향이 있습니다. 새로운 기술이 더 좋다고 믿기 때문에, 당연히 옛 기술은 좋지 않을 것이다 라고 자연스럽게 생각하는 것이죠.
저는 이 글에서 함수형 프로그래밍과 객체지향 프로그래밍이 상호 배제적인 관계가 아니라, 직교하는 관계라고 이야기하려고 합니다. 즉, 잘 짜여진 함수형 프로그램은 객체지향적일 수 있고(또 그래야 하고), 잘 짜여진 객체지향 프로그램도 함수형 프로그램의 장점을 가질 수 있다는 것이죠(역시 그래야 합니다). 이를 위해서 우리는 각 용어를 신중하게 정의하고 넘어가야합니다.
객체지향 프로그래밍이란?
여기서는 마치 환원주의자처럼 설명해볼까 합니다. 여기저기에 객체지향의 발상, 신념, 기법, 패턴, 철학 등을 설명하는 좋은 정의들이 많이 있습니다. 하지만 저는 그걸 다 무시하고 아주 기본적인 부분에만 집중해 설명하고자 합니다. 왜냐하면 객체지향에 관련된 여러 개념들은 사실 객체지향만의 것이 아니라 소프트웨어 개발 전반에 포함되는 것들이기 때문입니다. 따라서 객체지향에 한정되는 부분들을 집중해서 보겠습니다.
아래 두 표현을 생각해보면:
1. f(o);
2. o.f();
무엇이 다를까요?
확실한 것은 의미론적으로는 아무런 차이가 없다는 것입니다. 문법만 다를 뿐이죠. 하지만 하나는 절차지향적으로 보이고, 다른 하나는 객체지향적으로 보입니다. 왜냐하면 우리가 1.번 표현에서는 떠올리지 않는 특별한 의미론적 성질을 2.번 표현에서는 추론해냈기 때문입니다. 그 성질은 바로 다형성입니다.
1.번 표현을 볼 때는 o 라는 객체에 대해 f 라는 함수가 호출되는 것으로 볼 수 있습니다. f 라는 함수는 하나만 있을 것이고, 객체 o 를 둘러싼 표준 함수 집단의 일원은 아닐 거라고 추측할 수 있습니다.
반면 2.번 표현을 볼 때는 o 라는 객체가 f 라는 메세지를 받은 것으로 볼 수 있습니다. 이 때는 f 라는 메세지를 받을 수 있는 다른 종류의 객체들이 있을 거라고 예상할 수 있고, 따라서 정확히 f 가 어떤 것일지 알 수 없습니다. 이렇게 f 처럼 객체 o 의 타입에 따라 달라지는 성질을 “다형적이다” 라고 합니다.
이러한 다형성을 기대하는 것이 객체지향의 핵심입니다. 이것이 객체지향에서 떼어낼 수 없는 환원주의적 정의입니다. 다형성 없는 객체지향은 객체지향이 아닙니다. 이외에 객체지향의 특징으로 거론되는 캡슐화, 데이터와 함수의 연결, 심지어 상속까지도 표현 2.보다는 표현 1.과 관련 있는 것들입니다.
C언어와 파스칼 프로그래머들, 더 나아가면 포트란과 코볼 프로그래머들까지도 언제나 캡슐화된 함수와 데이터 구조의 시스템을 만들어 왔습니다. 그런 캡슐화된 구조를 만들고 사용하는 데 객체지향 프로그래밍 언어는 필요하지 않습니다. 캡슐화나 간단한 상속같은 개념들은 위 언어들에서도 자연스럽고 명료합니다. (특히 C언어와 파스칼에서 더욱)
따라서 객체지향 프로그램과 비-객체지향 프로그램을 진정으로 구분하는 것은 다형성이라고 할 수 있죠.
누군가 f 함수 안에 연속된 if/else문이나 switch문을 사용해서 다형성을 구현할 수 있다고 주장 할 지도 모르겠습니다. 맞습니다. 그래서 객체지향 조건에 한 가지를 더 추가해야겠습니다.
다형성 메커니즘은 반드시 호출자(Caller)에서 피호출자(Callee)로 소스코드 종속성을 만들어내서는 안된다.
설명하자면, 두 개의 표현을 다시 봅시다. 표현 1. f(o) 는 함수 f 에 대해 소스코드 종속성을 가지고 있을 것으로 보입니다. 왜냐하면 f 라는 함수가 하나만 있을 것으로 추측되기 때문에 호출자는 그게 뭔지 정확히 알아야 하기 때문이죠.
그런데 표현 2. o.f() 의 경우는 좀 다릅니다. f 의 구현이 많을 수도 있다는 것을 알고 있고, 그중에 어떤 것이 실제로 호출될지 알 수 없습니다. 따라서 표현 2.를 포함한 소스코드는 호출될 함수에 대해 소스코드 종속성을 가지지 않습니다.
구체적으로 말하면 함수를 다형적으로 호출하는 소스파일은 그 함수의 구현을 가진 소스파일을 참조해서는 안됩니다. include
혹은 use, require 같이 소스파일이 다른 소스파일에 의존하도록 하는 선언을 사용하지 않아도 됩니다.
따라서 객체지향 프로그래밍 환원적 정의는:
함수 호출의 다형성을 사용할 때, 호출자의 소스코드가 피호출자의 소스코드에 의존하지 않아도 되도록 하는 기술
함수형 프로그래밍이란?
다시 환원주의자가 되어보겠습니다. 함수형 프로그래밍은 소프트웨어 영역을 넘어서는 풍부한 역사와 전통을 가지고 있습니다. 객체지향과 마찬가지로 특유의 발상, 신념, 기법, 패턴, 철학이 있지만 무시하겠습니다. 곧바로 함수형 프로그래밍이 함수형 프로그래밍이도록 하는 고유의 성질을 봅시다. 그냥 단순히 이겁니다:
f(a) == f(b) when a == b.
함수형 프로그래밍에서는 어떤 값으로 어떤 함수를 호출하면 언제든 같은 결과를 얻습니다. 프로그램이 얼마나 오래 실행되고 있었는지는 상관없이 말이죠. 이런 성질은 참조 투명성이라고도 불립니다.
이 성질은 즉, 함수 f 가 자신의 결과를 변화시키는 전역 상태를 변경하면 안된다는 것을 뜻합니다. 더 나아가서 시스템의 모든 함수가 참조 투명하다고 하면, 어떤 함수도 시스템의 전역 상태를 변경할 수 없게 됩니다. 어떤 함수도 다른 함수가 같은 입력에 대해 다른 결과를 반환하도록 할 수 없습니다.
더 깊게 들어가면, 이름 붙은 값은 절대 바뀔 수 없습니다. 즉, 대입 연산이 존재할 수 없습니다.
이런 성질에 대해 생각하다보면 참조 투명한 함수들만으로 이루어진 프로그램은 결국 아무 것도 할 수 없다는 결론에 이르게됩니다. 시스템의 유용한 기능들은 항상 무언가의 상태를 바꾸기 때문입니다. 프린터나 모니터의 상태같은 것 말이죠. 하지만 하드웨어와 외부의 모든 요소들을 제외하고 나면, 사실 우리는 이런 성질로 아주 유용한 시스템을 만들 수 있게 됩니다.
비결은 재귀에 있습니다. 예를 들어 state 라는 구조체를 인자로 받는 함수를 생각해봅시다. 이 구조체는 함수에서 사용하는 모든 상태 정보를 가집니다. 함수가 끝날 때 업데이트된 값들로 새로운 state 구조체를 만들고, 그것을 인자로 자기자신을 호출합니다.
이것이 함수형 프로그램이 내부 상태를 실제로는 변경하지 않으면서도, 내부 상태 변경을 구현하는 방법 중의 한 가지입니다.
따라서 함수형 프로그래밍의 환원적 정의는:
참조 투명성 – 값을 다시 할당하지 않는 것
함수형 프로그래밍 vs 객체지향 프로그래밍
함수형 프로그래밍과 객체지향 프로그래밍 커뮤니티 모두가 나에게 총을 겨누도록 하는 방법.
환원주의는 친구를 사귀기에 좋은 방법은 아닙니다. 하지만 쓸 만한 구석도 있죠. 이번 경우에는 꼬리에 꼬리를 무는 함수형 vs 객체지향 대결에 빛을 비추는 것이 되겠네요.
위에서 설명한 두 환원적 정의는 확실히 서로 직교하는 관계로 보입니다. 다형성과 참조 투명성은 뭐가 됐든 서로에게 참견할 게 없으니까요. 교차점이 없는 거죠.
하지만 이런 직교 관계가 상호 배제를 뜻하는 것은 아닙니다.(제임스 클러크 맥스웰에게 물어보세요.) 다형성과 참조 투명성 모두를 가진 시스템을 만드는 것은 완벽하게 가능합니다. 사실 가능만 한 게 아니라, 오히려 바람직합니다!
왜 바람직한가? 함수형 프로그래밍과 객체지향 프로그래밍 각각이 바람직한 이유와 정확히 같습니다.
다형성은 강하게 비결합된(Strongly decoupled) 시스템을 만들어주기 때문에 바람직합니다. 설계된 구조의 경계 사이에서 종속성이 역전될 수 있게끔 해주기 때문입니다. 그렇게 되면 모킹이나 가짜 객체들을 사용해 테스트가 가능해집니다. 그리고 다른 모듈들에 영향을 주지 않으면서도 수정될 수 있게 됩니다. 유지보수와 개발이 편한 시스템이 되는 거죠.
참조 투명성은 시스템을 예상 가능하게 만들어주기 때문에 바람직합니다. 내부 상태를 바꿀 수 없다는 성질은 시스템을 이해하기 쉽고, 개발하기 쉽게 만들죠. 스레드 경쟁상태 같은 동시성 관련 문제도 현저히 줄여줍니다.
결국 결론은:
함수형 프로그래밍 vs 객체지향 프로그래밍 같은 건 없다.
함수형 프로그래밍과 객체지향 프로그래밍은 함께 잘 어우러집니다. 현대적 시스템에서 두 방법론의 성질은 모두 바람직합니다. 두 가지 방법을 모두 활용하면 유연하고, 유지 가능하고, 테스트 가능하면서, 단순하고 완성도 높은 시스템을 만들 수 있습니다. 한 가지 방법론을 선택했다고 해서 다른 쪽을 배제하려는 자세는 오히려 시스템의 구조를 약하게 한다는 것을 기억해주세요.
다른 Robert C. Martin 시리즈
- 함수형 프로그래밍 vs 객체지향 프로그래밍
- 이상적인 객체지향 if else switch 조건문?
- 객체지향 5원칙 (SOLID)은 구시대의 유물 ?
- 애자일의 몰락과 소프트웨어 장인정신의 비극
1. f(o);
2. o.f();
이 예시에 대한 설명이 잘 와닿지가 않습니다…제가 이해력이 부족한건지 뭔가 빠진건지 뒤바뀐건지 잘 모르겠는데 1. o 객체에 대한 f라는 함수가 호출 되었다..?? 2. o객체가 f라는 함수 메세지를 받았다..? 뭔가 말이 이상하지않나요..? ㅠㅠ 설명 부탁드립니다..
안녕하세요. 내용이 어렵기도 하고, 제 번역도 매끄럽지 못했던 것 같네요. ^^; 풀어서 설명하자면, 1번은 보통 생각하는 함수 호출입니다. 같은 이름의 함수는 여러개 있을 수 없기 때문에 f는 하나만 존재하고, 객체 o가 뭐든 간에 항상 같은 동작을 합니다. 즉, “객체 o를 가지고 ~를 하는 함수”죠. ~안에 들어갈게 무엇인지는 호출 시점에 알고 있어야합니다. 이걸 소스코드 종속성을 가진다고 합니다. 반대로 2번은 “o라는 객체가 f라는 메세지를 받은 것이다” 라고 표현했는데, 이건 객체지향 개념과 관련이 있습니다. 여기서 f는 사실 그냥 함수의 이름입니다. f가 무엇인지는 당장 알 필요도 없습니다. 객체 o에게 그냥 “f” 라고 이야기 한 거죠. 그럼 객체 o가 알아서 할 일을 합니다. 어떤 일을… 자세히 보기 »
안녕하세요!
정말 잘 읽었습니다.
리액트를 사용하는 제가 과연 객체지향주의자인가 함수형주의자인가 고민했거든요.
불변성을 다루며 다형적 처리를 하는 리액트 개발자라 더 그렇습니다.
고민하던 중 정말 좋은 글 읽게 돼서 기분 좋습니다.
감사합니다 좋은 하루 보내세요!
임보탬님 댓글 덕에 제 기분도 좋아지네요. 감사합니다ㅎㅎ
번역 감사합니다.
번역 감사합니다.