포스트

[Clean Code][TIL] 오류 처리

이 포스트는 로버트 C.마틴의 Clean Code 속 7장(130p ~ 142p) 내용에 대한 정리입니다.


“깨끗한 코드는 읽기도 좋아야 하지만 안정성도 높아야 한다.” (7장 142p)


오류를 막기 위한 예외 처리 try-catch

코드를 작성하다 보면 OutOfBounds, NullPointerException 등 여러 오류가 발생합니다.

1
2
vector<int> vec(5, 0);
cout << vec.at(5);


위 코드는 vector 에 할당된 메모리 외부에 접근하는 문제가 있습니다.
이러한 오류는 컴파일러도 발견하지 못하기 때문에 실행 중 프로그램이 중단됩니다.
따라서 프로그램이 실행 중 멈추지 않도록 다양한 오류들을 처리해야 합니다.
이를 위해 여러 프로그래밍 언어에서는 예외 처리 라는 기능을 제공하고 있습니다.

위 코드에 예외 처리를 추가하면 아래와 같습니다.

1
2
3
4
5
6
7
8
vector<int> vec(5, 0);
int target_index = 5;

try {
  cout << vec.at(target_index);
} catch (std::out_of_range error) {
  std::cout << "[Error]: " << error.what();
}

실행 결과


C++ 에서는 예외 처리를 try-catch 문 으로 처리할 수 있습니다.
std::out_of_range 는 이름처럼 허용된 범위 밖 접근이 있을 때 발생합니다.

실행 중 무엇이 오류를 발생시켰는지 확인하고 싶을 경우,
try 문 안에서 catch 문으로 예외를 전달하는 throw 키워드 를 사용해 확인할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
vector<int> vec(5, 0);
int target_index = 5;

try {
  if (vec.size() > target_index) {
    cout << vec.at(target_index);
  } else {
    throw target_index;
  }
} catch (int index) {
  std::cout << "[Error]: " << index << "\n";
} 


위의 코드에서는 throw 키워드로 target_index 값을 넘겨줍니다.
그리고 catch 문에서 이 값을 index로 받은 뒤 catch 문 내부를 실행하고 프로그램을 종료하게 됩니다.

추가적으로 try-catch 문을 작성할 때, try 문 안에 예외 처리가 필요한 모든 코드를 작성하는 것이 아니라
코드를 여러 개념으로 나누어 각각 try-catch 문으로 감싸는 것이 독립적으로 예외 처리를 할 수 있어
이후 예외를 받았을 때 처리하기 쉽습니다.

호출자를 고려해 예외 클래스를 정의하자

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class MiniSocket {
 public:
  void make_socket(string addr, int port) {
    int error_code;

    error_code = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (error_code != 0) {
      throw 0;
    }

    mini_socket = socket(PF_INET, SOCK_STREAM, 0);

    if (mini_socket == INVALID_SOCKET) {
      throw string("INVALID_SOCKET");
    }

    SOCKADDR_IN serverAddress;
    memset(&serverAddress, 0, sizeof(serverAddress));
    serverAddress.sin_family = AF_INET;
    serverAddress.sin_addr.s_addr = inet_addr(addr.c_str());
    serverAddress.sin_port = htons(port);

    error_code =
        connect(mini_socket, (SOCKADDR*)&serverAddress, sizeof(serverAddress));

    if (error_code == SOCKET_ERROR) {
      throw exception("SOCKET_ERROR");
    }
  }

  WSADATA wsaData;
  SOCKET mini_socket;
};

int main() {
  MiniSocket socket;

  try {
    socket.make_socket("127.0.0.1", 7890);
  } catch (const exception& error) {
    cout << "[MiniSocket error]: " << error.what();
  } catch (int error) {
    cout << "[MiniSocket error]: startup";
  } catch (string error) {
    cout << "[MiniSocket error]: " << error;
  }

  return 0;
}


위의 코드는 소켓 통신을 시작하기 전 소켓을 준비하는 과정의 일부분으로
WSAStartup() 함수, socket 생성자, connect() 함수에서 각각 예외를 반환합니다.
예외를 받으면 main 함수에서 각 예외에 대한 처리를 하는데 MiniSocket 클래스를 감싸면서
하나의 예외 유형으로 반환할 수 있습니다.

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
31
32
class MidSocket {
  MiniSocket* inner_socket;

public:
  MidSocket(int port) { 
      inner_socket = new MiniSocket(port);
  }

  void make_socket() {
    try {
      inner_socket->make_socket();
    } catch (const exception& error) {
      throw exception(error.what());
    } catch (int error) {
      throw exception("startup");
    } catch (string error) {
      throw exception(error.c_str());
    }
  }
};

int main() {
  MidSocket socket(7890);

  try {
    socket.make_socket();
  } catch (const exception& error) {
    cout << "[MidSocket error]: " << error.what();
  }

  return 0;
}


이때, MidSocket 클래스는 단순히 MiniSocket 클래스가 던지는 예외를 잡아 변환하는 클래스입니다.
이 코드처럼 감싸는 클래스 는 외부 API 등을 감싸면 외부 라이브러리와 프로그램 사이
의존성이 크게 줄어들 수 있습니다.

NULL 오류

NULL 은 값 자체가 존재하지 않음을 뜻합니다.
예를들어, int 형 변수에 0을 할당하는 것과 할당 자체가 없는 것은 전혀 다른 의미입니다.

반환되는 NULL 과 전달받는 NULL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
 private:
  string name;

 public:
  Person(const char* in) : name(in) {}
  string get_name() { return name; }
};

int main() {
  Person* person = new Person(NULL);
  string my_name = person->get_name();
  cout << my_name;
  return 0;
}


위의 코드에서 my_name 변수에 어떤 값이 들어갈지 예상이 되시나요?
이 코드는 컴파일도 불가능한 괴상한 코드입니다.
main 함수의 첫 행을 보면 Person 클래스 생성자에 NULL 을 전달하고 있습니다.

이 문제를 해결하는 방법은 이전 설명처럼 감싸기 클래스로 Person 객체를 감까거나
특수 사례 객체 를 반환하는 방법도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
 private:
  string name;

 public:
  Person(const char* in) {
    if (in == nullptr) {
      name = "Empty";
    } else {
      name = (in);
    }
  }
  string get_name() { return name; }
};

int main() {
  Person* person = new Person(NULL);
  string my_name = person->get_name();
  cout << my_name;
  return 0;
}


이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.