diff --git a/_includes/sidebar/article-menu.html b/_includes/sidebar/article-menu.html index 43c2fb3..95de160 100644 --- a/_includes/sidebar/article-menu.html +++ b/_includes/sidebar/article-menu.html @@ -15,7 +15,7 @@ function generateContent() { var menu = document.querySelector(".post-menu"); var menuContent = menu.querySelector(".post-menu-content"); - var headings = document.querySelector(".post-content").querySelectorAll("h2, h3, h4, h5, h6"); + var headings = document.querySelector(".post-content").querySelectorAll("h1, h2, h3, h4, h5, h6"); // Hide menu when no headings if (headings.length === 0) { @@ -53,16 +53,16 @@ var beginIndex = index; var endIndex = index + 1; while (beginIndex >= 0 - && !items[beginIndex].classList.contains('h-h2')) { + && !items[beginIndex].classList.contains('h-h1')) { beginIndex -= 1; } while (endIndex < items.length - && !items[endIndex].classList.contains('h-h2')) { + && !items[endIndex].classList.contains('h-h1')) { endIndex += 1; } for (var i = 0; i < beginIndex; i++) { item = items[i] - if (!item.classList.contains('h-h2')) { + if (!item.classList.contains('h-h1')) { item.style.display = 'none'; } } @@ -74,7 +74,7 @@ } for (var i = endIndex; i < items.length; i++) { item = items[i] - if (!item.classList.contains('h-h2')) { + if (!item.classList.contains('h-h1')) { item.style.display = 'none'; } } diff --git a/_posts/2022-03-17-TWL-02-1-OOP.md b/_posts/2022-03-17-TWL-02-1-OOP.md new file mode 100644 index 0000000..17a9958 --- /dev/null +++ b/_posts/2022-03-17-TWL-02-1-OOP.md @@ -0,0 +1,620 @@ +--- +layout: post title: TWL-02 첫번째. 객체지향 프로그래밍 subtitle: 객체지향 프로그래밍에 대해 알아봅니다. categories: [TWL, CS] +tags: [TWL, CS, OOP] +--- + +# 객체지향 프로그래밍 (OOP) + +> Object Oriented Programming + +요즘은 처음 코딩을 배울 때 어떤 프로그래밍 언어를 배우는지 모르겠지만 저는 첫 프로그래밍 언어로 `Java`를 배웠습니다. 기억 속에서 커피를 마시면서 만들어서 `Java`라는 이름을 가졌다는 이야기와 VM을 +사용하는 **객체지향적 프로그래밍 언어**라는 이야기를 처음 들으며 언어를 공부했고 이후에는 `C`, `C++`, `Python`의 언어들을 배우게 되었습니다. +(그 학생은 5년 뒤 Java를 전혀 쓰지 않고 `Rust`, `Go`를 사용하게 됩니다...) + +최근에는 Java를 사용하지는 않지만 객체지향적으로 개발하라는 말은 여전히 듣습니다. 그러면 어떤 장점이 있어서 객체지향적으로 개발하라는 걸까요? + +## 객체지향적으로 개발하라? + +먼저 `객체지향적으로 개발하라`는 의미를 알아야 합니다. 구글링을 해보면 주로 객체 지향적으로 개발한다는 것은 `실제 세상처럼 객체들의 집합으로 생각하고 프로그래밍한다`라는 말이 자주 보입니다. 그런데 사실 **실제 +세상의 객체처럼 프로그래밍 한다면 객체지향적인 개발이 아닙니다.** + +우리가 사는 세상에서는 주체 A가 행동을 하고 다른 객체들은 주체 A의 행동에 영향을 받아서 상태가 변경됩니다. 하지만 객체지향적으로 개발한다면 주체 A가 행동할 때, 다른 객체 B에게 행동을 시킵니다. 그러면 객체 +B는 자신의 행동을 통해 자신의 상태를 변경합니다. 그 후의 결과를 주체 A가 받아서 행동을 마무리하게 됩니다. + +개발을 하다보면 동작이 얼마 없을 때는 하나의 주체 A가 모든 상태를 변경해도 크게 문제가 없습니다. 하지만 점점 규모가 커져 주체 A, 객체 B, C, D, ... 로 늘어나게 되면 A의 동작 하나에 여러 객체들의 +상태가 변경된다면 실행 결과에 대한 분석이 어려워집니다. + +
+ 얽힌 실타래. +
+ +꼬여있는 코드를 분석하는 것이 새로 구현하는 것보다 어렵습니다. {: style="text-align: center; color: gray; margin-top:.5em;"} + +만약 A가 B, C, D, ... 의 상태를 변경하는 작업들이 있다면, A의 동작은 객체의 수에 비례해 늘어나고 B, C, D, ... 의 상태를 변경하는 작업들이 복잡하게 얽혀있을 것입니다. 그런데 코드가 처음부터 +복잡하게 얽혀있지는 않았을 겁니다. 그렇다면 처음부터 엉키지 않도록 조심히 구현하면 어떨까요? + +먼저 생각할 수 있는 방법은 `각 객체의 상태는 객체 스스로 다루도록 만드는 것`입니다. + +객체지향적으로 개발하라는 말의 의미는 `상태를 변경하는 작업을 객체라는 최대한 작은 범위로 줄여` 코드가 얽히지 않도록 개발하라는 의미입니다. + +### 절차적 프로그래밍 + +> Procedural Programming (프로시저 프로그래밍) + +객체지향적으로 개발하는 것은 `잘 프로그래밍 하기 위한` 방법 중 하나입니다. 당연하게도 `잘 프로그래밍 하기 위한` 여러 방법들이 제안되었고, 그 중 하나가 절차적 +프로그래밍(`procedural programming`)입니다. +> `procedural programming`은 사실 프로시저(함수) 프로그래밍에 가깝지만 (직역한) 번역명으로 더 잘 알려진 절차적 프로그래밍이라고 적겠습니다 +> `oriented`가 없는데 절차 "지향"적 언어라고는 못 적겠네요;; + +cpu는 특정 연산을 수행하라는 명령어(`instruction`)를 받아 처리하는 구조입니다. 이런 instruction을 기반으로 만들어졌다보니 프로그래밍 언어 또한 명령형 프로그래밍인 경우가 많습니다. 절차적 +프로그래밍은 이런 배경에서 나온 프로그래밍 방법입니다. + +절차적 프로그래밍은 명령형 프로그래밍 언어에서 `연산을 그대로 나열하는 것이 아닌` 무슨 작업을 할지 함수로 만들어 +`단계별로 호출하도록 개발하는 방법`입니다. 이런 모습이 어떠한 절차대로 진행되는 것처럼 보이기도 합니다. + +사실 객체 지향적으로 개발하면서도 내부적으로는 함수를 호출하도록 개발하기 때문에 완전히 다른 개발 방법이라고 하기에는 문제가 있습니다. 하지만 절차적 프로그래밍에서는 객체지향 프로그래밍과 다르게 상태를 어떻게 다룰 +것인가에 대한 내용은 다루지 않습니다. + +```c +void move(int* x, int* y, int move_x, int move_y) { + *x = *x + move_x; + *y = *y + move_y; +} + +int main() { + int my_x = 0; + int my_y = 0; + // 내 위치를 (2, 4) 만큼 이동한다. + move(&my_x, &my_y, 2, 4); + return 0; +} +``` + +c언어에서는 함수를 정의하고 데이터를 인자로 입력해 원하는 순서대로 호출합니다. {: style="text-align: center; color: gray; margin-top:.5em;"} + +절차적 프로그래밍 언어로 가장 유명하고 현재도 많이 사용되는 c 언어로 예를 들어보면 어떠한 동작을 하는 함수를 구현하고, 함수에 입력으로 값이나 포인터를 넘겨 연산을 순서대로 실행하도록 개발합니다. + +#### 추상화 + +> 모든 프로그래밍 방법론은 추상화에서 시작된다. + +절차적 프로그래밍에서 추상화가 없는 것은 아닙니다. 추상화라는 것은 구체적인 작업을 핵심적인 기능만으로 표현해내는 것입니다. 예를 들어 우리는 하드디스크에 어떻게 저장되어있는지 모르더라도 파일을 통해 접근하고 +CPU가 어떻게 돌아가는지 모르더라도 프로세스가 돌아가고 있습니다. 실제로 CPU와 하드디스크에서 어떻게 동작하는지 알면 좋겠지만, 모르더라도 추상화된 동작만 안다면(심지어 모르더라도 사용은 가능합니다) +사용하는데 문제가 없습니다. + +위의 코드에서는 `x, y를 조작하는 구체적인 작업`이 아닌 `move라는 핵심적인 기능(함수명)`만으로 사용할 수 있도록 추상화 한것입니다. 이후에는 어떻게 동작하는지는 함수명과 설명에 대한 신뢰를 +가지고 `move` 함수를 호출해 사용할 수 있습니다. +> 물론 실제 구현이 설명과 다르다면 문제가 발생합니다. +> 이때부터 개발자의 직업은 작명가로 바뀝니다. + +함수의 내부적인 구현을 우리가 알면 좋겠지만, 모르더라도 추상화된 동작만 알고 있다면 우리가 호출해서 사용하는데 문제가 없습니다. 내부적인 구현을 정확히 이해하지 않고도 사용할 수 있다는 점이 구체적인 동작을 그대로 +사용하지 않고 추상화하는 이유 중 하나입니다. +> 내부 구현이 대부분의 개발자가 이해하는 내용과 다르다면 버그입니다. + +```c +void move(int* x, int* y, int move_x, int move_y) { + *x = *x + move_x; + *y = *y + move_y; +} + +void jump(int* x, int* y, int move_x, int move_y) { + *x = move_x; + *y = move_y; +} + +int main() { + int my_x = 0; + int my_y = 0; + // move 대신 jump 하도록 수정 + // move(&my_x, &my_y, 2, 4); + jump(&my_x, &my_y, 2, 4); + return 0; +} +``` + +추상화된 동작은 대체하기도 쉽다. {: style="text-align: center; color: gray; margin-top:.5em;"} + +연산을 추상화하게 되면 `전체적인 로직에 대해서 쉽게 이해`할 수 있습니다. +`main` 안에서 `my_x`와 `my_y`를 단순히 더하거나 빼는 연산보다는 +`move`라는 함수 안에 로직을 넣고 `main`에서는 `move` 함수를 호출하는 것이 어떤 작업을 하는 건지 쉽게 이해할 수 있습니다. +> 디스크의 실제 동작을 보는 것보다 추상화된 파일 read/write가 더욱 이해하기 쉽습니다. + +뿐만 아니라 추상화된 작업은 `다른 작업으로 쉽게 변경할 수 있습니다`. 현재는 코드를 `move`하는 동작으로 사용하고 있지만 만약 `jump`하는 동작으로 수정되어야 한다면 호출하는 함수를 변경하는 것으로 쉽게 +다른 동작으로 변경할 수 있습니다. + +비즈니스 동작이 달라지거나 최적화가 필요하거나 혹은 업데이트를 하는 등 여러 이유로 `코드는 항상 변경될 가능성을 가지고 있습니다`. 그런 점에서 추상화를 통해 이전 코드를 간단하게 대체할 수 있다는 것은 굉장한 +장점입니다. + +절차적 언어에서는 함수 단위의 추상화를 제공하고 있습니다. 만약 어셈블리 언어로 개발했다면 함수를 통해 정해진 연산을 호출하는 작업은 생각하지 못했을 것입니다. 하지만 c언어에서는 구체적인 연산을 매번 사용하지 않고 +함수로 호출할 수 있도록 추상화를 제공하고 있습니다. +> 절차적 프로그래밍은 함수로 연산을 묶어 재사용하는 정도의 수준으로 함수형 프로그래밍과는 다릅니다 + +### 객체지향 + +> 객체로 모든 것을 생각해 프로그래밍 하는 것 + +그렇다면 이제 객체지향에서의 추상화에 대해 쉽게 이해할 수 있을 겁니다. 절차적 프로그래밍에서는 동작을 함수로 추상화했다면 객체지향 프로그래밍에서는 동작에 상태를 더해 객체로 추상화합니다. c언어에서 절차에 대한 +추상화 기능으로 함수를 제공해 절차적 언어라고 말하는 것처럼, 객체지향 언어는 객체를 추상화할 수 있도록 기능을 제공하는 언어를 말합니다. +> c언어에서 객체지향처럼 흉내낼 수는 있지만 객체지향 언어라고 하기에는 기능이 부족합니다. + +앞서 설명했던 예시를 객체지향적으로 수정해보겠습니다. + +```java +interface Mover { + void move(int moveX, int moveY); +} + +class Human implements Mover { + private int x; + private int y; + + public Human(int x, int y) { + this.x = x; + this.y = y; + } + + @Override + public void move(int moveX, int moveY) { + this.x += moveX; + this.y += moveY; + } +} + +class Jumper implements Mover { + private int x; + private int y; + + public Jumper(int x, int y) { + this.x = x; + this.y = y; + } + + @Override + public void move(int moveX, int moveY) { + this.x = moveX; + this.y = moveY; + } +} + +public class Main { + public static void main(String[] args) { + Mover mover = new Human(0, 0); + Mover jumper = new Jumper(0, 0); + mover.move(2, 4); + jumper.move(2, 4); + } +} +``` + +main에서는 추상화된 객체의 동작만이 실행된다. {: style="text-align: center; color: gray; margin-top:.5em;"} + +객체지향 프로그래밍에서는 동작만 추상화하는 것이 아니라 상태도 함께 객체로 추상화합니다. 상태를 가지고 있는 클래스에서 동작을 메소드로 구현해 사용할 때는 인터페이스의 함수를 호출해 상태가 변경되도록 합니다. ( +상태와 동작의 추상화) +또한 추상화된 함수에서 다룰 값도 함께 객체에서만 접근할 수 있도록 만들어 외부 동작에 의해 추상화된 로직이 깨지지 않도록 만들 수 있습니다.(캡슐화) +> c의 struct가 박스에 있는 데이터를 우리가 직접 꺼내는 구조라면 클래스는 박스에 손이 달려(?) 박스가 직접 데이터를 다루는 구조입니다. + +객체에 대해 추상화가 되었기 때문에 절차적 프로그래밍에서 함수를 다른 기능의 함수로 대체했듯이 다른 작업을 하는 객체로도 대체할 수 있습니다. + +### 객체지향의 특징 + +기본적으로 꼽히는 객체지향의 특징은 다음과 같습니다. + +1. 추상화 + * 객체를 추상화해 복잡한 로직이 아닌 객체를 통해 핵심적인 기능만을 볼 수 있도록 합니다. +2. 캡슐화 + * 데이터를 객체 내부에 저장해 접근제어자(`private`, `public`)를 통해 외부에서 조작하지 못하도록합니다. + * 추상화된 로직을 사용할 때 내부 데이터를 외부에서 접근한다면 정해진 로직이 깨질수 있습니다. +3. 상속 + * 상위 클래스의 필드와 연산을 하위클래스에서 가질 수 있습니다. + * 최근에는 상속의 문제점들로 인해 상속을 지원하지 않는 언어들이 보이고 있습니다. (`rust`, `go`) + * 대신 내부 필드로 확장할 객체를 가지는 `composition`으로 구현합니다. +4. 다형성 + * 같은 이름의 메소드를 다양한 타입이나 다양한 구현(`overriding`)으로 + * `overloading`, `overriding`을 이용해 다형성이 발현됩니다. + * `overriding`하면서 `Dependency Injection(DI)`을 가능하게 하는 기본 성질입니다. + * 테스트 코드의 mocking 에도 사용되는 성질입니다. + +#### 객체지향의 추상화 + +실생활에서 우리는 여러 전자 기기를 사용하지만 내부적으로 어떻게 동작하는지 모릅니다. 어떻게 돌아갈지 짐작만 하고 있을 뿐이죠. 대부분 사용설명서를 읽고 원하는 기능을 찾아 기기를 사용합니다. 추상화하는 것도 +동일합니다. + +
+ 애플의 사용설명서 +
+ +사용설명서를 보시나요? {: style="text-align: center; color: gray; margin-top:.5em;"} + +우리는 구현할 때와 사용할 때를 구분해서 생각해야합니다. 절차적 프로그래밍의 함수나 객제지향 프로그래밍의 객체를 구현할 때는 전자기기를 만드는 것과 마찬가지로 상태가 어떻게 관리되고 어떻게 동작하는 지 알고 있어야 +합니다. 하지만 구현된 함수나 객체를 사용할 때는 전자기기를 사용하는 것처럼 굳이 내부에 대해 모르더라도 사용할 수 있어야 합니다. +> 사용 설명서를 읽지도 않고 어림짐작으로 사용하던 모습은 api 설명을 안 보고 함수명만 보고 사용하는 모습과 비슷합니다 + +앞서 말했지만 추상화는 객체지향에서만의 특징은 아닙니다. 절차적 프로그래밍에서도 추상화가 들어있습니다. 다만 추상화하는 정도나 대상이 다를 뿐입니다. + +절차적 프로그래밍에서는 `어떤 상태를 받아 연산을 하는 함수` 로 추상화하고, 객체지향 프로그래밍에서는 `상태도 연산과 함께 추상화` 해서 객체에서 관리합니다. 상태를 함께 추상화한다는 것은 추상화된 인터페이스를 +사용할 때 `상태가 어떤 방식으로 관리되는지 모르더라도 사용할 수 있음`을 의미합니다. + +예를 들어 절차적으로만 생각하면 함수를 정의해 연산을 추상화하고, 함수를 호출해서 사용함으로써 내부 연산을 모르더라도 사용할 수 있지만 입력으로 전달하는 `struct` 값들은 따로 관리하고 함수의 인자로 넘겨주어야 +합니다. + +하지만 객체지향적으로 생각하면 상태에 대한 관리도 객체에서 함으로써 객체를 사용할 때 더이상 상태값이 어떻게 관리되는지 모르더라도 사용하는 데 문제가 없습니다. 상태 값을 어떻게 관리할 지는 객체를 구현할 때의 +문제입니다. +> `상태를 객체 내에서 잘 관리`하는 것이 객체지향 프로그래밍의 핵심입니다. + +예를 들어 처음 파이썬 또는 자바스크립트를 공부할 때, `dictionary`가 내부적으로 어떻게 구현되어있는지 모르더라도 +`key`로 값을 저장하고 가져온다는 사실만 알고 있으면 `dictionary`를 사용할 수 있습니다. 추상화하면 (성능은 잠시 미뤄두고...) +객체의 구현에 대해 모르더라도 `사용하는데는 문제가 없습니다`. 그 후 `dictionary`의 구현을 Hash를 이용할 것인지 Tree를 이용할 것인지는 `구현의 문제`입니다. + +#### 캡슐화, 정보은닉 + +캡슐화는 객체의 상태나 연산을 해당 객체에서만 접근할 수 있게해서 외부 연산에 의해 오류가 발생하지 않도록 합니다. Java나 C++에서 제공하는 `private`, `protected`, `public` +접근제어자를 통해 객체, 상속받은 객체, 외부에서 접근하는 것을 제어하며, golang은 대문자, 소문자를 이용하고, Rust에서는 기본적으로 private하며 `pub` 접근 제어자만을 제공합니다. +> 파이썬은 `_`를 앞에 붙여 접근제어자처럼 사용하지만 실제로 접근은 가능하기에 접근제어자가 없어 완벽한 객체지향언어가 아니라는 말이 나옵니다. + +연산이 복잡해지고 많아지면 메소드가 계속 늘어나는데 이때 객체 내부에서만 재활용할 메소드들은 `private`으로 객체 내부에서만 접근할 수 있도록 두어 외부 인터페이스와 내부에서 사용할 메소드를 구분하는데 주로 +사용합니다. + +#### 상속 + +최근 언어들에서는 클래스 상속 기능을 제공하지 않는 언어들이 많습니다. 상속은 상위 클래스에서 구현한 메소드나 필드를 하위 클래스에서도 물려받는 것을 의미합니다. 상위 클래스에서 구현한 기능을 하위 클래스에서 +구현하지 않더라도 사용할 수 있다보니 구현하는 측면에서 재사용성이 늘어나는 장점이 있습니다. +> 상속을 이용하면 확실히 구현할 코드가 줄어듭니다. + +하지만 상속으로 구현하게 되면 하위 클래스에서 메소드를 오버라이딩, 오버로딩하거나 여러 클래스를 다중 상속받으면서 하위 클래스에서 상위 클래스의 기능을 깨뜨리는 경우도 발생합니다. 자세한 내용은 이후 객체지향 +원칙에서 설명하겠지만 상속의 문제점들로 인해 최근에는 상위 클래스를 직접 상속받는 것이 아닌 필드로 받아 사용하는 `composition` 방식으로 구현하는 경우가 많습니다. + +하지만 go(`interface`)와 rust(`trait`)에서도 인터페이스를 `implements`하는 기능들은 제공합니다. 인터페이스는 실제 기능이 구현된 것이 아니기 때문에 상속에서 생기는 문제에서 비교적 +자유롭기 때문으로 보입니다. + +#### 다형성 + +객체의 메소드 이름은 같지만 다양한 타입이나 구현을 가질 수 있는 성질입니다. 기본적으로는 메소드 이름이 같지만 다른 타입의 인자와 리턴값을 가지는 `overloading`과 하위 클래스에서 메소드를 +재작성하는 `overriding`으로 이루어집니다. + +최근 언어에서는 `overloading`은 오히려 코드를 읽는데 헷갈릴 수 있다는 이유로 지원하지 않는 언어도 있지만 `overriding`은 다릅니다. 최소한 `interface`의 메소드를 `overriding` +할 수 있도록 기능을 제공하고 있습니다. 각 클래스에서는 구현한 클래스 타입을 필드로 가지는 것이 아니라 `interface`를 타입으로 필드를 가집니다. 그 후에 사용할 때 구현 클래스를 +대입해 `interface`로 호출하지만 대입한 구현 클래스를 실행시키는 효과를 얻을 수 있습니다. + +이를 통해 다른 기능을 사용할 때 다른 구현 클래스를 대입해 사용해 내부 구현을 변경하지 않더라도 다른 기능을 실행할 수 있고, 메소드의 인자로 `interface`를 받는다면 메소드의 구현을 변경하지 않더라도 +외부에서 원하는대로 메소드가 동작하도록 구현 클래스를 넘기는 +`Dependency Injection(DI)`을 할 수도 있습니다. + +### 객체지향의 원칙 (SOLID) + +> 어떻게 개발해야 좋은 구조로 개발하는 것인가? + +그럼 어떻게 개발해야 객체지향적으로 잘 개발하는 걸까요? 먼저 어떻게 개발하는 것이 잘 개발하는 것인지 생각해봅시다. 사람마다 다르게 생각할 수 있지만 제 생각에는 코드의 가독성이 좋으면서 유지보수가 쉽고 새로운 +기능을 확장하기 쉬운 구조로 개발하는 것입니다. 이렇게 개발할 때 지침으로 삼을 수 있는 것이 바로 SOLID 원칙입니다. + +* S: SRP: Single Responsibility (단일 책임의 원칙) + * 하나의 객체는 하나의 책임(기능, 역할)만을 가져야합니다. +* O: OCP: Open-Closed (개방 폐쇄의 원칙) + * 기능 확장에는 열려있고, 수정(구현 코드)에는 닫혀있어야합니다. +* L: LSP: Liskov Substitution (리스코프 치환의 원칙) + * 부모 클래스의 인스턴스 위치에 자식 클래스를 두더라도 문제가 없어야 합니다. + * 여기서 상속을 잘못 구현하면 문제가 발생합니다. +* I: ISP: Interface Segregation (인터페이스 분리의 원칙) + * 클래스 내에서 사용하지 않을 인터페이스(메소드)는 구현하지 않아야 합니다. + * 각 인터페이스를 최소화하여 분리합니다. +* D: DIP: Dependency Inversion (의존 역전의 원칙) + * 상위 모듈은 하위 모듈에 의존하지 않고 추상화가 세부 구현(인터페이스에 없는 메소드나 특정 구현 클래스)에 의존하지 않아야 합니다. + * 추상화하는 메소드가 primitive 타입이나 인터페이스를 받아 구현하는것은 괜찮지만 특정 구현 클래스를 받는 것은 지양해야 합니다. + +#### SRP: Single Responsibility (단일 책임의 원칙) + +> 하나의 객체는 하나의 책임(기능, 역할)만을 가져야합니다. +> 객체만이 아니라 모듈이나 함수를 구현할 때에도 마찬가지입니다. +> 한 객체가 하나의 메소드만 가져야만 한다는 것은 아닙니다. + +단일 책임의 원칙은 하나의 객체 또는 함수에서 하나의 역할만을 하도록 구현해야 한다는 원칙입니다. 클래스를 통해 개발할 때 주로 발생하는 문제는 하나의 객체가 너무 커진다는 것입니다. 주로 기능을 추가하다보면 필요한 +기능을 하나 둘씩 (편의를 위해) 기존에 있는 객체에 메소드를 추가하게 되고 점점 객체는 특정 하나의 역할만을 하는 것이 아닌 모든 것에 다재다능한 슈퍼 객체가 되어버립니다. +> 이때 메소드를 늘리는 것이 아닌 객체를 분리할 생각을 해야합니다. + +```java +class SuperDataViewer { + // 비슷한 작업의 메소드만 늘어가면서 객체가 비대해짐 + // 주로 이미 구현한 클래스의 필드 값이나 private 메소드를 재활용하기 위해 + // 편의상 메소드를 늘리는 경우가 많습니다. + public void viewHtml() { + ... + } + + public void viewMarkDown() { + ... + } + + public void viewCSV() { + ... + } + + // Viewer에서는 보여주는 작업만 하고 update는 Viewer를 사용하는 클래스에서 구현하는 것이 옳음 + public void updateView() { + ... + } +} + +interface DataViewer { + void view(); +} + +class HTMLViewer implements DataViewer { + @Override + public void view() { + ... + } +} +``` + +이것은 사실 객체지향적으로 프로그래밍한 코드라고 보기 어렵습니다. 객체 안에 절차적 프로그래밍의 함수를 때려넣은 것 뿐이죠. 주로 주의할 부분은 하나의 객체에서 비슷한 기능을 하는 작업을 메소드만 늘려서 구현하거나 +하나의 클래스의 역할을 너무 크게 생각해 너무 많은 기능을 넣은 경우입니다. + +위의 예에서는 각각 다른 데이터를 보여주는 상황으로 `HTMLViewer`, `MardownViewer`, `CSVViewer`와 같은 방식으로 각각 다른 클래스에서 `view`함수를 구현하는 방식으로 구현하는 것이 +더 좋아보입니다. + +그리고 `updateView`와 같은 `view`를 다루는 상위 작업은 `DataViewer`를 사용하는 클래스에서 구현해두면 +`DataViewer`는 `view`라는 작업에만 집중할 수 있어 역할이 더 명확해집니다. +`update`기능 구현이 `DataViewer`의 클래스에 있는게 구현이 조금 더 편하다는 이유로 분리되지 않는다면 점점 거대해진 모든 역할을 하는 클래스가 추가될 수 있습니다. + +#### OCP: Open-Closed (개방 폐쇄의 원칙) + +> 기능 확장에는 열려있고, 수정(구현 코드)에는 닫혀있어야합니다. +> 기능 추가는 쉽게 할 수 있지만 기존 코드는 건드리지 않아야 합니다. + +개방 폐쇄의 원칙은 기능을 추가하더라도 기존 코드를 건드리지 않아야한다는 원칙입니다. 새로운 기능을 추가한다는 이유로 기존 코드를 건드리게 되면 멀쩡히 동작하던 기능이 갑자기 동작하지 않을 수도 있습니다. + +```java +class RealTimeDataViewer { + private final DataViewer dataViewer; + + RealTimeDataViewer(DataViewer dataViewer) { + this.dataViewer = dataViewer; + } + + void updateView() { + ... + this.dataViewer.view(); + } +} + +interface DataViewer { + void view(); +} + +class HTMLViewer implements DataViewer { + @Override + public void view() { + ... + } +} + +class MarkDownViewer implements DataViewer { + @Override + public void view() { + ... + } +} +``` + +만약 MarkDown을 보여주는 viewer를 제공하기 위해 기존 구현을 건드린다면 객체가 너무 비대해져 코드를 읽기 어렵거나 기존 코드를 건드리면서 예상치 못한 버그를 발생시킬 수 있습니다. 애초부터 객체를 새로 +만들어 기능을 구현하고 새로운 기능이 필요할 때에 생성한 객체를 사용하면 됩니다. + +새로 클래스를 구현해서 사용하면 당연히 기존 코드의 수정에 닫혀있겠지만 그것만으로는 확장에 문제가 발생합니다. 개방 폐쇄의 원칙은 단순히 클래스만 따로 만들라는 것이 아닙니다. 클래스를 따로 만들지만 같은 +인터페이스를 `implements`함으로써 사용하는 곳에서는 다른 구현 클래스만 넘겨주어 구현 클래스에 따른 기능을 사용할 수 있게 하라는 것입니다. + +Java에서 `List` 인터페이스가 있지만 원하는 상황에 따라 다른 성능을 위해 `List list = new ArrayList<>();`처럼 각기 다른 `ArrayList` +, `LinkedList`를 사용하는 상황과 비슷합니다. 원하는 `List` 형태가 있다면 직접 구현해서 기능을 확장할 수 있지만 기존에 있는 다른 형태의 `List` 구현에 전혀 영향을 주지 않죠. + +> 인터페이스 설계부터 잘 고려해서 작성해야 개방 폐쇄의 원칙을 지킬 수 있게 됩니다. + +#### LSP: Liskov Substitution (리스코프 치환의 원칙) + +> 부모 클래스의 인스턴스 위치에 자식 클래스를 두더라도 문제가 없어야 합니다. + +보통 클래스를 구현할 때 구현한 클래스는 `어떤 조건을 만족해야한다`는 규칙을 두는 경우가 많습니다. 예를 들어 Queue는 `먼저 들어온(push) 값이 먼저 내보내져야(pop) 하는 규칙(FIFO)`을 가집니다. +그에 반해 Stack은 `최근에 들어온(push) 값이 먼저 내보내져야(pop)하는 규칙(LIFO)`을 가집니다. + +여기서 Queue와 Stack은 사용할 때 `push`와 `pop`을 사용하는 공통점을 가지고 있고, 구현도 Queue와 동일하게 `push`하고 `pop`만 역순으로 꺼내면 되기 때문에 Queue를 부모 클래스로 +두고 `pop`만 override해서 Stack을 구현하고 싶은 유혹을 받습니다. + +하지만 리스코프 치환 원칙은 이런 방식으로 구현하면 안된다고 말합니다. + +```java +Queue queue = new Stack(); +queue.push(1); +queue.push(2); + +queue.pop(); // 구현이 stack이기 때문에 2가 반환됨 +``` + +만약 리스코프 치환 원칙을 위반한다면 위와 같은 상황이 발생합니다. + +만약 상위 클래스를 상속받은 자식 클래스를 구현한다면 `자식 클래스에서는 상위 클래스의 규칙을 반드시 지켜야합니다`. 규칙을 지키지 않더라도 구현해 실행시킬 수는 있기 때문에 리스코프 치환 원칙을 자주 어기곤 +합니다. + +하지만 리스코프 치환 원칙을 지키지 않는다면 프로그래밍한 코드가 예상치 못한 결과를 내보낼 수 있습니다. 사실 우리들은 단순히 굴러가는 코드를 짜는 것이 아니라 좋은 코드를 작성하는 것이 목표이기 때문에 이런 원칙을 +생각해서 구현해야합니다. + +위의 Queue의 예시를 생각해보면 사실 Queue와 Stack은 기반으로 두는 규칙이 다르기 때문에(`FIFO와 LIFO`) +둘은 서로 상속관계를 가지면 안됩니다. Java에서도 Queue는 인터페이스로 두고 있지만 Stack은 Queue가 아닌 Vector를 상속받는 구조로 되어있습니다. + +#### ISP: Interface Segregation (인터페이스 분리의 원칙) + +> 클래스 내에서 사용하지 않을 인터페이스(메소드)는 구현하지 않아야 합니다. +> 인터페이스를 만들 때부터 최소한의 메소드만을 갖도록 합니다. + +```java +interface IO { + int write(byte[] b); + byte[] read(); +} + +class FileReader implements IO { + @Override + public int write(byte[] b) { + return 0; + } + + @Override + public byte[] read() { + ... + return b; + } +} +``` + +인터페이스 분리 법칙은 가능한 한 인터페이스를 분리해 설계하라는 원칙입니다. 위의 예시를 보면 `IO` 인터페이스에서 read와 write를 모두 가지고 있습니다. read와 write는 비슷한 역할(IO)을 하기 +때문에 주로 묶어서 생각하지만 구현에서는 read와 write의 기능은 매우 다릅니다. 그렇기 때문에 주로 reader나 writer를 따로 구현합니다. + +하지만 위처럼 IO를 묶어 생각하게 되면 Reader에서 write 기능을 구현하지 않고 read만을 구현하거나 Writer에서 read 기능을 구현하지 않고 write만을 구현하는 경우가 생깁니다. + +하지만 인터페이스를 사용하는 목적을 생각해보면 위와 같은 구현은 좋지 않습니다. +> 인터페이스의 메소드를 호출할 때 우리는 입력된 구현 클래스의 메소드가 구현되어있지 않을 것을 생각하지는 않습니다. + +인터페이스에서의 메소드는 모든 구현 클래스에서 제대로 구현해야합니다. 만약 인터페이스의 구현을 클래스에서 구현하지 않는 경우가 있다면 인터페이스가 너무 큰 기능을 가지고 있는 것이 아닌지 생각해봐야 합니다. + +```java +IO io = new FileReader(); + +class Logger { + void output(writer IO) { + writer.write(output); // FileReader에서는 제대로 동작하지 않음 + } +} +``` + +위와 같은 IO 인터페이스를 가지고 Logger에서 구현하고 있다고 가정해봅시다. 로거에서 로깅을 위한 output을 여러 IO를 통해 로깅 내용을 작성하도록 구현할 수 있습니다. 하지만 `FileReader`는 +IO를 구현했지만 내부 구현으로 write를 제대로 구현하지 않았기 때문에 IO를 인자로 전송하지만 제대로 IO의 기능을 하지 않는 것을 볼 수 있습니다. + +이것이 인터페이스가 분리되지 않을 때 생기는 문제입니다. + +인터페이스는 가능한 기능의 원자단위로 나누어 설계하고 클래스는 모든 기능을 구현해야 합니다. 구현에서는 반드시 인터페이스에서 설계된 기능대로 구현하는 것을 가정해야 추상화된 기능이 망가지지 않습니다. + +#### DIP: Dependency Inversion (의존 역전의 원칙) + +> 상위 모듈은 하위 모듈에 의존하지 않고 추상화가 세부 구현(구현 클래스의 메소드나 필드)에 의존하지 않아야 합니다. + +객체지향적으로 개발할 때 반드시 기억해두어야 할 원칙입니다. 우리가 함수나 클래스를 사용하는 이유는 코드를 재활용하기 위해서입니다. 그런데 만약 함수에서 인자로 받는 값이 정해진 클래스의 필드나 함수에만 의존한다면 +어떻게 될까요? + +```java +class AudioCaller { + public void call() { + ... + } +} + +class VideoCaller { + public void call() { + ... + } +} + +class Phone { + public void videoCall(VideoCaller caller) { + caller.call(); + } + + public void audioCall(AudioCaller caller) { + caller.call(); + } +} + +... +Phone phone = new Phone(); +phone.videoCall(new VideoCaller()); // 전화할 때 영상통화 +phone.videoCall(new AudioCaller()); // 타입 에러 +phone.audioCall(new AudioCaller()); // 전화할 때 음성통화 +phone.audioCall(new VideoCaller()); // 타입 에러 +``` + +음성 통화와 영상통화 기능을 모두 제공하는 `Phone` 클래스를 구현한다고 가정해봅시다. 음성통화와 영상통화는 내부적인 구현이 달라지기 때문에 각각 클래스로 구현하고 `Phone` 클래스에서 불러와서 호출하도록 +구현할 수 있습니다. + +복잡한 기능을 분리하기 위해 클래스를 분리했지만 위의 예시에서는 분리한 클래스의 기능을 위해 `Phone` 클래스에 각각 함수를 호출하는 함수가 늘어나는 문제가 발생합니다. `videoCall` +과 `audioCall` 함수에서 프로그래밍 언어적으로는 전혀 다른 클래스를 인자로 받기 때문에 함수를 다르게 두는 것 외에는 방법이 없습니다. + +그런데 정말 이 방법이 옳은 방법일까요? 현재는 음성통화, 영상통화만 있지만 최근 많이 사용하는 ZOOM이나 google meet과 같은 그룹회의, 카카오톡과 같은 채팅을 모두 같은 기능으로 묶을 수 있다고 +가정해봅시다. 그렇다면 새로운 기능을 구현하는 클래스가 나올 때마다 +`Phone` 클래스에서 새로운 함수가 나와야 한다는 말입니다. 이 방법은 Open-Closed 원칙에 위배되는 것으로 보입니다. + +이럴 때 필요한 것이 인터페이스입니다. + +```java +interface Calller { + void call(); +} + +// 각각 달라지는 구현을 구현 클래스에 구현 +class AudioCaller implements Calller { + ... +} + +// 각각 달라지는 구현을 구현 클래스에 구현 +class VideoCaller implements Calller { + ... +} + +class Phone { + ... + // call 함수 하나로 모든 작업을 만족 + public void call(Caller caller) { + caller.call(); + } +} + +... + +Phone phone = new Phone(); +phone.call(new VideoCaller()); // 전화할 때 영상통화 +phone.call(new AudioCaller()); // 전화할 때 음성통화 +``` + +앞서 말한 문제를 해결하기 위해서는 함수 인자나 클래스의 필드로 객체를 받을 때 인터페이스를 받아야 합니다. 프로그래밍 언어에서는 각 클래스들은 전혀 다른 타입입니다. 하지만 그 클래스들을 같은 `interface` +로 묶을 수 있다면 같은 타입으로 취급할 수가 있어집니다. + +클래스와 인터페이스를 활용하면 타입을 하나로 묶어주도록 구현할 수 있습니다. 하지만 반대로 생각해보면 인터페이스로 타입을 넘겨두고 각 클래스에서 구현을 한다면 하나의 인자에서 여러 구현을 받을 수 있는 것입니다. +심지어 공통으로 사용할 인터페이스를 함수 인자나 클래스의 필드로 받는다면 각 클래스의 세부 구현을 사용하는 메소드에서는 전혀 알지 않아도 됩니다. + +### 객체지향 프로그래밍의 적용 + +사실 객체지향 패러다임을 적용하는 것이 처음에는 어려울 수도 있습니다. 개발할 때 우리가 해결하려는 문제를 전체 구조에서부터 내려다 볼 수 있어야하기 때문입니다. 알고리즘 풀듯이 코드를 구현하는 바텀업 방식으로는 +어떻게 인터페이스를 묶어야 좋을 지는 크게 고려하지 않기 때문입니다. + +하지만 객체지향 프로그래밍의 핵심은 단순히 `객체로 구현한다`가 아닌 `객체를 잘 활용한다`이기 때문에 이런 원리들을 잘 지켜 좋은 코드를 작성했을 때 비로소 객체지향적인 프로그래밍을 했다고 할 수 있습니다. + +> 단순히 클래스를 사용한다고 객체지향적이지는 않습니다. +> 인터페이스를 잘 분리하고 활용하는게 객체지향의 핵심으로 보입니다. + +### 객체지향 프로그래밍의 문제는? + +너무 객체지향적으로 개발하는 것이 최고라는 글이 되는 것 같아 잠시 밸런스를 맞추겠습니다. + +객체지향 프로그래밍은 기본적으로 객체 내부에 `상태`를 두어 `어떻게 상태를 잘 변경할 것인지`를 다루는 방법입니다. 하지만 상태를 변경할 때 여러 문제가 발생합니다. + +첫째로는 단일 코어가 아닌 멀티코어 상황에서 개발을 하다보니 하나의 메모리를 동시에 여러 스레드에서 접근하는 경우가 발생합니다. 물론 이런 문제를 해결하기 위한 `atomic`연산과 `mutex` 등의 방법들이 +있지만 종종 실수가 발생하기도 합니다. 예를 들어 `mutex`로 값을 원자성 있게 변경했지만 반환한 값에 reference가 있어 메모리에 동시 접근이 되는 경우가 발생하기도 합니다. + +여기서 한가지 더 문제가 되는 것은 `atomic`연산과 `mutex`를 사용하면 병렬처리하는 효율이 떨어질 수 있다는 것입니다. 여러 스레드에서 계산하지만 결국 하나의 메모리를 접근할 때 병목이 발생하기 때문에 +특정 메모리를 접근할 때는 단일 스레드의 효율만 나오기도 합니다. + +두번째 문제로는 `변경될 수 있는 상태`를 가진 객체에서 발생한 버그는 원인을 분석하기 어렵다는 것입니다. 개발 과정에서는 초기 상태의 객체를 테스트하는데 여러 동작을 거친 객체에서는 메모리가 남아있거나 이상 동작을 +하는 경우가 종종 발생합니다. 하지만 이 과정에서 에러를 뱉은 코드는 정작 에러를 발생시킨 원인이 아닌 경우가 많습니다. + +물론 객체를 작게 만들고 역할을 분리해두어 구현했다면 그나마 낫습니다. 하지만 비즈니스 로직이 추가되거나 변경되어 객체가 점점 커지면서 문제가 발생합니다. 사실 이 부분은 객체지향 프로그래밍의 +문제라기보다는 `변경될 수 있는 상태(mutable state)`의 문제입니다. 객체지향 프로그래밍 방법은 이 문제를 없애기보다는 완화시키는 프로그래밍 방법이기 때문에 여전히 문제가 남아있습니다. 만약 이 문제를 +해결하려면 시스템 내에서 `변경될 수 있는 상태`를 저장하는 구조가 아예 없어야 합니다. + +## 마치며 + +물론 `외부에서만 입력을 받아 실행하는` (값도 외부에서 저장되어있는) 프로그램도 있지만 그렇지 못한 경우도 존재합니다. +하지만 상태를 저장하고 있어야하는 상황에서는 객체지향 프로그래밍은 굉장히 좋은 패러다임이 됩니다. +상태를 변경하는 포인트를 객체 내부로 최소화시켜 비교적 분석하기 편해진다는 사실을 알아야합니다. + +다음 장에서는 상태와 가변 데이터를 지양하는 함수형 프로그래밍에 대해 정리해보겠습니다. + +## Reference + +* [http://www.incodom.kr/객체지향](http://www.incodom.kr/%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5) +* [https://velog.io/@phs880623/객치-지향-프로그래밍](https://velog.io/@phs880623/%EA%B0%9D%EC%B9%98-%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D) +* [https://coding-factory.tistory.com/328](https://coding-factory.tistory.com/328) +* [https://www.digitalocean.com/community/conceptual_articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design](https://www.digitalocean.com/community/conceptual_articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design) diff --git a/_posts/2022-03-17-TWL-02-2-FP.md b/_posts/2022-03-17-TWL-02-2-FP.md new file mode 100644 index 0000000..d16f959 --- /dev/null +++ b/_posts/2022-03-17-TWL-02-2-FP.md @@ -0,0 +1,243 @@ +--- +layout: post +title: TWL-02 두번째. 함수지향 프로그래밍 +subtitle: 함수지향 프로그래밍에 대해 알아봅니다. +categories: [TWL, CS] +tags: [TWL, CS, FP] +--- + +프로그래밍을 하다보면 간혹 내가 예상한 값과 전혀 다른 결과가 나오곤 합니다. 왜 그런걸까요? +당연하게도 컴퓨터는 거짓말을 하지 않으니 제가 코드를 잘못 짠 탓일겁니다. + +개발 도중에 발생하는 문제는 실행시킨 후 얼마 안된 상태를 저장하고 있기 때문에 해결하기가 쉽습니다. +개발 도중에 발생한 문제는 프로그램을 재실행했을 때 금방 같은 문제가 재현되어 원인을 파악하기 쉽기 때문입니다. + +
+ 두꺼비 +
+ +원인을 제거하지 않으면 결국 뚫립니다. +{: style="text-align: center; color: gray; margin-top:.5em;"} + +하지만 한참을 돌던 프로그램이 갑자기 문제가 발생하면 원인 파악이 어렵습니다. +문제의 원인이 되는 코드를 찾지 못해 일단 프로그램에서 에러가 발생하지 않도록 땜빵 코드만 작성하기도 합니다. +하지만 땜빵한 코드는 결국 다른 곳에서 다시 에러를 발생시키기 때문에 임기응변에 그치는 경우가 많습니다. +> 2,3일동안 켜져있던 브라우저가 갑자기 종료되면 사용하던 사람도 무엇이 원인인지 알기 어렵습니다. + +어디가 문제인지 확인하기 위해 로그를 작성해 두었더라도 저장된 상태가 왜 이렇게 변경되었는지는 모든 로그를 보지 않는 한 찾기 어렵습니다. +하지만 저장되어있는 값에 따라 함수는 인자의 조건에 따라 문제가 없어보이던 코드도 문제가 발생할 수 있는 코드가 됩니다. +심지어 단순한 나누기 연산도 나누는 값에 따라 프로그램 전체에 panic을 줄 수도 있습니다. (`divide by zero`) + +그런 점에서 상태 값은 문제가 될 수 있습니다. 정확히는 변경 가능한 상태 값이 문제가 됩니다. +함수에 같은 변수 값을 인자로 입력하더라도 상태 값에 따라 결과값이 매번 변경될 수도 있습니다. + +어떤 방법에 문제가 있을 때 해결하기 위한 방법은 크게 두가지가 있습니다. +하나는 문제가 발생하더라도 컨트롤할 수 있도록 관리하는 방법이고, +다른 하나는 문제의 원인을 제거하는 방법입니다. + +잘 컨트롤할 수 있도록 하는 방법이 객체지향 프로그래밍, 문제의 원인을 제거하는 방법이 함수형 프로그래밍입니다. + +# 함수형 프로그래밍? +> 절차적 프로그래밍의 함수와는 함수의 `급`이 다르다. +> 함수형 프로그래밍은 순수함수와 불변값을 사용해 소프트웨어를 만드는 기법입니다. + +객체지향 프로그래밍은 그 이름대로 객체를 중심으로 생각해 개발합니다. 함수형 프로그래밍도 이름에서 알 수 있듯이 함수를 중심으로 생각해 개발하는 방법입니다. +객체지향 프로그래밍이 단순히 객체를 쓰기만 하면 객체지향인 것이 아닌 것처럼, 함수형 프로그래밍도 단순히 함수를 사용한다고 함수형 프로그래밍은 아닙니다. +절차적 프로그래밍에서 사용하는 함수와는 `급`이 다릅니다. + +절차적 프로그래밍에서도 함수를 정의하고 호출할 수 있지만 함수를 인자로 넘기거나 함수 내에서 함수를 생성하지 못합니다. +사실 함수를 인자로 넘기거나 함수 내에서 함수를 생성하는 것은 함수형 프로그래밍에서 특별한 것이 아닙니다. +객체지향 프로그래밍에서 필드값과 함수를 객체 내에서만 접근할 수 있도록 하고 인터페이스를 통해 메소드를 구현하는 것이 당연하듯이 +함수형 프로그래밍에서도 당연하게 함수를 인자로 넘기고 함수 내에서 함수를 생성할 수 있습니다. + +이 차이가 객체지향 프로그래밍과 함수형 프로그래밍의 차이를 만들어냅니다. +객체지향 프로그래밍에서는 함수의 인자나 객체의 필드로 `동작을 가지고 있는 객체`를 넘겨 함수 내부에서 동작을 호출하도록 만들었다면 +함수형 프로그래밍에서는 함수의 인자로 `동작` 자체를 넘겨 조합해 실행합니다. + +## 선언형 언어, 명령형 언어 + +먼저 짚고 넘어가야할 것이 함수형 프로그래밍은 객체지향 프로그래밍과 절차적 프로그래밍과는 구조가 다릅니다. + +
+ 선언형 명령형 프로그래밍. +
+ +선언형 언어로 대표적인 함수형 프로그래밍과 명령형 언어로 대표적인 객체지향 프로그래밍 +{: style="text-align: center; color: gray; margin-top:.5em;"} + +언어를 두가지로 나눠본다면 명령형 언어와 선언형 언어로 나눌 수 있습니다. +먼저 명령형 언어는 우리가 일반적으로 사용하는 프로그래밍 언어처럼 어떻게 동작해야하는지를 작성하는 언어입니다. +반대로 선언형 언어는 무엇을 표현하는 것인지를 작성하는 언어입니다. + +명령형 언어만이 프로그래밍 방법은 아니지만 우리가 주로 사용하는 프로그래밍 언어들은 대부분 명령형 언어를 사용중입니다. +어셈블리 같은 기계 코드에서도 연산들이 명령형으로 작성되어있기 때문에 그것을 기반으로 만들어지는 프로그래밍 언어들에서 명령형 +언어로 작성되는 것이 당연해보입니다. + +
+ HTML +
+ +HTML은 프로그래밍 언어는 아닙니다...만! 선언형 언어입니다. +{: style="text-align: center; color: gray; margin-top:.5em;"} + +그에 반해 선언형 언어로 주로 알려진 언어들은 `HTML`과 `SQL`입니다. `어떤 작업을 해야한다` 보다는 `무엇을 보여준다`에 가깝습니다. +HTML는 헤더, 각 컴포넌트들을 통해 무엇을 화면에 보여줄 지를 표현하는 언어이고, SQL도 DB에서 읽어오는 작업 때문에 +`어떻게 동작해야한다.`로 보이기도 하지만 `테이블 중에서 무엇을 보여준다`에 가깝습니다. + +그렇다면 함수형 프로그래밍 언어는 `무엇을 보여준다`를 통해 어떻게 개발하는 걸까요? + +### 함수형 언어? + +기존 언어들에서는 `함수가 무슨 동작을 하는지` 정의하고 호출할 때 정의된 동작들을 실행했습니다. +함수형 프로그래밍에서는 동작을 정의하기보다는 `함수가 무엇인지` 정의하고 호출할 때 인자들을 정의된 값에 대입한다고 생각하면 편합니다. + +```python +def fibo(n): + res = 1 + while n > 1: + res *= n + n -= 1 + return res +``` +```haskell +fibo n + | n < 2 = 1 + | otherwise = fibo(n-2) + fibo(n-1) +``` + +파이썬과 하스켈에서의 피보나치 +{: style="text-align: center; color: gray; margin-top:.5em;"} + +예를 들어 위의 파이썬 코드로 피보나치를 구현하면 입력 인자 n 을 받아 반복문을 돌면서 `res`값에 n을 곱하고 n을 1 감소시키는 동작을 반복하도록 +구현할 수 있습니다. (물론 파이썬에서도 재귀로 작성할 수 있지만 함수형과의 차이를 더 크게 보여주기 위해) +하스켈에서의 코드는 n의 값에 따라 `동작할 명령어의 순서`를 정의하기보다는 n 값에 따라 함수의 결과에 `대입될 값`을 정의합니다. +n이 2보다 작을 때는 1을 대입하고 그 외에는 `fibo(n-2) + fibo(n-1)`을 대입하게 됩니다. + +명령형 프로그래밍에서는 함수를 `실행해야할 동작들`로 보고 호출을 `실행한 결과를 반환하는 연산`으로 보지만 +함수형 프로그래밍에서는 함수를 `인자에 따른 값`으로 보고 호출을 `인자가 입력되었을 때에 표현되는 값`으로 봅니다. +한가지 더 중요한 것은 함수에 인자를 넣을 때에서야 **lazy**하게 값이 **평가**된다는 것입니다. + +```python +def add5(a): + return add(5, a) + +def add(a, b): + return a + b + +>>> add5(3) +8 +``` +```haskell +Prelude> add a b = a + b +Prelude> add5 = add 5 +Prelude> add5 3 +8 +``` + +여기서 얻을 수 있는 장점은 함수에서 함수를 만들어내기 편하다는 점입니다. 함수를 lazy하게 평가되는 값으로 보기 때문에 +함수에서 함수를 만들어내기 쉽습니다. lazy하게 평가되는 다른 값을 만들어내는 것이기 때문입니다. + +위의 코드는 비슷하게 보이지만 동작은 꽤 차이가 있습니다. 두 코드 모두 `add5`를 호출해 계산하고 있는데, +파이썬 코드에서는 `add5`함수가 호출될 때, `add` 함수에 5와 `a`값을 입력해 호출하도록 동작합니다. +하지만 하스켈에서는 `add5`에 `add` 함수에 5를 넣어 새로운 함수를 만듭니다. +그 후에 새로운 함수 `add5`에 3을 대입해서 계산을 합니다. + +물론 하스켈과 똑같이 동작하도록 만들수도 있습니다. + +```python +def add(a): + def _add(b): + return a + b + return _add + +>>> add5 = add(5) +>>> add5(3) +8 + +# 하지만 add를 사용할 때는 아래처럼 사용해야 합니다. +>>> add(5)(3) +``` + +파이썬으로 하스켈과 비슷하게 동작하도록 작성한 코드를 보면 더 쉽게 이해할 수 있습니다. +위의 `add`함수를 보면 실제로 연산이 실행되는것은 `a`와 `b` 모두 입력되었을 때입니다. + +이것이 바로 lazy한 연산입니다. 실제로 값이 모두 주어졌을 때 계산하는 것입니다. + +기존 프로그래밍 언어의 시각으로 생각해보면 `add`에 `a` 인자를 넘기면 `a`라는 상태가 **불변으로 저장**되고, +이후에 `b`를 입력받아 + 연산을 한다고 이해할 수 있습니다. + +## 함수형 프로그래밍의 특징 + +함수형 프로그래밍의 특징으로는 주로 아래 특징들이 꼽힙니다. + +* 순수 함수 + * 함수의 `결과가 파라미터에만 결정`되고 어떠한 `side effect도 일으키지 않는` 함수 + * 함수를 실행할 때 `외부의 값을 접근하거나 수정`하지 않습니다. (이렇게 개발해야 좋다 라는 의미입니다) + * side effect가 있는 함수는 비순수 함수라고 합니다. +* 참조 투명성 + * 인자가 같은 함수 호출을 함수의 결과로 대체할 수 있는 성질 + * 순수 함수로 구현한다면 함수 결과로 대체하더라도 문제가 없습니다. +* 불변 데이터 + * 인자로 입력한 데이터의 값을 변경시키지 않음 + * 함수를 실행할 때 `파라미터의 값을 변경하지 않음` +* 일급 함수 + * 함수를 파라미터로 사용하거나 반환값으로 사용하는 변수처럼 생각할 수 있는 특징 +* 지연 평가 + * 계산 결과가 필요할 때까지 연산을 늦추는 방법 + +명령형 프로그래밍 언어를 주로 사용하던 우리들에게 `상태를 변경하지 않고` 어떻게 구현할 지 감이 잘 오지 않습니다. + +하지만 흔하게 사용하는 선언형 언어인 HTML을 생각해보면 `상태를 변경하지 않는 것`이 오히려 당연합니다. +HTML 코드에서 각 element가 내부 element 값을 변경한다는 것은 상상하기 어렵습니다. +또한 `element 구조가 같다면 항상 같은 모습의 화면`을 보여줄 것입니다. +만약 element 구조가 같은데 외부적인 요소에 의해서 HTML 구조가 달라진다면 화면을 구성하기 더 어려울 것입니다. +내부 값을 변경하는 대신 `여러 element를 조합해서 전체적인 구조`를 만들어냅니다. + +함수형 프로그래밍에서도 마찬가지입니다. HTML처럼 `인자로 들어온 상태를 변경하지 않는 것`이 당연합니다. +함수에 인자로 들어온 `값을 변경하지 않고`, `함수의 인자가 같다면 같은 결과`를 내보내야 합니다. +그리고 인자나 변수의 값을 변경하는 동작 대신, `인자를 입력했을 때 여러 함수를 조합하여 원하는 값`을 만들어내는 것입니다. + +### 순수 함수 (side effect가 없는 함수)와 참조 투명성 +> 순수함수 = 결과값이 인자에 의해서만 결정 + side effect가 없음 + +순수 함수는 동일한 입력값을 받으면 같은 결과를 내보내고, 함수를 실행했을 때 값이 달라지는 `side effect`가 없는 함수입니다. +`side effect`란 값을 변경하는 행위입니다. 실제로 변수 값이 변경되지 않더라도 print를 찍어보거나 로그를 찍는 행위, 파일에 저장하는 행위는 +외부의 값을 변경하기 때문에 `side effect`입니다. 또한 변수의 값을 assign 하는 행위, `set` 함수를 통해 reference의 값을 변경하는 +행위 또한 `side effect`라고 할 수 있습니다. + +객체지향 프로그래밍에서는 이런 `side effect`를 객체의 메소드에서만 수행하도록 만들어 `side effect`는 발생하더라도 어디서 문제가 발생한 것인지 +비교적 쉽게 만들어줍니다. 하지만 함수형 프로그래밍에서는 애초부터 `side effect`를 허용하지 않는 순수함수를 사용하기를 권장합니다. + +순수함수는 입력을 넣었을때 결과가 달라지도록 하는 어떠한 `side effect`도 발생시키지 않기 때문에 다음에 같은 인자를 넘긴다면 +같은 결과를 내보냅니다. 참조 투명성은 이런 순수함수에서 함수를 다시 실행시키지 않고 결과값을 그대로 대입해도 같은 결과가 됨을 나타내는 성질입니다. +함수형 언어에서는 이 성질을 이용해 같은 결과를 여러번 이용해야하는 재귀 함수를 최적화하기도 합니다. + +### 불변 데이터 + +불변 데이터는 말 그대로 변하지 않는 값입니다. 예를 들어 자바에서 `final`과 같은 키워드를 볼 수 있습니다. +값이 이후에 더이상 변경되지 않도록 보장하는 키워드입니다. + +사실 객체지향 프로그래밍의 `private`에 해당하는 작업이 불변 데이터입니다. `private` 변수는 객체 내부에서만 +값을 변경할 수 있도록 하지만 함수형 프로그래밍은 값을 아예 변경하지 못하도록 권장합니다. + +### 일급함수 + +일급함수는 함수 + +## 함수형 프로그래밍의 기법들 + +함수형 프로그래밍의 개념만 보아서는 크게 핵심을 이해하기는 어렵습니다. + + +### 고차 함수 + +### 커링 + +### Partial Application + +### 클로저 + + +## Reference + +* [https://wiki.haskell.org/Thunk](https://wiki.haskell.org/Thunk) diff --git a/_posts/2022-03-17-TWL-02-3-OOP-FP.md b/_posts/2022-03-17-TWL-02-3-OOP-FP.md new file mode 100644 index 0000000..0b1e41a --- /dev/null +++ b/_posts/2022-03-17-TWL-02-3-OOP-FP.md @@ -0,0 +1,35 @@ +--- +layout: post +title: TWL-02 세번째. 어떻게 프로그래밍해야할까 +subtitle: 객체지향, 함수형 프로그래밍을 어떻게 적용할지 이야기합니다. +categories: [TWL, CS] +tags: [TWL, CS, OOP, FP] +--- + +사실 객체지향 프로그래밍과 함수형 프로그래밍 모두 좋은 코드를 작성하기 위한 방법론입니다. +우리의 목적은 객체지향적으로 작성하거나 함수형으로 작성하는것이 아니라 좋은 코드를 작성하는 것이기 때문에 +각 상황에 맞게 더 좋은 방법을 선택하면 됩니다. + +# 어떻게 개발해야할까? + +객체지향 + + +## 동일한 문제를 어떻게 해결하는지 + + +## + +# 상태를 가지지 않고 개발할 수 있을까? + + + + + +## Reference + +* [http://www.incodom.kr/객체지향](http://www.incodom.kr/%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5) +* [https://velog.io/@phs880623/객치-지향-프로그래밍](https://velog.io/@phs880623/%EA%B0%9D%EC%B9%98-%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D) +* [https://coding-factory.tistory.com/328](https://coding-factory.tistory.com/328) +* [https://www.digitalocean.com/community/conceptual_articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design](https://www.digitalocean.com/community/conceptual_articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design) +* [https://wiki.haskell.org/Thunk](https://wiki.haskell.org/Thunk) diff --git a/_posts/2022-03-17-TWL-02-OOP-FP.md b/_posts/2022-03-17-TWL-02-OOP-FP.md deleted file mode 100644 index e20c6fa..0000000 --- a/_posts/2022-03-17-TWL-02-OOP-FP.md +++ /dev/null @@ -1,474 +0,0 @@ ---- -layout: post -title: TWL-02 객체지향과 함수지향 -subtitle: 객체지향, 함수지향에 대해 알아봅니다. -categories: [TWL, CS] -tags: [TWL, CS, OOP, FP] ---- - -# 객체지향 프로그래밍 (OOP) -> Object Oriented Programming - -요즘은 어떤 프로그래밍 언어를 처음 시작하는지 모르겠지만 저는 첫 프로그래밍 언어로 `Java`를 배웠습니다. 커피를 마시면서 만들어서 `Java`라는 이름을 가졌다는 이야기와 VM을 사용하는 **객체지향적 -프로그래밍언어**라는 이야기를 처음 들으며 언어를 시작했고 이후에는 `Python`, `C`, `C++`의 언어들을 배우게 되었습니다. -(그 학생은 5년 뒤 Java를 쓰지 않고 `Rust`, `Go`를 사용하게 됩니다...) - -최근에는 Java를 사용하지는 않지만 객체지향적으로 개발하라는 말은 계속해서 듣고 있습니다. 결국 어떤 언어를 사용하는지보다는 어떻게 개발하는지가 더 중요한 것 같습니다. -그러면 어떤 장점이 있어서 객체지향적으로 개발하라는 걸까요? - -## 객체지향적으로 개발하라? - -먼저 객체지향적으로 개발한다는 의미를 알아야 합니다. 객체 지향적으로 개발한다는 것은 실제 세상처럼 객체들의 집합으로 생각하고 프로그래밍한다는 것입니다. -간단한 예를 들자면 강아지, 책상과 같은 객체에서의 값(나이, 책상서랍 등)과 동작(짖다, 서랍에 넣다 등)을 객체에서 갖도록 해서 좀 더 코드를 이해하기 쉽게 만드는 것입니다. -보통 우리가 다른 사람에게 무언가 설명할 때 현실에 있는 다른 무엇가를 비유해서 설명하는 것처럼 코드도 객체에 비유해서 쉽게 이해할 수 있도록 하는 것이죠. - -개발하다보면 동작이 얼마 없을 때는 문제 없지만 동작이 점점 커지면서 각 동작을 작은 단위로 분리해야하는데, -이때 어떤 객체에서 무슨 작업을 할 지 이름만으로 쉽게 알 수 있어 이런 방법은 합리적으로 보입니다. -물론 객체로 감싸는 과정에서 어느정도 오버헤드가 발생하기는 하지만 성능이 너무 중요한 상황이 아니라면 -충분히 감당 가능한 정도입니다. 어느정도 메모리, cpu 낭비를 하더라도 더 읽기 쉽고 생산성 있는 코드가 더 중요한 것입니다. -(물론 낭비하는 자원이 감당 가능한 레벨이어서 가능한 겁니다) - -### 절차적 언어 -> Procedural Programming (프로시저 프로그래밍) - -객체 지향적으로 개발하라고 말할 때 주로 비교되는 것은 절차적 프로그래밍(`procedural programming`)이었지만 -최근에는 함수형 프로그래밍(`functional programming`)이 주로 비교되는 것 같습니다. -> `procedural programming`은 사실 프로시저(함수) 프로그래밍에 가깝지만 (직역한) 번역명으로 더 잘 알려진 절차적 프로그래밍이라고 적겠습니다 -> -> `oriented`가 없는데 절차 "지향"적 언어라고는 못 적겠습니다. - -먼저 절차적 프로그래밍부터 알아보면 **"코드를 순서대로 작성하고 실행하는 언어"** 입니다. -사실 객체지향적으로 개발하면서도 내부적으로는 어떠한 절차를 거쳐서 개발하기 때문에 완전히 다른 개발 방법이라고 하기에는 문제가 있습니다. -객체지향적 프로그래밍 방법과 절차적 프로그래밍의 가장 큰 차이점은 데이터와 함수가 어떻게 묶이는지 입니다. - -```c -void move(int* x, int* y, int move_x, int move_y) { - *x = *x + move_x; - *y = *y + move_y; -} - -int main() { - int my_x = 0; - int my_y = 0; - // 내 위치를 (2, 4) 만큼 이동한다. - move(&my_x, &my_y, 2, 4); - return 0; -} -``` - -절차적 언어인 c 언어에서는 함수가 정해져있고 데이터를 입력으로 보냅니다. -{: style="text-align: center; color: gray; margin-top:.5em;"} - -절차적 언어로 가장 유명하고 현재도 많이 사용되는 c 언어로 예를 들어보면 어떠한 동작을 하는 함수를 구현하고, -함수에 입력으로 값이나 포인터를 넘겨 연산을 순서대로 실행하도록 개발합니다. - -#### 추상화 -> 모든 프로그래밍 방법론은 추상화에서 시작된다. - -절차적 언어인 c에서는 추상화가 없다고 할 수는 없습니다. -추상화라는 것은 구체적으로 작업을 표현하는 것이 아닌 핵심적인 개념만을 표현하는 것입니다. -위의 코드에서는 `x, y를 조작하는 구체적인 작업`이 아닌 `move라는 핵심적인 동작(함수명)`만으로 사용할 수 있도록 추상화 한것입니다. -이후에는 어떻게 동작하는지는 함수명과 설명에 대한 신뢰를 가지고 `move` 함수를 호출해 사용할 수 있습니다. -> 물론 실제 구현이 설명과 다르다면 문제가 발생합니다. - -내부적인 구현을 정확히 이해하지 않고도 사용할 수 있다는 점이 구체적인 동작을 그대로 사용하지 않고 추상화하는 이유 중 하나입니다. - -```c -void move(int* x, int* y, int move_x, int move_y) { - *x = *x + move_x; - *y = *y + move_y; -} - -void jump(int* x, int* y, int move_x, int move_y) { - *x = move_x; - *y = move_y; -} - -int main() { - int my_x = 0; - int my_y = 0; - // move 대신 jump 하도록 수정 - // move(&my_x, &my_y, 2, 4); - jump(&my_x, &my_y, 2, 4); - return 0; -} -``` - -추상화된 동작은 대체하기도 쉽다. -{: style="text-align: center; color: gray; margin-top:.5em;"} - -연산을 추상화하게 되면 사용할 때 `전체적인 로직에 대해서 쉽게 이해`할 수 있습니다. -`main` 안에서 `my_x`와 `my_y`를 단순히 더하거나 빼는 연산보다는 -`move`라는 함수 안에 로직을 넣고 `main`에서는 `move` 함수를 호출하는 것이 어떤 작업을 하는 건지 쉽게 이해할 수 있습니다. - -뿐만 아니라 추상화된 작업은 `다른 작업으로 쉽게 변경할 수 있습니다`. -현재는 코드를 `move`하는 동작으로 사용하고 있지만 만약 `jump`하는 동작으로 수정되어야 한다면 호출하는 함수를 변경하는 것으로 -쉽게 다른 동작으로 변경할 수 있습니다. - -비즈니스 동작이 달라지거나 최적화가 필요하거나 혹은 업데이트를 하는 등 여러 이유로 코드는 변경될 가능성을 가지고 있습니다. 그런 점에서 -추상화를 통해 이전 코드를 간단하게 대체할 수 있다는 것은 굉장한 장점입니다. - -절차적 언어에서는 함수 단위의 추상화를 제공하고 있습니다. 만약 어셈블리 언어로 -개발했다면 함수를 통해 정해진 연산을 호출하는 작업은 생각하지 못했을 것입니다. 하지만 c언어에서는 구체적인 연산을 매번 사용하지 않고 -함수로 호출할 수 있도록 추상화를 제공하고 있습니다. -> 절차적 프로그래밍은 함수로 묶는 정도의 수준으로 함수형 프로그래밍과는 다릅니다 - -어떠한 **"절차"** 에 대한 추상화만을 제공하는 것입니다. - -### 객체지향 -> 객체로 모든 것을 생각해 프로그래밍 하는 것 - -그렇다면 이제 객체지향에서의 추상화에 대해 조금 더 쉽게 이해할 수 있을 것 같습니다. 절차적 프로그래밍에서는 절차를 함수로 추상화했다면 객체지향 프로그래밍에서는 -객체를 추상화합니다. c언어에서 절차에 대한 추상화 기능으로 함수 기능을 제공해 절차적 언어라고 말하는 것처럼, -객체지향 언어는 객체를 추상화할 수 있도록 기능을 제공하는 언어를 말합니다. -> c언어에서 객체지향처럼 흉내낼 수는 있지만 객체지향 언어라고 하기에는 기능이 부족합니다. - -앞서 설명했던 예시를 객체지향적으로 수정해보겠습니다. - -```java -interface Mover { - void move(int moveX, int moveY); -} - -class Human implements Mover { - private int x; - private int y; - - public Human(int x, int y) { - this.x = x; - this.y = y; - } - - @Override - public void move(int moveX, int moveY) { - this.x += moveX; - this.y += moveY; - } -} - -class Jumper implements Mover { - private int x; - private int y; - - public Jumper(int x, int y) { - this.x = x; - this.y = y; - } - - @Override - public void move(int moveX, int moveY) { - this.x = moveX; - this.y = moveY; - } -} - -public class Main { - public static void main(String[] args) { - Mover mover = new Human(0, 0); - Mover jumper = new Jumper(0, 0); - mover.move(2, 4); - jumper.move(2, 4); - } -} -``` - -main에서는 추상화된 객체의 동작만이 실행된다. -{: style="text-align: center; color: gray; margin-top:.5em;"} - -객체지향 프로그래밍에서는 함수만 추상화하는 것이 아니라 사용되는 값도 함께 객체로 추상화합니다. -동작은 클래스에서 구현하지만 사용할 때는 인터페이스의 추상화된 함수를 호출합니다. (객체와 동작의 추상화) -또한 추상화된 함수에서 다룰 값도 함께 객체에서만 접근할 수 있도록 만들어 외부 동작에 의해 -추상화된 로직이 깨지지 않도록 만들 수 있습니다.(캡슐화) - -객체에 대해 추상화가 되었기 때문에 이전에 함수를 다른 기능의 함수로 대체했듯이 -다른 작업을 하는 객체로도 대체할 수 있습니다. - -### 객체지향의 특징 - -기본적으로 꼽히는 객체지향의 특징은 다음과 같습니다. - -1. 추상화 - * 객체를 추상화해 복잡한 로직이 아닌 객체, 함수를 통해 핵심적인 기능만을 볼 수 있도록 합니다. -2. 캡슐화 - * 데이터를 객체 내부에 저장해 접근제어자(`private`, `public`)를 통해 외부에서 조작하지 못하도록합니다. - * 추상화된 로직을 사용할 때 내부 데이터를 외부에서 접근한다면 정해진 로직이 깨질수 있습니다. -3. 상속 - * 상위 클래스의 필드와 연산을 하위클래스에서 가질 수 있습니다. - * 최근에는 상속의 문제점들로 인해 상속을 지원하지 않는 언어들이 보이고 있습니다. (`rust`, `go`) - * 대신 내부 필드로 확장할 객체를 가지는 `composition`으로 구현합니다. -4. 다형성 - * 같은 이름의 함수를 다양한 타입이나 다양한 구현(`overriding`)으로 - * `overloading`, `overriding`을 이용해 다형성이 발현됩니다. - * `overriding`하면서 `Dependency Injection(DI)`을 가능하게 하는 기본 성질입니다. - * 테스트 코드의 mocking 에도 사용되는 성질입니다. - -#### 객체지향의 추상화 - -실생활에서 우리는 여러 전자 기기를 사용하지만 내부적으로 어떻게 동작하는지 모릅니다. 어떻게 돌아갈지 짐작만 하고 있을 뿐이죠. -대부분 사용설명서를 읽고 원하는 기능을 찾아 기기를 사용합니다. 추상화하는 것도 동일합니다. 우리는 구현할 때와 사용할 때를 구분해서 -생각해야합니다. 절차적 프로그래밍의 함수나 객제지향 프로그래밍의 객체를 구현할 때는 전자기기를 만드는 것과 마찬가지로 -상태가 어떻게 관리되고 어떻게 동작하는 지 알고 있어야 합니다. 하지만 구현된 함수나 객체를 사용할 때는 전자기기를 사용하는 것처럼 -굳이 내부에 대해 모르더라도 사용할 수 있어야 합니다. -> 사용 설명서를 읽지도 않고 어림짐작으로 사용하던 모습은 api 설명을 안 보고 함수명만 보고 사용하는 모습과 비슷합니다 - -앞서 말했지만 추상화는 객체지향에서만의 특징은 아닙니다. 절차적 프로그래밍에서도 추상화가 들어있습니다. -다만 추상화하는 정도나 대상이 다를 뿐입니다. - -절차적 프로그래밍에서 코드를 연산 레벨에서 추상화 했다면, 객체지향 프로그래밍에서는 코드를 객체 레벨에서 추상화하게 됩니다. -절차적 프로그래밍에서는 `어떤 상태를 받아 연산을 하는 함수` 로 추상화하고, 객체지향 프로그래밍에서는 `상태도 연산과 함께 추상화` 해서 -객체에서 관리합니다. 상태를 함께 추상화한다는 것은 -추상화된 인터페이스를 사용할 때 `상태가 어떤 방식으로 관리되는지 모르더라도 사용할 수 있음`을 의미합니다. - -예를 들어 절차적 프로그래밍에서 함수를 정의해 연산을 추상화하고, 함수를 호출해서 사용함으로써 내부 연산을 모르더라도 사용할 수 있지만 -입력으로 전달하는 `struct` 값들은 따로 관리하고 있어야 합니다. - -하지만 객체지향 프로그래밍을 하면 상태에 대한 관리도 객체에서 함으로써 객체를 사용할 때 더이상 상태값이 어떻게 관리되는지 -모르더라도 사용하는 데 문제가 없습니다. 상태값을 어떻게 관리할 지는 객체를 구현할 때의 문제입니다. - -예를 들어 처음 파이썬 또는 자바스크립트를 공부할 때, `dictionary`가 내부적으로 어떻게 구현되어있는지 모르더라도 -`key`로 값을 저장하고 가져온다는 사실만 알고 있으면 `dictionary`를 사용할 수 있습니다. 추상화하면 (성능은 잠시 미뤄두고...) -객체의 구현에 대해 모르더라도 `사용하는데는 문제가 없습니다`. 그 후 `dictionary`의 구현을 Hash를 이용할 것인지 Tree를 -이용할 것인지는 `구현의 문제`입니다. - -#### 캡슐화, 정보은닉 - -캡슐화는 객체의 상태나 연산을 해당 객체에서만 접근할 수 있게해서 외부 연산에 의해 오류가 발생하지 않도록 합니다. -Java나 C++에서 제공하는 `private`, `protected`, `public` 접근제어자를 통해 객체, 상속받은 객체, 외부에서 -접근하는 것을 제어하며, golang은 대문자, 소문자를 이용하고, Rust에서는 `pub` 접근 제어자를 이용합니다. -> 파이썬은 `_`를 앞에 붙여 접근제어자처럼 사용하지만 실제로 접근은 가능하기에 접근제어자가 없어 완벽한 객체지향언어가 아니라는 말이 나옵니다. - -연산이 복잡해지게 되면 함수를 계속 생성하게 되는데 이때 객체 내부에서만 재활용할 연산들은 `private`으로 객체 내부에서만 -접근할 수 있도록 두어 외부 인터페이스와 내부에서 사용할 함수를 구분하는데 주로 사용합니다. - -#### 상속 - -최근 언어들에서는 클래스 상속 기능을 제공하지 않는 언어들이 많습니다. 상속은 상위 클래스에서 구현한 함수나 필드를 하위 클래스에서도 -물려받는 것을 의미합니다. 상위 클래스에서 구현한 기능을 따로 구현하지 않더라도 하위 클래스에서 재사용할 수 있다보니 -구현하는 측면에서 재사용성이 늘어나는 장점이 있습니다. - -하지만 상속으로 구현하게 되면 하위 클래스에서 함수를 오버라이딩하거나 오버로딩할 수 있고, 혹은 여러 클래스를 다중 상속받으면서 -하위 클래스에서 상위 클래스의 기능을 깨뜨리는 경우도 발생합니다. 자세한 내용은 이후 객체지향 원칙에서 설명하겠지만 -상속의 문제점들로 인해 최근에는 상위 클래스를 직접 상속받는 것이 아닌 필드로 받아 사용하는 `composition` 방식으로 주로 구현하고 있습니다. - -하지만 문제를 발생시키지 않는 인터페이스를 `implements`하는 기능들은 go(`interface`)와 rust(`trait`)에서 제공합니다. -아무래도 인터페이스의 구현은 객체지향 프로그래밍에서 가장 중요한 역할을 하기 때문에 최근에 나온 언어들에서도 제공되는 것이 아닌가 싶습니다. - -#### 다형성 - -함수 이름은 같지만 다양한 타입이나 구현을 가질 수 있는 성질입니다. 기본적으로는 함수이름이 같지만 다른 타입의 파라미터와 리턴값을 가지는 -`overloading`과 하위 클래스에서 함수를 재작성하는 `overriding`으로 이루어집니다. - -최근 언어에서는 `overloading`은 오히려 코드를 읽는데 헷갈릴 수 있다는 이유로 지원하지 않는 언어도 있지만 `overriding`은 다릅니다. -최소한 `interface`의 함수를 `overriding`할 수 있도록 기능을 제공하고 있습니다. -각 클래스에서는 구현한 클래스 타입을 필드로 가지는 것이 아니라 `interface`를 타입으로 필드를 가집니다. -그 후에 사용할 때 구현 클래스를 대입해 `interface`로 호출하지만 대입한 구현 클래스를 실행시키는 효과를 얻을 수 있습니다. - -이를 통해 다른 기능을 사용할 때 다른 구현 클래스를 대입해 사용해 내부 구현을 변경하지 않더라도 다른 기능을 실행할 수 있고, -함수의 인자로 `interface`를 받는다면 함수의 구현을 변경하지 않더라도 외부에서 원하는대로 함수가 동작하도록 구현 클래스를 넘기는 -`Dependency Injection(DI)`을 할 수도 있습니다.: - -### 객체지향의 원칙 -> 어떻게 개발해야 잘 개발하는 것인가? - -그럼 객체지향적으로 개발할 때 무슨 원리를 기반으로 개발하고 있을까요? 바로 SOLID입니다. - -* S: SRP: Single Responsibility (단일 책임의 원칙) - * 하나의 객체는 하나의 책임(기능, 역할)만을 가져야합니다. -* O: OCP: Open-Closed (개방 폐쇄의 원칙) - * 기능 확장에는 열려있고, 수정(구현 코드)에는 닫혀있어야합니다. -* L: LSP: Liskov Substitution (리스코프 치환의 원칙) - * 부모 클래스의 인스턴스 위치에 자식 클래스를 두더라도 문제가 없어야 합니다. - * 여기서 상속이 문제를 발생시킵니다. -* I: ISP: Interface Segregation (인터페이스 분리의 원칙) - * 클래스 내에서 사용하지 않을 인터페이스(함수)는 구현하지 않아야 합니다. - * 각 인터페이스를 최소화하여 분리합니다. -* D: DIP: Dependency Inversion (의존 역전의 원칙) - * 상위 모듈은 하위 모듈에 의존하지 않고 추상화가 세부 구현(인터페이스에 없는 함수나 특정 구현 클래스)에 의존하지 않아야 합니다. - * 추상화하는 함수가 primitive 타입이나 인터페이스를 받아 구현하는것은 괜찮지만 특정 구현 클래스를 받는 것은 지양해야 합니다. - -#### SRP: Single Responsibility (단일 책임의 원칙) -> 하나의 객체는 하나의 책임(기능, 역할)만을 가져야합니다. -> 객체만이 아니라 모듈이나 함수를 구현할 때에도 마찬가지입니다. -> 한 객체가 하나의 함수만 가져야만 한다는 것은 아닙니다. - -단일 책임의 원칙은 하나의 객체 또는 함수에서 하나의 역할만을 하도록 구현해야 한다는 원칙입니다. -클래스를 통해 개발할 때 주로 발생하는 문제는 하나의 객체가 너무 커진다는 것입니다. -주로 기능을 추가하다보면 필요한 기능을 하나 둘씩 편의를 위해 기존에 있는 객체에 함수로 추가하게 되고 -점점 객체는 특정 하나의 역할만을 하는 것이 아닌 모든 것에 다재다능한 슈퍼 객체가 되어버립니다. - -```java -class SuperDataViewer { - // 비슷한 작업의 함수만 늘어가면서 객체가 비대해짐 - public void viewHtml() { - ... - } - - public void viewMarkDown() { - ... - } - - public void viewCSV() { - ... - } - - // Viewer에서는 보여주는 작업만 하고 update는 Viewer를 사용하는 클래스에서 구현하는 것이 옳음 - public void updateView() { - ... - } -} - -interface DataViewer { - void view(); -} - -class HTMLViewer implements DataViewer { - @Override - public void view() { - ... - } -} -``` - -이것은 사실 객체지향적으로 프로그래밍한 코드라고 볼 수 없습니다. 객체 안에 절차적 프로그래밍의 함수를 때려넣은 것 뿐이죠. -주로 주의할 부분은 하나의 객체에서 비슷한 기능을 하는 작업을 함수만 늘려서 구현하거나 하나의 클래스의 역할을 너무 크게 생각해 -너무 많은 기능을 넣은 경우입니다. - -위의 예에서는 각각 다른 데이터를 보여주는 상황으로 `HTMLViewer`, `MardownViewer`, `CSVViewer`와 같은 방식으로 -각각 다른 클래스에서 `view`함수를 구현하는 방식으로 구현하는 것이 더 좋아보입니다. -그리고 `updateView`와 같은 `view`를 다루는 상위 작업은 `DataViewer`를 사용하는 클래스에서 구현해두면 -`DataViewer`는 `view`라는 작업에만 집중할 수 있어 역할이 더 명확해집니다. -`update`기능 구현이 `DataViewer`의 클래스에 있는게 구현이 조금 더 편하다는 이유로 분리되지 않는다면 -점점 거대해진 모든 역할을 하는 클래스가 추가될 수 있습니다. - -#### OCP: Open-Closed (개방 폐쇄의 원칙) -> 기능 확장에는 열려있고, 수정(구현 코드)에는 닫혀있어야합니다. -> 기능 추가는 쉽게 할 수 있지만 기존 코드는 건드리지 않아야 합니다. - -개방 폐쇄의 원칙은 기능을 추가하더라도 기존 코드를 건드리지 않아야한다는 원칙입니다. -새로운 기능을 추가한다는 이유로 기존 코드를 건드리게 되면 멀쩡히 동작하던 기능이 갑자기 동작하지 않을 수도 있습니다. -앞서 보여드렸던 코드가 이 문제를 해결하는 방법이 될 수 있습니다. - -```java -class RealTimeDataViewer { - private final DataViewer dataViewer; - - RealTimeDataViewer(DataViewer dataViewer) { - this.dataViewer = dataViewer; - } - - void updateView() { - ... - this.dataViewer.view(); - } -} - -interface DataViewer { - void view(); -} - -class HTMLViewer implements DataViewer { - @Override - public void view() { - ... - } -} - -class MarkDownViewer implements DataViewer { - @Override - public void view() { - ... - } -} -``` - -만약 MarkDown을 보여주는 viewer를 제공하기 위해 기존 구현을 건드린다면 객체가 너무 비대해져 코드를 읽기 어렵거나 -기존 코드를 건드리면서 예상치 못한 버그를 발생시킬 수 있습니다. -애초부터 다른 객체에 새로운 기능을 구현하고 해당 기능을 사용하면 됩니다. - -다른 클래스에 구현해서 사용하면 당연히 확장에도 열려있고 수정에도 닫혀있겠지만 그것만으로는 사용하는 클래스의 코드를 변경하는 문제가 발생합니다. -개방 폐쇄의 원칙은 단순히 클래스만 따로 만들라는 것이 아닙니다. 클래스를 따로 만들지만 같은 인터페이스를 `implements`함으로써 -사용하는 곳에서는 다른 구현체만 넘겨주어 수정된 구현을 사용할 수 있게 하라는 것입니다. - -Java에서 `List` 인터페이스가 있지만 원하는 상황에 따라 다른 성능을 위해 `List list = new ArrayList<>();`처럼 -각기 다른 `ArrayList`, `LinkedList`를 사용하는 상황과 비슷합니다. 원하는 `List` 형태가 있다면 직접 구현해서 -기능을 확장할 수 있지만 기존에 있는 다른 형태의 `List`에 전혀 영향을 주지 않죠. -> 인터페이스 설계부터 잘 고려해서 작성해야 개방 폐쇄의 원칙을 지킬 수 있게 됩니다. - -#### LSP: Liskov Substitution (리스코프 치환의 원칙) -> 부모 클래스의 인스턴스 위치에 자식 클래스를 두더라도 문제가 없어야 합니다. - -보통 클래스를 구현할 때 구현한 클래스는 `어떤 조건을 만족해야한다`는 가정을 두는 경우가 많습니다. -예를 들어 - -#### ISP: Interface Segregation (인터페이스 분리의 원칙) -> 클래스 내에서 사용하지 않을 인터페이스(함수)는 구현하지 않아야 합니다. -> 인터페이스를 만들 때부터 최소한의 함수만을 갖도록 합니다. - -```java -interface IO { - int write(byte[] b); - byte[] read(); -} - -class FileReader implements IO { - @Override - public int write(byte[] b) { - return 0; - } - - @Override - public byte[] read() { - ... - return b; - } -} -``` - - - -#### DIP: Dependency Inversion (의존 역전의 원칙) -> 상위 모듈은 하위 모듈에 의존하지 않고 추상화가 세부 구현(인터페이스에 없는 함수나 특정 구현 클래스)에 의존하지 않아야 합니다. - - -### 객체지향의 문제는? - -객체지향 프로그래밍을 보면 알 수 있듯이 객체에 `상태`를 두어 - -# 함수형 프로그래밍? -> 절차적 프로그래밍의 함수와는 함수의 `급`이 다르다. - -함수형 1급 객체 - -## 선언형 프로그래밍, 명령형 프로그래밍 - - - -### 함수형 언어? - - -## 함수형 프로그래밍의 특징 - -### 순수 함수 (side effect가 없는 함수) - -### 불변 데이터 - -### 일급함수 - -### 참조 투명성 - - -* 하스켈과 다른 언어에서의 http server 예시를 들어볼까? -* - -명령형 프로그래밍 - -선언형 프로그래밍 - - -## Reference - -* [http://www.incodom.kr/%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5](http://www.incodom.kr/%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5) -* [https://velog.io/@phs880623/%EA%B0%9D%EC%B9%98-%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D](https://velog.io/@phs880623/%EA%B0%9D%EC%B9%98-%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D) -* [https://coding-factory.tistory.com/328](https://coding-factory.tistory.com/328) -* [https://www.digitalocean.com/community/conceptual_articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design](https://www.digitalocean.com/community/conceptual_articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design) -* diff --git a/_sass/misc/article-menu.scss b/_sass/misc/article-menu.scss index bc168f5..75c0d1e 100644 --- a/_sass/misc/article-menu.scss +++ b/_sass/misc/article-menu.scss @@ -5,7 +5,7 @@ .post-menu { padding-left: 20px; min-width: 200px; - max-width: 230px; + max-width: 250px; .post-menu-title { font-size: $base-font-size * 1.5; @@ -20,11 +20,16 @@ $indent: $base-font-size / 4; $active-bgcolor: #ecebec; - @for $i from 2 to 7 { + @for $i from 1 to 7 { .h-h#{$i} { - padding-inline-start: $indent + ($i - 2) * $base-font-size * 1.3; - font-size: $base-font-size * 1.1; - line-height: 1.4; + padding-inline-start: $indent + ($i - 1) * $base-font-size * 1.3; + @if $i < 4 { + font-weight: bold; + font-size: $base-font-size * (1.25 - $i * 0.08); + } @else { + font-size: $base-font-size * (1.1 - $i * 0.03); + } + line-height: 1.5em; } } diff --git a/_sass/yat/_layout.scss b/_sass/yat/_layout.scss index 9791a3a..13c3c3b 100644 --- a/_sass/yat/_layout.scss +++ b/_sass/yat/_layout.scss @@ -445,7 +445,7 @@ html { display: block; } - h2, h3, h4, h5, h6 { + h1, h2, h3, h4, h5, h6 { margin: 60px 0 19px; } diff --git a/assets/2022-03-17-TWL-02-1-OOP/01-tangled-thread.png b/assets/2022-03-17-TWL-02-1-OOP/01-tangled-thread.png new file mode 100644 index 0000000..dd3591a Binary files /dev/null and b/assets/2022-03-17-TWL-02-1-OOP/01-tangled-thread.png differ diff --git a/assets/2022-03-17-TWL-02-1-OOP/02-apple-use.png b/assets/2022-03-17-TWL-02-1-OOP/02-apple-use.png new file mode 100644 index 0000000..6ddb5c7 Binary files /dev/null and b/assets/2022-03-17-TWL-02-1-OOP/02-apple-use.png differ diff --git a/assets/2022-03-17-TWL-02-2-FP/01-duggubi.gif b/assets/2022-03-17-TWL-02-2-FP/01-duggubi.gif new file mode 100644 index 0000000..698b4d6 Binary files /dev/null and b/assets/2022-03-17-TWL-02-2-FP/01-duggubi.gif differ diff --git a/assets/2022-03-17-TWL-02-2-FP/02-oop-fp.png b/assets/2022-03-17-TWL-02-2-FP/02-oop-fp.png new file mode 100644 index 0000000..94afa16 Binary files /dev/null and b/assets/2022-03-17-TWL-02-2-FP/02-oop-fp.png differ diff --git a/assets/2022-03-17-TWL-02-2-FP/03-html.jpeg b/assets/2022-03-17-TWL-02-2-FP/03-html.jpeg new file mode 100644 index 0000000..37e9c56 Binary files /dev/null and b/assets/2022-03-17-TWL-02-2-FP/03-html.jpeg differ