🎉 berenickt 블로그에 온 걸 환영합니다. 🎉
CS
GOF 디자인패턴-JAVA
01-행동 패턴

1. 싱글톤(Singleton) 패턴

gof_1_1

  • 인스턴스를 오직 한개만 제공하는 클래스
  • 시스템 런타임, 환경 세팅에 대한 정보 등, 인스턴스가 여러개 일 때 문제가 생길 수 있는 경우가 있다.
    • 인스턴스를 오직 한개만 만들어 제공하는 클래스가 필요하다.
  • e.g. 게임의 설정화면이 여러 개 설정화면이 아닌, 오직 하나의 설정화면을 글로벌하게 제공하는 것
    • 게임의 설정화면이 여러 인스턴스였다면, A라는 설정화면, B라는 설정화면 등 난잡해짐
  • cf. singleton은 ‘단독 개체’, ‘독신자’라는 뜻 말고도 ‘정확히 하나의 요소만 갖는 집합’ 등의 의미가 있다

1.1 구현 방법1-가장 단순

1
package me.whiteship.designpatterns._01_creational_patterns._01_singleton;
2
3
import java.io.*;
4
5
public class App {
6
7
public static void main(String[] args) {
8
Settings settings = new Settings();
9
Settings settings1 = new Settings()
10
11
System.out.println(settings != settings1); // true
12
}
13
}
  • 싱글톤 패턴을 구현하려면, new를 절대 사용하면 안된다.

  • Java에서 new을 사용해 생성자를 쓰지 못하게 만드려면,

    • private 생성자를 만들어서,
    • 해당 클래스 안에서만 접근할 수 있는 생성자를 만들어주면, 클래스 밖에서는 생성자를 사용할 수 없다.
  • 이렇게 되면 밖에서는 인스턴스를 만들 수 없기 떄문에,

    • 해당 클래스 안에서 인스턴스를 만들어주는 방법을 글로벌하게 접근할 수 있는 방법을 제공해줘야 한다.
    • 여기서 말하는 글로벌 접근이 가능한 방법은 static을 제공해주는 것이다.
1
package me.whiteship.designpatterns._01_creational_patterns._01_singleton;
2
3
/**
4
* private 생성자와 public static 메소드를 사용하는 방법
5
*/
6
public class Settings1 {
7
8
private static Settings1 instance;
9
10
private Settings1() {}
11
12
public static Settings1 getInstance() {
13
if (instance == null) {
14
instance = new Settings1();
15
}
16
17
return instance;
18
}
19
}
1
public class App {
2
public static void main(String[] args) {
3
Settings1 settings = Settings1.getInstance();
4
Settings1 settings1 = Settings1.getInstance();
5
6
System.out.println(settings == settings1); // true
7
}
8
}

이 방식의 심각한 문제점은, 그 중에서도 웹 앱을 만들떄, 멀티 쓰레드를 사용하게 된다.

  • 대부분의 코드는 멀티쓰레드, 즉, 여러 쓰레드가 동시 접근할 수 있는 코드가 된다
  • 멀티스레드 환경에서 이 코드는 안전하지 않다.

💡 복습

  1. 생성자를 private으로 만든 이유?
    • 오직 한 개의 인스턴스에만 접근하기 위해 생성자의 노출을 막기 위해서.
  2. getInstance() 메소드를 static으로 선언한 이유?
    • 글로벌하게 접근하게 만들기 위해 static 으로 선언하고,
    • 이는 JVM에 클래스 영역에 생성되어 글로벌하게 사용할 수 있다
  3. getInstance()가 멀티쓰레드 환경에서 안전하지 않은 이유?
    • 특히 웹 애플리케이션을 만들 때 멀티 스레드를 사용하게 된다.
    • 멀티쓰레드 환경에서는 오직 한 개의 인스턴스가 아니게 됩니다.
    • 새롭게 생성된 인스턴스에서도 instance가 null인지 여부를 판단하게 된다.
    • 이때 새로운 스레드에는 생성된 instance 가 없기 때문에 한 개의 인스턴스를 보장할 수 없다.

1.2 구현 방법2-멀티 쓰레드 환경에서 안전하게 구현

1
/**
2
* synchronized 키워드를 사용해서 동기화 처리
3
*/
4
public class Settings2 {
5
6
private static Settings2 instance;
7
8
private Settings2() {}
9
10
public static synchronized Settings2 getInstance() {
11
if (instance == null) {
12
instance = new Settings2();
13
}
14
15
return instance;
16
}
17
}

동기화(synchronized) 키워드를 사용해 멀티쓰레드 환경에 안전하게 만드는 방법

  • 다만 이 방법의 단점은 getInstance 메소드로 호출할 때마다,
    • 동기화처리하는 작업때문에 성능 상에 약간의 불이익이 생길 수 있다.
  • 왜냐하면 동기화라는 메커니즘 자체가 어떤 락(열쇠, 잠금)을 사용해,
    • 그 락을 가지고 있는 쓰레드만 이 영역에 접근할 수 있게끔 해주는 매커니즘이기 떄문이다.
    • 다 쓰고 나면 그 락을 해제하는 메커니즘을 처리하는 과정이 필요하기 때문에 부가적인 성능 부하가 생길 수 있다.

💡 복습

  1. 자바의 동기화 블럭 처리 방법은?
    • 정확하게는 메서드에 synchronized하는데, 메소드 전체가 임계영역으로 설정된다.
    • 임계 영역으로 설정된 부분은 쓰레드가 synchronized 메소드가 호출된 시점부터
    • 해당 메소드가 포함된 객체의 Lock을 얻어 작업을 수행하다가 메소드가 종료되면 Lock을 반환한다.
  2. getInstance() 메소드 동기화시 사용하는 락(lock)은 인스턴스의 락인가 클래스의 락인가? 그 이유는?
    • 클래스의 락이다.
    • 만약 락이 인스턴스의 락이라면, 동기화시 하나의 객체를 보장할 수 없게 되기 때문

1.3 구현 방법3-이른 초기화

만약에 이 객체를 꼭 나중에 만들지 않아도 되고, 이 객체를 만드는 비용이 그렇게 비싸지 않다면, 미리 만들 수도 있다.

  • 이른 초기화(eager initialization)을 사용하는 방법
1
/**
2
* 이른 초기화(eager initialization)을 사용하는 방법
3
*/
4
public class Settings3 {
5
6
private static final Settings3 INSTANCE = new Settings3();
7
8
private Settings3() { }
9
10
public static Settings3 getInstance() {
11
return INSTANCE;
12
}
13
14
}

