Design Pattern

14 분 소요




디자인패턴

일종의 설계 템플릿으로, 구체적인 구현방법을 제시하는것이 아니라 어떤 문제에 대해
어떤 구조와 접근법이 적절한지에 대한 가이드라인이다.
소프트웨어가 점점 복잡해지면서 다양한 문제가 발생하게 되었고, 이런 문제를 해결하기 위해
많은 개발자들이 각자의 방식으로 접근하다보니 비슷한 문제를 다른방식으로 해결하는 경우가 많았다.
결국 유지보수 등의 어려움을 겪게되고, 이러한 문제를 해결하기 위해 많은 개발자들의 전문지식을 모아
이미 검증된 방식을 템플릿 형태로 모아 가이드라인을 제시하게 되었다.
이로써, 소프트웨어의 유연성, 확장성, 생산성, 픔질 향상에 큰 도움이 된다.





어댑터 패턴

Adapter Pattern
호환되지 않는 여러 객체를 하나의 인터페이스 로 묶어서 사용할 수 있는 디자인 패턴이다.
이를통해, 재사용성, 유지보수성 을 향상시킬 수 있다.


인터페이스가 호환되지 않는 클래스들을 함께 사용하기 위해 사용한다.

  • 기존 코드나 라이브러리를 재사용하면서 새로운 시스템을 구축할 때
  • 호환성없는 두 클래스를 연결해 사용해야 할때
  • 인터페이스나 메소드가 다른 두 클래스 사이에서 호환성 문제를 해결해야 할 때


trade-off


장점

  • 기존코드의 수정없이 호환되지 않는 객체를 연결할 수 있다.
  • 객체간의 결합도를 줄일 수 있다.
  • 호환성이 없는 여러 객체를 하나의 어댑터로 묶어서 사용할 수 있다.


단점

  • 객체가 추가될 때 마다 어댑터 클래스가 늘어난다.
  • 어댑터가 객체를 감싸는 구조를 가지기 때문에 어댑터로 연결된 객체는 성능상 손실이 있을 수 있다.
  • 코드가 복잡해진다.


구현


일반적인 방식

public class Animal {
    public void makeSound() {
        System.out.println("Generic animal sound");
    }
}

// Wolf 클래스는 Animal 클래스와 호환되지 않는 인터페이스를 가지고 있다.
public class Wolf {
    public void howl() {
        System.out.println("Howl");
    }
}

/*
WolfAdapter 클래스는 Animal 클래스를 상속받고, makeSound() 메서드를 오버라이드 해서 
Wolf 클래스의 howl() 메서드를 호출하도록 구현한다.
*/
public class WolfAdapter extends Animal {

    private Wolf wolf;
    
    public WolfAdapter(Wolf wolf) {
        this.wolf = wolf;
    }

    public void makeSound() {
        wolf.howl();
    }
}

// 어댑터패턴으로 호환되지 않는 인터페이스를 연결하면 기존 코드를 수정하지 않아도 다른 객체와 같이 동작할 수 있게 된다.
public class AdapterExample {
    public static void main(String[] args) {
        Animal animal = new Animal();
        animal.makeSound(); // "Generic animal sound" 출력
        
        Wolf wolf = new Wolf();
        WolfAdapter adapter = new WolfAdapter(wolf);
        adapter.makeSound(); // "Howl" 출력
    }
}

JDBC API에 사용된 어댑터 패턴

/*
Class.forName() 메서드로 Driver 클래스를 로드한다. 이 클래스는 MySQL 제조사가 제공하는 드라이버이다.
그런다음 DriverManager.getConnection() 메서드를 사용하여 데이터베이스와 연결한다. 
이 메서드는 실제로 Driver 인터페이스를 구현한 객체를 생성하여 Connection 인터페이스와 연결한다.
*/
public static void main(String[] args) {
    // JDBC 드라이버 로드
    Class.forName("com.mysql.cj.jdbc.Driver");
    
    // 데이터베이스 연결
    String url = "jdbc:mysql://localhost/test";
    String user = "user";
    String password = "password";
    Connection conn = DriverManager.getConnection(url, user, password);
}

/*
JDBC 는 데이터베이스와 연결하기 위해 인터페이스를 제공한다. 데이테베이스 제조사마다 제공하는
드라이버의 인터페이스가 다르기 떄문에 DriverManager클래스에 어댑터 패턴을 사용한다.
*/
public class DriverManager {
    public static Connection getConnection(String url, String user, String password) {
        // 데이터베이스 제조사가 제공하는 드라이버를 찾아서 Driver 인터페이스를 구현한 객체를 생성
        Driver driver = findDriver(url);
        // Driver 인터페이스를 구현





프록시 패턴

Proxy Pattern
Proxy 를 사용하여 객체에 대한 접근을 제어하고 간접적으로 제어하는 디자인 패턴이다.
객체에 대한 직접적인 접근을 대신해 Proxy 객체 를 사용해 객체에 접근을 하고 Proxy 객체
실제 객체를 대신해 객체의 대리자 역할 을 수행하며 객체와 동일한 인터페이스를 제공한다
Proxy 객체는 실제 객체의 인터페이스를 구현하며, 실제 객체의 메서드를 호출하기전 추가작업을 할 수 있다.


객체에 대한 접근을 제어하고 보안, 성능 최적화 등의 추가 기능을 제공하기 위해 사용한다.

  • 원격 객체에 대한 접근을 제어하고 네트워크 부하를 줄이기 위해
  • 복잡한 객체에 대한 접근을 제어하고 메모리 사용량을 줄이기 위해
  • 객체에 대한 보안적인 제한 및 부가적인 기능을 제공하기 위해


trade-off


장점

  • 객체 생성과 초기화를 지연시킬 수 있어, 성능을 개선할 수 있다.
    • 원본객체의 생성 또는 초기화에 많은 비용이 들어갈 경우 유용하다.
    • 실제객체가 원격서버에 있는경우, 프록시객체로 서버접근에 대한 리소스를 줄일 수 있다.
    • 프록시객체는 원본객체의 인스턴스를 생성하지 않고, 요청이 들어올 경우 생성하고 요청을 처리한다.
  • 실제객체에 대한 접근을 제어할 수 있어, 보안성을 높일 수 있다.
    • 원본객체가 보안성이 중요할 경우 유용하다.
  • 객체의 메서드의 실행 전후로 추가적인 작업을 수행할 수 있어 유연성이 증가한다.
    • 이를 통해 로깅, 캐싱, 트랜잭션 등 다양한 처리가 가능하다.


단점

  • 프록시 객체는 실제객체에 접근하기 위해 추가적인 리소스가 들어가므로 성능이 저하될 수 있다.
  • 프록시 객체라는 추가적은 클래스가 필요하고, 이는 복잡성이 증가하고 유지보수를 어렵게 할 수 있다.
  • 중간에 다른 객체가 끼어있기 때문에 디버깅이 어려워질 수 있어 추가적인 로깅이나 디버깅 작업이 필요하다.
  • 프록시객체가 실제객체를 대신하기 때문에 실제객체가 생성되지 않아 에상하지 못한 문제가 발생할 수 있다.


구현


정적 프록시 방식
프록시 객체를 컴파일 시점에 미리 생성하는 방식

  • 컴파일 시점에 생성되므로 런타임에 추가적인 비용이 발생하지 않는다.
  • 인터페이스를 구현하는 클래스만 프록시 객체를 생성할 수 있다.
  • 프록시객체를 수정하려면 컴파일을 다시 해야한다.
public class Animal {
    public void eat() {
        System.out.println("Animal is eating");
    }
}

/*
프록시 객체가 구현할 AnimalInterface 인터페이스를 구현한다.
AnimalProxy 클래스는 AnimalInterface 인터페이스를 구현한다.
생성자를 통해 실제 객체를 전달받고, eat() 메서드에서 실제 객체의 eat() 메서드를 호출하기 전에 
수행할 작업과 호출한 후에 수행할 작업을 정의한다.
마지막으로 클라이언트에서 프록시 객체를 생성하고 사용한다.
이를 통해 클라이언트는 실제 객체를 알 필요가 없고, 프록시 객체를 이용해 추가적인 작업을 수행할 수 있다.
*/

public interface AnimalInterface {
    void eat();
}

public class AnimalProxy implements AnimalInterface {
    private Animal animal;

    public AnimalProxy(Animal animal) {
        this.animal = animal;
    }

    public void eat() {
        System.out.println("Before eating");
        animal.eat();
        System.out.println("After eating");
    }
}

// 클라이언트
Animal animal = new Animal();
AnimalInterface proxy = new AnimalProxy(animal);
proxy.eat();

동적 프록시 방식
프록시 객체를 런타임에 동적으로 생성하는 방식
일반적으로 유연성이 높고 프록시 객체를 동적으로 수정할 수 있어 더 많이 사용된다.

  • 인터페이스를 구현하지 않은 클래스도 프록시 객체를 생성할 수 있다.
  • 프록시 객체를 동적으로 수정할 수 있다.
  • 더 다양한 기능을 수행할 수 있어 AOP 같은 패러다임에 사용하기 적합하다.
  • 런타임에 프록시객체를 생성하므로, 정적프록시 방식에 비해 성능이 떨어질 수 있다.
  • 컴파일 시점에 프록시객체를 알수 없어서 디버깅이 어려울 수 있다.
public interface Animal {
    void eat();
}

public class Dog implements Animal {
    public void eat() {
        System.out.println("The dog is eating.");
    }
}

/*
AnimalInvocationHandler 클래스는 InvocationHandler 인터페이스를 구현하여 invoke() 메서드를 재정의 한다.
*/
public class AnimalInvocationHandler implements InvocationHandler {
    private Animal animal;

    public AnimalInvocationHandler(Animal animal) {
        this.animal = animal;
    }

    // 이 메서드는 실제 객체의 메서드를 호출하기 전후의 실행시간을 출력한다.
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long startTime = System.nanoTime();
        Object result = method.invoke(animal, args);
        long endTime = System.nanoTime();
        System.out.println("Execution time: " + (endTime - startTime) + " ns");
        return result;
    }
}

/*
Dog 객체를 생성한 후 Proxy.newProxyInstance 메서드를 사용해 Animal 인터페이스를 구현하는 동적 프록시 객체를 생성한다.
이때, AnimalInvocationHandler 클래스를 InvocationHandler로 사용한다. 
마지막으로, 프록시 객체의 eat() 메서드를 호출하여 AOP가 적용된 결과를 출력한다.
*/
Animal dog = new Dog();
Animal proxy = (Animal) Proxy.newProxyInstance(
    Animal.class.getClassLoader(),
    new Class[] { Animal.class },
    new AnimalInvocationHandler(dog)
);
proxy.eat();





데코레이터 패턴

Decorator Pattern
객체의 기능을 동적으로 추가 / 변경 할 수 있도록 해주는 디자인 패턴이다.
객체를 래핑하고, 래핑된 객체에 새로운 기능을 추가해 새로운 객체를 생성하는 방식으로 동작한다.
이렇게 생성된 객체는 래핑된 객체의 기능을 그대로 사용하면서 새로운 기능을 추가한 객체가 된다.
이를통해, 확장성, 유연성이 높아지고 객체 간의 결합도가 낮아지게 된다.


객체를 수정하지 않고 기능을 동적으로 확장하기 위해 사용한다.

