Functional Programming VS Object Oriented Programming
Functional Programming, Object Oriented Prgramming
- FP와 OOP를 구분하는 부분은 무엇일까?
- FP를 잘 하면 OOP는 못하는 것이 될까?
- 어떠한 관점에서 FP와 OOP를 보는 것이 타당할까?
FP, Functional Programming에 대해서
- 함수형 프로그래밍은 무엇(What)을 하는가, 즉 Domain에 기반을 두어 작업을 진행하는 데에 초점이 맞추어져 있다.
함수를 선언을 함으로써 그 기능을 정의하고 작동하며, 변수와 함수의 코드가 가장 중요하다.
또한 함수 f(x) = y 일 경우 함수 f는 x에 대해 항상 동일한 결과값 y를 얻게 된다. 이는 f라는 함수의 동작을 쉽게 이해하고 분석할 수 있도록 한다. - 함수형은 느슨한 결합구조, Nested function 구조, Chaining, 개별 task/application별 개발이 매우 용이하다.
- 함수형 프로그래밍에서는 1급 함수, 고차 함수, 데이터 불변성, Shared state의 회피에 대한 개념은 반드시 녹아 있어야 한다.
- 1급 함수: 함수가 변수 혹은 데이터 구조로서 동작할 수 있으며, arguments 혹은 return value로서 사용이 가능하다.
- 고차 함수: 함수에 함수를 전달할 수 있으며, 함수의 반환값으로 함수를 반환할 수 있다.
callback, promise 가 대표적인 예이다. - 데이터 불변성: 입력되는 데이터는 함수가 실행되는 동안 변하면 안된다. 이는 데이터 참조에 대한 투명성을 제공한다.
- Shared state의 회피: Shared State, 즉 공유 범위는 Global과 같이 다른 함수에서도 접근 가능한 영역을 말하며, 이 영역을 사용하는 것을 최대한 회피할 것을 요구한다.
Shared state를 사용하게 되면 이 영역의 데이터가 함수 내에 영향을 미치게 되고, 이는 곧 함수의 작동에 영향을 미치게 됨을 의미한다.
또한 공유 상태는 코드의 분석과 히스토리 작성에 복잡성을 부여하며 분석 효율성을 떨어뜨리게 된다.
OOP, Object Oriented Programming에 대해서
- 객체지향 프로그래밍은 어떻게(How) 하는가, 즉 객체가 어떠한 방법으로 처리를 하는가에 초점이 맞추어져 있다. 객체의 모든 프로퍼티를 정의하고, 정의된 객체들이 어떠한 방법으로 상호작용하며 스스로를 변화시키고 저장하는가를 이해하여야 한다. 때문에 객체를 어떻게 정의하는가, 그리고 객체간의 상호 작용을 잘 정의하는 방법이 객체지향 프로그래밍에서 중요하다고 할 수 있다.
- 객체지향 프로그래밍에서는 추상화, 캡슐화, 다형성의 개념이 반드시 녹아 있어야 한다.
- 추상화: 목적과 관련이 없는 특성들은 제거하고, 반드시 필요한 특성만을 모아 하나의 객체로 정의한다.
객체들은 목적과 관련된 특성만이 기록되고, 이 객체들의 공통된 부분을 파악하여 모아놓은 부분이다.
객체화할 대상들을 세부적으로 분석하고 공통의 특징을 하나로 묶어내어 객체의 독립성을 확보하고, 추상화된 객체를 사용하여 다형성과 재사용성을 추구한다. - 캡슐화: 객체의 상태를 저장, 변화하기 위해 필요한 변수/메소드를 묶어두고, 필요한 정보만을 노출해야 한다.
Java의 경우 클래스가 이를 제공하며, 클래스는 목적에 맞는 변수와 유기적 변화를 만들어낼 메소드를 가진다.
이는 객체가 가진 정보의 보호와 은닉을 제공하며, 이로 인하여 객체간의 결합도는 낮추고 사이드이펙트를 줄이려 하는데 그 목적이 있다. - 다형성, 상속: 추상화/캡슐화(abstract/interface)된 객체를 상속/구현(extends/implements)하여 형태는 같지만 전혀 다른 객체를 만드는 과정이다.
객체를 만들 때 특정 객체를 상속받게 된다면 상속받는 상위 객체의 모습을 가장하여 다양한 모습으로 존재할 수 있으며, 상위 객체의 모든 내용을 사용할 수 있다. 이는 높은 재사용성과 응집력을 제공한다.
// 추상 클래스 Animal을 만들고, Animal을 상속받는 Cat과 Dog를 만든다. // Cat과 Dog는 별개의 동작을 수행하는 play 메소드를 가진다. // Cat과 Dog는 Animal을 상속받았기 때문에, Animal로서 존재가 가능하다. public abstract class Animal { protected String name; protected String howl; constructor(String name) { this.name = name; } public void say(String howl) { System.out.println(this.name + " said " + this.howl); } abstract void play(); } public class Cat extends Animal { constructor() { super("Kitty"); this.howl = "Meow"; } public void play() { // implement do stroke cat. } } public class Dog extends Animal { constructor() { super("Dingo"); this.howl = "Bow-wow"; } public void play() { // implement do running with dog. } } public class AnimalHouse() { List<Animal> animals; constructor() { this.animals = new ArrayList<Animal>(); animals.add(new Cat()); animals.add(new Dog()); } } - 추상화: 목적과 관련이 없는 특성들은 제거하고, 반드시 필요한 특성만을 모아 하나의 객체로 정의한다.
FP vs OOP
- 데이터: FP는 처음 생성될 때 포함되는 데이터가 변하지 않는 것을 목표로 한다. 반대로 OOP는 객체에 저장된 데이터는 로직이 실행됨에 따라 지속적으로 수정될 수 있다.
- 기본 단위: FP는 변수와 함수 단위의, OOP는 객체와 메소드 단위로 구분할 수 있다.
- 상태: FP는 스스로가 상태를 가지지 않는다. 반대로 OOP는 객체 내부에 스스로의 상태를 저장한다. 따라서 OOP는 객체의 상태 완성을 위해 반드시 상태의 task와 return value, 그리고 task에 대한 순서 정의를 해야한다.
- 정의: FP는 무엇을(What) 하는가가 중요하며, OOP는 어떻게(How) 하는것이 중요하다.
- 설계: FP는 본연의 기능적인 면모를 구현한 함수를 정의하고, 이 함수들을 조합하여 사용한다. OOP는 객체를 구현하고, 객체간의 유기적인 관계를 이용한다. 때에 따라서 객체를 위해 준비된 라이브러리/프레임워크를 사용한다.
- 실행: FP는 side-effect가 없다. 즉 여러 processor에서 실행되어도 같은 input이라면 항상 같은 결과가 도출된다. 반대로 OOP는 스스로의 상태에 따라 항상 다른 결과를 도출한다.
FP를 OOP에 비교했을 때
- FP는 결과가 입력되는 데이터에만 의존되기 때문에, OOP에 비해 예측이나 이해가 쉽다. 입력되는 데이터에 의존하는 코드를 만들기 위해 노력해야 하는 의미이다.
- FP는 function을 마치 data처럼 다룰 수 있다. Argument로서 사용하는 것 또한 가능하다.
- Testing/Debugging이 비교적 쉽다. 항상 input/output이 매핑되므로, 이러한 문제를 찾아가는게 수월하다.
- 동시성/병렬처리가 비교적 쉽다. 다수의 프로세서에서도 항상 같은 실행결과를 도출하기 때문이다.
- 느슨한 할당 구조를 가지고 있기 때문에 반복적인 메모리 할당 작업이 없다. FP에서는 필요할 때 그 시점에 할당을 실행한다.
- FP는 함수의 실행 순서를 정의하지 않는다. FP는 함수를 선언하고, 선언된 함수들을 이용하여 합성 함수(하나의 결과를 만들기 위해 여러 함수를 사용하는 것)을 사용하여 최종 결과물을 획득한다.
- 객체와 상속/다형성을 구현하는 것 보다 고차 함수를 사용한다.
- FP에서 Recursion을 작성하면, 가독성과 분석에 그리 도움을 주지 않는다. 특히나 Recursion에서 함수를 argument로 삼는다면 더더욱 그렇다.
OOP를 FP에 비교했을 때
- OOP Object는 재사용성이 높다. 모든 대상은 객체로 만들어 관리하며, 정의된 객체(추상화/인터페이스)에서 수많은 서브객체(상속체/구현체)들이 만들어질 수 있다.
- OOP는 새로운 데이터를 넣기가 매우 수월하다. 객체 안에 프로퍼티를 생성하고 메소드를 정의함으로써, 객체가 데이터를 받아들일 준비가 끝나며, 이는 클래스/인터페이스간 연관이 있는 모든 객체들이 동시에 영향을 받기 때문이다.
- OOP는 상속 레벨이 올라갈수록, 객체를 유지보수하기 어려워진다. OOP에서 객체는 연관된 다른 객체들과 항상 교류하며 상태를 변화시키는데, 상속 레벨이 많을 수록 어떻게 영향을 받는지 분석하기 어려워진다.
- 데이터 혹은 객체의 구성 요소를 숨기는것이 가능하다. OOP는 객체의 속성으로 public/private/protected를 제공하며, 이를 사용하여 필요한 정보만을 노출시키는 것이 가능하다.
- OOP는 클래스의 추상화 및 상속에 있어 객체의 구현을 엄밀하게 구분해야 한다. 구현된 메소드가 해당되는 객체에 매우 밀접하게 관련이 있기 때문이다. 특히, 커플링되어서는 안되는 클래스/인터페이스간의 상속과 구현을 자제하여야 한다.
- 빅데이터를 다루기가 어렵다. 빅데이터는 정형화시키기 어려운 데이터인만큼, 정형화되어 객체로 정제된 데이터가 필요한 OOP에는 맞지 않는다.
javascript에서 OOP는 불가능할까?
- 먼저 javascript는 함수를 기반으로 작성되는 언어이다.
-
하지만 우리는 javascript 내의 코드들을 OOP로 작성할 수 있다.
아주 간단하게 object를 정의하고, 해당 객체를 생성하여 사용 가능하다.const Animal = { name: 'not defined', howl: 'not defined', ask: function() { console.log(`${this.name} asked ${this.howl}`) } }; const cat = Object.create(Animal); cat.name = 'Kitty'; cat.howl = () => ; cat.ask(); // Kitten asked Meow - javascript에서도 클래스/상속 개념을 구현하여 두어서 사용 가능하다. 위에서 설명된 Object를 만들어 사용하는 것 보다 훨씬 아름답다.
아래 링크 페이지는 MDN Docs 중 Class에 대해 설명하는 부분이다.
MDN Javascript Class
Conclusion
FP와 OOP는 전혀 다른 컨셉을 가지고 있다.
첫 코드를 작성할 때 부터, 데이터를 관리하는 법, 코드를 유지보수하는 방법까지 다르다.
다만, 두 기법 모두 지향하는 바는 같다. 더 간결한 코드, 더 명확한 코드, 그리고 유지보수가 쉽고 버그가 없는 코드가 그것이다.
따라서 우리는 각각의 장단점을 고려해보고 본인의 목적에 맞는 방법론을 선택할 수 있다.