LostCatBox

(ETC) 디자인 패턴에 대해서 CH1 (생성 디자인 패턴에 대해)

Word count: 4.9kReading time: 30 min
2023/03/12 Share

왜?

개발을 할때, 항상 디자인 패턴에 대해 생각해보고 어떤 전략을 활용해야 더 좋은 코드를 짤수있을지에 대한 생각을 했다.

각 패턴들의 나온 이유, 장점들을 알아보고 예시로 적용까지 해보자

참조

전체 참조

팩토리 메서드

스프링에서 빌더 패턴 참조

사전 지식

디자인 패턴은 소프트웨어 디자인 과정에서 자주 발생하는 문제들에 대한 전형적인 해결책이다.

이는 코드에서 반복되는 디자인 문제들을 해결하기 위해 맞춤화할 수 있는 미리 만들어진 청사진

객체 지향 설계의 일반적인 문제들에 대한 일반적인 해결책

패턴의 의도(문제점->해결)파악, 구조 파악, 코드 예시를 통해 습득해보자

  • 생성 패턴들은 기존 코드의 재활용과 유연성을 증가시키는 객체 생성 메커니즘들을 제공합니다.
  • 구조 패턴은 구조를 유연하고 효율적으로 유지하면서 객체와 클래스를 더 큰 구조로 조합하는 방법을 설명합니다.
  • 행동 패턴은 객체 간의 효과적인 의사소통과 책임 할당을 처리합니다.

생성 패턴

팩토리 메서드 패턴(Factory Method)

사용 상황 요약

팩토리 메서드는 부모 클래스에서 객체들을 생성할 수 있는 인터페이스를 제공하지만, 자식 클래스들이 생성될 객체들의 유형을 변경할 수 있도록 하는 생성 패턴입니다.

즉, 객체를 생성할 때 어떤 클래스의 인스턴스를 만들 지 서브 클래스에서 결정하게 합니다.

부모 추상 클래스는 인터페이스에만 의존하고 실제로 어떤 구현 클래스를 호출할 지는 서브 클래스에서 구현합니다.

이렇게 하면 새로운 구현 클래스가 추가되어도 기존 Factory 코드의 수정 없이 새로운 Factory 를 추가하면 됩니다.

팩토리 메서드란?

스크린샷 2023-03-14 오후 10.25.59

  1. 제품은 인터페이스를 선언합니다. 인터페이스는 생성자와 자식 클래스들이 생성할 수 있는 모든 객체에 공통입니다.

  2. 구상 제품들은 제품 인터페이스의 다양한 구현들입니다.

  3. 크리에이터(Creator) 클래스는 새로운 제품 객체들을 반환하는 팩토리 메서드를 선언합니다. 중요한 점은 이 팩토리 메서드의 반환 유형이 제품 인터페이스와 일치해야 한다는 것입니다.

    크리에이터 클래스 안에 팩토리 메서드abstract(추상)로 선언하여 모든 자식 클래스들이 각각 이 메서드의 자체 버전들을 구현하도록 강제할 수 있으며, 또 대안적으로 기초 팩토리 메서드가 디폴트(기본값) 제품 유형을 반환하도록 만들 수도 있습니다.

    크리에이터라는 이름에도 불구하고 크리에이터의 주책임은 제품을 생성하는 것이 아닙니다. 일반적으로 크리에이터 클래스에는 이미 제품과 관련된 핵심 비즈니스 로직이 있으며, 팩토리 메서드는 이 로직을 구상 제품 클래스들로부터 디커플링(분리) 하는 데 도움을 줄 뿐입니다. 대규모 소프트웨어 개발사에 비유해 보자면, 이 회사는 프로그래머들을 위한 교육 부서가 있을 수 있으나, 회사의 주 임무는 프로그래머를 교육하는 것이 아니라 코드를 작성하는 것입니다.

  4. 구상 크리에이터들은 기초 팩토리 메서드를 오버라이드(재정의)하여 다른 유형의 제품을 반환하게 하도록 합니다.

    참고로 팩토리 메서드는 항상 새로운 인스턴스들을 생성해야 할 필요가 없습니다. 팩토리 메서드는 기존 객체들을 캐시, 객체 풀 또는 다른 소스로부터 반환할 수 있습니다.

예시

