- 이 글 버전: 1.0
- 업데이트 계획 있음
- PDF 다운로드
레거시 프로젝트는 그 양이 방대하고 고칠 부분이 많으므로 처음부터 큰 목표를 정해서 작업하면 의욕만 떨어진다. 여기서는 과거에 리팩터링 작업을 할 때의 경험을 바탕으로 대규모 리팩터링에 어떻게 접근하는지 알아본다. 아래 지침을 한꺼번에 적용하면 삶이 고단해질 가능성이 농후하므로 하나씩 적용해서 문제를 해결해나가야 한다. 삶이 너무 즐거워서 조금 불행해지고 싶다면 모를까.
기본 런타임 검사 활성화하기
DEBUG 구성시 [프로젝트 구성 / C/C++ / 코드 생성] 다음을 활성화한다.
- 작은 형식 검사: 예(/RTCc)
- 기본 런타임 검사: 모두(/RTC1, /RTCsu와 동일)
경고 수준 4로 올리기
DEBUG/RELEASE 구성 모두에서 [프로젝트 구성 / C/C++ / 일반 / 경고 수준]을 “수준 3(/W3)”으로 바꾼다.
안 쓰는 인자 처리하기
경고 수준을 4로 올리면 사용하지 않는 인자도 잡아낸다. 실제로 인자를 사용하지 않는 게 분명하고 삭제하기도 쉽다면 코드를 제거하면 된다. 하지만 때에 따라선 당장 사용하진 않아도 인터페이스 규약상 인자를 놔둬야 때도 있다. 이럴 때는 인자를 DBG_UNREFERENCED_PARAMETER
로 묶는다.
안 쓰는 로컬 변수 처리하기
위와 마찬가지로 DBG_UNREFERENCED_LOCAL_VARIABLE
로 묶는다.
안전하지 않은 CRT 함수 처리하기
strcpy
등의 함수는 버퍼 오버플로우 공격 등에 취약하다. 그래서 strcpy_s
등의 안전한 함수로 바꾸는 편이 좋다. 컴파일러가 어떤 함수로 바꿔야 하는지 명확하게 알려주고 MSDN 라이브러리에 관련 정보가 충분히 정리되어 있으므로 귀차니즘 빼고는 크게 문제가 없다.
경고를 오류로 처리하기
DEBUG/RELEASE 구성 모두에서 [프로젝트 구성 / C/C++ / 일반 / 경고를 오류로 처리하기]을 “예”으로 바꾼다.
printf
등의 C 스타일 함수를 iostream
함수로 바꾸기
printf
계열의 함수는 문자열 치환함으로써 외부 공격에 쉽게 노출된다. 예를 들어,
printf("%s", msg);
라는 코드에서 msg
값이 "%s%s%s"
가 들어오면 바로 예외가 발생한다. 외부 사용자가 보내는 메시지를 출력하는 코드라면 악의적인 공격이나 의도하지 않은 메시지 입력에 의해 프로그램이 죽는 문제가 발생한다. 그렇기 때문에 이런 문제에서 자유로운 iostream 계열의 함수를 사용하는 편이 좋다. 더불어 iostream은 자원 반환의 문제에서 어느 정도 자유로우며 더 복잡한 작업에 용이하다.
안 쓰는 코드나 주석 삭제
안 쓰는 코드나 주석이 의외로 많다. 소스 버전 서버를 쓰면 지운 코드를 복구하기는 쉬우니 과감하게 지우자. 이해하고 알아야 할 코드량이 줄어야 머리 쓸 일이 많은 리팩터링을 조금이라도 원활히 진행할 수 있다. 이 작업은 리팩터링하는 내내 꾸준히 해야 한다.
const
한정자를 여기 저기 붙여보기
C++의 const
한정자는 크게 세 가지 용도가 있다. Effective C++ 같은 명저를 읽으면 좋지만 귀찮으면 C++ const? 같은 웹 문서를 읽어도 좋다. 포인터가 보이는 곳마다 멤버 함수마다 const
한정자를 붙일 수 없는지 매번 확인한다. 이렇게 하여 어느 곳에서 데이터 변경을 예상해야 하는지 쉽게 파악이 가능해지고 추후 리팩터링 작업이 무척 쉬워진다.
조금 다른 이야기지만 const
한정자를 잘 쓰는 게 어찌나 중요하던지 매우 유명한 서구권 소프트웨어 회사가 내는 입사 과제에서는 이 한정자만 잘 써도 100점 만점 중 30점을 먹고 들어간다.
멤버 메서드에 static
붙여보기
멤버 변수에 접근하지 않는 메서드가 은근히 많다. 이런 코드는 static
메서드 그러니까 클래스 메서드로 바꾼다. static
메서드는 해당 클래스 밖으로 빼는 대상이 되기 때문에 추후 리팩터링의 준비 단계라 하겠다.
inline
함수 줄이기
Effective C++을 정독했다면 컴파일러가 inline
함수를 실제로 inline
으로 처리해주는 경우는 극히 드물다. 아주 간단하게 규칙을 정해 일괄적으로 inline
함수를 삭제하자. 이를테면 세 줄이 넘는 함수는 inline
대상에서 제외하는 식으로 규칙을 정하면 된다.
인라인 함수는 코드 컴파일 시간을 늘리고 코드 변경시 다시 빌드를 해야 하는 상황이 계속해서 벌어지게 한다.
헤더 파일에 있는 구현부를 소스 파일에 옮기기
.h 파일에 구현부가 있으면 빌드 시간이 길어지기 마련이다. 헤더 파일에 구현부가 있으면 구현이 바뀔 때마다 그 헤더 파일을 참조하는 다른 소스 파일도 다 영향을 받는다. 그에 반해 소스 파일에 구현부가 있고 헤더 파일의 인터페이스는 변하지 않았다면 링크 시간만 더 필요할 뿐이다. 이런 식으로 빌드 시간은 길어진다. 대규모 프로젝트일수록 빌드 시간이 감당 못할 정도로 증가하므로 귀찮더라도 인내심을 발휘해 하나하나 처리하자.
#include
정리
쓰지도 않는 헤더 파일을 포함해서 불필요하게 입출력과 CPU의 연산 능력을 낭비하지 말자. C# 등의 현대적인 언어와 달리 C++에서는 #include
가 일으키는 부하는 결코 무시 못한다.
#include
를 헤더 파일에서 소스 파일로 옮기기
헤더 파일은 또 다른 헤더 파일이 #include
하므로 헤더 파일의 #include
는 소스 파일의 #include
보다 몇 배 도는 몇십 배 많이 포함된다. 전방 선언을 적극적으로 이용하고 구현부를 소스 파일로 옮기는 방식으로 헤더 파일에 #include
를 둘 필요성을 줄이자. 클래스 설계시 포인터와 전방 선언을 잘 이용하면 헤더 파일을 정리하는데 큰 도움이 되기도 하는데 보통 pimpl 기법이라고 한다.
불필요한 #include
를 제거하기
헤더 파일 뿐 아니라 소스 파일에도 불필요한 #include
가 잔뜩 있기 마련이니 적극적으로 찾아 지우자.
미리 컴파일된 헤더 정리하기
미리 컴파일된 헤더를 잘못 구성하면 크게 두 가지 문제가 발생한다. 하나는 다시 빌드해야 하는 경우가 많아진다. 또 순환 참조 등의 설계 상의 문제를 가려서 리팩터링이나 설계 변경이 어려워진다.
미리 컴파일된 헤더에서는 가급적 표준 라이브러리나 Windows SDK 같이 소스 코드 변경이 없는 외부 프로젝트의 헤더 파일만 #include
한다. 자체 개발한 C++ 프로젝트를 참조할 때는 자주 변경되지 않는 헤더 파일만 남긴다. 특히 헤더 파일에 구현부가 없게 잘 정리하자. 무엇보다 미리 컴파일된 헤더에는 자신의 프로젝트에 속한 헤더 파일은 넣어선 안 된다.
눈에 띄는 놈은 모두 private
으로 넣기
클래스 외부로는 내부 구현을 드러내지 말자. 정말 필요한 부분을 제외하면 모두 private
로 선언한다. 객체 지향을 단순히 상속이라고 생각하는 사람은 protected
를 자주 사용하는데 당장 필요하지 않은 부분은 모두 private
으로 선언한다. 외부로 노출되는 게 많을수록 복잡도가 증가한다. 코드가 복잡하면 코드를 이해하기 힘들고 정리하기는 더 어렵다.
protected
나 public
으로 선언할 필요가 있을 때조차도 friend
선언으로 대체 가능하곤 하다. 물론 friend
를 쓸지 말지는 꽤나 경험과 직관이 필요한 설계 이슈이므로 API Design for C++ 같은 서적을 읽어보면 좋다는 정도로 정리한다.
일회성 유틸리티 함수는 anonymous namespace 에
코드를 정리하다 보면 특정 클래스의 구현부에서만 쓰는 함수는 static
메서드가 되곤 한다. 그런데 이러한 클래스 메서드를 외부에서는 사용하지 않는다면 굳이 헤더 파일에 선언부를 놓을 필요조차 없다. 메서드 선언만으로도 외부에 굳이 드러낼 필요 없는 구현 상세를 보여주는 것이기 때문이다. 이런 메서드는 소스 파일에 무명 이름 공간을 두고 그 안에 별도의 함수로 선언하고 구현함으로써 헤더에서 제거하면 된다.
MS에서 코딩할 때는 함수 내에서 member 변수 이용이 없다면 static 한정자를 붙이라고 에러가 뜹니다. 그것도 어디엔가 세팅이 있을텐데.. 하면 리팩토링에 도움되지 않을까 싶습니다. 🙂
어라라, 그런 오류가 뜨나요? Visual Studio 버전 몇 인가요? Visual C++ 말씀하시는 것 맞죠?
DEBUG/RELEASE 구성 모두에서 [프로젝트 구성 / C/C++ / 일반 / 경고 수준]을 “수준 3(/W3)”으로 바꾼다. 에서 ‘수준4(/W4)’ 가 아닌가 싶습니다..^^