Backend/Design Pattern

[CS스터디-헤드퍼스트 디자인 패턴] 1. 전략패턴

Emil :) 2022. 12. 10. 13:12
728x90
반응형

목차

이 글은 Notion에서 작성 후 재편집한 포스트입니다.


서론


스터디원들과 책 하나를 정해서, 각자 파트를 공부하고 주 1회 미팅마다 발표하는 시간을 갖기로 했습니다.
그렇게 해서 결정된 책은 [헤드퍼스트 디자인 패턴] 이라는 책입니다.

안그래도 디자인 패턴에 대한 무조건적인 두려움이 있었는데, 이번 기회에 조금씩 알아가보는 시간이 되었으면 좋겠습니다.
그림으로 이루어져 있어서 책은 두껍지만 생각보다 술술 읽혔기에, 정말 재밌게 공부했네요.

참고


https://www.coupang.com/vp/products/6403382250?itemId=13700178772&vendorItemId=80951605437&src=1042503&spec=10304982&addtag=400&ctag=6403382250&lptag=10304982I13700178772&itime=20221210115503&pageType=PRODUCT&pageValue=6403382250&wPcid=15730553344490846187221&wRef=&wTime=20221210115503&redirect=landing&gclid=Cj0KCQiA1sucBhDgARIsAFoytUsR3Vik_OevxodcLNtgECFd34jaUngo-H_vE0iSiNhcnK5-8bdglOUaAhjOEALw_wcB&campaignid=18626086777&adgroupid=&isAddedCart= 

 

헤드 퍼스트 디자인 패턴:14가지 GoF 필살 패턴!

COUPANG

www.coupang.com

https://y-oni.tistory.com/53#toc111

 

GoF(Gang of Four)란? 디자인패턴

GoF(Gang of Four) 란? 《디자인 패턴》(Design Patterns, ISBN 0-201-63361-2)은 소프트웨어 설계에 있어 공통된 문제들에 대한 표준적인 해법과 작명법을 제안한 책이다. 이 분야의 사인방(Gang of Four, 줄여 GoF)

y-oni.tistory.com

본론


GoF가 뭔가요?


이 책에 제목부터 GoF를 말하고 있습니다. GoF가 뭘 의미하는건지 몰라서, 이에 대한 공부가 선행되어야 할 것 같아 찾아본 결과, 크게 별 뜻은 없고 소프트웨어 설계에 있어 공통된 문제들에 대한 표준적인 해법과 작명법을 제안한 4인방이 있는데, 그들을 일컫는 것이 GoF(Gang of Four) 라고 합니다. 이들이 낸 책 이름이 [디자인 패턴] 이라서 [GoF 디자인 패턴] 이라고 합니다.

 

오리 시뮬레이션을 예제로 한 디자인 패턴의 필요성 이해


책에 있는 내용을 각색하여 정리해보겠습니다.

오리 시뮬레이션 게임을 운영하는 사람이 되었다고 가정해봅니다.
오리를 나타내는 Duck 이라는 슈퍼클래스를 만들었고, 그 클래스를 활용 및 상속시켜 여러 종류의 오리를 만들었습니다.

public class Duck {
    // 꽥꽥, 헤엄치기를 나타내는 메소드
    quack();
    swim();
    // 모든 오리의 모양이 달라 이를 표현하는 display() 메소드
    display();
}

 그리고, 이 클래스를 상속받는 오리의 종마다 여러가지 Duck들이 있습니다.

public class MallardDuck extends Duck {
	display() { //MallardDuck의 적당한 모양을 표시.. }
}
public class RedHeadDuck extends Duck {
	display() { //MallardDuck의 적당한 모양을 표시.. }
}

이렇게 만들어놨는데, 회사 임원진들이 여러분을 압박합니다. 다른 회사와의 차별점을 두기 위해 오리가 날 수 있어야 한다는 것이죠. 그래서 우리는 슈퍼클래스 Duck에 fly() 메소드를 추가하고자 합니다.

public class Duck {
    // 꽥꽥, 헤엄치기를 나타내는 메소드와 모든 오리의 모양이 달라 이를 표현하는 display() 메소드
    quack();
    swim();
    // ★ 추가하기!
    fly();
    
