brunch

You can make anything
by writing

C.S.Lewis

by 백명석 Oct 04. 2015

객체지향의 핵심

왜 객체지향을  하는가?

들어가기

Martin Fowler는 "Patterns of Enterprise Application Architecture"에서 도메인 로직(업무 로직)을 구현할 수 있는 3가지 패턴(Transaction Script, Domain Model, Table Module)에 대해서 설명했다. 

이 중 절차지향 방식이라고 할 수 있는 Transaction Script와 객체지향 방식인 Domain Model이 많이 사용된다고 생각한다. 책에서 Martin Fowler는 Transaction Script 방식이 쉽고, 직관적이어서 시작하기는 쉽지만 복잡도가 증가함에 따라 중복이 많아지는 등 어려움이 발생한다고 설명한다. Domain Model 방식의 경우는 초기 투자 비용이 큰 편이고 참여자들의 객체지향 분석, 설계, 개발 능력이 필요로 되는 어려움이 있지만 복잡한 로직을 다루기에 적합한 방법이라고 설명한다.

나는 새로운 프로젝트를 많이 하는 것보다는 하나의 시스템을 오랫동안 개발하고, 운영하며 개선하는 작업을 더 많이 해 왔다. 처음에는 쉽고 빠르게 개발할 수 있을 것이라 여겨지던 시스템들은 운영에 들어가면 점점 더 복잡해지는 경향이 있었다. 한 번만 쓰고 버릴 것이라고 해서 시작한 이벤트 시스템도 다음해에 다시 사용하자고 하면서 요구사항이 추가되기 일쑤였다. 세상에 안 변하는 것은 없더라...

진짜 단순한 시스템은 없다. 만약 있다면 그 시스템은 사용되지 않는(사용자가 없는) 시스템일 것이다.

객체지향의 핵심은 복잡함을 잘 다루고, 변경할 수 있는 시스템을 만드는 것이라고 생각한다. 이를 위한 가장 중요한 객체지향의 역할은 아래와 같다.

- Encapsulation

- 의존성 관리를 통한 독립적인 개발

Encapsulation

캡슐화는 데이터와 그 데이터를 사용하는 기능을 하나의 객체로 구현하고, 외부에서는 데이터나 구체적인 구현을 모른 체 인터페이스를 통해서 기능을 사용하는 것을 의미한다. 이 말은 학교 때 C++ 등을 교과서적으로 배운 것과는 차이가 있다. 학교 때 클래스는 공통적인 객체들을 표현하기 위한  어쩌고 저쩌고... 이렇게 배웠었다. 하지만 실제 현업에서 경험하여, 배우고, 공감하는 정의는 클래스는 관련된 데이터(속성, 필드 변수 등)와 그 데이터들에 동작하는 기능(메소드)들의 집합이다는 것이다.

아래 그림을 통해 절차지향과 객체지향의 차이를 살펴보자. 그림은 최범균님 블로그에서 차용했다.

절차지향

데이터를 중심으로 새로운 프로시저를 추가하는 방식으로 구현한다. 어떤 프로시저가 사용하는 데이터가 변경된다면 그 프로시저는 당연히 변경되어야 하고, 해당 프로시저를 사용하는 다른 프로시저들도 영향을 받는다.

객체지향

데이터와 해당 데이터에 대한 기능을 객체로 캡슐화하고, 객체 간에 메시지를  주고받는 방식으로 구현한다.  주고받는 메시지의 프로토콜만 변경되지 않는다면 객체 내부에 캡슐화된 데이터나 기능의 변경은 외부에 영향을 미치지 않는다. 

객체지향 방식은 절차지향 방식에 비해서 어렵다는 단점이 있다. 그래서 Martin Fowler는 Domain Model 방식을 복잡한 시스템을 객체지향 역량이 있는 개발자들이 개발할 때 사용하라고 한다. 근데 문제는 서두에 언급한 것처럼 아무리 단순해 보이고, 다시는 안 쓸 것 같은 시스템도 대부분 복잡해지고, 다시 사용(유지보수를 통해 기능을 변경, 추가하면서)된다는 것이다.

그리고 현업에서 무엇이 변화하여 어려움을 겪는지 고민해 보자. 전혀 없던 종류의 기능(프로시저)이 서비스 오픈 후에 추가되는 경우는 매우 드물다. 오픈 후에는 데이터(화면에 보이거나 입력받는 필드, 데이터베이스 테이블 등)에 대한 변경 요청이 훨씬 더 많다. 따라서 이러한 변경을 캡슐화를 통해 변경을 최소화하도록 하는 것은 객체지향의 중요한 핵심 중 하나이다.

의존성 관리를 통한 독립적인 개발

아래와 같이 게시글을 수정하는 클래스들 있다고 가정하자.

- Domain 레이어(패키지)의 UpdateArticleService는 같은 레이어의 ArticleRepository#findById를 호출하여 수정하려는 Article을 얻는다.

- UpdateArticleService는 Article#update를 호출하여 수정을 위임한다.

- UpdateArticleService는 수정된 결과를 저장하기 위해 ArticleRepository#save를 호출한다.

* 분홍색의 RT는 런타임 의존성을, 녹색의 Compile은 컴파일 타임 의존성을 의미한다. 


ArticleRepository는 인터페이스이고 이에 대한 구현체는 persistency 레이어에서 구현한다. 실제 제어의 흐름은 UpdateArticleService에서 ArticleRepositoryORMImpl로 흐르지만 컴파일 타임 의존성은 역전되어 있다(ArticleRepositoryORMImpl가 Domain 레이어의 ArticleRepository에 의존한다). 이것을 DIP(Dependency Inversion Principle)이라고 한다.

DIP는

High Level Policy(Domain 레이어의 로직 - UpdateArticleService, Article 등에 존재하는)는 Low Level Detail(Persistency 레이어의 구현체)에 의존성을 갖지 말아야 한다. 
Low Level Detail이 High Level Policy에 의존성을 가져야 한다.

는 것을 의미한다.

위의 예에서 게시글 수정 유스케이스를 담당하는 UpdateArticleService(high level policy)는 데이터베이스에 저장하는 구현체(low level detail)에 의존성을 가지면 안된다. 물론 High Level Policy는 결국엔 Low Level Detail을 호출한다.


이게 뭐 그리  중요할까?


DIP를 통해서 우리는 우리에게 중요한 업무 로직인 UpdateArticleService, Article을 자주 변경될 수 있는 persistency 레이어로부터 보호할 수 있어서 유지보수의 안정성을 얻는다. ORM 구현체가 변경되더라도 우리의 업무 로직은 재사용될 수 있다는 것이다. 이것이 진정한 객체지향의 재사용일 것이다.


DIP를 통해 우리는 어떠한 인터페이스가 존재한다고 가정하고 그 인터페이스의 구현체가 완료될 때까지 기다리지 않고 점진적으로 필요로 하는 인터페이스를 발견해 나가면서 개발을 진행할 수 있다. 즉, ArticleRepositoryORMImpl이 구현되지 않았더라도 UpdateArticleService, Article의 구현을 진행, 완료할 수 있다는 말이다. 이 말은 내가 아닌 다른 사람이 ArticleRepositoryORMImpl를 구현해 되고, 동시에 개발을 진행할 수도 있다는 것을 의미한다. 다시 말해 독립적인 개발(뿐만 아니라 배포도)이 가능하다는 것이다. TDD를 한다면 Mocking을 통해 구현체 없는 인터페이스만으로 개발을 계속 진행할 수 있어서 Context Switching(Domain을 구현하다 멈추고 Persistency를 구현하고 다시 Domain으로 돌아오는)의 피로를 피할 수도 있다.

결론

이상에서 캡슐화, 의존성 관리를 통해 객체지향이 필요한 이유를 적어봤다. 오랜 기간 서비스를 운영하며 많은 변경을 해야 하는 입장에서 읽기 좋고, 기능을 추가/변경하기 쉬운 코드는 정말 매력적이다. 위의 2가지만 지켜도 그런 코드를 만드는데 큰 도움이 될 것이라고 생각한다.

작가의 이전글 능숙자 vs 전문가
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari