Java

모든 소프트웨어 개발자가 알아야 할 디자인 패턴

체리필터 2021. 3. 12. 09:32
728x90
반응형

이 글은 viveknaskar.medium.com/design-patterns-that-every-software-developer-must-know-ac71f575e68 에 나와 있는 내용을 정리한 글입니다.

참고로 번역은 아니며 개발자의 의사소통 수단인 코드만 보고서 제가 임의로 적은 글입니다.

Singleton Pattern

싱글톤 패턴은 인스턴스가 1개만 생성되도록 하는 것입니다. Spring 자체가 Bean으로 등록될 시 자체적으로 인스턴스를 1개만 생성 하기 때문에 싱글톤 패턴을 쓰고 있는 것이지만, 코드 상으로는 어떻게 구현 하는 것인지 아래의 예제에서 볼 수 있습니다.

/**
 * Singleton is a design pattern by which you create a singleton class that has only one instance
 * at any time
 *
 */
public class SingletonExample {

    private static SingletonExample single_instance = null;

    public String msg;

    /**
     * Making the constructor as private
     */
    private SingletonExample() {
        msg= "Hello World!";
    }

    /**
     * Static method to create instance of Singleton class
     * @return single object of 'SingletonExample' class
     */
    public static SingletonExample getInstance() {
        /**
         * Ensuring only one instance is created
         */
        if (single_instance == null)
            single_instance = new SingletonExample();
        return single_instance;
    }

    public static void main(String[] args) {

        /**
         * Instantiating SingletonExample class with variable 'singletonObject'
         */
        SingletonExample singletonObject = SingletonExample.getInstance();

        System.out.println(singletonObject.msg);

    }

}

코드를 간단하게 설명하자면...

SingletonExample의 instance를 private static 으로 선언하고 초기 값은 null로 만들어 둡니다. 이 상태에서 생성자도 private으로 만들어 두어 instance가 복수개로 생성되지 않도록 만듭니다.

instance는 오로지 getInstance 메소드를 통해서만 생성할 수 있는데, 여기서 SingletonExample instance가 null 이라면 새로 생성해서 return 하고 아니라면 기존의 instance 를 return 합니다.

이렇게 함으로 static에 생성된 instance를 공유하여 사용할 수 있게 됩니다.

즉 Singleton이란 말에 맞게 instance가 복수개가 생기는 것이 아니라 한 개만 생성되게 되는 것입니다.

 

Factory Method Pattern

여기서 말하는 Factory란 우리가 알고 있는 '공장'이 맞습니다. 공장에서는 물건을 스펙에 맞게 생산하고 요구사항이 좀 달라지면 그에 맞게 조금 다른 물건도 생산을 하게 됩니다.

그와 마찬가지로 원하는 스펙을 입력하면 그에 맞는 instance를 생산해 주는 것이 Factory Method Pattern 입니다.

이를 구현 하기 위해서는 Abstract Class(추상 클래스) 또는 interface를 구현 해야 합니다. 왜냐 하면 Factory에서 생산된 instance를 받아 사용하는 측에서는 이 제품이 무엇인지 정확히 알 필요 없이 '사용'만 하면 되기 때문입니다.

말로 설명하는 것보다 위의 링크에 나와 있는 소스코드 예제를 보시죠.

import java.util.Scanner;

public class FactoryMethodExample {

    public static void main(String[] args) {

        System.out.println("Enter the type of car:\nAudi \nTesla");

        Scanner in = new Scanner(System.in);
        String carType = in.nextLine();

        /**
         * The factory method is called to get the object of the concrete classes by passing
         * the information of the car from the user.
         */
        CarFactory carFactory = new CarFactory();
        carFactory.manufactureCar(carType.toLowerCase());

    }

}

/**
 * Abstract Class with abstract and concrete method
 */
abstract class Car {
    public abstract void addEngineType();
    public void deliverCar() {
        System.out.println("Your car will be delivered at your doorstep.");
    }
}

/**
 * Concrete class 'AudiCar' extending the abstract Class
 */
class AudiCar extends Car {
    @Override
    public void addEngineType() {
        System.out.println("You have ordered a car with gasoline Engine.");
    }
}

/**
 * Concrete class 'TeslaCar' extending the abstract Class
 */
class TeslaCar extends Car {
    @Override
    public void addEngineType() {
        System.out.println("You have ordered a car with electric Engine. ");
    }
}

