본문 바로가기
SpringBoot

확장 가능한 서비스 구현 패턴

by ByteBridge 2024. 11. 17.
반응형

소프트웨어 개발에서 공통적인 기능을 재사용하면서도 쉽게 확장 가능한 구조를 만드는 것은 중요한 과제입니다. 이번 글에서는 스프링부트를 사용하여 다양한 케이스에 유연하게 대응할 수 있는 전략 패턴추상 클래스를 활용한 구현 패턴을 알아보고, 이를 통해 얻을 수 있는 장점, 단점, 그리고 확장 가능성에 대해 설명해 보겠습니다.

추상 클래스를 활용한 결제 처리 시스템 설계 및 전략 패턴 도입

이번에 구현할 시스템에서는 여러 가지 결제 수단(신용카드, 간편결제 등)을 제공하며, 결제 전 공통적으로 처리해야 하는 로직이 존재합니다. 이 경우 추상 클래스전략 패턴을 조합하여 유연하고 확장 가능한 결제 시스템을 설계할 수 있습니다.

예제 코드

  1. 추상 클래스 정의
package com.example.payment;

import org.springframework.beans.factory.annotation.Autowired;

public abstract class PaymentAbstractService {

    @Autowired
    private PaymentTypeService paymentTypeService;

    @Autowired
    private PaymentCheckService paymentCheckService;

    public void processPayment() {
        // 공통 처리 로직
        paymentTypeService.validatePaymentType();
        paymentCheckService.checkPayment();

        // 하위 클래스에서 구현해야 하는 구체적인 결제 처리 로직 호출
        executePayment();
    }

    // 각 결제 수단별로 다르게 구현될 추상 메서드
    protected abstract void executePayment();

    // 전략 패턴 적용을 위한 메서드
    public abstract boolean isApplicable(String paymentType);
}
  • PaymentAbstractService: 결제 타입 검증과 결제 전 체크 등 공통적인 로직을 포함합니다. 실제 결제 로직은 executePayment() 메서드로 정의하고 하위 클래스에서 구현합니다.
  • isApplicable(): 특정 결제 타입에 해당하는지 여부를 판단하는 메서드로, 전략 패턴에서 각 하위 클래스가 자신이 적용 가능한지 여부를 결정합니다.
  1. 구체적인 구현 클래스 정의
    • 신용카드 결제
    • 간편결제
package com.example.payment;

import org.springframework.stereotype.Service;

@Service("creditCardPaymentService")
public class CreditCardPaymentService extends PaymentAbstractService {

    @Override
    protected void executePayment() {
        // 신용카드 결제 처리 로직
        System.out.println("Processing credit card payment...");
        // 신용카드 결제 관련 세부 처리
    }

    @Override
    public boolean isApplicable(String paymentType) {
        return "creditCard".equalsIgnoreCase(paymentType);
    }
}

package com.example.payment;

import org.springframework.stereotype.Service;

@Service("easyPayPaymentService")
public class EasyPayPaymentService extends PaymentAbstractService {

    @Override
    protected void executePayment() {
        // 간편결제 처리 로직
        System.out.println("Processing easy pay payment...");
        // 간편결제 관련 세부 처리
    }

    @Override
    public boolean isApplicable(String paymentType) {
        return "easyPay".equalsIgnoreCase(paymentType);
    }
}
  • **CreditCardPaymentService**와 EasyPayPaymentService: 각각 신용카드 결제와 간편결제를 처리하는 클래스입니다. executePayment() 메서드를 구현하여 각기 다른 결제 로직을 정의하고, isApplicable() 메서드를 통해 자신이 처리 가능한 결제 타입을 결정합니다.
  1. 전략 패턴 적용을 위한 컨트롤러 구현
package com.example.payment;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class PaymentController {

    @Autowired
    private List<PaymentAbstractService> paymentServices;

    @GetMapping("/pay")
    public String processPayment(@RequestParam String paymentType) {
        for (PaymentAbstractService paymentService : paymentServices) {
            if (paymentService.isApplicable(paymentType)) {
                paymentService.processPayment();
                return "Payment processed successfully";
            }
        }
        return "Unsupported payment type";
    }
}
  • **PaymentController**는 List<PaymentAbstractService>를 사용하여 모든 결제 서비스를 가져온 후, 각 서비스의 isApplicable() 메서드를 호출하여 해당 결제 타입에 맞는 서비스를 선택합니다.
  • 특정 결제 타입의 요청이 들어오면, 해당하는 서비스가 있는지 확인하고 동적으로 결제를 처리합니다.

