Learning Man

코드짜는 법-OOP와 FP

May 16, 2021

JESSE

프로그래밍을 어느 정도 하다 보면 코드를 어떻게 하면 잘 짤 수 있을지에 대해 고민을 하게 된다

문법을 하다 땠다. 무엇을 구현할 것인지에 대한 지식만 있다면 이제 무엇이든지 만들어 낼 수 있을 것 같다. 하지만 알고 보니 이제 아스팔트 길에서 등산로로 한 발짝 내디딘 것 뿐이다. 정산으로 가려면 수많은 난관이 남아 있다.

그중에서도 제일 애매한 난관이 있다. 코드를 잘 쓰는 방법에 대한 고민이다. 지식은 배우고 외우면 어떻게 할 수 있을 것 같은데, 잘 쓴 코드를 작성하는 건 왠지 어떻게 해야 할지 모르겠다.

증복을 끝까지 따라가서 제거해야 합니다.

우리 모두의 선생님 이고잉님이 종종 하시는 말씀이다. 중복을 끝까지 제거해야 유지보수 하기 편하고 에러가 적게 나고 사람이 이해하기 쉽다. 이것이 코드를 잘 쓰는 것의 본질이다.

근데 그래서 어떻게 해야 중복을 끝까지 따라가서 제거할 수 있는 거지? 중복을 제거하는 방식에는 어떤 게 있는 거지? 이런 의문이 자연스럽게 머리에 떠오른다.

이러한 고민을 하다 보면 흔히 마주치게 되는 철학이 바로 OOP와 FP다.

당연하게도 우리의 조상 프로그래머님들은 어떻게 코드를 잘 쓸 것인지에 대한 고민을 많이 해 두셨다. OOP와 FP는 그중에서도 제일 대표적인 패러다임이다. 아주 하이레벨에서 이야기하면, 중복되는 것을 객체를 기준으로 묶어서 제거할지, 함수를 기준으로 묶어서 제거할지에 대한 접근 방식에서 차이가 난다.

그래서 OOP와 FP를 좀 공부해 보았다

글 잘 쓰는 법을 안다고 해서 글 잘 쓰는 게 아닌 것처럼, 이런 개념을 안다고 해서 코드를 잘 짤 수 있는 것은 아니다.다만 코드를 잘 짜는 연습을 할 때 이 철학에 비추어 내 코드를 평가해 볼 수 있다는 장점이 있다고 생각한다. (원래 목적은 여기서 더 나아가 이러한 철학을 프로그래밍을 넘어선 우리 일상에 적용해 보는 것이었지만, 거기까지는 무리여서 프로그래밍 세계에서의 논의만 하게 된 점이 아쉽다)

먼저 나는 멀티 패러다임 언어인 Javascript로만 프로덕션 레벨의 코드를 짜 보았기 때문에, OOP나 FP에 대해서 잘못된 이해를 하고 있을 수 있음을 미리 밝힌다.

OOP는 프로그래밍을 객체 간의 소통으로 이해하려는 접근 방식이다.

객체란 프로퍼티와 메서드를 가지고 있는 자료구조를 의미한다. 프로퍼티는 각 객체의 상태라고 생각할 수 있고 메서드는 각 객체의 상태를 조절하거나 기능을 수행하게 할 수 있는 장치로 생각하면 된다.

이해가 잘 안 된다면, 마치 여러 사람을 모아 놓고 일을 시키는 모습을 상상해 보면 좋다. 각 사람은 객체고, 프로퍼티는 각 개인의 머릿속이다. 메서 드는 각 개인에게 내릴 수 있는 명령의 집합이다. 청소부 역할을 하는 사람 A에게 청소를 시키면, 그 사람의 상태는 ‘바쁨’으로 변경되고 청소라는 작업을 수행하게 되는 모습을 상상하면 된다.

OOP라고 검색을 해보면, 추상화, 캡슐화, 상속, 다형성과 같은 것을 설명하는 글을 볼 수 있는데, OOP라는 개념을 처음 만든 Alan Kay에 의하면 OOP의 본질은 객체 간의 메시지 교환(즉 객체 간의 유기적 상호작용) 이므로 다른 개념에 대한 구체적 이야기는 여기서 다루지 않도록 하겠다.

OOP를 따라가면 ‘중복’ 은 객체라는 개념을 통해서 해결된다. 청소의 예를 계속 생각해보자. 청소부 A, 청소부 B, 청소부 C에게 각각 구역 A,B, C를 청소하게 시키는 코드를 짜야 하는 상황이다. 제일 멍청하게 구현을 하는 방법은 각 청소부를 모두 따로 구현하고 청소부가 청소하는 코드를 또 각각 구현하는 것이다. 1,000명의 청소부 동작을 관리하려면 100번 코드를 바꾸어야 한다.

