[FP] 객체 지향과 함수형

yongkini ·2024년 4월 13일
0

Functional Programming

목록 보기
2/4

객체 지향과 함수형 프로그래밍 비교해보기

class Person {

    constructor(firstName, lastName, age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    getFullName () {
        return `${this.firstName} ${this.lastName}`;
    }
}

class Developer extends Person {

    constructor(firstName, lastName, age, position) {
        super(firstName, lastName, age);
        this.position = position
    }

}

const yongki = new Developer('yongki', 'hong', 32, 'fe');
yongki.getFullName(); // 'yongki hong'

// 여기서 부터는 자료형은 객체 지향으로 여전히 표현하되, 
// 그 기능(메서드)에 있어서 객체 종속된 메서드를 쓰지 않고, 함수형으로 진행해 본다. 

const checkIsPerson = (person) => {
    return person?.firstName && person?.lastName;
}

const getFullName = (person) => {
    return `${person.firstName} ${person.lastName}`
}

const getPersonFullName = (person) => {
    return checkIsPerson(person) ? `${person.firstName} ${person.lastName}` : 'not Person';
}

getPersonFullName(yongki); // 'yongki hong'

class Animal {
    constructor(name, age, kind) {
        this.name = name;
        this.age = age;
        this.kind = kind;
    }
}

const tiger = new Animal('danny', 5, 'tiger');

getPersonFullName(tiger); // 'not Person'

// 유연하게? 동물 객체에서도 해당 함수를 쓸 수 있음. 
// 해당 객체에 이 메서드를 재정의 하지 않아도 쓸 수 있음. ![](https://velog.velcdn.com/images/0715yk/post/2a5be2c0-4cbb-427d-9d3c-b3e9c7a634f1/image.jpg)

Mission

: 어떤 사람과(Person) 거주 국가가 같은 사람을 전부 찾고, 어떤 학생과 거주 국가와 다니는 학교가 모두 같은 학생을 전부 찾기

class Person {

    constructor(firstName, lastName, age) {
        this._firstName = firstName;
        this._lastName = lastName;
        this._age = age;
        this._address = null;
    }

    get address() {
        return this._address;
    }

    set address(addr) {
        this._address = addr;
    }

    peopleInSameCountry(friends) {
        const result = [];

        for(let i=0;i<friends.length;i++) {
            if(this._address === friends[i].address) {
                result.push(friends[i]);
            }
        }

        return result;
    }
}

class Student extends Person {
    constructor(firstName, lastName, age, school) {
        super(firstName, lastName, age, school)
        this._school = school;
    }

    get school() {
        return this._school;
    }

