디자인 패턴이란?
프로그램을 설계할 때 발생했던 문제점들을 객체 간의 상호 관계 등을 이용하여 해결할 수 있도록 하나의 '규약' 형태로 만들어 놓은 것을 의미한다.
싱글톤 패턴
- 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴이다.
- 하나의 인스턴스만 가지고 로직을 만드는데 사용하며, 보통 데이터베이스 연결 모듈에서 많이 사용한다.
- 하나의 인스턴스를 다른 모듈들이 공유해서 사용하므로 인스턴스를 생성하는 발생하는 비용을 줄일 수 있지만, 의존성이 높아진다는 단점이 존재한다.
싱글톤 패턴 예제
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
}
}
- singleInstanceHolder에 Singleton 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 전공지식 노트