[Spring] 팩토리패턴을 활용한 서비스 관리

taeni·2021년 8월 10일
3

비즈니스 로직을 구현할 때 입력받은 값에 따라 일부 로직을 각각 다르게 구현해야 하는 경험을 많이 하게 된다.

필자는 요즘 Clean code에 대해 관심이 많아 위 같은 경우에 대해 어떤 식으로 구현을 하면 좋을지 고민한 끝에 여러 가지 자료를 참조한 끝에 아래와 같이 구현한 정보를 기록하려 한다.

Spring framework 개발환경에서 if문으로 장황스럽게 구현된 코드를 Factory pattern을 활용하여 관리가 용이하도록 변경해보려한다.

아래 코드를 예로 들어보자.
구매한 상품 타입에 따라 포인트 계산식이 달라지는 로직이다.

의류포인트계산 Service

@Service
public class ClothesPointCalculateService {
    public int calculatePoint(PointCalculate pointCalculate) {
        return (int) Math.ceil(pointCalculate.getPrice() * 0.05);
    }
}

식품포인트계산 Service

@Service
public class FoodPointCalculateService {
    public int calculatePoint(PointCalculate pointCalculate) {
        return (int) Math.ceil(pointCalculate.getPrice() * 0.03);
    }
}

전자제품포인트계산 Service

@Service
public class ElectronicsPointCalculateService {
    public int calculatePoint(PointCalculate pointCalculate) {
        return (int) Math.ceil(pointCalculate.getPrice() * 0.10);
    }
}

포인트계산 Test case

@SpringBootTest
class PointCalculateTest {

    private final int PRICE = 10000;

    @Autowired
    private ClothesPointCalculateService clothesPointCalculateService;

    @Autowired
    private FoodPointCalculateService foodPointCalculateService;

    @Autowired
    private ElectronicsPointCalculateService electronicsPointCalculateService;

    @Test
    public void clothesPointCalculate_if_success() {
        assert(calculatePoint(ProductType.CLOTHES) == 500);
    }

    @Test
    public void foodPointCalculate_if_success() {
        assert(calculatePoint(ProductType.FOOD) == 300);
    }

    @Test
    public void electronicsPointCalculate_if_success() {
        assert(calculatePoint(ProductType.ELECTRONICS) == 1000);
    }

    private int calculatePoint(ProductType productType) {
        PointCalculate pointCalculate = PointCalculate.builder()
                .productType(productType)
                .price(PRICE)
                .build();

        if (productType == ProductType.CLOTHES) {
            return clothesPointCalculateService.calculatePoint(pointCalculate);
        } else if (productType == ProductType.FOOD) {
            return foodPointCalculateService.calculatePoint(pointCalculate);
        } else if (productType == ProductType.ELECTRONICS) {
            return electronicsPointCalculateService.calculatePoint(pointCalculate);
        }

        return 0;
    }
}

포인트계산을 사용하는 곳이 당장은 한 군데 밖에 없어 크게 문제될 것이 없다하더라도, 추후 주문/정산/상품상세 등 다양한 곳에서 사용하게 될 수 있고 상품타입도 추가될 수 있다.
상품타입이 추가되면 아래 if문을 사용하는 곳을 모두 찾아 수정하여야한다.

if (productType == ProductType.CLOTHES) {
	return clothesPointCalculateService.calculatePoint(pointCalculate);
} else if (productType == ProductType.FOOD) {
	return foodPointCalculateService.calculatePoint(pointCalculate);
} else if (productType == ProductType.ELECTRONICS) {
	return electronicsPointCalculateService.calculatePoint(pointCalculate);
}

시스템을 운영하는 데 굉장히 비효율적이므로 위에 예시를 든 코드를 Factory Pattern을 활용하여 개선해보자

우선 포인트계산 Service를 인터페이스로 분리하자.
(기존에 생성해둔 Service와의 이름이 중복되어
class명 앞에 Factory를 붙였다)

public interface PointCalculateService {
    int calculatePoint(PointCalculate pointCalculate);
    ProductType getProductType();
}

@Service
public class FactoryClothesPointCalculateService implements PointCalculateService {

    @Override
    public int calculatePoint(PointCalculate pointCalculate) {
        return (int) Math.ceil(pointCalculate.getPrice() * 0.05);
    }

    @Override
    public ProductType getProductType() {
        return ProductType.CLOTHES;
    }
}
@Service
public class FactoryElectronicsPointCalculateService implements PointCalculateService {

    @Override
    public int calculatePoint(PointCalculate pointCalculate) {
        return (int) Math.ceil(pointCalculate.getPrice() * 0.10);
    }

    @Override
    public ProductType getProductType() {
        return ProductType.ELECTRONICS;
    }
}
@Service
public class FactoryFoodPointCalculateService implements PointCalculateService {

    @Override
    public int calculatePoint(PointCalculate pointCalculate) {
        return (int) Math.ceil(pointCalculate.getPrice() * 0.03);
    }

    @Override
    public ProductType getProductType() {
        return ProductType.FOOD;
    }
}

이제 포인트계산 Service를 담아사용할 Factory component를 생성하자

포인트계산 Service Factory class

@Component
public class PointCalculateServiceFactory {

    private final Map<ProductType, PointCalculateService> pointCalculateServiceMap = new HashMap<>();

    /**
     * 생성자주입
     * @param pointCalculateServices
     */
    public PointCalculateServiceFactory(List<PointCalculateService> pointCalculateServices) {
        pointCalculateServices.forEach(s -> pointCalculateServiceMap.put(s.getProductType(), s));
    }

    public PointCalculateService getPointCalculateService(ProductType productType) {
        return pointCalculateServiceMap.get(productType);
    }
}

포인트계산 Test cass

@SpringBootTest
class PointCalculateTest {

    private final int PRICE = 10000;

    @Autowired
    private PointCalculateServiceFactory pointCalculateServiceFactory;

    @Test
    public void clothesPointCalculate_if_success() {
        assert(calculatePoint(ProductType.CLOTHES) == 500);
    }

    @Test
    public void foodPointCalculate_if_success() {
        assert(calculatePoint(ProductType.FOOD) == 300);
    }

    @Test
    public void electronicsPointCalculate_if_success() {
        assert(calculatePoint(ProductType.ELECTRONICS) == 1000);
    }

    private int calculatePoint(ProductType productType) {
        PointCalculate pointCalculate = PointCalculate.builder()
                .price(PRICE)
                .productType(productType)
                .build();
        return pointCalculateServiceFactory.getPointCalculateService(productType).calculatePoint(pointCalculate);
    }
}

장황스러운 if문이 아래와 같이 이해하기도 쉽고 유지보수도 용이하도록 변경되었다.

pointCalculateServiceFactory.getPointCalculateService(productType).calculatePoint(pointCalculate);

앞으로는 새로운 포인트계산서비스가 필요할 경우 CalculateService interface를 상속받는 Service bean만 생성해주면 된다.

위 코드는 github에서 확인 할 수 있다.

profile
정태인의 블로그

0개의 댓글