객체지향 프로그래밍
적은 기능을 수행하는 프로그램을 만들때는 프로그램의 기능간 의존성을 고려할 필요가 없지만, 복잡할수록 생각나는대로 짜다가는 이후의 수정이 어려워지고, 심지어는 아예 구현이 불가능한 경우도 있을 수 있다. 그러한 때일수록 각 요소의 객체화를 꼼꼼히 시킬 필요가 있다고 생각한다. 이후 특정 기능을 수정할 일이 생기면, 소스코드 전체를 갈아엎을 필요가 없이 특정 요소만 고치면 되므로 이는 더더욱 매력적이다.
그렇다면 이를 어떻게 할 수 있을까... 에 대한 답은 수학 문제처럼 다양한 유형이 있다고 생각하는데, 문제는 정답이 없다는 것. 해당 유형들은 디자인 패턴이라고 일컫어지며, 굉장히 다양한 디자인 패턴이 있다. 대표적인 것들로 컴포지트, 어댑터, 싱글턴, 팩토리, 옵저버, 이터레이터 ...
그리고 이러한 디자인 패턴은 c에는 없었던 cpp의 객체지향적 특징을 굉장히 잘 살려줄 수 있는 도구라고 생각이 든다. 이는 사람이 구조적인 생각을 컴퓨터로 실현하는 데 있어서 맥락을 같이하기에 보다 편한 점이 있기 때문에, 복잡한 코드를 짤 때 있어서 큰 도움이 된다고 생각함. 아래의 예시들이 근래 자주 봤던 것들.
복잡한 struct 구조체들과 이를 사용하는 메서드
예시는 SDL_ttf. 허접이라 그동안 봐온 소스코드가 별루 없다 ㅠ
보이는 것처럼, 모든 폰트의 정보를 TTF_Font 객체에 넣고서, 이를 사용하는 함수를 전역선언 시켜놓았다.
c++ 스타일로 바꾼다면 이런 느낌.
c스타일이 나쁘다는 건 아니지만, 객체 관리 쉽고 함수 네이밍하기 쉽고, 그래서 코드 길이 짧아지고, 머리가 편해지고, 뭐 나쁠 건 없잖아...
보통 처음엔 간단했지만 보수유지 하다보니 덩치가 산만해질때 메서드랑 struct가 중구난방해지는 경우가 많던데 (의도치 않은 c스타일화), 이런 식으로 객체화시키면 더 사용하기 쉽더라 하던 경험.
복잡한 switch ~ case 구문
예를 들어서, 100가지 종류의 case를 처리하는 모듈이 있고, 각 모듈별로 하는 작업이 다르다고 가정하자. 예컨데 100가지 적이 있고, 각 적마다 데미지를 준다고 칠때 ...
이런 느낌으로 될 것이며, 각 Type마다 특수한 패턴의 처리를 해야 할 경우 코드는 훨씬 더 복잡해질 것이다.
그럴 땐 그냥 각 경우에 대한 인터페이스를 만들고 상속시켜서 짜버리는 쪽이 편했음.
깔-끔.
물론, 각 객체들이 너무 중구난방으로 동작할 경우이고, 경우가 많지 않은 복잡하지 않은 루틴의 경우에는 switch 구문 충분히 애용가능하다고 생각한다. 적당히 잘 가려서 써야겠지 ...
구 포멧/모듈과의 호환
이건 절대적으로 "그냥 어댑터 모듈 쓰면 되잖아?"라고 답할 수 있겠지만, 그게 생각보다 어디 써야 되는지 막상 쓰려니까 잘 감이 안와서 ...
이런 경험이 있었다. 모종의 구형 '스킨'파일이 있어서 이를 새로운 형태로 바꾸는 작업이고, 구형 파일의 포멧은 명확하게 정해져 있으나 새 스킨 파일의 포멧은 아직 제대로 정해지지 않아 계속 바뀌는 상황.
이런식으로 대충 기존 구형 파일포멧인 "XMLNode" 객체를 "CNewElement"로 변환한다고 가정합시다. 그런데 의도치 않게 이러한 구형 객체가 CNewElement 말고 CAnotherElement도 생성해야 하는 경우라고 생각하면(즉 1:1매칭이 아닌 경우) 지금 있는 메서드로는 구현이 어려워지고, 새로운 메서드/객체를 만들어야 하기 때문에 골치가 아파집니다.
그래서 그냥 XMLNode를 COldElement로 변환이 아니라 "어댑터화" 시켰는데, 이런 식으로 구성이 됨.
사실 여기에 Render() 메서드 붙이면서 비교해야 확실한 어댑터화구나! 라고 쓸 텐데, 위 예시랑 대조해서 이게 어댑터다! 라고 설명하기엔 좀 모호한 감이 많이 있는덧... 이런 느낌으로 바꿔쓰면 좋겠구나 생각하면 그걸로 충분.
"이중 상속"도 보통 이런 느낌인데, 그때도 이렇게 "상속보다는 구성"으로 해결하는 쪽이 나은 듯. 더더군다나 이런 경우는 다시 구 포멧으로 export 할 일이 있을때 호환성 면에서도 꽤 뛰어난 듯.
모듈의 독립화 = 모듈의 input와 output을 명확하게 하는 것
아무래도 게임 같은 프로그램이 크고 복잡한 경우가 많다 보니 자꾸 게임을 예시로 들게 되는데 ... 어찌됐든 Player이라는 객체가 있다고 생각합시다. 플레이어에 필요한 건 뭘까요? 플레이어 사이드, 초기 체력, 가지고 있는 무기, 무기 남은 양, 최고기록, 현재 플레이한 기록 ....
그런데 아무래도 이것만으로는 부족한 게, 게임 옵션에 들어있는 게임 난이도가 플레이어에게 영향을 끼칠 것으로 생각이 되기 때문에, 그 값을 가져오게 됩니다.
이렇게 되면 플레이어 객체는 Setting 모듈에 대해서 의존성을 가지게 되고, 나중에 Setting 파일명이 바뀌거나 수정을 하거나, 심지어는 Player 객체를 다른 데 떼서 쓸때 곤란한 경우가 생기게 될 수 있으니 아무래도 Player::InitalizePlayer의 인자로서 Setting 객체를 인자, 즉 input으로 주는 게 더 좋을 것 같습니다.
이러한 input이 상호 의존하는 관계는 보통 구성이나 모듈 통합으로 해결할 수 있고... 뭐 어떻게든 해결이 되긴 되니, 각 모듈의 input과 output을 어떻게 짤지가 결국 관건일 듯.
아무튼 이런식으로 남의 소스코드를 보면서 어떤 디자인 패턴으로 구현되어 있는지 보는 게 참 좋다고 생각한다. 남이 쓴 코드를 이해할 수 있다는 건, 내가 그렇게 코드를 짤 수 있다는 이야기이고, 그러한 코드가 20년 넘게 코딩한 GNU 커미터라던가 MS Edge 코드라던가 하면 기분이 얼마나 좋겠는가.
물론 이렇게 적용해 볼 기회를 생각해보면 더 좋을 것이고.
몇가지 도움이 될만한 참고 문서들 (검증되지 않은 문서들 삭삭 긁어서 링크했으니 읽는데 유의)
- 상속과 구성
- 객체지향 프로그래밍 5원칙 / 그리고 구체적인 예시들
개인적으로 반복자(iterator)패턴/체인패턴/싱글턴패턴/옵저버,중재자패턴(Broadcaster)/브리지패턴(상속?)/어댑터패턴/풀링패턴/인터프리터패턴/스레드안전인터페이스 정도는 알아두면 매우 편리하다고 생각... (그냥 다 알아야 하나;)