신림프로그래머 모임에 발표할 모델링 연습 리뷰 자료입니다.
발표 자료에 나오는 JPA의 AttributeConverter에 대한 내용은 아래 링크에 정리했습니다.
- http://javacan.tistory.com/entry/How-to-use-JPA-21-by-AttributeConverter-for-custom-value-type
신림프로그래머 모임에 발표할 모델링 연습 리뷰 자료입니다.
발표 자료에 나오는 JPA의 AttributeConverter에 대한 내용은 아래 링크에 정리했습니다.
유연한 코드에 대해 살펴보기 전에 먼저 유연하지 않은 코드를 살펴보자. 앞서 예제들에서 사용했던 로그 수집을 생각해보자. 최초에 로그 처리는 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 에서 확인하실 수 있습니다.
[그림3.18] 데이터를 중심으로 개발되는 절차 지향 방식
근데 Time 클래스에 요구사항이 늘어감에 따라 메소드가 추가되는 것이 보기 않 좋네.
방법이 있지 않나 ?
하나 또 오르는 방법이 있는데... 어찌 생각하나 ?
메서드가 증가가 많지 않을 것 같긴 하지만, 메서드가 계속 증가한다면 다음과 같은 메서드를 추가하면 어떨까 합니다.
long elapsedTime(TimeUnit unit)
이 경우에는 StopWatch 클래스에서 바로 위 클래스를 넣어도 될 듯 합니다. Time 클래스에 넣어야 한다면
long value(TimeUnit unit) 정도의 이름이 좋을 것 같구요.
아니면 Visitor Pattern으로 원하는 timeUnit 별로 elapse Time을 계산하는 객체를 전달하여 계산하도록 하면 어떨까 ?
long getElapsedTime(ElapsedTimeCalculateVisitor visitor) 이런 식으로.
구조체는 새로운 타입 추가시 많은 변경을 유발하지만, 새로운 기능 추가시에는 해당 기능만 추가하면 되고,
클래스는 polymorphism을 활용하여 변경 없이 새로운 타입 추가할 수 있지만, 기존 클래스에 새로운 기능 추가시에는 해당 클래스의 모든 클라이언트와 서브클래스까지 변경되어야 하는 재앙이 발생한다.
그런데 이 같은 클래스의 문제는 Visitor로 해소할 수 있지 않을가 싶다 ^^
너무 많이 가시는 거 같아요. 저도 Visitor와 같은 double dispatch 방식을 생각해보긴 했는데, Visitor가 결국 StopWatch의 내장(시간 저장하기 위한 타입)을 알게 되는 상황이 발생해서 고민이 좀 되더라구요.
그래서 Time을 좀 더 추상화해서 Time 객체가 크기 비교를 할 수 있는 기능을 추가하고, Time이 스스로 자기를 표현할 수 있도록 구현해주는 것도 생각해 볼 만 할 것 같아요.
그런데요,,, 이 논의 거기 팀원들하고 마저 하심 안 되요? 제가 어제 술독에 빠졌다가 나와서 정신이 없어요.
"클래스보다 인터페이스로 프로그래밍하는 것으로 재사용성을 얻을 수 있다. 메소드의 모든 파라미터가 이미 알고 있는 인터페이스에 대한 레퍼런스이고, 그 인터페이스가 전혀 알 수 없는 클래스에 의해 구현된다면, 그 메소드는 코드가 작성될 때 존재하지도 않는 클래스의 객체와도 작동할 수 있다. 기술적으로, 재사용 가능한 것은 메소드이며, 메소드로 전달되는 객체가 아니다."이러한 사항을 1단계의 결과에 적용하면, 어떤 기능의 코드가 독립하여 전역적으로 노출된 프로시져이면, 클래스 타입의 입력 파라미터를 인터페이스 타입으로 바꿈으로서 재사용 가능성을 더 높일 수 있다. 그러면 인터페이스 타입을 구현한 어떤 클래스의 객체도 파라미터로 사용될 수 있게 된다. 따라서, 이 프로시져는 잠재적으로 다수의 객체 타입에 대하여 사용이 가능해진다. 예를 들면, 다음과 같은 전역적으로 노출된 정적 메소드를 생각해보자.