프로그래머에게 프로그래밍의 관점을 갖게 하고 코드를 어떻게 작성할 지 결정하는 역할을 하는 것이 프로그래밍 패러다임이다. 새로운 프로그래밍 패러다임을 통해서는 새로운 방식으로 생각하는 법을 배우게 되고, 이를 바탕으로 코드를 작성하게 된다.
프로그래밍 패러다임은 분류적으로는 크게 **명령형**과 **선언형** 프로그래밍 방법이 존재하며, 선언형은 다시 함수형이라는 하위 구조를 갖고 명령형은 객체지향과 절차지향언어로 구분된다.
- 프로그래밍 패러다임
- 명령형(Imperative) : What보다 How의 관점
- 객체지향(Object-Oriented) : C++, Java , C#, Kotlin
- 절차지향(Procedural) : C, C++
- 선언형(Declarative) : How 보다 What의 관점
- 함수형(Functional) : 클로저, 하스켈, 리스프
- 명령형(Imperative) : What보다 How의 관점
명령형과 선언형 정의
명령형 프로그래밍과 선언형 프로그래밍의 개념은 그 이름에서 드러나듯 해당 로직을 처리하는 방식을 컴퓨터에게 명령하거나 무슨 일을 처리해야하는지 선언한다.
약간 애매모호한 말인것 같아 예시와 함께 설명을 덧붙여보자면, 내가 ‘붕어빵을 먹고싶다’라고 하였을때 ‘집에서 나와 아파트 출입구 기준으로 오른쪽 방향 100M 직진후 붕어빵 가게에서 붕어빵을 구매한 뒤 먹으세요’라고 한다면 이는 명령형 프로그래밍이고 ‘붕어빵 가게를 소개시켜드릴까요?’ 라고한다면 선언형 프로그래밍이다.
좀더 간결히 설명하자면 명령형 프로그래밍은 무엇을 어떻게 할 것인가에 가깝고, 선언형 프로그래밍은 무엇을 할 것인가에 가깝다.
명령형과 선언형 특징
명령형 프로그래밍
- 목표를 달성하기까지의 flow가 자세히 드러나 있다.
- 알고리즘은 명시하되, 목표는 명시하지 않는다.
- How의 개념이 중요시되며, 코드적 관점에서 구현하는 방법이 무엇을 개발하느냐 보다 중요하다.
선언형 프로그래밍
- 필요한 것을 달성하는 과정 하나하나 기술하는 것 보다 필요한 것이 어떤 것인지 기술하는데 초점을 맞추어 서비스의 설계를 세워나가는 구조
- 목표를 명시하고, 알고리즘은 명시하지 않는다.
- 프로그램이 함수형 또는 논리형 프로그래밍 언어로 작성된 경우
- 장점 : 가독성과 재사용성이 높고 오류가 적다. 프로그램 동작을 변경하지 않고도 관련 값을 대체 할 수 있다
함수형 프로그래밍
- 성공적인 프로그래밍을 위해 부수 효과를 미워하고 조합성을 강조하는 프로그래밍 패러다임
- 부수효과를 미워한다 -> 순수함수를 만든다.
- 순수함수 : 들어온 인자가 같으면 결과가 항상같은 함수
- 부수효과 : 리턴값으로 결과를 만드는것 외에 들어온 인자를 직접 변경하는 등 외부에 영향을 주는것
- 부수효과를 미워한다 -> 순수함수를 만든다.
- 선언형 프로그래밍 패러다임의 하나이며, 자료처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임을 의미한다.
- 클로저,스칼라,하스켈 등의 언어가 존재하며 최근 들어서 자바스크립트, 코틀린, 파이썬 등에도 최근버전의 함수형 프로그래밍 문법이 추가되었다.
순수함수
- 받은 인자외에 다른 외부의 상태에 영향을 끼치지 않고 리턴값 외에는 외부와의 소통이 이루어져 있지 않다.
- 순수함수로 함수형 프로그래밍을 할 경우 오류를 줄이고 안정성을 높인다.
- 순수함수는 평가 시점이 중요하지 않다. 만약 순수함수가 아니라면 동일한 인자를 넣어도 다른 값이 나오는 경우 어떠한 시점에서 함수를 평가할지가 중요해진다. 하지만, 순수함수는 정의적으로 동일한 인자로 동일한 결과가 나와야하기에 평가시점을 중요시하지 않는다.
순수함수의 옳바른 예
1. 항상 동일한 인자를 줄경우 동일한 결과를 리턴
private int pureFunction(int a, int b){
return a+b;
}
class Main{
public static void main(String[] args) {
int sum=pureFunction(5,10);
System.out.println(15.equal(sum));//true
}
}
2. 순수함수가 아닌 경우
int c=10; //값의 변화 유발
private int notPureFunction(int a,int b){
return a+b+c;
}
c=15;
위의 코드는 항상 동일한 인자가 들어가도 c값에 따라서 결괏값이 달라질수 있다.
int c=20; //값의 변화를 유발한다.
private int notPureFunction(int a,int b){
c=b;
return a+b;
}
고차함수
- 함수를 인자로 받을 수 있고, 함수의 형태로 리턴할 수 있는 함수이다.
함수형 프로그래밍 등장배경
명령형 프로그래밍을 기반으로 개발했던 개발자들은 개발하는 소프트웨어 크기가 커짐에 따라, 복잡하게 엉켜있는 스파게티 코드를 유지보수하는 것이 매우 힘들다는 것을 깨닫게 되었다.
그리고 이를 해결하기 위한 함수형 프로그래밍이라는 프로그래밍 패러다임을 고안하게 되었다. 함수형 프로그래밍은 거의 모든 것을 순수함수로 나누어 문제를 해결하는 기법으로, 작은 문제를 해결하기 위한 함수를 작성하여 가독성을 높이고 유지보수를 용이하게 해준다.
로버트 C.마틴의 저서인 클린코드에서 함수형 프로그래밍은 대입문 없는 프로그래밍이라고 정의한다.
함수형 프로그래밍 이해
함수형 프로그래밍은 앞서 말했듯, 대입문을 사용하지 않는 프로그래밍이며, 작은 문제를 해결하기 위한 함수를 작성한다고 하였다.
앞서 설명하였듯 함수형 프로그래밍은 무엇을(What)에 포커스를 두는 프로그래밍이라고 하였다. 그렇기 때문에 함수형 프로그래밍에서는 '출력을 하는 함수 '를 파라미터로 넘길 수 있으며, 이는 함수형 프로그래밍의 기본 원리 中 함수를 1급 시민(First-Class Citizen) 또는 1급 객체(First-Class Object)로 관리하는 특징 때문이다.
함수형 프로그래밍의 특징
부수 효과가 없는 순수함수를 1급 객체로 간주하여 파라미터나 반환값으로 사용할 수 있으며, 참조 투명성을 지킬 수 있다.
중요 KeyWord: 부수효과, 순수함수, 1급 객체, 참조 투명성
부수효과(Side Effect)
여기서 부수효과(Side Effect)란 다음과 같은 변화 또는 변화가 발생하는 작업을 의미한다.
- 변수의 값이 변경됨
- 자료 구조를 제자리에서 수정함
- 객체의 필드값을 설정함
- 예외나 오류가 발생하며 실행이 중단됨
- 콘솔 또는 파일 I/O가 발생함
순수 함수(Pure Function)
그리고 이러한 부수 효과(Side Effect)들을 제거한 함수들을 순수 함수(Pure Function)이라고 부르며, 함수형 프로그래밍에서 사용하는 함수는 이러한 순수 함수들이다.
- Memory or I/O의 관점에서 Side Effect가 없는 함수
- 함수의 실행이 외부에 영향을 끼치지 않는 함수
순수 함수(Pure Function)의 장점
순수 함수(Pure Function)을 이용하면 얻을 수 있는 효과는 다음과 같다.
- 함수 자체가 독립적이며 Side-Effect가 없기 때문에 Thread에 안전성을 보장받을 수 있다.
- Thread에 안정성을 보장받아 병렬 처리를 동기화 없이 진행할 수 있다.
1급 객체(First-Class Object)
1급 객체란 다음과 같은 것들이 가능한 객체를 의미한다.
- 변수나 데이터 구조 안에 담을 수 있다.
- 파라미터로 전달 할 수 있다.
- 반환값으로 사용할 수 있다.
- 할당에 사용된 이름과 무관하게 고유한 구별이 가능하다.
함수형 프로그래밍에서 함수는 1급 객체로 취급받기 때문에 위의 예제에서 본 것 처럼 함수를 파라미터로 넘기는 등의 작업이 가능한 것이다. 또한 우리가 일반적으로 알고 개발했던 함수들은 함수형 프로그래밍에서 정의하는 순수 함수들과는 다르다는 것을 인지해야 한다.
참조 투명성(Referential Transparency)
참조 투명성(Referential Transparency)이란 다음과 같다.
- 동일한 인자에 대해 항상 동일한 결과를 반환해야 한다.
- 참조 투명성을 통해 기존의 값은 변경되지 않고 유지된다.(Immutable Data)
결론적으로 명령형 프로그래밍과 함수형 프로그래밍의 차이로는 부수효과가 발생하는지에 차이가 존재한다.
함수형 프로그래밍 예제 코드
단어 모음에 대하여 다음과 같은 요구사항을 지니고 있는 코드가 존재한다.
- 단어의 크기가 2 이상인 경우를 필터링한다.
- 모든 단어를 대문자로 변환한다.
- 모든 단어를 앞글자만 잘라내어 변환한다.
- 모든 단어를 스페이스로 구분한 하나의 문자열로 합친다.
public class WordProcessTest {
private final List<String> words = Arrays.asList("TONY", "a", "hULK", "B", "america", "X", "nebula", "Korea");
@Test
void wordProcessTest() {
String result = words.stream()
.filter(w -> w.length() > 1)
.map(String::toUpperCase)
.map(w -> w.substring(0, 1))
.collect(Collectors.joining(" "));
assertThat(result).isEqualTo("T H A N K");
}
}
해당 예제 코드는 Stream API를 이용하여 기존이라면 for문을 이용한 loop문을 활용하여 지역변수가 사용되어 생기는 부수효과를 없애 순수함수로 만든 예제이다.
OOP(Object-Oriented Programming)
우리가 흔히 말하는 객체 지향 프로그래밍을 뜻하며 Java,C++,C# 등이 해당 패러다임을 사용하는 언어에 속한다.
객체 지향이란 무엇인가?
이름 그대로 객체를 지향하는 프로그래밍 방법이며, 객체의 개념을 이해하고 있어야한다.
객체 지향이란 패러다임은 우리(사람)가 살아가는 세상(실제세계)을 컴퓨터세계로 옮긴 것이다. 그럼 현실 세계에서 물리적이나 추상적으로 무언가 행동을 일으키는 주체를 객체라고한다.
객체는 유/무형을 따지지 않으며 입장에 따라 달라지게된다.
객체지향의 사실과 오해라는 조영호님의 저서를 읽어보면 엘리스가 물약을 마시는건 엘리스가 주체가되어 물약을 마시는 것이다.
class Elise{
private void drinkBottle(){
System.out.println("물약을 마시다");
}
}
하지만 물약이 주체가 된다면 물약은 마셔지게되는 것이다. 또는 자신의 물약을 주는 것으로 행위가 바뀌게 된다.
class Bottle(){
private void giveDrink(){
System.out.print("물약을 주다");
}
}
이처럼 모든 객체에는 상태 와 동작 이 존재한다. 이를 Java의 관점에서 보면 다음과 같다.
객체 ⇒ 클래스
상태 ⇒ 멤버변수
동작 ⇒ 메소드(함수)
객체지향 이전에 사용된 패러다임
- 절차적 프로그래밍 : 프로세스가 함수단위로 실행
- 구조적 프로그래밍 : Top-Down 방식
- 객체 지향 프로그래밍 : Bottom-Up 방식
OOP의 특징 4가지
- 캡슐화
- 상속
- 추상화
- 다형성
캡슐화(Encapsulation)
캡슐화라고하면 가장 크게 떠오르는 키워드는 정보은닉일 것이다.
하지만 캡슐화!=정보은닉으로 캡슐화가 정보은닉이라는 말은 아니며 캡슐화에서 정보은닉의 특성을 가지고 사용하는 것이다.
캡슐화의 정의
- 하나의 객체에 대해 그 객체의 목적 달성을 위한 필요한 변수나 메소드를 하나로 묶는 것을 의미한다.
- 실제로 구현부분을 외부에 들어내지 않으며 이특징이 정보은닉이다.
- 데이터를 외부에서 직접적으로 접근하지 않고 함수를 통해서만 접근한다.
- 객체가 특정한 목적을 잘 수행할 수 있도록 사용해야할 변수와 그 변수를 갖고 특정한 메서드를 관련성있게 클래스를 구성
정보은닉
캡슐화를 하는 가장 중요한 목적으로 정보은닉을 꼽는다.
우리가 Spring에서 Entity를 작성할 때 멤버 변수들의 접근제어자를 private 으로 데이터를 보호하는 것이 가장 가까운 예시이다.
이렇게 보호된 변수는 Getter 또는 Setter로도 꺼내게 된다면 캡슐화의 진정한 본질이 깨지게된다.
자세한 것은 OCP의 디미터의 법칙에 대해 찾아보면 알 수 있다.
추상화(Abstraction)
- 추상화는 목적과 관련없는 부분을 제거하여 필요한 부분만을 표현한다.
추상화 개념설명
사실 위의 설명만 들으면 진짜 추상화에 대해 추상적인 설명이다.
조금 더 정의적으로 접근하면 추상화란 모든 객체들에 대해 공통적인 요소나 특징을 추출하는 것을 추상화라고 한다.
예를 들어 모든 휴대폰에는 전화기능이 있다. 이외에도 문자, 카카오톡, 인터넷 등이 있다고 하면
이를 휴대폰 공통기능으로 추상화하여, 추상클래스 또는 인터페이스로 정의할 수 있다.
다형성(Polymorphism)
다형성은 상속을 통해 기능을 확장하거나 변경하는 것을 가능하게 해준다.
즉, 다형성은 형태가 같지만 다른 기능을 하는 것을 의미한다. (같은동작으로 다른결과물을 산출하는 것이다.)
이를 통해 코드의 재사용성, 코드길이의 감소가 되어 유지보수에 용이하다.
다형성 개념설명
고양이라는 클래스에 냥냥펀치 라는 속성이 정의 되어있다.
class Cat{
int power = 3;
private void punch(int power){
System.out.print("냥냥펀치 데미지 : "+power);
}
}
이때 호랑이는 고양이과에 속하기 때문에 고양이 클래스를 상속받아 이 속성을 갖고 있다. 하지만 둘의 파워는 다르기 때문에 코드로 보자면 다음과 같다.
class Tiger extends Cat{
int power = 50;
@Overriding
private void punch(int power){
System.out.print("냥냥펀치 데미지 : "+power);
}
}
Overriding VS Overloading
다형성의 대표적인 개념 두가지가 오버로딩과 오버라이딩이다.
- Overriding
- 부모 클래스에서 상속받은 메소드를 자식(하위)클래스에서 필요에 따라 재정의하여 사용하는 것을 말한다.
- Overloading
- 같은 이름의 메서드를 사용하지만 메서드마다 다른 용도로 사용되며 그 결과물도 다르게 구현할 수 있게 만든는 개념
- 오버로딩이 가능하기 위해선 메서드끼리 이름은 같지만, 매개변수의 개수나 타입이 다르다면 적용이 가능하다.
- 메서드 이름이 같아도 Syntax Error는 유발하지 않는다.
다형성의 필요성
같은 이름의 속성은 유지하기 때문에, 속성을 사용하기 위한 인터페이스를 유지하고, 메서드의 이름을 낭비하지 않는다.
한마디로 재사용성이 높아진다.
상속성(재사용성, Inheritance)
객체 지향에서 가장 중요한 특성이라고 꼽히는 상속이다.
- 상속은 기존 클래스 또는 상위 클래스를 근거로 새롭게 클래스의 행위를 정의하도록 도와준다.
- 기존 클래스에 기능을 가져와 재사용성을 높이고 동시에 새로운 기능도 추가 가능하기에 확장성 또한 존재한다.
여기서 주의점은 자바는 단일상속만을 지원한다. 다중 상속이 객체지향관점에서 크게 중요치않다고 보기에 그렇지만, 다중 상속의 필요성을 일부 인정하여 인터페이스의 경우 다중 상속이 가능하다. 그러나 이것이 인터페이스의 존재의의는 아니다.
상속 개념정리
우리가 상위 클래스를 부모, 하위 클래스를 자식 이라고 칭하는 이유는 상속성이 실제 세계의 자식이 부모의 유전형질을 물려받는 것에 빗대어 표현하기 때문이다.
예를들어 음식에는 종류가 상당히 많고 이를 단순히 음식이라고 정의하기엔 너무 방대하기에 한식, 중식, 일식, 양식으로 분류하고 이들은 음식을 상속받는 관계라고 생각해야한다.
상속의 필요성
예시로도 알수 있다시피 코드의 중복이 없어진다.
코드의 중복은 개발 단계에서 개발의 속도를 늦추며, 유지보수단계에서 또한 상당한 시간소요로 꽤나 높은 비용(cost)이 발생한다. 상속을 사용하면 해당 문제를 구조적으로 해결할 수 있다.
OOP의 장점과 단점
장점
- 코드의 재사용성이 높아진다.
- 모듈화된 객체를 기반으로 코드가 작성되기 때문에, 해당 객체의 특징을 비슷한 다른 로직에도 적용해서 사용하거나, 다른 개발자가 구현한 객체를 가져와 쓰기에도 용이하다
- 유지보수성이 높은 구조설계가 가능하다.
- 객체를 수정할 경우, 해당 객체를 이용하는 모든 객체에 변경사항이 적용되므로 중복 코드에 대한 관리가 간단하며 수정이 쉬워진다.
- 코드의 가독성이 높아지고 명시성이 높아진다.
- 큰 규모의 프로그래밍에 유리하다.
- 객체, 모듈 단위로 구분되는 특징으로 인해 분업이 쉽고, 각 모듈의 연관성을 도식하기 용이하다.
단점
- 프로그램 설계에 많은 고민과 시간을 투자해야한다.
- 모듈 단위의 상호작용으로 이루어진 방식은 모듈의 정확한 명세와 상호 간의 연관성을 고려하여 설계해야한다.
- 처리시간이 비교적 오래 걸린다.
- 객체의 의존관계로 인해 대체적으로 속도가 느리다.
- 코드의 잠재적인 복잡성이 존재한다.
- 높은 수준의 설계역량과 더불어 추상화, 상속 들은 코드의 구조를 파악하기 어렵게 만든다.
Functional Programming VS OOP
가장 큰 차이점으로 데이터의 상태를 다루는 개념과 간결한 코드작성에 대한 관점차이이다.
객체지향이 함수의 동작부를 캡슐화해서 코드의 이해를 돕는다면, 함수형 프로그래밍은 동작부를 최소화하여 코드의 이해를 돕는다.
객체지향 프로그래밍의 경우, 클래스 디자인과 객체들의 관계를 중심으로 코드작성이 이루어진다. 따라서 상태,멤버변수,메서드 등이 긴밀한 관계를 갖고 있으며, 멤버변수의 상태에 따라 결과가 달라진다.
함수형 프로그래밍의 경우, 값의 연산 및 결과 도출을 중심으로 코드작성이 이루어진다. 함수 내부에서 인자로 받은 별도로 저장하거나 하지 않고, 간결한 과정으로 처리하고 매핑하는데 주 목적을 둔다.
함수형 언어에서 함수는 그 자체로 일급 객체로 존재하지만, 객체지향에서는 객체가 일급객체가 된다
SOLID 5원칙
클린코드의 저자이자, 에자일 방법론으로도 유명한 Robert C.Martin이 제시한 객체지향 프로그래밍의 다섯가지 기본원칙이다.
S(SRP: Single Responsibility Principle) : 단일 책임 원칙
- 하나의 클래스는 하나의 책임만을 가져야한다.
- 하나의 클래스에서 하나의 기능만을 담당한다.
- 또한 클래스의 변경사항이 존재할 경우에는 오직 하나의 이유뿐이어야한다.
- SRP 원칙을 적용하면 책임영역이 확실해지기 때문에, 변경의 연쇄작용에서 자유롭다
- 이는 다른 원칙들의 기초가됨
O(OCP: Open Close Principle) : 개방폐쇄의 원칙
- 소프트웨어의 구성요소(컴포넌트,모듈,클래스,함수)는 확장에는 열려있고, 변경에는 닫혀있어야한다라는 원리
- 변경을 위한 비용은 가능한 줄이고, 확장을 위한 비용에는 아끼지 말아야한다.
- 새로운 요구사항이나 변경이 일어나도 기존의 구성요소에는 수정이 일어나선 안된다.
- 이말의 뜻은 기존 요소를 쉽게 확장해서 재사용할 수 있어야한다.
- OCP를 가능하게 하는 추상화와 다형성이며 객체지향의 장점을 극대화하는 원칙
L(LSP: The Liskov Substitution Principle) : 리스코브 치환의 원칙
- 서브타입은 언제나 기반타입으로 교체가능해야한다.
- 이 말은 서브타입은 기반타입이 약속한 규약을 지켜야한다.
- 서브클래스가 확장에 대한 인터페이스를 준수
- 교체시 프로그램의 객체는 프로그램 정확성을 깨트리지 않아야한다.
I(ISP: Interface Segregation Principle) : 인터페이스 분리의 원칙
- 하나의 클래스가 다른 클래스에 종속될 때에는 가능한 최소환의 인터페이스만을 사용해야한다.
- 하나의 일반적인 인터페이스보다는 여러개의 구체적인 인터페이스가 좋다
- 인터페이스의 단일 책임을 강조하는 원칙
- 하지만 SRP와는 달리 하나만 책임지는 것이 아닌 그 의미를 명확히 해야한다는 용도
D(DIP: Dependency Inversion Principle) : 의존성역전의 원칙
- 하위모듈의 변경으로 인한 상위 모듈의 변경이 요구되어 위계관계를 끊는 의미의 역전
- 실제 사용관계는 바뀌지 않는다.
- 추상을 매개체로 메시지를 주고받으며 관계를 최대한 느스이 만든다.
- 추상화를 중요시하는 원칙
절차적 프로그래밍
일반적으로 객체지향 프로그래밍에 대조되는 것으로 생각하는 경우가 많다. 절차지향 프로그래밍 = 절차적 프로그래밍 이며, 절차적 프로그래밍이 조금 더 맞는 단어이다.
또한, 절차적 프로그래밍과 객체지향 프로그래밍은 반대되는 단어가 절대 아니다.
절차적 프로그래밍은 순차적인 처리를 중요시 여기며, 프로그램 전체가 유기적으로 연결되도록 만드는 프로그램 기법
절차적 프로그래밍이란 프로시저(Procedure)를 이용하여 작성하는 프로그래밍 스타일이다. 프로시저의 목록으로는 루틴,서브루틴,메소드,함수 등이 존재한다.
프로시저의 목록
- 루틴 : main 문 (실행구문)
- 서브루틴 : main문 밖에서 정의한 코드 블럭 중 반환 값이 없는 것
- 함수 : main문 밖에서 정의한 코드 블럭 중 반환 값이 있는 것
절차적 프로그래밍의 장점
- 모듈 구성이 용이하다.
- 이로 인한 구조적인 프로그래밍 가능
- 컴퓨터의 처리구조와 유사하여 실행 속도가 빠르다.
절차적 프로그래밍의 단점
- 유지보수의 힘듬이 존재
- 절차적인 코드의 순서 때문에, 코드의 순서가 변경될 경우 동일한 결과를 보장받기 힘들다.
- 코드가 길어질 경우 가독성이 떨어진다.
- 대형 프로젝트에 부적합하다.
'ComputerScience' 카테고리의 다른 글
[CS] GoF 디자인 패턴 -2 (0) | 2022.12.27 |
---|---|
[CS] GoF 디자인 패턴 -1 (0) | 2022.12.20 |
[CS] 시간복잡도와 공간복잡도 (0) | 2022.12.06 |
[CS] 싱글스레드 VS 멀티스레드 (1) | 2022.11.29 |