프로그래머에게 프로그래밍의 관점을 갖게 해주는 역할을 하는 개발 방법론
각 언어는 특정한 패러다임만 지원하기도 하고 여러 패러다임을 지원하기도 합니다.
프로그래밍 패러다임은 다음과 같이 나뉩니다.
선언형 프로그래밍(declarative programming)
'무엇'을 풀어내는가에 집중하는 패러다임이며, "프로그램은 함수로 이루어진 것이다."라는 명제가 담겨 있는 패러다임
함수형 프로그래밍은 선언형 패러다임의 일종으로 해당 프로그래밍 패러다임을 사용하는 대표적인 예시는 Javascript 입니다.
자바스크립트에서 자연수로 이루어진 배열의 최댓값을 구하는 코드를 봅시다.
const list = [1, 2, 3, 4, 5, 11, 12]
const ret = list.reduce((max, num) => (num > max ? num : max), 0)
console.log(ret) // 12
이 코드에서 reduce()
는 '배열'만 받아서 누적한 결괏값을 반환하는 순수 함수입니다.
함수형 프로그래밍(functional programming)
"순수 함수"들을 블록처럼 쌓아 로직을 구현하고 "고차 함수"를 통해 재사용성을 높인 프로그래밍 패러다임
자바스크립트는 단순하고 유연한 언어이며, 함수가 입급 객체이기 때문에 함수형 프로그래밍 방식이 선호됩니다.
출력이 입력에만 의존하는 함수
어떤 외부 상태에 의존하지도 않고 변경시키지도 않는 부수 효과(Side Effect)가 없는 함수 즉, 동일한 입력이 주어지면 항상 동일한 출력을 반환하는 함수.
const pure = (a, b) => {
return a + b
}
이 함수는 입력으로 들어오는 매개 변수 a, b만이 출력에 영향을 미칩니다. 따라서 이 함수는 순수 함수입니다.
만약 a, b 외의 다른 전연 변수들이 출력에 영향을 주면 순수 함수가 아닙니다.
함수가 함수를 값처럼 매개변수로 받아 로직을 생성할 수 있는 함수
다음과 같은 것들이 가능한 객체를 말합니다.
- 변수나 데이터 구조 안에 담을 수 있습니다.
- 파라미터로 전달할 수 있습니다.
- 반환값으로 사용할 수 있습니다.
- 할당에 사용된 이름과 무관하게 고유한 구별이 가능해야 합니다.
함수형 프로그래밍에서 함수는 일급 객체로 취급받기 때문에 고차 함수를 사용할 수 있습니다.
이외에도 함수형 프로그래밍은 커링, 불변성 등 많은 특징이 있습니다.
커링(Currying)
함수에 인자를 하나씩 적용해나가다가 필요한 인자가 모두 채워지면 함수 본체를 실행하는 기법으로 여러 개의 인자를 받는 함수를 단일 인자를 받는 함수의 체인을 이용하는 방식으로 바꾸는 것을 의미합니다.
불변성
값이나 상태를 변경할 수 없는 것을 의미하며, 한 번 생성된 데이터의 값이나 상태를 변경하지 않고 새로운 값을 할당하는 것을 의미합니다.
프로그래밍에서 필요한 데이터를 추상화시켜 상태와 행위를 가진 객체로 만들고, 객체들 간의 상호작용을 통해 로직을 구성하는 프로그래밍 방법입니다.
모듈화, 캡슐화로 인해 유지보수에 용이하고 객체 자체가 하나의 프로그램이기에 재사용에 용이합니다.
하지만 설계에 많은 시간이 소요되며 처리 속도가 다른 프로그래밍 패러다임에 비해 상대적으로 느립니다.
이전에 본 최댓값을 구하는 로직으로 객체지향 프로그래밍으로 변경해보면 다음과 같습니다.
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
객체들이 공통적으로 필요로 하는 속성이나 동작을 하나로 추출해 내는 작업으로 복잡한 시스템으로부터 핵심적인 개념 또는 기능을 간추려내는 것을 의미합니다.
프로그래밍에서 추상화는 클래스를 정의할 때 불필요한 부분들을 생략하고 객체의 속성 중 중요한 것에만 중점을 두어 개략화 하는 것을 말합니다. 즉 추상화로 구현해 두면, 새로운 객체를 만들 때, 클래스의 고유 기능만 새로 만들어주고, 공통된 기능은 상속을 통해서 이용 가능합니다.
예를 들어 사과, 바나나라는 객체가 있을 때, 이 객체들을 하나로 묶을 때 공통적인 특징을 과일로 이름을 붙이는 것을 추상화라고 할 수 있습니다
따라서 공통된 기능을 과일에서 미리 구현하고, 상속을 통해서 공통된 기능을 사과와 바나나에 이어주면 사과와 바나나는 각자의 고유 기능을 집중적으로 개발할 수 있습니다.
덕분에 공통된 기능을 다시 정의할 필요가 없기 때문에 코드의 재사용이 용이해지는 장점이 있습니다.
객체의 속성과 메서드를 하나로 묶고 일부를 외부에 감추어 은닉하는 것을 말합니다.
이러한 캘슐화는 외부에서 알 필요가 없는 부분을 감춤으로써 대상을 단순화하는 추상화의 한 종류입니다.
데이터 보호와 데이터 은닉을 위해 캡슐화를 해야 합니다.
💡 데이터 보호(data protection) – 외부로부터 클래스에 정의된 속성과 기능들을 보호
💡 데이터 은닉(data hiding) – 내부의 동작을 감추고 외부에는 필요한 부분만 노출
상위 클래스의 특성을 하위 클래스가 이어받아서 재사용하거나 추가, 확장하는 것을 말합니다. 코드의 재사용 측면, 계층적인 관계 생성, 유지 보수성 측면에서 중요합니다.
상속은 클래스 간 공유될 수 있는 속성과 기능들을 상위 클래스로 추상화 시켜 해당 상위 클래스로부터 확장된 여러 개의 하위 클래스들이 모두 상위 클래스의 속성과 기능들을 간편하게 사용할 수 있도록 합니다.
덕분에 반복적인 코드를 최소화하고 공유하는 속성과 기능에 간편하게 접근하여 사용할 수 있습니다.
어떤 객체의 속성이나 기능이 상황에 따라 여러 가지 형태를 가질 수 있는 성질을 의미하며 하나의 메서드나 클래스가 다양한 방법으로 동작하는 것을 말합니다.
대표적으로는 오버로딩, 오버라이딩이 있습니다.
같은 이름을 가진 메서드를 여러 개 두는 것을 말합니다.
메서드의 이름은 같고 메서드의 타입, 매개변수의 유형, 개수 등이 달라야 하며, 컴파일 중에 발생하는 '정적' 다형성입니다.
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[] args) {
Person a = new Person();
a.eat("apple");
b.eat("tomato", "grape");
}
}
/**
* I eat apple
* I eat tomato and grape
*/
주로 메서드 오버라이딩을 말하며 상위 클래스로부터 상속받은 메서드를 하위 클래스가 재정의하는 것을 의미합니다.
이는 런타임 중에 발생하는 '동적' 다형성입니다.
class Animal {
public void bark() {
System.out.println("mumu! mumu!");
}
}
class Dog extends Animal {
@Override
public void bark() {
System.out.println("bow! wow!");
}
}
public class Main {
public static void main(String[] args) {
Dog d = new Dog();
d.bark();
}
}
/**
* bow! wow!
*/
객체지향 프로그래밍을 설계할 때는 SOLID
원칙을 지켜주어야 합니다.
모든 클래스는 각각 하나의 책임만 가져야 하는 원칙
A라는 로직이 존재한다면 어떤 클래스는 A에 관한 클래스여야 하고 이를 수정한다고 했을 때도 A와 관련된 수정이어야 합니다. 즉, 클래스를 변경하는 이유는 단 하나여야 합니다.
이를 지키지 않으면 한 책임의 변경에 의해 다른 책임과 관련된 코드에 영향을 미칠 수 있어 유지 보수가 비효율적이게 됩니다.
유지 보수 사항이 생긴다면 코드를 쉽게 확장할 수 있도록 하고 수정할 때는 닫혀 있어야 하는 원칙
즉, 기존의 코드는 잘 변경하지 않으면서도 확장은 쉽게 할 수 있어야 합니다. 따라서 기존 코드를 변경하지 않고 기능을 수정하거나 추가할 수 있도록 설계해야 합니다.
이를 지키지 않으면 하나의 모듈의 기능을 수정하면 그 모듈을 이용하는 다른 모듈들을 모두 고쳐야 되므로 유지 보수가 어려워집니다.
프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 하는 것을 의미합니다.
따라서 하위 타입 객체는 상위 타입 객체에서 가능한 행위를 수행할 수 있어야 합니다.
클래스는 부모-자식 계층 관계를 만들고 이때 부모 객체에 자식 객체를 넣어도 시스템이 문제 없이 돌아가게 만드는 것을 의미합니다. 이는 상속 관계에서는 꼭 일반화 관계가 성립해야 함을 의미하기도 합니다.
하나의 일반적인 인터페이스보다 구체적인 여러 개의 인터페이스를 만들어야 하는 원칙
객체는 자신이 사용하는 메서드에만 의존해야 하고, 인터페이스는 지나치게 광범위하거나 많은 기능을 구현해서는 안된다는 것을 의미합니다.
자신보다 변하기 쉬운 것(구체화된 클래스)에 의존하기 보다는 변하기 어려운 것(추상 클래스나 인터페이스 등 추상적인 것)에 의존해야 한다는 원칙
변하기 쉬운 것의 변화에 영향받지 않게 하는 원칙
즉, 상위 계층은 하위 계층의 변화에 대한 구현으로부터 독립해야 합니다.
고수준 모듈은 저수준 모듈의 구현에 의존하면 안되고 저수준 모듈이 변경되어도 고수준 모듈은 변경이 필요 없는 형태가 이상적입니다.
로직이 수행되어야 할 연속적인 계산 과정으로 이루어진 프로그래밍 패러다임
일이 진행되는 방식으로 그저 코드를 구현하기만 하면 되기 때문에 코드의 가독성이 좋으며 실행 속도가 빠르기 때문에 주로 계산이 많은 작업 등에 쓰입니다. 대표적으로는 머신 러닝의 배치 작업이 있습니다.
하지만 모듈화하기가 어렵고 유지 보수성이 떨어진다는 단점이 있습니다.
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) // 12
어떠한 패러다임이 가장 좋을까요? 답은 "그런 것은 없다."입니다.
비즈니스 로직이나 서비스의 특징을 고려하여 패러다임을 정하는 것이 좋습니다. 또한 하나의 패러다임으로 서비스를 구축하는 것도 좋지만 여러 패러다임을 조합하여 상황과 맥락에 따라 패러다임 간의 장점만 취해 개발하는 것이 좋습니다.
[프로그래밍] 함수형 프로그래밍(Functional Programming) 이란?
객체 지향 프로그래밍(OOP)의 개념과 4가지 특징
객체 지향 프로그래밍의 4가지 특징ㅣ추상화, 상속, 다형성, 캡슐화