디자인 패턴

hjkim·2022년 8월 10일
1

Singleton 패턴

객체의 생성(Creational)과 관련된 패턴이다. 객체의 인스턴스가 메모리에 오직 1개만 존재하도록 생성하는 패턴을 의미한다. 주의할 부분은 단 하나만 존재하도록 코드를 구현하였으나 복수개의 Object를 가질 수 있는 여지가 존재한다. 따라서 Single Object와 Singleton을 구분해야 한다.

Singleton 패턴을 사용해야 하는 경우

  • 객체를 하나 생성해서 공용으로 사용하고 싶은 경우

Request가 들어올 때마다 DB 연결 정보를 담은 객체를 생성하게 된다면 메모리에 생성되는 객체의 수가 유저의 수만큼 늘어나게 된다. 심지어 이 request들의 DB 연결 정보들은 같은 DB를 바라보고 있으므로 전부 동일하다. 이렇듯 동일한 정보를 담은 객체가 유저의 수만큼 늘어나게 되는 것은 비효율적이다. 따라서 Singleton 패턴을 도입하여 최초에 한 번 연결 정보를 담은 객체를 생성한 뒤 그 이후부터는 생성된 동일한 객체를 받아와 사용하는 편이 좋다.

1. Singleton 구현 - private 생성자

public class MySingleton {
    private MySingleton() {}
}

외부에서 MySingleton 인스턴스를 생성할 수 있게 된다면 메모리에 여러개의 인스턴스가 존재하게 되어 Singleton 패턴이 될 수 없다. 따라서 생성자를 private으로 만들어 외부에서 MySingleton 인스턴스를 생성할 수 없게 한다.

이때, 외부에서 MySingleton 인스턴스를 사용해서 작업을 해야 하는 경우가 존재할 수 있다. 이를 위해 MySingleton 객체 내에서 객체를 생성하고 이 객체를 얻어올 수 있게 하는 static method를 만든다.

2. Singleton 구현 - static method

public class MySingleton {
    private MySingleton() {}
	
    private static MySingleton instance;
    
    public static MySingleton getInstance() [
    	if (instance == null) {
        	instance = new MySingleton();
        }
        return instance;
    ]
}

위와 같이 static 필드와 getInstance라는 static 메소드를 정의함에 따라 외부에서 MySingleton.getInstance()를 통해 MySingleton 객체에 접근하여 사용할 수 있게 된다. MySingleton 인스턴스는 한 번만 생성되므로 Singleton 패턴이라 할 수 있다.
이때, Thread1과 Thread2가 동시에 if (instance == null)이란 코드에 도달하는 경우를 생각해본다. Thread1과 Thread2는 동시에 접근했으므로 instance를 null로 판단한다. 따라서 Thread1에서도, Thread2에서도 MySingleton 객체를 생성하려 한다. 이렇게 될 경우 MySingleton 인스턴스가 2개 생성이 되어 Singleton 패턴을 지키지 못하게 된다. 이러한 동시성 문제를 해결하기 위해 synchronized를 사용한다.

3. Singleton 구현 - synchronized

public class MySingleton {
    private MySingleton() {}
	
    private static MySingleton instance;
    
    public static MySingleton getInstance() [
    	if (instance == null) {
        	synchronized(MySingleton.class) {
            	if (instance == null) {
                	instance = new MySingleton();
                }
            }
        }
        return instance;
    ]
}

Synchronized를 method에 붙이게 될 경우 Thread1이 작업을 진행하는 동안 Thread2가 계속 대기해야 하는 상황이 발생하게 된다. Thread1과 관계없이 진행할 수 있는 작업임에도 불구하고 class 전체에 대해 동기화되어야 하므로 Thread1의 작업이 끝날 때까지 기다려야 한다는 면에서 매우 비효율적이다. 따라서 위의 코드처럼 class 단위가 아닌 메소드 단위로 동기화를 시켜준다.