스크린샷 2023-03-14 오후 10.39.39

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// 크리에이터 클래스는 제품 클래스의 객체를 반환해야 하는 팩토리 메서드를
// 선언합니다. 크리에이터의 자식 클래스들은 일반적으로 이 메서드의 구현을
// 제공합니다.
abstract public class Creator {
// 크리에이터는 팩토리 메서드의 일부 디폴트 구현을 제공할 수도 있습니다.
abstract public Person creatperson();

// 크리에이터의 주 업무는 제품을 생성하는 것이 아닙니다. 크리에이터는
// 일반적으로 팩토리 메서드에서 반환된 제품 객체에 의존하는 어떤 핵심
// 비즈니스 로직을 포함합니다. 자식 클래스들은 팩토리 메서드를 오버라이드 한
// 후 해당 메서드에서 다른 유형의 제품을 반환하여 해당 비즈니스 로직을
// 간접적으로 변경할 수 있습니다.
public void doSomething(){
// 팩토리 메서드를 호출하여 제품 객체를 생성
Person person = creatperson();
// 제품 사용
person.speak();
person.dance();
}


// 구상 크리에이터들은 결과 제품들의 유형을 변경하기 위해 팩토리 메서드를
// 오버라이드합니다.
public class DoctorCreator extends Creator{
@Override
public Person creatperson() {
return new Doctor();
}
}

public class PoliceCreator extends Creator{
@Override
public Person creatperson() {
return new Police();
}
}

// 제품 인터페이스는 모든 구상 제품들이 구현해야 하는 작업들을 선언합니다.
public interface Person {

void speak();
void dance();
}

// 구상 제품들은 제품 인터페이스의 다양한 구현을 제공합니다.
public class Police implements Person{

public void speak() {
System.out.println("경찰이 말합니다.");
}

public void dance(){
System.out.println("경찰이 춤을 춥니다");
}
}

public class Doctor implements Person{

public void speak() {
System.out.println("의사가 말합니다.");
}

public void dance() {
System.out.println("의사가 춤을 춥니다");
}
}

// Controller에서의 사용
@Controller
@RequestMapping("/basic")
public class BasicController {
@GetMapping("/designpattern1/{subject}")
public String designpattern1(HttpServletRequest request, @PathVariable String subject) {

System.out.println(request.getRequestURL());

Creator creator = null;

// 앱은 현재 설정 또는 환경 설정에 따라 크리에이터의 유형을 선택합니다.
if (subject.equals("police")){
creator = new PoliceCreator();
} else if (subject.equals("doctor")){
creator = new DoctorCreator();
} else{
new Exception("Error! Unknown Person Subject");
}

// 클라이언트 코드는 비록 구상 크리에이터의 기초 인터페이스를 통하는 것이긴
// 하지만 구상 크리에이터의 인스턴스와 함께 작동합니다. 클라이언트가
// 크리에이터의 기초 인터페이스를 통해 크리에이터와 계속 작업하는 한 모든
// 크리에이터의 자식 클래스를 클라이언트에 전달할 수 있습니다.
creator.doSomething();

return "basic/designpattern1";
}
}
1
2
3
4
5
6
7
8
>>>
http://localhost:8080/basic/designpattern1/police
경찰이 말합니다.
경찰이 춤을 춥니다
>>>
http://localhost:8080/basic/designpattern1/doctor
의사가 말합니다.
의사가 춤을 춥니다

요약

결국 팩토리 메서드가 포함된 Creator Class는 다음과같이 DoctorCreator, PoliceCreator로 구현체로 만든다.

Creator Class에서 abstract를 통해 미리 구현해야하는 조건을 선언해놓는다.

creator = new DoctorCreator(), dialog = new PoliceCreator()를 통하면, 결국 Person의 구현체인 new Doctor()new Police()이 각각 반환된다. 이를 creator.doSomething()로 편하게 어떤 객체를 만들었든 바로 각 기능이 동작하도록 사용할수있다.

적용

팩토리 메서드는 당신의 코드가 함께 작동해야 하는 객체들의 정확한 유형들과 의존관계들을 미리 모르는 경우 사용하세요.

팩토리 메서드는 제품 생성 코드를 제품을 실제로 사용하는 코드와 분리합니다. 그러면 제품 생성자 코드를 나머지 코드와는 독립적으로 확장하기 쉬워집니다.

예를 들어, 앱에 새로운 제품을 추가하려면 당신은 새로운 크리에이터 자식 클래스를 생성한 후 해당 클래스 내부의 팩토리 메서드를 오버라이딩(재정의)하기만 하면 됩니다.

팩토리 메서드는 당신의 라이브러리 또는 프레임워크의 사용자들에게 내부 컴포넌트들을 확장하는 방법을 제공하고 싶을 때 사용하세요.

상속(inheritance)은 아마도 라이브러리나 프레임워크의 디폴트 행동을 확장하는 가장 쉬운 방법일 것입니다. 그러나 프레임워크는 표준 컴포넌트 대신 당신의 자식 클래스를 사용해야 한다는 것을 어떻게 인식할까요?

해결책은 일단 프레임워크 전체에서 컴포넌트들을 생성하는 코드를 단일 팩토리 메서드로 줄인 후, 누구나 이 팩토리 메서드를 오버라이드 할 수 있도록 하는 것입니다.

그러면 해결책의 예시를 한번 살펴봅시다. 당신이 오픈 소스 UI 프레임워크를 사용하여 앱을 작성하고 있고, 당신이 개발 중인 앱에는 둥근 버튼들이 필요한데 프레임워크는 사각형 버튼만 제공한다고 가정합시다. 또 표준 Button(버튼) 클래스는 Round­Button(둥근 버튼) 자식 클래스로 확장했지만, 이제 메인 UIFramework(사용자 인터페이스 프레임워크) 클래스에 디폴트 클래스 대신 새로운 Round­Button(둥근 버튼) 자식 클래스를 사용하라고 지시해야 한다고 가정해 봅시다.

이를 달성하려면 기초 프레임워크 클래스에서 자식 클래스 UIWith­Round­Buttons를 만들어서 기초 프레임워크 클래스의 create­Button 메서드를 오버라이딩(재정의)합니다. 이 메서드는 기초 클래스에 Button 객체들을 반환하지만, 당신은 당신의 자식 클래스가 Round­Button 객체들을 반환하도록 만듭니다. 이제 UIFramework 클래스 대신 UIWith­Round­Buttons 클래스를 사용하면 끝입니다!

팩토리 메서드는 기존 객체들을 매번 재구축하는 대신 이들을 재사용하여 시스템 리소스를 절약하고 싶을 때 사용하세요.

이러한 요구 사항은 데이터베이스 연결, 파일 시스템 및 네트워크처럼 시스템 자원을 많이 사용하는 대규모 객체들을 처리할 때 자주 발생합니다.

기존 객체를 재사용하려면 무엇을 해야 하는지 한번 생각해 봅시다.

  1. 먼저 생성된 모든 객체를 추적하기 위해 일부 스토리지를 생성해야 합니다.
  2. 누군가가 객체를 요청하면 프로그램은 해당 풀 내에서 유휴(free) 객체를 찾아야 합니다. 그 후…
  3. … 이 객체를 클라이언트 코드에 반환해야 합니다.
  4. 유휴(free) 객체가 없으면, 프로그램은 새로운 객체를 생성해야 합니다. (그리고 풀에 이 객체를 추가해야 합니다).

이것은 정말로 많은 양의 코드입니다! 그리고 프로그램을 중복 코드로 오염시키지 않도록 이 많은 양의 코드를 모두 한곳에 넣어야 합니다.

아마도 이 코드를 배치할 수 있는 가장 확실하고 편리한 위치는 우리가 재사용하려는 객체들의 클래스의 생성자일 것입니다. 그러나 생성자는 특성상 항상 새로운 객체들을 반환해야 하며, 기존 인스턴트를 반환할 수는 없습니다.

따라서 새 객체들을 생성하고 기존 객체를 재사용할 수 있는 일반적인 메서드가 필요합니다. 이 설명, 꼭 팩토리 메서드처럼 들리지 않나요?

Police로 팩토리 패턴 + 싱글톤 패턴으로 구현

  • PoliceCreator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class PoliceCreator extends Creator{

private static Police police;

public static Police getPolice() {
if(police==null){
police= new Police();
}

return police;
}

@Override
public Person creatperson() {
return getPolice();
}

}
  • 출력
1
2
3
4
5
6
7
System.out.println(creator.creatperson());
System.out.println(creator.creatperson());

>>>
hello.thymeleaf.basic.FactoryMethod.Police@3de7fa0f
hello.thymeleaf.basic.FactoryMethod.Police@3de7fa0f
동일한 객체

장단점

장점

  • 크리에이터와 구상 제품들이 단단하게 결합되지 않도록 할 수 있습니다.
  • 단일 책임 원칙. 제품 생성 코드를 프로그램의 한 위치로 이동하여 코드를 더 쉽게 유지관리할 수 있습니다.
  • 개방/폐쇄 원칙. 당신은 기존 클라이언트 코드를 훼손하지 않고 새로운 유형의 제품들을 프로그램에 도입할 수 있습니다.

단점

  • 패턴을 구현하기 위해 많은 새로운 자식 클래스들을 도입해야 하므로 코드가 더 복잡해질 수 있습니다. 가장 좋은 방법은 크리에이터 클래스들의 기존 계층구조에 패턴을 도입하는 것입니다.

다른 패턴과의 관계

  • 많은 디자인은 복잡성이 낮고 자식 클래스들을 통해 더 많은 커스터마이징이 가능한 팩토리 메서드로 시작해 더 유연하면서도 더 복잡한 추상 팩토리, 프로토타입 또는 빌더 패턴으로 발전해 나갑니다.
  • 추상 팩토리 클래스들은 팩토리 메서드들의 집합을 기반으로 하는 경우가 많습니다. 그러나 당신은 또한 프로토타입을 사용하여 추상 팩토리의 구상 클래스들의 생성 메서드들을 구현할 수도 있습니다.
  • 팩토리 메서드반복자와 함께 사용하여 컬렉션 자식 클래스들이 해당 컬렉션들과 호환되는 다양한 유형의 반복자들을 반환하도록 할 수 있습니다.
  • 프로토타입은 상속을 기반으로 하지 않으므로 상속과 관련된 단점들이 없습니다. 반면에 프로토타입은 복제된 객체의 복잡한 초기화가 필요합니다. 팩토리 메서드는 상속을 기반으로 하지만 초기화 단계가 필요하지 않습니다.
  • 팩토리 메서드템플릿 메서드의 특수화라고 생각할 수 있습니다. 동시에 대규모 템플릿 메서드의 한 단계의 역할을 팩토리 메서드가 할 수 있습니다.

추상 팩토리 패턴(Abstract Factory)

사용 상황 요약

추상 팩토리는 관련 객체들의 구상 클래스들을 지정하지 않고도 관련 객체들의 모음을 생성할 수 있도록 하는 생성패턴입니다.

즉, 추상 팩토리를 통해, 클라이언트는 어떤 구현 팩토리인지 신경안쓰고 사용가능

추상 팩토리 패턴이란?

추상 공장 패턴의 첫 번째 방안은 각 제품 패밀리(제품군)에 해당하는 개별적인 인터페이스를 명시적으로 선언하는 것입니다. (예: 의자, 소파 또는 커피 테이블). 그다음, 제품의 모든 변형이 위 인터페이스를 따르도록 합니다. 예를 들어, 모든 의자의 변형들은 Chair(의자) 인터페이스를 구현한다; 모든 커피 테이블 변형들은 Coffee­Table(커피 테이블) 인터페이스를 구현한다, 등의 규칙을 명시합니다.

스크린샷 2023-03-15 오후 10.55.16

추상 공장 패턴의 다음 단계는 추상 팩토리 패턴을 선언하는 것입니다. 추상 공장 패턴은 제품 패밀리 내의 모든 개별 제품들의 생성 메서드들이 목록화되어있는 인터페이스입니다. (예: create­Chair(의자 생성), create­Sofa(소파 생성), create­Coffee­Table(커피 테이블 생성) 등).

스크린샷 2023-03-15 오후 10.55.33

클라이언트가 팩토리에 의자를 주문했다고 가정해 봅시다. 클라이언트는 팩토리의 클래스들을 알 필요가 없으며, 팩토리가 어떤 변형의 의자를 생성할지 전혀 신경을 쓰지 않습니다. 클라이언트는 추상 Chair(의자) 인터페이스를 사용하여, 현대식 의자이든 빅토리아식 의자이든 종류에 상관없이 모든 의자를 항상 동일한 방식으로 주문하며, 그가 의자에 대해 아는 유일한 사실은 제품이 sit­On(앉을 수 있다) 메서드를 구현한다는 것뿐입니다. 그러나, 생성된 의자의 변형은 항상 같은 팩토리 객체에서 생성된 소파 또는 커피 테이블의 변형과 같을 것입니다.

여기에서 명확히 짚고 넘어가야 할 점이 있습니다. 클라이언트가 추상 인터페이스에만 노출된다면 실제 팩토리 객체를 생성하는 것은 무엇일까요? 일반적으로 프로그램은 초기화 단계에서 구상 팩토리 객체를 생성합니다. 그 직전에 프로그램은 환경 또는 구성 설정에 따라 팩토리 유형을 선택해야 합니다.

구조

스크린샷 2023-03-15 오후 10.58.43