/**
 * In Factory method, the object of the Car is created.
 */
class CarFactory {
    public Car manufactureCar(String type){
        Car car;
        switch (type.toLowerCase())
        {
            case "audi":
                car = new AudiCar();
                break;
            case "tesla":
                car = new TeslaCar();
                break;
            default: throw new IllegalArgumentException("No such car available.");
        }
        car.addEngineType();
        car.deliverCar();
        return car;
    }
}

 

코드를 하나씩 살펴 보겠습니다. Abstract Class인 Car 클래스가 있고 이를 구현한 AudiCar와 TeslaCar가 있습니다.

보통 이를 구분해서 생성 하려면 로직에 맞게 if, else 를 써가며 직접 new 해서 instance를 생성합니다.

하지만 이를 Factory 안에 집어 넣고, 필요한 인자값만 넣어주면 그에 맞는 instance를 알아서 생성해서 돌려 주게 됩니다. (공장에서 제품을 생산하듯이)

위에서는 CarFactory의 manufactureCar 메소드가 그런 역할을 하는 것이죠.

사용하는 쪽에서는 이 Car 가 무슨 종류인지 구분하지 않고 그냥 사용만 하면 되는 것입니다.

가령 Car Absctract Class 안에 시동을 건다는 start 라는 메소드를 만들어 두면 AudiCar, TeslaCar 모두 구현하게 되고, 가져다 쓰는 쪽에서는 구분하지 않고 instance를 받아 start라는 메소드를 호출만 하면 되는 것입니다.

 

이렇게 Factory method pattern을 이용하는 이유는 Business Logic에 분기문을 없애므로 인해 코드가 잘 읽힐 수 있으며, 확장성이 용이해 지는 결과를 가져오기 때문입니다.

728x90

Decorator Pattern

Decorator란 말은 장식 이란 의미입니다. 기존 내용에 무엇인가를 덧 붙인다는 의미가 강하죠.

따라서 패턴도 이와 비슷하게 생각하면 됩니다. 참고 사이트의 예제는 피자를 예로 들었습니다. 기본 피자 위에 토핑을 추가하는 느낌으로 설명한 것 같네요.

우선 소스를 보고 오시죠.

/**
 * An illustration of decorator design pattern
 */
public class DecoratorExample {

    public static void main(String args[]) {

        /**
         * Creating new Margherita pizza
         */
        Pizza margheritaPizza = new Margherita();
        System.out.println(margheritaPizza.getDescription() 
                + " Cost :$" + margheritaPizza.getCost());

        /**
         * Creating new FarmHouse pizza
         */
        Pizza farmhousePizza = new FarmHouse();

        /**
         * Decorating with FreshTomato topping
         */
        farmhousePizza = new FreshTomato(farmhousePizza);
        System.out.println(farmhousePizza.getDescription() 
                + " Cost :$" + farmhousePizza.getCost());

        Pizza cheeseburstPizza = new CheeseBurst(margheritaPizza);
        System.out.println(cheeseburstPizza.getDescription() 
                + " Cost :$" + cheeseburstPizza.getCost());

    }
}

/**
 * Abstract pizza class
 */
abstract class Pizza {
    String description = "";

    public String getDescription() {
        return description;
    }
    public abstract double getCost();
}

/**
 * Concrete classes for abstract Pizza class where the pizza types are different
 */
class FarmHouse extends Pizza {
    public FarmHouse() { description = "FarmHouse"; }
    public double getCost() { return 200.00; }
}

class Margherita extends Pizza {
    public Margherita() { description = "Margherita"; }
    public double getCost() { return 100.00; }
}

/**
 * Toppings is the decorator abstract class here that extends abstract Pizza class where
 * the toppings like CheeseBurst, and FreshTomato are different
 */
abstract class Toppings extends Pizza {
    public abstract String getDescription();
}

/**
 * Decorator concrete classes extending abstract Toppings class
 */
class CheeseBurst extends Toppings {
    Pizza pizza;
    public CheeseBurst(Pizza pizza) { this.pizza = pizza; }
    public String getDescription() {
        return pizza.getDescription() + " with Cheese Burst topping ";
    }
    public double getCost() { return 50.00 + pizza.getCost(); }
}

