티스토리 뷰
처음 자바를 배울때 인터페이스, 추상클래스, 구현클래스에 대해서 각각 이렇게 배우고, 암기하게된다.
* 인터페이스 : 메서드의 몸통을 가질 수 없음. 추상메서드와 상수만 보유가능.
* 추상클래스 : 구현메서드와 추상메서드를 모두 가질 수 있음. 단독으로 객체생성 불가능.
* 구현클래스 : 구현메서드만 가질 수 있음. 인터페이스를 구현하거나 추상클래스를 상속받은 경우 해당 추상메서드를 반드시 구현해야 함.
자바를 처음 배울때는 보통 추상 클래스 혹은 구현 클래스를 이용해 중복 코드를 상위 클래스로 뽑아내고 하위 클래스들에서 재사용하는것에 큰 매력을 느끼게되고, 구현부따위없는 인터페이스에는 그다지 매력을 못느끼는 경우가 많다.
코드를 재사용할 수 있는것도 아니고 그저 메서드 시그니처만 강제하는게 무슨 의미가있다는건가? 라고 의문을 가지게 되는것이다. 나도 자바를 처음 접할때 가졌던 의문이고, 같이 스터디를 하는 초보개발자들도 한번쯤 생각해보게 만들었던 부분이다.
하지만 인터페이스는 중복코드 제거보다는 다형성의 관점에서 봐야하는데 꽤 예전에 회사에서 코드를 짜다가 개인적으로 다형성의 이로움(?)에 대해 크게 감명받은적이 있어서 그 내용을 토대로 포스팅하려한다. 참고로 코드자체가 난이도가 높다거나 하지는 않다.
1. 쿠폰 발급 기능 추가
기존에 우리가 해오고있던 프로젝트가 쇼핑몰이라고 가정하자. 이번에 쿠폰 발급 기능을 추가하려한다. 회원들은 로그인 후 특정 쿠폰을 발급받을 수 있다. 쿠폰에는 발급가능한 기간이 있고, 회원은 그 기간 내에 발급을 받을 수 있다. 발급 조건 유효성 검사를 하는 클래스를 만들어보자.
public class PublishValidator {
public boolean validate(CouponPublishRequest request){
// 발급가능 유효기간
if(request.isPublishableDateTime()){
return true;
}
return false;
}
}
@Test
public void validate() throws Exception {
this.request = new CouponPublishRequest(new Member(),
new Coupon(new PublishPeriod(makeTodayMinusDay(15), makeTodayPlusDay(15)), false), makeToday());
assertThat(this.publishValidator.validate(this.request), is(true));
}
유효성 검사 클래스를 만들고, 테스트 케이스도 작성했다. CouponPublishRequest 안에 쿠폰의 정보와 발급 요청날짜들이 모두 들어있어서 스스로 처리하게끔 구현했는데, 어느날 또다른 유효성 검사가 추가됐다. 쿠폰의 총 발급 개수를 정해놓고 확인해야한다고 한다. 즉 100장 발급가능한 쿠폰을 회원이 발급요청했을때 100장이 전부 발급된상태인지를 체크해야한다는 것이다. if 하나 추가하면 큰 문제없을것같다.
public class PublishValidator {
public boolean validate(CouponPublishRequest request){
// 발급가능 유효기간
if(!request.isPublishableDateTime()){
return false;
}
// 쿠폰 잔여 수량
if(!request.isPublishableRemainCount()){
return false;
}
return true;
}
}
잔여수량도 체크하게끔 분기문을 추가했다. 소소하게 바뀐부분은 아까는 조건이 맞으면 true를 반환했는데 이제는 조건이 안맞으면 false를 반환하고, 마지막에 true를 반환하게끔 변경했다. 여러 조건을 체크해야니 어쩔 수 없는 변경이었다. 테스트 케이스도 만들었다.
@Test
public void validate_발급가능쿠폰개수() throws Exception {
Coupon coupon = new Coupon(new PublishPeriod(makeTodayMinusDay(15), makeTodayPlusDay(15)), false, 100);
this.request = new CouponPublishRequest(new Member(), coupon, makeToday());
coupon.publishing();
assertThat(this.publishValidator.validate(this.request), is(true));
}
@Test
public void validate_발급불가쿠폰개수() throws Exception {
long totalPublishableCount = 100;
Coupon coupon = new Coupon(new PublishPeriod(makeTodayMinusDay(15), makeTodayPlusDay(15)), false, totalPublishableCount);
this.request = new CouponPublishRequest(new Member(), coupon, makeToday());
for(int i = 0; i < totalPublishableCount; i++) {
coupon.publishing();
}
assertThat(this.publishValidator.validate(this.request), is(false));
}
곰곰히 생각해보자. 모든 조건이 Validator 안에 들어있게되고 조건이 추가되거나 삭제, 변경될때마다 validator를 수정하게된다. 이미 이 자체로 OCP(Open Close Principle, 개방 폐쇄 원칙), SRP(Single Responsibility Principle, 단일 책임 원칙)이 위반되었다. validator 클래스는 자기가 들고있는 수많은 if문중 그 어떤것이 변경되어도 변경되는 클래스가 되어버렸고 조건이 추가되거나 삭제될때도 스스로 변경되는 클래스가 되어버렸다.
테스팅 관점에서 생각해보자. 각각의 분기문이 서로 순서 종속을 갖게됨으로서 분기마다 테스트하는게 매우 어렵게되었다. 뒤에있는 if문을 테스트하기위해서는 일단 앞에있는 if문들을 전부 신경써줘야하기 때문이다.
2. 분리
이 조건들을 다 쪼갤생각이다. 어떻게 바꾸면좋을까?
public interface PublishCheckable {
boolean check(CouponPublishRequest request);
}
일단 유효성 검사를 할 인터페이스를 분리했다.
public class DateTimeChecker implements PublishCheckable {
@Override
public boolean check(CouponPublishRequest request) {
return request.isPublishableDateTime();
}
}
public class RemainCountChecker implements PublishCheckable {
@Override
public boolean check(CouponPublishRequest request) {
return request.isPublishableRemainCount();
}
}
각 조건들마다 별도의 구현클래스로 구현했다. 클래스명에서부터 어떤 조건을 체크하겠다는건지가 확연히보이고 메서드가 짧아 가독성이 확실히 증진된것이 보인다.
분리한건 알겠다. 그럼 얘네를 어떻게 호출하면 될것인가?
public class CouponPublishCheckContainer implements PublishCheckable {
private final List<PublishCheckable> checkables;
public CouponPublishCheckContainer(PublishCheckable... checkables){
this.checkables = Collections.unmodifiableList(Arrays.asList(checkables));
}
@Override
public boolean check(CouponPublishRequest request) {
for(PublishCheckable checkable : this.checkables){
if(!checkable.check(request)){
return false;
}
}
return true;
}
}
모든 조건들을 들고있을 컨테이너 구현체를 하나 만들었다. 컨테이너 구현체는 내부에서 컬렉션에 조건 클래스들을 담고 하나씩 반복적으로 체크하게된다. 어차피 동일한 인터페이스를 구현했기때문에 클라이언트쪽에서는 이게 조건인지 컨테이너인지 구현클래스가 뭔지는 신경쓸필요없이 인터페이스로 호출하기만 하면 된다.
이제 조건이 추가되거나 삭제하면 단순히 그 조건에 맞는 클래스를 추가구현하거나 삭제하기만 하면 되고, 코드의 수정 이유도 조건 1개뿐이다. 조건별로 클래스로 완전히 분리가 되었기때문에 조건들마다 의존성이 사라졌고, 당연히 테스트 코드를 짜기에도 훨씬 간단하다. 더군다나 인터페이스를 이용해 참조함으로서 DIP까지 지키게 됐다.
3. 정리
처음 구현을 클래스 다이어그램으로 표현하면 이렇다.
수정된 설계를 클래스 다이어그램으로 표현하면 이렇다.
이게 정말 올바른 설계이고 잘 구현한건지는 잘 모르겠다. 다만 실제 현업에서 이런식으로 인터페이스를 활용했고, 그 이후 조건의 추가/삭제/변경에 더이상 스트레스 받지않고 편하게 작업하는 스스로를 보면서 인터페이스의 이점이 뭔지 몸으로 느꼈던 코드였기에 블로그에 포스팅으로 남겨놓는다.
'Java' 카테고리의 다른 글
Test#01. jUnit으로 테스트하기 (0) | 2017.08.18 |
---|---|
DDD#01. Domain Object (1) | 2017.07.23 |
Garbage Collector (0) | 2017.07.05 |
DTO와 VO (8) | 2017.07.01 |
jackson custom serializer, deserializer 만들기 (3) | 2017.04.19 |
- Total
- Today
- Yesterday
- generics
- Kotlin
- java
- EffectiveJava
- Spring
- toby
- code
- programming
- DesignPattern
- http
- MySQL
- JavaScript Core
- java8
- servlet
- db
- backend개발환경
- OOP
- JPA
- javascript
- Git
- spring cloud
- go-core
- clean code
- frontcode
- Design Pattern
- TEST
- mariadb
- frontend개발환경
- Jackson
- 정규표현식
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |