assert 활용하기

|

assert를 잘 쓰면 개발에 꽤 유용할 때가 많습니다.

assert는 디버그 모드(Debug Mode)에서만 작동하고 릴리즈 모드(Release Mode)에서는 컴파일 단계에서 무효화되어 걸러지기 때문에 디버깅 용도로 쓰기에 참 좋습니다. (보통 디버깅 환경에서는 로그 메세지를 남발하며 뿌리다가 릴리즈 모드에서는 그 로그 메세지들을 일일이 막는 것도 번거로운 작업들입니다. 그래서 Log 함수들을 별도의 클래스로 Wrapper하는 경우도 많이 있습니다.)

assert는 괄호안에 들어가는 조건식이 false인 경우 오류 메세지를 출력하면서 프로그램을 강제 종료시킵니다. 프로그램을 강제 종료함으로 인해, 나중에 발생할지도 모르는 논리적인 오류들을 사전에 막아주는 역할을 해서 디버깅 환경에 큰 도움이 됩니다.

assert 활용 예제는 다음과 같습니다.


assert 활용 예제

static const char* weeks[] = {
    "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday",
    "Friday", "Saturday",
};
void displayWeek(int day) {
  assert((day >= 0) && (day <=6));
  cout<<weeks[day];
}

위 코드에서 displayWeek 함수의 인자로 0보다 작거나 6보다 큰 값을 입력하게 되면 다음과 같은 오류 메세지를 출력합니다.

assertion "(day >= 0) && (day <=6)" failed: file "/cygdrive/c/Workspace/Clion/SnowDeer/main.cpp", line 11, function: void displayWeek(int)


주의할 점

assert의 매개변수에는 조건식만 들어가야 합니다. 만약 조건식이 아닌 실행 가능한 구문이 들어가게 되면 큰 오류를 범할 수 있습니다. assert는 디버그 모드에서만 활성화되며, 릴리즈 모드에서는 활성화되지 않기 때문에 다음과 같은 코드는 문제가 발생할 수 있습니다.

void openFile() {
  FILE* fp;
  assert((fp = fopen(file_name, "r")) != nullptr));
}