  • 객체에 대해 동적으로 새로운 기능을 추가하거나 기존 기능을 변경할 필요가 있을 때
  • 상속으로 인한 클래스의 확장이 어려운 경우에 확장이 필요한 기능을 데코레이터 클래스로 추가할 때
  • 객체의 수정 없이 기능을 추가하고 싶을 때


trade-off


장점

  • 객체의 기능을 동적으로 추가 / 변경 할 수 있어서 기존 코드를 변경하지 않고 기능을 추가할 수 있다.
    • 데코레이터 패턴은 OCP원칙 을 준수한다.
  • 객체간의 결합도가 낮다.
  • 객체를 책임별로 분리하기 용이하고 여러개의 데코레이터를 조합해 다양한 기능을 구현할 수 있다.


단점

  • 새로운 데코레이터 클래스를 만들어야 하기 때문에 클래스가 늘어난다.
  • 런타임에 객체를 생성하기 때문에 아주 약간의 오버헤드가 발생한다.
  • 초기 설계에 리소스가 든다.


구현


인터페이스를 이용한 구현

public interface Animal {
    void speak();
}

// Animal 구현클래스
public class Dog implements Animal {
    @Override
    public void speak() {
        System.out.println("멍멍!");
    }
}

// Animal을 데코레이팅하는 인터페이스
public interface AnimalDecorator extends Animal {
    void decorate();
}

// AnimalDecorator를 구현한 구현 클래스
public class DogWithHat implements AnimalDecorator {
    private Animal animal;

    public DogWithHat(Animal animal) {
        this.animal = animal;
    }

    @Override
    public void speak() {
        animal.speak();
    }

    @Override
    public void decorate() {
        System.out.println("모자를 쓴 강아지");
    }
}

public class Main {
    public static void main(String[] args) {
        // 기존의 Dog 객체
        Animal dog1 = new Dog();
        dog1.speak();

        // 모자를 쓴 Dog 객체
        Animal dog2 = new Hat(new Dog());
        dog2.speak();
    }
}

추상클래스를 이용한 구현

abstract class Animal {
    public abstract String speak();
}

// Animal 구현클래스
class Dog extends Animal {
    @Override
    public void speak() {
        System.out.println("멍멍!");
    }
}

// Animal을 데코레이팅하는 데코레이터 추상 클래스
public abstract class AnimalDecorator extends Animal {
    protected Animal animal;

    public AnimalDecorator(Animal animal) {
        this.animal = animal;
    }

    public void speak() {
        animal.speak();
    }
}

// AnimalDecorator를 구현한 Hat 클래스
public class Hat extends AnimalDecorator {
    public Hat(Animal animal) {
        super(animal);
    }

    @Override
    public void speak() {
        super.speak();
        System.out.println("모자를 쓴 강아지");
    }
}

public class Main {
    public static void main(String[] args) {
        // 기존의 Dog 객체
        Animal dog1 = new Dog();
        dog1.speak();

        // 모자를 쓴 Dog 객체
        Animal dog2 = new Hat(new Dog());
        dog2.speak();
    }
}

람다를 이용한 구현

interface Animal {
    String speak();
}

class Dog implements Animal {
    @Override
    public String speak() {
        return "멍멍";
    }
}

class Hat implements Animal {
    private Animal animal;
    private Function<String, String> decorate;  // 함수형 인터페이스 사용

    public Hat(Animal animal) {
        this.animal = animal;
        decorate = (sound) -> sound + ", 모자쓴 강아지";
    }

    @Override
    public String speak() {
        return decorate.apply(animal.speak());
    }
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        System.out.println(dog.speak()); // "멍멍"

        Animal dogWithHat = new Hat(new Dog());
        System.out.println(dogWithHat.speak()); // "멍멍, 모자쓴 강아지"
    }
}





싱글톤 패턴

Singleton Pattern
어떤 클래스의 인스턴스가 오직 한 개만 존재하도록 보장하고, 전역적인 접근점을 제공하는 디자인 패턴이다.


애플리케이션에서 단 하나의 인스턴스만을 생성하고 이에 대한 전역적인 접근을 제공하기 위해 사용한다.

  • 자원의 공유가 필요한 경우에 여러 객체를 생성하는 것이 비효율적인 경우
  • 상태를 유지해야 하는 로깅이나 캐싱 등의 객체가 필요한 경우
  • 불필요한 객체 생성을 방지하고 메모리 사용량을 줄이기 위해 객체를 관리할 필요가 있는 경우


trade-off


장점

  • 인스턴스를 오직 하나만 생성하기 때문에, 메모리 사용량이 줄어든다.
  • 유일한 인스턴스에 대한 접근점이 하나뿐이기 때문에, 인스턴스를 다른 클래스에서 변경할 수 없다.


단점

  • 멀티스레드 환경에서 동시성 문제가 발생할 수 있다.
    • 동기화 처리가 필요하기 때문에 성능저하를 가져올 수 있다.
  • OCP원칙 을 위반한다.
    • 싱글톤객체를 사용하는 객체가 다른 객체에 의존할 경우, 변경 시 싱글톤객체도 같이 변경해야 할 수 있다.
  • 전역적으로 사용할 수 있기 때문에 다른 객체들과의 결합도가 높아질 수 있다.
    • Dependency Injection 사용으로 해결할 수 있다.


