DesignPattern 적용하기 #4 프로토 타입 & 빌더 패턴

강철진·2023년 3월 29일
0

DesignPattern

목록 보기
5/5
post-thumbnail

배경

고객사는 원하는 고객을 타겟팅하기 위해 세그먼트를 설정합니다. 세그먼트에 따라 비교하는 방식은 크게 3가지(문자열, 숫자, 날짜 타입)가 존재합니다. 상품 주문 수 세그먼트라면 숫자타입의 비교가 될 것이고 방문 페이지 URL 이라면 문자열 타입이 될 것입니다. 이러한 로직은 어떤 세그먼트라도 동일한 로직을 가지지만 특정 고객사에 따라 비교 프로세스를 재정의 해야하는 경우도 염두해야합니다.

기존 코드의 문제점

@Service
public class CampaignProcess {
	...
    public boolean isSegment1 {
    	...
        String[] userActionData = 실제 고객 데이터 
        String[] ruleVal = 어드민에 설정한 룰 데이터(문자열)
        ...
        문자열 비교 로직
        ...
        비교 결과 리턴
    }
    
    public boolean isSegment2 {
    	String[] userActionData = 실제 고객 데이터 
        String[] ruleVal = 어드민에 설정한 룰 데이터(문자열)
        ...
        문자열 비교 로직
        ...
        비교 결과 리턴
    }
    
    public boolean isSegment3 {
    	int[] userActionData = 실제 고객 데이터 
        int[] ruleVal = 어드민에 설정한 룰 데이터(숫자)
        ...
        숫자 비교 로직
        ...
        비교 결과 리턴
    }
    ...
}

데이터를 불러와서 해당 데이터가 존재하는지 검증하는 로직이 공통화가 제대로 이루어지지 않아있어서 비교를 하는 로직에서 중복코드가 많이 존재하게 됩니다.

저는 이러한 중복코드를 제거하고자 값 비교 로직을 각 타입 별로 따로 빈으로 등록하여 관리하고자 했습니다. 다만 특정 고객사는 특정 타입 로직을 재정의할 수 있어야하며 재정의되지 않은 타입은 기존 프로세스를 실행하기위해 프로토 타입과 빌더 패턴을 활용했습니다.

프로토 타입 패턴

프로토타입은 코드를 그들의 클래스들에 의존시키지 않고 기존 객체들을 복사할 수 있도록 하는 생성 디자인 패턴입니다.

빌더 패턴

빌더는 복잡한 객체들을 단계별로 생성할 수 있도록 하는 생성 디자인 패턴입니다. 이 패턴을 사용하면 같은 제작 코드를 사용하여 객체의 다양한 유형들과 표현을 제작할 수 있습니다.

Class Diagram

프로토 타입 & 빌더 패턴 주요 코드

CompareProcess.java

public interface CompareProcess extends Type1Process, Type2Process, Type3Process {
}

Type1ProcessImpl.java

@Component
public class Type1ProcessImpl implements Type1Process {
    @Override
    public boolean isType1Pass(String ruleVal, String userVal) {
        return false;
    }
}

AbstractCompareProcess.java

public abstract class AbstractCompareProcess implements CompareProcess {
    protected final CompareProcessBeanFactory compareProcessBeanFactory;

    protected AbstractCompareProcess(CompareProcessBeanFactory compareProcessBeanFactory) {
        this.compareProcessBeanFactory = compareProcessBeanFactory;
    }

    @Override
    public boolean isType1Pass(String ruleVal, String userVal) {
        return compareProcessBeanFactory.getType1ProcessImpl().isType1Pass(ruleVal, userVal);
    }

    @Override
    public boolean isType2Pass(double ruleVal, double userVal) {
        return compareProcessBeanFactory.getType2ProcessImpl().isType2Pass(ruleVal, userVal);
    }

    @Override
    public boolean isType3Pass(LocalDate ruleVal, LocalDate userVal) {
        return compareProcessBeanFactory.getType3ProcessImpl().isType3Pass(ruleVal, userVal);
    }
}