`assert’는 잘 쓰면 좋은 효과를 가져올 수 있지만, 입력해야 하는 매개변수 또한 주의하지 않으면 또다른 문제를 발생하기도 합니다. 따라서 매개변수에 들어가는 조건문을 최대한 간결하게해서 필요한 부분에만 적절히 사용한다면 개발에 큰 도움이 될 수 있을 것 같습니다.

Fragment의 라이프사이클

|

Fragment의 라이프사이클

Fragment의 라이프사이클은 Activity의 라이프사이클과 비슷하지만 약간 더 복잡합니다. 다음 이미지와 같은 모습의 라이프사이클을 갖고 있습니다.

image -fullwidth

onAttach()onDetach() 이벤트가 Fragment가 실제로 Activity와 연결되거나 끊어지는 부분입니다.

단계 설명
onAttach Activity와 연결될 때 호출. 이 시점에서 getActivity()null 리턴함
onCreate Fragment 생성시 호출
onCreateView View 생성
onActivityCreated 뷰 생성(setContentView 호출 등)
onStart Fragment 시작될 때 호출(화면에 보이기 직전)
onResume 화면에 표시될 때 호출
onPause 화면에서 가려질 때 호출
onStop Fragment가 종료될 때 호출
onDestoryView View가 해제될 때 호출
onDestory Fragment가 해제될 때 호출
onDetach Activity와 연결이 해제될 때 호출. 가장 마지막 단계

Binary Indexed Tree

|

Binary Indexed Tree는 이진수 값을 인덱스로 활용해서 구간별 최소/최대/합 등을 쉽게 구할 수 있게 해주는 트리 형태의 데이터 구조입니다.

Segment Tree vs Binary Indexed Tree

Segment Tree로부터 좀 더 발전된 트리이며, 개념도 비슷합니다. Segment Tree 보다 더 적은 데이터 공간을 필요로 하며 코드 구현이 더 쉽습니다.

다만 처음 이해하는 과정이 조금 복잡하여 Segment Tree의 개념부터 이해를 하고 도전을 하는 것이 좋을 것 같습니다.

BIT

위의 그림은 어디서 많이 본 트리입니다. 바로 Segment Tree입니다. 여기서 잘 생각해보면 부모 노드는 어차피 자식 노드들의 정보로 이루어져 있기 때문에 중복된 데이터들이 존재하는 것을 알 수 있습니다.

예를 들어, 아래 예시와 같은 데이터가 있다면

왼쪽 자식 노드의 값 100, 오른쪽 자식 노드의 값 200
- 부모 노드는 자식 노드들의 값의 합이라고 하면 100 + 200 = 300
- 오른쪽 자식 노드가 없더라도 부모 노드의 값과 왼쪽 자식 노드의 값만 알면, 오른쪽 자식 노드의 값을 유추할 수 있음


즉, Segment Tree에서

BIT 회색 부분의 공간은 불필요한 공간이 되며,

BIT 와 같은 형태로 데이터 구조를 가져갈 수 있습니다.


Binary Indexed Tree 구조

실제로 값을 넣어보도록 하겠습니다.

BIT 실제 데이터는 대략 이런 식으로 저장이 된다고 가정 하면, Binary Indexed Tree는 다음과 같은 형태로 구성이 됩니다. (해당 구간의 데이터의 합을 구하는 Binary Indexed Tree라고 가정했습니다.)


BIT 다시 1차원 배열로 표현해보면 Binary Tree는

BIT 이 됩니다.


Binary Indexed Tree를 이용한 구간 합 구하기

Binary Indexed Tree를 이용하면 구간 합을 아주 빠르게 구할 수 있습니다. 예를 들어, 1 번째 노드부터 7 번째 노드까지의 구간 합은

BIT 와 같이 3개의 노드만 탐색하면 구할 수 있습니다.

Binary Indexed Tree의 노드 인덱스

Binary Indexed Tree의 각 노드별 인덱스를 구할 수 있으면, Binary Indexed Tree를 사용할 수 있는 준비는 거의 끝났다고 볼 수 있습니다.

각 노드마다 인덱스를 붙여보도록 하겠습니다.

BIT 이름 그대로 이진(Binary)법을 각 인덱스에 적용하면 다음과 같은 값이 되는데, 노드간 값을 잘 보면 규칙성이 있습니다.

BIT 예를 들어서, 노드 3번의 인덱스는 ‘0011’ 입니다. 노드 3번의 부모는 4번 노드로 인덱스는 ‘0100’ 입니다. 4번 노드의 부모는 8번 노드로 인덱스는 ‘1000’ 입니다.

예를 하나만 더 들어보도록 하겠습니다. 노드 5번의 인덱스는 ‘0101’ 입니다. 노드 5의 부모는 6번 노드이고, 인덱스는 ‘0110’ 입니다. 노드 6의 부모는 노드 8이며 인덱스는 ‘1000’ 입니다.

부모 노드와 자식 노드간의 규칙은 다음과 같습니다.

부모 노드의 인덱스는 자식 노드의 인덱스의 가장 마지막 '1' 값에 1을 더한 값


현재 이진 인덱스 값에서 가장 마지막에 위치한 ‘1’의 위치는 ‘index & (-index)’의 bit 연산을 통해서 얻을 수 있습니다. 즉, 현재 인덱스 값에 위의 ‘index & (-index)’ 값 을 더하면 부모 노드의 인덱스 값이 됩니다.

Binary Indexed Tree 값 업데이트 및 구간 합 구하기

Binary Indexed Tree에 값을 업데이트하거나 구간 합을 구하는 방법은 Segment Tree에서의 방법과 비슷합니다. Segment Tree와 마찬가지로 자식 노드의 값이 변경이 되면 부모 노드의 값들을 전부 업데이트해야 합니다.

그리고 자식과 부모 노드간의 관계는 앞에서 알아본 자식 노드 인덱스 가장 마지막 ‘1’ 값에 1을 더하는 방법으로 구할 수 있습니다.

값을 업데이트 할 때는 자식 노드부터 시작해서 가장 마지막 1 bit에 1을 더하면서 부모 노드를 찾아가서 값을 계속 갱신해주면 됩니다.

거꾸로 1부터 N 노드까지의 합을 구할 때는 N 노드부터 시작해서 가장 마지막 1 bit에 1을 빼면서 0이 될때까지 각 노드들의 합을 구하면 됩니다.

직접 코드로 확인해보도록 하겠습니다.


코드 (정의)

static const int MAX_TREE_SIZE = 100000;
static const int INFINITE = 9999999;
int data[] = { 0, 2, 4, 1, 7, 3, 6, 2, 5, };
int N = 8;
int bit[MAX_TREE_SIZE];

void initialize() {
    int size = 2 * N - 1;
    for (int i = 1; i <= size; i++) {
        bit[i] = 0;
    }
}

void debug() {
    for (int i = 1; i <= N; i++) {
        printf("%d ", bit[i]);
    }

    printf("\n");
}


코드 (데이터 갱신)

void update(int index, int value) {
    while (index <= N) {
        bit[index] = bit[index] + value;
        index = index + (index & (-index));
    }
}


코드 (1부터 N까지 구간 합)

int sum(int index) {
    int sum = 0;
    while (index > 0) {
        sum = sum + bit[index];
        index = index - (index & (-index));
    }

    return sum;
}

코드 (전체 소스 코드)

#include <stdio.h>

static const int MAX_TREE_SIZE = 100000;
static const int INFINITE = 9999999;
int data[] = { 0, 2, 4, 1, 7, 3, 6, 2, 5, };
int N = 8;
int bit[MAX_TREE_SIZE];

void initialize() {
    int size = 2 * N - 1;
    for (int i = 1; i <= size; i++) {
        bit[i] = 0;
    }
}

void debug() {
    for (int i = 1; i <= N; i++) {
        printf("%d ", bit[i]);
    }

    printf("\n");
}

void update(int index, int value) {
    while (index <= N) {
        bit[index] = bit[index] + value;
        index = index + (index & (-index));
    }
}

int sum(int index) {
    int sum = 0;
    while (index > 0) {
        sum = sum + bit[index];
        index = index - (index & (-index));
    }

    return sum;
}

int main(int argc, char** argv) {
    initialize();

    for (int i = 1; i <= N; i++) {
        update(i, data[i]);
    }

    // Binary Indexed Tree 출력
    debug();

    printf("Sum (1 ~ %d) : %d\n", 3, sum(3));
    printf("Sum (1 ~ %d) : %d\n", 5, sum(5));
    printf("Sum (1 ~ %d) : %d\n", 7, sum(7));

    return 0;
}

Segment Tree

|

세그먼트 트리(Segment Tree)는 구간별 최대값, 최소값 등을 빠르게 구할 수 있게 해주는 트리 형태의 데이터 구조입니다.


Segment Tree 구조

Segment Tree는 완전 이진 트리이며, 부모 노드는 각 자식 노드들의 범위에 대한 정보를 갖고 있습니다. 각 노드간의 관계는 아래와 같습니다.

SegmentTree

실제 데이터는 맨 아래의 Leaf 노드들이 가지게 되며, 부모 노드들은 각각의 자식 노드들의 구간내의 정보를 갖게 됩니다. 예를 들어, 구간 내 최소값을 관리하는 Segment Tree를 알아보도록 하겠습니다.


구간별 최소값을 관리하는 Segment Tree

SegmentTree

실제 데이터는 대략 이런 식으로 저장이 됩니다. (최하위 노드에만 값이 저장됩니다.) 이 때, 부모 노드들에 들어갈 값들을 살펴보도록 하겠습니다.


SegmentTree

이런 식으로 각 부모 노드들은 해당 구간에 속하는 자식 노드들의 최소값을 저장하게 되며,


SegmentTree

차곡차곡 각 부모 노드들의 데이터가 채워지게 됩니다.


구간별 최소값 조회

이제, Segment Tree를 이용한 구간별 최소값 조회를 한 번 해보도록 하겠습니다. 예를 들어, 1번째 노드부터 7번째 노드까지의 최소값을 구해보겠습니다.

SegmentTree 원래는 1번째 노드부터 7번째 노드까지 차례대로 값을 확인해야 했지만, Segment Tree에서는 위 그림과 같이 3개의 노드만 검색을 하면 최소값을 얻을 수 있습니다. 데이터 사이즈가 커지면 더욱 속도 차이가 많이 나게 될 것입니다.


데이터 갱신

데이터 갱신을 할 경우 Segment Tree는 해당 범위를 포함하고 있는 모든 부모 노드들의 값을 다시 업데이트해줘야 합니다.

SegmentTree

만약 4번째 노드의 값이 변경될 경우, 이런 식으로 4번째 노드를 포함하고 있는 모든 부모 노드들의 값을 업데이트 해줘야 합니다.


SegmentTree


코드

Segment Tree 정의

Segment Tree의 구조를 보면 실제 데이터보다 약 2배만큼의 저장 공간을 더 사용하고 있습니다. (실제 데이터 공간과 그 범위를 관리하는 부모 노드들의 공간을 합치면 약 2배가 됩니다.)

따라서 아래 코드와 같이 실제 데이터보다 2배 정도의 크기를 선언해서 사용하면 되고, 초기값은 무한대로 설정했습니다.


static const int MAX_TREE_SIZE = 100000;
static const int INFINITE = 9999999;
int data[] = { 0, 6, 3, 7, 5, 2, 4, 8, 1, };
int N = 8;
int segment_tree[2 * MAX_TREE_SIZE];

int getMin(int a, int b) {
    if (a <= b) return a;
    return b;
}

void initialize() {
    int size = 2 * N - 1;
    for (int i = 1; i <= size; i++) {
        segment_tree[i] = INFINITE;
    }
}

void debug() {
    int size = 2 * N - 1;
    int pow = 2;
    for (int i = 1; i <= size; i++) {
        printf("%d ", segment_tree[i]);
        if (i == (pow - 1)) {
            printf("\n");
            pow = pow * 2;
        }
    }

    printf("\n");
}


데이터 갱신

각 노드들에 인덱스를 매겨보면,

SegmentTree

와 같이 인덱스를 붙일 수 있습니다. 부모 노드와 자식 노드간의 관계는 아래 그림과 같습니다.

SegmentTree

즉, 데이터 갱신은 다음과 같은 코드를 통해서 할 수 있습니다.

void update(int pos, int value) {
    pos = pos + (N -1);
    segment_tree[pos] = value;

    while (true) {
        pos = pos / 2;
        if (pos == 0) break;
        segment_tree[pos] =
            getMin(segment_tree[2 * pos], segment_tree[2 * pos + 1]);
    }
}


데이터 조회

데이터 조회 부분이 좀 복잡한데, 다음과 같은 재귀 함수를 이용해서 조회를 할 수 있습니다.

// left, right : 구하고자 하는 값의 범위
// segment_left, segment_right : 현재 Segment의 구간
// node_id : 현재 node의 index
int query(int left, int right, int segment_left,
    int segment_right, int node_id) {
    // 전혀 관련이 없으면 INFINITE 리턴
    if ((segment_right < left) || (segment_left > right))
        return INFINITE;

    // 만약 현재 구간이 구하고자 하는 값의 범위 안에 있으면 SegmentTree값 리턴
    if ((segment_left >= left) && (segment_right <= right))
        return segment_tree[node_id];

    // 현재 Segment의 중간점
    int mid = (segment_left + segment_right) / 2;

    int left_value = query(left, right,
        segment_left, mid, 2 * node_id);
    int right_value = query(left, right,
        mid + 1, segment_right, 2 * node_id + 1);

    return getMin(left_value, right_value);
}


전체 소스 코드

#include <stdio.h>

static const int MAX_TREE_SIZE = 100000;
static const int INFINITE = 9999999;
int data[] = { 0, 6, 3, 7, 5, 2, 4, 8, 1, };
int N = 8;
int segment_tree[2 * MAX_TREE_SIZE];

int getMin(int a, int b) {
    if (a <= b) return a;
    return b;
}

void initialize() {
    int size = 2 * N - 1;
    for (int i = 1; i <= size; i++) {
        segment_tree[i] = INFINITE;
    }
}

void debug() {
    int size = 2 * N - 1;
    int pow = 2;
    for (int i = 1; i <= size; i++) {
        printf("%d ", segment_tree[i]);
        if (i == (pow - 1)) {
            printf("\n");
            pow = pow * 2;
        }
    }

    printf("\n");
}

void update(int pos, int value) {
    pos = pos + (N -1);
    segment_tree[pos] = value;

    while (true) {
        pos = pos / 2;
        if (pos == 0) break;
        segment_tree[pos] =
            getMin(segment_tree[2 * pos], segment_tree[2 * pos + 1]);
    }
}

// left, right : 구하고자 하는 값의 범위
// segment_left, segment_right : 현재 Segment의 구간
// node_id : 현재 node의 index
int query(int left, int right, int segment_left,
    int segment_right, int node_id) {
    // 전혀 관련이 없으면 INFINITE 리턴
    if ((segment_right < left) || (segment_left > right))
        return INFINITE;

    // 만약 현재 구간이 구하고자 하는 값의 범위 안에 있으면 SegmentTree값 리턴
    if ((segment_left >= left) && (segment_right <= right))
        return segment_tree[node_id];

    // 현재 Segment의 중간점
    int mid = (segment_left + segment_right) / 2;

    int left_value = query(left, right,
        segment_left, mid, 2 * node_id);
    int right_value = query(left, right,
        mid + 1, segment_right, 2 * node_id + 1);

    return getMin(left_value, right_value);
}

int main(int argc, char** argv) {
    initialize();

    for (int i = 1; i <= N; i++) {
        update(i, data[i]);
    }

    // Segment Tree 출력
    debug();

    // 최소값을 구하고자 하는 구간 입력
    int left = 2;
    int right = 8;
    int value = query(left, right, 1, N, 1);
    printf("Minimum between %d and %d : %d\n",
        left, right, value);

    return 0;
}

정적 웹페이지 vs 동적 웹페이지

|

정적인 웹페이지를 생성시켜주는 툴인 Jekyll를 소개하고자 합니다. 그러기에 앞서 먼저 정적 웹페이지에 대해서 먼저 소개하도록 하겠습니다.


정적 웹페이지 vs 동적 웹페이지

이름에서 눈치를 챌 수 있듯이 사용자의 인터랙션에 따라 웹페이지가 바뀌어가는 것을 동적 웹페이지, 그렇지 않고 항상 같은 내용을 보여주는 웹페이지를 정적 웹페이지라고 합니다. 일반적인 게시판 형태의 사이트들은 데이터베이스(DB, Database)를 활용한 동적 웹페이지 방식을 사용하고 있고, 정적 웹페이지 방식의 경우에는 DB를 사용하지 않습니다. 정적 웹페이지 방식은 글 자체가 전부 html 단위의 파일들로 이루어져 있습니다.

흔히 말하는 블로그 서비스들인 티스토리네이버 블로그는 동적 웹페이지입니다. 사용자가 글을 작성하거나 블로그 설정 등을 바꾸면 그 내용이 서버에 있는 DB에 저장이 되고 그 결과가 웹페이지에 반영이 되는 형태로 동작합니다. 동적 웹페이지는 블로그에 접속해서 웹 브라우저 상에서 새 글 작성도 할 수 있고, 통계나 다양한 위젯 등 화려한 기능들을 많이 제공해줍니다.

그에 반해 정적 웹페이지는 단순히 사이트 관리자가 미리 만들어놓은 웹페이지만 볼 수 있습니다. 관리도 힘들고 새 글을 쓰거나 수정할 때도 글을 다운로드 받아 편집하고 업로드까지 수동으로 해야 합니다.

하지만, 이렇게 단점이 많은데도 정적 웹페이지가 주목받는 이유는 다음과 같습니다.


정적 웹페이지의 장점

  • 동적인 요소가 없기 때문에 데이터베이스 등이 필요없고, 구축이 쉬움.
  • 단순 문서로만 이루어져 있어서 서버간 통신이 거의 없고 속도가 빠름.
  • 정적인 문서들로만 이루어져 있기 때문에 어떤 호스팅 서버에서도 동작 가능.
  • 일반 텍스트 에디터기에서 문서를 작성이 가능. 즉, 익숙한 에디터기를 사용할 수 있음.
  • 백업, 복원이 쉬움.

위 와 같은 장점이 있기 때문입니다.

블로그 같은 경우는 역동적인 인터랙션보다는 대부분이 정적인 글들로 이루어져 있는 경우가 많아서 정적 웹페이지만으로도 충분히 훌륭한 결과물을 보여줄 수 있습니다. 그래서 블로그와 정적 웹페이지는 상성이 아주 좋아서 최근에 정적 웹페이지 방식의 블로그가 많이 생겨나고 있습니다.


정적 웹페이지 플랫폼

이미 수백 개의 정적 웹페이지 플랫폼들이 존재합니다. 그 중에서 대표적인 플랫폼으로 Jekyll이나 Hugo 등이 있습니다. 여기에서 좀 더 많은 정적 웹페이지 플랫폼 리스트를 볼 수 있습니다. (평점으로 정렬해보면 Jekyll은 당당하게 1위를 차지하고 있습니다.)

이 블로그는 Jekyll로 만들어졌졌습니다. 잠깐 Hugo으로 블로그를 운영해 본 적도 있는데, 다시 Jekyll로 돌아왔습니다.

Jekyll과 Hugo는 서로 장단점이 있습니다.


Jekyll vs Hugo

Jekyll Hugo
Ruby 기반 Go 언어 기반
설치 과정이 복잡하다. 단순 실행 파일만 있으면 된다.
컴파일 속도가 느림. 컴파일 속도가 빠름.
사용자 수가 아주 많다. Jekyll 만큼은 아니지만 사용자 수가 점점 많아지고 있다.
많은 Plugin & 테마를 지원한다. Jekyll 만큼은 아니지만 점점 증가하고 있다.

대부분이 Hugo 쪽이 더 좋은 것처럼 썼지만, 제가 Jekyll을 쓰는 이유는 GitHub Pages에는 Jekyll이 더 최적화가 되어 있다는 점입니다. Hugo를 이용할 경우 사이트를 Hugo로 빌드를 한 다음 그 결과물을 서버에 업로드해야 하지만 Jekyll의 경우는 그냥 소스 그대로 서버에 올리면 GitHub Pages가 알아서 빌드를 해주기 때문에 별도의 배포 작업이 필요없어 관리가 더 수월합니다.

물론 반대로 말하면 Hugo는 결과물만 서버에 올리기 때문에 원본 소스는 비교적 안전하다고 볼 수 있고, Jekyll은 전체 소스를 그대로 올리기 떄문에 아무나 소스를 가져갈 수 있다는 단점도 있습니다. 하지만, GitHub Pages에서 블로그를 운영하기로 한 이상 소스 보호는 큰 의미가 없는 것 같아서 관리가 수월한 Jekyll로 다시 돌아오게 되었습니다.

그리고, Jekyll의 사용자 수가 훨씬 더 많기 때문에 자료 찾기가 수월하다는 점도 있습니다.

Jekyll의 빌드 속도가 느리다거나 Ruby 기반이라 설치가 복잡하고 무겁다는 단점도 로컬 PC에서 빌드를 직접 돌릴 떄나 해당되는 말이며, 저 같은 경우는 그냥 로컬 빌드없이 서버상에서 포스팅하는 글만 관리하다보니 큰 문제는 없는 것 같습니다. (하지만, 테마를 적용한다던지 블로그 전체의 레이아웃이나 메뉴 등을 바꾼다거나 할 때는 미리 로컬 PC에서 확인하는 작업이 필요하기 때문에 결국은 Jekyll을 로컬 PC에 설치해야 할 것입니다. 단순히 글만 작성할 때는 상관없습니다.)