실무에서 주로 사용되는 주요 프로그래밍 패러다임은 3가지다.
1. 절차지향 프로그래밍
- 순차적인 처리 흐름
- 대표 언어: C
- 특징: 데이터와 함수의 분리, 순서대로 실행
- 장점: 단순한 문제 해결에 효율적
- 단점: 큰 프로그램의 유지보수 어려움
2. 객체지향 프로그래밍
- 객체 단위로 설계
- 대표 언어: Java, C++
- 특징: 캡슐화, 상속, 다형성
- 장점: 재사용성, 유지보수 용이
- 단점: 설계가 복잡할 수 있음
3. 함수형 프로그래밍
- 순수 함수와 불변성 강조
- 대표 언어: Haskell, (최근) JavaScript
- 특징: 상태 변경 없음, 부수효과 최소화
- 장점: 테스트 용이, 병렬 처리 유리
- 단점: 러닝커브가 높음
실무 활용:
- 대규모 엔터프라이즈: 객체지향
- 웹 프론트엔드: 함수형 + 객체지향
- 시스템 프로그래밍: 절차지향
현대에는 이러한 패러다임들을 상황에 맞게 혼합해서 사용하는 것이 일반적
1. 절차지향 프로그래밍
- 순차적인 처리가 중심이 되는 프로그래밍 방식이다
- 프로그램의 흐름을 순서대로 처리
- 위에서 아래로 실행되는 구조
- 레시피처럼 단계별 실행
- 데이터와 함수가 분리되어 있다
- 함수는 독립적으로 존재
- 중요 데이터를 전역 변수를 통해 공유
- 데이터를 함수가 필요할 때 사용
- 코드의 재사용성이 낮다
- 전역 변수에 의존적
- 함수 간의 강한 결합
- 모듈화가 어려움
- 단순하고 직관적이다
- 실행 흐름 파악이 쉬움
- 코드 작성이 단순
- 초보자가 이해하기 쉬움
- 특수한 영역에서 주로 사용된다
- 임베디드 시스템
- 시스템 프로그래밍
- 하드웨어 제어
- 간단한 스크립팅
절차지향이라는 말처럼 '절차' 즉, 순서를 중요시하는 패러다임이다. 어떤 문제를 해결하기 위한 코드를 작성한다면 그 문제를 해결하는 절차에 맞게 코드의 순서도 배치되야 한다는 것이다. 예를들어 A라는 문제를 해결하기 위한 단계가 1,2,3이라면 코드도 1,2,3 순서로 작성되야한다.
또한 중요한 데이터는 전역 변수를 사용해서 공통적으로 사용하고 독립된 함수들이 해당 변수에 접근하여 사용한다. 물론 함수 내부에서 임시적으로 사용되는 지역 변수 또한 존재한다.
전역 변수를 공통적으로 사용하기 때문에 전역 변수에 의존적이며 동일한 전역변수를 사용하는 함수들간의 결합력이 강하다고 할 수 있다. 하지만 역으로 그러한 점때문에 함수를 모듈화 하기 힘들어서 그만큼 재사용성이 떨어지게 된다.
즉, 절차지향이란 기본적으로 코드를 절차 순서대로 적용하기 때문에 이에 필요한 데이터를 전역 변수로 관리하게 되고 함수 간의 결합력이 증가하게 되지만 그만큼 특정 부분만을 다른 절차에 사용하기 어려워진다.
하지만 절차지향 패러다임에서는 절차 순서대로 코드를 작성하기 때문에 코드의 흐름 파악이 용이하다. 순서대로 1,2,3 으로 배치되어 있어서 어느 곳에서 어떤 기능이 실행되는지 헷갈릴 일이 없다. 때문에 그만큼 가독성이 좋다.
또한 이러한 특징 때문에 실행 순서나 정확한 타이밍 등이 중요한 분야에서 쓰이게 된다. 시스템, 임베디드, 하드웨어 제어 등에서 사용되는데 이는 이런 시스템들이 순서대로 코드를 실행해야하는 분야이기 때문이다.
예를 들어 로켓 발사같은 경우 1. 발사 전 점검, 2. 엔진 점화, 3. 발사 카운트다운, 4. 리프트오프 등의 순서로 진행되며 공중에서 방향을 제어할 때나 추진체를 분리할 때도 1. 센서 데이터 읽기, 2. 자세 제어, 3. 추진체 분리 등 특정 절차대로만 행동을 제어하게 되기 때문에 절차지향 프로그래밍 방식을 사용하게 된다. 만약에 추진체를 분리하면서 자세도 제어하고 센서로 데이터도 읽고 그러면 그만큼 변수가 늘어나고 위험도가 증가할 것이다.
이처럼 절차지향은 장점도 있지만 그만큼 함수간의 결합력이 높아서 코드를 재사용하기 어렵다. 그런 관계로 프로그램의 규모가 커질 경우 코드 관리와 유지보수가 어려워지는 한계가 있었다. 간단한 제어를 순서대로 한다면 명확한 장점으로 작용하겠지만 100단계가 넘는 경우를 모두 순서대로 제어해야한다면? 그리고 그 중 한개의 단계를 수정해야한다면 상당한 어려움이 있을 것이다.
이러한 복잡성 관리와 현실 세계의 더 나은 모델링을 위해 보완적으로 등장한 것이 객체지향 프로그래밍이다.
2. 객체지향 프로그래밍
- 추상화(Abstraction)
- 공통적인 특성을 추출하여 정의
- 복잡한 내용을 숨기고 핵심만 표현
- 인터페이스와 추상 클래스를 통한 구현
- 캡슐화(Encapsulation)
- 데이터와 메서드를 하나의 객체로 묶음
- 데이터를 보호하기 위한 접근 제어
- 내부 구현을 숨기고 필요한 기능만 외부에 제공
- 객체의 불변성을 보장
- 상속(Inheritance)
- 기존 클래스의 특성을 새로운 클래스가 재사용
- 코드의 재사용성 증가
- 계층적 관계 구성 가능
- 다형성(Polymorphism)
- 같은 이름의 메서드가 다르게 동작
- 오버라이딩: 상속받은 메서드 재정의
- 오버로딩: 같은 이름의 메서드를 다른 매개변수로 정의
코드란 결국 현실의 동작을 모방하여 프로그래밍 언어로 표현한 것인데 만약에 자동차를 코드로 만들 때, 코드 안에 색, 모양, 엔진,가속, 정지, 유턴, 뒤로가기 등 자동차가 가지고 있는 모든 특성을 전부 모방해서 구현하지 않고 핵심 기능인 가속, 색상 같은 부분만 코드로 구현할 수 있는데 이 부분을 추상화라고 한다. 너무 복잡한 내용을 세세하게 표현하지 않고 특정 부분만 코드로 만드는 것이다.
또한 객체 지향에서는 이처럼 추상화를 통해 만들어진 코드를 캡슐화를 통해서 관리한다. 가루약을 캡슐 안에 집어 넣어서 외부의 접근을 차단고 약의 변질을 막는 것처럼 코드를 하나의 클래스로 만들고, 외부에서 해당 클래스를 변경하지 못하도록 관리해서 데이터의 변형을 막는 것이다. 이런 구조로 인해서 데이터의 불변성을 보장할 수 있는데 이는 코드의 상태가 바뀌지 않는다는 것을 뜻한다. 만약에 자동차를 코드로 구현했는데 외부에서 이 데이터를 접근해서 바꿀 수 있으면 자동차였던 코드가 어느 순간 비행기가 되버릴 수도 있을 것이다. 여기에 '날기'라는 기능을 추가됐다면 그것을 자동차에 적용할 수 있겠는가?
객체 지향에서는 이런 캡슐화된 클래스를 사용해서 프로그램을 구현한다. 이때 상속이라는 개념을 사용하게 되는데 이는 다른 클래스의 특성을 받아서 사용하는 방식이다. 특성을 받아서 사용한다는 말은 코드 자체의 상태가 바뀌는 것이 아니라 상속 받은 데이터를 활용한다는 의미다. 즉, 상속을 받은 원본 클래스는 변화가 없지만 외부의 클래스에서 필요한 부분만 받아서 재활용 하는 방식으로 기능을 좀 더 풍부하게 사용할 수 있는 것이다. 또 재활용한다는 말처럼 이는 절차지향보다 코드의 재사용성이 높아졌다는 것을 의미한다.
예를 들어 매개변수로 받은 a와 b라는 데이터를 더해주는 기능을 가진 코드가 있다면 각각 1, 3을 넣을 경우 4가 나올 것이다. 또한 4, 6을 넣을 경우엔 10이 나올 것이다. 그렇다면 이러한 동작을 위해서는 숫자를 받아주는 부분, 그리고 그 숫자를 반환하는 부분이 필요할 것이다. 이처럼 특정 동작을 수행하는 코드를 하나의 단위로 묶어 놓은 것을 메서드라고한다.
그런데 이런 기능을 가진 메서드에 다른 메서드의 데이터를 상속해서 기능을 확장한다면 어떨까?
예를 들면 특정 값을 화면에 표시하는 기능을 가진 메서드의 기능을 상속한다면 반환 받은 10이라는 숫자를 화면에 출력할 수 있을 것이다. 반대로 a-b라는 기능을 가진 메서드가 이 기능을 상속한다면 4-6 = -2라는 결과를 화면에 출력할 수 있을 것이다. 화면에 표시한다는 점은 같지만 실제로 표시하는 부분이 달라졌기 때문에 실제로는 메서드가 다르게 작동한 것과 동일하다. 이를 오버라이딩이라고 하는데 상속받은 코드를 다르게 재정의 하는 부분이다. 또한 이렇게 동일한 코드가 다르게 동작하는 것을 다형성이라고 한다.
반대로 a+b같은 기능을 가진 메서드의 이름을 add라고 정해둔 채로 a+b+c 같은 방식으로 바꾼 코드를 추가로 적을 수도 있을 것이다. 그렇다면 add라는 동일한 이름을 가졌지만 a+b와 a+b+c로 동작하는 2개의 메서드가 만들어지게 되는데 이처럼 같은 이름의 메서드를 다른 매개변수로 정의하는 것을 오버로딩이라고 한다. 이 또한 다형성의 일종이다.
만약에 a+b, a-b, 화면에 출력하는 메서드 이렇게 3개의 메서드가 할 수 있는 조합을 절차 지향으로 만들어야한다면
- a+b
- a-b
- a+b, 출력
- a-b, 출력
- a+b, a-b
- a+b, a-b, 출력
이렇게 끝도 없는 조합의 절차를 각각 따로 만들어야 했을 것이다. 이는 절차지향에 비해서 코드의 재사용성이 압도적으로 높아진 것을 의미한다.
3. 함수형 프로그래밍
- 순수 함수를 사용한다
- 동일한 입력 → 항상 동일한 출력
- 외부 상태에 의존하지 않음
- 부수 효과(side effect) 없음
- 외부 상태를 변경하지 않는다
- 외부 변수나 객체를 직접 수정하지 않음
- 대신 새로운 값을 반환
- 불변성(Immutability) 유지
- 함수 합성을 통해 프로그램을 구성한다
- 작은 함수들을 조합해서 복잡한 기능 구현
- 각 함수는 독립적으로 동작
- 함수들은 서로 값만 전달
- 데이터 변환 방식
- 원본 데이터는 유지
- 필요한 데이터는 매개변수로 전달
- 처리 결과는 새로운 값으로 반환
- 함수의 독립성 보장
- 모든 필요한 데이터는 매개변수로 전달받음
- 함수 내부에서만 데이터 가공
- 외부 값을 직접 수정하지 않고, 새로운 값을 반환
- 다른 함수의 실행에 영향을 주지 않음
이러한 특징들로 인해:
- 코드 예측이 쉬움
- 테스트가 용이
- 버그 발생 가능성 감소
- 병렬 처리에 유리
함수의 독립성이라는 것은 하나의 함수가 다른 함수에 영향을 주지 않는다는 것이다. 함수가 매개변수를 받기 때문에 서로 영향을 주는 것 같아보여서 헷갈릴 수 있는 부분인데 함수형 프로그래밍의 독립성이란 하나의 함수를 실행한 결과가 다른 함수 내부의 코드에 영향을 주지 않는 경우를 말한다.
즉, 함수 내부의 값이나 코드 부분이 항상 동일해야한다. 이부분이 바로 불변성을 유지한다는 부분이다. 흔히 말하는 "동일한 입력에 대한 동일한 출력"을 말하는 것이다.
이는 특정한 매개변수를 집어 넣었을 때에도 항상 동일한 값이 동일하게 반환되며 함수 내부의 로직도 동일하게 작동한다는 의미다. 즉 랜덤성이 없어야한다. 때문에 Math.random()이나 new Date()같이 특정 상황 마다 변하는 값을 가졌을 경우 순수함수가 아니게 되고 이 경우엔 함수의 불변성을 상실하게 되어 함수형 프로그래밍의 패러다임을 만족시키지 못하게된다.
다른 함수가 리턴한 매개변수 값을 받아서 사용하는 함수의 경우에도 그것의 '값을 받아서 사용해도' 가공해서 사용한다면 독립성과 불변성을 유지하게 된다. 만약에 어떤 함수가 가진 지역 변수의 값이 다른 함수의 영향으로 변화한다면 그것은 함수형 프로그래밍이 아니다. 독립성과 불변성이 사라졌기 때문이다.
하지만 실제로 순수함수로만 만들어진 프로그램은 존재하지 않는다고 할 수 있다. 함수형 프로그래밍이란 패러다임은 그냥 방법론이지 문법처럼 절대성을 가진 규칙이 아니기 때문에 가능한 부분은 순수함수로 제작하되 특정 필요한 부분은 일반적인 함수로 제작하게 된다.