    display();
}

모든 오리들은 날아야 한다고 하니... 슈퍼클래스에 fly() 기능 하나만 넣어주면 모든 오리들은 이를 상속받아 사용하므로, 간단하게 추가만 해주면 되기 때문입니다.

그런데 치명적인 결함이 발생합니다. 동물이 아닌 고무 오리 러버덕 도 날아버리는 현상이 게임 내에서 발생합니다.

굉장히 강해진 RubberDuck
public class RubberDuck extends Duck {
    fly() { // 아무 것도 하지 않도록 오버라이딩 }
    quack() { // 러버덕은 '꽥꽥'이 아닌 '삑삑'을 내도록 quack을 오버라이딩 }
    display() { // MallardDuck의 적당한 모양을 표시.. }
}

 게다가, 소리도 못내는 나무 오리를 추가해야 하는 일까지 생깁니다. 그럼 다시 quack() 메소드를 오버라이딩 해야합니다.

public class DecoyDuck extends Duck {
    fly() { // 아무 것도 하지 않도록 오버라이딩 }
    quack() { // 아무것도 하지 않도록 오버라이딩 }
    display() { // MallardDuck의 적당한 모양을 표시.. }
}

이를 통해, 이런 케이스는 상속으로 해결하는 방법이 아니라는 것을 알 수 있습니다.
물론 가능은 합니다. 그런데 유지보수 난이도가 극악일 것입니다.
따라서 이렇게 해결하기로 정합니다.

fly() 를 Duck 슈퍼클래스에서 빼고
fly() 메소드가 들어있는 Flyable 인터페이스를 만듭니다.
이렇게 하면 날 수 있는 오리에게만 그 인터페이스를 구현해서 fly() 메소드를 넣을 수 있습니다.
모든 오리가 꽥꽥거리는건 아니니, Quackable이라는 인터페이스도 같이 만들도록 합니다.
public interface Flyable {
    fly()
}

public interface Quackable {
    quack()
}

그런데, 이렇게 하면 코드 중복이라는 심각한 문제에 맞닿습니다. 인터페이스는 재사용이 불가능하기 때문입니다.
게다가 만약 날아가는 동작을 조금 바꾸려면 Duck의 서브클래스에서 날아다닐 수 있는 수많은 코드를 전부 고쳐야 합니다.
어떤 오리는 '파닥파닥' 날고, 어떤 오리는 '퍼덕퍼덕' 날아야 하는걸 구현해야 한다면, 작업량이 상당할 것입니다.

 

그렇다면 어떻게 해결해야 할 것인가?


위 상황을 통해 우리는 다음과 같은 문제점을 도출해낼 수 있습니다.

  • 상속을 활용하면, 기능을 수행하지 않길 원하는 다른 코드들에도 영향을 끼친다.
  • 인터페이스를 사용하는 방법은 변경사항이 생길 때마다 서브 클래스를 모두 찾아서 직접 고쳐야 한다.

이런 상황에 어울리는 디자인 법칙이 있습니다.

애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.

 

달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 '캡슐화' 합니다.
이렇게 한다면 코드를 변경하는 과정에서 의도치 않게 발생하는 일을 줄이면서 시스템의 유연성을 향상시킬 수 있습니다.

다시말해, 코드에 새로운 요구 사항이 있을 때마다 바뀌는 부분이 있다면 분리해야 합니다.

그렇다면 이제 바뀌는 부분과 그렇지 않은 부분을 분리하도록 합니다.

가장 큰 것은 fly()와 quack() 입니다. 이러한 문제를 제외하면 Duck 클래스 자체는 대체적으로 잘 작동하고 있기 때문입니다.
따지고 보면 뭔가 문제는 있을 수 있겠지만, 일단은 이 예제의 비즈니스 로직에 한해서 이야기 해봅시다.

 

1) 클래스 집합 구현하기


위 상황을 통해 우리는 다음

flry()와 quack() 2개의 클래스 집합(set)을 만듭니다. 하나는 나는 것과 관련된 부분이고, 다른 하나는 꽥꽥거리는 것과 관련된 부분입니다.
각 클래스 집합에는 각각의 행동을 구현한 것을 전부 집어넣습니다.
예를 들자면 다음과 같습니다.

  • 꽥꽥거리는 행동을 구현하는 클래스
  • 삑삑거리는 행동을 구현하는 클래스
  • 아무 소리도 내지 않는 행동을 구현하는 클래스

fly()와 quack()은 Duck 클래스 안에 있는 오리 종류에 따라 달라지는 부분입니다.
fly()와 quack()을 Duck 클래스로부터 분리하려면 2개의 메소드를 모두 Duck 클래스에서 끄집어내서 각 행동을 나타낼 클래스 집합을 새로 만들어야 합니다.

 

2) 분리된 클래스 집합 디자인 & 구현하기


위에서 2가지 유형에 따라 클래스를 분리하였으며, 그에 따라 나는 행동과 꽥꽥거리는 행동을 구현하는 클래스 집합을 디자인 해야 합니다.
우선 최대한 유연하게 만드는 것이 좋습니다. 그리고 Duck의 인스턴스에 행동을 할당할 수 있어야 합니다.
예를 들어 앞서 말한 MallardDuck 인스턴스를 새로 만들고, 특정 형식의 나는 행동으로 초기화하는 방법도 생각해 볼 수 있습니다.
그리고 오리의 행동을 동적으로 바꾸면 더 좋습니다. Duck클래스에 행동과 관련된 setter()를 포함하여, 프로그램 실행 중에도 MallardDuck의 나는 행동을 바꿀 수 있으면 좋겠습니다.

일단 이렇게 목표를 정해 놓고 두 번째 디자인 원칙을 살펴봅니다.

구현보다는 인터페이스에 맞춰서 프로그래밍 한다

 각 행동은 인터페이스로 표현(FlyBehavior, QuackBehavior)로 표현하고, 이런 인터페이스를 사용해서 행동을 구현합니다.
이제부터 Duck의 행동은 특정 행동 인터페이스를 구현한 별도의 클래스 안에 들어있습니다.
그렇게 되면 Duck 클래스에서는 그 행동을 구체적으로 구현할 필요가 없습니다.

public interface FlyBehavior{
    fly();
}

public class FlyWithWings implements FlyBehavior {
	public void fly() { //나는 것 구현 }
}

public class FlyNoWay implements FlyBehavior {
	public void fly() { //날지 못하도록 구현 }
}

FlyBehavior 인터페이스의 구현체인 FlyWithWings와 FlyNoWay에서 구현을 담당해줍니다.
마찬가지로 Quack()도 동일하게 만들어봅니다.

public interface QuackBehavior{
    public void quack();
}

public class Quack implements QuackBehavior {
	public void quack() { //꽥 소리 내는 것 구현 }
}

public class MuteQuack implements QuackBehavior {
	public void quack() { //꽥 소리 안내는 것 구현 }
}

public class SQuack implements QuackBehavior {
	public void quack() { //'꽥'이 아닌 '삑' 소리 내는 것 구현 }
}

이렇게 된다면, 다른 형식의 객체에서도 나는 행동과 꽥꽥거리는 행동을 재사용할 수 있습니다. 그런 행동이 더이상 Duck클래스 안에 숨겨져 있지 않기 때문입니다.

또한, 기존의 행동 클래스를 수정하거나 날아다니는 행동을 사용하는 Duck클래스를 전혀 건드리지 않고도 새로운 행동을 추가할 수 있습니다. 따라서, 상속을 쓸 때 떠안게 되는 부담을 전부 떨칰고 재사용의 장점을 누릴 수 있습니다.

 

3) 오리 행동 통합하기


가장 중요한 점은 나는 행동과 꽥꽥거리는 행동을 Duck 클래스(또는 그 서브클래스) 에서 정의한 메소드를 써서 구현하지 않고 다른 클래스에 위임한다는 것입니다.

각 오리 객체에서는 실행 시에 FlyWithWIngs, Squeak 등과 같은 변수들의 레퍼런스를 다형적으로 설정합니다.
나는 행동과 꽥꽥 거리는 행동은 FlyBehavior와 QuackBehavior 인터페이스로 옮겨놨으니, Duck 클래스와 모든 서브클래스에서 fly()와 quack() 메소드를 제거합니다.
대신 performFly()와 performQuack() 이라는 메소드를 넣습니다.

public abstract class Duck {
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;
    
    public Duck() { }
    public abstarct void display();
    
    public void performFly(){
    	//행동클래스에 위임
    	flyBehavior.fly(); 
    }
    
    public void performQuack() {
    	//행동클래스에 위임
    	quackBehavior.quack();
    }
    
    public void swim() { //헤엄치는 내용 }
}

이런식으로 사용하게 됩니다만, 이렇게 별도로 사용하는 이유는 다형성 때문인 것 같습니다.
발표를 진행하며 스터디원들과 왜 이렇게 되는건지 논의해보고자 합니다.

 

결론


이렇게 처음으로 디자인 패턴을 공부해봤습니다.

그리고 이 패턴은 전략 패턴 이라고 합니다!

생각했던 것보다 이해도 쉽고 재밌게 공부했습니다.
이런 내용들을 업무에서도 적용해보고 싶다 라는 생각이 들게 됐네요.
새로운 API를 설계할 때 이런 식으로 패키지 구조를 구성한다면 유지보수가 굉장히 원활해질 것 같습니다.

 

 

구독 및 하트는 정보 포스팅 제작에 큰 힘이됩니다♡

728x90
반응형