Neoself의 기술 블로그

BookMatchKit 리펙토링 과정-3/3 (Factory Method 패턴) 본문

개발지식 정리/Swift

BookMatchKit 리펙토링 과정-3/3 (Factory Method 패턴)

Neoself 2025. 2. 17. 23:10

1. 3차 리펙토링

2차 리펙토링 이후, 구조에 대해 아래와 같은 문제점을 파악하게 되었습니다.

2차 리펙토링 이후 의존성 그래프

 

1. 서비스 레이어의 분산으로 인한 응집도 저하

BookSearchService는 BookMatchAPI 모듈 내부에 위치한 반면, ImageProcessService와 TextExtractionService는 BookOCRKit에 위치했으며, BookValidationService는 BookRecommendationKit 내부에 위치했습니다.

때문에 같은 비즈니스 로직을 담당하는 서비스 레이어 컴포넌트끼리의 응집도가 저하되는 문제가 있었습니다.

 

2. 의존성 관리의 복잡성

2개의 최상위 모듈이 3개의 서비스 레이어 컴포넌트들 중 일부를 직접 import하여 사용하고 있기에 Service 레이어와 Integration 레이어 간 응집도가 높아지면서 책임 분리가 불명확해지는 문제가 있었습니다. 이는 자연스럽게 최상위 모듈에서의 의존성 주입도 복잡하게 만들었습니다. 

// 하위 모듈들을 직접 import
import BookMatchAPI
import BookMatchCore
import BookMatchService
import BookMatchStrategy

// ServiceFactory 없이 직접 의존성을 생성/관리하는 경우
class BookOCRKit {
    init(naverClientId: String, naverClientSecret: String) {
        let apiConfig = APIConfiguration(
            naverClientId: naverClientId,
            naverClientSecret: naverClientSecret,
            openAIApiKey: ""
        )
        
        // 각 서비스를 직접 생성하고 설정해야 함
        self.naverAPI = NaverAPI(configuration: apiConfig)
        self.imageDownloadAPI = ImageDownloadAPI(configuration: apiConfig)
        self.textExtractionService = TextExtractionService()
        // ... 더 많은 서비스들
    }
}

 

또한 BookValidationService와 같이 임계값과 같이 인자값 전달이 필요한 서비스들에 대한 인스턴스를 생성하는 과정에서 중복 코드가 발생했습니다.

// 서비스 생성 로직이 여러 곳에 분산됨
class ServiceA {
    let validationService = BookValidationService(
        similiarityThreshold: [0.4, 0.8],
        maxRetries: 3,
        titleWeight: 0.8
    )
}

class ServiceB {
    // 다른 곳에서 다른 설정으로 생성할 수 있음
    let validationService = BookValidationService(
        similiarityThreshold: [0.5, 0.9],  // 다른 임계값
        maxRetries: 5,                      // 다른 재시도 횟수
        titleWeight: 0.7                    // 다른 가중치
    )
}

 

이러한 문제들을 해결하기 위해 Factory Method 패턴을 도입했습니다.

팩토리 메서드 패턴 (Factory Method Pattern)

목적
- 객체 생성을 위한 인터페이스를 정의하되, 실제 생성할 객체의 타입은 서브클래스가 결정하도록 함
객체 생성 로직을 중앙화하여 코드 중복 방지
객체 생성과 사용을 분리하여 결합도 감소
기존 코드 수정 없이 새로운 타입의 객체 추가 가능

작동 방식
Creator(상위 클래스)에서 팩토리 메서드를 선언
ConcreteCreator(하위 클래스)에서 실제
객체 생성 구현 객체 생성 시 new 연산자 대신 팩토리 메서드 사용
생성된 객체는 공통 인터페이스나 추상 클래스를 따름

구성 요소
Product: 생성될 객체의 인터페이스
ConcreteProduct: Product 인터페이스 실제 구현
Creator: 팩토리 메서드를 선언하는 클래스
ConcreteCreator: 실제 객체를 생성하는 팩토리 메서드 구현

장점
객체 생성 코드와 사용 코드의 분리
단일 책임 원칙 준수: 객체 생성 코드를 한 곳에서 관리
개방-폐쇄 원칙 준수: 기존 코드 수정 없이 새로운 객체 타입 추가 가능
코드 재사용성과 유지보수성 향상

