본문 바로가기
Spring

[Spring] 스프링이 사랑한 디자인 패턴 (2)

by jaee_ 2021. 10. 1.
본 글은 📚 스프링 입문을 위한 자바 객체지향의 원리와 이해 를 읽고 정리한 내용입니다.

 

 

이전글 : [Spring] 스프링이 사랑한 디자인 패턴 (1)

 

[Spring] 스프링이 사랑한 디자인 패턴 (1)

본 글은 📚 스프링 입문을 위한 자바 객체지향의 원리와 이해 를 읽고 정리한 내용입니다. 요리와 객체 지향 프로그래밍(OOP)을 비교하면 아래의 표와 같다. 요리 객체 지향 프로그래밍(OOP) 요

yeoonjae.tistory.com


템플릿 메서드 패턴(Template Method Pattern)

볼트이라고 하는 강아지와 키티이라고 하는 고양이를 키운다고 가정해보자. 볼트, 키티와 함께 재밌는 시간을 보내기 위해 클래스를 다음과 같이 2개 만들어보자.

// Dog 클래스
public class Dog {

    public void playWithOwner() {
        System.out.println("놀자!");
        System.out.println("멍! (좋아!)");	// 강아지 울음
        System.out.println("꼬리살랑");
        System.out.println("잘했어!");
    }
}
// Cat 클래스
public class Cat {

    public void playWithOwner() {
        System.out.println("놀자!");
        System.out.println("냐옹!)");	// 고양이 울음
        System.out.println("꼬리살랑");
        System.out.println("잘했어!");

    }
}

위의 코드를 보면 고양이와 강아지의 울음소리 뺴고는 모두 동일한 것을 볼 수 있다. 동일한 코드가 중복되는 것은 상속을 통해 상위클래스로 구현하면 개선할 수 있다. 자, 그럼 코드를 개선해보자. 

// 상위 클래스
public abstract class Animal {
    
    // 템플릿 메서드
    public void playWithOwner() {
        System.out.println("놀자!");
        play();
        runSomething();
        System.out.println("잘했어!");

    }
    
    // 추상 메서드
    abstract void play();

    // Hook(갈고리) 메서드
    void runSomething(){
        System.out.println("꼬리 살랑~ㄴ");
    }
}
// Dog 클래스
public class Dog extends Animal{

    @Override
    void play() {
        System.out.println("멍! 멍!");
    }

    void runSomething(){
        System.out.println("멍~ 꼬리 살랑!");
    }
}
// Cat 클래스
public class Cat extends Animal{

    @Override
    void play() {
        System.out.println("야옹~");
    }

    void runSomething(){
        System.out.println("냐옹~ 꼬리 살랑~");
    }
}
// 실행 클래스
public class Driver {

    public static void main(String[] args) {
        Animal bolt = new Dog();
        Animal kitty = new Cat();

        bolt.playWithOwner();

        System.out.println();
        System.out.println();

        kitty.runSomething();
    }
}


// 결과
놀자!
멍! 멍!
멍~ 꼬리 살랑!
잘했어!


냐옹~ 꼬리 살랑~

상위 추상 클래스인 Animal에는 템플릿 메소드인 playWithOwner() 메서드와 상속받은 하위 클래스에게 구현을 강제하는 추상 메소드인 play()메서드 , 선택적으로 오버라이딩이 가능한 runSomething() 메서드가 있다. 추상 메서드로 선언이 된 메소드는 반드시 하위 클래스에서 구현이 이루어져야 하며 추상메서드가 아닌 메서드는 선택적으로 오버라이딩을 할 수 있다. 

 

이처럼 상위 클래스에 공통 로직을 수행하는 템플릿 메서드와 하위 클래스에서 오버라이딩을 강제하는 추상 메서드 또는 선택적으로 오버라이딩할 수 있는 훅(Hook) 메서드를 두는 패턴을 템플릿 패턴이라고 한다. 즉, 템플릿을 만들어놓고 변경이 필요한 부분만 추상/훅 메서드를 호출하는 형식으로 구현하여 하위 클래스에서 재정의할 수 있도록 만들어놓은 것이라고 할 수 있다. 이를 클래스 시퀀스 다이어그램으로 표현하면 다음과 같다. 

Cat과 Dog의 이름이 바뀐 것 같습니다. 이 점을 감안하고 봐주세요. 

템플릿 메서드 패턴의 구성요소를 정리하면 다음과 같다.

  • 템플릿 메서드 : 공통 로직을 수행, 로직 중에 하위 클래스에서 오버라이딩 당한 추상/훅 메서드를 호출
  • 템플릿 메서드에서 호출하는 추상 메서드 : 하위 클래스가 반드시 오버라이딩해서 재구현 해야한다.
  • 템플릿 메서드에서 호출하는 훅(Hook) 메서드 : 하위 클래스가 선택적으로 오버라이딩 한다. 

템플릿 메서드 패턴을 한 문장으로 정리해 보자. 

 

"상위 클래스의 견본 메서드에서 하위 클래스가 오버라이딩한 메서드를 호출하는 패턴"

 

템플릿 메서드 패턴은 의존 역전 원칙(DIP)을 활용하고 있다. 


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

팩터리 메서드라는 것에 대해 생각해보자. 팩터리는 공장, 공장은 물건을 생성하는 곳. 이를 프로그래밍적으로 접근해보면 물건은 객체, 객체를 만드는 것은 메서드(생성자도 메서드)라고 할 수 있다. 즉, 팩터리 메서드란 객체를 만들어 반환하는 메서드라고 생각할 수 있다. 여기서 패턴이 붙으면 하위 클래스에서 팩터리 메서드를 오버라이딩해서 객체를 반환하게 하는 것을 의미한다. 위에서 만들었던 Dog, Cat 클래스를 활용해 장난감만 추가하여 예제 코드를 작성해보자. 

// 추상클래스 Animal
public abstract class Animal {
    // 추상 팩터리 메서드
    abstract AnimalToy getToy();
}
// 추상클래스 AnimalToy = 팩터리 메서드가 생성할 객체의 상위 클래스
public abstract class AnimalToy {

    abstract void identify();
}
// Dog 클래스
public class Dog extends Animal{

    // 추상 팩터리 메서드 오버라이딩
    @Override
    AnimalToy getToy() {
        return new DogToy();
    }
}
// Cat 클래스
public class Cat extends Animal {

    // 추상 팩터리 메서드 오버라이딩
    @Override
    AnimalToy getToy() {
        return new CatToy();
    }

}
// Dog의 장난감 클래스
public class DogToy extends AnimalToy{

    @Override
    void identify() {
        System.out.println("나는 테니스 공! 강아지의 친구");
    }
}
// Cat 의 장난감 클래스
public class CatToy extends AnimalToy{

    @Override
    void identify() {
        System.out.println("난 고양이니까 캣타워 갖고싶어.");
    }
}
// 실행클래스
public class Driver {

    public static void main(String[] args) {
        // 팩터리 메서드를 보유한 객체들 생성
        Animal boli = new Dog();
        Animal kitty = new Cat();

        // 팩터리 메서드가 반환하는 객체들
        AnimalToy  boltBall = new DogToy();
        AnimalToy  kittyTower = new CatToy();

        // 팩터리 메서드가 반환한 객체들을 사용
        boltBall.identify();
        kittyTower.identify();
    }
}

// 결과
나는 테니스 공! 강아지의 친구
난 고양이니까 캣타워 갖고싶어.

위 코드의 시퀀스 다이어그램을 보자.

 

팩터리 메서드 패턴을 한 문장으로 나타내면 다음과 같다. 

 

"오버라이드된 메서드가 객체를 반환하는 패턴"

 

팩터리 메서드 패턴은 의존 역전 원칙(DIP)을 활용하고 있다. 


전략 패턴(Strategy Pattern)

디자인 패턴의 꽃인 전략 패턴이다. 잘 기억해두자. 전략 패턴을 구성하는 세 요소는 반드시 기억해두어야 한다. 

  • 전략 메서드를 가진 전략
  • 전략 객체를 사용하는 컨텍스트 (전략 객체의 사용자/소비자)
  • 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트 (제3자, 전략 객체의 공급자)

전략 패턴의 개념도

클라이언트는 다양한 전략 중 하나를 선택해 생성한 후 컨텍스트에 주입한다. 

 

군인이 있다고 가정하자. 그리고 그 군인이 사용할 무기가 있다고 하자. 보급 장교가 무기를 군인에게 지급해 주면 군인은 주어진 무기에 따라 전투를 수행하게 된다. 이 이야기를 전략 패턴에 따라 구분해보면 무기는 전략이 되고, 군인은 컨텍스트, 보급 장교는 제 3자, 즉 클라이언트가 된다. 이를 자바 코드로 구현해보자. 우선 다양한 전략을 공통된 방식으로 사용하기 위해 인터페이스로 정의한다. 

// 전략 인터페이스
public interface Strategy {
    public abstract void runStrategy();
}
// 총
public class StrategyGun implements Strategy{

    @Override
    public void runStrategy() {
        System.out.println("탕! 탕!");
    }
}
// 검
public class StrategySword implements Strategy{

    @Override
    public void runStrategy() {
        System.out.println("챙 챙챙");
    }
}
// 활
public class StrategyBow implements Strategy{

    @Override
    public void runStrategy() {
        System.out.println("피슝~ , 활");
    }

}
// 무기를 사용할 군인 (전략을 사용할 컨텍스트)
public class Soldier {