Thread1과 Thread2가 동시에 접근했을 때 이를 동기화시키고 있다. 이때 synchronized 블록 내부에서 null 체크를 한 번 더 진행함으로써 Thread1이 먼저 인스턴스를 생성했다면 Thread2에서는 해당 인스턴스를 생성하지 않도록 하고 있다.
synchronized 블록을 감싸고 있는 if문은 한 번 인스턴스가 생성되고 나면 synchronized 블록을 타지 않도록 막아준다. 이를 "double checked locking"이라 한다. 이를 통해 multi-thread 환경에서 safe한 Singleton 패턴이 만들어진다.

또한 MySingleton이라는 인스턴스 내부에서 처리하는 일이 시간을 오래 잡아먹는 일들로 이루어져 있다면 사용되기 전까지 먼저 생성해둘 필요가 없다. 해당 인스턴스가 아예 사용되지 않을 가능성도 있기 때문이다. 따라서 위의 코드처럼 getInstance 메소드가 호출되었을 때, 즉 MySingleton 인스턴스가 필요해진 시점에 이를 생성한다. 이와 같은 Lazy initialization 효과가 존재한다.

Singleton 패턴이 기피되는 이유

  • 객체지향의 원칙에 위배된다.

private 생성자를 지니고 있어 상속이 불가하므로 객체지향의 특징이 적용되지 않는다.


Factory 패턴

객체의 생성(Creational)과 관련된 패턴이다. factory method 패턴과 abstract method 패턴이 존재한다. 객체를 생성하는 인터페이스는 미리 정의하되, 인스턴스를 만들 클래스의 세부적인 사항을 결정하는 것은 서브 클래스인 패턴이다.

Factory 패턴을 사용해야 하는 경우

  • 생성할 객체를 기술하는 책임을 자신의 서브 클래스에 넘기고자 할 때

Client쪽의 객체에서 너무 자세한 내용을 지니게 된다면 코드의 변경이 많아지게 된다. 예를 들어 Client에서 Red라는 색 관련 class를 생성, 사용하는 코드를 작성했다. 추후 Orange도, Yellow도, Green도 생성, 사용하려 할 수 있다. 그때마다 Client 쪽의 코드는 계속 변경되어야 한다. 하지만 이를 Color라는 class로 공통적인 필드, 메소드를 묶는다면 색이 계속 추가되어도 Client 코드의 변경을 줄일 수 있다. 따라서 세부적인 내용을 서브 클래스에 넘기고자 할 때 Factory 패턴을 사용한다.

Factory Method 패턴 구현

@Getter
@Setter
public class Color {
	private String htmlName;
    private String hexCode;
}

public class Red extends Color {
	public Red() {
    	setHtmlName("Red");
        setHexCode("#FF0000");
    }
}

public class Yellow extends Color {
	public Yellow() {
    	setHtmlName("Yellow");
        setHexCode("#FFFF00");
    }
}

앞서 설명했던 것과 같이 Red, Yellow 내부의 세부사항들을 각자의 객체 내에서 지정하여 생성하도록 하였고 이들을 Color라는 상위 클래스로 묶었다.

이 상태에서 Red와 Yellow를 생성하려면 Client 쪽의 코드에서

Color red = new Red();
Color yellow = new Yellow();

와 같이 각각을 생성해주어야 한다. 이러한 부분 역시 interface로 추상화한다.

public interface ColorFactory {
	Color createColor();
}

public class RedColorFactory implements ColorFactory {
	@Override
    public Color createColor() {
    	return new Red();
    }
}

public class YellowColorFactory implements ColorFactory {
	@Override
    public Color createColor() {
    	return new Yellow();
    }
}

이 경우 Client에서 Red와 Yellow를 생성하는 코드는 아래와 같이 수정가능하다.

Color red = create(new RedColorFactory);
Color yellow = create(new YellowColorFactory);

private Color create(ColorFactory colorFactory) {
	Color someColor = colorFactory.createColor();
    return someColor
}

