고객사는 자신들이 원하는 조건으로 고객을 타겟팅하기 위해 그루비 솔루션을 사용하여 세그먼트를 설정합니다. 그리고 그러한 조건은 사용 브라우저, 현재 상품의 이름, 현재 페이지 URL 등 다양한 조건들이 있습니다. 다양한 세그먼트가 존재하지만 결국 알고자 하는 내용은 해당 고객이 해당 세그먼트 조건을 만족하느냐 입니다.
private Object check(parameter1, parameter2) {
Class cls = ...
Mehtod method = ...
return method.invoke();
...
return 0;
}
보시면 아시겠지만 자바의 리플랙션을 활용하여 구현되어있습니다. 간단히 리플랙션을 설명하자면 구체적인 클래스 타입을 알지 못하더라도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API 입니다. 리플랙션에 대해서는 설명하자면 너무 길어지기 때문에 추후 시간이 되면 정리해서 블로그에 올리도록 하겠습니다.
어쨋든 제가 아는 리플랙션은 어떠한 클래스를 사용할지 모를 때 사용해야 의도에 맞게 사용하는 것입니다. 그러나 현재 세그먼트는 각자 고유의 코드 값을 DB에 적재하고 있기때문에 어떤 클래스의 어떤 메서드를 호출해야할지 알 수 있습니다.
그러나 리플랙션을 이용해 패키지 이름과 클래스 이름, 메서드 이름을 참조하여 객체 생성 및 해당 메서드를 호출합니다. 심지어 className 과 methodName 은 DB 를 참조하여 실행합니다.
이렇게 되면 패키지가 변경되거나, 혹은 클래스 이름, 메서드 이름을 마음대로 변경할 수 없습니다. 해당 로직은 DB 의존적인 코드가 된다는 얘기입니다. 또한 어떤 메서드가 어디서 사용되는지 추적이 어렵습니다. 이러한 문제를 해결하기 위해 전략패턴을 사용하여 리플랙션 코드를 걷어내고자 했습니다.
전략 패턴은 알고리즘들의 패밀리를 정의하고, 각 패밀리를 별도의 클래스에 넣은 후 그들의 객체들을 상호교환할 수 있도록 하는 행동 디자인 패턴입니다.
전체적인 코드 작성 흐름은 다음과 같습니다.
public interface CommonStrategy {
CheckType getCheckType();
}
public interface ATypeStrategy extends CommonStrategy {
boolean execute();
CheckType getCheckType();
}
public interface BTypeStrategy extends CommonStrategy {
boolean execute();
CheckType getCheckType();
}
@Component
public class TypeFactory<T extends CommonStrategy> {
private final Map<CheckType, ATypeStrategy> aTypeStrategyHashMap = new HashMap<>();
private final Map<CheckType, BTypeStrategy> bTypeStrategyHashMap = new HashMap<>();
public TypeFactory(List<T> typeStrategies) {
for (T typeStrategy : typeStrategies) {
if(typeStrategy instanceof ATypeStrategy)
this.aTypeStrategyHashMap.put(typeStrategy.getCheckType(), (ATypeStrategy) typeStrategy);
else if(typeStrategy instanceof BTypeStrategy)
this.bTypeStrategyHashMap.put(typeStrategy.getCheckType(), (BTypeStrategy) typeStrategy);
}
}
public ATypeStrategy getATypeStrategy(CheckType checkType) {
return aTypeStrategyHashMap.get(checkType);
}
public BTypeStrategy getBTypeStrategy(CheckType checkType) {
return bTypeStrategyHashMap.get(checkType);
}
}
public class Segment1 {
protected static final CheckType TYPE = CheckType.SEGMENT1;
@Component
public static class Segment1A implements ATypeStrategy {
@Override
public boolean execute() {
return false;
}
@Override
public CheckType getCheckType() {
return TYPE;
}
}
@Component
public static class SegmentB implements BTypeStrategy {
@Override
public int execute() {
return 0;
}
@Override
public CheckType getCheckType() {
return TYPE;
}
}
}
@Service
public class SegmentCheckProcessImpl implements SegmentCheck {
private final TypeFactory<CommonStrategy> typeFactory; // 빈주입
...
public boolean isATypeSegmentPass(String segmentType) {
CheckType type = CheckType.getCheckType(segmentType);
// 팩토리 클래스에서 A인터페이스를 상속받은 해당 체크타입 빈을 받아 로직 수행
boolean result = typeFactory.getATypeStrategy(type).execute();
return result;
}
public boolean isBTypeSegmentPass(String segmentType) {
CheckType type = CheckType.getCheckType(segmentType);
// 팩토리 클래스에서 B인터페이스를 상속받은 해당 체크타입 빈을 받아 로직 수행
boolean result = typeFactory.getATypeStrategy(type).execute();
return result;
}
...
}
기존 리플랙션 코드를 걷어내고 나니 DB 의존성도 제거되고 코드가 훨씬 간결해졌습니다. 내부 로직은 다르지만 하는 일은 같으므로 전략패턴과 팩토리 메서드 패턴을 사용함으로 이제 하나의 세그먼트에 대해 내부로직을 확인하거나 수정이 있을 때 해당 클래스 파일만 보면 되므로 유지보수에도 유리합니다.
썸네일 이미지 및 학습자료 출처
https://refactoring.guru/ko/design-patterns/strategy