구현 전략: 전략 패턴과 템플릿 메서드 패턴의 결합

이번 구현은 전략 패턴템플릿 메서드 패턴을 결합한 형태입니다. 템플릿 메서드 패턴은 공통 로직을 상위 클래스에서 정의하고, 구체적인 처리는 하위 클래스에 위임하는 방식입니다. 여기에 전략 패턴을 도입하여 결제 방식을 유연하게 선택할 수 있도록 했습니다. 이를 통해 코드 중복을 줄이고, 공통된 로직은 한 곳에서 관리할 수 있게 됩니다.

장점

  1. 코드 재사용성: 공통적인 결제 전 처리 로직을 추상 클래스에 정의함으로써 코드 재사용성이 높아집니다.
  2. 유지보수 용이성: 결제 전 처리 로직을 한 곳에서 관리할 수 있어 유지보수가 쉬워집니다. 만약 공통 로직에 변경이 필요하다면 추상 클래스만 수정하면 됩니다.
  3. 확장성: 새로운 결제 수단을 추가할 때 기존 코드를 수정할 필요 없이 새로운 하위 클래스를 추가하고, 필요한 로직을 구현하기만 하면 됩니다. 이는 **OCP (Open/Closed Principle)**를 따르는 구조입니다.
  4. 유연한 선택: 전략 패턴을 통해 각 결제 수단에 대해 동적으로 알맞은 결제 서비스를 선택할 수 있습니다.

단점

  1. 복잡성 증가: 단순한 로직의 경우에도 추상 클래스와 여러 하위 클래스를 만들어야 하므로 설계가 다소 복잡해질 수 있습니다.
  2. 의존성 관리: 공통 로직에서 사용하는 서비스들이 많아질수록 추상 클래스가 많은 의존성을 가질 수 있어, 이를 관리하는 것이 어려워질 수 있습니다.

확장 방안

  1. 새로운 결제 수단 추가: 새로운 결제 방식이 필요하다면 PaymentAbstractService를 상속하는 새로운 클래스를 추가하고, executePayment() 메서드를 구현합니다.
  2. 유연한 관리: isApplicable() 메서드를 통해 새로운 결제 수단을 쉽게 등록하고 관리할 수 있습니다.

Mock 테스트 코드 작성

아래는 PaymentAbstractService의 구현 클래스에 대한 Mock 테스트 예시입니다. 이 테스트는 각 결제 서비스의 processPayment() 메서드가 올바르게 호출되고, executePayment() 메서드가 정확히 작동하는지 확인합니다.

테스트 코드 예제

package com.example.payment;

import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.boot.test.context.SpringBootTest;

import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

@SpringBootTest
public class PaymentServiceTest {

    @Mock
    private PaymentTypeService paymentTypeService;

    @Mock
    private PaymentCheckService paymentCheckService;

    @InjectMocks
    private CreditCardPaymentService creditCardPaymentService;

    @InjectMocks
    private EasyPayPaymentService easyPayPaymentService;

    @Test
    public void testCreditCardPaymentService() {
        // when
        creditCardPaymentService.processPayment();

        // then
        verify(paymentTypeService, times(1)).validatePaymentType();
        verify(paymentCheckService, times(1)).checkPayment();
        // executePayment()는 실제로 호출되어야 합니다.
    }

    @Test
    public void testEasyPayPaymentService() {
        // when
        easyPayPaymentService.processPayment();

        // then
        verify(paymentTypeService, times(1)).validatePaymentType();
        verify(paymentCheckService, times(1)).checkPayment();
        // executePayment()는 실제로 호출되어야 합니다.
    }
}
  • @Mock: PaymentTypeServicePaymentCheckService를 Mock 객체로 설정하여 의존성을 주입합니다.
  • @InjectMocks: CreditCardPaymentServiceEasyPayPaymentService에 Mock 객체를 주입합니다.
  • 각 테스트에서 processPayment()가 호출되면, validatePaymentType()checkPayment() 메서드가 정확히 호출되었는지 검증합니다.

마무리

이번 글에서는 Spring Boot에서 추상 클래스와 전략 패턴을 결합하여 결제 처리 시스템을 구현하는 방법에 대해 알아보았습니다. 이러한 구조는 공통 로직의 재사용성과 유지보수성을 높이고, 확장성을 보장하면서도 명확한 설계 구조를 제공하는 데 유용합니다. 단점으로는 구조의 복잡성이 증가할 수 있지만, 이를 잘 관리한다면 효율적인 결제 시스템을 구축할 수 있습니다.

반응형