💡 복습

  1. 이른 초기화가 단점이 될 수도 있는 이유?
    • 미리 만들게 되는 것이 단점이 될 수 있는데, 만약 생성자에 많은 리소스를 사용되는 경우에는 좋지 않다.
  2. 만약에 생성자에서 checked 예외를 던진다면 이 코드를 어떻게 변경해야 할까요?
    • 만약 생성자에서 예외를 던진다면,
    • 그 안에서 try-catch 으로 예외 핸들링을 해야만 한다. 그렇지 않다면, 이른 초기화를 사용할 수 없다.

1.4 구현 방법4-double checked locking

1
package me.whiteship.designpatterns._01_creational_patterns._01_singleton;
2
3
/**
4
* double checked locking
5
*/
6
public class Settings4 {
7
8
private static volatile Settings4 instance;
9
10
private Settings4() {
11
}
12
13
public static Settings4 getInstance() {
14
if (instance == null) {
15
synchronized (Settings4.class) {
16
if (instance == null) {
17
instance = new Settings4();
18
}
19
}
20
}
21
22
return instance;
23
}
24
25
}
  • double checked locking으로 효율적인 동기화 블럭 만들기
  • java 1.5 이상에서 동작

💡 복습

  1. double check locking이라고 부르는 이유?
    • 2번에 걸쳐 단 하나의 객체임을 체크하기 때문이다.
    • if 문이 2번 사용되는데, 첫번째는 instance가 null일 경우와,
      • 동기화 클래스 synchronized(xx.class) 에서 한번 더 체크하기 때문이다.
    • 동기화 매커니즘을 사용하지 않습니다.
    • 만약 instance가 있다면 바로 리턴하기 때문이다.
  2. instacne 변수는 어떻게 정의해야 하는가? 그 이유는?
    • volatile을 사용하여, 가장 최신의 객체를 가져오도록 한다.
    • Multi Thread 환경에서 하나의 Thread만 read & write하고,
    • 나머지 Thread가 read하는 상황에서 가장 최신의 값을 보장한다.

1.5 구현 방법5-static inner 클래스 사용 (권장)

1
package me.whiteship.designpatterns._01_creational_patterns._01_singleton;
2
3
/**
4
* static inner 클래스 홀더
5
*/
6
public class Settings5 {
7
8
private Settings5() {
9
}
10
11
private static class Settings5Holder {
12
private static final Settings5 INSTANCE = new Settings5();
13
}
14
15
public static Settings5 getInstance() {
16
return Settings5Holder.INSTANCE;
17
}
18
19
}
  • 권장하는 방법중 하나.
  • 이 방법은 static final 를 썻는데도, 왜 지연 초기화(lazy intialization)라고 볼 수 있는가?
    • Holder를 통해 객체를 생성하게 되는데,
    • 이렇게 할 경우 getIntance()가 호출될 때 로딩되기 때문이다.

1.6 싱글톤 구현 깨트리는 방법1

1
public class App {
2
3
public static void main(String[] args) throws IOException, ClassNotFoundException {
4
Settings settings = Settings.getInstance();
5
6
Constructor<Settings> declaredConstructor = Settings.class.getDeclaredConstructor();
7
declaredConstructor.setAccessible(true);
8
Settings settings1 = declaredConstructor.newInstance();
9
10
System.out.println(settings == settings1); // false
11
}
12
13
}

💡 복습

  1. 리플렉션에 대해 설명하세요.
    • 구체적인 클래스 타입을 알지 못해도,
    • 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API
  2. setAccessible(true)를 사용하는 이유는?
    • private 생성자에 접근하기 위한 목적이다.

1.7 싱글톤 구현 깨트리는 방법2

1
public class App {
2
3
public static void main(String[] args) throws IOException, ClassNotFoundException {
4
Settings5 settings = Settings5.INSTANCE;
5
6
Settings5 settings1 = null;
7
try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settings.obj"))) {
8
out.writeObject(settings);
9
}
10
11
try (ObjectInput in = new ObjectInputStream(new FileInputStream("settings.obj"))) {
12
settings1 = (Settings5) in.readObject();
13
}
14
15
System.out.println(settings == settings1);
16
}
17
18
}

💡 복습

  1. 자바의 직렬화 & 역직렬화에 대해 설명하세요.
    • 직렬화: 자바 시스템 내부에서 사용되는 객체 또는 데이터를 바이트(byte) 형태로 변환
    • 역직렬화: 바이트로 변환된 데이터를 다시 객체로 변환
  2. SerializableId란 무엇이며 왜 쓰는가?
    • 직렬화된 클래스의 버전을 기억하여 로드된 클래스와 직렬화된 객체가 호환되는지 확인한다.
    • SerializableId가 다르면 역직렬화 할 수 없다.
  3. try-resource 블럭에 대해 설명하세요
    • try 코드 블럭이 끝나면 자동으로 자원을 종료해주기 때문에 명시적으로 자원 반환을 하지 않아도 된다.

1.8 구현 방법6-enum (권장)

1
/**
2
* Enum을 사용해서 싱글톤 만들기
3
*/
4
public enum Settings5 {
5
INSTANCE,
6
}
  1. enum 타입의 인스턴스를 리팩토링을 만들 수 있는가?
    • 만들 수 없다.
    • enum 타입의 클래스는 리플랙션을 통해 만들 수 없도록 제한한다.
  2. enum으로 싱글톤 타입을 구현할 때의 단점은?
    • 단점은 이른 초기화와 같이 미리 만들어진다는 것이다.
    • 그리고 상속을 사용할 수 없다.
  3. 직렬화 & 역직렬화 시에 별도로 구현해야 하는 메소드가 있는가?
    • 별다른 장치가 없어도 Enum 클래스는 직렬화 & 역직렬화가 된다.
    • 그러나 getResolves() 구현시 역직렬화시 변경을 가할 수 있다.

1.9 실무에서는 어떻게 쓰이나?

  • 스프링에서 빈의 스코프 중에 하나로 싱글톤 스코프.
  • 자바 java.lang.Runtime
  • 다른 디자인 패턴(빌더, 퍼사드, 추상 팩토리 등) 구현체의 일부로 쓰이기도 한다.

2. 팩토리 메서드(Factory method) 패턴

gof_1_2

구체적으로 어떤 인스턴스를 만들지는 서브 클래스가 정한다.

  • 다양한 구현체(Product)가 있고,
    • 그 중에서 특정한 구현체를 만들 수 있는 다양한 팩토리 (Creator)를 제공할 수 있다.
  • e.g. 어떤 배를 만든다고 가정해보면,
    • 하얀 배를 만드는 공장에는 하얀배만 만들다가,
    • 사업이 잘되서 추후에 검정 배를 만드는 공장도 만들었다.
    • 색 칠하는 작업, 로고를 새기고, 다 만들면 알림을 울리는 등의 처리 프로세스가 있을 것이다.
    • 이 과정을 한 로직에 담아두면 복잡해질 것이다.
    • 그래서 개별 과정이 담긴 추상화되어 있는 공장을 만든다.
  • How? 위 그림처럼 공장(factory) 역할을 할 인터페이스를 만들고,
    • 인터페이스에 정의되어 있는 메소드는 여러 개가 있을 수 있다.
    • 이 구현부 중에 일부 바뀌어야 되는 것들을 추상 메서드로 빼내서 하위 클래스에서 만들게끔 한다.
    • 이 팩토리에서 만들어낸 다양한 객체를 만들 수 있게끔 Product라는 인터페이스를 만들고,
    • 그 각각의 구체적인 팩토리 안에서 구체적인 인스턴스들을 만들게끔 설계하면,
    • 유연한 확장에 용이한 구조가 된다.
  • cf. factory는 ‘공장’이란 뜻이고, 공장은 물건을 만드는 곳이다. 여기서 물건에 해당되는 것이 바로 인스턴스이다

2.1 구현 방법

gof_1_3

팩토리 메소드 구현 방법 : 확장에 열려있고 변경에 닫혀있는 구조로 만들어보자


2.2 예시 - 기존

1
// Ship.java
2
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method._01_before;
3
4
public class Ship {
5
6
private String name;
7
8
private String color;
9
10
private String logo;
11
12
public String getName() {
13
return name;
14
}
15
16
public void setName(String name) {
17
this.name = name;
18
}
19
20
public String getColor() {
21
return color;
22
}
23
24
public void setColor(String color) {
25
this.color = color;
26
}
27
28
public String getLogo() {
29
return logo;
30
}
31
32
public void setLogo(String logo) {
33
this.logo = logo;
34
}
35
36
@Override
37
public String toString() {
38
return "Ship{" + "name='" + name + '\'' + ", color='" + color + '\'' + ", logo='" + logo + '\'' + '}';
39
}
40
}
1
// ShipFactory.java
2
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method._01_before;
3
4
public class ShipFactory {
5
6
public static Ship orderShip(String name, String email) {
7
// validate
8
if (name == null || name.isBlank()) {
9
throw new IllegalArgumentException("배 이름을 지어주세요.");
10
}
11
if (email == null || email.isBlank()) {
12
throw new IllegalArgumentException("연락처를 남겨주세요.");
13
}
14
15
prepareFor(name);
16
17
Ship ship = new Ship();
18
ship.setName(name);
19
20
// Customizing for specific name
21
if (name.equalsIgnoreCase("whiteship")) {
22
ship.setLogo("\uD83D\uDEE5️");
23
} else if (name.equalsIgnoreCase("blackship")) {
24
ship.setLogo("⚓");
25
}
26
27
// coloring
28
if (name.equalsIgnoreCase("whiteship")) {
29
ship.setColor("whiteship");
30
} else if (name.equalsIgnoreCase("blackship")) {
31
ship.setColor("black");
32
}
33
34
// notify
35
sendEmailTo(email, ship);
36
37
return ship;
38
}
39
40
private static void prepareFor(String name) {
41
System.out.println(name + " 만들 준비 중");
42
}
43
44
private static void sendEmailTo(String email, Ship ship) {
45
System.out.println(ship.getName() + " 다 만들었습니다.");
46
}
47
}
48
1
// Client.java
2
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method._01_before;
3
4
public class Client {
5
6
public static void main(String[] args) {
7
Ship whiteship = ShipFactory.orderShip("Whiteship", "keesun@mail.com");
8
System.out.println(whiteship);
9
10
Ship blackship = ShipFactory.orderShip("Blackship", "keesun@mail.com");
11
System.out.println(blackship);
12
}
13
}

2.3 예시 - 변경

1
// ShipFactory.java
2
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after;
3
4
public interface ShipFactory {
5
default Ship orderShip(String name, String email) {
6
validate(name, email);
7
prepareFor(name);
8
Ship ship = createShip();
9
sendEmailTo(email, ship);
10
return ship;
11
}
12
13
void sendEmailTo(String email, Ship ship);
14
15
Ship createShip();
16
17
private void validate(String name, String email) {
18
if (name == null || name.isBlank()) {
19
throw new IllegalArgumentException("배 이름을 지어주세요.");
20
}
21
if (email == null || email.isBlank()) {
22
throw new IllegalArgumentException("연락처를 남겨주세요.");
23
}
24
}
25
26
private void prepareFor(String name) {
27
System.out.println(name + " 만들 준비 중");
28
}
29
}
1
// DefaultShipFactory.java
2
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after;
3
4
public abstract class DefaultShipFactory implements ShipFactory {
5
6
@Override
7
public void sendEmailTo(String email, Ship ship) {
8
System.out.println(ship.getName() + " 다 만들었습니다.");
9
}
10
}
1
// WhiteshipFactory.java
2
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after;
3
4
public class WhiteshipFactory extends DefaultShipFactory {
5
6
@Override
7
public Ship createShip() {
8
return new Whiteship();
9
}
10
}
1
// Ship.java
2
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after;
3
4
import me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._02_after.Anchor;
5
import me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._02_after.Wheel;
6
7
public class Ship {
8
9
private String name;
10
11
private String color;
12
13
private String logo;
14
15
private Wheel wheel;
16
17
private Anchor anchor;
18
19
public String getName() {
20
return name;
21
}
22
23
public void setName(String name) {
24
this.name = name;
25
}
26
27
public String getColor() {
28
return color;
29
}
30
31
public void setColor(String color) {
32
this.color = color;
33
}
34
35
public String getLogo() {
36
return logo;
37
}
38
39
public void setLogo(String logo) {
40
this.logo = logo;
41
}
42
43
@Override
44
public String toString() {
45
return (
46
"Ship{" + "name='" + name +
47
'\'' + ", color='" + color +
48
'\'' + ", logo='" + logo + '\'' +
49
'}'
50
);
51
}
52
53
public Wheel getWheel() {
54
return wheel;
55
}
56
57
public void setWheel(Wheel wheel) {
58
this.wheel = wheel;
59
}
60
61
public Anchor getAnchor() {
62
return anchor;
63
}
64
65
public void setAnchor(Anchor anchor) {
66
this.anchor = anchor;
67
}
68
}
1
// Whiteship.java
2
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after;
3
4
public class Whiteship extends Ship {
5
6
public Whiteship() {
7
setName("whiteship");
8
setLogo("\uD83D\uDEE5");
9
setColor("white");
10
}
11
}
1
// BlackshipFactory.java
2
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after;
3
4
public class BlackshipFactory extends DefaultShipFactory {
5
6
@Override
7
public Ship createShip() {
8
return new Blackship();
9
}
10
}
1
// Blackship.java
2
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after;
3
4
public class Blackship extends Ship {
5
6
public Blackship() {
7
setName("blackship");
8
setColor("black");
9
setLogo("⚓");
10
}
11
}
1
// Client.java
2
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after;
3
4
public class Client {
5
6
public static void main(String[] args) {
7
Client client = new Client();
8
client.print(new WhiteshipFactory(), "whiteship", "keesun@mail.com");
9
client.print(new BlackshipFactory(), "blackship", "keesun@mail.com");
10
}
11
12
private void print(ShipFactory shipFactory, String name, String email) {
13
System.out.println(shipFactory.orderShip(name, email));
14
}
15
}

2.4 정리 및 장단점

구체적으로 어떤 것을 만들지는 서브 클래스가 정한다.

  • 팩토리 메소드 패턴을 적용했을 때의 장점은? 단점은?
    • 장점 : 확장에 열려있고 변경에 닫혀있는 객체 지향 원칙을 적용해서,
      • 기존 코드를 변경하지 않고, 새로운 인스턴스를 다른 방법으로 확장이 가능하다
    • 단점 : 클래스가 많아 진다.
  • “확장에 열려있고 변경에 닫혀있는 객체 지향 원칙”을 설명하세요.
    • 기존 코드를 변경하지 않으면서, 새로운 기능을 확장할 수 있는 구조를 만드는 것
  • 자바 8에 추가된 default 메소드에 대해 설명하세요.
    • 인터페이스에서 기본적인 구현체를 만들 수 있다.
    • 오히려 추상 클래스에서 하던 일을 인터페이스에서도 할 수 있게끔 되었기 때문에
      • Java 8부터는 추상 클래스를 그렇게 많이 쓰지는 않는다.
    • 정말 추상 클래스가 필요한 경우가 아니라면, 인터페이스에 있는 defaut 메서드를 사용해서 시도해보길 바란다.
    • 나아가 Java 9에서는 인터페이스에 private 메소드를 정의하는 것도 있다.

2.5 실무에서는 어떻게 쓰이나?

단순한 팩토리 패턴

  • 매개변수의 값에 따라 또는 메소드에 따라 각기 다른 인스턴스를 리턴하는 단순한 버전의 팩토리 패턴
  • java.lang.Calendar 또는 java.lang.NumberFormat
1
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method._03_java;
2
3
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after.Blackship;
4
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after.Whiteship;
5
6
public class SimpleFactory {
7
8
public Object createProduct(String name) {
9
if (name.equals("whiteship")) {
10
return new Whiteship();
11
} else if (name.equals("blackship")) {
12
return new Blackship();
13
}
14
15
throw new IllegalArgumentException();
16
}
17
}
1
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method._03_java;
2
3
import java.util.Calendar;
4
import java.util.Locale;
5
6
public class CalendarExample {
7
8
public static void main(String[] args) {
9
System.out.println(Calendar.getInstance().getClass()); // 그레고리력 달력
10
System.out.println(
11
Calendar
12
.getInstance(Locale.forLanguageTag("th-TH-x-lvariant-TH"))
13
.getClass()
14
); // 타이완 달력
15
System.out.println(
16
Calendar
17
.getInstance(Locale.forLanguageTag("ja-JP-x-lvariant-JP"))
18
.getClass()
19
); // 일본 달력
20
}
21
}

스프링 BeanFactory

  • Object 타입의 Product를 만드는 BeanFacotry라는 Creator!
1
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method._03_java;
2
3
import org.springframework.beans.factory.BeanFactory;
4
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
5
import org.springframework.context.support.ClassPathXmlApplicationContext;
6
7
public class SpringBeanFactoryExample {
8
9
public static void main(String[] args) {
10
BeanFactory xmlFactory = new ClassPathXmlApplicationContext("config.xml");
11
String hello = xmlFactory.getBean("hello", String.class);
12
System.out.println(hello);
13
14
BeanFactory javaFactory = new AnnotationConfigApplicationContext(
15
Config.class
16
);
17
String hi = javaFactory.getBean("hello", String.class);
18
System.out.println(hi);
19
}
20
}

3. 추상 팩토리(Abstract factory) 패턴

gof_1_4

서로 관련있는 여러 객체(=인스턴스)를 만들어주는 인터페이스.

  • 구체적으로 어떤 클래스의 인스턴스를(concrete product)를 사용하는지 감출 수 있다.

  • 인터페이스로 정의하거나, 추상 클래스로 정의하거나 그래서 구체적인 Factory를 만드는 것까지는 팩토리 메서드와 비슷하지만,

    • 초점이 클라이언트쪽에 있다.
    • Factory를 사용하는 클라이언트 쪽에 초점을 바라보면 추상 팩토리 패턴이다.
    • 반대로 Factory쪽에만 초점으로 바라보면 팩토리 메서드 패턴이다.
  • 팩토리 메소드의 목적 자체가 클라이언트의 코드를 인터페이스 기반으로 코딩하는 것에 있다.

  • 위 그림은 팩토리 메서드 패턴에서 클라이언트만 추가된 것일 뿐이다.

    • 내부 구조는 팩토리 메서드 패턴과 굉장히 유사하다.
  • e.g. 하얀 배와 검정 배를 만드는데, 비슷한 부품들이 세트로 있을 것이다. (e.g. 닻이나 배의 바퀴)

    • 그러다 발전된 부품이 새로 나왔다면, 해당 제품에 모두 다 바꿔줘야 될 것이다.
    • 이렇게 모두 바뀌는 것을 바뀌지 않게끔 하면서 제품군을 늘려나갈 수 있는 방법으로 추상 팩토리를 적용해본다.

3.1 구현 방법

gof_1_5

클라이언트 코드에서 구체적인 클래스의 의존성을 제거한다.


3.2 예시 - 기존

1
// Anchor.java
2
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._02_after;
3
4
public interface Anchor {}
1
// WhiteAnchor.java
2
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._01_before;
3
4
import me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._02_after.Anchor;
5
6
public class WhiteAnchor implements Anchor {}
1
// Wheel.java
2
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._02_after;
3
4
public interface Wheel {}
1
// WhiteWheel.java
2
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._01_before;
3
4
import me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._02_after.Wheel;
5
6
public class WhiteWheel implements Wheel {}
1
// WhiteshipFactory.java
2
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._01_before;
3
4
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after.DefaultShipFactory;
5
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after.Ship;
6
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after.Whiteship;
7
8
public class WhiteshipFactory extends DefaultShipFactory {
9
10
@Override
11
public Ship createShip() {
12
Ship ship = new Whiteship();
13
ship.setAnchor(new WhiteAnchor());
14
ship.setWheel(new WhiteWheel());
15
return ship;
16
}
17
}

3.3 예시 - 변경

1
// WhiteAnchorPro.java
2
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._02_after;
3
4
public class WhiteAnchorPro implements Anchor {}
1
// WhiteWheelPro.java
2
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._02_after;
3
4
public class WhiteWheelPro implements Wheel {}
1
// ShipPartsFactory.java
2
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._02_after;
3
4
import me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._01_before.WhiteAnchor;
5
import me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._01_before.WhiteWheel;
6
7
public class WhiteshipPartsFactory implements ShipPartsFactory {
8
9
@Override
10
public Anchor createAnchor() {
11
return new WhiteAnchor();
12
}
13
14
@Override
15
public Wheel createWheel() {
16
return new WhiteWheel();
17
}
18
}
1
// WhiteshipPartsFactory.java
2
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._02_after;
3
4
public class WhitePartsProFactory implements ShipPartsFactory {
5
6
@Override
7
public Anchor createAnchor() {
8
return new WhiteAnchorPro();
9
}
10
11
@Override
12
public Wheel createWheel() {
13
return new WhiteWheelPro();
14
}
15
}
1
// WhiteshipFactory.java
2
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._02_after;
3
4
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after.DefaultShipFactory;
5
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after.Ship;
6
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after.Whiteship;
7
8
public class WhiteshipFactory extends DefaultShipFactory {
9
10
private ShipPartsFactory shipPartsFactory;
11
12
public WhiteshipFactory(ShipPartsFactory shipPartsFactory) {
13
this.shipPartsFactory = shipPartsFactory;
14
}
15
16
@Override
17
public Ship createShip() {
18
Ship ship = new Whiteship();
19
ship.setAnchor(shipPartsFactory.createAnchor());
20
ship.setWheel(shipPartsFactory.createWheel());
21
return ship;
22
}
23
}
1
// ShipInventory.java
2
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._02_after;
3
4
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after.Ship;
5
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after.ShipFactory;
6
7
public class ShipInventory {
8
9
public static void main(String[] args) {
10
ShipFactory shipFactory = new WhiteshipFactory(new WhiteshipPartsFactory());
11
Ship ship = shipFactory.createShip();
12
System.out.println(ship.getAnchor().getClass());
13
System.out.println(ship.getWheel().getClass());
14
}
15
}

3.4 정리

팩토리 메소드 패턴과 굉장히 흡사한데 무엇이 다른건가.

  • 모양과 효과는 비슷하지만…
    • 둘 다 구체적인 객체 생성 과정을 추상화한 인터페이스를 제공한다.
  • 관점이 다르다.
    • 팩토리 메소드 패턴팩토리를 구현하는 방법(inheritance)에 초점을 둔다.
    • 추상 팩토리 패턴팩토리를 사용하는 방법(composition)에 초점을 둔다.
  • 목적이 조금 다르다.
    • 팩토리 메소드 패턴은 구체적인 객체 생성 과정을 하위 또는 구체적인 클래스로 옮기는 것이 목적.
    • 추상 팩토리 패턴은 관련있는 여러 객체를 구체적인 클래스에 의존하지 않고 만들 수 있게 해주는 것이 목적

3.5 실무에서는 어떻게 쓰이나?

자바 라이브러리

  • javax.xml.xpath.XPathFactory#newInstance()
  • javax.xml.transform.TransformerFactory#newInstance()
  • javax.xml.parsers.DocumentBuilderFactory#newInstance()
1
// DocumentBuilderFactoryExample.java
2
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._03_java;
3
4
import java.io.File;
5
import java.io.IOException;
6
import javax.xml.parsers.DocumentBuilder;
7
import javax.xml.parsers.DocumentBuilderFactory;
8
import javax.xml.parsers.ParserConfigurationException;
9
import org.w3c.dom.Document;
10
import org.xml.sax.SAXException;
11
12
public class DocumentBuilderFactoryExample {
13
14
public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException {
15
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
16
DocumentBuilder builder = factory.newDocumentBuilder();
17
18
Document document = builder.parse(new File("src/main/resources/config.xml"));
19
System.out.println(document.getDocumentElement());
20
}
21
}

스프링

  • FactoryBean과 그 구현체
1
// ShipFactory.java
2
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._03_java;
3
4
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after.Ship;
5
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after.Whiteship;
6
import org.springframework.beans.factory.FactoryBean;
7
8
public class ShipFactory implements FactoryBean<Ship> {
9
10
@Override
11
public Ship getObject() throws Exception {
12
Ship ship = new Whiteship();
13
ship.setName("whiteship");
14
return ship;
15
}
16
17
@Override
18
public Class<?> getObjectType() {
19
return Ship.class;
20
}
21
}
22
1
// FactoryBeanConfig.java
2
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._03_java;
3
4
import org.springframework.context.annotation.Bean;
5
import org.springframework.context.annotation.Configuration;
6
7
@Configuration
8
public class FactoryBeanConfig {
9
10
@Bean
11
public ShipFactory shipFactory() {
12
return new ShipFactory();
13
}
14
}
1
// FactoryBeanExample.java
2
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory._03_java;
3
4
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method._02_after.Ship;
5
import org.springframework.context.ApplicationContext;
6
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
7
import org.springframework.context.support.ClassPathXmlApplicationContext;
8
9
public class FactoryBeanExample {
10
11
public static void main(String[] args) {
12
// **** XML
13
// ApplicationContext applicationContext = new ClassPathXmlApplicationContext("config.xml");
14
// Ship whiteship = applicationContext.getBean("whiteship", Ship.class);
15
// System.out.println(whiteship.getName());
16
17
// **** Java
18
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(FactoryBeanConfig.class);
19
Ship bean = applicationContext.getBean(Ship.class);
20
System.out.println(bean);
21
}
22
}

4. 빌더(Builder) 패턴

4.1 패턴 소개

gof_1_6

동일한 프로세스를 거쳐 다양한 구성의 인스턴스를 만드는 방법.

  • (복잡한) 객체를 만드는 프로세스를 독립적으로 분리할 수 있다
  • 복잡한 객체의 생성 과정과 표현 방법을 분리하여,
    • 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 디자인 패턴

gof_1_7

e.g. 빌더 패턴을 적용하여 여행 계획을 만드는 과정은 마치 여행사에서 여행 패키지를 조합하는 것과 유사하다.

  • 여행 계획을 만들기 위해, 우리는 여행의 제목, 시작 날짜, 숙박 일수, 숙소, 그리고 여행 일정 등 다양한 세부 사항을 결정해야 한다.
  • 이 모든 정보를 한 번에 설정하는 대신, 빌더 패턴을 사용하면 여행 계획을 단계별로 구성할 수 있다.
  1. 여행 계획 시작: 먼저, 여행 계획을 만들기 위한 빌더 객체를 생성한다. 이 객체는 여행 계획의 기초를 마련한다.
  2. 세부 사항 추가: 빌더 객체를 사용하여 여행 계획의 세부 사항을 단계별로 추가한다.
    • e.g. 여행의 제목을 설정하고, 여행의 시작 날짜를 지정한다.
    • 그 다음, 숙박 일수와 숙소를 결정하고, 여행 일정에 여러 활동을 추가한다.
    • 이 과정에서 각 단계는 메소드 체이닝을 통해 연결되어, 코드의 가독성을 높인다.
  3. 여행 계획 완성: 모든 세부 사항을 추가한 후, 빌더 객체는 최종적으로 여행 계획 객체를 반환합니다.
    • 이 객체는 앞서 설정한 모든 세부 사항을 포함합니다.