Color red = new Red();로 각각을 생성할 경우와 달리 interface로 생성을 하게 되면 Red를 생성할 때, Yellow를 생성할 때와는 달리 "Red made!"라는 문구를 출력하게 해달라는 요구사항이 있을 때 이를 처리하기에 훨씬 용이하다.
Color red = new Red();로 구현했을 경우에는 해당 코드를 일일이 찾아 그 줄 앞에 System.out.println("Red made!");를 추가해야 한다. 하지만 RedColorFactory가 존재하는 코드에서는 RedColorFactory에서 구현하고 있는 createColor의 메소드만 수정하면 되므로 요구사항 반영이 훨씬 용이해진다.

Abstract Factory 패턴 구현

Factory method 패턴의 확장판이다. RedColorFactory를 통해 Red라는 Color를 생성해냈다면 Abstract Factory 패턴에서는 RedColorFactory에서 Red와 관련된 객체들을 추가적으로 생성할 수 있게 해준다. 즉, Red와 관계된 객체의 집합을 생성해준다.

Red는 R:255, G:0, B:0으로 이루어져 있으므로 Red를 생성할 때 이를 구성하는 R과 G, B의 객체 집합을 함께 만들어야 하는 상황에 이를 활용할 수 있다.

@Getter
@Setter
public class Color {
	private String htmlName;
    private String hexCode;
    private RCode rCode;
    private GCode gCode;
    private BCode bCode;
}

public class Red extends Color {
	public Red() {
    	setHtmlName("Red");
        setHexCode("#FF0000");
    }
}
public interface ColorFactory {
	Color createColor();
}

public class RedColorFactory implements ColorFactory {

	private RGBFactory rgbFactory;
	@Override
    public Color createColor() {
    	Color red = new Red();
        red.setRCode(rgbFactory.createR());
        red.setGCode(rgbFactory.createG());
        red.setBCode(rgbFactory.createB());
    	return new Red();
    }
}
public interface RGBFactory {
	RCode createR();
    GCode createG();
    BCode createB();
}

public class RedRGBFactory implements RGBFactory {
	@Override
    public RCode createR() {
    	return new RCode(255);
    }
    
    @Override
    public GCode createG() {
    	return new GCode(0);
    }
    
    @Override
    public BCode createB() {
    	return new BCode(0);
    }
}

RCode 객체, GCode 객체, BCode 객체는 class로 만들 수도 있고 RCode, GCode, BCode와 관련된 메서드를 정의하고 싶은 경우에는 각각을 interface로 만들어 RedRCode, RedGCode, RedBCode 객체에서 각각을 implements하여 따로 구현하는 방법이 존재한다. 해당 부분의 코드는 생략한다. Client의 코드는 아래와 같이 변경된다.

ColorFactory colorFactory = new RedColorFactory(new RedRGBFactory());
Color red = colorFactory.createColor();

Builder 패턴

객체의 생성(Creational)과 관련된 패턴이다. 객체지향 프로그래밍에서 다양한 객체를 유연하게 생성할 수 있도록 하기 위해 디자인 되었다. 복잡한 객체의 생성과 표현 방법을 분리하고자 하는 의도가 담겨있다.

Builder 패턴을 사용해야 하는 경우

  • 필요한 데이터만 설정하여 생성하고 싶을 때

User를 등록할 때 User의 고유번호는 DB에서 auto increment 값을 걸어두었기 때문에 DB가 정하게 된다. 따라서 Spring 객체에서는 userSeq에 불필요하게 null 값을 넣어 생성하게 되는데, 이때 builder 패턴을 사용하면 userSeq에 값을 지정해주지 않아도 객체 생성이 가능해진다.

  • 동일한 타입과 개수를 갖는 생성자로 서로 다른 객체를 생성해야 하는 경우

