그리고 몇몇은 개발에 대해 얼마나 공부했냐를 가늠하는 척도로 면접 질문에 자주 등장하죠.
적어도 제가 경험한 바는, 잘 알려진 디자인 패턴들은 코드를 쓸데없이 복잡하게 만듭니다. 3~4줄이면 작성할 수 있는 코드를 10줄에 걸쳐 작성하는 느낌입니다.
대부분의 디자인 패턴은 오래전, 그러니까 Java와 C++이 주력으로 쓰이던 시절에 나온 개념들입니다. 그리고 그 둘의 특징은 정말로 3~4줄짜리 코드를 10줄에 걸쳐서 작성해야 합니다.
단순히 모던 프로그래밍 언어가 축약된 함수를 가지고 있고, 많은 작업을 줄일 수 있어서가 아닙니다. 위 두 언어는 실제로 복잡한 OOP구조와 패러다임을 가지고 있고 그 위에서 돌아가는 프레임워크들은 더 복잡한 구조와 더 많은 베이스 코드를 작성하도록 유도합니다.
그뿐만 아니라 프로그래밍 언어의 패러다임또한 많은 시간에 걸쳐 진화했습니다. 이것에 대한 가장 좋은 예시는 React의 함수형 컴포넌트겠네요. 이는 단순히 업그레이드 된 DOM 조작 프레임워크가 아니라, 패러다임의 진화는 더 이상 구식 코딩 패턴을 필요없도록 만듭니다. (React에서 싱글톤을 써보셨나요?)
(정확히는 Javascript의 module은 그 자체로 싱글톤처럼 작동합니다. 여기서 제 의미는
getInstance()
같은 형태의 패턴적인 싱글톤을 써 본 경험에 대한 이야기입니다.)
이 글에서는 영감을 주는 패턴들에 대해 알아봅니다.
그러니까, 이것들에 대해 알고 모르고는 단순히 개발지식 +1을 좌우하는것이 아니라 앞으로의 전반적인 코드에 영향을 줄 수도 있습니다.
RAII에 대한 한줄짜리 설명은, (지역)변수가 선언될 때 생성자가 불리고, 스코프에서 벗어날 때 파괴자가 호출되는 것입니다.
#include <iostream>
using namespace std;
class foo {
public:
foo() {
cout<<"HI";
}
~foo() {
cout<<"BYE";
}
};
int main() {
foo f;
return 0;
}
아주 간단하고 당연한 개념처럼 보이지만, 실제로는 그 이상의 의미를 내포합니다.
변수에 접근이 가능한 시점에 변수가 사용 가능함을 컴파일러가 보장합니다.
예를들어 아래와 같은 코드는 굉장히 흔하게 작성됩니다.
class foo {
public:
init () {
/* 초기화 */
}
dispose() {
/* 정리 */
}
doAction() {
/* 뭔가 함 */
}
};
근데 위 코드는 문제가 많습니다.
제대로 짜려면 아래처럼 짜야합니다.
class foo {
public:
~foo() {
dispose();
}
init () {
if (이전에 초기화 했나?) return;
/* 초기화 */
}
dispose() {
if (아직 초기화조차 안되었나?) return;
if (이미 dispose 되었나?) return;
/* 정리 */
}
doAction1() {
if (이전에 초기화 안했나?) return;
/* 뭔가 함 */
}
doAction2() {
if (이전에 초기화 안했나?) return;
/* 뭔가 함 */
}
doAction3() {
if (이전에 초기화 안했나?) return;
/* 뭔가 함 */
}
};
복잡한 이 코드조차도 굉장히 흔합니다.
근데 여기서 끝이 아닙니다.
init
가 실패한 경우엔 dispose
를 해야할까요? 아니면 init
이 안됬으니까 굳이 dispose
를 호출할 필요도 없을까요?
RAII는 단순히 생성자와 파괴자를 호출하는 문법이 아니라, 이에 대한 명쾌한 해답을 제공하는 철학적 접근입니다.
dispose
나 doAction
이 불려질 일이 없습니다. doAction
은 항상 초기화가 되어있음을 가정하고 동작할 수 있습니다.아래 글 또한 이 주제에 대해서 다룹니다.
http://occamsrazr.net/tt/297
위 블로그 링크에도 살짝 언급되어 있습니다.
RAII는 초기화뿐만 아니라 파괴에 대해서도 깔끔한 솔루션을 제공합니다.
그리고 이 부분은 저같이 이상한 코드를 작성하는것을 즐기는 사람들에게 좋은 도구로 쓰이기도 합니다.
일단 아래 코드를 보세요:
#include <iostream>
using namespace std;
class logger {
public:
logger(const std::string &label) {
this->label = label;
cout<<"ENTER "<< this->label <<std::endl;
}
~logger() {
cout<<"LEAVE "<< this->label <<std::endl;
}
private:
std::string label;
};
int main() {
{
logger("1st codeblock");
/* do stuff */
}
{
logger("2nd codeblock");
/* do stuff */
}
return 0;
}
logger는 코드블럭에 들어가는 순간에 한번, 종료되는 순간에 한번 로그를 출력합니다.
{
cout<< '들어옴';
// do stuff
cout<< '나감';
}
원래라면 이렇게 짜야 하는데, 코드 딱 한줄로 두가지 시점을 다 제어할 수 있게 되는거죠.
이 기능의 가능성은 무궁무진합니다.
다른 언어 또는 프레임워크의 AOP기능을 일부 옮겨올수도 있고, 아래와 같은 여러가지 장난감을 만들어볼 수도 있습니다.
여기까지가 RAII에 대한 내용입니다.
그리고 이 글은 절대로 C++를 가르치는 글이 아닙니다. 리소스 초기화와 파괴에 대한 디자인은 언어를 막론하고 무조건적으로 필요한 개념이며, RAII를 알고 모르고(정확히는 개념에 대한 이해)는 미래에 작성할 코드에 영감을 주어 영향을 끼칠수도 있습니다.
(아무래도 저는 C#의 using 키워드가 RAII에 영감을 받아 만들어졌다고 생각합니다.)
다음 글은 (아마도) 다중상속과 mixin에 대해 다룹니다.