[디자인패턴] 팩토리 메소드 패턴 적용기

728x90

 

1. 들어가며


실무를 하며 느끼는게 하나가 있다.

내가 생각한대로 개발 일정 및 개발 요구사항이 평온하게 진행되는 경우는 없다고 느꼈다.

 

그렇기에 실질적인 요구사항에 맞춰 개발하는 나로써는 항상 이러한 고민을 하게 된다.

 

'어떻게 하면 유동적으로 코드를 변경할 수 있을까?'
'어차피 요구사항이 계속 추가될걸 예상하고, 어떻게 코드를 추상화시켜서 내가 덜 고생할 수 있을까?'

 

 

라는 고민이 요즘들어 많이 든다.

 

평소에는 내가 코드를 짜는 방식은 일단, 작동은 되게 만들어 둔 후, 천천히 리팩토링 및 고도화를 시키는게 나만의 코딩 방식이였다.

 

하지만...바뀌는 요구사항들 및 버려지는 내 코드들을 보며, 느끼게 되었다. 

처음부터 내가 코드 설계를 쪼금만 더 잘했다면??? 덜 고생하지 않았을까? 하는 생각을 한다.

 

무작정 코드를 짜는 것보다는 유연한 코드를 만들기 위해 처음부터 노력을 해야한다.

 

이제부터라도 코드를 치기 전에 더 많은 고민을 한 후에 코딩을 하는 습관을 가져보려고 한다.

 

내가 아래에 설명할 상황은 아래와 같다. 

(필자는 Java/SpringBoot 환경에서 SSR 페이지를 개발한 상황을 빗대어 설명한다)

  • 매번 다른 Path 값에 따라서 유동적으로 정해진 값을 줘야한다.
    •    ex) localhost:8080/kakaopay 면 html 화면에는 미리 설정해둔 kakaopay 에 해당하는 Value들이 화면에 보여져야 했다. 
      • kakaopay 는 100 이라 치고, naverpay 는 90 이라고 치면 /path 가 /kakaopay 면 100 이 화면에 나와야한다는 말이다.
      • SSR 방식을 사용하기에 Spring + thymeleaf 조합을 사용하였다.

 

근데 만약에 kakaopay, naverpay, linepay, paybook 등 여러개의 페이사들이 막 20,30개가 한꺼번에 늘어난다면?? 

어떻게 해야지 공통 코드를 추상화하여 내 수고를 덜할 수 있을까??

 

 

2. 본론


[기존코드]

@GetMapping("/{pay}")
public String payPathOptions (Model model, @PathVariable String pay) {

	switch (pay) {
		case "kakao" :
		model.addAttribute("kakao","100");
         model.addAttribute("naver","90");
         model.addAttribute("line","80");
			model.addAttribute("pay",pay);
                
			return "index";

		case "naver" :
		 model.addAttribute("naver","100");
         model.addAttribute("kakao","90");
         model.addAttribute("line","80");
		 model.addAttribute("pay",pay);

			return "index";

		default:
			return "pathErrorPage";
		}
	}

 

기존 코드는 위와 같다.

Path 로 들어오는 값에 따라 Switch~case 문을 통하여 처리를 하고 있었다.

 

하지만 여러개의 pay 가 생기게 된다면 switch ~ case 안에 case 가 무수히 많게 늘어나게 될 것이고

그에 따라 Controller 로직이 엄청 더러워 질 것으로 예상이 된다.

 

그래서 처음생각해본건, 로직을 Service 에 분리를 해서 진행해보려고 했지만,

바뀌는건 무수히 많은 Switch~case 문의 위치가 Service Layer 로 가는 것 밖에는 없었다.

그에 따라 Service 에서도 분명 위 처럼 하드코딩이 될 것으로 예상이 된다.

 

그래서 검색을 하고 고민을 하던 중 팩토리 메소드 패턴에 대해서 알게 되었다.

 

팩토리 메소드 패턴은 간단하게 말하여 객체의 생성 과정을 서브클래스에 위임하는 패턴입니다.

 

객체 생성 로직을 캡슐화하여 코드의 결합도를 낮출 수 있습니다.

새로운 서비스가 추가될 때, 클라이언트 코드를 수정할 필요 없이 팩토리 클래스만 수정하면 됩니다.

 

 

저는 어떠한 개념을 글로만 보면 잘 이해가 되지 않습니다.

그래서 직접 해보거나, 코드 및 그림을 통해 이해하는게 좋다고 생각합니다.

 

 

바로 아래 코드를 보며 이해를 도와보겠습니다.

 

1. 팩토리 메소드 패턴을 적용하기 위해선 공통 메소드를 추상화할 인터페이스를 구현해야 합니다.

 

[PaymentPathInter.java]

public interface PaymentPathInter {
	Map<String, String> getPaymentPathOptions();
	Map<String, Boolean> getPaymentShowOptions();
	String getMerchantName();
}

 

저는 총 2개의 공통 메소드를 추상화 시키기 위해 선언해 두었습니다.

 

- getPaymentPathOptions() : Payment 경로 별 다른 값을 보여 줄 메소드 

- getPaymentShowOptions() : Payment 경로 별, 화면에 보여줄 값과 보이지 않을 값을 매핑해 줄 메소드 

 

크게 2가지로 공통 메소드가 있다.

위 인터페이스는 빈으로 만들어 사용하지 않기에 따로, 스프링 컨텍스트에 등록을 하지는 않았다.

 

 

2. 다음으로는 Factory 클래스를 만들어야 한다.

위 Factory 클래스의 역할은, 공장의 역할하고 같다. 

 