class FreshTomato extends Toppings {
    Pizza pizza;
    public FreshTomato(Pizza pizza) { this.pizza = pizza; }
    public String getDescription() {
        return pizza.getDescription() + " with Fresh Tomato topping ";
    }
    public double getCost() { return 35.00 + pizza.getCost(); }
}

 

소스코드를 분석해 보죠. 추상 클래스인 Pizza를 상속받아 FarmHouse, Margherita Pizza를 구성할 수 있습니다.

이러한 피자 위에 신선한 토마토를 올린다던가, 치즈를 둠뿍 뿌리는 데코레이터를 추가할 수 있겠죠. 이럴 때 사용하는 것이 Decorator Pattern 입니다.

위의 예에서는 생성된 피자 instance를 생성자로 받은 다음 description과 cost를 조금씩 변화를 주어서 값을 리턴하도록 만들었습니다.

 

Adapter Patten

어댑터는 볼트수가 맞지 않을 때 중간에 연결해 주는 돼지코를 생각나게 해 줍니다.

패턴도 마찬가지입니다. 양쪽에 서로 맞지않는 인터페이스가 있다면 이 둘 간에 연결시켜줄 수 있는 Bridge를 만들어 주는 것입니다.

위의 링크에 나와 있는 예제는 mp3만을 재생할 수 있는 MediaPlayer와 조금 더 다양한 음원을 재생할 수 있는 AdvancedMediaPlayer 사이를 어떻게 연결시켜줄 수 있는지 보여 줍니다.

/**
 * An illustration of adapter design pattern
 */
public class AdapterExample {

    public static void main(String[] args) {

        /**
         * Using AudioPlayer for playing different audio formats
         */
        AudioPlayer audioPlayer = new AudioPlayer();

        audioPlayer.play("mp3", "Waiting For Love.mp3");
        audioPlayer.play("vlc", "Wake Me Up.vlc");
        audioPlayer.play("mp4", "Summer of 69.mp4");
        audioPlayer.play("wma", "Lady.wma");
    }
}

interface MediaPlayer {
    void play(String audioType, String fileName);
}

interface AdvancedMediaPlayer {
    void playVlc(String fileName);
    void playMp4(String fileName);
}

class VlcPlayer implements AdvancedMediaPlayer{

    @Override
    public void playVlc(String fileName) {
        System.out.println("Playing vlc file. Name: "+ fileName);
    }

    @Override
    public void playMp4(String fileName) {
        //do nothing
    }
}

class Mp4Player implements AdvancedMediaPlayer{

    @Override
    public void playVlc(String fileName) {
        //do nothing
    }

    @Override
    public void playMp4(String fileName) {
        System.out.println("Playing mp4 file. Name: "+ fileName);
    }
}

/**
 * The Adapter will be named MediaAdapter and it must implement
 * the MediaPlayer interface. The FormatAdapter class must have a
 * reference to AdvancedMediaPlayer, the incompatible interface.
 */
class MediaAdapter implements MediaPlayer {

    AdvancedMediaPlayer advancedMusicPlayer;

    public MediaAdapter(String audioType){

        if(audioType.equalsIgnoreCase("vlc")) {
            advancedMusicPlayer = new VlcPlayer();

        } else if(audioType.equalsIgnoreCase("mp4")) {
            advancedMusicPlayer = new Mp4Player();
        }
    }

    @Override
    public void play(String audioType, String fileName) {

        if(audioType.equalsIgnoreCase("vlc")) {
            advancedMusicPlayer.playVlc(fileName);
        }
        else if(audioType.equalsIgnoreCase("mp4")) {
            advancedMusicPlayer.playMp4(fileName);
        }
    }
}

class AudioPlayer implements MediaPlayer {
    MediaAdapter mediaAdapter;

    @Override
    public void play(String audioType, String fileName) {

        /**
         * inbuilt support to play mp3 music files
         */
        if(audioType.equalsIgnoreCase("mp3")){
            System.out.println("Playing mp3 file. Name: " + fileName);
        }

        /**
         * mediaAdapter is providing support to play other file formats
         */
        else if(audioType.equalsIgnoreCase("vlc")
                || audioType.equalsIgnoreCase("mp4")){
            mediaAdapter = new MediaAdapter(audioType);
            mediaAdapter.play(audioType, fileName);
        }

        else{
            System.out.println("Invalid media. " + audioType + " format not supported");
        }
    }
}

 