구현


Eager Initialization 방식

// 클래스가 로딩될 때, 인스턴스를 미리 생성하는 방식, thread safe 하지 않다.
public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}

Lazy Initialization 방식

// Double-Checked Locking
// 인스턴스가 필요한 시점에 생성하는 방식, thread safe 하다.
public class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

스프링 프레임워크 방식

/*
스프링에서는 빈(Bean)이라는 개념을 사용하며, 이 빈들은 스프링 컨테이너에서 생성되고 관리된다. 
이때, 스프링 컨테이너는 기본적으로 싱글톤 패턴을 적용하므로, 모든 빈들은 기본적으로 싱글톤으로 생성된다.
thread safe 하다.
*/
@Component
public class MyComponent {
    // ...
}





템플릿 메서드 패턴

Template Method Pattern
상위 클래스에서 공통적인 로직을 구현하고 하위클래스에서 구체적으로 구현하기 위한 디자인 패턴이다.
추상클래스를 사용하는데, 공통로직을 구현하는 템플릿 메서드를 정의하고 하위클래스에선 이 템플릿 메서드를
상속받아 구체적인 구현을 한다. 상위클래스에서는 추상메서드를 선언해 하위클래스에서 구현을 강제할 수 있다.


상위클래스에서 공통부분을 정의하고 하위클래스에서 그 일부를 구체적으로 구현하기 위해 사용한다.

  • 일련의 공통작업이 있지만, 그 중 일부는 하위클래스에서 구현해야 하는 경우
    • 다양한 구현 방법을 지원하면서 일관성을 유지해야 하는 경우
  • 복잡한 알고리즘을 캡슐화하여 코드의 가독성을 높이고 유지보수를 용이하게 하는 경우


trade-off


장점

  • 중복코드를 줄일 수 있다.
    • 공통으로 사용되어야 하는 로직이 있을경우, 추상클래스로 정의하여 한곳에서 관리할 수 있다.
  • 일관성을 유지할 수 있다.
    • 추상클래스에서 공통로직을 정의하므로, 여러 하위클래스에서 일관성있는 코드를 유지할 수 있다.
  • 추상 클래스에서 정의한걸 상속받아 하위 클래스에서 구현하므로, 유연한 확장성을 가진다.


단점

  • 추상클래스와 구체클래스 간의 결합도가 높다.
  • 상 클래스가 변경될 경우 하위 클래스들도 함께 수정해야 한다.
  • 상속을 사용하기 때문에 하위 클래스의 개수가 많아질 경우 클래스의 수가 증가할 수 있다.


구현


// 추상클래스로 play() 메서드에서 공통로직을 정의하고, makeSound(), move() 의 구현을 강제한다.
public abstract class Animal {
    public void play() {
        makeSound();
        move();
    }

    protected abstract void makeSound();
    protected abstract void move();
}

// Animal 클래스를 상속받은 구현클래스
public class Dog extends Animal {
    @Override
    protected void makeSound() {
        System.out.println("Bark!");
    }

    @Override
    protected void move() {
        System.out.println("Running!");
    }
}

// Animal 클래스를 상속받은 구현클래스
public class Cat extends Animal {
    @Override
    protected void makeSound() {
        System.out.println("Meow!");
    }

    @Override
    protected void move() {
        System.out.println("Jumping!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        dog.play();

        Animal cat = new Cat();
        cat.play();
    }
}





팩토리 메서드 패턴

Factory Method Pattern
객체 생성 코드를 클라이언트에서 분리시켜 객체 생성을 캡슐화하는 디자인 패턴이다.
객체 생성의 책임을 서브클래스에 위임하여 서브클래스가 어떤 클래스의 인스턴스를 만들지 결정하게 한다.
이렇게 함으로써 코드를 수정하지 않고도 객체의 타입을 바꿀 수 있는 유연성을 제공한다.


객체 생성을 서브클래스에서 처리하도록 하여 객체 생성 과정을 유연하게 관리하기 위해 사용한다.