객체지향 적으로 이 문제에 접근하면 클래스를 통해 이 문제가 해결될 수 있다. 청소부라는 클래스를 만들고 이 클래스를 통해서 청소부 A, 청소부 B, 청소부 C 라는 인스턴스를 만드는 것이다. (클래스는 객체를 찍어내는 틀이고 인스턴스는 찍혀서 나온 객체라고 생각하면 된다)

그럼 각 청소부의 중복된 동작은 모두 청소부 클래스에 모여 있고 이 동작이 필요할 때는 이 클래스를 통해 청소부 객체를 만들어 일을 시키면 된다. 코드 유지보수가 필요할 때는 이 클래스에서 코드를 수정하면 청소부가 몇만 명 이더라도 한 줄의 코드로 모두의 동작을 제어할 수 있게 된다.

이것만 안다고 해서 좋은 OOP 코드를 짤 수 있는 것은 또 아니다. 다양한 객체가 상호 작용을 하는 것이 프로그램이기 때문에 이런 컨셉 위에서 코드를 잘 짜는 패턴들이 정형화되어 있다. 이러한 것들을 디자인 패턴이라 하며, OOP 디자인 패턴이라고 검색하면, 방대한 공부 자료들을 마주하게 될 것이다.

FP는 프로그래밍을 함수의 조합으로 이해하려는 접근 방식이다.

함수란, 데이터가 들어가면, 그 데이터에 따른 결괏값을 제공하는 도구다. 이러한 함수를 재사용하기 좋은 방식으로 쪼개고 조합해서 사용하는 방식으로 중복을 해결한다.

마치 컨베이어 벨트에 재료를 넣으면 각 공정을 거쳐서 제품이 나오는 것과 같다.

그럼 그냥 function을 쓰면 그게 FP냐? 라고 생각할 수 있지만 그건 아니다. FP에서의 function은 순수함수(pure function)을 의미한다. 순수함수란 함수가 함수 바깥의 세계에 미치는 영향이 없으며, 같은 인풋이 들어갔을 때 항상 같은 아웃풋이 기대되는 것을 말한다.

바깥 세계에 영향을 안 미친 다는 것은 다른 말로 하면 사이드 이펙트가 없다는 말이다. 함수 내부의 코드는 함수 바깥의 것들을 건드려서는 안 된다. 아래의 코드는 함수 바깥에 있는 a를 변조시키고 있어 순수 함수가 아니다.

let a = 1;
function notPure(num){ a = num
}

순수 함수이려면 아래 함수 처럼 함수 바깥 세계와 관련이 없어야 한다.

function pure(num){ return num + 1}

이러한 특성 때문에, FP에서는 객체의 값을 변경하는 함수를 만들 경우 객체의 값을 직접 변경하는 것이 아니라, 그 객체를 복사한 새로운 객체를 리턴 하는 방식을 취한다. 객체를 직접 변경하는 것은 외부 세계에 영향을 끼쳤다는 의미이기 때문이다. 아래와 같은 느낌으로 객체가 인풋인 경우 그 객체를 복사한 값을 리턴하는 형식을 취한다.

function addOne (inputObj){ return {...inputObj, val : inputObj.val + 1 }
}

같은 인풋이 들어갔을 때 같은 아웃풋이 나온다는 것은, 함수가 어떻게 행동할이 일관되게 예측 가능하다는 의미다.

아래와 같은 함수는 순수함수가 아니다. 인풋과 상관없이 다른 값이 나온다.

function notPure(num){ return new Math.random()
}

아래는 순수함수다. 같은 인풋에 대해서 같은 아웃풋이 보장된다.

function pluseOne (num){ return num + 1
}

이런 제약 조건으로 함수를 잘게 쪼개고 조합하여 필요한 기능을 만들어 내는 것이 FP의 핵심이다.

굳이 이러한 제약 조건을 걸어서 더 함수를 구성하기 어렵게 만드는 이유는 코드의 유지 보수성을 더 높이기 위함이다.

함수가 사이드 이팩트가 없기 때문에 이 함수는 문맥에서 자유롭다. 특정 맥락에서 특정 방식으로 사용되어야 하는 것이 아니라, 하나의 부품으로 취급될 수 있다.

또한 사이드이팩트가 없어 데이터를 직접 변조하지 않아서 코드의 흐름을 예측하거나 디버깅하기 좋다. 데이터가 어딘가에서 오염되어서 잘못된 동작을 일으킬 염려가 적다. 데이터가 중간에 어떻게 변하고 있는지 파악하게 좋다. 같은 인풋에 같은 아웃풋이 나오는 특징은 함수를 예측 가능하게 만들어 함수 간의 조합을 용이하게 만든다.

즉, 이러한 제약 조건이 각 함수를 재사용 및 유지보수에 좋은 도구로 만들어 주는 것이다.