User라는 객체 내부에 String name, String nickname, String englishName이 존재한다고 가정한다. 이때 어떠한 경우에는 name과 nickname 만으로 객체를 생성하고 싶을 수 있고, 어떠한 경우에는 name과 englishName 만으로 객체를 생성하고 싶을 수 있다. 두 경우에는 모두 String 타입을 2개씩 갖는 생성자이므로 원하는 2개의 객체를 얻을 수 없다. 이러한 경우 Builder 패턴을 사용하여 name, nickname, englishName을 각각 명시하여 생성할 수 있다. 이러한 방식을 통해 코드의 가독성 또한 높아진다.

Builder 패턴 구현

앞에서 소개했던 name, nickname, englishName이라는 인스턴스 변수를 갖는 User 객체를 생성하는 Builder 패턴을 구현해보고자 한다.

public interface UserBuilder {
	UserBuilder name(String name);
    UserBuilder nickname(String nickname);
    UserBuilder englishName(String englishName);
    User build();
}

public class DefaultUserBuilder implements UserBuilder {
	private String name;
    private String nickname;
    private String englishName;
    
    @Override
    public UserBuilder name(String name) {
    	this.name = name;
        return this;
    }
    
    @Override
    public UserBuilder nickname(String nickname) {
    	this.nickname = nickname;
        return this;
    }
    
    @Override
    public UserBuilder englishName(String englishName) {
    	this.englishName = englishName;
        return this;
    }
    
    @Override
   	public User build() {
    	return new User(name, nickname, englishName);
    }

Override 하는 메소드에서 단순히 인스턴스 내부 변수의 값을 세팅하는 것이 아니라 nickname을 설정하려면 반드시 name 변수가 있어야 한다는 조건을 담은 코드를 추가하여 생성 과정에서 순서를 부여할 수도 있다.
Client를 통해 User 객체를 생성하는 방법은 아래와 같다.

public User createUser() {
	return userBuilder.name("hjkim")
    				  .nickname("dev-hjkim")
                      .englishName("hjkim")
                      .build();
}

Proxy 패턴

구조(Structural)와 관련된 패턴이다. 프록시는 "대리인" 이란 의미를 지니는 단어로, 말 그대로 실제 객체에 바로 접근하지 않고 대리인에 해당하는 객체를 통해 실제 객체에 접근하도록 구현되어 있다.

Proxy 패턴을 사용해야 하는 경우

  • 실제 객체의 기능이 반드시 필요한 시점까지 객체의 생성을 미루고자 할 때

프록시 객체에서는 실제 객체를 필요로 하는 시점까지 객체의 생성을 연기한다. 따라서 리소스가 많이 요구되는 작업들이 필요할 때 프록시 패턴이 사용된다.

  • 실제 객체로 바로 접근하기 전 사전 처리가 필요한 경우

구현한 코드 앞 뒤로 소요시간을 측정하는 코드를 넣고 싶은 경우나 실제 객체에 접근하기 전 접근자가 해당 객체에 접근할 권한이 있는지 확인해야 하는 경우와 같이 실제 객체에 접근하기 전 사전 처리가 필요한 경우에도 프록시 패턴이 사용된다.

Proxy 패턴 구현

이전에 작성했던 글인 프록시 패턴(Proxy Pattern)이 존재하여 생략한다.

프록시 패턴(Proxy Pattern)

Strategy 패턴

행동(Behavioral)과 관련된 패턴이다. 실행 중에 알고리즘을 선택할 수 있게 한다. 하나의 알고리즘을 즉시 실행하는 것 대신 런타임 중에 여러 개의 알고리즘 중 하나를 선택해 수행한다.

Strategy 패턴을 사용해야 하는 경우

