recoen.

(번역)FP와 OOP는 가까운 형제입니다. (객체지향과 함수형)

본 글은 FP and OOP are close siblings 라는 글을 번역한 글입니다.

이 포스트의 목적은 FP와 OOP를 함께 조화롭게 사용할 수 있다는 것을 이야기하려는 것이 아닙니다. 또한 어느것이 더 좋으냐는 패러다임 전쟁이 무가치하다는 것을 반복적으로 이야기하고 싶습니다.

제가 여기서 하고자 하는 말은 FP와 OOP는 굉장히 가까우며, 두 영역에서의 특정 패턴에 대한 추론을 약간이라도 배운다면 진정으로 두 가지를 모두 이해할 수 있다는 것을 말하고자 합니다.

이런 지식을 갖추게 되면 당신은 각 패러다임의 최고의 부분을 결합하고 각각의 해결책의 아름다움을 인정하게 될 것입니다.

맞습니다. OOP와 FP는 둘 다 "같은 문제"에 대한 해결책입니다. - 어떻게하면 인간 세계의 복잡한 문제를 더 잘 표현하고,해결할 수 있는지에 대한 문제말입니다. 그리고 제가 여기서 다루게 될 메인 요리는 바로 : "함수 커링"입니다.

당신은 이 글을 "OOP 개발자를 위한 FP" 정도로 여겨도 괜찮습니다.

당신은 모를 수도 있지만, 매일 이것을 이용하고 있습니다.

커링을 통해서 얻을 수 있는 것이 무엇일까요? 이것에 대한 대답을 얻기 위해서는 OOP에 대해서 이야기를 해야합니다. OOP 전에는, 데이터는 그냥 뭉치에 들어가 있었습니다. 이것을 structs, JSON Objects 등등으로 부르기도 하는데, 이것은 본질적으로 무언가를 담는 것입니다. 복잡한 앱들에서는 추상화는 정말 많은 복잡성을 감춥니다. 그리고 다루어야 할 문제에 집중하게 합니다.

이런 가방들을 작동시키기 위해서, 우리는 이 가방으로부터의 값을 사용하는 함수를 작성해서 또 다른 가방이나 원시 값을 만들어냅니다.

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

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

한번 상상해봅시다. 이 두 가지 기능만으로 복잡한 표현을 만들어야 한다면 얼마나 번거롭고 불필요한 중복이 많을까요? 전역 변수를 무분별하게 사용하지 않고, 어떤 인스턴스가 아직 살아있는지 또는 더 이상 필요하지 않은지에 대한 걱정 없이 여러 사용자를 생성하고 그들에게 작업을 수행하는 것이 어려울 것입니다.

이런 데이터의 가방들을 해당하는 기능에 맞춰서 문맥을 가지도록 만드는 것이 훨씬 더 직관적이지 않을까요? 이것이 부분적으로 OOP가 왜 설계되었는지에 대한 설명이 됩니다.(객체를 더 문맥적으로 만들고, 덜 멍청하게 만드는 것) 데이터 가방 대부분의 내용을 추상화하고, 문맥에 적합한 메서드를 가지도록 만듭니다. 실세계의 객체처럼 말입니다.

Enter OOP

객체지향에서는, 데이터와 기능을 객체라는 하나의 추상 안에 결합할 수 있습니다.

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가 더 이상 파라미터를 받지 않는다는 것입니다. 이것은 문맥에 결합되었습니다. (contructor에 의해 생성되는) 이 User 객체로부터 생성되는 각각의 인스턴스들은 그들만의 문맥과 데이터 필드의 집합을 가지게 됩니다.

이 기본적인 그룹화는 현실 세계의 복잡한 객체들과 행동들을 모방하는 추상화를 향한 첫 걸음이었습니다. 많은 양의 코드 중복이나 절차적으로 호출되는 가운데에서 데이터 문맥을 가지고 다닐 필요도 없게 되었습니다.

이 내용들이 FP와 커링과 어떤 연관성이 있을까요?

모든것과 연관이 있습니다! 커링은 함수에서 긴긴 문맥을 정의내리는 방법입니다. 그래서 암시적으로 다른 밀접하게 연관된 함수들에 사용됩니다.

이것을 증명하기 위해서 한번 문제를 만들고 해결해나가봅시다 :

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

차이점(그리고 문제)를 발견하셨나요? 저희는 객체를 생성한 후에, 더 이상 논리의 일부분을 재사용할 수 없게 되었습니다. 만약 숫자를 확대하려면 매번 새로운 객체를 생성해야합니다. 일반적인 의미에서 이것이 잘못된 것은 아니지만, 이것은 상당히 불편하며 객체를 통해서 많은 작업을 하려할 때 우리를 방해합니다. 원래 데이터 가방의 형태와 별반 다를 것이 없습니다.

다시 FP로 돌아가서, 우리는 어떻게 함수에서 암묵적인 문맥을 구현할 수 있을까요? 이 값들을 보유하기 위해서 클로저를 사용할 수 있습니다.

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

const fiveScaler = numberScaler(5); // returns a new function that accepts a factor parameter to multiply it by 5
const fiveScaledBy2 = fiveScaler(2); // 10
const fiveScaledBy14 = fiveScaler(14); // 70

보셨나요? 이것은 우리가 초기값과 함께 "constructor"를 생성하고 그것을 나중에 재사용한 것과 같습니다. 이것이 정확하게 우리가 한 일입니다. numberScaler 함수를 하나의 매개변수로 부분적으로 적용했습니다. 이것은 마치 5를 곱하기 위한 인수를 제공하는 또 다른 함수의 팩토리처럼 보입니다. 이것은 커링 함수의 부분적용이라고 불립니다.

Byproducts(부산물)

별다른 변화 없이, 우리는 두 가지 모델을 모두 사용하여 정말 유용하고 재사용 가능한 작업을 수행할 수 있습니다.

// 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

이제 우리의 데이터 가방은 훨씬 더 다양하게 사용할 수 있게 되었으며, 어떠한 맞춤 코드를 작성하지 않고도 많은 것을 수행할 수 있습니다. 가장 중요한 것은, 우리가 이를 OOP와 FP 모두를 사용하여 달성했다는 것입니다! 제가 말하건대, FP에서는 훨씬 더 간단하고 우아하게 해결할 수 있습니다.

Retrospection

OOP와 FP 모두에서 우리는 암묵적 문맥의 다른 유형을 사용하여 거의 동일한 방식으로 문제를 해결했습니다. OOP에서는 객체 필드를 사용했고, FP에서는 함수 커링을 사용했습니다. 이것은 우리가 코드를 느긋하게 실행할 수 있게 해주는 것 뿐 아니라, 불필요한 중복성을 제거할 수 있게도 해줍니다.

우리는 전역 데이터 가방이 필요하지 않았습니다. 우리는 절차를 실행할 때마다 반복할 필요가 없었습니다. 저희가 객체 인스턴스나 함수 참조를 다 사용하고 나면 문맥을 파괴할 필요가 없습니다. 저희가 신경 써야 할 것은 로직의 추상적인 표현뿐입니다.