주요글: 도커 시작하기
반응형
객체 지향을 하는 이유는? 앞서 '객체 지향 기초 이야기 1, 객체(http://javacan.tistory.com/entry/OO-Basic-1-Object)' 글에서 유지보수 비용을 절감하기 위해 객체 지향적으로 개발한다고 했던 것을 기억할 것이다. 유지보수 비용을 줄일 수 있다는 것은 기능을 쉽게 추가하거나 변경할 수 있다는 것을 의미하는데, 이는 다음의 두 가지를 통해서 얻을 수 있다.
  • 캡슐화: 내부의 구현이 변경되더라도 외부에 영향을 최소화
  • 다형성: 구현체 변경의 유현함을 제공
이번에 살펴볼 내용은 위 두 가지 중 다형성이 주는 유연함에 대한 것이다.

유연하지 않은 코드


유연한 코드에 대해 살펴보기 전에 먼저 유연하지 않은 코드를 살펴보자. 앞서 예제들에서 사용했던 로그 수집을 생각해보자. 최초에 로그 처리는 FTP로부터 파일을 읽어오는 요구만 있었다. 그래서 로그 처리 과정을 제어하는 객체는 FTP로부터 로그를 다운받아오는 FtpLogCollector를 사용해서 구현하였다.


// 흐름 제어

FtpLogCollector logCollector = new FtpLogCollector(....);

FtpLogSet logSet = logCollector.collect();

....


그런데, 요구 사항이 갑자기 바뀌었다. FTP가 아닌 DB에 쌓여 있는 데이터를 로그로 사용해야 한다는 것이었다. 이제 해야 하는 일은? DB에서 로그를 읽어오는 객체를 구현하고 FtpLogCollector 클래스가 출현하는 부분을 모두 DbLogCollector로 바꿔주는 것이다.


DbLogCollector logCollector = new DbLogCollector(....);

DbLogSet logSet = logCollector.collect();

....


로그의 수집 방식이 바뀌면, 로구를 수집하는 구현 클래스가 바뀌는 것 뿐만 아니라 수집 기능을 사용하는 코드도 바뀌게 된다. 즉, 하나의 변화가 다른 코드의 변화를 유발시키는 것이다. 이런 걸 당해본 개발자들은 알겠지만, 이런 종류의 일은 어딜 변경해야 하는지 코드를 검색해야 하고, 노동 집약적인 방식으로 일을 하도록 만든다.


이런 일이 벌어지는 이유는 추상화된 타입이 아닌 실제 구현체를 이용해서 코드를 만들었기 때문이다.


구현체(implementation)가 아닌 인터페이스(interface)에 대고 프로그래밍하기


객체 지향의 기본 규칙 중에 '구현체가 아닌 인터페이스에 대고 프로그래밍'하라는 규칙이 있다. 이 규칙이 나온 이유는 바로 유연함 때문이다. 앞서 봤듯이 추상화된 인터페이스 타입이 아닌 실제 구현을 제공하는 타입을 이용해서 프로그래밍 할 경우, 구현에 변화가 생겼을 때 변화의 영향력이 이곳 저곳으로 퍼져 나가게 된다.


변화가 발생하는 부분이 있다면, 이 부분을 추상화해서 인터페이스 타입을 이용해서 프로그래밍 해 주어야 한다. 인터페이스 타입을 사용해서 구현하게 되면, 실제 구현 객체가 변경되더라도 인터페이스 타입을 이용해서 구현한 코드는 영향을 받지 않게 된다. 예를 들어, 아래는 FtpLogCollector 대신 로그 수집을 추상화한 LogCollector 인터페이스에 대고 프로그래밍을 한 코드이다.


LogCollector logCollector = ... // LogCollector 구현 객체를 어디선가 구함

LogSet logSet = logCollector.collect();

...


LogCollector의 실제 구현 객체를 구하는 부분은 일단 잊어두고 나머지 코드를 살펴보자. 로그 수집하는 방식이 FTP에서 DB로 바뀌더라도 위 코드는 바뀌지 않는다. DB 뿐만 아니라 어떤 방식으로 바뀌더라도, 심지어 FTP와 DB에서 모두 로그를 읽어오도록 구현하더라도 위 코드는 바뀌지 않는다. 즉, 인터페이스에 대고 프로그래밍함으로써 실제 사용되는 구현 객체를 쉽게 변경할 수 있는 유연함을 얻게 된 것이다.


위 코드에서 바뀌는 부분은 실제 LogCollector 구현 객체를 생성하는 부분이다. 실제 사용할 구현 객체를 구하는  부분은 Factory 패턴을 사용하거나 DI(Dependecy Injection)을 통해서 코드 변경을 최소화할 수 있는데, 이 두 가지에 대한 내용은 본 글의 범위를 벗어나므로 패턴 관련 서적 등을 참고하기 바란다.


하지만, 유연함을 얻는 과정은 간단하지 않다. 먼저 실제 구현 객체를 추상화한 인터페이스를 도출해내야 한다. 처음 객체 지향을 접하는 사람들이 어려워 하는 것 중의 하나가 객체를 알맞게 추상화한 인터페이스를 도출하는 것이기 때문에, 유연한 코드를 얻어 내려면 기능(객체)을 추상화하는 연습을 많이 해 주어야 한다.


인터페이스를 도출하기 위한 기본적인 방법은 구현 클래스의 public 메서드를 인터페이스로 변경해보는 것이다. public 메서드는 외부에 기능을 제공할 목적으로 만들어지는 경우가 많기 때문에 인터페이스 후보가 될 수 있다. 내부의 private 메서드 중에서도 인터페이스 후보가 나올 수 있다. private 메서드는 클래스 내부에서 사용되는 기능을 분리한 경우가 많은데, 이 경우 private 메서드를 별도의 인터페이스로 분리함으로써 해당 기능에 대한 유연함을 얻을 수 있게 된다.


인터페이스를 항상 만들어야 하나?


당장 발생하지도 않을 변화를 준비하느라 미리 앞서 인터페이스를 사용할 필요는 없다. 실제로 모든 구현 타입에 대해 인터페이스 타입을 만드는 것은 매우 성가시면서도 불필요한 일이 될 수 있다. 하지만, 그럼에도 불구하고 필자는 인터페이스를 최대한 도출해내려고 노력하는데, 그 이유 중 하나는 단위 테스트에 필요한 Mock 테스트를 할 수 있도록 하기 위함이다.


구현을 하다보면 다른 기능이 구현되지 않아서 현재 작업중인 코드를 테스트 할 수 없는 경우가 발생하는데, 이 경우 Mock 객체를 사용하면 다른 기능의 구현이 되어 있지 않더라도 작업중인 코드를 어느 정도 테스트 할 수 있게 된다. 다른 기능이 만약 인터페이스로 추상화되어 있다면 여러 Mock 라이브러리를 사용해서 다른 기능을 가짜로 제공할 수 있게 되며, 이는 다른 기능의 구현이 완성되어 있지 않더라도 현재 기능을 테스트 할 수 있다는 것을 의미한다.


관련글

참고자료


제가 쓴 객체 지향 입문서입니다.


http://www.aladin.co.kr/shop/wproduct.aspx?ISBN=8969090010  에서 확인하실 수 있습니다.


+ Recent posts