함수형 프로그래밍과 객체지향 프로그래밍

함수형 프로그래밍과 객체지향 프로그래밍을 공부하고 정리한 내용입니다.

함수형 프로그래밍

  • side-effect 가 없는 순수 함수와 동작의 결과를 강조하는 프로그래밍 패러다임이다.
  • 여기서 말하는 side-effect는 숨겨진 입력과 출력으로 이루어진 함수로부터 일어나는 부작용을 얘기한다.

순수 함수란?

모든 입력이 입력으로 선언되고(숨겨진 것이 없어야 한다) 마찬가지로 모든 출력이 출력으로 선언된 함수를 순수(pure)하다고 부른다. 이 정의에 맞는 순수한 함수를 순수 함수라고한다.

side-effect 가 나쁜가 ?

side-effect가 원래 작성한 프로그래머가 예상한 그대로 정확하게 동작한다면 괜찮을 것이다. 허나 우리는 원래 프로그래머의 숨겨진 예상이 정확하다고, 그리고 시간이 지나도 여전히 정확할 것이라고 신뢰해야만 한다.

이 함수가 작성될 때 기대했던 것과 똑같이 세상의 상태(문맥:context)를 셋업했는가? 아니면 어딘가를 바꾸지는 않았는가? 이로인해서 함수가 바뀌지 않았는 가?

아마 겉으로 봐서는 전혀 연관없어 보이는 코드 조각을 수정했을 지 모른다. 아니면 새로운 환경에 소프트웨어를 설치하고 있기 때문일지도 모른다.

side-effect는 이러한 문맥 상태에 대한 숨겨진 가정이 있다는 것이 충분히 비슷해서 잘 동작할 것이라는 우리의 숨겨진 희망으로 만들어 진 것 일 수 있다는 것이다.

이러한 side-effect를 가진 코드는 문맥과 완전히 분리하여 테스트 할 수 없다. 코드를 열어보고 숨겨진 원인과 결과를 파악하고 문맥을 그럴듯하게 시뮬레이션해야한다. 따라서 side-effect를 허용하게되면 블랙박스 테스트를 할 수 없게된다. 내부의 문맥을 알아야하고 블랙 박스 테스트 여지를 없애버린다. 박스를 열고 그 안에 무엇이 들어있는지 확인하지 않고서는 입력과 출력을 결정할 수 없기 때문이다.

이 side-effect는 디버깅 시에 증폭이 된다. 함수가 side-effect를 허용하지 않는 경우 우리는 단지 몇 가지 입력에 대해 출력을 확인하여 올바른지 여부를 알 수 있다.

그러나 side-effect가 있는 함수라면 어떨 것 같은가?

테스트를 위해 시스템의 다른 부분을 어디까지 고려해야 할지 그 끝을 알 수 없다. 함수가 무엇에든 의존할 수 있고 무엇이든 변경할 수 있다면 버그는 어느 곳에든 있을 수 있게 된다.

이제 side-effect가 있는 함수와 없는 함수를 살펴보겠다.

public Program getCurrentProgram(TVGuide guide, int channel) {
  Schedule schedule = guide.getSchedule(channel);

  Program current = schedule.programAt(new Date());

  return current;
}

이 함수는 현재 시간(new Date())를 숨겨진 입력으로 가진다. 순수 함수(side-effect)가 없는 함수는 이 숨겨진 입력을 함수 시그니처로 드러내면서 side-effect를 표면에 드러낸다.

다음과 같이 말이다.

// 인자 when에 주목하면 된다.
public Program getProgramAt(TVGuide guide, int channel, Date when) {
  Schedule schedule = guide.getSchedule(channel);

  Program program = schedule.programAt(when);

  return program;
}

더 복잡해 보인다는 단점이 생기지만 아래와 같은 장점들을 갖게된다.

  • 더 복잡하지 않다. 의존성을 숨긴다고 더 간단해지지는 않는다는 말이다. 의존성을 정직하게 드러낸다고 더 복잡해지지 않는다.
  • 훨씬 테스트가 쉽다. 하루 중 어느 때든 시차 변경이나 윤년을 테스트 하는 경우에도 원하는 시간을 넘겨주기만 하면 되므로 간단하다. 첫 번째 코드의 경우 시스템 시간을 바꿔가며 테스트를 해야할 것이다. 얼마나 테스트가 어려운지 알 수 있다.
  • 추론하기 더 쉽다. 이 함수는 단지 입력과 출력의 관계를 기술하고 있다. 개발자가 입력을 알고 있다면 결과가 무엇이어야 하는 지 모든 것을 알고 있을 것이다. 이는 매우 대단한 효과이다. 우리는 이 코드를 따로 떼어내어 확인할 수 있다. 입력과 출력 사이의 관계만 테스트하면 함수 전체를 테스트한 것이 된다.

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

  • 객체 중심적인 사고를 갖고 데이터를 추상화시켜 상태와 행위를 가진 객체를 만들고 객체간의 유기적인 상호작용을 통해 로직을 구성하는 프로그래밍 방법이다.
  • 🔗 캡! 상추다와 같은 특징을 갖는다.
  • 객체를 이용해 상호작용하기 때문에 개발자는 객체의 상태와 행위를 예측할 수 있고 개발하기 용이하다.

다형성 메커니즘에서는 반드시 호출자(Caller)에서 피호출자(Callee)로 소스코드 종속성을 만들어내서는 안된다. 다음의 예시를 보자.

1. f(o);

2. o.f();

1번을 볼때 o라는 객체에 대해 f라는 함수가 호출되는 것을 볼 수 있다. f라는 함수는 하나만 있을 것이고 객체 o를 둘러싼 표준 함수 집단의 일원은 아닐 것이라고 추측할 수 있다.

반면 2번을 보면 o라는 객체가 f라는 메시지를 받은 것으로 볼 수 있다. 이때는 f라는 메시지를 받을 수 있는 다른 종류의 객체들이 있을 거라고 예상할 수 있다. 따라서 정확히 f가 어떤 것일지 알 수 없다. 이렇게 f처럼 객체 o의 타입에 따라 달라지는 성질을 다형적이라고 하고 객체지향의 핵심이다.

두 개의 표현을 다시 보면 f(o) 표현은 함수 f에 대해 소스코드 종속성을 가지고 있을 것으로 보인다. f라는 함수는 하나만 있을 것으로 추측되기 때문에 호출자는 그게 뭔지 정확히 알아야 하기 때문이다.

그런데 o.f()의 경우는 다르다. f의 구현이 많을 수도 있다는 것을 알고 있고, 그 중에 어떤 것이 실제로 호출될 지 알 수 없다. 따라서 o.f()를 포함한 소스코드는 호출될 함수에 대해 소스코드 종속성을 갖지 않는다.

구체적으로 말하면 함수를 다형적으로 호출하는 소스파일은 그 함수의 구현을 가진 소스 파일을 참조해서는 안된다라는 말이다.

따라서 객체지향 프로그래밍 환원적 정의는 다음과 같다.

함수 호출의 다형성을 사용할 때, 호출자의 소스코드가 피호출자의 소스코드에 의존하지 않아도 되도록 하는 기술

함수형 프로그래밍 vs 객체지향 프로그래밍

함수형 프로그래밍과 객체지향 프로그래밍의 정의는 서로 직교하는 관계(서로의 길을 가는 관계)라고 볼 수 있다. 따라서 교차점이 없어서 비교할 수가 없다.

하지만 이런 관계가 상호 배제를 뜻하는 것은 아니다. 다형성과 참조 투명성(위에서 본 순수 함수의 특징) 모두를 가진 시스템을 만드는 것은 가능하다. 아니 오히려 바람직하다.

왜 바람직하지 ?

이 이유는 함수형 프로그래밍과 객체지향 프로그래밍이 각각 바람직한 이유와 같다.

다형성은 강하게 비결합된(Strongly decoupled) 시스템을 만들어준다. 설계된 구조의 경계 사이에서 종속성이 역전될 수 있도록 해주기 때문이다. (예를 들면 인터페이스) 이렇게 되면 모킹이나 가짜 객체들을 사용해 테스트가 가능하게된다. 그리고 다른 모듈들에 영향을 주지 않으면서도 수정할 수 있다. 결론적으로 유지보수와 개발이 편한 시스템이 되게 된다.

참조 투명성은 시스템을 예상 가능하게 만들어주기 때문에 바람직하다. (위에서 보았듯 함수 시그니처로 결과를 알 수 있다.) 또한, 내부 상태를 바꿀 수 없다는 성질은 시스템을 이해하기 쉽고, 개발하기 쉽게 만든다.

Reference

https://medium.com/@jooyunghan/함수형-프로그래밍이란-무엇인가-fab4e960d263#.aq6ezag6z

https://mangsby.com/blog/programming/fp-vs-oop/

⤧  Next post HashMap vs Hashtable ⤧  Previous post DB 페이지네이션