패러다임(Paradigm)은 한 시대의 사람들의 견해나 사고를 지배하는 인식의 틀이나 이론적 체계를 의미합니다.


1. 프로그래밍 패러다임

프로그래밍을 하는 것에 있어 어떤 관점으로 집중해서 문제를 풀어내는가?

1. 선언형 프로그래밍

무엇을 풀어내는가? 에 집중하는 패러다임.

함수형 프로그래밍이 이 패러다임의 일종이다.

const numbers = [1, 2, 3, 4, 5, 11, 12];

const max = numbers.reduce((maxNum, current) => 
    current > maxNum ? current : maxNum, 0
);

console.log(max); // 12

최댓값을 찾는 과정보다 그저 결과를 반환하는 것에 집중한다.
어떻게 처리할지는 컴퓨터나 프레임워크에 위임한다.

이런 함수형 프로그래밍은 순수 함수와 고차 함수를 통해 결과 중심으로 반환하는 프로그래밍 패러다임이다.


순수 함수는 출력이 입력에만 의존하는 함수다.

const pure = (a, b) => {	
	return a + b	
}

console.log(pure(4, 5)); // 9

함수 내부의 로직이 오직 input에만 의존해 output을 만든다는 특징이 있다.


고차 함수는 함수를 input으로 받거나 output으로 반환하는 함수이다.
JavaScript에서는 map, filter, reduce와 같은 고차 함수들이 있다.

어떻게 처리하는지에 대한 로직을 넘겨줌으로 결과에 집중할 수 있게 만든다.


일급 객체는 고차 함수를 쓰기 위해 필요한 특징이다.
그 특징으로는 3가지가 있다.

  • 변수, 메서드에 할당할 수 있다.
  • 함수의 input으로 전달할 수 있다.
  • 함수의 output으로 사용할 수 있다.
// 1. 변수에 함수를 담기
const func = function() {
    return "함수";
};
console.log(func()); // "함수"


// 2. 함수의 input으로 전달
function operate(fn) {
    return fn();
}
console.log(operate(func)); // "함수"


// 3. output으로 함수를 반환
function createMultiplier(multiplier) {
    return function(num) {
        return num * multiplier;
    };
}

const double = createMultiplier(2); // double은 함수가 됨
console.log(double(5)); // 10

2. 명령형 프로그래밍

어떻게 풀어내는가? 에 집중하는 패러다임.

객체지향 프로그래밍, 절차형 프로그래밍이 이 패러다임의 일종이다.

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

선언형과 같은 최댓값을 찾는 코드지만, 이번엔 과정을 직접 하나하나 명시한다.
배열을 순회하고, 비교하고, 값을 갱신하는 방법을 개발자가 컴퓨터에게 지시한다는 것이 선언형과의 차이점이다.

3. 객체지향 프로그래밍

객체들의 집합으로 프로그램의 상호 작용을 표현하는 패러다임.
데이터와 데이터를 처리하는 메서드를 객체라는 것으로 묶어서 다룬다.

class Calculator {
    constructor(numbers) {
        this.numbers = numbers;
    }
    max() {
        let max = this.numbers[0];
        for (const n of this.numbers) {
            if (n > max) max = n;
        }
        return max;
    }
}
const calc = new Calculator([1, 2, 3, 4, 5, 11, 12]);
console.log(calc.max()); // 12

객체지향의 특징은 4가지가 있다.

추상화는 복잡한 시스템에서 핵심적인 개념이나 기능만 간추려내는 것이다.

public interface Animal {
    void sound();
}

캡슐화는 객체의 속성과 메서드를 하나로 묶고, 일부를 외부로부터 감추는 것이다.

public class Account {
    private int balance; // 외부에서 직접 접근 불가

    public void deposit(int amount) {
        if (amount > 0) balance += amount;
    }
    public int getBalance() {
        return balance;
    }
}

외부로부터 감춤으로서 데이터를 안전하게 보호할 수 있다.

상속성은 상위 클래스의 특성을 하위 클래스가 물려받아 재사용하고, 추가하거나 확장하는 것이다.

