[번역] 함수형 프로그래밍과 객체지향 프로그래밍은 닮은 부분이 있습니다

Saetbyeol·2023년 4월 28일
18

translations.zip

목록 보기
4/13
post-thumbnail

원문: FP and OOP are close sibling

YouTube: Functional Programming for OOP devs: Contexts & Currying
by Muhammad Hashim

이 글에서 함수형 프로그래밍(FP)과 객체지향 프로그래밍(OOP)을 조화롭게 사용할 수 있다고 말하고자 하는 게 아닙니다. 또한 어리석은 패러다임 논쟁을 반복하고자 함도 아닙니다.

다만 저는 여러분들에게 함수형 프로그래밍과 객체지향 프로그래밍은 상당히 닮았으며, 두 패러다임의 특정한 패턴에 대한 추론을 배우고 나면 더욱 이 두 가지를 깊게 이해할 수 있다는 걸 말씀드리고 싶습니다.

이러한 지식을 통해 두 패러다임의 장점을 적절히 결합하고 각 "해결책"의 아름다움을 감상할 수 있게 됩니다.

맞습니다. 함수형 프로그래밍과 객체지향 프로그래밍 모두 인간 세계의 복잡한 문제들을 더 잘 표현하고 해결하는 방법이 될 수 있습니다. 그리고 이 글에서 저는 커링 함수를 주로 살펴보겠습니다.

이 글은 "객체지향 프로그래밍 개발자를 위한 함수형 프로그래밍"의 하나로 보시면 되겠습니다.

우리는 사실 커링을 매일 쓰고 있습니다

커링에 대해 들어본 적이 없다면 이 설명글을 참고하세요.
커링된 함수는 일반적으로 여러 개의 매개변수를 받지 않습니다. 대신 각 함수는 하나의 매개변수를 받고, 매개변수가 더 필요한 경우라면 함수는 두 번째 또는 그 이상의 매개변수들을 받기 위해 또 다른 "내부" 함수를 반환해야 합니다.
// 전형적인 함수
const sum = (n1, n2, n3) => n1 + n2 + n3;

sum(1, 2, 3); // 6
// 커링된 함수
const sum = (n1) => (n2) => (n3) => n1 + n2 + n3;

sum(1)(2)(3); // 6

특히 자바스크립트에는 커링을 위한 문법적 설탕(syntactic sugar)이 없기 때문에 예쁘게 보이지는 않습니다. 그리고 꽤 바보 같아 보이는 것도 압니다. 하지만 이 방식은 사실 훌륭하며 객체지향 프로그래밍에서 의미만 다르게 동일한 패턴을 사용하고 있습니다.

커링을 통해 무엇을 얻을 수 있을까요? 이 질문에 답을 하기 위해 먼저 객체지향 프로그래밍에 관해 얘기해야겠네요 (잠시만 기다려 주세요). 객체지향 프로그래밍 이전에, 데이터는 더미 가방에 넣어졌습니다. 구조체, JSON, 객체 등 어떤 이름으로 부르든 이것들은 하나의 가방 역할을 할 뿐입니다. (인생과 같이) 복잡한 애플리케이션에서 추상화는 많은 복잡함을 숨기고 직면한 문제에만 집중할 수 있게 합니다.

이러한 가방에서 작업하려면, 우리는 가방의 값을 사용하여 또 다른 가방이나 원시값을 생성하는 함수를 작성합니다.

const user = {
  username: "mhashim6",
  firstName: "Muhammad",
  lastName: "Hashim",
  email: "msg at mhashim6.me",
};

const fullName = (user) => `${user.firstName} ${user.lastName}`;

이 두 가지 기능으로만 복잡한 표현을 생성한다고 가정해 보세요. 모든 곳에 전역 값을 두지 않고 어떤 인스턴스가 더 이상 필요하지 않은지 걱정하지 않으면서 여러 유저를 인스턴스화하고 작업하는 것은 얼마나 힘들고 중복이 많을까요?

이러한 데이터 가방을 각 기능과 컨텍스트 또는 데이터의 상태와 암시적으로 연결하여 컨텍스트에 맞게 생성한다면 더 직관적이지 않을까요? 이것이 바로 객체를 컨텍스트에 그리고 덜 바보 같게 만들고자 객체지향 프로그래밍을 설계한 이유입니다. 데이터 가방 콘텐츠의 대부분을 추상화하여 현실 세계의 객체처럼 컨텍스트에 맞는 동작(메서드)을 갖기 위해서입니다.

객체지향 프로그래밍으로 들어가 봅시다

객체지향 프로그래밍에서는 객체라는 추상화에 데이터와 기능을 결합할 수 있습니다.

class User {
  constructor(username, firstName, lastName, email) {
    this.username = username;
    this.firstName = firstName;
    this.lastName = lastName;
    this.email = email;
  }

  fullName = () => "${this.firstName} ${this.lastName}";
}

fullName이 매개변수를 더 이상 받지 않는 게 보이시나요? 컨텍스트(필드로 저장되는 생성자 매개변수)에 바인딩되기 때문입니다. 그리고 각 User 객체의 인스턴스는 각자만의 컨텍스트와 데이터 필드의 집합을 가집니다.

이러한 기본적인 그룹화는 많은 코드를 복제하거나 모든 프로시저 호출에서 컨텍스트(데이터 필드)를 가질 필요 없이 대부분의 복잡한 객체와 동작을 모방할 수 있는 추상화를 향한 첫 번째 단계였습니다.

함수형 프로그래밍 또는 커링과 어떤 관련이 있나요?

모든 것이 관련이 있습니다! 커링은 함수에서 장기적인 컨텍스트를 정의하여 밀접하게 연관된 다른 함수에서 암시적으로 사용할 수 있도록 하는 방법입니다.

이를 더 자세히 설명하고자 문제를 구성하고 해결해 보겠습니다.

class NumberScaler {
  constructor(value) {
    this.field = value;
  }

  scaledBy = (factor) => this.field * factor;
}

const five = new NumberScaler(5);
const fiveScaledBy2 = five.scaledBy(2); // 10
const fiveScaledBy14 = five.scaledBy(14); // 70

위 코드에서 5의 값을 가지는 NumberScaler 객체를 생성했습니다. 이제 이 객체를 느리게 사용하여 전달한 초깃값(들)에 대해 더 많은 연산을 수행하여 객체 내 데이터의 값을 증가시킬 수 있습니다.

이제 함수만을 사용하여 동일한 작업을 수행한다고 가정해 봅시다.

const numberScaler = (value, factor) => value * factor;

const fiveScaledBy2 = numberScaler(5, 2); // 10
const fiveScaledBy14 = numberScaler(5, 14); // 70

결과 값은 같지만, 숫자 5의 값을 조정할 때마다 함수에 컨텍스트를 열심히 제공해야 한다는 것을 알 수 있습니다. 만약 더 많은 매개변수를 전달해야 하는 복잡한 예시였다면, 매번 모든 매개변수를 전달하거나 매개변수를 담는 가방을 만들어 작업하는 것은 아주 성가신 일이었을 겁니다.

실제로 우리가 한 작업을 객체지향 프로그래밍 방식으로 나타내면 다음과 같습니다.

class NumberScaler {
  constructor(value, factor) {
    this.field = value;
    this.factor = factor;
  }

  scaled = () => this.field * this.factor;
}

const fiveScaledBy2 = new NumberScaler(5, 2).scaled(); // 10
const fiveScaledBy14 = new NumberScaler(5, 14).scaled(); // 70

차이점 (그리고 문제점) 이 무엇인지 아시겠나요? 객체를 생성한 후에는 더 이상 객체에서 로직의 일부를 재사용할 수 없습니다. 숫자의 크기를 조정하려면 매번 새로 만들어야 합니다. 일반적으로 이 방법이 "틀리지는" 않지만, 제한적이며 객체로 많은 일을 할 수 없게 됩니다. 원래 사용하던 데이터 가방보다 약간 덜 멍청할 뿐입니다!

다시 함수형 프로그래밍으로 돌아가서, 함수에서 암시적인 컨텍스트를 어떻게 구현했었나요? 값들을 저장하는 클로저를 구성하는 방식을 사용했었죠!

const numberScaler = (value) => (factor) => value * factor;

const fiveScaler = numberScaler(5); // factor 매개변수를 받아 5를 곱하는 새로운 함수를 반환
const fiveScaledBy2 = fiveScaler(2); // 10
const fiveScaledBy14 = fiveScaler(14); // 70

보이시나요? 마치 초깃값으로 "생성자"를 생성하고 나중에 초깃값을 사용하는 것과 비슷하네요! 이것이 바로 우리가 한 일입니다. 숫자 5의 크기를 조정하기 위해 제공한 factor를 취하는 다른 함수의 팩토리 함수인 것처럼, 매개변수를 하나만 가지는 numberScaler 함수를 부분적으로 적용했습니다. 이를 (두구두구...) 커링된 함수의 "부분 적용"이라고 부릅니다.

결과

많은 변경사항 없이, 유용하고 재사용할 수 있는 작업을 위해 두 가지 모델을 모두 사용할 수 있습니다.

// OOP
const doubler = new NumberScaler(2);

doubler.scaledBy(5); // 10
doubler.scaledBy(6); // 12
doubler.scaledBy(7); // 14
// FP
const doubler = numberScaler(2);

doubler(5); //10
doubler(6); //12
doubler(7); //14

데이터 가방은 훨씬 더 다재다능해졌으며, 사용자 정의 코드를 작성하지 않고도 자체적으로 많은 작업을 수행할 수 있습니다. 가장 중요한 것은 객체지향 프로그래밍과 함수형 프로그래밍 모두에서 이를 달성했다는 점입니다! 제가 보기엔 함수형 프로그래밍의 방식이 훨씬 더 간단하고 우아합니다.

회고

객체지향 프로그래밍과 함수형 프로그래밍 모두에서 다른 유형의 암시적인 컨텍스트를 사용하여 거의 비슷한 방식으로 문제를 해결했습니다. 객체지향 프로그래밍에서는 객체 필드를 사용했고, 함수형 프로그래밍에서는 커링 함수를 사용했습니다. 이를 통해 코드를 느리게 실행할 수 있으며 불필요한 중복도 제거할 수 있습니다.

전역 데이터 가방이 필요하지 않으며 프로시저를 실행하기 위해 반복 작업을 수행하지 않아도 됩니다. 또한 객체 인스턴스나 함수 참조가 파괴될 가능성에 대해 걱정할 필요도 없습니다. 우리는 그저 로직의 추상적인 표현식에만 집중하면 됩니다.

3개의 댓글

comment-user-thumbnail
2023년 5월 9일

우와 너무 참신해서 여러모로 응용해보고 싶네요! 좋은 글 감사합니다!

답글 달기
comment-user-thumbnail
2023년 5월 9일

커링을 어렵게만 생각했는데 저렇게 사용하면 많은 중복을 제거할 수 있겠네요. 감사합니다.

답글 달기
comment-user-thumbnail
2023년 5월 11일

java만 써본 입장에서, class로만 객체의 상태와 행동을 정의해왔었는데,
함수로도 클로저와 이를 바탕으로 커링함수를 구현하여,
상태값을 갖는 객체를 만들수 있다는것을 알게되었네요.
좋은 글 감사합니다.

답글 달기