본문 바로가기
Engineering/SW Design

[디자인 패턴] 생성패턴 - 팩토리 메서드

by 쿨쥰 2023. 5. 22.

생성패턴 - 팩토리 메서드


이전 글 : 디자인 패턴 개요

2023.05.19 - [Engineering/SW Design] - 디자인 패턴 개요

 

디자인 패턴 개요

디자인 패턴 이란? 디자인 패턴 SW 설계 과정에서 자주 발생하는 문제들에 대한 전형적인 해결책이다. 이는 코드에서 반복되는 디자인 문제들을 해결하기 위해 미리 만들어진 코드의 붕어빵 틀

skidrow6122.tistory.com

 

 

요약

부모 클래스에서 객체들을 생성할 수 있는 인터페이스를 제공하지만, 자식 클래스들이 생성될 객체들의 유형을 변경할 수 있도록 하는 생성패턴이다.

객체를 생성할 때 어떤 클래스의 인스턴스를 만들지를 서브 클래스에서 결정하게 한다.

부모 추상 클래스는 인터페이스에만 의존하고, 실제로 어떤 구현 클래스를 호출할 지는 서브 클래스에서 구현한다.

이렇게 하면 새로운 구현 클래스가 추가되어도 기존 factory 코드의 수정 없이 새로운 factory 를 추가하면 되기 때문이다.

 

키워드 : 팩토리 메서드 Creator - 부모 인터페이스 / 자식 서브클래스 - ConcreateCreator 구현 -  ConcreateProduct 반환     

              팩토리 메서드의 객체를 통해 Product 의 인스턴스를 반환받아 사용함

 

 

 

문제

물류 관리 앱을 개발하고 있다고 가정해보자.

앱의 첫번째 버전은 트럭 운송만 취급하므로 대부분의 코드가 Truck 클래스에 있다.

얼마 후 앱에 해상 물류 회사들로 부터 해상 물류 기능을 탑재 해달라는 요청을 받기 시작했다고 치자.

이때, 대부분의 코드는 Truck 클래스에 결합되어 있을 것이다. 따라서 앱에 Ship 클래스를 추가하려면 전체 코드 베이스를 변경해야 한다.

결과 적으로 많은 if문들이 운송 수단 객체들의 클래스에 따라 앱의 행동을 바꾸는 복잡한 코드가 나오게 될 것 같다.

이처럼 나머지 코드가 이미 기존 클래스들에 결합 되어 있다면 프로그램에 새 클래스를 추가하는 일은 그리 간단하지 않다.

 

 

 

해결책

자식 클래스들은 팩토리 메서드가 반환하는 객체들의 클래스를 변경할 수 있음

위 예시에서 팩토리 메서드는 Logistics를 가르키고, 이 팩토리 메서드에서 반환 된 객체는 제품 (product) 이라고도 불린다.

여기서는 자식 클래스에서 팩토리 메서드를 오버라이딩 하고 그 메서드에 의해 생성되는 제품 (product) 들의 클래스를 변경 할 수 있게 되었다.

단, 자식 클래스들은 다른 유형의 product 들을 해당 product 들이 공통 기초 클래스 또는 공통 인터페이스가 있는 경우에만 반환 할 수 있다.

또한 모든 product 들에 공통인 Transport 인터페이스로 Logistics 기초 클래스의 createTransport 팩토리 메서드의 반환 유형을 선언해야 한다.

모든 product 들은 같은 인터페이스를 따라야 함

Transport 인터페이스에서 deliver() 라는 메서드를 선언한다.

Truck 과 Ship 클래스들은 모두 Transport 인터페이스를 구현해야 한다. 하지만, 각 클래스는 이 deliver 메서드를 다르게 구현한다.

트럭은 육로로 화물을 배달하고 선백은 해상으로 화물을 배달하기 때문이다.

 

RoadLogistics 클래스에 포함된 팩토리 메서드 + createTransport() 는 Truck 객체들을 반환하는 반면,

SeaLogistics 클래스에 포함된 팩토리 메서드 + createTransport() 는 Ship 객체들을 반환하게 된다.

 

