Programming paradigm과 Flutter UI

Programming paradigm과 Flutter UI

Flutter를 공부하다보니 Flutter는 Declaravitve Programming 방식을 따르며 어떤 Function에 application state를 input으로 넣으면 UI가 생성되도록 한다고 한다.

이제까지는 아무 의구심 없이 Flutter UI를 이런 방식으로 만들어 왔지만 Declarative Programming은 뭔지 또 그와 반대되는 Imperative Programming은 뭔지 다른 Programming Paradigm은 어떤 것들이 있는지 파보려고 한다.

Introduce 🛫

어떤 프로그램을 만들 때 그 규모가 커짐에 따라 Complexity가 높아진다. 그리고 이 커진 Complexity는 프로그래머의 가장 큰 적이다. Complexity가 높아지면 높아질수록 우리는 어떤 문제가 생겼을 때 추적하기 어려워지며 관리가 힘들어진다. Complexity가 작을수록 사람이 이해하기 쉬워지고, 문제가 생겼을 때 디버깅을 하기 쉬워진다.

프로그래머는 이 Complexity를 관리하는 데에 많은 노력을 쏟는다. 그렇다면 Complexity를 어떻게 관리해야 할까? Programming Paradigm은 프로그래머가 Complexity를 관리하기 위한 여러 방식들을 제시한다.

각 paradigm이 제시하는 방식에는 장단점이 있고, 우리는 우리가 만드는 프로그램의 성격에 따라 맞는 paradigm을 사용하면 된다.

Paradigm? 🤔

Paradigm은 다른 말로 하면 프로그래밍 스타일이라고 할 수 있다. 이 스타일은 우리가 프로그램을 만드는 방식에 관한 것이지, 프로그래밍 언어에 국한되지는 않는다. 현재 수많은 프로그래밍 언어들이 나오고 있고 각 언어의 문법이 있다. 하지만 이 문법과는 별개로 프로그래밍할 때 어떤 전략으로 프로그램을 구현할 것 인가를 생각해야 하는데 이 전략이 바로 paradigm 이다.

[Programming Paradigm의 종류]


📗 Imperative programming paradigm

한국말로 번역하면 명령형 프로그래밍 방식이다. Imperative라는 단어는 I command 라는 의미를 가지는 라틴어 impero 라는 단어에서 유래되었다. 이 패러다임은 이 단어의 의미와 정확히 일치한다.

우리는 컴퓨터가 수행할 작업들을 순서대로 일일이 명령하며, 컴퓨터는 이것을 수행한다.

  1. 수행할 task를 순서대로 작성함

  2. 어떤 상태들을 가지고 있음

  3. task가 하나하나 순서대로 실행됨

  4. 실행 될 때마다의 결과를 state에 저장함

Imperative programming에서는 task들의 순서가 매우 중요하다. 이전 task의 결과가 다음 task의 input으로 들어가게 되는데 이 순서가 바뀌게 되면 원하는 결과 값을 얻을 수 없기 때문이다. 예시를 들어보자.

1
2
3
4
5
6
7
8
9
10
11
void main() {
int num1 = 1;
num1 *= 4;
num1 = (num1 / 2).floor();
print(num1); // 2

int num2 = 1;
num2 = (num2 / 2).floor();
num2 *= 4;
print(num2); // 0
}

위 예시는 1에 4를 곱하고 2를 나눈 것의 몫을 결과 값으로 가지는 것과 1을 2로 나눈 것의 몫과 4를 곱한 것의 결과값을 print 한 것이다. 위의 예시와 같이 순서대로 수행하지 않으면 input이 달라지기 때문에 result가 달라진다.


Procedural programming parardigm

Imperative parardigm 의 한 종류인 procedural programming을 살펴보자. 한국말로 해석하면 절차지향 프로그래밍이라고 할 수 있겠다.

Procedural programming은 어떤 명령을 어떤 절차들로 쪼갠 것이다. 하지만 이 의미가 절차 == 함수는 아니다. 함수는 결과 값을 리턴하지만 절차는 어떠한 값도 리턴하지 않는다.

좀 더 자세히 얘기해보자. 함수들은 해당 스코프 내에서 최대한 작은 side effect(프로그래밍에서 side effect란 어떤 변화가 일어나는 행위이다.)를 발생시키며 항상 같은 input이 들어오면 같은 output을 내도록 설계되어있다. 절차는 어떠한 값도 리턴하지 않으며 단지 원하는 만큼의 side effect를 발생시키는 데에 목적을 둔다.

예시를 보자.

1
2
3
4
5
6
7
8
void main() {
int sum = 0;
for(int i = 1; i < 11; i++) {
sum += i;
}

print(sum);
}

어떤 상태가 있고(sum), 최소 또는 최대가 아닌 원하는 만큼의 side effect를 발생시키고 있고(for loop 10 times), 아무런 값도 return하지 않는다.

🙋Procedural programming은 언제 사용하는 게 좋을까?

  • 어떤 task들 간의 디펜던시가 있어서 꼭 순서대로 진행해야 할 때

  • 중복되는 코드가 거의 없을 때

  • 코드가 미래에 바뀔 일이 거의 없을 때

  • 해당 프로젝트에 추가될 기능들이 미래에 거의 없다고 생각될 때

🙋Procedural programming를 왜 써야 할까?

  • 쉽다

  • 메모리를 적게 먹어서 효율적이다


Object oriented programming paradigm

OOP는 코드를 모듈화할 수 있고, 실제 세계를 코드에 반영하기 쉽기 때문에 널리 쓰이고있는 programming paradigm이다.

OOP는 Class, Abstraction, Encapsulation, Inheritance and Polymorphism 다섯 개의 특징을 가지고 있다.

  • Class는 설계도이다. Object는 class의 인스턴스이며 Object는 상태와 행동을 가진다.

  • Abstraction은 실제 구현과 인터페이스를 분리한다. Encapsulation은 어떤 Object의 내부 구현을 숨기는 과정이다.

  • Inheritance는 Object간의 계층적인 관계를 가지게 한다. Polymorphism는 다른 타입의 Object들에게 같은 메세지를 받을 수 있게 하며 다른 결과물이 나오도록 한다.

⚠️하지만 의문이 하나 든다. 왜 OOP가 Procedural programming과 같은 범주인 imperative programming에 들어가는가??

imperative programming은 명령형 프로그래밍이며, 어떤 task를 순서대로 상세히 명령해야 하며 그 task들의 순서가 중요하다. 그러한 관점에서 OOP는 어떤 클래스들의 동작(메소드)들이 순서대로 동작해며 상호작용해야 하므로 imperative programming 범주에 들어간다.

🙋OOP는 언제 사용하는 게 좋을까?

  • 프로그램을 만드는 여러명의 프로그래머가 있으며, 각자의 파트를 몰라도 될 때
  • 프로그램 내에 많은 코드들이 재사용되어야할때
  • 프로젝트의 내용이나 기능이 많이 추가되고 바뀔 것이 예상될때

🙋OOP를 왜 써야 할까?

  • 상속을 통한 코드의 재사용
  • 다형성을 통한 유연함
  • 개발 생산성을 위해 (빠른 속도, 적은 비용)
  • 높은 퀄리티의 소프트웨어를 위해

Parallel processing approach

여러 작업을 processor에 나눠서 할당해줘야 할 때 병렬 작업을 한다. 각각을 쪼개서 동시에 처리하기 때문에 적은 시간 안에 테스크를 처리할 수 있다.

이 paradigm도 병렬처리시 테스트 순서가 중요하기 때문에, 그리고 어떻게 처리되어야 하는지 상세히 명령해야하기 때문에 imperative programming에 속한다.


📗 Declarative programming paradigm

Imperative programming과 반대되는 declarative programming은 한국어로 풀어 얘기하면 선언형 프로그래밍이다.

이또한 단어의 의미와 같이, 프로그래머가 프로그램을 구현할 때 단지 어떤 기능이 실행되었으면 좋겠다고 선언하는 것이며 그 상세한 구현에 대해서는 선언하지 않는다.

예를들어보자. Declarative programming은 마치 대통령이 각 장관에게 ~~일을 해주세요 하는 것과 같이 좀 더 큰 범주 내에서 명령을 할 뿐이고 실제로 각 파트의 장관들, 그 하위 계층에서는 어떤 일을 하는지 알지 못한다.

반대로 Imperative programming은 마치 맥도날드 지점장이 각 알바들에게 손님들에게 친절하게 해라 몇시부터 나와라 하는 등의 세세한 지시를 하는 것과 같다.

결론적으로 Imperative programming은 어떤 일을 어떻게 해야하는지를 나열하는 방식이라면 Declarative programming은 어떤 일을 해야하는지를 나열할 뿐 어떻게 해야하는지는 말해주지 않는다. 단지 선언한 대로 동작하기를 기대할 뿐이다.

Logic programming paradigm

이 패러다임은 이름 그대로 로지컬하게 로직을 풀어나간다. 예시를 보는게 가장 빠를듯 하다.

소크라테스는 사람이고, 모든 사람은 죽으며 따라서 소크라테스는 죽는다는 삼단 논법이 있다고 하자. 이걸 logic programming 으로 나타내면

1
2
man(Socrates) // 소크라테스는 사람이다
mortal(X) :- man(X) // :-기호는 if와 같다. 즉 X가 사람이면 X는 죽는다

위의 코드와 같이 선언해놓으면 어떤 X가 입력으로 들어오면 man인지 확인하고 man이면 mortal이라고 결과값을 뱉는다.

1
?- mortal(Socrates)

위의 코드 한줄은 Socrates는 죽는가? 라는 의미를 가지며 위에 선언한 mortal(X) :- man(X) 식에 따라 X에는 Socrates가 들어가게 되고 Socrates는 죽는다는 결론이 나온다.

🙋Logic programming paradigm는 언제 사용하는게 좋을까?

  • 철학적인 문제를 해결하거나 어떤 시스템을 구축할때

🙋Logic programming paradigm를 왜 써야할까?

  • 코드를 구현하기 쉽다
  • 디버깅이 쉽다
  • true, false로 모든 구조가 이루어져있으면 아주 빠르게 구현 가능하다

Functional programming paradigm

함수형 프로그래밍은 요새 많이 각광받고있고 여기저기서 사용되고있다. 이 패러다임은 수학에 기초하고있으며 프로그래밍 언어와는 독립적인 개념이다. 함수형 프로그래밍의 원리는 수학적인 함수를 연속적으로 실행시키는 것이다.

모든 변수는 함수 내에 존재하며 짧은 함수들이 연속적으로 실행되면서 이전 output이 다음 input으로 들어가며 계산이 진행된다.

함수형 프로그래밍에서는 함수 내에 스코프가 아닌 외부 스코프에 있는 변수를 변화시키지 않는다. 오직 자신의 스코프에 있는 변수만 변화시키며 결과값을 리턴할 뿐이다.

예를들어보자. 어떤 수가 소수인지 아닌지 판단하는 코드가 존재한다.

1
2
3
4
5
6
7
8
9
10
11
function isPrime(number) {
for(let i = 2; i <= Math.floor(Math.sqrt(number)); i++) {
if (number % i == 0) {
return false;
}
}

return true;
}

isPrime(15) // return false

위의 코드에서는 외부 스코프의 변수를 변화시키지도 않는다. 인풋으로 들어온 number를 단지 sqrt와 floor를 통해 변화시키고 i와 비교할 뿐이다. floor, sqrt 는 단지 버림, 루트와 같은 동작을 한다고 메소드 이름이 지어졌을 뿐 내부 구현이 어떻게 되어있는진 모른다. 단지 선언할 뿐이다.

