Spring Boot 환경에서 전략 패턴과 팩토리 패턴을 결합한 다형성 프로토콜 처리

비디오 스트리밍 또는 SDI 출력 기능을 구현할 때, UDP나 SRT와 같이 서로 다른 전송 프로토콜을 지원해야 하는 경우가 많습니다. 각 프로토콜은 고유한 네트워크 설정과 초기화 로직을 요구하며, 향후 새로운 프로토콜이 추가될 가능성을 고려해야 합니다. 이러한 상황에서 단순한 조건문(if-else)은 코드의 유지보수성을 심각하게 떨어뜨립니다. 전략 패턴(Strategy Pattern)을 적용하여 프로토콜별 로직을 분리할 수 있지만, 컨텍스트에서 적절한 전략을 선택하는 과정에서 여전히 조건문이 발생하는 한계가 있습니다. 이를 해결하기 위해 팩토리 패턴(Factory Pattern)을 결합하여 조건문을 완전히 제거하고 확장 가능한 구조를 구축하는 방법을 살펴봅니다.

전략 인터페이스 설계

먼저 각 프로토콜별 동작을 추상화하는 인터페이스를 정의합니다. 팩토리 클래스에서 조건문 없이 자동으로 전략을 매핑할 수 있도록, 자신이 지원하는 프로토콜 타입을 반환하는 메서드를 인터페이스에 포함시킵니다.

public interface StreamOutputStrategy {

    /**
     * 현재 전략이 처리할 수 있는 프로토콜 타입을 반환합니다.
     */
    ProtocolType getSupportedProtocol();

    /**
     * 네트워크 경로를 설정하고 스트림 출력을 초기화합니다.
     */
    void provisionStream(NetworkRoute route, StreamCreationRequest request);
}

프로토콜별 전략 구현

인터페이스를 구현하여 각 프로토콜에 맞는 구체적인 로직을 작성합니다. Spring의 @Component 어노테이션을 사용하여 빈으로 등록합니다.

@Component
public class UdpStreamOutput implements StreamOutputStrategy {

    @Override
    public ProtocolType getSupportedProtocol() {
        return ProtocolType.UDP;
    }

    @Override
    public void provisionStream(NetworkRoute route, StreamCreationRequest request) {
        // UDP 내부망 스트림 초기화 로직 (IP 바인딩, 버퍼 설정 등)
    }
}
@Component
public class SrtStreamOutput implements StreamOutputStrategy {

    @Override
    public ProtocolType getSupportedProtocol() {
        return ProtocolType.SRT;
    }

    @Override
    public void provisionStream(NetworkRoute route, StreamCreationRequest request) {
        // SRT 외부망 스트림 초기화 로직 (포트 할당, 지연시간, 암호화, TTL 설정 등)
    }
}

프로토콜 타입 정의

프로토콜을 안전하게 관리하고 문자열을 열거형으로 변환하기 위한 Enum 클래스를 작성합니다.

public enum ProtocolType {
    UDP("udp"),
    SRT("srt");

    private final String value;

    ProtocolType(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    public static ProtocolType fromValue(String value) {
        for (ProtocolType type : values()) {
            if (type.value.equalsIgnoreCase(value)) {
                return type;
            }
        }
        throw new IllegalArgumentException("지원하지 않는 프로토콜 타입: " + value);
    }
}

팩토리 패턴을 통한 전략 관리

기존의 if-elseswitch 문을 사용하여 전략을 선택하는 방식 대신, Spring의 의존성 주입 기능을 활용하여 모든 전략 implementation을 리스트로 주입받고, 이를 Map으로 변환하여 O(1) 시간 복잡도로 전략을 조회하는 팩토리를 구현합니다. 이 방식은 새로운 프로토콜이 추가되어도 팩토리 코드를 수정할 필요가 없는 OCP(개방-폐쇄 원칙)를 완벽하게 준수합니다.

@Component
public class StreamStrategyFactory {

    private final Map<ProtocolType, StreamOutputStrategy> strategyMap;

    public StreamStrategyFactory(List<StreamOutputStrategy> strategies) {
        this.strategyMap = strategies.stream()
                .collect(Collectors.toMap(
                        StreamOutputStrategy::getSupportedProtocol,
                        Function.identity()
                ));
    }

    public StreamOutputStrategy getStrategy(String protocolValue) {
        ProtocolType type = ProtocolType.fromValue(protocolValue);
        return Optional.ofNullable(strategyMap.get(type))
                .orElseThrow(() -> new IllegalStateException("등록되지 않은 스트림 전략: " + type));
    }
}

서비스 계층에서의 호출

최종적으로 비즈니스 로직을 처리하는 서비스 계층에서는 팩토리를 통해 적절한 전략을 가져와 실행하기만 하면 됩니다. 이로 인해 서비스 코드는 프로토콜의 종류에 대해 완전히 독립적으로 동작하게 됩니다.

@Service
@RequiredArgsConstructor
public class StreamProvisioningService {

    private final StreamStrategyFactory strategyFactory;

    public void createStreamDestination(NetworkRoute route, StreamCreationRequest request) {
        String protocol = request.getProtocol();
        StreamOutputStrategy strategy = strategyFactory.getStrategy(protocol);
        strategy.provisionStream(route, request);
    }
}

태그: SpringBoot StrategyPattern FactoryPattern java DesignPatterns

9월 12일 13:40에 게시됨