  • 객체 생성에 필요한 정보가 런타임 시에 결정되는 경우
  • 객체 생성 방법을 변경해야 할 때 전체 코드를 수정하지 않고 유지보수성을 높이고 싶을 때


trade-off


장점

  • 객체생성 코드를 캡슐화 할 수 있다.
  • 객체생성 코드를 중앙화 하여 한곳에서 관리해 가독성을 높일 수 있다.
  • 서브클래스를 추가하거나 수정해 객채생성에 대한 확장성이 높아진다.
  • 다형성을 활용한 디자인 패턴이다.
    • 클라이언트 코드가 객체의 타입이 아닌 인터페이스를 통해 객체에 접근할 수 있다.


단점

  • 서브클래스를 추가해야 하므로 클래스 수가 증가한다.
  • 추상화 수준이 높아지기 때문에 가독성이 떨어진다.


구현


// Animal 인터페이스
public interface Animal {
    String getAnimalName();
}

// Animal 인터페이스를 구현하는 Dog 클래스
public class Dog implements Animal {
    @Override
    public String getAnimalName() {
        return "Dog";
    }
}

// Animal 인터페이스를 구현하는 Cat 클래스
public class Cat implements Animal {
    @Override
    public String getAnimalName() {
        return "Cat";
    }
}

// AnimalFactory 인터페이스
public interface AnimalFactory {
    Animal createAnimal();
}

// AnimalFactory 인터페이스를 구현하는 DogFactory 클래스
public class DogFactory implements AnimalFactory {
    @Override
    public Animal createAnimal() {
        return new Dog();
    }
}

// AnimalFactory 인터페이스를 구현하는 CatFactory 클래스
public class CatFactory implements AnimalFactory {
    @Override
    public Animal createAnimal() {
        return new Cat();
    }
}

public class Main {
    public static void main(String[] args) {
        AnimalFactory dogFactory = new DogFactory();
        Animal dog = dogFactory.createAnimal();
        System.out.println(dog.getAnimalName()); // "Dog" 출력

        AnimalFactory catFactory = new CatFactory();
        Animal cat = catFactory.createAnimal();
        System.out.println(cat.getAnimalName()); // "Cat" 출력
    }
}





전략 패턴

Strategy Pattern
알고리즘의 집합을 정의하고 각각을 캡슐화하여 동적으로 교체하여 사용할 수 있는 디자인 패턴


알고리즘을 캡슐화하여 동적으로 교환할 수 있도록 만들어 유연한 애플리케이션을 구현하기 위해 사용한다.

  • 런타임 시에 알고리즘을 선택할 필요가 있는 경우
  • 비슷한 알고리즘을 여러 개 가지고 있고, 이를 쉽게 변경하고 유지보수하고 싶은 경우
  • 알고리즘의 구현 내용을 클라이언트와 분리하여 의존성을 낮추고 싶은 경우


trade-off


장점

  • 알고리즘을 동적으로 교체할 수 있고 캡슐화 할 수 있다.
  • 전략객체를 독립적으로 변경할 수 있기 때문에 객체간 결합도가 감소한다.


단점

  • 인터페이스와 클래스 수가 많아진다.
  • 많은 전략을 사용할 수 있지만 이걸 모두 객체로 생성하는건 무리다.
  • 컨텍스트, 전략객체, 인터페이스 사용으로 비용이 증가할 수 있다


구현


// 전략(알고리즘)을 캡슐화한 인터페이스
public interface MoveStrategy {
    void move();
}

// 전략(알고리즘)을 구현한 클래스
public class WalkStrategy implements MoveStrategy {
    @Override
    public void move() {
        System.out.println("걷는다.");
    }
}

// 전략(알고리즘)을 구현한 클래스
public class RunStrategy implements MoveStrategy {
    @Override
    public void move() {
        System.out.println("달린다.");
    }
}

// 컨텍스트(전략을 사용하는 클래스)
// 생성자를 통해 MoveStrategy 인터페이스를 구현한 객체를 받아서 실행한다.
public class Animal {
    private MoveStrategy moveStrategy;

    public Animal(MoveStrategy moveStrategy) {
        this.moveStrategy = moveStrategy;
    }

