[FP] 객체 지향, 함수형 비교

yongkini ·2024년 8월 23일
0

Functional Programming

목록 보기
6/10

먼저 객체 지향으로 Person, Student 객체 그리고, 그 안에 종속된 메서드를 사용해보자.

class Person {
    constructor(firstname, lastname, ssn) {
        this._firstname = firstname;
        this._lastname = lastname;
        this._ssn = ssn;
        this._address = null;
        this._birthYear = null;
    }

    get ssn() {
        return this._ssn;
    }

    get firstname() {
        return this._firstname;
    }

    get lastname() {
        return this._lastname;
    }

    get address() {
        return this._address;
    }

    get birthYear() {
        return this._birthYear;
    }

    set birthYear(year) {
        this._birthYear = year;
    }

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

    toString() {
        return `Person(${this._firstname}, ${this._lastname})`;
    }

    peopleInSameCountry(friends) {
        var result = [];
        for(let idx in friends) {
            var friend = friends[idx];
            if(this.address.country === friend.address.country) {
                result.push(friend);
            }
        }
        return result;
    }
}

class Student extends Person {
    constructor(firstname, lastname, ssn, school) {
        super(firstname, lastname, ssn);
        this._school = school;
    }

    get school() {
        return this._school;
    }

    studentsInSameCountryAndSchool(friends) {
        var closeFriends = super.peopleInSameCountry(friends);
        var result = [];
        for(let idx in closeFriends) {
            var friend = closeFriends[idx];
            if(friend.school === this.school) {
                result.push(friend);
            }
        }
        return result;
    }
}

 class Address {
	/**
	 * Construct a new address object
	 * @param country Country code (required)
	 * @param state State code 
	 * @param city City name
	 * @param zip Zip code value object instance  
	 * @param street Street name
	 *
	 */
	constructor(country, state = null, city = null, zip = null, street = null) {
		this._country = country;
		this._state = state;
		this._city = city;
		this._zip = zip;
		this._street = street;
	}
	
	get street() {
		return this._street;
	}

	get city() {
		return this._city;
	}

	get state() {
		return this._state;
	}
	
	get zip() {
		return this._zip;
	}
	
	get country() {
		return this._country;
	}

	set country(country) {
		this._country = country;
		return this;
	}
};

const s1 = new Student('111-11-1111', 'Haskell', 'Curry', 'Princeton', 1900, new Address('US'));	
s1.address = new Address('US');

const s2 = new Student('222-22-2222', 'Barkley', 'Rosser', 'Princeton', 1907, new Address('Greece'));
s2.address = new Address('England');

const s3 = new Student('333-33-3333', 'John', 'von Neumann', 'Princeton', 1903, new Address('Hungary'));
s3.address = new Address('US');

const s4 = new Student('444-44-4444', 'Alonzo', 'Church', 'Princeton', 1903, new Address('US'));
s4.address = new Address('US');

s4.studentsInSameCountryAndSchool([s1,s2,s3]) // [s1, s2]

위의 코드를 바탕으로 분석해보면(나만의 관점)

  • 객체 지향 설계는 특정 객체의 데이터 설계에 종속적으로 메서드를 만들어야 한다. 예를 들어, school 정보는 Student에 있고, 나머지 정보는 Person에 있는 상황에서 studentsInSameCountryAndSchool 를 만들기 위해서는 Student 클래스에 정의를 해야하고, 나머지 기능은 다른 자식 클래스에서도 사용하기 위해선 상위 클래스인 Person에 만들게 된다. 이런 설계를 하다보면, 나중엔 특별히 사용하지 않는 메서드를 강제로(?) 상속받게 되는 일도 생긴다. 어쨌든 메서드를 하나 만들 때에도 객체의 데이터 설계에 종속되는 부분이 있다. 또 다른 종속성 측면에서의 나름의 불평(?)을 해보면, 만약에 peopleInSameCountry 내부 로직을 바꿔야 한다고 했을 때, 사이드 이펙트로 studentsInSameCountryAndSchool 도 영향을 받는다. 이건 근데 함수형도 마찬가지긴할텐데, 조금 다른 점이 있다. 함수형의 경우 예를 들어, 합성 함수를 구성하는 함수중에 어떤 함수의 로직이 바뀌어야 한다고 했을 때, 그냥 새로운 함수를 만들어서 다시 합성해서 쓰면 된다. 그러나, 객체지향의 경우,, 는 객체지향도 마찬가지인데??.. 이건 내가 잘못 생각했다(하지만 각 클래스 내부의 메서드에 함수형에 비해 좀 더 강하게 종속돼 있다는 측면에서는 함수형이 우위에 있다고 생각한다). 객체지향도 peopleInSameCountry2 같은걸 하나 만들어서 쓰면 된다. 물론 이게 선언형이 아니라는 점은 다르겠지만, 내가 느끼기에 절차형 vs 선언형 이 싸움은 논리적인 경쟁으로는 결판이 안날만한 대결 같다..(물론 나는 선언형에 한표).
  • 이것도 객체지향 내에서 어떻게 설계를 잘하면 가져갈 수 있는 부분일 수 있지만, 함수형만의 장점이라고 부각되는 부분은 좀 더 유연하게 함수를 재사용할 수 있다는 것이다. 일단 앞서 말한 두 메서드는 각각 Person, Student에 종속돼 있다. 물론 이걸 뭐 Public으로 공개할 수 있지만, this 라는 키워드를 써서 객체 내의 값을 참조하고 있기 때문에 사실상 의미가 없다. 하지만, 만약 이걸 클래스에 종속적이지 않은 함수형으로 만들었다면 Person, Student 데이터 스키마에 특정 함수 조합을 쓰다가도, 다른 데이터 스키마에도 유동적으로 재사용해서(함수의 조합을 달리하던지, 데이터 스키마를 유동적으로 받던지 등등) 쓸 수 있다. 물론 school, country가 이미 특정한 값이기 때문에 다른 데이터를 통해 이 메서드를 쓸 상황이 있을지는 모르지만, 사실 이걸 함수형으로 만들었다면, 내부 로직을 좀 더 세분화해서 compose 등을 통해 조합해서 썼을 것이기 때문에 재사용성 측면에서는 확실히 함수형이 더 좋아보인다.
  • 이렇게 혼자 주저리주저리 생각을 적다보니까 결국 객체지향의 매력은 프로그램을 설계할 때 이렇게 좀 더 엄격하게? 세계관을 정해놓고 시작하는게 아닐까 생각이든다. 하지만 FE 개발자로 3년 남짓한 기간동안 일해본 결과...(물론 이게 맞는지는 모르겠다) 엄격한 세계관은 기획과 디자인에 의해 산산조각나는 일이 많다. 그렇기 때문에 분명 그 세계관에 수정을 가하는 일이 많이 생긴다. 그렇다 했을 때, 객체 지향의 관계 종속성은 완벽한 프로그래밍을 위해 선호되는 일종의 코딩 기법이 될 수 있지만, 유지 보수나 수정을 자주 해야한다는 측면에서는 독립적인 순수함수들이 클래스 내의 메서드들 처럼 강하게 결속, 종속돼 있지 않고, 느슨하게 compose, pipe 등을 통해 결속됐다가, 해체됐다가 하는게 더 효율적이지 않을까 싶다(사실 이 장점이 내가 함수형 프로그래밍에 관심을 갖게된 계기이기도 하다).
  • 첨언해보면, 사실 나는 객체지향 프로그래밍을 이전에 웹게임을 만들던 프로젝트에서 밖에 해보지 않았다. 하지만, 그 때 워낙 레거시 코드가 방대했고, 게임 개발자 분들이 만든 객체지향형(?) FE 코드였기 때문에 객체지향을 체험하는 데에는 꽤나 좋은 기회였다. 그 때 내가 느낀건 객체 지향은 앞서 말한 것처럼 뭔가를 수정해야할 때, 절차형과 비슷하게 하나를 고치면 그에 대한 사이드 이펙트가 너무 크다는 것이다. 이미 사이즈가 커진 코드에서 뭔가 하나를 수정했을 때 그 사이드 이펙트까지 다 고려를 해야한다는 점은 업무 효율을 떨어뜨리고, 프로그램 정확성을 떨어뜨렸던 것 같다. 물론 이건 내 실력 부족과 이해도 등의 원인도 있겠지만, 어쨌든 내 경험 내에서 객체 지향과 함수형의 차이(하지만 좀더 함수형 편에서 본,,)는 객체 지향은 세계관 구축에 너무 힘을 빼야한다는 점이다. 그리고 그 세계관이 뭔가 하나라도 바뀌거나 헝클어지면, 대공사를 해야한다는 점이 나에게는 좀 안맞았다.

여기까지 함수형 코드를 보여주지도 않고, 내 관점을 적어봤는데, 그럼 이쯤에서 위의 코드를 함수형으로 만들어보자(객체는 그대로 사용한다).

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

findStudentsBy([s1,s2,s3], selector(s4.address.country, s4.school));  // [s1, s2]

이렇게 보면, 결국 함수형 vs 객체지향은 선언형 vs 절차형.. 이 맞나 싶기도 하다(앞서 말했듯이 나는 선언형 쪽이다..). 어쨌든, 객체지향에서는 각 클래스 내에 메서드가 종속됐고, 클래스 간의 관계에 따라 메서드를 가져다 쓰고 등등 이런 종속성이 확실히 존재했다. 하지만, 함수형으로 만든 함수를 보면, findStudentsBy 같은 경우에는 굳이 이 기능을 위해서만 쓰지 않아도, 다른 함수와 합성 혹은 다른 객체를 위해서 이 함수를 또 사용할 수 있다. 즉, 클래스와 객체 데이터에 종속되는 정도가 상대적으로 낮다. 또한, 재사용성 측면에서도 더 효율적이다.

이에 더해서 함수형 프로그래밍에 익숙해지고, 이 방향으로 사고하다보면, 이제 객체 혹은 데이터라는 것을 고려하는 자세에서 잘게 쪼갠 함수를 어떻게 합성해서 새로운 기능을 만들까 혹은 하나의 함수를 어떻게 더 잘게 쪼개서 재사용하기 좋게할까 등의 고민으로 바뀌게 된다. 나같은 초짜 함수형 프로그래머도 저 로직을보고 lens, reduce 등을 써서 property 조회하는 로직을 또다른 함수로 만들면, selector 자체도 school, country만을 체킹하는 함수에서 좀 더 범용적으로 재사용할 수 있는 함수로 바꿀 수 있지 않을까 라는 생각을 하게 되는데, 이게 그냥 함수형 프로그래밍의 핵심인 것 같다.

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

0개의 댓글