  1. 추상 제품들은 제품 패밀리를 구성하는 개별 연관 제품들의 집합에 대한 인터페이스들을 선언합니다.
  2. 구상 제품들은 변형들로 그룹화된 추상 제품들의 다양한 구현들입니다. 각 추상 제품(의자/소파)은 주어진 모든 변형(빅토리안/현대식)에 구현되어야 합니다.
  3. 추상 팩토리 인터페이스는 각각의 추상 제품들을 생성하기 위한 여러 메서드들의 집합을 선언합니다.
  4. 구상 팩토리들은 추상 팩토리의 생성 메서드들을 구현합니다. 각 구상 팩토리는 제품들의 특정 변형들에 해당하며 해당 특정 변형들만 생성합니다.
  5. 구상 팩토리들은 구상 제품들을 인스턴스화하나, 그 제품들의 생성 메서드들의 시그니처들은 그에 해당하는 추상 제품들을 반환해야 합니다. 그래야 팩토리를 사용하는 클라이언트 코드가 팩토리에서 받은 제품의 특정 변형과 결합되지 않습니다. 클라이언트는 추상 인터페이스를 통해 팩토리/제품 변형의 객체들과 소통하는 한 그 어떤 구상 팩토리/제품 변형과 작업할 수 있습니다.

예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// 추상 팩토리 인터페이스는 다른 추상 제품들을 반환하는 메서드들의 집합을
// 선언합니다. 이러한 제품들을 패밀리라고 하며 이들은 상위 수준의 주제 또는
// 개념으로 연결됩니다. 한 가족의 제품들은 일반적으로 서로 협력할 수 있습니다.
// 제품들의 패밀리(제품군)에는 여러 변형이 있을 수 있지만 한 변형의 제품들은 다른
// 변형의 제품들과 호환되지 않습니다.
public interface KindFactory {
Chair createChair();
Table createTable();
}


// 구상 팩토리들은 단일 변형에 속하는 제품들의 패밀리(제품군)을 생성합니다. 이
// 팩토리는 결과 제품들의 호환을 보장합니다. 구상 팩토리 메서드의 시그니처들은 추상
// 제품을 반환하는 반면, 메서드 내부에서는 구상 제품이 인스턴스화됩니다.
public class AntiqueFactory implements KindFactory {
@Override
public Chair createChair() {
return new AntiqueChair();
}

@Override
public Table createTable() {
return new AntiqueTable();
}
}

// 각 구상 팩토리에는 해당하는 제품 변형이 있습니다.
public class ModernFactory implements KindFactory{
@Override
public Chair createChair() {
return new ModernChair();
}

@Override
public Table createTable() {
return new ModernTable();
}
}

// 제품 패밀리의 각 개별 제품에는 기초 인터페이스가 있어야 합니다. 이 제품의 모든
// 변형은 이 인터페이스를 구현해야 합니다.
public interface Chair {
void sitDown();
}

// 구상 제품들은 해당하는 구상 팩토리에서 생성됩니다.
public class AntiqueChair implements Chair{
@Override
public void sitDown() {
System.out.println("엔틱 의자에 앉았습니다.");
}
}

public class ModernChair implements Chair{
@Override
public void sitDown() {
System.out.println("모던의자에 앉았습니다.");
}
}

// 다음은 다른 제품의 기초 인터페이스입니다. 모든 제품은 상호 작용할 수 있지만 같은
// 구상 변형의 제품들 사이에서만 적절한 상호 작용이 가능합니다.
public interface Table {
void putOn();
}
public class AntiqueTable implements Table{
@Override
public void putOn() {
System.out.println("엔틱 테이블에 놓습니다.");
}
}

public class ModernTable implements Table{
@Override
public void putOn() {
System.out.println("모던 테이블에 놓습니다.");
}
}

// 클라이언트 코드는 KindFactory, Chair 및 table와 같은 추상 유형을
// 통해서만 팩토리들 및 제품들과 작동하며, 이는 클라이언트 코드를 손상하지 않고
// 클라이언트 코드에 모든 팩토리 또는 하위 클래스를 전달할 수 있게 해줍니다.
// 현재 추상 팩토리는 param에 따라 런타임에서 결정됩니다.
@GetMapping("/designpattern2/{param}")
@ResponseBody
public HttpStatus designpattern2(HttpServletRequest request, @PathVariable String param) {

System.out.println(request.getRequestURL());

KindFactory kindFactory = null;

if (param.equals("modern")){
kindFactory = new ModernFactory();
} else if (param.equals("antique")){
kindFactory = new AntiqueFactory();
} else{
new Exception("Error! Unknown KindFactory");

}

// 추상 팩토리는 해당 원하는 환경을 가지고있고, 클라이언트는 사용만하면된다.
if (kindFactory != null){
Chair chair = kindFactory.createChair();
Table table = kindFactory.createTable();

chair.sitDown();
table.putOn();
}

return HttpStatus.OK;
}
1
2
3
4
5
6
7
8
>>>
http://localhost:8080/basic/designpattern2/modern
모던의자에 앉았습니다.
모던 테이블에 놓습니다.
>>>
http://localhost:8080/basic/designpattern2/antique
엔틱 의자에 앉았습니다.
엔틱 테이블에 놓습니다.

적용

추상 팩토리는 당신의 코드가 관련된 제품군의 다양한 패밀리들과 작동해야 하지만 해당 제품들의 구상 클래스들에 의존하고 싶지 않을 때 사용하세요. 왜냐하면 이러한 클래스들은 당신에게 미리 알려지지 않았을 수 있으며, 그 때문에 향후 확장성(extensibility)을 허용하기를 원할 수 있기 때문입니다.

  • 추상 팩토리는 제품 패밀리의 각 클래스에서부터 객체들을 생성할 수 있는 인터페이스를 제공합니다. 위 인터페이스를 통해 코드가 객체들을 생성하는 한 당신은 당신의 앱에서 이미 생성된 제품들과 일치하지 않는 잘못된 제품 변형을 생성하지 않을지 걱정할 필요가 없습니다.

코드에 클래스가 있고, 이 클래스의 팩토리 메서드들의 집합의 기본 책임이 뚜렷하지 않을 때 추상 팩토리 구현을 고려하세요.

  • 잘 설계된 프로그램에서는 각 클래스는 하나의 책임만 가집니다. 클래스가 여러 제품 유형을 상대할 경우, 클래스의 팩토리 메서드들을 독립실행형 팩토리 클래스 또는 완전한 추상 팩토리 구현으로 추출할 가치가 있을 수 있습니다.

장단점

장점

  • 팩토리에서 생성되는 제품들의 상호 호환을 보장할 수 있습니다.
  • 구상 제품들과 클라이언트 코드 사이의 단단한 결합을 피할 수 있습니다.
  • 단일 책임 원칙. 제품 생성 코드를 한 곳으로 추출하여 코드를 더 쉽게 유지보수할 수 있습니다.
  • 개방/폐쇄 원칙. 기존 클라이언트 코드를 훼손하지 않고 제품의 새로운 변형들을 생성할 수 있습니다.

단점

  • 패턴과 함께 새로운 인터페이스들과 클래스들이 많이 도입되기 때문에 코드가 필요 이상으로 복잡해질 수 있습니다.

빌더패턴

사용 상황 요약

빌더는 복잡한 객체들을 단계별로 생성할 수 있도록 하는 생성 디자인 패턴입니다. 이 패턴을 사용하면 같은 제작 코드를 사용하여 객체의 다양한 유형들과 표현을 제작할 수 있습니다.

빌더 패턴 이란?

빌더 패턴은 자신의 클래스에서 객체 생성 코드를 추출하여 builders(건축업자들)라는 별도의 객체들로 이동하도록 제안합니다.

스크린샷 2023-03-15 오후 11.12.42

스크린샷 2023-03-15 오후 11.15.06

