ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 모든 햄버거는 다르다, Strategy Pattern
    Java & Kotlin 2023. 3. 29. 14:15
    반응형

    1) 서론

    혹시 무언가를 얻기 위해 협상을 했던 적이 있나요?
     
    어릴 적 어린이날 선물을 받고 싶어 부모님께 청소를 열심히 하겠다고 했던 적이 있는데요. 당시 부모님께서 자신이 먹은 그릇을 치우고, 어지럽힌 것을 치우는 것은 당연하다고 말씀하셨던 게 기억이 납니다.
     
    청소라는 전략을 사용해서는 어린이날 선물을 받을 수 없다는 것을 깨닫고, 어린이날까지 높임말을 잘 쓰겠다는 전략으로 변경했고 부모님께서는 흔쾌히 승낙했습니다. 아마도 예의를 중요시 여기는 아버지께서는 내심 그러한 것을 바라셨는지도 모르겠습니다.
     
    이렇듯 현실에서 당연히 될것이라고 선택했던 전략이 통하지 않을 때가 많습니다. 왜냐하면 인생은 실전이고, 한 치 앞도 알 수 없습니다. 이럴 때 중요한 것은 기존 전략에 대한 고집을 버리고, 빠르게 다른 전략을 사용해야 하는 것인데요.
     
    이러한 현실 전략 변경에 유연하게 대처할 수 있는, 전략 패턴에 대해서 공유드리겠습니다.
     


    2) 햄버거 가게의 문제점

    햄버거 가게라는 추상클래스가 있고 필요한 여러 가지 행위들이 정의되어 있습니다.

    • 빵 주문
    • 로고송 부르기
    • 햄버거 만들기
    • 햄버거 가게 클래스를 상속받은 맥도널드 클래스
    • A 빵 주문 override
    • 로고송 부르기 override
    • 햄버거 만들기 override

     
    혹시 맥도널드 광고를 보신적 있으신가요? 맥도날드 광고 중 유명한 것이 "참깨빵 위의 순 쇠고기 패티 ~ "로 시작하는 노래인데요. 맥도널드를 구현하는 데는 꼭 필수인 기능입니다.
     
    하지만 롯데리아는 어떨까요?

    • 햄버거 가게 클래스를 상속받은 롯데리아 클래스
    • A 빵 주문 override
    • 로고송 부르기 override
    • 햄버거 만들기 override

     
    롯데리아 클래스입니다.
     
    아쉽게도 롯데리아는 맥도널드처럼 특별한 노래가 존재하지 않습니다. 그래서 햄버거 가게에서 오버라이딩한 로고송 부르기는 필요 없습니다.
     
    하지만 상속은 반드시 이루어져야 합니다. 만약 롯데리아 객체의 구현을 정확히 모른 채 사용하면 어떻게 될까요? 노래를 부르기를 기대했지만, 엉뚱하게 노래를 부르지 않는다는 음성이 나올 것입니다.
     
    메서드 이름은 노래를 한다고 되어 있기 때문에 구현체를 직접 봐야 합니다. 이상합니다. 애초에 노래 부르는 행위가 포함되지 않거나, 사용하는 곳에서 알 수 있도록 해야 하지 않을까요?
     
    이러한 기능들이 늘어나게 되면, 구현체에서는 일일이 다르게 구현해줘야 하고 수정되어야 할 것입니다.


    3) 노래 부르는 기능만 인터페이스로 분리?

    햄버거 가게에 정의된 내용들을 강제로 상속하는 것이 문제인 것처럼 보입니다. 그래서 이것을 인터페이스로 분리해 봅니다.

    • 햄버거 가게 추상 클래스에서 노래 부르는 기능 제거
    • 로고송 인터페이스 분리
    • 맥도널드 클래스
    • 기존 햄버거 가게 상속, Jingle(로고송) 인터페이스 구현
    • 롯데리아 클래스
    • 기존 햄버거 가게 그대로 상속
    • 로고송 인터페이스는 구현하지 않으므로 기능 제거됨

     
    노래 부르는 기능을 별도의 인터페이스로 분리했습니다. 그리고 이것을 맥도널드에서만 구현하고, 롯데리아는 구현하지 않습니다. 기능들이 구분되고 좋은 것 같습니다.
     
    하지만 여전히 단점이 존재하는데요.
     
    햄버거 가게 객체에서 광고 노래를 부르는 행위가 필요하지만, 독립적인 인터페이스로 존재합니다. 즉 햄버거 가게 도메인 모델에서 필요한 상태와 행위들의 응집도가 낮습니다.
     
    또 다른 문제점으로는 필요한 클래스에서 모두 오버라이딩 후 직접 구현해야 합니다. 수정이 필요할 때 SOLID 중 OCP(Open-Closed Principle)를 위반하게 됩니다. 즉 행위가 수정될 때 직접 구현체를 수정해줘야 합니다.


    4) 각 기능별 전략 도입!

    햄버거 가게의 상태와 행위들은 이렇습니다.

    • 노래 부르기 / 부르지 않기
    • A 빵 / B 빵 주문
    • 맥도널드 레시피 / 롯데리아 레시피

     
    즉 하나의 행위를 사용하는 방법은 구현체마다 다를 수밖에 없습니다. 이렇게 각기 다른 것을 '전략'이라고 표현하고, 서로 다른 전략들을 구현해 보겠습니다.

    • Jingle(로고송) 인터페이스
    • 인터페이스를 구현한 Singable(노래 가능), UnSingable(노래 불가능) 클래스들

     
    즉 로고송이라는 인터페이스가 존재하고, 해당 인터페이스를 구현한 두 개의 전략으로 나누어집니다. 노래를 부르거나, 부르지 않는 각기의 전략이 구현되었습니다.
     

    • 햄버거 가게 부모 클래스
    • 부모 클래스를 상속한 맥도널드, 롯데리아 가게들

     
    BurgerShop 부모 클래스는 아래와 같이 변경되었습니다.

    • 햄버거 가게 모델 객체에서 노래 부르기, 빵 주문, 버거 레시피 전략들을 가집니다
    • 햄버거 가게에서 직접 구현하는 것이 아니라, 각 전략 객체에 위임합니다
    • 이때 각 전략 객체들을 구성(composition)합니다

     
    McDonalds, Lotteria 자식 클래스는 아래와 같이 변경되었습니다

    • 각각의 햄버거 가게를 만들 때 전략을 선택합니다

     
    기존에는 개별 행위들을 override 후 비즈니스 로직을 직접 구현했어야 합니다. 하지만 객체를 생성하면서 필요한 로직을 직접 외부에서 넣어주는 방식입니다.

    4. 1) 각 전략들을 테스트해 봅시다!

    테스트 코드라고 할 수 없지만, 단순히 흐름만 봐주세요!

    • 맥도널드는 노래를 부르고
    • 빵 주문하고
    • 햄버거를 만듭니다

     
    하지만 만약 맥도널드에서 A빵의 재고가 떨어져서, 급하게 B빵을 주문해야 한다면 어떻게 될까요? 
     
    기존의 코드라면 이럴 것 같은데요.

    // 만약 A 빵이 소진되고, B 빵으로 바꿔야 한다면?
    fun order(): String {
        return "A 브랜드 빵 주문합니다"
    }
    
    // 직접 구현한 비지니스 로직을 변경해야 한다
    fun order(): String {
        return "B 브랜드 빵 주문합니다"
    }

     

    • 직접 A 브랜드 빵 주문 비즈니스 로직을 변경해야 합니다
    • SOLID 중 OCP 위배합니다. 빵 주문 행위를 변경하기 위해 구현체의 로직을 직접 수정해야 합니다

     
    하지만 위와 같이 전략패턴을 사용한다면 간단합니다.

    • A빵 재고가 떨어져서, B빵으로 교체해야 합니다
    • 맥도널드 객체를 다시 생성하면서 B빵을 주입해 줍니다
    • 정상적으로 B 브랜드 빵 주문 로직이 동작합니다

     
    전략 패턴을 이용하여 직접 구현체 로직을 변경하지 않고, 유연하게 다른 전략 객체를 사용합니다. 즉 현실 비즈니스 상황 때문에 코드가 변경되어야 할 때 유연하게 대처할 수 있게 되었습니다. 그저 다른 빵에 대한 전략 객체를 외부에서 넣어주면 됩니다.
     
    롯데리아 가게를 새로 만들 때도 마찬가지입니다.

    4. 2) 꼭 생성자 주입 방식을 사용해야 하나요?

    사실 대부분의 전략 패턴 예제들은 객체 생성 후 각 전략들을 setter를 이용해서 변경해주고 있는데요. 개인적으로 이미 생성된 객체를 setter를 이용해서 변경하는 것을 선호하지 않습니다. 이때는 두 가지의 문제점이 있는데요.

     
     
    그래서 객체를 생성하면서 전략 객체를 외부에서 넣어주는 방식을 사용했습니다. 즉 생성자 DI를 사용했는데요. 이유는 이미 생성된 객체를 외부에서 변경하지 않는 불변성을 최대한 보장하려고 했습니다.
     
    다만 꼭 생성자 DI를 할 필요는 없습니다. setter 대신에 도메인에 맞게 적합한 이름으로 하면 됩니다. 예를 들어 changeBread()와 같은 메서드 이름을 사용한다면, 빵이 떨어졌을 때 변경한다는 표현을 할 수 있을 것 같습니다.


    5) 결론

    처음에 프로그래밍 공부를 시작할 때는 디자인 패턴에 대해서 중요하게 생각하지 않았습니다. 어떻게든 1초라도 단축되는 성능 고도화가 최고이고, 코드는 부지런하게 변경해 주면 되지 않을까 막연히 생각했던 적도 있는데요.
     
    개발자가 된 후 업무를 하며 현실 비즈니스는 계속해서 변경된다는 것을 깨달았습니다. 현실에 맞게 코드도 변경되어야 하는데요. 이러한 상황마다 구현체가 직접 변경되는 것은 다른 곳에서도 사용되고 있을 수 있고, 원래의 로직과 달라질 수 있기 때문에 위험합니다. 또한 어느 순간 정의한 도메인 모델이 어지럽혀지기도 하는데요.
     
    그래서 유연한 설계에 대해서 관심을 가지게 되었고, 이러한 디자인 패턴은 큰 도움이 되고 재밌는 것 같습니다.


    6) 참고 문헌

    GoF의 디자인 패턴 (책)
     
     
     

    반응형

    댓글

Designed by Tistory.