    void runContext(Strategy strategy) {
        System.out.println("전투 시작");
        strategy.runStrategy();
        System.out.println("전투 종료");
    }
}
// 무기를 조달해서 군인에게 지급해 줄 보급 장교
// = 전략을 생성해 컨텍스트에 주입할 클라이언트
public class Client {

    public static void main(String[] args) {
        Strategy strategy = null;
        Soldier rambo = new Soldier();

        // 총을 람보에게 전달해서 전투를 수행하게 한다.
        strategy = new StrategyGun();
        rambo.runContext(strategy);

        System.out.println();

        // 검을 람보에게 전달해서 전투를 수행하게 한다.
        strategy = new StrategySword();
        rambo.runContext(strategy);

        System.out.println();

        // 활을 람보에게 전달해서 전투를 수행하게 한다.
        strategy = new StrategyBow();
        rambo.runContext(strategy);
    }
}

// 결과
전투 시작
탕! 탕!
전투 종료

전투 시작
챙 챙챙
전투 종료

전투 시작
피슝~ , 활
전투 종료

위 코드처럼 전략을 다양하게 변경해가면서 컨텍스트를 실행할 수 있다. 전략 패턴은 다양한 곳에서 다양한 문제 상황의 해결책으로 사용된다. 템플릿 메서드 패턴과 유사하며 같은 문제의 해결책으로 상속을 이용하는 템플릿 메서드 패턴과 객체 주입을 통한 전략 패턴 중에서 선택/적용할 수 있다. 

 

단일 상속만이 가능한 자바에서는 상속이라는 제한이 있는 템플릿 메서드 패턴보다는 전략 패턴이 더 많이 활용된다. 

 

전략패턴을 한 문장으로 정리하면 다음과 같다.

 

"클라이언트가 전략을 생성해 전략을 실행할 컨텍스트에 주입하는 패턴"

 

전략 패턴은 개방 폐쇄 원칙(OCP) 과 의존 역전 원칙(DIP)이 적용되었다. 


템플릿 콜백 패턴(Template Callback Pattern - 견본/회신 패턴)

템플릿 콜백 패턴은 전략 패턴의 변형으로, 스프링의 3대 프로그래밍 모델 중 하나인 DI(의존성 주입) 에서 사용하는 특별한 형태의 전략 패턴이다. 템플릿 콜백 패턴은 전략 패턴과 모든 것이 동일한데 전략을 익명 내부 클래스로 정의해서 사용한다는 특징이 있다. 위의 전략패턴의 코드를 템플릿 콜백 패턴의 코드로 변경해보자. 

// 전략 인터페이스
public interface Strategy {
    public abstract void runStrategy();
}
// Soldier 코드
public class Soldier {

    void runContext(String weaponSound) {
        System.out.println("전투 시작");
        executeWeapon(weaponSound).runStrategy();
        System.out.println("전투 종료");
    }
    
    // 전략을 생성하는 코드가 Soldier 클래스 내부로 옮겨짐
    private Strategy executeWeapon(final String weaponSound) {
        return new Strategy() {
            @Override
            public void runStrategy() {
                System.out.println(weaponSound);
            }
        };
    }
}

익명 내부 클래스를 사용하기 때문에 StrategyGun, StrategySword, StrategyBow 클래스는 필요없다. 전략을 생성하는 코드를 Soldier 클래스 내부로 구현했다.

public class Client {

    public static void main(String[] args) {
        Soldier rambo = new Soldier();

        rambo.runContext("총 탕탕 총총");

        System.out.println();

        rambo.runContext("칼! 칼 스윽");

        System.out.println();

        rambo.runContext("도끼 도독 퍽");
    }
}


//결과
전투 시작
총 탕탕 총총
전투 종료

전투 시작
칼! 칼 스윽
전투 종료

전투 시작
도끼 도독 퍽
전투 종료

클라이언트에서 구현했으면 중복됐을 전략을 생성하는 코드를 컨텍스트로 이관했기 때문에 클라이언트 클래스의 코드에서는 총을 쏘는 효과음인지, 칼을 쓰는 효과음인지, 도끼를 쓰는 효과음인지만 넣어주면 된다. 스프링은 이런 형식으로 리팩터링된 템플릿 콜백 패턴을 DI에 적극 활용하고 있다. 따라서 스프링을 이해하고 활용하기 위해서는 전략 패턴고 ㅏ템플릭 콜백 패턴, 리팩터링된 템플릿 콜백 패턴을 잘 기억해두어야 한다. 

 

템플릿 콜백 패턴을 한 문장으로 정리하면 다음과 같다. 

 

"전략을 익명 내부 클래스로 구현한 전략 패턴"

 

템플릿 콜백 패턴은 개방 폐쇄 원칙(OCP)과 의존 역전 원칙(DIP)이 적용된 설게 패턴이다.

 

댓글