빌더 패턴의 장점은 여행 계획과 같은 복잡한 객체를 유연하고 직관적으로 구성할 수 있다.

  • 각 단계에서 필요한 정보만을 설정할 수 있으며, 모든 세부 사항을 한 번에 설정할 필요가 없다.
  • 또한, 빌더 패턴은 선택적인 세부 사항을 쉽게 처리할 수 있게 해주며, 최종 객체의 생성 과정을 더 명확하고 이해하기 쉽게 만들어 준다.

4.2 예시 - 기존

1
// TourPlan.java
2
package me.whiteship.designpatterns._01_creational_patterns._04_builder._01_before;
3
4
import java.time.LocalDate;
5
import java.util.List;
6
7
public class TourPlan {
8
9
private String title;
10
11
private int nights;
12
13
private int days;
14
15
private LocalDate startDate;
16
17
private String whereToStay;
18
19
private List<DetailPlan> plans;
20
21
public TourPlan() {
22
}
23
24
public TourPlan(String title, int nights, int days, LocalDate startDate, String whereToStay,
25
List<DetailPlan> plans) {
26
this.title = title;
27
this.nights = nights;
28
this.days = days;
29
this.startDate = startDate;
30
this.whereToStay = whereToStay;
31
this.plans = plans;
32
}
33
34
@Override
35
public String toString() {
36
return "TourPlan{" + "title='" + title + '\'' + ", nights=" + nights + ", days=" + days + ", startDate="
37
+ startDate + ", whereToStay='" + whereToStay + '\'' + ", plans=" + plans + '}';
38
}
39
40
public String getTitle() {
41
return title;
42
}
43
44
public void setTitle(String title) {
45
this.title = title;
46
}
47
48
public int getNights() {
49
return nights;
50
}
51
52
public void setNights(int nights) {
53
this.nights = nights;
54
}
55
56
public int getDays() {
57
return days;
58
}
59
60
public void setDays(int days) {
61
this.days = days;
62
}
63
64
public LocalDate getStartDate() {
65
return startDate;
66
}
67
68
public void setStartDate(LocalDate startDate) {
69
this.startDate = startDate;
70
}
71
72
public String getWhereToStay() {
73
return whereToStay;
74
}
75
76
public void setWhereToStay(String whereToStay) {
77
this.whereToStay = whereToStay;
78
}
79
80
public List<DetailPlan> getPlans() {
81
return plans;
82
}
83
84
public void setPlans(List<DetailPlan> plans) {
85
this.plans = plans;
86
}
87
88
public void addPlan(int day, String plan) {
89
this.plans.add(new DetailPlan(day, plan));
90
}
91
}
1
// DetailPlan.java
2
package me.whiteship.designpatterns._01_creational_patterns._04_builder._01_before;
3
4
public class DetailPlan {
5
6
private int day;
7
8
private String plan;
9
10
public DetailPlan(int day, String plan) {
11
this.day = day;
12
this.plan = plan;
13
}
14
15
public int getDay() {
16
return day;
17
}
18
19
public void setDay(int day) {
20
this.day = day;
21
}
22
23
public String getPlan() {
24
return plan;
25
}
26
27
public void setPlan(String plan) {
28
this.plan = plan;
29
}
30
31
@Override
32
public String toString() {
33
return "DetailPlan{" + "day=" + day + ", plan='" + plan + '\'' + '}';
34
}
35
}
1
// App.class
2
package me.whiteship.designpatterns._01_creational_patterns._04_builder._01_before;
3
4
import java.time.LocalDate;
5
6
public class App {
7
8
public static void main(String[] args) {
9
TourPlan shortTrip = new TourPlan();
10
shortTrip.setTitle("오레곤 롱비치 여행");
11
shortTrip.setStartDate(LocalDate.of(2021, 7, 15));
12
13
TourPlan tourPlan = new TourPlan();
14
tourPlan.setTitle("칸쿤 여행");
15
tourPlan.setNights(2);
16
tourPlan.setDays(3);
17
tourPlan.setStartDate(LocalDate.of(2020, 12, 9));
18
tourPlan.setWhereToStay("리조트");
19
tourPlan.addPlan(0, "체크인 이후 짐풀기");
20
tourPlan.addPlan(0, "저녁 식사");
21
tourPlan.addPlan(1, "조식 부페에서 식사");
22
tourPlan.addPlan(1, "해변가 산책");
23
tourPlan.addPlan(1, "점심은 수영장 근처 음식점에서 먹기");
24
tourPlan.addPlan(1, "리조트 수영장에서 놀기");
25
tourPlan.addPlan(1, "저녁은 BBQ 식당에서 스테이크");
26
tourPlan.addPlan(2, "조식 부페에서 식사");
27
tourPlan.addPlan(2, "체크아웃");
28
}
29
}

4.3 예시 - 변경

1
// TourPlanBuilder.java
2
package me.whiteship.designpatterns._01_creational_patterns._04_builder._02_after;
3
4
import me.whiteship.designpatterns._01_creational_patterns._04_builder._01_before.TourPlan;
5
6
import java.time.LocalDate;
7
8
public interface TourPlanBuilder {
9
10
TourPlanBuilder nightsAndDays(int nights, int days);
11
12
TourPlanBuilder title(String title);
13
14
TourPlanBuilder startDate(LocalDate localDate);
15
16
TourPlanBuilder whereToStay(String whereToStay);
17
18
TourPlanBuilder addPlan(int day, String plan);
19
20
TourPlan getPlan();
21
22
}
1
// DefaultTourBuilder.java
2
package me.whiteship.designpatterns._01_creational_patterns._04_builder._02_after;
3
4
import me.whiteship.designpatterns._01_creational_patterns._04_builder._01_before.DetailPlan;
5
import me.whiteship.designpatterns._01_creational_patterns._04_builder._01_before.TourPlan;
6
7
import java.time.LocalDate;
8
import java.util.ArrayList;
9
import java.util.List;
10
11
public class DefaultTourBuilder implements TourPlanBuilder {
12
13
private String title;
14
15
private int nights;
16
17
private int days;
18
19
private LocalDate startDate;
20
21
private String whereToStay;
22
23
private List<DetailPlan> plans;
24
25
@Override
26
public TourPlanBuilder nightsAndDays(int nights, int days) {
27
this.nights = nights;
28
this.days = days;
29
return this;
30
}
31
32
@Override
33
public TourPlanBuilder title(String title) {
34
this.title = title;
35
return this;
36
}
37
38
@Override
39
public TourPlanBuilder startDate(LocalDate startDate) {
40
this.startDate = startDate;
41
return this;
42
}
43
44
@Override
45
public TourPlanBuilder whereToStay(String whereToStay) {
46
this.whereToStay = whereToStay;
47
return this;
48
}
49
50
@Override
51
public TourPlanBuilder addPlan(int day, String plan) {
52
if (this.plans == null) {
53
this.plans = new ArrayList<>();
54
}
55
56
this.plans.add(new DetailPlan(day, plan));
57
return this;
58
}
59
60
@Override
61
public TourPlan getPlan() {
62
return new TourPlan(title, nights, days, startDate, whereToStay, plans);
63
}
64
}
1
// TourDirector.java
2
package me.whiteship.designpatterns._01_creational_patterns._04_builder._02_after;
3
4
import java.time.LocalDate;
5
import me.whiteship.designpatterns._01_creational_patterns._04_builder._01_before.TourPlan;
6
7
public class TourDirector {
8
9
private TourPlanBuilder tourPlanBuilder;
10
11
public TourDirector(TourPlanBuilder tourPlanBuilder) {
12
this.tourPlanBuilder = tourPlanBuilder;
13
}
14
15
public TourPlan cancunTrip() {
16
return tourPlanBuilder
17
.title("칸쿤 여행")
18
.nightsAndDays(2, 3)
19
.startDate(LocalDate.of(2020, 12, 9))
20
.whereToStay("리조트")
21
.addPlan(0, "체크인하고 짐 풀기")
22
.addPlan(0, "저녁 식사")
23
.getPlan();
24
}
25
26
public TourPlan longBeachTrip() {
27
return tourPlanBuilder
28
.title("롱비치")
29
.startDate(LocalDate.of(2021, 7, 15))
30
.getPlan();
31
}
32
}
1
// App.java
2
package me.whiteship.designpatterns._01_creational_patterns._04_builder._02_after;
3
4
import me.whiteship.designpatterns._01_creational_patterns._04_builder._01_before.TourPlan;
5
6
public class App {
7
8
public static void main(String[] args) {
9
TourDirector director = new TourDirector(new DefaultTourBuilder());
10
TourPlan tourPlan = director.cancunTrip();
11
TourPlan tourPlan1 = director.longBeachTrip();
12
}
13
}

4.4 장단점

  • 장점
    • 만들기 복잡한 객체를 순차적으로 만들 수 있다.
    • 복잡한 객체를 만드는 구체적인 과정을 숨길 수 있다.
    • 동일한 프로세스를 통해 각기 다르게 구성된 객체를 만들 수도 있다.
    • 불완전한 객체를 사용하지 못하도록 방지할 수 있다.
  • 단점
    • 원하는 객체를 만들려면 빌더부터 만들어야 한다.
    • 구조가 복잡해 진다. (트레이드 오프)

4.5 실무에서 어떻게 쓰이나?

자바 8 Stream.Buidler API

1
// StreamExample.java
2
package me.whiteship.designpatterns._01_creational_patterns._04_builder._03_java;
3
4
import java.util.stream.Stream;
5
6
public class StreamExample {
7
8
public static void main(String[] args) {
9
Stream<String> names = Stream.<String>builder().add("keesun").add("whiteship").build();
10
names.forEach(System.out::println);
11
}
12
}

StringBuilder는 빌더 패턴일까?

1
// StringBuilderExample.java
2
package me.whiteship.designpatterns._01_creational_patterns._04_builder._03_java;
3
4
public class StringBuilderExample {
5
6
public static void main(String[] args) {
7
StringBuilder stringBuilder = new StringBuilder();
8
String result = stringBuilder.append("whiteship").append("keesun").toString();
9
System.out.println(result);
10
}
11
}
12

롬복의 @Builder

1
package me.whiteship.designpatterns._01_creational_patterns._04_builder._03_java;
2
3
import lombok.Builder;
4
5
@Builder
6
public class LombokExample {
7
8
private String title;
9
10
private int nights;
11
12
private int days;
13
14
public static void main(String[] args) {
15
LombokExample trip = LombokExample
16
.builder()
17
.title("여행")
18
.nights(2)
19
.days(3)
20
.build();
21
}
22
}

스프링

  • UriComponentsBuilder
  • MockMvcWebClientBuilder
  • …Builder
1
// SpringExample.java
2
package me.whiteship.designpatterns._01_creational_patterns._04_builder._03_java;
3
4
import org.springframework.web.util.UriComponents;
5
import org.springframework.web.util.UriComponentsBuilder;
6
7
public class SpringExample {
8
9
public static void main(String[] args) {
10
UriComponents howToStudyJava = UriComponentsBuilder
11
.newInstance()
12
.scheme("http")
13
.host("www.whiteship.me")
14
.path("java playlist ep1")
15
.build()
16
.encode();
17
System.out.println(howToStudyJava);
18
}
19
}

5. 프로토타입(Prototype) 패턴

gof_1_8

기존 인스턴스를 복제하여 새로운 인스턴스를 만드는 방법

  • 복제 기능을 갖추고 있는 기존 인스턴스를 프로토타입으로 사 용해 새 인스턴스를 만들 수 있다

gof_1_9

e.g. GitHub에서는 각각의 저장소(repository)가 고유한 설정, 파일, 이슈(issue) 등을 가지고 있다.

  • 특정 프로젝트를 위한 새로운 저장소를 만들 때, 기존 저장소의 설정이나 구조를 그대로 사용하기 위해,
    • 기존 저장소를 복사하여 새로운 저장소를 만드는 것이 더 효율적이다.
  • 프로토타입 패턴을 적용하면, 기존의 GitHub 저장소 객체를 복사(clone)하여 새로운 저장소 객체를 생성할 수 있다.
    • 이 과정에서 기존 저장소의 설정, 파일 구조, 이슈 템플릿 등이 새로운 저장소로 복사된다.
    • 복사된 새 객체는 필요에 따라 추가적인 수정을 거쳐 사용될 수 있다.

프로토타입 패턴의 핵심은 복사수정이다.

  • 기존 객체를 복사하여 새 객체를 만들고, 이 새 객체에 대해 필요한 수정을 수행함으로써,
    • 객체 생성 과정을 간소화하고, 효율성을 높일 수 있다.
  • GitHub 저장소 예시에서 볼 수 있듯,
    • 프로토타입 패턴은 설정이 복잡하거나, 객체의 상태가 다양하게 변할 수 있는 상황에서 특히 유용합니다.

5.1 예시 - 기존