OOP와 마찬가지로 FP도 잘 쓸 수 있는 패턴들이 있으니 참고해 보면 더 좋은 FP 코드를 쓰는 데 도움이 될 것이다.

내가 공부하기 전에 가졌던 의문들에 대해서 답해 보자

OOP랑 FP랑 그렇게 다른 거야?

사실 둘 다 튜링 완전성을 가지고 있어서 구현하지 못하는 것은 없다고 한다. 다만 프로그램을 대하는 방식이 다른 것뿐이다.

둘이 완전히 반대되는 개념도, 적대하는 개념도 아니다. 둘 중에 하나에 특화된 프로그래밍 언어들이 있지만, 그런 언어에서조차 다른 프레임워크의 사고방식을 차용한 구현을 할 수 있다고 한다. 예를 들어 FP적 사고로 만들어진 메서드가 사용되는 클래스가 있다면 이는 둘을 섞어서 사용했다고 볼 수 있다.

그래서 언제 어떤 걸 써야 하는 거야?

애플리케이션을 구성하는 컴포넌트의 수는 많고 그 컴포넌트 간의 상호작용은 빈번히 일어나지만, 각각의 계산의 복잡도가 낮은 경우에는 OOP 적 접근이 더 효율적이다. 컴포넌트의 수는 적지만 계산이 복잡하고 데이터를 추적하는 것의 중요성이 클 때는 FP 적 접근이 더 좋다는 의견이 지배적인 듯 하다.

이거 실제로 쓰면 어떻게 되지?

일단 언어적으로 갈리는 부분이 분명히 있다. OOP 기반 언어(Java 등)에서는 객체 지향적 코드 구조를 강제하고 FP 기반 언어(Haskell 등)에서는 함수형 구조를 강제한다. 이 경우에는 각각의 프로그래밍 패러다임을 염두에 두고 언어의 의도에 맞는 코드를 짜는 것이 중요하다.

두 가지 모두를 할 수 있는 멀티 패러다임 언어인 경우(Javascript 등) 어떤 패러다임으로 코드를 짜지 개발자가 좀 더 생각해 보아야 한다.

프론트엔드 개발자가 제일 많이 사용하는 Javascript 라이브러리 리액트를 보면 패러다임의 변화가 코드를 어떻게 변화시키는지 더 자세히 엿볼 수 있다.

처음 리액트가 나왔을 때 리액트의 컴포넌트가 상태를 가지기 위해서 클래스의 형태를 띠고 있었다. 각 컴포넌트는 클래스의 인스턴스로, 자신의 상태를 가지고 있고 React.component 라는 클래스에서 상속받은 메서드로lifecycle method 사용했다. 리액트는 완전히 OOP적으로 구성된 것은 아니었지만, OOP의 특징을 사용하는 컨셉을 가지고 있었다.

하지만 hooks 라는 개념이 도입된 이후로 리액트는 함수형 프로그래밍의 특징을 강하게 가져가기 시작했다. 컴포넌트들은 클래스가 아니라 함수로 구성되었고, custom hooks등의 함수를 컴포넌트 함수 내부에서 조합해서 사용하는 방식으로 컴포넌트의 state를 흉내 내었다.

리액트 문서에서는 가급적이면 state를 가지지 않는 함수형 컴포넌트를 사용하기를 권장하고 컴포넌트 로직에서 DOM을 직접적으로 조작하는것을 지양시키는데, 이는 FP에서 말하는 순수 함수의 특징을 활용하기 위함이다.

이처럼 어떠한 패러다임을 가지고 코드를 조직하느냐에 따라서 다른 코드의 구현 형태가 나오게 된다.

코드를 짤 때 이 생각의 프레임워크가 도움이 됐기를 바란다.

이번에는 OOP와 FP라는 언어를 넘어선, 코드를 어떻게 짤 것인지에 대한 글을 적어 보았다. 이걸 안다고 해서 당장 코딩 실력이 늘지는 않겠지만, 단순히 기능을 구현하는 것에서 그치는 것이 아니라, 더 큰 수준에서 어떻게 코드를 조직해 나가는지에 대한 이해가 있다면, 한 단계 성장한 프로그래밍을 할 수 있을 것으로 믿는다.

다른 사람이 짠 코드를 보거나 라이브러리를 활용할 때에도 이런 생각의 프레임워크를 이해하고 있다면 저자의 의도를 파악하기도 한결 쉬울 것이다.

모자라지만, OOP FP 입문에 도움이 된 글이었길 바란다.

증권사 연구원 -> 블록체인 컨설팅회사 창업 -> 개발자. 커리어 전환의 달인입니다. 차분한 마음으로 꾸준히 하면 어디서든 두각을 드러낼 수 있다고 믿습니다. 개발 실력, 마음의 자유에 관심이 많습니다. 애쓰지 않으면서도 열심히 사는 삶을 추구합니다.

© 2024Learning Man