    public void move() {
        moveStrategy.move();
    }
}

// Animal 객체를 생성할 때 원하는 전략 객체를 주입하여 원하는 동작을 실행한다.
public class Main {
    public static void main(String[] args) {
        Animal dog = new Animal(new WalkStrategy());
        dog.move();

        Animal cat = new Animal(new RunStrategy());
        cat.move();
    }
}





템플릿 콜백 패턴

Template Callback Pattern
알고리즘의 구조를 정의하고 구체적인 단계나 구현 방법을 서브클래스에서 결정하는 방식의 디자인 패턴
자바에선 인터페이스나 추상클래스를 상위 클래스로 사용하고 하위클래스에서 구현한 메서드(콜백) 를
상위클래스에서 호출하면서, 하위클래스가 직접 실행할 수 있는 유연성을 제공한다.

템플릿 콜백 패턴은 템플릿 메서드 패턴을 확장한 개념으로, 기본적인 구조는 매우 유사하다.
다른점은, 하위클래스에서 콜백메서드를 제공하고 상위클래스에서 호출하는 방식으로 동작한다.


중복 코드를 줄이고 알고리즘을 캡슐화하여 재사용성과 유지보수성을 높이기 위해 사용한다.

  • 알고리즘의 골격을 유지하면서 상세한 구현 부분을 변경할 수 있는 경우
    • 비슷한 알고리즘에 대해 중복 코드를 줄이면서 구현을 변경하고 싶을 때
  • 다양한 클라이언트 요구사항을 만족시키면서 일관성을 유지해야 하는 경우
    • 여러 클라이언트 요구사항을 수용하면서 알고리즘을 유지보수 가능한 방식으로 구현하고 싶을 때


trade-off


장점

  • 상위클래스에서 정의한 알고리즘 구조를 여러 하위클래스에서 재사용 및 수정 할 수 있다.
    • 콜백메서드를 추가하거나 하위클래스를 추가하여 확장성에서도 용이하다.
  • 다형성
    • 추상메서드를 상위클래스에 선언하고 하위클래스에서 구체적으로 구현하는 방식
    • 상위클래스에선 구현에 대한 세부정보를 몰라도 되고, 콜백메서드를 호출함으로써 다형성 활용
  • 제어의 역전
    • 상위클래스에서 하위클래스의 콜백 메서드를 호출하기 때문에 제어의 흐름이 역전된다.


단점

  • 추상클래스 또는 인터페이스 등을 사용해야 하기 때문에 복잡해진다.
  • 알고리즘 구조를 변경하려면 상위클래스를 수정해야 한다.
  • 다수의 메서드 호출이 발생하기 때문에 약간의 오버헤드 발생


구현


추상클래스를 사용한 방식

/*
performActivity() 메서드를 실행하여 알고리즘의 구조를 정의하고, 
하위 클래스에서 구현할 specificActivity() 메서드를 추상 메서드로 선언
*/
public abstract class Animal {
    public final void doActivity() {
        System.out.println("Getting ready to do activity...");
        performActivity();
        System.out.println("Activity is completed!");
    }

    // 하위 클래스에서 구현해야 할 콜백 메서드
    protected abstract void specificActivity();

    private void performActivity() {
        System.out.println("Performing activity...");
        specificActivity();
    }
}

// 콜백 메서드인 specificActivity() 메서드를 구현
public class Dog extends Animal {
    @Override
    protected void specificActivity() {
        System.out.println("Dog is barking.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        dog.doActivity();
    }
}

인터페이스를 사용한 방식

/*
performActivity() 메서드를 실행하여 알고리즘의 구조를 정의하고, 
하위 클래스에서 구현할 specificActivity() 메서드를 선언
*/
public interface Animal {
    default void doActivity() {
        System.out.println("Getting ready to do activity...");
        performActivity();
        System.out.println("Activity is completed!");
    }

    // 하위 클래스에서 구현해야 할 콜백 메서드
    void specificActivity();
    
    private void performActivity() {
        System.out.println("Performing activity...");
        specificActivity();
    }
}

// 콜백 메서드인 specificActivity() 메서드를 구현
public class Dog implements Animal {
    @Override
    public void specificActivity() {
        System.out.println("Dog is barking.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        dog.doActivity();
    }
}






댓글남기기