업데이트:

카테고리:

/

태그: , ,

선언형과 함수형 프로그래밍

선언형은 ‘무엇을’ 풀어내는가에 집중하는 패러다임이며, “프로그램을 함수로 이루어진 것이다”라는 명제가 담겨 있는 패러다임

  const ret = [1, 2, 3, 4, 5, 11, 12].reduce((max, num) => num > max ? num : max, 0)
  console.log(ret)

reduce()는 ‘배열’만 받아서 누적한 결과값을 반환하는 순수 함수이다. 순수함수 → 로직 → 고차 함수(재사용성 높힘)
자바스크립트는 함수가 일급 객체이기 때문에 함수형 프로그래밍이 선호된다.

  1. 순수 함수
    • 동일한 입력에는 항상 같은 값을 반환
    • 함수의 실행이 프로그램의 실행에 영향을 미치지 않는다.
    • 함수 내부에서 인자의 값을 변경하거나 프로그램 상태를 변경하는 Side Effect가 없다.
  2. 고차 함수
    • 함수가 함수를 값처럼 매개변수로 받아 로직을 생성할 수 있는 것
    • 함수의 반환 값으로 또 다른 함수를 사용할 수 있다.
  3. 일급 객체
    • 변수나 메서드에 함수를 할당할 수 있다.
    • 함수 안에 함수를 매개변수로 담을 수 있다.
    • 함수가 함수를 반환할 수 있다.
    • 할당에 사용된 이름과 관계없이 고유한 구별이 가능하다.
    • 동적으로 프로퍼티 할당이 가능하다
  4. 비상태, 불변성
    • 함수형 프로그래밍에서의 데이터는 변하지 않는 불변성을 유지
    • 데이터 변경이 필요한 경우, 원본 데이터 구조를 변경하지 않고 그 데이터의 복사본을 만들어서 그 일부를 변경하고, 변경한 복사본을 사용해 작업한다. ```js // 비상태, 불변성 만족 const person = { name: “jongmin”, age: “26” };

    function increaseAge(person) { return { …person, age: person.age + 1 }; } ```

장점

  1. 높은 수준의 추상화를 제공
  2. 함수 단위의 코드 재사용이 수월
  3. 불변성을 지향하기 때문에 프로그램의 동작 예측이 쉬워짐

단점

  1. 순수함수를 구현하기 위해서는 코드의 가독성이 안 좋을 수 있다.
  2. 함수형 프로그램에서는 반복이 for이 아니라 재귀를 통해서 이루어지는데, 재귀적 코드 스타일은 무한 루프에 빠질 수 있다.
  3. 순수함수를 사용하는 것은 쉽지만 조합하는 것은 쉽지 않다.

객체지향 프로그래밍(OOP, Object-Oriented Programming)

객체들의 집합으로 프로그램의 상호 작용을 표현하며 데이터를 객체로 취급하여 객체 내부에 선언된 메서드를 활용하는 방식
설계에 많은 시간이 소요되며 처리 속도가 다른 프로그래밍에 비해 상대적으로 느리다.

  const ret = [1,2,3,4,5,11,12]
  class List {
    // 최대값을 추출하는 로직
    constructor(list) {
      this.list = list
      this.mx = list.reduce((max, num) => num > max ? num : max, 0)
    }

    getMax() {
      return this.mx
    }
  }
 
 const a = new List(ret)
 console.log(a.getMax()) // 12

특징

  1. 추상화
    • 복잡한 시스템으로부터 핵심적인 개념 또는 기능을 간추려내는 것
    • 어떤 사람의 특징으로 군인, 장교, 키180, 여친있음, 안경씀, 축구못함, 롤마스터… 의 특징이 있을 때 ⇒ 군인, 장교만 뽑아낸다.
  2. 캡술화
    • 객체의 속성과 메서드를 하나로 묶고 일부를 외부에 감추어 은닉하는 것
  3. 상속성
    • 상위 클래스의 특성을 하위 클래스가 이어 받아서 재사용하거나 추가, 확장하는 것
    • 코드의 재사용 측면, 계층적인 관계 생성, 유지 보수성 측면에서도 중요
  4. 다형성
    • 하나의 메서드나 클래스가 다양한 방법으로 동작하는 것