    studentInSameCountryAndSameSchool(students) {
        const result = [];
        let sameCountryStudents = super.peopleInSameCountry(students);
        for(let i=0;i<sameCountryStudents.length;i++) {
            if(sameCountryStudents[i].school === this.school) {
                result.push(sameCountryStudents[i]);
            }
        }

        return result; 
    }
}

const jessie = new Student('jessie', 'park', 32, 'princeton');
const jae = new Student('jae', 'hong', 38, 'yonsei');
const junhee = new Student('junhee', 'lee', 67, 'honkong');
const minji = new Student('minji', 'kim', 30, 'korea');
const jihoon = new Student('jihoon', 'hong', 33, 'yonsei');
const yongki = new Student('yongki', 'hong', 32, 'princeton');
const chaeun = new Student('chaeun', 'hong', 26, 'honkong');

jihoon.address = 'mokdong'
yongki.address = 'pangyo'
chaeun.address = 'gangnam'
jessie.address = 'pangyo'
jae.address = 'mokdong'
junhee.address = 'pangyo'
minji.address = 'gangnam'

jessie.studentInSameCountryAndSameSchool([jae, junhee, minji, jihoon, yongki, chaeun]); // [yongki]

위에 함수를 함수형으로 바꾸면(기능만)

const selector = (address, school) => student => student.address === address && student.school === school;
const findStudentsBy = (friends, selector) => friends.filter(selector);

findStudentsBy([jae, junhee, minji, jihoon, yongki, chaeun], selector('pangyo', 'princeton'));/ / [yongki]

위와 같이 바꿀 수 있고, 이 함수는 유연하게 성이 같고, 학교가 같은 함수로 쉽게 기능을 추가할 수 있다(이전의 기능을 건들지 않고).

const findStudentsBy = (friends, selector) => friends.filter(selector);
const selector = (lastName, school) => student => student._lastName === lastName && student._school === school;

findStudentsBy([jessie, jae, junhee, minji, jihoon, yongki, chaeun], selector('hong', 'princeton')); // [yongki]

위와 같이 selector를 하나 추가해주는 것만으로도 쉽게 기능을 추가할 수 있다. 그리고 이전의 기능은 건들지 않고, 다른 함수를 추가해서 인수를 다르게 적용하는 것만으로도 기능을 추가했기에 확장성이 좋다고 할 수 있다. 기존 객체 지향에서 해당 기능을 추가하려면 클래스에 있는 함수의 인수와 절차 내의 로직을 직접 수정해야한다(이미 있던 기능을 수정해야한다 => 테스팅에 써놓은 변수도 건드려야하고, 여러곳에서 쓰고 있으면 그 여러곳을 모두 찾아서 수정해야하고, 기존 코드를 유연하게 확장할 수 없다는 증거가 된다).

불변성 유지를 위한 상태 관리법

: 불변성 유지에 취약한 JS에서 FP를 하려면 기법을 써야한다. 예를 들어, 클로저 패턴을 써서, 실제로는 순수 함수가 아니지만, 클로저 패턴의 특징을 살려서 java의 final 키워드와 같이 상태를 수정할 수 없도록 하는 패턴을 써서 데이터의 변경을 막을 수 있다.

클로저 패턴

function coordinate(lat, long) {
    let _lat = lat;
    let _long = long;

    return {
        latitude: function () {
            return _lat;
        },
        longitude: function () {
            return _long;
        },
        translate: function (dx, dy) {
            return coordinate(_lat + dx, _long + dy);
        },
        toString: function() {
            return '(' + _lat + ',' + _long + ')';
        }
    }
}

const greenwich = coordinate(51.4778, 0.0015);
const plushGreenWich = greenwich.translate(25, 0.0130);

console.log(plushGreenWich.latitude()); // 76.4778
console.log(plushGreenWich.longitude()); // 0.014499999999999999

위와 같이 해주지 않으면(특정한 기법을 써서 통제를 하지 않으면) 앞서 표현한 객체 지향 코드에서


jihoon._lastName = 'kim'
console.log(jihoon) // Student {_firstName: 'jihoon', _lastName: 'kim', _age: 33, _address: 'mokdong', _school: 'yonsei', …}

이런식으로 막무가내 ?로 성을 바꿀 수 있다(JS가 불변성 관리 측면에서 취약한 언어임을 보여주는 대목?이다.).

Object.freeze

: Object.freeze를 사용하는 방법도 있다. 앞서 말한 클로저 패턴은 유용한 기법이지만, 클로저 패턴으로 실세계의 모든 문제를 모형화 하기에는 한계가 있다. Person, Student와 같이 계층적 데이터를 처리할 필요가 생긴다는 것. 따라서, Object.freeze를 앞선 코드에 적용해볼 수 있다.

const youngji = Object.freeze(new Student('youngji', 'hong', 33, 'yonsei'));

youngji.address = 'pangyo'; 
// error 
// VM1521:15 Uncaught TypeError: Cannot assign to read only // property '_address' of object '#<Student>'
//    at set address [as address] (<anonymous>:15:23)
//    at <anonymous>:1:17
    
youngji._address = 'pangyo'; // error는 발생하지 않음
youngji._lastName = 'pangyo'; // error는 발생하지 않음
console.log(youngji); // Student {_firstName: 'youngji', _lastName: 'hong', _age: 33, _address: null, _school: 'yonsei'} 
// 위에서 error는 발생하지 않았지만, 값이 수정되지 않음 writable false가 됐기에 

Object.freezes는 상속한 속성도 동결한다(but, 중첩된 객체 속성까지 동결하진 못한다). 물론 lodash의 cloneDeep처럼 하나하나 재귀적으로 깊은 동결을 구현할 순 있다.

위에 두가지 방법 모두 한계가 있다..

렌즈(lense) 기법?

: 람다 JS의 lense를 쓰면 리액트에서 cloneDeep을 이용해서 불변성을 유지하며 객체를 깊은 복사하여 setState를 쓸 때 이용하는 것처럼 객체 내의 깊은 depth의 값을 렌즈를 통해 확대해서 특정 값을 깊은 복사하여 바꾼 새로운 객체를 생성하는걸 간편하게 해준다.

함수

: JS의 함수는