public class Animal {
    public void eat() {
        System.out.println("먹는다");
    }
}
public class Dog extends Animal {
    public void bark() {
        System.out.println("멍멍");
    }
}
Dog dog = new Dog();
dog.eat();  // "먹는다" - 물려받은 메서드
dog.bark(); // "멍멍"

상속성을 이용하면 재사용성과 유지보수성을 얻을 수 있다.

다형성은 하나의 메서드나 클래스가 상황에 따라 다양한 방법으로 동작하는 것이다.
대표적으로 오버로딩과 오버라이딩이 있다.

오버로딩은 같은 이름의 메서드를 매개변수의 타입, 개수를 다르게 해서 여러 개 정의하는 것이다.

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
    public int add(int a, int b, int c) {
        return a + b + c;
    }
    public double add(double a, double b) {
        return a + b;
    }
}

컴파일 시점에 어떤 메서드를 호출할지 결정되기에 정적 다형성이라고도 한다.

오버라이딩은 상위 클래스로부터 물려받은 메서드를 하위 클래스에서 재정의하는 것이다.

public class Animal {
    public void sound() {
        System.out.println("[동물 울음소리]");
    }
}
public class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}
Animal animal = new Dog();
animal.sound(); // "멍멍"

런타임 시점에 어떤 메서드를 호출할지 결정되므로 동적 다형성이라고도 한다.


객체지향은 설계할 때 SOLID 원칙을 지켜주어야 한다.

단일 책임 원칙(SRP) 은 모든 클래스는 각각 하나의 책임만 가져야 한다는 원칙이다.

하나의 클래스가 변경되어야 하는 이유는 하나뿐이어야 한다.

개방-폐쇄 원칙(OCP) 은 확장에는 열려 있고, 수정에는 닫혀 있어야 한다는 원칙이다.

새로운 할인 정책이 생겨도 기존 코드를 수정하지 않고 클래스를 추가하기만 하면 된다.

리스코프 치환 원칙(LSP) 은 하위 타입은 언제나 상위 타입으로 교체할 수 있어야 한다는 원칙이다.

하위 클래스는 상위 클래스의 동작 규약을 지켜야 한다.

인터페이스 분리 원칙(ISP) 은 하나의 범용 인터페이스보다 구체적인 여러 개의 인터페이스를 만들어야 한다는 원칙이다.

클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.

의존 역전 원칙(DIP) 은 구체적인 클래스가 아니라 추상화(인터페이스)에 의존해야 한다는 원칙이다.

상위 모듈이 하위 모듈의 변화에 영향을 받지 않게 된다.
Spring의 의존성 주입(DI) 이 이 원칙을 기반으로 한다.

4. 절차형 프로그래밍

로직이 수행되어야 할 연속적인 계산 과정으로 이루어진 패러다임.
계산이 많은 곳에 쓰인다.

const numbers = [1, 2, 3, 4, 5, 11, 12];

function findMax(arr) {
    let max = arr[0];
    for (let i = 1; i < arr.length; i++) {
        if (arr[i] > max) max = arr[i];
    }
    return max;
}

function printResult(value) {
    console.log(`최댓값: ${value}`);
}

const max = findMax(numbers);
printResult(max); // 최댓값: 12

일이 진행되는 방식 그대로 코드를 작성하기 때문에 코드의 가독성이 좋고 실행 속도가 빠르다.
대신 모듈화가 어렵고 유지보수성이 떨어진다는 단점이 있다.
C언어가 대표적인 절차형 언어다.

5. 패러다임의 혼합

그렇다면 어떤 패러다임이 가장 좋을까?
정답은 없다. 비즈니스 로직이나 서비스의 특징에 따라 적합한 패러다임을 선택하면 된다.
하나의 패러다임만 고집하기보다 여러 패러다임을 조합해서 사용하는 것이 일반적이다.

Java는 대표적인 객체지향 언어지만, Java 8부터 람다와 스트림을 지원하면서 함수형 스타일도 함께 사용할 수 있게 됐다.
JavaScript 역시 클래스 기반 객체지향과 map, filter, reduce 같은 함수형 스타일을 모두 지원한다.

0개의 댓글