프로그래밍을 하다 보면 다양한 방식으로 문제를 해결할 수 있다는 것을 느낀다. 그 중에서 가장 많이 접하게 되는 패러다임들이 바로 선언형, 함수형, 객체지향형, 절차형 프로그래밍이다. 각각의 패러다임은 고유의 특징을 가지고 있으며, 각기 다른 상황에서 장점이 있을 수 있다. 그럼, 각 패러다임이 어떻게 다르고, 어떤 상황에서 유용한지 한번 살펴보자.
선언형 프로그래밍은 "어떻게"
보다는 "무엇을"
해결할 것인지에 집중하는 방식이다. 코드를 작성할 때, 우리가 원하는 결과를 선언적으로 표현하고, 그 결과를 도출하기 위한 구체적인 방법은 컴퓨터가 알아서 처리한다. 즉, 우리는 결과를 선언하고, 컴퓨터는 그걸 어떻게 할지 찾아낸다.
함수형 프로그래밍은 선언형 프로그래밍의 일종으로, 함수가 일급 객체(First-Class Citizens)로 취급된다. 이는 함수를 값처럼 다루어 고차 함수(Higher-Order Function)와 같이, 다른 함수를 인자로 받아서 새로운 함수를 반환하거나 조작하는 방식이다.
함수형 프로그래밍의 주요 특징은 순수 함수(Pure Functions)를 사용한다는 점이다. 순수 함수는 입력값만으로 출력을 결정하며, 부수 효과(side effects)가 없다.
🔍 예시 - 배열에서 최댓값을 찾기:
const numbers = [1, 2, 3, 4, 5, 11, 12];
const max = numbers.reduce((max, num) => (num > max ? num : max), 0);
console.log(max); // 12
이 코드는 배열의 각 값을 순차적으로 비교하여 최댓값을 구한다. 여기서 reduce는 고차 함수로, 배열을 처리하는 방식에 대한 구체적인 내용은 내포되어 있다.
🔑 핵심: 함수형 프로그래밍은 "무엇을" 해결할지를 명확하게 선언하고, 그 해결 방법은 컴퓨터가 알아서 찾아간다. 즉, 코드의 가독성과 재사용성을 높여준다.
객체지향 프로그래밍(OOP)은 프로그램을 객체의 집합으로 바라보고, 객체 간의 상호작용을 통해 문제를 해결한다. 객체는 데이터와 그 데이터를 다루는 메서드를 포함하고 있으며, 객체끼리 메시지를 전달하여 상호작용한다. OOP의 주요 특징은 추상화, 캡슐화, 상속, 다형성이다.
추상화 (Abstraction)
: 복잡한 시스템을 간소화하고 핵심 기능만 노출캡슐화 (Encapsulation)
: 객체 내부 데이터를 보호하고 외부에서 접근을 제한상속 (Inheritance)
: 상위 클래스의 기능을 하위 클래스가 이어받아 재사용다형성 (Polymorphism)
: 같은 메서드가 다른 방식으로 동작하도록 함
🔍 예시 - 배열에서 최댓값을 찾는 객체지향 방식:
class List {
constructor(list) {
this.list = list;
this.max = list.reduce((max, num) => (num > max ? num : max), 0);
}
getMax() {
return this.max;
}
}
const list = new List([1, 2, 3, 4, 5, 11, 12]);
console.log(list.getMax()); // 12
🔑 핵심: OOP는 데이터와 기능을 객체로 묶어, 재사용성과 유지보수성을 향상시킨다. 복잡한 시스템을 효율적으로 관리할 수 있게 도와준다.
원칙 | 영문 약자 | 설명 | 예시 |
---|---|---|---|
단일 책임 원칙 | SRP (Single Responsibility Principle) | 모든 클래스는 하나의 책임만 가져야 하며, 그 책임에 대한 수정만 일어나야 하는 원칙. | 한 클래스에서 UI 로직과 비즈니스 로직을 분리하여 각각의 클래스가 별도의 책임을 갖도록 설계. |
개방-폐쇄 원칙 | OCP (Open Closed Principle) | 기존 코드는 수정하지 않으면서도 확장할 수 있도록 설계해야 하는 원칙. | 새 기능 추가 시 기존 코드를 수정하지 않고 새로운 클래스를 만들어 확장. |
리스코프 치환 원칙 | LSP (Liskov Substitution Principle) | 부모 객체를 사용하는 프로그램에 자식 객체를 대신 넣어도 시스템이 문제없이 동작해야 한다는 원칙. | Animal 클래스를 상속한 Dog 클래스가 Animal 대신 사용되어도 문제없이 동작해야 함. |
인터페이스 분리 원칙 | ISP (Interface Segregation Principle) | 하나의 일반적인 인터페이스보다 구체적이고 작은 여러 개의 인터페이스로 나누어야 하는 원칙. | Printer 인터페이스를 ScanPrinter 와 PrintOnlyPrinter 로 나누어 각각 필요한 기능만 구현. |
의존 역전 원칙 | DIP (Dependency Inversion Principle) | 구체적인 구현보다 추상화된 인터페이스나 상위 클래스에 의존하도록 설계하여, 변화에 유연한 구조를 만드는 원칙. | Car 클래스가 Tire 인터페이스를 사용해 다양한 타이어를 교체할 수 있게 설계. |
SOLID 원칙은 객체지향 설계의 핵심 가이드라인으로, 이를 준수하면 코드의 유지보수성, 확장성, 재사용성이 향상된다. 프로젝트 개발 시 이러한 원칙을 고려하여 설계하면 복잡한 문제도 체계적으로 해결할 수 있다.
구분 | 오버로딩 (Overloading) | 오버라이딩 (Overriding) |
---|---|---|
정의 | 같은 이름의 메서드를 매개변수로 구분하여 여러 개 정의 | 상위 클래스의 메서드를 하위 클래스에서 재정의 |
발생 시점 | 컴파일 시 (정적 다형성) | 런타임 시 (동적 다형성) |
목적 | 매개변수에 따라 다른 동작을 하게 함 | 하위 클래스에서 상위 클래스 메서드를 재정의하여 새로운 동작을 구현 |
특징 | 매개변수의 개수, 타입, 순서가 달라야 함 | 메서드 시그니처가 동일해야 함 |
예시 | void print(int num) void print(String str) | class Dog extends Animal { @Override void makeSound() |
오버로딩
: 메서드 이름은 같지만 매개변수에 따라 구분되며, 컴파일 시점에 메서드 호출이 결정됨.
오버라이딩
: 상속받은 메서드를 하위 클래스에서 재정의하여, 런타임 시점에 메서드 호출이 결정됨.
절차형 프로그래밍은 "순차적"으로 명령을 수행하는 방식이다. 즉, 코드를 실행하는 순서대로 처리가 이루어진다. 로직이 수행되는 순서대로 계산 과정이 이루어지며, 변수에 값을 할당하고, 조건문과 반복문을 사용하여 문제를 해결한다.
🔍 예시 - 배열에서 최댓값을 찾는 절차형 방식:
let numbers = [1, 2, 3, 4, 5, 11, 12];
let max = 0;
for (let i = 0; i < numbers.length; i++) {
max = Math.max(numbers[i], max);
}
console.log(max); // 12
🔑 핵심: 절차형 프로그래밍은 단순하고 효율적인 로직 구현이 가능하다. 계산량이 많은 작업에 유리하다. 하지만 복잡한 시스템에서는 코드의 유지보수성에 어려움이 있을 수 있다.
모든 프로그래밍 패러다임에는 장단점이 존재하며, 비즈니스 로직이나 프로젝트의 특성에 따라 적합한 패러다임을 선택하는 것이 중요하다. 상황에 따라 다양한 패러다임을 조합하는 것이 더 효율적일 수 있다.
🔄 혼합 예시:
🔑 핵심: 프로그래밍 패러다임은 상황에 맞춰 유연하게 선택하고 혼합하는 것이 가장 효율적인 접근법이다.
예를 들어, 간단한 알고리즘을 구현할 때는
함수형 프로그래밍
이 적합하고, 복잡한 비즈니스 로직을 다룰 때는객체지향 프로그래밍
이 유리할 수 있다. 중요한 것은문제 해결에 최적화된 방식을 선택
하는 것이다.
프로그래밍 패러다임을 선택할 때, 하나의 패러다임만 고수할 필요는 없다.
각 패러다임이 가지는 특성과 장점은 문제를 해결하는 방법에 따라 다르게 적용될 수 있기 때문이다. "최고의 패러다임은 없다"는 생각으로 상황에 맞는 패러다임을 조합하여 사용하는 것이 중요하다. 어떤 방식으로 프로그래밍할지 고민하며, 각 패러다임의 특징을 이해하고 유연하게 접근하는 것이 더 좋은 코드를 만들 수 있다. ✨
[ 참고 서적 ] : 주홍철, 면접을 위한 CS 전공지식 노트, 길벗, 2022년 04월 28일.