— 2 min read
지난 글 specification pattern (1) 개념과 구현 에 이어서 구체적인 여러 예시들을 통해 어떤 상황에서 이 패턴을 활용하기 좋은지 살펴보려고 한다. 그리고 패턴을 코드에 적용해보려고 할 때의 여러가지 구현방법들에 대해서도 소개할 예정이다.
객체가 어떤 요건을 충족시키거나 특정 목적으로 사용할 수 있는지 가늠하고자 객체를 검증할 때.
예시>
특정한 조건을 만족하는 컬렉션 내의 객체를 선택할 때.
예시>
특정한 요구사항을 만족하는 새로운 객체의 생성을 명시할 때. 아직 존재하지 않는 객체에 대해 명시적으로 생성 규칙을 설정하여 새로운 객체를 만들어내거나 재구성하는 경우.
예시>
⇒ 위 세가지 케이스들은 엄밀하게 구분되지 않기도 한다. 비즈니스 요구사항에 따라 검증을 위해 만들어놓은 specification을 다른 기능을 위해 생성 용도로 활용할 수도 있다. 개념적으로 도메인에 대한 규칙이란 점에선 동일하기 때문이다. 그러나 specification을 사용하지 않는다면 동일한 규칙임에도 불구하고 각기 다른 구현 방식으로 표현하게 될 수도 있다.
내 코드에 써먹어보자- 라고 마음먹었다면 단계적으로 비용이 적게 드는 구현부터 시작해볼 수 있다. 명백하게 자주 사용될 경우라면 처음부터 풀 스펙을 다 구현해놓고 적용을 할수도 있지만, 요구사항 구현으로 바쁜 상황에서 쉽지 않을 수 있으니.. 😇
java의 경우 Specification interface와 유사한 Predicate라는 함수형 인터페이스를 사용해서 비슷하게 구현이 가능하다.
Predicate 인터페이스 기본 메서드:
boolean test(T t)
→ isSatisfiedBy
대신 쓸 수 있다.
default Predicate<T> and(Predicate<? super T> other)
default Predicate<T> or(Predicate<? super T> other)
default Predicate<T> negate()
→ composite로 specification 구현할 때 사용되는 함수들이 기본메서드로 구현되어 있어 쉽게 활용하기 좋다.
예시>>
1Predicate<Customer> isSenior = customer -> customer.getAge() >= 60;2Predicate<Customer> isVip = Customer::isVip;3Predicate<Customer> isSeniorOrVip = isSenior.or(isVip);45/* client 코드 */6if (isSeniorOrVip.test(customer1)) {7 // 조건을 만족할 때 실행할 로직8}
그럼 Specification을 구현하지 않고 그냥 이 Predicate를 쓰면 될일 아닌가? 라는 생각이 들 수 있지만 두가지 단점이 있다.
specification 인터페이스를 구현하면서 할 수 있는 가장 쉬운 방법은 필요한 스펙만 하드코딩하는 것이다.
1interface StorageSpecification {2 boolean isSatisfiedBy(Container aContainer);3}
식품 보관창고에 대한 검증을 해야하는 예시:
고기는 -4도 이하의 식품위생용 컨테이너에 보관한다
1public class MeatStorageSpecification implements StorageSpecification {2 @Override3 public boolean isSatisfiedBy(Container aContainer) {4 return aContainer.canMaintainTemperatureBelow(-4) && aContainer.isSanitaryForFood();5 }6}
⇒ 장점: 쉽고 적은 비용
⇒ 단점: 변경에 취약하다
하드코딩은 사실 좀 너무했다… Specification 클래스에 파라미터를 추가해보자.
고기는 -4도 이하의 식품위생용 컨테이너에 보관한다
1public class CargoStorageSpecification implements StorageSpecification {2 private final int maxTemp;3 private final boolean isSanitaryForFood;45 public CargoStorageSpecification(int maxTemp, boolean isSanitaryForFood) {6 this.maxTemp = maxTemp;7 this.isSanitaryForFood = isSanitaryForFood;8 }910 @Override11 public boolean isSatisfiedBy(Container aContainer) {12 boolean tempCheck = aContainer.canMaintainTemperatureBelow(maxTemp);13 boolean sanitationCheck = isSanitaryForFood ? aContainer.isSanitaryForFood() : true;14 return tempCheck && sanitationCheck;15 }16}1718/* specification 생성 코드 */19StorageSpecification meatStorage = new CargoStorageSpecification(4, true);
지난 글에서 소개했던 방식이다. 각각의 조건/제약사항마다 specification클래스를 만들고, 디자인 패턴 중 composite pattern을 활용하여 결합한다.
.
1/** 하나의 조건마다 Leaf Specification 클래스를 만든다 **/ 2public class MaximumTemperatureSpecification implements Specification<Container> {3 private final int maxTemp;45 public MaximumTemperatureSpecification(int maxTemp) {6 this.maxTemp = maxTemp;7 }89 @Override10 public boolean isSatisfiedBy(Container container) {11 return container.canMaintainTemperatureBelow(maxTemp);12 }13}1415public class SanitaryForFoodSpecification implements Specification<Container> {1617 @Override18 public boolean isSatisfiedBy(Container container) {19 return container.isSanitaryForFood();20 }21}2223/** Composite Specification은 leaf를 가지고 있다. **/24public class CompositeSpecification<T> implements Specification<T> {25 private final List<Specification<T>> components = new ArrayList<>();2627 public CompositeSpecification<T> with(Specification<T> specification) {28 components.add(specification);29 return this;30 }3132 @Override33 public boolean isSatisfiedBy(T candidate) {34 for (Specification<T> each : components) {35 if (!each.isSatisfiedBy(candidate)) {36 return false;37 }38 }39 return true; // 모든 조건을 만족하면 true 반환40 }41}
위 예시는 모든 조건이 다 만족해야 isSatisfiedBy
가 true를 반환한다.
더 유연하게, 조건이 한개만 만족해도 true를 반환하는 등 다른 논리연산자들을 통해 specification을 결합할 수도 있다.
1public abstract class Specification<T> {2 public abstract boolean isSatisfiedBy(T candidate);34 public Specification<T> and(Specification<T> other) {5 // 모든 조건을 만족하면 true 반환6 return new ConjunctionSpecification<>(this, other);7 }89 public Specification<T> or(Specification<T> other) {10 // 여기선 하나만 만족해도 true를 반환11 return new DisjunctionSpecification<>(this, other); 12 }13}
핵심적으로 Specification은 어떤 객체를 선택할 것인지에 대한 선언과, 선택을 하는 객체를 분리하는 패턴이다. 이 선언적이고 명시적인 정의 가 필요하거나, 객체에 대한 제약조건/요구사항으로 인해 객체가 하는 역할이 잘 보이지 않게 될 수 있는 경우 활용하는게 좋을거라 생각된다.
특히 composite specification을 활용하면 요구사항이 추가되었을 때 객체를 변경하는 것이 아니라, 새로운 specification을 추가하는 방식을 쓸 수 있다. 객체지향의 SRP(변경의 이유는 한가지여야한다)와 OCP(확장에 열려있고 수정에는 닫혀야한다) 원칙과도 연관이 된다. 구현에 활용해보려한다면 당장 필요한 요구사항에서부터 출발해서 점진적으로 확장해볼 수 있다.