한 번 만들어진 소프트웨어는 얼마나 사용될까? 윈도우 XP는 10년 넘게 사용되고 있고, 포털 사이트의 카페 서비스 역시 10년 넘게 운영을 하고 있다. 여기서 알 수 있는 사실은, 한 번 쓰고 버릴 소프트웨어를 제외한 나머지 다수의 소프트웨어는 개발 기간보다 더 오랜 시간 동안 사용된다는 점이다. 소프트웨어는 살아 있는 생물과도 같아서 지속적으로 변화를 하게 되며, 이는 곧 소프트웨어를 유지하기 위해서는 계속해서 비용이 발생한다는 것을 뜻한다. 소프트웨어를 유지하기 위해 비용이 계속해서 발생하는데, 소프트웨어를 개발하는 시간보다 소프트웨어를 사용하는 시간이 더 길다는 것은, 다시 말해서 소프트웨어를 개발하는 비용보다 소프트웨어를 유지하는 비용이 더 많이 발생한다는 사실을 말해준다.
초기 개발 비용 <<<<<<<< 유지보수 개발 비용
비용을 줄이려면 어떻게 코드를 만들어야 할까? 여러 가지 답이 있겠지만 필자는 다음의 두 가지라고 생각한다.
- 코드 분석이 용이
- 코드 변경이 용이
절차 지향과 객체 지향
절차 지향의 핵심은 데이터를 중심으로 구현을 한다는 데 있다. 각각의 기능을 수행하는 프로시저(함수)들이 데이터를 조작하거나 사용하는 방식으로 구현을 한다.
다양한 프로시저들이 데이터를 공유해서 동작하기 때문에 데이터의 변화는 곧 데이터를 사용하는 코드(프로시저)의 변화로 이어진다. 심지어 한 데이터 타입의 변화가 다른 데이터의 변화를 일으키기는 경우도 있다. 예를 들어, 화폐를 표현하는 데이터의 타입이 int에서 double로 변경되면 화폐를 사용하는 프로시저 뿐만 아니라 잔고, 판매금액 등의 다른 데이터 타입까지도 모두 변하게 된다. 심할 경우, 한 변수의 타입 변화가 수 일이 소용되는 노가다를 만들어내기도 한다. 이는 데이터 중심인 절차 지향으로 구현할 경우 유지보수 비용이 높아진다는 것을 의미한다.
중심에 데이터를 놓고 프로시저를 구현하는 절차 지향과 달리 객체 지향은 데이터와 기능을 함께 담고 있는 객체들이 서로 대화하는 방법으로 구현된다. 객체 간에는 데이터를 (최대한) 주고 받지 않으며, 객체 상호간에 기능을 실행해 달라고 요청하는 방식으로 구현을 하게 된다.
각 객체들은 내부적으로 데이터를 담고 있지만 다른 객체에게 데이터를 노출하지는 않는다. 객체와 객체 간에 데이터가 흐르지 않기 때문에 특정 객체가 담고 있는 데이터에 변화가 생기더라도 해당 객체로만 변화가 한정되며 다른 객체에까지 변하가 전파되지 않게 된다. 이 얘기는 변화에 따른 유지보수 비용이 절차 지향에 비해 상대적으로 더 작다는 것을 의미한다.
프로시저와 객체 지향에 대한 보다 구체적인 예는 필자가 이전에 쓴 '캡슐화 http://javacan.tistory.com/entry/EncapsulationExcerprtFromJavaBook'란 글을 참고하기 바란다.
모든 것을 객체 지향으로 할 필요는 없다. 단순히 한 개의 DB 테이블의 데이터를 읽어서 보여주는 웹 방명록을 생각해보자. 이 기능은 데이터를 중심으로 한 절차 지향으로 구현하는 것이 훨씬 간단하다. 또한 일회성으로 사용될 이벤트 프로그램을 구현하기 위해 객체 지향을 추구할 필요는 없다. 왜냐면 유지보수 비용이 발생하지 않기 때문이다. 하지만, 도메인이 복잡하고 유지보수 기간이 길어질수록 절차 지향 방식은 높은 개발 비용을 발생시키기 때문에, 이런 경우에는 객체 지향적으로 하는 것이 비용측면에서 유리하다. 물론, 코드를 만져야 하는 개발자의 정신 건강 측면에서도 훨씬 유리하다.
객체의 핵심은 기능을 제공하는 것
객체를 가장 간단하게 정의하자면 '기능을 제공하는 것'이라고 할 수 있다. 조금 더 길게 설명하자면 다음과 같다.
- 제공하는 기능으로 정의된다.
- 내부적으로 어떤 데이터를 들고 있고 어떻게 기능을 구현하는 지는 숨긴다.
객체는 기능을 제공한다. 예를 들어, TV라는 객체를 생각해보자. TV는 다음의 기능들을 제공할 것이다.
- 채널 증가
- 채널 감소
- 지정 번호로 채널 변경
- 볼륨 증가
- 볼륨 감소
TV가 내부적으로 볼륨의 크기를 어떤 데이터 타입으로 관리하는지는 중요하지 않고, 단지 TV 객체에 볼륨을 증가시키라고 요청을 할 뿐이다. 이를 코드로 표현하면 다음과 같다.
TV tv = new TV();
tv.increaseVolume(); // 기능을 제공
만약 데이터가 흘러다니도록 구현을 한다면 다음과 같은 코드가 만들어질 것이다.
TV tv = new TV();
int volume = tv.getVolume();
tv.setVolume(volume + 1);
두 코드의 차이는 명확하다. 앞서 첫 번째 코드의 increaseVolume()는 볼륨을 증가시키는 기능을 제공하며, 내부적으로 볼륨의 타입이 변경되더라도 영향을 받지 않는다. 반면에 두 번째 코드는 볼륨 값을 저장하는 타입이 int 임을 노출하고 있으며, 이 데이터에 기대서 볼륨 조절 기능을 구현하고 있다. 두 번째 코드에서 볼륨을 보관하는 데이터 타입이 short이나 byte와 같은 다른 타입으로 변경될 경우 그 데이터를 사용하는 코드가 함께 변경되게 된다.
객체의 역할(또는 책임)과 메시징
객체는 기능을 제공하는 존재이기 때문에, 객체 지향적으로 설계를 한다는 것은 각각의 객체가 어떤 기능을 제공하는지 정의하는 작업이 된다. 또한, 각 객체들이 서로 어떻게 상대방의 기능을 사용하는지를 설계하는 것이 객체 설계가 된다. 이런 의미에서 객체 지향 설계를 할 때 가정 먼저 해야 할 작업은 객체가 어떤 역할을 수행하는 지 결정하는 것이다.
하나의 객체가 모든 기능을 제공하는 것은 유지보수 측면에서 바람직하지 않기 때문에, 각 기능들을 관련된 것들끼리 분리해서 각각의 객체들에 역할을 분배해준다. 예를 들어, 로그 분석하는 시스템을 생각해보자. 이 경우 다음과 같이 객체 별로 역할을 구분해서 할당할 수 있다.
- 로그 수집 객체 객체: 로그를 수집해서 한 곳에 모아준다.
- 로그 분석 객체: 로그 파일로부터 데이터를 추출해서 중간 결과를 생성한다.
- 리포트 생성 객체: 중간 결과로부터 실제 원하는 리포트를 생성한다.
- DB 연동 객체: DB에 데이터를 삽입해주는 기능을 제공한다.
- 흐름 제어 객체: 위 객체들간의 실행 순서를 제어한다.
객체의 역할과 함께 도출되는 것은 객체들 간의 관계이다. 전체 기능이 각각의 객체에 분산되어 있기 때문에, 완성된 기능을 제공하려면 각 객체들이 제공하는 기능을 엮어주어야 한다. 이는 객체 간의 메시징을 통해서 이루어진다. 한 객체는 다른 객체에 기능 실행을 요구하기 위한 요청을 보내는데 이 요청을 보통 메시지라고 표현한다. 다른 객체로부터 메시지를 받은 객체는 메시지에 알맞은 기능을 실행하고 그 결과를 메시지로 보낸다. 자바의 경우 기능 실행을 요청하는 메시지가 메서드 호출로 구현되며, 응답 메시지는 리턴이나 익셉션 등으로 구현된다.
객체의 역할과 메시지로부터 뭔가 떠오르는 것이 있을 것이다. 그것은 바로 커뮤니케이션 다이어그램이다. 커뮤니케이션 다이어그램은 객체 간의 메시지를 주고 받는 과정을 표현하기 위한 다이어그램으로, 이는 객체 지향 설계의 기본이 된다. 실제로 객체를 설계한다는 것은 다음을 반복하는 과정이다.
- 기능들을 제공할 객체 후보를 선별한다.
- 각 객체들이 어떻게 메시지를 주고 받는 지 연결한다.
- 안 좋은 냄새가 나면 위 단계를 되풀이한다.
좋은 객체를 만드는 기본 규칙: 데이터 말고 기능 제공해 줘!
좋은 객체가 한 번에 나오면 좋겠지만, 설계란 그리 호락호락하지 않음을 누구나 알고 있다. 그래서 지속적으로 설계가 바뀌게 되고, 그렇기에 리팩토링이 구현에서 중요한 과정이 되는 것이다. 하지만, 출발점을 좋게 잡을 수는 있다. 그 중 하나가 다음에 따라 객체의 기능을 제공하는 것이다.
- 데이터를 달라고 하지 않고, 해 달라고 하기!
- 데이터 조회 기능은 필요한 경우에 한정해서 제공한다.
위 규칙에 따라 객체를 설계하다보면 자연스럽게 객체들 사이로 데이터가 흘러다니는 것을 줄이고, 객체가 내부 내장을 드러내지 않고 (즉, 캡슐화하고) 알맞은 기능을 제공하도록 만들 확률을 높일 수 있게 된다.
내용 정리
간단하게 내용을 정리해 보자.
- 객체는 기능을 제공하는 역할을 수행하며, 객체의 내부가 어떻게 구현되었는지는 숨긴다.
- 각 객체는 메시지를 주고 받으며 협업하며, 이를 통해 하나의 완성된 기능을 제공한다.
- 객체 지향은 변화의 영향이 주변으로 퍼져나가는 것을 감소시켜 유지보수 비용을 줄여준다.