[FP] OOP와 FP 의 차이

yongkini ·2024년 12월 5일
0

Functional Programming

목록 보기
15/21
post-thumbnail

OOP와 FP의 차이(느낀점 위주의 서술)

  • 객체 지향형으로 프로그래밍을 하다보면 관심사의 분리 혹은 그 세계관을 각각의 객체로 정리해서 쓰기 때문에 뭐가 필요할 때 그 세계관 별로 찾아쓰기 좋음. 그런 모델링이 명확하게 구축돼 있으면 기획이 좀 더 뚜렷하게 분리돼 구현되는 느낌이 있음.
    • 클래스 안에 프로퍼티 등의 값을 정의해놓고 공유하고 쓰기 때문에 확실히 같은 클래스 안에 내용물들은 그 세계관의 영향력에서 완전히 자유로울 수 없음. 따라서 함수형처럼 순수함수들로 조합하여 확장성을 자유롭게 가져가는건 어려움.
  • 이에 반해, 함수형 프로그래밍은 조각조각 함수를 순수함수로 만들고, 딱히 어떤 개념이나 세계관에 얽매이지 않고, 함수를 만들어서 조합해서 쓰는 방식이기 때문에 뭔가 세계관의 한정 느낌이 없음. 이게 장단점이 될 수 있는게, 한정 느낌이 없기 때문에 더욱 자유롭게 쓸 수 있다는 장점이 있으나 OOP처럼 딱 이쪽 세계관에서 뭘 찾아서 써야겠다 혹은 유지 보수할 때 뭐를 고쳐야겠다 이런 느낌이 잘 안선다 해야하나.. 아직까지는 이정도 표현에서 말할 수 있을 것 같다.
    • 함수형의 장점은 딱히 저런 모델링, 세계관의 명확한 규정 없이도 그때그때 필요한 함수 조각을 만들어서 이전에 쓰던 조각과 합성해서 쉽게 비즈니스 로직을 추가해나갈 수 있다는 것임. 사실 세계관 정의 등은 그냥 같은 폴더에 모아놓으면 된다고 해석할수도 있음. 또한, OOP처럼 클래스 내부에서 프로퍼티 등을 공유하지도 않고, 순수함수를 지향하는 쪽이기 때문에 이점이 확실하게(클래스처럼 순환참조 이슈나 클래스 간의 관계 고민 등이 필요없음) 장점인 것 같다 .
    • 근데 아직은 내가 익숙치 않아서 그런지 함수형 프로그래밍을 할 때 폴더 구조나 특정 함수를 어디에 종속시켜야 할지가 헷갈린다. 뭔가 정해진 틀 내에서 하는 것에 익숙해져서 그런 것 같기도하다.
  • 물론 플젝을 진행하다 보면 무조건 혹은 대부분 둘중 하나만 쓸수는 없다. FP 만으로 플젝을 완성하는건 유토피아(?)적인 생각이다.. 따라서, 이 둘을 자연스럽게 합쳐서 쓰면 좋을 것 같다. 앞서 말한 아쉬운 부분인 세계관의 정의 등을 위해 OOP 로 설계하고, 그 안에서 순수함수로 조합할 수 있는 것들은 FP 를 통해 개발하면 두 방식의 이점을 다 가져와서 쓸 수 있지 않을까 ?.
  • 실질적인 예시 하지만, 현실적으로 생각해보면, 함수형 프로그래밍을 해도 결국엔 모델링, 세계관의 정의는 불가피하다. 단지 함수형 프로그래밍은 OOP처럼 데이터와 로직을(속성과 메서드)캡슐화하진 않고, 분리하여 관리한다. 하지만, 결국 데이터 구조와 이를 처리하는 함수들의 관계를 모델링 해야한다는 측면에서(로직은 순수함수로, 데이터는 불변 객체나 인터페이스로 표현) 모델링은 한다고 볼 수 있다.
    interface ClassicStockData {
      price: number;
      stockName: string;
      volatility: number;
    }
    
    interface ClassicUSStockData extends ClassicStockData {
      exchangeRate: number;
    }
    
    interface CryptoStockData extends ClassicStockData {
      koreanPremium: number;
    }
    
    // 순수 함수 정의
    const getPrice = (data: ClassicStockData): number => data.price;
    
    const applyExchangeRate = (price: number, exchangeRate: number): number =>
      price * exchangeRate;
    
    const applyKoreanPremium = (price: number, koreanPremium: number): number =>
      price * koreanPremium;
    
    // 핸들러 매핑을 활용한 타입별 처리
    const calculateHandlers = {
      exchangeRate: (data: ClassicUSStockData) =>
        applyExchangeRate(getPrice(data), data.exchangeRate),
      koreanPremium: (data: CryptoStockData) =>
        applyKoreanPremium(getPrice(data), data.koreanPremium),
    };
    
    // 핸들러를 사용한 타입 처리 함수
    const calculatePrice = (
      data: ClassicStockData | ClassicUSStockData | CryptoStockData
    ): number => {
      // 각 타입별 핸들러를 실행하거나 기본 동작 수행
      return "exchangeRate" in data
        ? calculateHandlers.exchangeRate(data as ClassicUSStockData)
        : "koreanPremium" in data
        ? calculateHandlers.koreanPremium(data as CryptoStockData)
        : getPrice(data);
    };
    
    // Example usage
    const classicStock: ClassicStockData = {
      price: 100,
      stockName: "AAPL",
      volatility: 0.2,
    };
    
    const usStock: ClassicUSStockData = {
      price: 100,
      stockName: "GOOGL",
      volatility: 0.3,
      exchangeRate: 1.2,
    };
    
    const cryptoStock: CryptoStockData = {
      price: 100,
      stockName: "BTC",
      volatility: 0.5,
      koreanPremium: 1.3,
    };
    
    // Test
    console.log(calculatePrice(classicStock)); // 100
    console.log(calculatePrice(usStock));      // 120
    console.log(calculatePrice(cryptoStock));  // 130
    
    하지만, 객체 지향형은 아래와 같이(물론 각각 다른 클래스로, 즉, 상속을 안쓸수도 있다) 따로 전통 주식시장, 미국 시장 그리고 신흥 코인 시장을 별개의 세계관으로 나눠서 구현할 것이다.
    class ClassicStockData {
      constructor(
        public price: number,
        public stockName: string,
        public volatility: number
      ) {}
    
      getPrice(): number {
        return this.price;
      }
    }
    
    class ClassicUSStockData extends ClassicStockData {
      constructor(
        price: number,
        stockName: string,
        volatility: number,
        public exchangeRate: number
      ) {
        super(price, stockName, volatility);
      }
    
      getPrice(): number {
        return this.price * this.exchangeRate;
      }
    }
    
    class CryptoStockData extends ClassicStockData {
      constructor(
        price: number,
        stockName: string,
        volatility: number,
        public koreanPremium: number
      ) {
        super(price, stockName, volatility);
      }
    
      getPrice(): number {
        return this.price * this.koreanPremium;
      }
    }
    
    // Example usage
    const classicStock = new ClassicStockData(100, "AAPL", 0.2);
    const usStock = new ClassicUSStockData(100, "GOOGL", 0.3, 1.2);
    const cryptoStock = new CryptoStockData(100, "BTC", 0.5, 1.3);
    
    console.log(classicStock.getPrice()); // 100
    console.log(usStock.getPrice());      // 120
    console.log(cryptoStock.getPrice());  // 130
    
    사실 인간의 머리(?)로 이해했을 때 객체 지향형 코드가 더욱 관련 세계관을 정의하여 유지보수하는 데에 있어서 명확해보인다고 생각한다. 그리고 저정도 수준의 구현을 했을 때 OOP 스타일이 오히려 코드 가독성도 깔끔해보이기도 하다.

    비교 포인트

    1. 데이터와 로직의 분리:
      • OOP는 getPrice 메서드로 데이터와 로직을 함께 캡슐화.
      • FP는 데이터(ClassicStockData)와 로직(calculateHandlers)을 분리.
    2. 확장성:
      • OOP: 새로운 데이터 타입 추가 시, 클래스를 상속받아 새 클래스를 정의해야 함.
      • FP: 핸들러 매핑에 새 함수를 추가하여 확장 가능.
    3. 가독성과 테스트:
      • OOP: 객체 상태를 변경하거나 메서드를 호출해 테스트.
      • FP: 순수 함수로 인해 입력과 출력만 테스트하면 됨.
    4. 복잡성 관리:
      • OOP: 클래스 상속 계층이 깊어질수록 관리가 어려워질 수 있음.
      • FP: 데이터와 로직의 독립성을 유지하여 복잡성을 줄임.
  • OOP의 경우에는 기본 validation class를 만들고, 이를 상속해서 tag, image, title 등에 대한 validation class로 나누고
  • FP 의 경우에는
    const validationHandlers = {
      tag: [validateTagLength, validateTagCharacter, validateTagDuplicated],
      category: [validateCategoryLength, validateCategoryCharacter],
      question: [validateSimilarQuestionLength, validateSimilarQuestionDuplicated],
    };
    
    const validate = (type: keyof typeof validationHandlers, value: string, list?: any[]) => {
      return validationHandlers[type].reduce(
        (acc, validator) => acc.chain((v) => validator(v, list)),
        Either.of(value)
      );
    };
    이런식으로 데이터와 로직을 분리하되, 이런식의 모델링으로 관리해서 만든다.
  • 오늘 배운 것중에 가장 뭔가 확 와닿은건 OOP는 데이터와 로직을 캡슐화해서 관리하지만, FP는 데이터와 로직을 분리해서 관리한다는 것
    • 은근 데이터-로직의 연결이 끈끈해진다는게 플젝 규모가 커지면 복잡하고, 뭔가 수정할 때 상당히 고칠게 많아질 수 있다.

개인 과제 유효성 검사를 OOP를 통해 만들어보고, 함수형 프로그래밍으로도 만들어보자.

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

0개의 댓글

Powered by GraphCDN, the GraphQL CDN