  1. 빌더 인터페이스는 모든 유형의 빌더들에 공통적인 제품 생성 단계들을 선언합니다.
  2. 구상 빌더들은 생성 단계들의 다양한 구현을 제공합니다. 또 구상 빌더들은 공통 인터페이스를 따르지 않는 제품들도 생산할 수 있습니다.
  3. 제품들은 그 결과로 나온 객체들입니다. 다른 빌더들에 의해 생성된 제품들은 같은 클래스 계층구조 또는 인터페이스에 속할 필요가 없습니다.
  4. 디렉터 클래스는 생성 단계들을 호출하는 순서를 정의하므로 제품들의 특정 설정을 만들고 재사용할 수 있습니다.(반드시 필요한것은 아니지만, 다양한 생성 루틴을 배치할수있어 중복코드 줄일수있음)
  5. 클라이언트는 빌더 객체들 중 하나를 디렉터와 연결해야 합니다. 일반적으로 위 연결은 디렉터 생성자의 매개변수들을 통해 한 번만 수행되며, 그 후 디렉터는 모든 추가 생성에 해당 빌더 객체들을 사용합니다. 그러나 클라이언트가 빌더 객체를 디렉터의 프로덕션 메서드에 전달할 때를 위한 대안적 접근 방식이 있습니다. 이 경우 디렉터와 함께 무언가를 만들 때마다 다른 빌더를 사용할 수 있습니다.

예시

자동차

특별한 자동차를 조립하는 경우, 빌더를 직접 작동한다.

디랙터 클래스는 빌더를 사용하여 가장 인기 있는 자동차 모델을 조립하는 방법을 알고있고, 바로 조립완료할수있다.

스크린샷 2023-03-19 오후 6.07.35

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// 빌더 패턴을 사용하는 것은 제품에 매우 복잡하고 광범위한 설정이 필요한 경우에만
// 의미가 있습니다. 다음 두 제품은 공통 인터페이스는 없지만 관련되어 있습니다.
@Getter
@Setter
public class Car {

private int seat;

private String gps;

private String engine;
}
}
// 자동차에는 GPS, 트립 컴퓨터 및 몇 개의 좌석이 있을 수 있습니다. 다른
// 모델의 자동차(스포츠카, SUV, 오픈카)에는 다른 기능들이 설치되거나
// 활성화되어 있을 수 있습니다.

public class Manual {
// 각 자동차에는 자동차의 설정에 해당하는, 모든 기능을 설명하는 사용 설명서가
// 있어야 합니다. 예시는 간단하게만함

public String hello(){
return "car메뉴얼 응답";
}
}



// 빌더 인터페이스는 제품 객체들의 다른 부분들을 만드는 메서드들을 지정합니다.
public interface Builder {
void reset();

void setSeats(int seat);

void setEngine(String engine);

void setGPS(String gps);
}


// 구상 빌더 클래스들은 빌더 인터페이스를 따르고 빌드 단계들의 특정 구현들을
// 제공합니다. 당신의 프로그램에는 각기 다르게 구현된 여러 가지 빌더 변형들이 있을
// 수 있습니다.
public class CarBuilder implements Builder{
private Car car;

private Car product;

// 새로운 빌더 인스턴스에는 인스턴스가 추가적인 조립과정에서 사용하는 빈 제품
// 객체가 포함되어야 합니다.
public CarBuilder() {
this.reset();
}

// reset 메서드는 구축 중인 객체를 지웁니다.
@Override
public void reset(){
this.car = new Car();
}

// 모든 생성 단계들은 같은 제품의 인스턴스와 작동합니다.
@Override
public void setSeats(int seat) {
car.setSeat(seat);
}

@Override
public void setEngine(String engine) {
car.setEngine(engine);
}

@Override
public void setGPS(String gps) {
car.setGps(gps);
}

// 구상 빌더들은 결과들을 가져오기 위한 자체 메서드들을 제공해야 합니다.
// 왜냐하면 다양한 유형의 빌더들은 모두 같은 인터페이스를 따르지 않는 완전히
// 다른 제품들을 생성할 수 있기 때문입니다. 따라서 이러한 메서드는 빌더
// 인터페이스에서 선언할 수 없습니다. 적어도 이는 정적 타입 언어에서는
// 불가능합니다.
//
// 최종 결과를 클라이언트에 반환한 후 일반적으로 빌더 인스턴스는 다른 제품
// 생산을 시작할 준비가 되어 있을 것이라고 예상됩니다. 이것이
// `getProduct` 메서드의 본문 끝에서 reset 메서드를 호출하는 것이
// 일반적인 관행인 이유입니다. 하지만 반드시 이렇게 해야 하는 것은
// 아니라서, 빌더가 클라이언트 코드로부터 명시적으로 reset 호출을 받을
// 때까지 이전 결과를 삭제하지 않고 기다리게 만들 수 있습니다.
public Car getProduct(){
product = this.car;
this.reset();

return product;
}
}


// 다른 생성 패턴과 달리 빌더를 사용하면 공통 인터페이스를 따르지 않는 제품들을
// 생성할 수 있습니다.
public class CarManualBuilder implements Builder{

private Manual manual;

private Manual product;

public CarManualBuilder() {
this.reset();
}

@Override
public void reset() {
this.manual = new Manual();
}

@Override
public void setSeats(int seat) {

}

@Override
public void setEngine(String engine) {

}

@Override
public void setGPS(String gps) {

}

public Manual getProduct(){
product = this.manual;
this.reset();

return product;
}
}


// 디렉터는 특정 순서로 생성 단계들을 실행하는 책임만 있습니다. 이것은 특정 순서나
// 설정에 따라 제품들을 생성할 때 유용합니다. 엄밀히 말하면, 클라이언트가 빌더들을
// 직접 제어할 수 있으므로 디렉터 클래스는 선택 사항입니다.
public class Director {
// 디렉터는 클라이언트 코드가 전달하는 모든 빌더 인스턴스와 함께 작동합니다.
// 그러면 클라이언트 코드는 새로 조립된 제품의 최종 유형을 변경할 수
// 있습니다. 디렉터는 같은 생성 단계들을 사용하여 여러 제품 변형들을 생성할
// 수 있습니다.
public void constructSportCar(Builder builder){
builder.reset();
builder.setSeats(2);
builder.setEngine("superEngine");
builder.setGPS("superGPS");
}
public voild constructOtherCar(Builder builder){
...
}
}

// 클라이언트 코드는 빌더 객체를 만든 후 이를 디렉터에게 전달한 다음 생성
// 프로세스를 시작합니다. 최종 결과는 빌더 객체에서 가져옵니다.
class Application is

@GetMapping("/designpattern3/{param}")
@ResponseBody
public HttpStatus designpattern3(HttpServletRequest request, @PathVariable String param) {

System.out.println(request.getRequestURL());

Director director = new Director();

if (param.equals("sportcar")) {
CarBuilder builder = new CarBuilder();
director.constructSportCar(builder);
Car car = builder.getProduct();

System.out.println(car);

CarManualBuilder builder2 = new CarManualBuilder();
// 디렉터는 구상 빌더들 및 제품들에 의존하지 않고 인식하지 못하기 때문에
// 최종 제품은 종종 빌더 객체에서 가져옵니다.
director.constructSportCar(builder);
Manual manual = builder2.getProduct();

System.out.println(manual.hello());

CarBuilder builder3 = new CarBuilder();
builder3.setGPS("gps1");
builder3.setEngine("normalengine");
builder3.setSeats(2);
Car specificCar = builder3.getProduct();

System.out.println(specificCar);
}

return HttpStatus.OK;
}

응답결과

1
2
3
4
5
>>>
http://localhost:8080/basic/designpattern3/sportcar
Car{seat=2, gps='superGPS', engine='superEngine'}
car메뉴얼 응답
Car{seat=2, gps='gps1', engine='normalengine'}