🙋Functional programming paradigm는 언제 사용하는게 좋을까?

  • 수학적인 계산을 할때
  • 병행 또는 병렬 처리를 작업을 해야하는 프로그램일때

🙋Functional programming paradigm를 왜 써야할까?

  • 쉽고 빠르게 구현할 수 있다. (세부 구현은 신경 안쓰고 이어붙이기만 하면 되니까)
  • 일반적인 목적의 함수들은 계속 재사용되기 때문에 개발 속도가 빠르다.
  • 테스트, 디버깅이 쉽다.

Flutter는 declarative programming 방식을 따른다. 물론 내부적으로 서버와 통신을 한다거나 앱의 목적에 따라서 procedural 방식을 따를수도 있고 OOP를 따르며 클래스들이 상호작용하고 상태를 바꾸게 할 수 있다.

하지만 UI를 만드는 방식은 확실히 declarative하다고 말할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'package:flutter/material.dart';

class TestWidget extends StatefulWidget {
@override
_TestWidgetState createState() => _TestWidgetState();
}

class _TestWidgetState extends State<TestWidget> {

final String name = 'wonjerry';

@override
Widget build(BuildContext context) {
return Container(
child: Text(name),
);
}
}

build 메소드를 보자. 우리는 Container가 어떻게 구현되어있는진 모르겠지만 Container안에 Text라는 자식 위젯을 넣어줬다. Text또한 어떻게 구현되어있는진 모르겠지만 ‘wonjerry’라는 String을 넣어주어서 화면에 text를 띄우는 UI를 구현하였다.

전형적으로 구현에는 신경쓰지 않고 여러 선언을 통해 원하는 결과값을 도출해내는 Declarative programming 방식이다.

하지만 UI가 아닌 다른 부분에서는 helper class, model class, api 통신을 위한 class들에서는 OOP를 따르며 어떤 일련의 순서가 중요하며 어떻게 구현되어야하는지 일일이 써줘야 하는 부분도 있다.

생각해보면 우리가 보통 프로그래밍을 할 때는 프레임워크에서 뭔가를 제공해주지 않는 한 우리는 Imperative 하게 프로그래밍을 할 수 밖에 없다. 예를들면 api 통신을 할때 header를 뭘 넣고 어떤 method를 통해 어떤주소로 어떻게 request를 할것인지 일일히 작성한다.

하지만 우리는 구현해 나가면서 우리의 프로그램을 관리하기 쉽게, 디버깅하기 쉽고, 구조를 단순하게 구현하기 위해서 중복된 코드를 메소드로, class로 빼고 모듈화 시키면서 wrapping 하게 되며 이러한 과정을 통해 나온 메소드들을 마치 레고 블록을 쌓듯 imperative하게 조합시키면서 프로그램을 완성시키는듯 하다.


Conclusion 🛬

Programming paradigm들은 프로그램의 Complexity를 줄여준다. 모든 프로그래머는 어떤 패러다임을 따르는것이 좋겠다는 생각이 든다.

하지만 조금 더 생각을 해보면 요새 나온 프로그래밍 언어들은 특정한 패러다임을 따르면서 탄생되고, 코그를 짜면서 그 패러다임을 따르도록 유도한다.

어떤 특정한 하나의 패러다임을 따르는것은 멍청하다고 본다. 오히려 우리가 쓰는 프레임워크, 툴에서 어떤 패러다임을 따르며 만들어졌는지 파악하고 우리가 짜는 코드가 어떤 패러다임을 따르는지 느끼며 각 상황별 적절한 패러다임을 따르면 된다는 생각이 든다.

궁극적인 목적은 사람이 이해하기 쉽고, 단순하고, 모듈화 시키는 것이다. 그리고 이걸 이루면 디버깅 및 관리가 쉬운 코드를 짤 수 있다. 그리고 이러한 코드를 짜는 프로그래머가 좋은 프로그래머라는 생각이 든다.

댓글

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×