프로그래밍 패러다임
프로그래머에게 프로그래밍의 관점을 갖게 해주는 개발 방법론
소프트웨어 설계와 구현 과정에 큰 영향을 미침.
- 객체지향 프로그래밍 : 객체지향 프로그래밍은 상호작용하는 객체들의 집합으로 프로그램을 구성하는 패러다임
- 함수형 프로그래밍 : 함수형 프로그래밍은 상태 값을 지니지 않는 순수 함수와 함숫값들의 연속으로 프로그램을 구성
(이후에 더 깊게 설명할 예정)
Java의 프로그래밍 패러다임
jdk 1.8 이전의 자바는 객체지향 프로그래밍을 지원했었지만, 이후로는 함수형 프로그래밍 패러다임을 지원하기 위해 람다식, 생성자 레퍼런스, 메서드 레퍼런스를 도입함. - 각 방식에 대해서는 이후에 따로 정리해 볼 예정
프로그래밍의 패러다임은 크게 선언형, 명령형으로 나뉘며
선언형은 함수형이라는 하위 집합을 가짐. 또한, 명령형은 다시 객체지향, 절차지향으로 나뉨.
선언형 프로그래밍과 함수형 프로그래밍
선언형 프로그래밍(Declarative Programming)은 '무엇을' 해야 하는지에 집중하는 프로그래밍 패러다임으로, 문제를 해결하기 위한 결과에 초점을 맞춘다. 이는 절차적 프로그래밍(명령형 프로그래밍)과 대조되는 개념으로, 어떻게 해결하는지는 추상화되어 있다.
- 특징: 코드의 간결성, 높은 가독성, 병렬 처리에 적합
- 대표적인 예시: SQL, HTML, CSS, 함수형 프로그래밍, 스트림 API 등
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
// 순수 함수 reduce() 사용
Integer result = list.stream().reduce((a, b) -> a + b).get();
// 메서드 레퍼런스 사용 -> Integer::sum
Integer result2 = list.stream().reduce(Integer::sum).get();
위의 reduce() 메서드는 리스트의 각 요소를 누적하여 최종 결과를 반환하는 순수 함수이. Integer::sum은 메서드 레퍼런스를 사용한 예제로, 코드의 가독성을 높여준다.
함수형 프로그래밍은 이와 같은 작은 순수 함수들을 블록처럼 쌓아 로직을 구현하고 고차 함수를 통해 재사용성을 높인 프로그래밍 패러다임이다.
*참고* 메소드 레퍼런스란?
메소드 레퍼런스(Method References)
Java 8에서 도입된 방법. Lambda 표현식을 더 간단하게 표현하는 방법.
Consumer<String> func = text -> System.out.println(text);
func.accept("Hello");
해당 Hello를 출력하는 람다식을 Method References를 사용하면 더 가독성 높게 표현이 가능하다.
Consumer<String> func = System.out::println;
func.accept("Hello");
나는 Java가 주력 언어가 아니기에 깊게 다루지는 않지만, 언젠간 쓸 일이 생기지 않을까 싶어 레퍼런스를 남긴다.
그렇다면, 함수형 프로그래밍의 특징인 순수 함수와 고차 함수에 대해 알아보자.
1. 순수 함수
순수 함수는 출력이 입력에만 의존하며, 외부 상태나 전역 변수의 영향을 받지 않는다.
public int sum(int a, int b) {
return a + b;
}
위의 예시처럼, 함수는 입력값 a,b에만 의존하며 이외의 다른 변수인 c 같은 변수들에는 영향을 주지 않는다.
int c = 5;
public int sum(int a, int b) {
return a + b + c; // 전역 변수 c에 의존
}
예를 들어 위와 같이 입력이 a와 b만 있는데, 출력이 c (전역 변수)에도 영향을 준다면 그건 순수 함수가 아니다.
2. 고차 함수
고차 함수는 함수를 값처럼 매개변수로 받거나 반환할 수 있는 함수이다. 이를 통해 재사용성을 높이고, 유연한 로직 구성이 가능하다.
public static int applyFunction(int a, int b, BiFunction<Integer, Integer, Integer> function) {
return function.apply(a, b);
}
public static void main(String[] args) {
// 덧셈 함수 전달
int sum = applyFunction(5, 3, (x, y) -> x + y);
System.out.println(sum); // 출력: 8
// 곱셈 함수 전달
int product = applyFunction(5, 3, (x, y) -> x * y);
System.out.println(product); // 출력: 15
}
위는 고차 함수의 예시이다. 함수를 매개변수에 담아, 원하는 형태로 전달한다.
이처럼 고차 함수는 함수의 동작을 동적으로 변경하거나 확장할 수 있어 코드의 유연성과 재사용성을 크게 높인다.
고차 함수를 사용하기 위해서는 해당 언어가 일급 객체라는 특징을 가져야 한다.
일급 객체(First-class Citizen)
- 변수나 매서드에 함수를 할당할 있다.
- 함수 안에 함수를 매개변수로 담을 수 있다.
- 함수가 함수를 반환할 수 있다.
객체지향 프로그래밍 (Object-Oriented Programming)
객체지향 프로그래밍은 객체를 중심으로 프로그램을 설계하는 방식이다. 객체는 데이터(속성)와 메서드(함수)를 포함하며, 프로그램 내에서 객체들 간의 상호작용을 통해 동작을 구현한다. 객체지향 프로그래밍은 설계에 많은 시간이 소요되며 처리 속도가 다른 프로그래밍 패러다임에 비해 상대적으로 느리다.
객체 Accumulator를 사용하여 리스트의 최대값을 구하는 프로그램이다.
#include <iostream>
#include <vector>
#include <algorithm> // std::max_element
class Accumulator {
public:
// 최대값을 구하는 메서드
int getMax(const std::vector<int>& list) {
if (list.empty()) return 0; // 리스트가 비어있으면 0 반환
return *std::max_element(list.begin(), list.end());
}
};
int main() {
// 숫자 리스트 생성
std::vector<int> list = {1, 2, 3, 4, 5};
// Accumulator 객체 생성 및 getMax() 메서드 호출
Accumulator accumulator;
int result = accumulator.getMax(list);
// 결과 출력
std::cout << "max: " << result << std::endl;
return 0;
}
객체지향 프로그래밍의 특징 (4대 특성)
1. 캡슐화(Encapsulation)
캡슐화란 데이터와 메서드를 하나의 객체로 묶고, 외부에서 객체 내부의 세부 구현을 숨기는 것이다.
C++에서는 private와 public 접근 제한자를 사용하여 데이터 보호가 가능하다.
2. 상속(Inheritance)
상속이란 기존 클래스(부모 클래스)의 속성과 메서드를 새 클래스(자식 클래스)에서 상속받아 재사용하는 것이다. 코드의 재사용 측면, 계층적인 관계 생성, 유지 보수성 측면에서 중요함.
C++에서는 : 키워드를 사용하여 상속을 구현한다.
class Animal {
public:
void eat() { std::cout << "Eating..." << std::endl; }
};
class Dog : public Animal { // Animal로부터 상속
public:
void bark() { std::cout << "Barking..." << std::endl; }
};
int main() {
Dog dog;
dog.eat(); // 부모 클래스의 메서드 사용
dog.bark(); // 자식 클래스의 메서드 사용
return 0;
}
3. 다형성(Polymorphism)
다형성이란 동일한 메서드 이름이 다른 방식으로 동작할 수 있도록 만드는 것이다.
C++에서는 컴파일 시간 다형성과 런타임 다형성을 제공한다.
- 컴파일 시간 다형성: 함수 오버로딩 및 연산자 오버로딩. 컴파일 중에 발생하는 '정적 다형성'
- 런타임 다형성: 오버라이딩(Overriding). 주로 매서드 오버라이딩(Method Overriding)을 의미함. 상위 클래스로부터 상속받은 메서드를 하위 클래스가 재정의하는 것. 런타임 중에 발생하는 '동적 다형성'.
오버로딩과 오버라이딩은 꽤나 중요한 개념이며 그 차이를 제대로 알고 가야할 필요성이 있다.
오버로딩 (Overloading)
오버로딩이란 같은 이름의 메서드가 다른 매개변수를 가지도록 정의하여, 컴파일 시간 다형성을 구현하는 방식이다. 이는 함수의 매개변수 개수나 타입에 따라 호출되는 메서드가 달라진다.
#include <iostream>
#include <string>
class Calculator {
public:
// 두 정수를 더하는 함수
int add(int a, int b) {
return a + b;
}
// 세 정수를 더하는 함수 (매개변수 개수가 다름)
int add(int a, int b, int c) {
return a + b + c;
}
// 두 실수를 더하는 함수 (매개변수 타입이 다름)
double add(double a, double b) {
return a + b;
}
};
int main() {
Calculator calc;
std::cout << "add(2, 3): " << calc.add(2, 3) << std::endl; // 정수 덧셈
std::cout << "add(2, 3, 4): " << calc.add(2, 3, 4) << std::endl; // 세 정수 덧셈
std::cout << "add(2.5, 3.1): " << calc.add(2.5, 3.1) << std::endl; // 실수 덧셈
return 0;
}
오버라이딩 (Overriding)
오버라이딩이란 부모 클래스에서 정의된 메서드를 자식 클래스에서 재정의하여, 런타임 다형성을 구현하는 방식이다.
C++에서는 가상 함수(virtual)를 사용하여 런타임 다형성을 구현하며, 오버라이딩된 메서드는 부모 클래스의 포인터로도 호출할 수 있다.
#include <iostream>
class Animal {
public:
// 가상 함수로 선언하여 런타임 다형성 지원
virtual void sound() {
std::cout << "Animal makes a sound." << std::endl;
}
};
class Dog : public Animal {
public:
// 부모 클래스의 메서드를 오버라이딩
void sound() override {
std::cout << "Dog barks." << std::endl;
}
};
class Cat : public Animal {
public:
// 부모 클래스의 메서드를 오버라이딩
void sound() override {
std::cout << "Cat meows." << std::endl;
}
};
int main() {
Animal* animal;
Dog dog;
Cat cat;
animal = &dog;
animal->sound(); // 출력: Dog barks.
animal = &cat;
animal->sound(); // 출력: Cat meows.
return 0;
}
4. 추상화(Abstraction)
추상화란 불필요한 세부 사항은 숨기고, 필요한 정보만 제공하여 객체의 동작을 단순화하는 것이다.
C++에서는 순수 가상 함수(pure virtual function)를 포함하는 추상 클래스를 통해 구현한다.
class AbstractShape {
public:
virtual void draw() const = 0; // 순수 가상 함수
virtual ~AbstractShape() {} // 가상 소멸자
};
class Triangle : public AbstractShape {
public:
void draw() const override { std::cout << "Drawing Triangle" << std::endl; }
};
int main() {
AbstractShape* shape = new Triangle();
shape->draw(); // 출력: Drawing Triangle
delete shape;
return 0;
}
객체지향 프로그래밍의 장점
- 코드 재사용성 증가: 상속과 캡슐화를 통해 기존 코드를 효율적으로 재사용할 수 있다.
- 유지보수 용이성: 모듈화된 코드로 변경 사항을 쉽게 적용할 수 있다.
- 확장성: 다형성과 추상화를 통해 새로운 기능을 손쉽게 추가할 수 있다.
- 직관적 설계: 객체 단위로 시스템을 설계하므로 실제 문제를 더 잘 모델링할 수 있다.
객체지향 프로그래밍을 설계함에 있어서 SOLID 원칙을 지켜야 한다. (글의 길이상 이 글에서는 다루지 않는다. 따로 레퍼런스를 남겨놓거나, 정리하는 포스팅을 업로드 할 예정이다)
절차형 프로그래밍 (Procedural Programming)
절차형 프로그래밍이란 로직을 수행하기 위한 연속적인 계산 과정을 중심으로 프로그램을 작성하는 방식이다.
진행 순서에 따라 코드를 작성하기만 하면 되므로 직관적이며, 객체지향 프로그래밍에 비해 실행 속도가 빠르다는 장점이 있다. 주로 계산이 많은 작업 등에 쓰인다. 특히, 포트란(fortran)을 이용한 대기 과학 관련 연산 작업 혹은 머신 러닝의 배치 작업이 있다. 그러나, 모듈화 하기가 어렵고 유지 보수성이 떨어진다는 단점이 있다.
절차형 프로그래밍의 특징
- 직관적 설계: 코드의 흐름이 순차적으로 진행되므로 이해하기 쉽다.
- 가독성: 단순한 구조로 작성되므로 코드가 간결하다.
- 성능 우위: 객체 생성과 같은 오버헤드가 없기 때문에 실행 속도가 상대적으로 빠르다.
- 단점:
- 모듈화가 어렵고, 동일한 코드를 반복해서 작성해야 할 가능성이 높다.
- 유지보수성이 떨어지고, 복잡한 시스템에서는 관리가 어려워진다.
C++에서의 예시
#include <iostream>
#include <vector>
#include <climits> // INT_MIN 상수를 위한 헤더
int main() {
// 리스트 생성
std::vector<int> list = {1, 2, 3, 4, 5};
// 최대값 변수 초기화
int max = INT_MIN;
// 리스트 순회하며 최대값 계산
for (size_t i = 0; i < list.size(); ++i) {
if (list[i] > max) {
max = list[i];
}
}
// 결과 출력
std::cout << "max: " << max << std::endl;
return 0;
}
절차형 프로그래밍과 객체지향 프로그래밍 비교
구조 | 순차적 실행을 중심으로 설계 | 객체와 객체 간의 상호작용을 중심으로 설계 |
코드 재사용성 | 낮음 | 높음 |
확장성 | 제한적 | 우수 |
성능 | 상대적으로 빠름 | 약간의 오버헤드 발생 |
적합한 프로젝트 | 단순한 문제나 작은 프로젝트 | 대규모 시스템이나 복잡한 구조 설계 |
'CS > 면접대비용 CS' 카테고리의 다른 글
[CS 기초] 네트워크의 기초 (2) - TCP/IP 4계층 구조 (3) | 2024.12.09 |
---|---|
[CS 기초] 네트워크의 기초 (1) (2) | 2024.12.06 |