@Builder 패턴 원리? (클래스 레벨 에너테이션 부분)

1
2
3
4
5
6
7
@Builder
public class BuildMe {

private String username;
private int age;

}

다음과같이 변환한다.(래핑?)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class BuildMe {
private String username;
private int age;

BuildMe(String username, int age) {
this.username = username;
this.age = age;
}

public static BuildMe.BuildMeBuilder builder() {
return new BuildMe.BuildMeBuilder();
}

public static class BuildMeBuilder {
private String username;
private int age;

BuildMeBuilder() {
}

public BuildMe.BuildMeBuilder username(String username) {
this.username = username;
return this;
}

public BuildMe.BuildMeBuilder age(int age) {
this.age = age;
return this;
}

public BuildMe build() {
return new BuildMe(this.username, this.age);
}

public String toString() {
return "BuildMe.BuildMeBuilder(username=" + this.username + ", age=" + this.age + ")";
}
}
}

적용

‘점층적 생성자’를 제거하기 위하여 빌더 패턴을 사용하세요.

10개의 선택적 매개변수가 있는 생성자가 있다고 가정합니다. 이렇게 복잡한 생성자를 호출하는 것은 매우 불편합니다. 따라서 이 생성자를 오버로드하고 더 적은 수의 매개변수들을 사용하는 더 짧은 생성자 버전들을 여러 개 만듭니다. 이러한 생성자들은 여전히 주 생성자를 참조하며, 생략된 매개변수들에 일부 기본값들을 전달합니다.

1
2
3
4
5
class Pizza {
Pizza(int size) { ... }
Pizza(int size, boolean cheese) { ... }
Pizza(int size, boolean cheese, boolean pepperoni) { ... }
// …

이렇게 복잡한 생성자를 만드는 것은 C#나 자바와 같이 메서드 오버로딩을 지원하는 언어들에서만 가능합니다.

빌더 패턴을 사용하면 실제로 필요한 단계들만 사용하여 단계별로 객체들을 생성할 수 있으며, 패턴을 구현한 후에는 더 이상 수십 개의 매개변수를 생성자에 집어넣을 필요가 없습니다.

빌더 패턴은 당신의 코드가 일부 제품의 다른 표현들(예: 석조 및 목조 주택들)을 생성할 수 있도록 하고 싶을 때 사용하세요.

빌더 패턴은 제품의 다양한 표현의 생성 과정이 세부 사항만 다른 유사한 단계를 포함할 때 적용할 수 있습니다.

기초 빌더 인터페이스는 가능한 모든 생성 단계들을 정의하고 구상 빌더들은 이러한 단계들을 구현하여 제품의 여러 표현을 생성합니다. 또 한편 디렉터 클래스는 건설 순서를 안내합니다.

빌더를 사용하여 복합체 트리들 또는 기타 복잡한 객체들을 생성하세요.

빌더 패턴을 사용하면 제품들을 단계별로 생성할 수 있으며, 또 최종 제품을 손상하지 않고 일부 단계들의 실행을 연기할 수 있습니다. 그리고 재귀적으로 단계들을 호출할 수도 있는데, 이는 객체 트리를 구축해야 할 때 매우 유용합니다.

빌더는 생성 단계들을 수행하는 동안 미완성 제품을 노출하지 않으며, 이는 클라이언트 코드가 불완전한 결과를 가져오는 것을 방지합니다.

장단점

장점

  • 객체들을 단계별로 생성하거나 생성 단계들을 연기하거나 재귀적으로 단계들을 실행할 수 있습니다.
  • 제품들의 다양한 표현을 만들 때 같은 생성 코드를 재사용할 수 있습니다.
  • 단일 책임 원칙. 제품의 비즈니스 로직에서 복잡한 생성 코드를 고립시킬 수 있습니다.

단점

  • 패턴이 여러 개의 새 클래스들을 생성해야 하므로 코드의 전반적인 복잡성이 증가합니다.

프로토타입

사용 상황 요약

프로토타입은 코드를 그들의 클래스들에 의존시키지 않고 기존 객체들을 복사할 수 있도록 하는 생성 디자인 패턴입니다.

프로토타입 패턴이란?

프로토타입 패턴은 실제로 복제되는 객체들에 복제 프로세스를 위임합니다. 패턴은 복제를 지원하는 모든 객체에 대한 공통 인터페이스를 선언합니다. 이 인터페이스를 사용하면 코드를 객체의 클래스에 결합하지 않고도 해당 객체를 복제할 수 있습니다. 일반적으로 이러한 인터페이스에는 단일 clone 메서드만 포함됩니다.

스크린샷 2023-03-19 오후 7.27.45

