티스토리 뷰

코드를 복잡하게 만드는 문제의 근원은 if 조건문 또는 for 반복문 등의 제어문이다. 제어문이 없는 코드는 위에서 아래로 쭉 읽으면 되지만, 제어문이 뒤섞여 버린 코드는 처리의 흐름을 추적하기 어렵고 보수하기도 어렵다.

if 조건문 감축과 단순화

if 조건문을 하나 추가할 때마다 실행 경로가 하나 증가하게 된다. 따라서 if 조건문을 사용하지 않고 코드를 구성하는 것이 포인트이다.

하한값, 상항값 체크 단순화

하한값과 상한값을 체크하는 if 조건문은 대부분의 프로그램에서 사용하는 코드이다.

if (x > 10) {
    x = 10;
}

이럴 때는 조건문을 추가하는 것보다 표준함수나 함수화시켜서 사용하는 것이 좋다. 아래는 C++에서 STL의 min함수를 사용한 방법이다.

x = std::min(x, 10);

조기 리턴 활용

중첩된 if 조건문은 보기에 좋지 않으며 가독성도 매우 떨어진다. 함수의 입구에서 예외조건을 확인하고 조기에 리턴하여 중첩 if 조건문을 제거하는 것을 조기 리턴이라고 한다.

void update() {
    if (health > 0) {
        if (life_time > 0) {
            ...
        }
    }
}

조기 리턴을 사용하면 다음과 같이 변환할 수 있다.

 void update() {
    if (health <= 0) return;
    if (life_time <= 0) return;
    ...
}

중첩된 조건문을 빼내어 함수화하면 다음과 같이 작성할 수도 있다.

bool is_dead() {
    if (health <= 0) return true;
    if (life_time <= 0) return true;
    return false;
}

void update() {
    if (is_dead()) return;
    ...
}

bool로 리턴되는 조건식은 return 조건식형식을 사용하고, 리턴 값이 2가지라면 삼항연산자를 사용할 수 있다.