오버로딩
같은 이름을 가진 메서드를 여러 개 두는 것
메서드의 타입, 매개변수의 유형, 개수 등으로 여러 개를 둘 수 있으며 컴파일 중에 발생하는 ‘정적’다형성 이다.

    class Person {
      public void eat(String a) {
        System.out.println('I eat ' + a);
      }

      public void eat(String a, String b) {
        System.out.println('I eat ' + a + ' and ' + 'b');
      }
    }

    public class CalculateArea {
      public static void main(String[] agrs) {
        Person a = new Person();
        a.eat('apple'); 
        a.eat('banana', 'tomato');
      }
    }

    /*
    I eat apple
    I eat banana and tomato
    */

같은 이름의 함수이지만 매개변수의 갯수에 따라서 다른 함수가 호출된다.

오버라이딩

상위 클래스로부터 상속받은 메서드를 하위 클래스가 재정의하는 것을 의미, 이것은 런타임 중에 발생하는 동적 다형성이다.

  class Animal {
    public void bark() {
      System.out.println('mumu! mumu!');
    }
  }

  class Dog extends Animal {
    @Override
    public void bark() {
      System.out.println('wal! wal!')
    }
  }

  public class Main {
    public static void main(String[] args) {
      Dog d = new Dog();
      d.bark()
    }
  }

부모 클래스는 mumu! mumu!로 정의되어 있지만 자식 클래스에서 메소드를 재정의하여 wal! wal!이 출력됩니다.

설계 원칙