어떠한 클래스가 들어오면, 공장에서 무언가를 찍어 내듯이 객체를 찍어서 만들어 낸다.

 

[PaymentFactory.java]

@Component
public class PaymentFactory {
	private final Map<String,PaymentPathInter> paymentPathMap = new HashMap<>();

	public PaymentFactory () {
		paymentPathMap.put("kakao",new KakaoService());
		paymentPathMap.put("naver",new NaverService());
		paymentPathMap.put("line",new LineService());
		paymentPathMap.put("paybook",new PaybookService());
	}

	public PaymentPathInter getPaymentService(String merchant) {
		return paymentPathMap.get(merchant);
	}

}

 

@Component 를 통해 인터페이스는 빈에 등록하지 않지만, 추후 사용될 위 *Service 들이 PaymentFactory 생성자를 생성할 때

빈에 등록되기 때문에 @Component 를 붙여줘야 한다.

 

 

3. 실제 메소드 구현

[KakaoService.java]

@Service
public class KakaoService implements PaymentPathInter {
	@Override
	public Map<String, String> getPaymentPathOptions () {
		var opts = new HashMap<String, String>();
		opts.put("kakao","100%");
		opts.put("naver","90%");
        	opts.put("line","80%");
	        opts.put("paybook","70%");
    		opts.put("union","60%");

		return opts;
	}

	@Override
	public Map<String, Boolean> getPaymentShowOptions () {
		var opts = new HashMap<String, Boolean>();
		opts.put("showKakaoPay",true);
		opts.put("showNaverPay",true);
		opts.put("showLinePay",true);
		opts.put("showPaybookPay",true);
        	opts.put("unionpay", false);
        
		return opts;
	}

	@Override
	public String getMerchantName () {
		return "kakao";
	}

}

 

[NaverService.java]

@Service
public class NaverService implements PaymentPathInter {
	@Override
	public Map<String, String> getPaymentPathOptions () {
		var opts = new HashMap<String, String>();
		opts.put("naver","100%");
		opts.put("kakao","90%");
        	opts.put("paybook","80%");
	        opts.put("union","70%");
    		opts.put("line","60%");

		return opts;
	}

	@Override
	public Map<String, Boolean> getPaymentShowOptions () {
		var opts = new HashMap<String, Boolean>();
		opts.put("showKakaoPay",true);
		opts.put("showNaverPay",true);
		opts.put("showLinePay",true);
		opts.put("showPaybookPay",false);
        	opts.put("unionpay", true);
        
		return opts;
	}

	@Override
	public String getMerchantName () {
		return "naver";
	}

}

 

위와 같은 코드로 실제구현을 하게 된다.  실제로는 결제가(=Payment) 가 많이 생길 수록 위 클래스들은 더욱더 많아지게 될 것이다.

 

위 2가지 Naver,Kakao 서비스를 보면 내부 구현이 미묘하게 차이점이 있다.

Map 에 들어가는 값들에 차이가 있다.

 

마지막으로 이제 위 팩토리 를 사용을 해야한다.

 

 

4. 팩토리 사용

[PaymentController]

@RequiredArgsConstructor
@Controller
public class PaymentController {
	private final MerchantPathFactory merchantPathFactory;

	@GetMapping("/{payment}")
	public String paymentOptions (Model model, @PathVariable String payment) {
    	// 인터페이스 생성
		PaymentPathInter paymentPathInter = merchantPathFactory.getPaymentService(merchant);
		if(paymentPathInter==null)
			return "pathErrorPage";

		Map<String, String> pathOptions = paymentPathInter.getMerchantPathOptions();
		Map<String, Boolean> showOptions = paymentPathInter.getMerchantShowOptions();

		model.addAllAttributes(pathOptions);
		model.addAllAttributes(showOptions);
		model.addAttribute("payment",payment);

		return "index";
	}

 

위 컨트롤러에서 실제로 팩토리를 사용해서 객체를 생성을 함으로써 유동적으로 구현을 하였다.

 

나는 SSR 을 사용하기 때문에 @RestController 가 @Controller 를 사용하였고

Json 값이 아닌 form 을 내려주어야 했다.

 

 

thymeleaf 를 사용하여 구현을 하였고, 아래는 유동적으로 바뀌어야하는 코드 이다.

<table>
	<tr th:if="${showKakaoPay}">
		<td>KakaoPay</td>
		<td th:text="${kakao}"></td>
	</tr>
	<tr th:if="${showNaverPay}">
		<td>NaverPay</td>
		<td th:text="${naver}"></td>
	</tr>
	<tr th:if="${showLinePay}">
		<td>LinePay</td>
		<td th:text="${line}"></td>
	</tr>
	<tr th:if="${showPaybookPay}">
		<td>paybookPay</td>
		<td th:text="${paybook}"></td>
	</tr>
</table>

 

위처럼 타임리프 문법을 사용하여 동적 페이지를 구현하였다.

 

이를 끝으로 팩토리 메소드 패턴을 처음으로 활용해 보았다.

 

 

 

3. 결론


디자인패턴이 왜 대대로 내려와 지는지 알았고, 사람들이 왜 사용하는지에 대한 이해도가 더 높아졌다

 

결국 모든 것에는 이유가 존재한다. 쓸 때 없이 사용되는 무언가는 없다고 느꼈다.

 

평소에 디자인 패턴에 대한 관심이 크게 있지않았지만, 이번 기회를 통해 조금 더 유연한 코드에 대하여 
고민을 많이하게 되는 시간이였고, 틈틈히 디자인 패턴을 공부해봐야 할 것 같다.

 

 

 

 

728x90