본문 바로가기

CS

[CS] 디자인 패턴(1)

디자인 패턴이란?

프로그램을 설계할 때 발생했던 문제점들을 객체 간의 상호 관계 등을 이용하여 해결할 수 있도록 하나의 '규약' 형태로 만들어 놓은 것을 의미한다.

 

싱글톤 패턴
  • 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴이다.
  • 하나의 인스턴스만 가지고 로직을 만드는데 사용하며, 보통 데이터베이스 연결 모듈에서 많이 사용한다.
  • 하나의 인스턴스를 다른 모듈들이 공유해서 사용하므로 인스턴스를 생성하는 발생하는 비용을 줄일 수 있지만, 의존성이 높아진다는 단점이 존재한다.

 

싱글톤 패턴 예제

public class Singleton1 {
    private static class singleInstanceHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return singleInstanceHolder.INSTANCE;
    }

    public static void main(String[] args) {
        Singleton instanceA = Singleton.getInstance();
        Singleton instanceB = Singleton.getInstance();
        System.out.println(instanceA == instanceB); // true
    }
}

public class Singleton2 {
    private static final Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        Singleton instanceA = Singleton.getInstance();
        Singleton instanceB = Singleton.getInstance();
        System.out.println(instanceA == instanceB); // true
    }
}
  • singleInstanceHolderSingleton Class의 인스턴스를 하나 생성해서 보유하고 있는다.
  • Singleton.getInstance()를 호출할 때, singleInstanceHolder에 생성해둔 인스턴스를 반환함으로써 가져온 인스턴스가 동일한 인스턴스임을 만족할 수 있다.
  • 두 가지 예제 모두 INSTANCE의 유일성을 보장한다.

 

싱글톤 패턴 장점

  • 한 번의 new로 생성한 인스턴스를 사용하기 때문에 메모리 낭비를 막을 수 있어, 인스턴스를 생성하는데 발생하는 비용을 줄일 수 있다.
  • 싱글톤으로 만들어진 클래스의 인스턴스는 전역적으로 사용할 수 있기 때문에, 다른 클래스 인스턴스에서 사용하기 쉽다.
  • 인스턴스가 절대적으로 하나 존재함을 보장할 수 있다.

싱글톤 패턴 단점

  • 싱글톤으로 만들어진 클래스의 인스턴스가 너무 많은 일을 하거나, 너무 많은 데이터를 공유시킬 경우 다른 클래스의 인스턴스들과의 결합도(의존성)가 높아질 수 있다.
  • 높아진 결합도로 인해, 수정이 어려워지고 유지보수의 비용이 높아질 수 있다.
  • TDD(Test Driven Development)를 할 때 걸림돌이 될 수 있다.
    • TDD를 진행할 때는 보통 단위 테스트로 진행하고, 단위 테스트는 테스트가 서로 독립적이어야 하며 테스트를 어떤 순서로든 진행할 수 있어야 한다.
    • 하지만, 싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 구현하는 패턴이기 때문에, 각 테스트마다 독립적인 인스턴스를 만들기 어렵다.

 

* 싱글톤 패턴 사용으로 인한 높아진 결합도는 의존성 주입(Dependency Injection)을 통해 모듈 간의 결합을 조금 더 느슨하게 만들어 해결할 수 있다. -> 상위 클래스에서 하위 클래스를 생성하는 것이 아닌, 하위 클래스에서 인스턴스를 생성하고, 생성된 인스턴스를 상위 클래스에서 주입한다.

* 의존성 주입의 원칙: 상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 하고, 둘 다 추상화에 의지해야 하며, 추상화는 세부 사항에 의존하지 않아야 한다.

 

 

팩토리 패턴
  • 객체를 사용하는 코드에서 객체 생성 부분을 떼어내 추상화한 패턴이자 상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고, 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정하는 패턴이다.

 

팩토리 패턴 예제 

public class Factory {
    public static Car getCar(String type, String name) {
        if ("Hyundai".equalsIgnoreCase(type)) {
            return new Hyundai(name);
        } else if ("Kia".equalsIgnoreCase(type)) {
            return new Kia(name);
        }

        return new DefaultCar(name);
    }

    public abstract static class Car {
        public abstract String getName();

        @Override
        public String toString() {
            return "Car name is " + this.getName();
        }
    }

    public static class Hyundai extends Car {
        private String name;

        public Hyundai(String name) {
            this.name = name;
        }

        @Override
        public String getName() {
            return this.name;
        }
    }

    public static class Kia extends Car {
        private String name;

        public Kia(String name) {
            this.name = name;
        }

        @Override
        public String getName() {
            return this.name;
        }
    }

    public static class DefaultCar extends Car {
        private String name;

        public DefaultCar(String name) {
            this.name = name;
        }

        @Override
        public String getName() {
            return this.name;
        }
    }

    public static void main(String[] args) {
        Car genesis = Factory.getCar("Hyundai", "Genesis");
        Car k7 = Factory.getCar("Kia", "K7");

        System.out.println(genesis.toString());
        System.out.println(k7.toString());
    }
}
  • Car(상위 클래스)와 Hyundai, Kia(하위 클래스)를 분리하고, Factory Class를 사용해서 간단하게 원하는 하위 클래스를 생성한다. 

 

팩토리 패턴 장점

  • 상위 클래스와 하위 클래스가 분리되기 때문에 느슨한 결합을 가진다.
  • 상위 클래스에서는 인스턴스 생성 방식에 대해 전혀 알 필요가 없기 때문에 더 많은 유연성을 가지게 된다.
  • 객체 생성 로직이 따로 떼어져 있기 때문에, 코드를 리팩터링 하더라도 한 곳만 고칠 수 있게 되어 유지 보수성이 증가된다.

 

팩토리 패턴 단점

  • 필요할 때마다 새로운 서브 클래스를 생성해야 하기 때문에, 클래스가 많아진다.

 

전략 패턴
  • 객체의 행위를 바꾸고 싶은 경우 직접 수정하지 않고, 전략이라고 부르는 캡슐화한 알고리즘을 컨텍스트 안에서 바꿔주면서 상호 교체가 가능하게 만드는 패턴이다.
  • 같은 문제를 해결하는 여러 알고리즘이 클래스별로 캡슐화 되어 있고, 이들을 필요할 때마다 교체해서 사용할 수 있게 한다.

 

전략 패턴 예제

public abstract class Person {
    private String name;
    private MoveStrategy moveStrategy;

    public Person(String name, MoveStrategy moveStrategy) {
        this.name = name;
        this.moveStrategy = moveStrategy;
    }

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

    public void setMoveStrategy(MoveStrategy moveStrategy) {
        this.moveStrategy = moveStrategy;
    }
}

public class Man extends Person {
    public Man(String name, MoveStrategy moveStrategy) {
        super(name, moveStrategy);
    }
}

public interface MoveStrategy {
    void move();
}

public class RunningStrategy implements MoveStrategy {
    @Override
    public void move() {
        System.out.println("Running...");
    }
}

public class WalkingStrategy implements MoveStrategy {
    @Override
    public void move() {
        System.out.println("Walking...");
    }
}

public class Client {
    public static void main(String[] args) {
        Person agent1 = new Man("Agent1", new WalkingStrategy());
        agent1.move(); // Walking...

        agent1.setMoveStrategy(new RunningStrategy());
        agent1.move(); // Running...
    }
}
  • agent1의 move 전략을 기존 걷기 전략에서 뛰기 전략으로 바꾸고 싶을 때, 기존 코드를 뛰기로 변경하는 것은 "개방-폐쇄" 원칙에 어긋나는 방법이다.
  • 위와 같이 전략 패턴을 사용하게 되면, move 전략으로 뛰기 전략을 새롭게 만들어서 agent1의 move 전략을 뛰기 전략으로 바꿔줄 수 있다.
  • 새로운 기능을 추가하게 되면, 기존의 코드에는 영향을 주지 않고 걷기에서 뛰기로 변경할 수 있다.

 

전략 패턴 장점

  • 런타임에 객체 내부에서 사용되는 알고리즘을 바꿀 수 있다.
  • 기존의 컨텍스트를 변경하지 않고, 새로운 전략을 설정함으로써 개방-폐쇄 원칙을 만족할 수 있다.

 

전략 패턴 단점

  • 변경 사항이 많아지면, 그만큼 새로운 클래스가 생성되어 프로그램을 복잡하게 만든다.
  • 개발자는 적절한 전략을 선택하기 위해서, 전략을 모두 파악해야 한다.

 

* REF

면접을 위한 CS 전공지식 노트