[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;
}