소스코드를 살펴 보시죠. mp3 재생만 지원하는 MediaPlayer에서 mp4나 vlc 플레이를 하고 싶다면 어떻게 해야 할까요?

이럴 경우 위의 소스코드에서 살펴볼 수 있는 것 처럼 MediaAdapter Class를 사용하면 됩니다.

MediaAdapter는 MediaPlayer를 구현 하지만 내부에 AdvancedMediaPlayer를 audioType에 따라 가지고 있으며 그에 맞게 AdvancedMediaPlayer의 play method를 호출해 줍니다.

이렇게 하게 될 경우 굳이 MediaPlayer에서 AdvancedMediaPlayer를 위해 무엇인가를 수정하거나 추가할 필요가 없어지게 되는 것이죠.

말 그대로 중간에 돼지코 하나만 필요한 상태가 된 것입니다.

 

State Pattern

State Pattern은 저도 처음 들어 봅니다만... 위의 링크에서 예제 소스를 살펴보니 상태값에 대해서 interface를 만들어 놓고 나서 각 상태가 변화할 때 어떠한 변화가 있을 것인지를 정의해서 사용할 수 있도록 하는 것 같습니다.

말로 설명하기 복잡하니 코드를 보시죠. (개발자는 코드로 이야기를...)

/**
 * An illustration of state design pattern
 */
public class StateExample {

    public static void main(String[] args) {

        Parcel parcel = new Parcel();
        parcel.printStatus();

        parcel.nextState();
        parcel.printStatus();

        parcel.nextState();
        parcel.printStatus();

        parcel.nextState();
        parcel.printStatus();
    }
}

class Parcel {

    private ParcelState state = new OrderedState();

    public ParcelState getState() {
        return state;
    }

    public void setState(ParcelState state) {
        this.state = state;
    }

    public void previousState() {
        state.prev(this);
    }

    public void nextState() {
        state.next(this);
    }

    public void printStatus() {
        state.printStatus();
    }
}

interface ParcelState {
    void next(Parcel parcel);
    void prev(Parcel parcel);
    void printStatus();
}

class OrderedState implements ParcelState {

    @Override
    public void next(Parcel parcel) {
        parcel.setState(new DeliveredState());
    }

    @Override
    public void prev(Parcel parcel) {
        System.out.println("The parcel is in its initial state.");
    }

    @Override
    public void printStatus() {
        System.out.println("Parcel ordered, not delivered to the delivery center yet.");
    }

    @Override
    public String toString() {
        return "Ordered";
    }
}

class DeliveredState implements ParcelState {

    @Override
    public void next(Parcel parcel) {
        parcel.setState(new ReceivedState());
    }

    @Override
    public void prev(Parcel parcel) {
        parcel.setState(new OrderedState());
    }

    @Override
    public void printStatus() {
        System.out.println("The parcel delivered to the delivery center, not received yet.");
    }

    @Override
    public String toString() {
        return "Delivered";
    }

}

class ReceivedState implements ParcelState {

    @Override
    public void next(Parcel parcel) {
        System.out.println("The parcel is received by a customer.");
    }

    @Override
    public void prev(Parcel parcel) {
        parcel.setState(new DeliveredState());
    }

    @Override
    public void printStatus() {
        System.out.println("The parcel was received by customer.");
    }

    @Override
    public String toString() {
        return "Received";
    }
}

 

소스코드를 보시면... ParcelState 를 구현한 OrderedState, DeliveredState, ReceivedState가 있고 Parcel(소포)는 무엇인가 변화가 있을 때 마다 다음 상태를 모르지만 다음 Step으로 넘기게 되면 알아서 상태가 변화하게 되는 것이죠.

OrderedState <----> DeliveredState <----> ReceivedState 의 상태가 변화될 수 있고, 이 상태는 Parcel이 가지고 있지 않아도 되는 것으로 보입니다.

 

정리

해당 링크에 나와 있는 패턴들을 살펴 봤는데요. 이 외에도 많은 패턴들이 있씁니다.

기본적으로 strategy pattern, facade pattern, observer pattern, pub sub pattern 등 도 알고 있으면 좋은 패턴들입니다.

차차 시간이 있을 때 마다 이러한 패턴들도 정리를 한 번씩 해 봐야 겠습니다.

 

 

 

 

 

728x90
반응형