  • 일급이다 = 일급 시민이다. 객체이기 때문에.
  • 고계 함수이다. 앞서 말했듯이 일급이기 때문에, 즉, 객체이기 때문에 함수를 인수로 전달하거나, 함수를 반환할 수 있다.

따라서, JS 함수는 일급 + 고계이기에 여느 값이나 다름이 없다. 즉, 자신이 받은 입력값을 기반으로 정의돼, 언젠가는 실행되고, 값을 리턴한다. 이는 모든 함수형 프로그래밍에 깊숙히 자리잡은 기본 원리이기에 기억해두자.

고계 함수 개념을 가지고, FP가 어떻게 유의미한 표현식을 만드는지, 약간은 길어질 코드를 어떻게 심플하게 만드는지 간단한 예시로 살펴보면,

Mission : 판교에 사는 사람들을 출력해보자(위에서 쓴 Person, Student 객체를 사용)

: 함수형 프로그래밍의 기본인 함수(작은 기능)로 쪼개서 생각해보자. 그리고 단일성의 원리로 하나의 함수는 하나의 목표를 갖는다고 생각해보자.

  • 많은 사람들 중
const people = [chaeeun, jessie, jae, junhee, minji, jihoon, yongki];
  • 판교에 사는 사람들을 판별하여
const inPangyo = person => person.address === 'pangyo';
  • 출력한다
console.log

=> 함수를 인수로 쓸 수 있기에 이런 식으로 쓴다.

const printPeople = (people, selector, printer) => people.filter(selector).forEach(printer);

printPeople(people, inPangyo, console.log);

이렇게 하면


function printPeopleInThePangyo(people) {
    for(let i=0;i<people.length;i++) {
        var thisPerson = people[i];
        if(thisPerson.address === 'pangyo') {
            console.log(thisPerson);
        }
    }
}

이렇게 절차형으로 한 것과 달리 유연하게 위의 함수를 쓸 수 있다. 예를 들어, mokdong 거주자를 프린트 하고 싶으면,

const inMokdong = person => person.address === 'mokdong';

printPeople(people, inPangyo, console.log);

이렇게 inMokdong만 추가해주고, 다른 함수들을 건들지 않아도 되기에 확장성이 좋은 편이다.

이렇게 좀 더 편하게 쓸수도 있을 것 같기도 하다.

const inSomeCountry = country => person => person.address === country;

const inPangyo = inSomeCountry('pangyo');
const inMokdong = inSomeCountry('pangyo');

printPeople(people, inPangyo, console.log);

물론 이게 함수형의 정석은 아니고, 이런 사고 방식을 유지하도록 노력해 나아가면 될 것 같다. 앞서 보여준 절차형 프로그래밍 보다 훨씬 유연하여 확장성, 재사용성 측면에서 객관적으로 FP가 좀 더 유리하다고 생각이 든다.

이 때, 위에 내가 만든 로직보다 더 심플하게 하고 싶으면 혹은 유연하게 하고 싶으면 'pangyo', 'mokdong' 등이 필요할 때마다 inSomeCountry 으로 판별식을 만드는 것보다는 아예 판별식 자체에서 매개변수를 받는편이 훨씬 효율적이다.

printPeople(people, inSomeCountry('pangyo'), console.log);

이런식으로 쓸수도 있다는 것. 추가로 아직 렌즈 사용법에 대해서 잘은 모르지만, 렌즈를 쓰면 이렇게도 표현할 수 있다고 한다.

const countryPath = ['address'];
const countryL = R.lens(R.path(countryPath), R.assocPath(countryPath));
const inCountry = R.curry((country, person) => R.equals(R.view(countryL, person), country));

people.filter(inCountry('US')).map(console.log);

** 커링은(Currying or curry) 여러개의 인자를 받는 함수가 있을 때 일부의 인자만 받도록 하는 함수를 만드는 기법.

profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자

0개의 댓글