CompareProcessBeanFactory.java

@Component
public class CompareProcessBeanFactory {
    private final Type1Process type1ProcessImpl;
    private final Type2Process type2ProcessImpl;
    private final Type3Process type3ProcessImpl;


    private CompareProcessBeanFactory(
            Type1Process type1ProcessImpl
            , Type2Process type2ProcessImpl
            , Type3Process type3ProcessImpl) {
        this.type1ProcessImpl = type1ProcessImpl;
        this.type2ProcessImpl = type2ProcessImpl;
        this.type3ProcessImpl = type3ProcessImpl;
    }

    public Type1Process getType1ProcessImpl() {
        return this.type1ProcessImpl;
    }

    public Type2Process getType2ProcessImpl() {
        return this.type2ProcessImpl;
    }

    public Type3Process getType3ProcessImpl() {
        return this.type3ProcessImpl;
    }

    public CompareProcessBeanFactoryBuilder builder() {
        return new CompareProcessBeanFactoryBuilder(
                this.type1ProcessImpl, this.type2ProcessImpl, this.type3ProcessImpl);
    }


    public static class CompareProcessBeanFactoryBuilder {
        private Type1Process type1ProcessImpl;
        private Type2Process type2ProcessImpl;
        private Type3Process type3ProcessImpl;

        public CompareProcessBeanFactoryBuilder(
                Type1Process type1ProcessImpl
                , Type2Process type2ProcessImpl
                , Type3Process type3ProcessImpl) {
            this.type1ProcessImpl = type1ProcessImpl;
            this.type2ProcessImpl = type2ProcessImpl;
            this.type3ProcessImpl = type3ProcessImpl;
        }

        public CompareProcessBeanFactoryBuilder setType1ProcessImpl(Type1Process type1ProcessImpl) {
            this.type1ProcessImpl = type1ProcessImpl;
            return this;
        }

        public CompareProcessBeanFactoryBuilder setType2ProcessImpl(Type2Process type2ProcessImpl) {
            this.type2ProcessImpl = type2ProcessImpl;
            return this;
        }

        public CompareProcessBeanFactoryBuilder setType2ProcessImpl(Type3Process type3ProcessImpl) {
            this.type3ProcessImpl = type3ProcessImpl;
            return this;
        }

        public CompareProcessBeanFactory build() {
            return new CompareProcessBeanFactory(
                    this.type1ProcessImpl, this.type2ProcessImpl, this.type3ProcessImpl);
        }
    }
}

각 타입별 프로세스는 각자의 컴포넌트로 관리합니다. 그래서 만약에 새로운 타입이 생기면 해당 프로세스 또한 새로운 클래스로 정의하여 관리하도록 했습니다. 하지만 클래스가 늘어나면 해당 프로세스를 사용하는 클래스에서 사용하기 위해 빈을 정의하는 코드가 계속 늘어나게 됩니다.

따라서 이러한 타입 프로세스를 한 군데서 관리하기 위해 팩토리 클래스를 두었습니다. 또한 해당 프로세스는 필요한 경우 재정의가 필요하지만 스프링은 싱글톤으로 관리됩니다. 그래서 재정의되는 프로세스의 경우 기존 프로세스 로직과 별개로 생성해서 관리되어야합니다.

이를 위해 프로토타입 패턴을 이용(객체 복사)하여 해당 고객사의 로직에서만 재정의해서 사용할 수 있도록 했고, 재정의되지 않은 타입의 프로세스의 경우 빌더패턴을 통해 기존 프로세스 로직을 가질 수 있게 했습니다.

각 타입별 프로세스를 분리한 덕에 불필요한 코드가 제거되고 필요한 비교 프로세스 객체를 팩토리 클래스에서 빼서 쓰면됩니다.

profile
자바/스프링 백엔드 개발자입니다.

0개의 댓글