여기서 팩토리 메서드를 사용하는 코드를 클라이언트 코드라고 부르며, 이 클라이언트 코드는 다양한 지식 클래스에서 실제로 반환되는 여러 product 간의 차이에 대해 알지 못한다.

클라이언트 코드는 모든 제품을 추상 Transport 로 간주하기 때문이다.

클라이언트는 모든 Transport 객체들이 deliver 메서드를 가져야 한다는 사실을 알고 있지만, 이 메서드가 정확히 어떻게 작동하는지는 클라이언트에게 중요한 정보는 아니다.

 

 

구조

- Creator 가 팩토리 메서드이고 예시에서의 Logistics 에 해당

- Creator 의 자식 클래스에서 는 Product 을 구현한 ConcreteProductA, B를 각각 리턴한다

 

 

 

예제 클래스 다이어그램

국내에 서비스하는 포인트 종류는 굉장히 많다.

그중 OCB와 CJ 포인트를 예시로 하여, 각 포인트를 결제 옵션으로 사용할 수 있는 결제 시스템을 가정한다.

  • PointFactory 클래스는 Creator로서 동작하며, OCB or CJ 에 대한 인스턴스를 리턴해준다.
  • Point 인터페이스에서는 조회, 취소 함수를 인터페이스 함수로 제공한다.
  • OCBPoint, CJPoint 에서는 Point 인터페이스의 함수들을 오버라이드해서 실제 클라이언트에서 사용할 수 있는 함수로 재정의한다. 보통 여기서 각 포인트 타입에 따른 대정책을 구현하게 될 것이다.

 

 

Hands on

class PointFactory {
    fun createPointInstance(code: String) : Point {
        return when (code) {
            "OCB" -> OCBPoint()
            "CJ" -> CJPoint()
            else -> throw IllegalArgumentException("파라미터 타입 오류")
        }
    }
}
  • Creator 인 팩토리 메서드 클래스에서는 편의상 포인트 타입 코드 (보통 enum 으로 관리되게 될 것이다) 를 인풋받아서, 각 타입에 맞는 포인트 Concrete 인스턴스를 리턴하게 구현 하였다.
  • 이 크리에이터의 코드에서 제품 생성자들에 대한 모든 참조를 삽입하여야하는데, 인스턴트화 할 제품 클래스를 선택하는 switch 문이 포함되기도 한다.

 

interface Point {
    // 현재 사용가능한 금액을 조회
    fun withdrawal(membershipId: Long): Long
    // 포인트 결제 분을 취소하고 잔액을 리턴
    fun cancel(transactionId: Long, cause: String): Long
}
  • Point 인터페이스는 Product 이다.
  • 여기서는 포인트 관련 공통된 기능을 인터페이스 함수로 제공한다.

 

class OCBPoint : Point {
    override fun withdrawal(membershipId: Long) : Long {
        println("OCB포인트 사용")
        TODO("Not yet implemented : only for OCB specific")
    }
    override fun cancel(transactionId: Long, cause: String) : Long {
        println("OCB포인트 사용 취소")
        TODO("Not yet implemented : only for OCB specific")
    }
}


class CJPoint : Point {
    override fun withdrawal(id: Long): Long {
        println("CJ포인트 사용")
        TODO("Not yet implemented : only for CJ specific")
    }
    override fun cancel(id: Long, cause: String): Long {
        println("CJ포인트 사용 취소")
        TODO("Not yet implemented : only for CJ specific")
    }
}
  • Concreate 클래스 들이다.
  • Product인 Point 인터페이스 함수를 오버라이드하여 실제 활용할 수 있는 형태의 함수로 재정의 하였다.

 

class PointService {
    private var pointFactory = PointFactory()

    fun OCBPointWithdrawal(membershipNumber: Long) : Long {
        val point = pointFactory.createPointInstance("OCB")
        //TODO OCB 포인트 관련 비즈니스 로직
        return point.withdrawal(membershipNumber)
    }

    fun OCBPointCancel(transactionId: Long) : Long {
        val point = pointFactory.createPointInstance("OCB")
        //TODO OCB 포인트 관련 취소정책 반영
        return point.cancel(transactionId, "시스템 차감취소")
    }

