4.1) 무엇이 필요한가?
선언과 계산기로 만들겠다고 했으니 당연히 두 모듈이 모두 필요하다. 다음을 가져온다.
- 계산기 모듈: 01_StackAndCalc.06_read_infix.06_read_infix.cpp
- 선언 분석 모듈: 02_cdecl.03_dcl.03_dcl_main.cpp
- StringBuffer 클래스
이때 이전에 구현한 모듈을 리팩토링할 것이다. 리팩토링(refactoring)이란 내부 논리나 구조를 바꾸고 개선하는 유지보수 행위이다. 그럼 이전에 구현한 프로그램에 개선할 점이 있다는 뜻인데, 과연 이 필자란 사람은 어떤 부분을 개선할 생각인걸까?
바로 정답을 말하자면 모든 모듈이다. 애초에 하나의 목적을 가지고 있지 않았던 코드들을 하나로 묶으려면 아주 잘 만든 라이브러리가 아닌 이상 코드의 수정은 불가피하다(라이브러리를 사용하지 않고 바로 프로그래밍을 공부하는 학부생 수준에서라면 이런 현상은 더 자주 일어난다). 이는 필자가 이전 모듈을 올바른 방향으로 작성하지 못했다는 뜻도 된다. 하지만 이런 과정을 통해 우리는 앞으로 리팩토링할 때 어떤 부분을 개선하고 줄여야 하는지를 연습하여 후에 있을 큰 프로젝트에서 실수가 일어나지 않도록 할 수 있게 된다.
사실 우리가 작성한 코드의 양이 그렇게 많지 않은 만큼, 선언 분석을 공부하여 얻은 지식을 이용해 프로그램을 처음부터 새롭게 작성하는 것도 좋은 방법이다. 필자는 두 방법을 모두 보일 생각이다. 먼저 작성된 코드가 있는 것을 수정하는 리팩토링이 더 설명하기 간단하므로 이것을 먼저 진행하겠다.
프로젝트를 새롭게 생성하고 C Compiler라는 뜻으로 이름을 cc라고 하자. main 소스 파일을 생성하고 다른 소스 파일을 모두 복사하여 프로젝트에 붙인다. 그러면 main을 포함하여 총 5개의 파일이 프로젝트에 있게 된다.
그런데 사실 선언 분석 모듈과 계산기 모듈은 서로 같이 사용하는 함수가 있다. 기본 판별 함수로서 소스의 위에 정의한 is_digit, is_lower와 같은 함수들이 바로 그렇다. 이 함수들은 이후의 모든 프로젝트에서도 반드시 자주 사용될 함수들이기 때문에, 모듈 각각의 소스 파일이 아닌 다른 소스 파일로 옮겨야 한다. 그래야 모듈에 필요 없는 코드가 줄어들고 가독성이 높아져 생산성에 기여하게 된다.
앞으로 수식 해석 모듈은 read expression을 줄여서 rdx, 선언 분석 모듈은 dcl이라고 부르겠다. 또한 모든 모듈이 공유하는 함수는 common 파일에 기록하는 것으로 하겠다. 예를 들어 기본 판별 함수의 원형은 common.h에 기록하고, 그 구현은 common.cpp에서 할 것이다. 즉 다음과 같다.
common.h |
#ifndef __COMMON_H__ #define __COMMON_H__ #include <string> // 예외 형식 Exception에 대한 임시적인 정의입니다. typedef std::string Exception; // 기본 판별 함수입니다. bool is_digit(char ch); // 문자가 숫자라면 참입니다. bool is_lower(char ch); // 소문자라면 참입니다. bool is_upper(char ch); // 대문자라면 참입니다. bool is_alpha(char ch); // 알파벳이라면 참입니다. bool is_alnum(char ch); // 알파벳 또는 숫자라면 참입니다. bool is_space(char ch); // 공백이라면 참입니다. // 식별자로 가능한 문자인지 확인합니다. bool is_namch(char ch); // 식별자 문자라면 참입니다. bool is_fnamch(char ch); // 첫 식별자 문자라면 참입니다. #endif |
common.cpp |
#include "common.h" // 기본 판별 함수입니다. bool is_digit(char ch) { // 문자가 숫자라면 참입니다. return ('0' <= ch && ch <= '9'); } bool is_lower(char ch) { // 소문자라면 참입니다. return ('a' <= ch && ch <= 'z'); } bool is_upper(char ch) { // 대문자라면 참입니다. return ('A' <= ch && ch <= 'Z'); } bool is_alpha(char ch) { // 알파벳이라면 참입니다. return is_lower(ch) || is_upper(ch); } bool is_alnum(char ch) { // 알파벳 또는 숫자라면 참입니다. return is_digit(ch) || is_alpha(ch); } bool is_space(char ch) { // 공백이라면 참입니다. return (ch == ' ' || ch == '\t' || ch == '\n'); } // 식별자로 가능한 문자인지 확인합니다. bool is_namch(char ch) { // 식별자 문자라면 참입니다. return is_alnum(ch) || (ch == '_'); } bool is_fnamch(char ch) { // 첫 식별자 문자라면 참입니다. return is_alpha(ch) || (ch == '_'); } |
그리고 이에 따라 각 모듈에서 정의했던 판별 함수를 삭제하고 헤더 파일을 추가하여 리팩토링한다. 이때 rdx 모듈에 정의되어있는 clear_input_buffer 또한 common으로 옮기겠다. 이 함수가 iostream 헤더에 정의되어있는 cin객체를 사용하기 때문에, common 소스 파일에 iostream 헤더를 추가해야 한다.
다음은 정의했던 스택을 read_infix 소스 파일이 아닌 소스 파일로 분리하는 작업이다. Stack.h 파일을 만든다. Stack은 템플릿 클래스이기 때문에 컴파일 시에 구현 전체의 정의를 컴파일러가 반드시 알아야 한다. 즉 이 경우Stack은 헤더 파일에 그대로 구현하고 별도의 cpp 파일을 만들지 않는다.
Stack.h |
#ifndef __HANDY_STACK_H__ #define __HANDY_STACK_H__
// 형식에 자유로운 스택을 만들기 위해 템플릿 클래스로 변경 template <typename Data> class Stack { static const int MAX_STACK_SIZ = 256; Data _list[MAX_STACK_SIZ]; int _count; private: inline bool is_full() const { return _count == MAX_STACK_SIZ; } public: Stack() : _count(0) {} void push(const Data &data) { if (is_full()) throw Exception("Stack is full"); _list[_count++] = data; } Data pop() { if (is_empty()) throw Exception("Stack is empty"); return _list[--_count]; } Data top() const { if (is_empty()) throw Exception("Stack is empty"); return _list[_count - 1]; } inline bool is_empty() const { return _count == 0; } inline int count() const { return _count; } };
#endif |
아니면 굳이 신뢰도도 성능도 떨어지는 우리 스택을 쓸 게 아니라 C++ 표준 템플릿 라이브러리가 지원하는 스택 클래스를 쓰는 것도 좋다. 다만 STL의 stack은 pop 함수의 구현이 일반적인 구현과 달라 예제에서 불편할 수 있어 넣지 않았는데, 우리가 사용하던 스택을 그대로 사용하고 싶다면 Stack만 리팩토링하는 방법을 사용할 수도 있다.예를 들면 다음과 같다.
Stack.h |
#ifndef __HANDY_STACK_H__ #define __HANDY_STACK_H__ #include <stack> template <typename Data> class Stack { std::stack<Data> stack; public: Stack() {} void push(const Data &data) { stack.push(data); } Data pop() { Data popValue = stack.top(); stack.pop(); return popValue; } Data top() const { return stack.top(); } inline bool is_empty() const { return stack.empty(); } inline int count() const { return stack.size(); } }; #endif |
이렇게 스택 클래스의 리팩토링도 끝났다. 이제 각각의 모듈에 존재하는 main 함수를 적당히 이름을 바꾸고 컴파일 하라.
main.cpp |
#include <iostream> using namespace std; int main(void) { int main_rdx(), main_dcl(); main_rdx(); // read expression main_dcl(); // analyze declaration return 0; } |
두 함수 모두 정상적으로 실행됨을 확인할 수 있다.