1
// GithubRepository.java
2
package me.whiteship.designpatterns._01_creational_patterns._05_prototype._01_before;
3
4
public class GithubRepository {
5
6
private String user;
7
8
private String name;
9
10
public String getUser() {
11
return user;
12
}
13
14
public void setUser(String user) {
15
this.user = user;
16
}
17
18
public String getName() {
19
return name;
20
}
21
22
public void setName(String name) {
23
this.name = name;
24
}
25
}
1
// GithubIssue.java
2
package me.whiteship.designpatterns._01_creational_patterns._05_prototype._01_before;
3
4
public class GithubIssue {
5
6
private int id;
7
8
private String title;
9
10
private GithubRepository repository;
11
12
public GithubIssue(GithubRepository repository) {
13
this.repository = repository;
14
}
15
16
public int getId() {
17
return id;
18
}
19
20
public void setId(int id) {
21
this.id = id;
22
}
23
24
public String getTitle() {
25
return title;
26
}
27
28
public void setTitle(String title) {
29
this.title = title;
30
}
31
32
public GithubRepository getRepository() {
33
return repository;
34
}
35
36
public String getUrl() {
37
return String.format(
38
"https://github.com/%s/%s/issues/%d",
39
repository.getUser(),
40
repository.getName(),
41
this.getId()
42
);
43
}
44
}
1
// App.java
2
package me.whiteship.designpatterns._01_creational_patterns._05_prototype._01_before;
3
4
public class App {
5
6
public static void main(String[] args) {
7
GithubRepository repository = new GithubRepository();
8
repository.setUser("whiteship");
9
repository.setName("live-study");
10
11
GithubIssue githubIssue = new GithubIssue(repository);
12
githubIssue.setId(1);
13
githubIssue.setTitle("1주차 과제: JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가.");
14
15
String url = githubIssue.getUrl();
16
System.out.println(url);
17
}
18
19
}

5.2 예시 - 변경

1
// GithubIssue.java
2
package me.whiteship.designpatterns._01_creational_patterns._05_prototype._02_after;
3
4
import java.util.Objects;
5
6
public class GithubIssue implements Cloneable {
7
8
private int id;
9
10
private String title;
11
12
private GithubRepository repository;
13
14
public GithubIssue(GithubRepository repository) {
15
this.repository = repository;
16
}
17
18
public int getId() {
19
return id;
20
}
21
22
public void setId(int id) {
23
this.id = id;
24
}
25
26
public String getTitle() {
27
return title;
28
}
29
30
public void setTitle(String title) {
31
this.title = title;
32
}
33
34
public GithubRepository getRepository() {
35
return repository;
36
}
37
38
public String getUrl() {
39
return String.format(
40
"https://github.com/%s/%s/issues/%d",
41
repository.getUser(),
42
repository.getName(),
43
this.getId()
44
);
45
}
46
47
@Override
48
protected Object clone() throws CloneNotSupportedException {
49
GithubRepository repository = new GithubRepository();
50
repository.setUser(this.repository.getUser());
51
repository.setName(this.repository.getName());
52
53
GithubIssue githubIssue = new GithubIssue(repository);
54
githubIssue.setId(this.id);
55
githubIssue.setTitle(this.title);
56
57
return githubIssue;
58
}
59
60
@Override
61
public boolean equals(Object o) {
62
if (this == o) return true;
63
if (o == null || getClass() != o.getClass()) return false;
64
GithubIssue that = (GithubIssue) o;
65
return (
66
id == that.id &&
67
Objects.equals(title, that.title) &&
68
Objects.equals(repository, that.repository)
69
);
70
}
71
72
@Override
73
public int hashCode() {
74
return Objects.hash(id, title, repository);
75
}
76
}
1
// App.java
2
package me.whiteship.designpatterns._01_creational_patterns._05_prototype._02_after;
3
4
public class App {
5
6
public static void main(String[] args) throws CloneNotSupportedException {
7
GithubRepository repository = new GithubRepository();
8
repository.setUser("whiteship");
9
repository.setName("live-study");
10
11
GithubIssue githubIssue = new GithubIssue(repository);
12
githubIssue.setId(1);
13
githubIssue.setTitle("1주차 과제: JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가.");
14
15
String url = githubIssue.getUrl();
16
System.out.println(url);
17
18
GithubIssue clone = (GithubIssue) githubIssue.clone();
19
System.out.println(clone.getUrl());
20
21
repository.setUser("Keesun");
22
23
System.out.println(clone != githubIssue);
24
System.out.println(clone.equals(githubIssue));
25
System.out.println(clone.getClass() == githubIssue.getClass());
26
System.out.println(clone.getRepository() == githubIssue.getRepository());
27
28
System.out.println(clone.getUrl());
29
}
30
31
}

5.3 장단점

  • 장점
    • 복잡한 객체를 만드는 과정을 숨길 수 있다.
    • 기존 객체를 복제하는 과정이 새 인스턴스를 만드는 것보다 비용(시간 또는 메모리)적인 면에서 효율적일 수도 있다.
    • 추상적인 타입을 리턴할 수 있다.
  • 단점
    • 복제한 객체를 만드는 과정 자체가 복잡할 수 있다. (특히, 순환 참조가 있는 경우)

5.4 실무에서 어떻게 쓰이나?

  • 자바 Object 클래스의 clone 메소드와 Cloneable 인터페이스
  • shallow copy와 deep copy
  • ModelMapper
1
// ModelMapperExample.java
2
package me.whiteship.designpatterns._01_creational_patterns._05_prototype._03_java;
3
4
import me.whiteship.designpatterns._01_creational_patterns._05_prototype._02_after.GithubIssue;
5
import me.whiteship.designpatterns._01_creational_patterns._05_prototype._02_after.GithubRepository;
6
import org.modelmapper.ModelMapper;
7
8
public class ModelMapperExample {
9
10
public static void main(String[] args) {
11
GithubRepository repository = new GithubRepository();
12
repository.setUser("whiteship");
13
repository.setName("live-study");
14
15
GithubIssue githubIssue = new GithubIssue(repository);
16
githubIssue.setId(1);
17
githubIssue.setTitle("1주차 과제: JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가.");
18
19
ModelMapper modelMapper = new ModelMapper();
20
GithubIssueData githubIssueData = modelMapper.map(githubIssue, GithubIssueData.class);
21
System.out.println(githubIssueData);
22
}
23
}
1
// JavaCollectionExample.java
2
package me.whiteship.designpatterns._01_creational_patterns._05_prototype._03_java;
3
4
import java.util.ArrayList;
5
import java.util.List;
6
7
public class JavaCollectionExample {
8
9
public static void main(String[] args) {
10
Student keesun = new Student("keesun");
11
Student whiteship = new Student("whiteship");
12
List<Student> students = new ArrayList<>();
13
students.add(keesun);
14
students.add(whiteship);
15
16
List<Student> clone = new ArrayList<>(students);
17
System.out.println(clone);
18
}
19
}