bool is_death() {
    return health <= 0;

int bonus(int time) {
    return (time < 10) ? 1000 : 0;
}

중복된 조건식 통합

중첩된 여러개의 if 조건문 내부에서 조건이 중복된다면 중복이 일어난 조건문을 먼저 판정하면 if 조건문을 줄일 수 있다.

if (state == STATE_FAIL) {
    if (wait_timer > WAIT_TIME) {
        fail();
    }
}
if (state == STATE_MOVE) {
    if (wait_timer > WAIT_TIME) {
        move();
    }
}

if 조건문 안에 중복된 if 조건문이 있다. 조기 리턴을 사용해서 중복된 if 조건문을 통합해보자.

if (wait_timer <= WAIT_TIME) return;

if (state == STATE_FAIL) {
    fail();
}
if (state == STATE_MOVE) {
    move();
}

중복된 복합 조건 통합

다음과 같이 복합 조건이 중복되는 경우에도 조건식으로 통합할 수 있다.

if (state == STATE_FALL && wait_timer > WAIT_TIME) {
    fail();
}
if (state == STATE_MOVE && wait_timer > WAIT_TIME) {
    move();
}

중복되는 wait_timer > WAIT_TIME 조건을 if 조건문으로 통합해보자.

if (wait > WAIT_TIME) {
    if (state == STATE_FAIL) {
        fail();
    }
    if (state == STATE_MOVE) {
        move();
    }
}

조건식이 직접적으로 관련된 부분을 간소화하기

다음 예는 어떤 케릭터의 이동 계산이다. 대시 중에는 이동 속도가 2배가 된다는 것을 나타낸다.

if (is_dash()) {
    position += direction * 10.0f;
} else {
    position += direction * 5.0f;
}

계산식 부분이 중복되고 있다. 대시 조건식에 직접 관련있는 부분은 속도의 수치이다. 따라서 속도 변화 부분만 따로 간소화시켜보자.

float speed = 5.0f;
if (is_dash()) {
    speed = 10.0f;
}
position += direction * speed;

이번에는 speed 연산 부분을 함수화 시켜보자.

float speed() {
    if (is_dash()) return 10.0f;
    return 5.0f;
}

리턴 값이 2개므로 삼항연산자를 사용하여 더 간소화 시켜보자.

float speed() {
    return is_dash() ? 10.0f : 5.0f;
}

속도 수치를 나타내는 열거형을 사용해서 이름을 붙여보자.

float speed() {
    return is_dash() ? SPEED_DASH : SPEED_NORMAL;
}

postion += direction * speed();

speed()함수를 사용하면 if 조건문이 없어지고 중복된 계산식도 없어진다.

배열을 활용한 if 조건문 제거

배열 등의 자료 구조를 사용하면, 쓸데없이 긴 if 조건문을 제거할 수 있다. 다음 코드는 0~2까지의 순서를 특정한 값으로 교환하는 예이다.

int id_to_num(int id) {
    if (id == 0) return 10;
    if (id == 1) return 15;
    if (id == 2) return 20;
    assert(!"부정확한 id");
    return 0;
}

변환 전용 배열을 사용하여 if 조건문을 제거할 수 있다. 반복 초기화를 피할 수 있게 static const를 붙여서 상수화하자.

int id_to_num(int id) {
    static const int table[] = {10, 15, 20};
    assert((0 <= id && id <= 3) && "부정확한 id");
    return table[id];
}

이번에는 반대로 변환하는 예를 살펴보자.

int num_to_id(int num) {
    static const int table[] = {10, 15, 20};
    for (int i = 0; i < 3; ++i) {
        if (table[i] == num) {
            return i;
        }
    }
    assert(!"부정확한 숫자");
    return 0;
}

STL의 find함수를 사용하면 다음과 같이 작성할 수 있다.

int num_to_id(int num) {
    static const int table = {10, 15, 20};
    assert(std::find(&table[0], &table[3], num) != &table[3]);
    return std::find(&table[0], &table[3], num) - &table[0];
}

C++11에서는 unordered_map클래스를 이용해서 다음과 같이 작성할 수 있다.

int num_to_id(int num) {
    static const std::unordered_map table = {
        {10, 0}, {15, 1}, {20, 3}
    };
    assert((table.find(num) != table.end()) && "부정확한 숫자");
    return table.at(num);
}

C++11 사양에서는 함수 내부의 static 변수 초기화는 스레드 세이프를 보증하지만, 일부 컴파일러는 대응하지 못하는 경우가 있어서 함수 외부에 선언해야 할 수도 있다.

결정표를 사용한 if 조건문 제거

결정표는 판정 조건의 조합과 그에 대응하는 결과를 정리한 배열을 의미한다. 아래 코드는 결정표를 이용하여 가위 바위 보 게임의 승패를 판정하는 코드이다.

enum Hand {
    Rock,
    Scissors,
    Paper
}
enum Result {
    Win,
    Lose,
    Draw
}

Result judgement(Hand my, Hand target) {
    static const Result result[3][3] = {
        // 바위, 가위, 보(상대방)
        {Draw, Win, Lose}, // 바위(자신)
        {Lose, Draw, Win}, // 가위
        {Win, Lose, Draw} // 보
    }
    return result[my][target];
}

null 객체 도입

null 객체란 null포인터를 대신하는 더비 객체를 의미한다. 다음과 같이 객체의 존재를 확인하는 if 조건문이 많을 때 null객체를 사용한다.

if (player != nullptr) {
    plyer->move();
}
...
if (player != nullptr) {
    player->draw();
}

null 객체를 사용하려면 대상 클래스의 부모 클래스가 있어야 하며 모든 멤버 함수가 가상함수여야 한다. 아래는 null객체를 사용하여 if 조건문을 제거한 코드이다.

class Actor{
    virtual void move() = 0;
    virtual void draw() = 0;
};
class NullActor : public Actor {
    virtual void move() { }
    virtual void draw() { }
};
player = new NullActor();
player->move();
player->draw();

여러개의 null 체크를 해야하는 설계는 되도록 피하는 것이 가장 좋다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함