사용 시점
객체를 생성해야 하는 클래스가 미리 정확한 타입을 알 수 없을 때
객체 생성 로직을 하위 클래스에 위임하고 싶을 때
객체 생성 코드를 중앙화하여 관리하고 싶을 때
기존 객체를 재사용하여 시스템 자원을 절약하고 싶을 때

 

2. Factory Method 패턴 변경 전후 비교 분석

인스턴스 생성 패턴

기존 코드의 경우, 최상위 모듈의 initializer에서 서비스 생성에 필요한 세부 인스턴스(SearchService)와 함께 직접 서비스 인스턴스를 생성해줘야 했습니다.

public final class BookRecommendationKit: BookRecommendable {
    public init(
        naverClientId: String,
        naverClientSecret: String,
        openAIApiKey: String,
        similiarityThreshold: [Double] = [0.4, 0.8],
        maxRetries: Int = 3,
        titleWeight: Double = 0.8
    ) {
        let apiConfig = APIConfiguration(
            naverClientId: naverClientId,
            naverClientSecret: naverClientSecret,
            openAIApiKey: openAIApiKey
        )

        let naverAPI = NaverAPI(configuration: apiConfig)
        openAiAPI = OpenAIAPI(configuration: apiConfig)
        validationService = BookValidationService(
            similiarityThreshold: similiarityThreshold,
            maxRetries: maxRetries,
            titleWeight: titleWeight,
            searchService: BookSearchService(naverAPI: naverAPI)
        )
    }
}

 

 

하지만 리펙토링 이후, ServiceFactory를 통해 SearchService를 비롯한 모든 서비스레이어 컴포넌트와 APIClient 인스턴스 생성을 중앙화할 수 있게 되었습니다.

이로 인해 최상위 모듈에서 ServiceFactory만을 직접 의존성으로 추가해도 서비스 레이어 내부 로직들을 생성할 수 있게 되었으며, 이는 의존성 주입 단순화와 모듈 간 결합도 감소로 이어졌습니다.

// Service Factory
public final class ServiceFactory {
    @MainActor public static let shared = ServiceFactory()

    private let config: ServiceConfiguration
    
    public func makeBookSearchService() -> BookSearchService {
        BookSearchService(naverAPI: makeNaverAPI())
    }
    
    public func makeBookValidationService() -> BookValidationService {
        BookValidationService(
            similiarityThreshold: config.similarityThreshold,
            maxRetries: config.maxRetries,
            titleWeight: config.titleWeight,
            searchService: makeBookSearchService()
        )
    }
    
    public func makeOpenAIAPI() -> OpenAIAPI {
        OpenAIAPI(configuration: makeAPIConfiguration())
    }
}

// 최상위 모듈
import ServiceFactory // Factory만 import

public final class BookRecommendationKit: BookRecommendable {

    private let openAiAPI: OpenAIAPI
    private let validationService: BookValidatable
    private let serviceFactory: ServiceFactory

    public init() {
        self.serviceFactory = ServiceFactory(
            naverClientId: naverClientId,
            //... 인스턴스 생성에 필요한 설정값 주입
        )
        
        // ServiceFactory 내부 메서드를 통해 서비스 컴포넌트 및 APIClient 생성
        self.openAiAPI = serviceFactory.makeOpenAIAPI()
        self.validationService = serviceFactory.makeBookValidationService()
    }
    
    // 기존 메서드들...
}

 

 

모듈간의 결합도가 낮아졌기에 ServiceFactory만을 Mock 객체로 주입하여도, 최상위 모듈에 대한 단위 테스트 또한 가능해졌습니다.

// TestServiceFactory.swift
class TestServiceFactory: ServiceFactory {
    override func makeBookSearchService() -> BookSearchService {
        return MockBookSearchService()
    }
}

// Tests
func testBookRecommendation() {
    let testFactory = TestServiceFactory()
    let kit = BookRecommendationKit(serviceFactory: testFactory)
    // ... test code
}

 

이러한 개선은 시스템의 유지보수성, 테스트 용이성, 그리고 전반적인 코드 품질을 향상시켰으며, 아래 도식과 같이 모듈 간 의존성을 최소화하며 책임을 명확히 분리할 수 있게 되었습니다.

이상으로 BookMatchKit의 리펙토링 과정 정리를 마치겠습니다.

 

감사합니다.