  1. 프로토타입 인터페이스는 복제 메서드들을 선언하며, 이 메서드들의 대부분은 단일 clone 메서드입니다.
  2. 구상 프로토타입 클래스는 복제 메서드를 구현합니다. 원본 객체의 데이터를 복제본에 복사하는 것 외에도 이 메서드는 복제 프로세스와 관련된 일부 예외적인 경우들도 처리할 수도 있습니다. (예: 연결된 객체 복제, 재귀 종속성 풀기).
  3. 클라이언트는 프로토타입 인터페이스를 따르는 모든 객체의 복사본을 생성할 수 있습니다.

스크린샷 2023-03-19 오후 7.28.22

모든 shape(모양) 클래스는 같은 인터페이스를 따르며, 이 인터페이스는 복제 메서드를 제공합니다. 자식 클래스는 자신의 필드 값들을 생성된 객체에 복사하기 전에 부모의 복제 메서드를 호출할 수 있습니다.

예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124

// 기초 프로토타입.
abstract class Shape {
public int x;
public int y;
public String color;

// 일반 생성자.
public Shape() {
}

// 프로토타입 생성자. 기존 객체의 값들로 새로운 객체가 초기화됩니다.
public Shape(Shape shape){
this();
this.x = shape.x;
this.y = shape.y;
this.color = shape.color;
}

// 복제 작업은 Shape(모양) 자식 클래스 중 하나를 반환합니다.
abstract public Shape clone();

@Override
public boolean equals(Object object2) {
if (!(object2 instanceof Shape)) return false;
Shape shape2 = (Shape) object2;
return shape2.x == x && shape2.y == y && Objects.equals(shape2.color, color);
}
}

// 구상 프로토타입. 복제 메서드는 현재 클래스의 생성자를 호출해 현재 객체를
// 생성자의 인수로 전달함으로써 한 번에 새로운 객체를 생성합니다. 생성자에서
// 실제로 모든 것을 복사하게 되면 결과의 일관성이 유지됩니다. 생성자가 새로운
// 객체가 완전히 완성되기 전까지 결과를 반환하지 않아서 어떤 객체도 일부분만 완성된
// 복제본을 참조할 수 없습니다.
public class Rectangle extends Shape{

public int width;
public int height;

public Rectangle(){

}
public Rectangle(Rectangle rectangle){
// 부모 클래스에 정의된 비공개 필드들을 복사하려면 부모 생성자 호출이
// 필요합니다.
super(rectangle);
this.width = rectangle.width;
this.height = rectangle.height;
}

public Shape clone(){
return new Rectangle(this);
}

@Override
public boolean equals(Object object2) {
if (!(object2 instanceof Rectangle)) return false;
Shape shape2 = (Shape) object2;
return shape2.x == x && shape2.y == y && Objects.equals(shape2.color, color);
}
}

public class Circle extends Shape{

public int radius;

public Circle(){

}

public Circle(Circle circle) {
super(circle);
this.radius = circle.radius;
}

public Shape clone(){
return new Circle(this);
}

@Override
public boolean equals(Object object2) {
if (!(object2 instanceof Circle)) return false;
Shape shape2 = (Shape) object2;
return shape2.x == x && shape2.y == y && Objects.equals(shape2.color, color);
}
}



@GetMapping("/designpattern4")
@ResponseBody
public HttpStatus designpattern4(HttpServletRequest request) {
System.out.println(request.getRequestURL());

Circle circle = new Circle();
circle.x = 10;
circle.y = 10;
circle.radius = 20;
circle.color = "blue";

Circle anotherCircle = (Circle) circle.clone();

if(circle.equals(anotherCircle)){
System.out.println("circle"+ "sameAs" +"anotherCircle");
}else{
System.out.println("circle"+ "NotSameAs" +"anotherCircle");
}


Rectangle rectangle = new Rectangle();
rectangle.x = 10;
rectangle.y = 20;

Rectangle anotherRectangle = (Rectangle) rectangle.clone();

if(rectangle.equals(anotherRectangle)){
System.out.println("rectangle"+ "sameAs" +"anotherRectangle");
}else{
System.out.println("rectangle"+ "NotSameAs" +"anotherRectangle");
}

return HttpStatus.OK;
}

응답값

1
2
3
4
>>>
http://localhost:8080/basic/designpattern4
circlesameAsanotherCircle
rectanglesameAsanotherRectangle

적용

프로토타입 패턴은 복사해야 하는 객체들의 구상 클래스들에 코드가 의존하면 안 될 때 사용하세요.

이와 같은 경우는 당신의 코드가 어떤 인터페이스를 통해 타사 코드에서 전달된 객체들과 함께 작동할 때 많이 발생합니다. 이러한 객체들의 구상 클래스들은 알려지지 않았기 때문에 이러한 클래스들에 의존할 수 없습니다.

프로토타입 패턴은 클라이언트 코드에 복제를 지원하는 모든 객체와 작업할 수 있도록 일반 인터페이스를 제공합니다. 이 인터페이스는 클라이언트 코드가 복제하는 객체들의 구상 클래스들에서 클라이언트 코드를 독립시킵니다.

프로토타입 패턴은 각자의 객체를 초기화하는 방식만 다른 자식 클래스들의 수를 줄이고 싶을 때 사용하세요.

사용하기 전에 많은 설정이 필요한 복잡한 클래스가 있다고 가정해 봅시다. 이 클래스를 설정하는 데는 몇 가지 일반적인 방법들이 있으며 설정되어야 하는 클래스의 새로운 인스턴스들의 생성을 담당하는 코드는 당신의 앱에 흩어져 있습니다. 중복을 줄이기 위해 당신은 여러 자식 클래스들을 만들어 모든 공통 설정 코드를 그 클래스들의 생성자들에 넣었습니다. 이렇게 중복 문제는 해결했지만 이제 쓸모없는 자식 클래스들이 많이 생겼습니다.

프로토타입 패턴은 다양한 방식으로 설정된 미리 만들어진 객체들의 집합을 프로토타입들로 사용할 수 있도록 합니다. 일부 설정과 일치하는 자식 클래스를 인스턴스화하는 대신 클라이언트는 간단하게 적절한 프로토타입을 찾아 복제할 수 있습니다.

장단점

장점

  • 객체들을 구상 클래스들에 결합하지 않고 복제할 수 있습니다.
  • 반복되는 초기화 코드를 제거한 후 그 대신 미리 만들어진 프로토타입들을 복제하는 방법을 사용할 수 있습니다.
  • 복잡한 객체들을 더 쉽게 생성할 수 있습니다.
  • 복잡한 객체들에 대한 사전 설정들을 처리할 때 상속 대신 사용할 수 있는 방법입니다.

단점

  • 순환 참조가 있는 복잡한 객체들을 복제하는 것은 매우 까다로울 수 있습니다.

싱글턴 패턴(Singleton)

사용 상황 요약

싱글턴은 클래스에 인스턴스가 하나만 있도록 하면서 이 인스턴스에 대한 전역 접근(액세스) 지점을 제공하는 생성 디자인 패턴입니다.

싱글턴의 모든 구현은 공통적으로 다음의 두 단계를 갖습니다.

  • 다른 객체들이 싱글턴 클래스와 함께 new 연산자를 사용하지 못하도록 디폴트 생성자를 비공개로 설정하세요.
  • 생성자 역할을 하는 정적 생성 메서드를 만드세요. 내부적으로 이 메서드는 객체를 만들기 위하여 비공개 생성자를 호출한 후 객체를 정적 필드에 저장합니다. 이 메서드에 대한 그다음 호출들은 모두 캐시된 객체를 반환합니다. => 싱글턴의 생성자는 항상 클라이언트 코드에서부터 숨겨져야 합니다. get­Instance 메서드를 호출하는 것이 Singleton 객체를 가져올 수 있는 유일한 방법이어야 합니다.

스크린샷 2023-03-19 오후 8.09.16

예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Singleton {
private static Singleton instance;
public String value;

private Singleton(String value) {
// The following code emulates slow initialization.
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
this.value = value;
}

public static Singleton getInstance(String value) {
if (instance == null) {
instance = new Singleton(value);
}
return instance;
}
}

@GetMapping("/designpattern5")
@ResponseBody
public HttpStatus designpattern5(HttpServletRequest request) {
System.out.println(request.getRequestURL());

System.out.println("싱글톤 반환이라면 같은 value 출력" + "\n" +
"다른 value 2개 출력시, 싱글톤이 아닌 인스턴스 2개" + "\n\n" +
"RESULT:" + "\n");
Singleton singleton = Singleton.getInstance("FOO");
Singleton anotherSingleton = Singleton.getInstance("BAR");
System.out.println(singleton.value);
System.out.println(anotherSingleton.value);

return HttpStatus.OK;
}

적용

싱글턴 패턴은 당신 프로그램의 클래스에 모든 클라이언트가 사용할 수 있는 단일 인스턴스만 있어야 할 때 사용하세요. 예를 들자면 프로그램의 다른 부분들에서 공유되는 단일 데이터베이스 객체처럼 말입니다.

싱글턴 패턴은 특별 생성 메서드를 제외하고는 클래스의 객체들을 생성할 수 있는 모든 다른 수단들을 비활성화합니다. 이 메서드는 새 객체를 생성하거나 객체가 이미 생성되었으면 기존 객체를 반환합니다.

싱글턴 패턴은 전역 변수들을 더 엄격하게 제어해야 할 때 사용하세요.

전역 변수들과 달리 싱글턴 패턴은 클래스의 인스턴스가 하나만 있도록 보장해 줍니다. 캐시 된 인스턴스는 싱글턴 클래스 자체를 제외하고는 그 어떤 것과도 대체될 수 없습니다.

참고로 이 제한은 언제든 조정할 수 있고 원하는 수만큼의 싱글턴 인스턴스 생성을 허용할 수 있습니다. 그러기 위해서 변경해야 하는 코드의 유일한 부분은 get­Instance 메서드의 본문입니다.

장단점

장점