  • 특정 작업을 수행하는 방법이 여러가지인 경우

전략 패턴에서는 일을 수행하는 방법이 여러가지인 경우 각각을 캡슐화하여 공통된 부분을 interface로 추상화한다. Client에서는 이 추상화된 interface의 메소드를 사용한다. 따라서 추후 수행할 수 잇는 방법을 추가해야할 때에도 Client와 interface 쪽의 코드 변경 없이 쉽게 전략을 추가할 수 있다. 단, Client 코드 쪽에서 전략이 어느 것들이 있는지 전부 알고 있어야 한다는 점에 유의해야 한다.

Strategy 패턴 구현

담장을 색칠하려고 한다. 빨간색으로 색칠할 수도 있고, 노란색으로 색칠할 수도 있고, 파란색으로 색칠할 수도 있다. 이렇게 칠하는 방법이 여러가지인 경우 strategy 패턴을 적용할 수 있다.

public interface PaintStrategy {
	void paintWall();
}

public class RedStrategy implements PaintStrategy {
	@Override
    public void paintWall() {
    	System.out.println("red wall");
    }
}

public class YellowStrategy implements PaintStrategy {
	@Override
    public void paintWall() {
    	System.out.println("yellow wall");
    }
}

RedStrategy라는 전략, YellowStrategy라는 전략이 구현되어있다. 이제 이 전략을 사용하는 Context 코드를 작성한다.

public class Wall {
	public void paint(PaintStrategy paintStrategy) {
    	paintStrategy.paintWall();
    }
}

아래의 코드는 Wall이라는 context를 통해 선택한 전략을 수행하고 있는 Client쪽 코드이다.

Wall wall = new Wall();
wall.paint(new RedStrategy());

벽을 파란색으로 칠하는 방법을 추가해야 한다고 할 때, BlueStrategy class만 생성해주면 되고 Client쪽 코드와 Context쪽 코드는 변하지 않는다. 단, 벽을 파란색으로 칠하는 방법으로 로직이 결정 된다면 Client쪽 코드가

wall.paint(new BlueStrategy());

와 같이 변경된다. 이처럼 코드의 변경을 최소화하여 전략을 새로 생성하거나 변경할 수 있다는 장점이 존재한다.


[참조] 인프런 백기선, 디자인패턴 강의
[참조] https://jeong-pro.tistory.com/86
[참조] https://gerrk.tistory.com/48
[참조] https://ssoco.tistory.com/65
[참조] https://en.wikipedia.org/wiki/Builder_pattern
[참조] https://mangkyu.tistory.com/163
[참조] https://en.wikipedia.org/wiki/Proxy_pattern
[참조] https://en.wikipedia.org/wiki/Strategy_pattern
[참조] https://kscory.com/dev/design-pattern/strategy

profile
피드백은 언제나 환영입니다! :)

2개의 댓글

comment-user-thumbnail
2022년 10월 28일

싱글턴에 대한 간략한 코멘트

  1. 사람들이 static을 싫어함 -> 충분히 OO적이지 못하다는...
  2. 그래서 OO적으로 쓸 수 있으면서도 전역적인 존재를 만듦... 바로 singleton!
  3. static에 비해 singleton이 갖는 장점:
    3.1. 다형성 사용 가능
    3.2. multiton으로 사용 가능
    3.3. 개체 생성 시점 제어 가능

팩토리에 대한 간략한 코멘트

  1. 생성자에 비해 create~류의 함수가 갖는 장점: null 반환 가능!
  2. 팩토리 메서드의 주의점
    2.1. 처음부터 추상적으로 사용하지 말고, 구체적인 것에서 시작
    2.2. 그러다보면 자연스럽게 팩토리로 갈 수도
  3. 장점:
    3.1. 클라이언트는 본인에게 익숙한 인자로 개체 생성 가능 (아아 300ml요! vs 톨이요!)
    3.2. null 반환을 통한 예외/오류 처리
    3.3. 다형성 적용 가능

프록시에 대한 간략한 코멘트

  1. 조심해서 사용해야함
  2. 보통 메모리 로딩에 대한 내용
    2.1. eager loading vs lazy loading
  3. 요즘엔 걍 한 방에 다 로딩해두는 게 일반적 (메모리가 많아짐)
  4. 프록시 쓰지 말고 아예 그냥 클라이언트한테 조작 권한을 주는게 나을 수도
1개의 답글