    fun CJPointWithdrawal(cjId: Long) : Long {
        val point = pointFactory.createPointInstance("CJ")
        //TODO CJ 포인트 관련 비즈니스 로직
        return point.withdrawal(cjId)
    }

    fun CJPointCancel(transactionId: Long) : Long {
        val point = pointFactory.createPointInstance("CJ")
        //TODO CJ 포인트 관련 취소정책 반영
        return point.cancel(transactionId, "시스템 차감취소")
    }
}
  • 각 포인트들에 대한 결제 관련 기능을 모아둔 서비스 레이어이다.
  • 클라이언트 라고 표현하기는 했지만, 현실에서는 OCB, CJ 각 타입에 대한 기능들은 별도의 클래스로 분리지어 배치하여 기능 목록만 망라한 후, 실제 사용되는 곳은 인터랙션 에서 구현하는 것이 적합할 것이다.

 

 

핵심

  • 객체들의 정확한 유형들과 의존관계들을 미리 모르는 경우 사용함
    • 팩토리 메서드는 제품 생성 코드를 제품을 실제로 사용하는 코드와 분리하여, 제품 생성자 코드를 나므지 코드와는 독립적으로 확장하기 쉽게 만든다
    • .새로운 제품이 추가될때는 새로운 크리에이터 자식 클래스를 생성한 후 해당 클래스 내부의 팩토리 메소드를 오버라이딩(재정의)하기만 하면 된다.
  • 기존 객체들을 매번 재구축한느 대신 이들을 재사용하여 시스템 리소스를 절약하고 싶을때 사용함
  • 팩토리 메서드로 시작해 더 유연하면서도 더 복잡한 추상팩토리, 프로토타입 또는 빌더 패턴으로 발전해 나감

 

 

패턴 접근 과정

  1. 모든 Product가 같은 인터페이스를 따르도록 함
     - 이 인터페이스는 모든 Product에서 의미가 있는 메서드들을 선언해야 함

  2. Creator 클래스 내부에 빈 팩토리 메서드를 추가
     - 이 메서드의 반환 유형은 공통 Product 인터페이스와 일치해야 함

  3. Creator 코드에서 Product 생성자들에 대한 모든 참조를 찾고, 이 참조들을 하나씩 팩토리 메소드에 대한 호출로 교체하면서
    Product 생성 코드를 팩토리 메서드로 추출

  4. 팩토리 메서드에 나열된 각 Product 유형에 대한 Creator 자식 클래스들의 집합을 생성한 후, 자식 클래스들에서 팩토리 메서드를 오버라이딩하고 기초 메서드에서 생성자 코드의 적절한 부분들을 추출

  5. Product 유형이 너무 많아 모든 Product에 대하여 자식 클래스들을 만드는 것이 합리적이지 않을 경우, 자식 클래스들의 기초 클래스의 제어 매개변수를 재사용할 수 있음

  6. 추출이 모두 끝난 후 기초 팩토리 메서드가 비어 있으면, 해당 팩토리 메서드를 추상화할 수 있음
    - 팩토리 메서드가 비어 있지 않으면, 나머지를 그 메서드의 디폴트 행동으로 만들 수 있음

 

 


간단히 hands on 을 통해 팩토리 메서드 패턴을 구현 해 보았다.

실제 더 복잡한 기능이 많다는 것을 감안하면, service 부분 (클라이언트)는 인터랙션 부분에 더 자세히 구현하는 것이 맞을 것이다.

다만, 본 예제를 통하여 factory method 클래스에서 포인트 타입을 인자로 받아, 각 타입에 맞는 인스턴스들을 리턴하여 아키텍처 경계를 명확히 구분하는 패턴에 주목해보았다.

 

이러한 패턴을 통하여 추후 서비스하는 포인트 타입이 추가되거나, 특정 포인트에 대한 특수한 업무규칙이 생겨나더라도 확장과 변경에 굉장히 유연한 모델을 확보할 수 있게 된다.

factory method 클래스에서 구분 하던지, 각 포인트 별 concrete 클래스에서 구분 하여 상세를 구현하면 실제 각 기능을 조합하는 인터랙션 layer 에서는 수정이 필요 없어지기 때문이다.

댓글