  • 클래스가 하나의 인스턴트만 갖는다는 것을 확신할 수 있습니다.
  • 이 인스턴스에 대한 전역 접근 지점을 얻습니다.
  • 싱글턴 객체는 처음 요청될 때만 초기화됩니다.

단점

  • 단일 책임 원칙을 위반합니다. 이 패턴은 한 번에 두 가지의 문제를 동시에 해결(두가지 책임)합니다.
    • 클래스에 인스턴스가 하나만 있도록 합니다.(런타임중 인스턴스의 개수를 통제, 관리, 생성하는 책임)
    • 해당 인스턴스에 대한 전역 접근 지점을 제공합니다. (자신의 비즈니스 로직에 대한 책임)(전역적인 위험, private, stateless한 변수 관리)
  • 또 싱글턴 패턴은 잘못된 디자인(예를 들어 프로그램의 컴포넌트들이 서로에 대해 너무 많이 알고 있는 경우)을 가릴 수 있습니다.
  • 그리고 이 패턴은 다중 스레드 환경에서 여러 스레드가 싱글턴 객체를 여러 번 생성하지 않도록 특별한 처리가 필요합니다.
  • 싱글턴의 클라이언트 코드를 유닛 테스트하기 어려울 수 있습니다. 그 이유는 많은 테스트 프레임워크들이 모의 객체들을 생성할 때 상속에 의존하기 때문입니다. 싱글턴 클래스의 생성자는 비공개이고 대부분 언어에서 정적 메서드를 오버라이딩하는 것이 불가능하므로 싱글턴의 한계를 극복할 수 있는 창의적인 방법을 생각해야 합니다. 아니면 그냥 테스트를 작성하지 말거나 싱글턴 패턴을 사용하지 않으면 됩니다.
CATALOG
  1. 1. 왜?
  2. 2. 참조
  3. 3. 사전 지식
  4. 4. 생성 패턴
  5. 5. 팩토리 메서드 패턴(Factory Method)
    1. 5.1. 사용 상황 요약
    2. 5.2. 팩토리 메서드란?
    3. 5.3. 예시
    4. 5.4. 요약
    5. 5.5. 적용
      1. 5.5.1. 팩토리 메서드는 당신의 코드가 함께 작동해야 하는 객체들의 정확한 유형들과 의존관계들을 미리 모르는 경우 사용하세요.
      2. 5.5.2. 팩토리 메서드는 당신의 라이브러리 또는 프레임워크의 사용자들에게 내부 컴포넌트들을 확장하는 방법을 제공하고 싶을 때 사용하세요.
      3. 5.5.3. 팩토리 메서드는 기존 객체들을 매번 재구축하는 대신 이들을 재사용하여 시스템 리소스를 절약하고 싶을 때 사용하세요.
    6. 5.6. 장단점
    7. 5.7. 다른 패턴과의 관계
  6. 6. 추상 팩토리 패턴(Abstract Factory)
    1. 6.1. 사용 상황 요약
    2. 6.2. 추상 팩토리 패턴이란?
    3. 6.3. 구조
    4. 6.4. 예제
    5. 6.5. 적용
    6. 6.6. 장단점
  7. 7. 빌더패턴
    1. 7.1. 사용 상황 요약
    2. 7.2. 빌더 패턴 이란?
    3. 7.3. 예시
    4. 7.4. @Builder 패턴 원리? (클래스 레벨 에너테이션 부분)
    5. 7.5. 적용
      1. 7.5.1. ‘점층적 생성자’를 제거하기 위하여 빌더 패턴을 사용하세요.
      2. 7.5.2. 빌더 패턴은 당신의 코드가 일부 제품의 다른 표현들(예: 석조 및 목조 주택들)을 생성할 수 있도록 하고 싶을 때 사용하세요.
      3. 7.5.3. 빌더를 사용하여 복합체 트리들 또는 기타 복잡한 객체들을 생성하세요.
    6. 7.6. 장단점
      1. 7.6.1. 장점
      2. 7.6.2. 단점
  8. 8. 프로토타입
    1. 8.1. 사용 상황 요약
    2. 8.2. 프로토타입 패턴이란?
    3. 8.3. 예시
    4. 8.4. 적용
      1. 8.4.1. 프로토타입 패턴은 복사해야 하는 객체들의 구상 클래스들에 코드가 의존하면 안 될 때 사용하세요.
      2. 8.4.2. 프로토타입 패턴은 각자의 객체를 초기화하는 방식만 다른 자식 클래스들의 수를 줄이고 싶을 때 사용하세요.
    5. 8.5. 장단점
      1. 8.5.1. 장점
      2. 8.5.2. 단점
  9. 9. 싱글턴 패턴(Singleton)
    1. 9.1. 사용 상황 요약
    2. 9.2. 예시
    3. 9.3. 적용
      1. 9.3.1. 싱글턴 패턴은 당신 프로그램의 클래스에 모든 클라이언트가 사용할 수 있는 단일 인스턴스만 있어야 할 때 사용하세요. 예를 들자면 프로그램의 다른 부분들에서 공유되는 단일 데이터베이스 객체처럼 말입니다.
      2. 9.3.2. 싱글턴 패턴은 전역 변수들을 더 엄격하게 제어해야 할 때 사용하세요.
    4. 9.4. 장단점
      1. 9.4.1. 장점
      2. 9.4.2. 단점