영감을 주는 코딩 패턴들 - RAII

Park June Chul·2021년 3월 10일
2

코딩 잘하기

목록 보기
1/2
post-thumbnail

많은 개발자들은 디자인 패턴에 대해 공부합니다.

그리고 몇몇은 개발에 대해 얼마나 공부했냐를 가늠하는 척도로 면접 질문에 자주 등장하죠.

적어도 제가 경험한 바는, 잘 알려진 디자인 패턴들은 코드를 쓸데없이 복잡하게 만듭니다. 3~4줄이면 작성할 수 있는 코드를 10줄에 걸쳐 작성하는 느낌입니다.

대부분의 디자인 패턴은 오래전, 그러니까 Java와 C++이 주력으로 쓰이던 시절에 나온 개념들입니다. 그리고 그 둘의 특징은 정말로 3~4줄짜리 코드를 10줄에 걸쳐서 작성해야 합니다.
단순히 모던 프로그래밍 언어가 축약된 함수를 가지고 있고, 많은 작업을 줄일 수 있어서가 아닙니다. 위 두 언어는 실제로 복잡한 OOP구조와 패러다임을 가지고 있고 그 위에서 돌아가는 프레임워크들은 더 복잡한 구조와 더 많은 베이스 코드를 작성하도록 유도합니다.

그뿐만 아니라 프로그래밍 언어의 패러다임또한 많은 시간에 걸쳐 진화했습니다. 이것에 대한 가장 좋은 예시는 React의 함수형 컴포넌트겠네요. 이는 단순히 업그레이드 된 DOM 조작 프레임워크가 아니라, 패러다임의 진화는 더 이상 구식 코딩 패턴을 필요없도록 만듭니다. (React에서 싱글톤을 써보셨나요?)

(정확히는 Javascript의 module은 그 자체로 싱글톤처럼 작동합니다. 여기서 제 의미는 getInstance() 같은 형태의 패턴적인 싱글톤을 써 본 경험에 대한 이야기입니다.)



디자인 패턴이 구세대의 유산이고, 구리다는 이야기는 뒤로 하고

이 글에서는 영감을 주는 패턴들에 대해 알아봅니다.

그러니까, 이것들에 대해 알고 모르고는 단순히 개발지식 +1을 좌우하는것이 아니라 앞으로의 전반적인 코드에 영향을 줄 수도 있습니다.



첫번째는 RAII(Resource Acquisition Is Initialization)입니다.

RAII에 대한 한줄짜리 설명은, (지역)변수가 선언될 때 생성자가 불리고, 스코프에서 벗어날 때 파괴자가 호출되는 것입니다.

#include <iostream>

using namespace std;

class foo {
public:
    foo() {
        cout<<"HI";
    }
    ~foo() {
        cout<<"BYE";
    }
};

int main() {
    foo f;
    return 0;
}

아주 간단하고 당연한 개념처럼 보이지만, 실제로는 그 이상의 의미를 내포합니다.

RAII에 대한 첫번째 관점은, 리소스 획득입니다.

변수에 접근이 가능한 시점에 변수가 사용 가능함을 컴파일러가 보장합니다.

예를들어 아래와 같은 코드는 굉장히 흔하게 작성됩니다.

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는 단순히 생성자와 파괴자를 호출하는 문법이 아니라, 이에 대한 명쾌한 해답을 제공하는 철학적 접근입니다.

  • 접근 가능 시점에 이미 리소스가 초기화되었음을 보장합니다.
  • 초기화가 실패하면 throw되어 스코프 밖으로 넘어감으로 자동으로 파괴자가 호출됩니다.
  • 스코프 밖으로 나갔으니 disposedoAction 이 불려질 일이 없습니다. doAction은 항상 초기화가 되어있음을 가정하고 동작할 수 있습니다.

아래 글 또한 이 주제에 대해서 다룹니다.
http://occamsrazr.net/tt/297

RAII에 대한 두번째 관점은, 파괴자의 호출입니다.

위 블로그 링크에도 살짝 언급되어 있습니다.

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기능을 일부 옮겨올수도 있고, 아래와 같은 여러가지 장난감을 만들어볼 수도 있습니다.

  • 로깅
  • 코드블럭이 실행되는 시간 재기
  • synchorized 영역 만들기


여기까지가 RAII에 대한 내용입니다.
그리고 이 글은 절대로 C++를 가르치는 글이 아닙니다. 리소스 초기화와 파괴에 대한 디자인은 언어를 막론하고 무조건적으로 필요한 개념이며, RAII를 알고 모르고(정확히는 개념에 대한 이해)는 미래에 작성할 코드에 영감을 주어 영향을 끼칠수도 있습니다.
(아무래도 저는 C#의 using 키워드가 RAII에 영감을 받아 만들어졌다고 생각합니다.)

다음 글은 (아마도) 다중상속과 mixin에 대해 다룹니다.

profile
다른 곳에서 볼 수 없는 이상한 주제를 다룹니다. https://pjc0247.github.io/new-home

0개의 댓글