SOLID원칙을 지켜주어야 한다.

  1. 단일 책임 원칙(Single Responsibility Principle)

    모든 클래스는 각각 하나의 책임만 가져야 하는 원칙

    예를 들어 A라는 로직이 있으면 어떠한 클래스는 A에 관한 클래스이고, 아를 수정해야한다고 했을 때 A와 관련된 수정이어야 한다.

    R1280x0

    Car 클래스는 4가지 책임을 가지고 있어 SRP를 위반하고 있다.

    Driver가 사용하는 코드를 변경하게 된다면 나머지 Client도 재컴파일 및 재배포를 해야한다.

    한 클래스에 많은 책임이 있는 경우 책임 중 하나를 변경할 때 다른 책임에 영향이 가 버그를 발생시킬 수 있다.

    R1280x0 (1)

    이런식으로 책임별로 클래스를 분리하면 단일 책임 원칙을 따르게 되서 어떠한 변경이 생겨도 관련 없는 동작에는 영향을 끼치지 않게 된다.

    장점

    • 쉬운 테스트 - 책임이 하나인 클래스는 테스트 케이스가 줄어들기 때문에 테스트가 쉬워짐
    • 낮은 결합 - 단일 클래스의 기능이 적어져 종속성이 줄어듦
    • 쉬운 검색 - 작고 잘 조작된 클래스는 모놀리식 클래식보다 검색하기 쉬움
    • 구현하기 쉬움 - 하나의 책임만 가지고 있기 때문에 구현 및 이해가 쉬움
  2. 개방-폐쇄 원칙(Open Closed Principle)

    유지 보수 사항이 생긴다면 코드를 쉽게 확장할 수 있도록 하고 수정할 깨는 닫혀 있어야 하는 원칙, 기존의 코드는 잘 변경하지 않으면서도 확장은 쉽게 할 수 있어야 한다.

    이 원칙을 지키기 위해서 주로 객체지향의 추상화와 다형성을 활용한다.

  3. 리스코프 치환 원칙(Lsikov Substitution Principle)

    프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스f로 바꿀 수 있어야 한다.

    클래스는 상속이 되기 마련 부모, 자식이라는 계층 관계가 만들어진다. 이때 부모 객체에 자식 객체를 넣어도 시스템이 문제없이 돌아가게 만드는 것을 의미한다.

    상위 타입인 Item과 그 하위 타입 Apple이라면, 위 인자로 item이 아닌 apple을 넘겨도 동작해야 된다.

    대표적인 예는 ‘직사각형-정사각형 문제’이다.
    직사각형은 정사각형이 아니지만, 정사각형은 직사각형이라는 사실을 알고 있다.

       public class Rectangle {
         private int width;
         private int height;
            
         public void setWidth(final int width) {
           this.width = width;
         }
    
         public void setHeight(final int height) {
           this.height = height;
         }
    
         public int getWidth() {
           return width;
         }
    
         public int getHeight() {
           return height;
         }
       }
    
       // 직사각형 객체를 상속 받아 정사각형 객체를 정의할 수 있다.
       // 정사각형은 가로와 세로가 길이가 같으므로 메소드를 재정의함
       public class Square extends Rectangle {
         @Override
         public void setWidth(final int width) {
           super.setWidth(width);
           super.setHeight(width);
         }
    
         @Override
         public void setHeight(final int Height) {
           super.setWidth(height)
           super.setHeight(height)
         }
       }
    

    세로의 길이를 늘릴는 메소드

     public void increaseHeight(final Rectangle rectangle) {
       if (rectangle instanceof Square) {
         throw new IllegalStateException();
       }
    
       // 가로의 길이가 더 길다면 세로의 길이를 가로의 길이 + 1 로 바꾼다.
       if (rectangele.getHeight() <= rectangle.getWidth()) {
         rectangle.setHeight(rectangle.getWidth() + 1)
       }
     }
    

    정사각형일 경우 문제가 생기기 때문에 익셉션을 발생시키는 방향으로 코드를 만들 수 있지만 이건 개방-폐쇄 원칙에 어긋난다. increaseHeight()가 확장에는 열려있지 않기 때문이다.
    따라서 Square 클래스는 Rentangle 클래스를 상속받으면 안된다.

    Rectangle 클래스의 setHeight는 높이만 변경되고 폭은 그대로 유지되지만, Square 클래스의 setHeight()는 높이와 폭이 모두 바꾸게 된다. 따라서, 상위 타입에서 정한 명세를 하위 타입에서도 그대로 지킬 수 있을 때 상속을 해야된다.

  4. 인터페이스 분리 원칙(Interface Segregation Principle) 하나의 일반적인 인터페이스보다 구체적인 여러 개의 인터페이스를 만들어야 하는 원칙

    예제
    image

    Vehicle는 go(), fly() 메서드를 가진 추상 클래스이다.
    Aircraft와 Car는 모두 Vehicle를 상속받지만 Car는 날 수 없으니 fly()를 예외처리 해야되는데 Vehicle를 상속받기 때문에 fly를 구현해야되지만 Car는 fly를 사용하지 않는다. 이것은 ISP를 위반한다.

    ISP 적용 후
    image Vehicle 인터페이스를 Movable, Flyable 인터페이스로 나누고 Flyable 클래스를 Movable 클래스에 상속받도록 한다.

    이렇게 되면 Car 클래스는 go() 메서드만 구현하면 되고 fly 메서드를 구현할 필요가 없어 ISP를 만족한다.

    즉, ISP는 클라이언트별로 세분화된 인터페이스를 만들고 클라이언트트 사용하지 않는 메서드를 구현하지 않도록 주의해야된다.

  5. 의존 역전 원칙(Dependency Inversion Principle) 자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향받지 않게 하는 원칙

절차형 프로그래밍

로직이 수행되어야 할 연속적인 계산 과정으로 이루어져 있다. 일이 진행되는 방식으로 그저 코드를 구현하기만 하면 되기 때문에 계산작업에 많이 사용한다. 대표적으로 대기 과학 관련 연산 작업, 머신 러닝의 배치 작업이 있다.

  const ret = [1, 2, 3, 4, 5, 11, 12]
  let a = 0
  for (let i =0; i < ret.length ; i++){
    a = Math.max(ret[i], a)
  }
  console.log(a)

장점

  • 코드의 가독성이 좋다
  • 실행속도가 빠르다.

단점

  • 모듈화 하기 어렵다.
  • 유지보수가 어렵다.