ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 전략 패턴
    디자인 패턴 2022. 8. 23. 08:39

    들어가기전에...

    디자인 패턴을 공부 & 사용하는 이유

    코드 짠걸로 의사소통을 할 때,

    "A라는 클래스가 존재 할 때,  A클래스에 새로운 데이터가 들어오면 A클래스에 의해 관리되는 객체들에게 

    이 데이터를 전송해줄 수 있고, 관리되는 객체들은 언제든지 관리에서 이탈하거나 참여할 수 있는 코드를

    만들었어"

    라는 말은 사실 '옵저버패턴'을 이야기 한 것입니다.

    '옵저버패턴'을 정확히 이해하고있으면 앞의 장황한 설명보다 훨씬 빠르게 의사전달을 할 수 있습니다.

     

    코드를 수정 하거나 작성할 때,

    나의 설계디자인이 잘 못 된것은 아닌지 되짚어 볼 수 있습니다.

    많은 사람들이 오랜 시간 고민해서 찾아낸 디자인 패턴은 유연성, 재사용성, 관리하기 쉬운 시스템을 만들 수 있게

    도와줍니다.

    물론, 무조건 디자인패턴을 도입하는게 정답은 아닙니다. 적어도 가이드라인이 될 수 있습니다.

     

    소스 분석 할 때,

    수 많은 라이브러리들이 패턴들을 사용해서 작성된 것이 많으므로, 패턴에 대해 잘 알고만 있으면 소스 분석을

    하는 시간을 줄일 수 있습니다.

     

     


    자동차 만들기

    자동차를 추상화하여 Car클래스를 만들어봤습니다.

     

     

    간단하게 방향전환을 할 수 있는 changeDirection 메서드와 move메서드를 만들어 보았습니다.


    다양한 차종 만들기

    이 자동차를 상속받아 특수한 기능이 추가된 자동차를 만들어보겠습니다.

     

    자동차 메인 클래스를 상속받아서 스텔스(투명) 기능이 있는 차와 부스터를 사용할 수 있는 차를 만들었고,

    그외에 자동차들도 생성하였습니다.

     

    자 여기 까지는 이상이 없어보입니다. (구체적인 메서드를 제외하고)

     

    객체지향적으로 잘 설계 되었구나? 를 할 수도 있습니다.


    차에 기능 추가하기

    이제 자동차에 색상을 넣기로 했습니다.

     

    색상은 공통적인 요소이니 자동차 메인클래스에 넣어두면 편하겠죠??

     

     

    자 색상을 변경할 수 있는 메서드를 만들었습니다. 

     

    만들고보니 무언가 이상한 느낌이 들지 않나요??

     

    그렇습니다. 스텔스 자동차는 색상변경이 필요하지 않을 뿐만 아니라 변경되어서는 안될 것 입니다.

    뿐만아니라 만약 상속 받은 자동차중 색상변경이 불가능한 차량들이 있다면 어떻게 할까요??

    그리고 움직이지 않는 차종들을 만들어 달라고 하면 어떻게할까요??? (장식용)

    상속의 기능중 오버라이드를 사용해 볼까요?

     

    만약에 상속받는 클래스가 수십개 수백개가 있다면 모두 오버라이드를 이용해서 구현을 다시 해줄까요??

     

    상속을 받게 되면 다음과 같은 단점이 생기게 됩니다.

    1. 서브클래스에서 코드가 중복된다 (오버라이드)
    2. 모든 서브클래스의 구현내용을 알기 힘들다.
    3. 실행 시 특징을 바꾸기 힘들다.
    4. 코드를 변경 하였을때, 다른 서브클래스에 악영향이 갈 수 있다.

    다른방법을 생각해봅시다.

     


    인터페이스로 구현해보기

    인터페이스를 만들어서 그 인터페이스를 구현할 수 있게 만들어 봅시다.

    이동이 가능한 차량, 색상이 변경 가능한 차량 분리가 되어 깔끔해진 것처럼 보이지만,

    이동이 가능한 차량들은 모두 move()라는 메서드의 구체적인 내용을 구현해주어야 하고

    색상이 변경가능한 차량도 마찬가지 입니다.

    따라서 무자비한 코드 중복이 생깁니다.

     


    해결책 첫번째 : 바뀌는 부분을 뽑아서 캡슐화하자

    디자인 원칙중 다음과 같은 원칙이 있습니다.

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

     

    최초의 Car로 돌아가서 분리를 해봅시다.

     

     

    여기서 move()changeColor()는 바뀌는 부분입니다.

    이 두 행동을 Car클래스로부터 분리해서 각 행동을 나타낼 클래스들을 새로 만들어야 합니다.

    Car 클래스는 다음과 같이 바뀌게 됩니다.

    분리를 끝냈습니다. 다음으로 넘어가겠습니다.

    해결책 두번째 : 인터페이스에 맞춰서 프로그래밍 하자

    디자인원칙 두번째입니다.

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

     

    엥? 아까 인터페이스로 개발하였잖아?? 라고 하실 수 있습니다.

     

    여기서 "인터페이스에 맞춰서 프로그래밍 한다"는 말은 "상위 형식에 맞춰서 프로그래밍 한다"라는 말입니다.

    이말인 즉슨 인터페이스를 반드시 사용하라는 말이 아닙니다.

    핵심은 상위 형식에 맞춰 프로그래밍해서 다형성을 활용한다는 것입니다. 

    코드를 예시로 들면 다음과 같습니다.

     

    BoosterCar car = new BoosterCar();  //BoosterCar로만 생성 받을 수 있다. 
                                        //다른종류의 카로 대체될수 없다.
    
    Car car = new BoosterCar();  //BoosterCar뿐만아니라 다른 Car가 들어와도 상관없다.
    car.changeColor(color);
    
    car = new StealthCar(); // 예를 들면 스텔스 카를 넣어도 된다.
    car.changeColor(); // StealthCar에 changeColor를 오버라이드 하여 빈 메소드를 호출한다.
    
    Car car = getStealthCar();  // 더 바람직한 방법이다. (구현된 객체를 실행시 대입)

    다형성을 활용해서 위에서 분리한 행동을 구현해보겠습니다.

     

    move()  에 관련된 인터페이스와 2개의 구현체

    changeColor() 에 관련된 인터페이스와 2개의 구현체를 각각 만들었습니다.

     

    이제 이 행동들을 어떻게 자동차마다 알맞게 셋팅을 할 수 있을까요??

     


    자동차 기능 통합하기

     

    Car클래스를 다시 한번 만들어 봅시다.

     

     

    Car 클래스에서 move() 메소드를 구현하지 않고 MoveBehavior와 ChangeColorBehavior에 위임할 수 있게 만들었습니다.

     

    그럼 move()나 changeColor는 어떻게 실행할까요??

     

    public abstract class Car {
      MoveBehavior moveBehavior;
      
      performMove() {
        moveBehavior.move();
      }
    }

     

    위와 같이 코드를 작성하고, car.performMove()를 호출하면 moveBehavior의 구현체에 따라 기능이 호출됩니다.

     

    moveBehavior는 어떻게 설정하는지는 다음과 같습니다.

    public class StealthCar extends Car {
      
      public StealthCar() {
        moveBehavior = new Mover();
        changeColorBehavior = new NotChangeColor();
      }
    }

    생성자 호출시 필드로 생성해주는 방법도 있고

     

    세터를 사용하여 

    moveBehavior.setMovcBehavior(new NotMove());

    사용중에 해당 차종을 움직일 수 없게 만들 수도 있습니다.

     

     


    정리하기

    Car와 MoveBehavior, ChangeColorBehavior의 관계를 생각해봅시다.

    Car는 MoveBehavior와 changeColorBehavior를 가지고 있고, 각각으로 부터의 행동을 위임받습니다.

     

    이러한 관계를 "A는 B를 가지고 있다" 라는 관계가 성립하는데,

    이런식으로 Car가 다른 클래스의 기능을 이용하는 것을 구성을 이용한다라고 할 수 있습니다.

     

    구성은 매우 중요한 테크닉이면서 디자인 원칙중 세번째를 말합니다.

    상속보다는 구성을 활용한다.

     

    조금전에 본 것 처럼, 구성을 활용하면 유연성있는 코드를 만들 수 있습니다.

    또한, MoveBehavior와 ChangeColorBehavior를 각각 하나의 알고리즘 군이라고 생각하면

    이 알고리즘 군을 별도의 클래스 집합으로 캡슐화 할 수 있으며, 구성요소로 사용하는 객체에서는 

    생성시나 실행시에 알고리즘을 바꿀 수 있습니다.

     

     

    이 모든 설명은 '전략 패턴'을 설명하기 위한 것이었습니다.

    이 패턴에 대한 정의는 다음과 같습니다.

    전략 패턴(Strategy Pattern)은 알고리즘 군을 정의하고 캡슐화 해서 각각의 알고리즘 군을 수정해서 쓸 수 있게
    해줍니다.
    전략패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있습니다.

     

    단점이라면

    1. 구조가 복잡해진다 - 한 두개의 알고리즘 만 필요한 경우에도 위와 같은 디자인패턴을 적용하면                             
      인터페이스가 많아져 프로젝트 구조가 복잡해질 수 있습니다.
    2. 클라이언트가 사용할 구체적인 전략(구현된 클래스)를 알고 사용해야 하기 때문에 이에 대한 
      올바른 이해가 필요합니다.
    3. 현재 new Move()와 같이 세팅을 직접 관여하고 객체 생성을 남발하는데 이에대한 새로운 방법이 필요합니다.
      - 추후 다른 패턴을 얹어서 수정해보도록 하겠습니다.

     


     

    사용 예제 찾아보기

    스프링에서..

     

        public static void main(String[] args) {
            SpringApplication.run(TransactionStudyApplication.class, args);
        }

    해당 코드는 spring boot 프로젝트에서 스프링 어플리케이션을 실행하기 위한 메서드입니다.

    SpringApplication의 생성자를 한번 살펴 보겠습니다.

     

    	public SpringApplication(Class<?>... primarySources) {
    		this(null, primarySources);
    	}
    
    	@SuppressWarnings({ "unchecked", "rawtypes" })
    	public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    		this.resourceLoader = resourceLoader;
    		Assert.notNull(primarySources, "PrimarySources must not be null");
    		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    		this.webApplicationType = WebApplicationType.deduceFromClasspath();
    		this.bootstrapRegistryInitializers = new ArrayList<>(
    				getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
    		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    		this.mainApplicationClass = deduceMainApplicationClass();
    	}

     

    이중 하나인 ResourceLoader 가 어떠한 구조를 가지고 있는지 확인해보겠습니다.

    public interface ResourceLoader {
    
    	String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
    
    	Resource getResource(String location);
        
    	@Nullable
    	ClassLoader getClassLoader();
    
    }

      

    해당 소스를 확인해보니 인터페이스인 것을 확인했습니다.

     

    그렇다면 SpringApplication 클래스는 해당 인터페이스를 인스턴스 변수로 추가해놓고 다형성을 이용하여 

    다양한 ResourceLoader 구현체를 사용한다는 것을 알 수 있었습니다.

     

    활용은 다음과 같이 한다는 것을 알 수 있습니다.

     

    	protected void postProcessApplicationContext(ConfigurableApplicationContext context) {
            ...
    		if (this.resourceLoader != null) {
    			if (context instanceof GenericApplicationContext) {
    				((GenericApplicationContext) context).setResourceLoader(this.resourceLoader);
    			}
    			if (context instanceof DefaultResourceLoader) {
    				((DefaultResourceLoader) context).setClassLoader(this.resourceLoader.getClassLoader());
    			} //여기서 리로스 로더의 메소드를 호출!
    		}
    		...
    	}

     

    해당 로직은 ResourceLoader 가 null값이 아닌경우 context에 따라서 리소스 로더를 다시세팅해주거나,

    리소스로더가 가지고있는 getClassLoader()에 의해 클래스 로더를 세팅해주는 것을 해주는 메소드입니다.

     

    이렇게 되면 어떠한 ResourceLoader 구현체가 오더라도 해당 메소드를 구현하고 있기 때문에 오류가 날일이 없을

    것입니다.

     

     

     

     

     

    상기글은 '헤드퍼스트 디자인 패턴 개정판'을 읽고 정리하였습니다.

    헤드 퍼스트 디자인 패턴 : 네이버 도서 (naver.com)

     

    헤드 퍼스트 디자인 패턴 : 네이버 도서

    네이버 도서 상세정보를 제공합니다.

    search.shopping.naver.com

    스프링 예제를 찾는것은 해당블로그를 참조하였습니다.

    [Spring & Design Pattern] Spring에서 발견한 Design Pattern_strategy pattern (tistory.com)

     

    '디자인 패턴' 카테고리의 다른 글

    옵저버 패턴  (0) 2022.09.07
Designed by Tistory.