1. 소개
디자인 패턴 은 소프트웨어를 작성할 때 사용하는 일반적인 패턴입니다 . 이는 시간이 지남에 따라 개발된 확립된 모범 사례를 나타냅니다. 그런 다음 코드가 잘 설계되고 잘 구축되었는지 확인하는 데 도움이 될 수 있습니다.
생성 패턴 은 객체의 인스턴스를 얻는 방법에 중점을 둔 디자인 패턴입니다 . 일반적으로 이것은 클래스의 새 인스턴스를 구성하는 방법을 의미하지만 어떤 경우에는 사용할 준비가 된 이미 구성된 인스턴스를 얻는 것을 의미합니다.
이 기사에서는 몇 가지 일반적인 창작 디자인 패턴을 다시 살펴보겠습니다. 어떻게 생겼는지, JVM이나 다른 핵심 라이브러리에서 찾을 수 있는 위치를 알아보겠습니다.
2. 공장 방식
팩토리 메소드 패턴은 우리가 구성하고 있는 클래스에서 인스턴스 구성을 분리하는 방법입니다. 이는 정확한 유형을 추상화하여 클라이언트 코드가 인터페이스 또는 추상 클래스 측면에서 대신 작동하도록 하기 위한 것입니다.
class SomeImplementation implements SomeInterface {
// ...
}
public class SomeInterfaceFactory {
public SomeInterface newInstance() {
return new SomeImplementation();
}
}
여기서 클라이언트 코드는 SomeImplementation 에 대해 알 필요가 없으며 대신 SomeInterface 측면에서 작동합니다 . 그러나 이것보다 훨씬 더, 우리는 공장에서 반환된 유형을 변경할 수 있으며 클라이언트 코드는 변경할 필요가 없습니다 . 여기에는 런타임에 유형을 동적으로 선택하는 것도 포함될 수 있습니다.
2.1. JVM의 예
JVM에서 이 패턴의 가장 잘 알려진 예는 singleton() , singletonList() 및 singletonMap() 과 같은 Collections 클래스 의 컬렉션 구축 메서드입니다 . 이들 모두는 적절한 컬렉션( Set , List 또는 Map)의 인스턴스를 반환 하지만 정확한 유형은 관련이 없습니다 . 또한 Stream.of() 메서드와 새로운 Set.of() , List.of() 및 Map.ofEntries() 메서드를 사용하면 더 큰 컬렉션에서도 동일한 작업을 수행할 수 있습니다.
요청한 이름에 따라 Charset 클래스 의 다른 인스턴스를 반환하는 Charset.forName() 과 리소스 번들에 따라 다른 리소스 번들을 로드하는 ResourceBundle.getBundle() 를 포함하여 이것에 대한 다른 예도 많이 있습니다 . 제공된 이름에.
이들 모두가 다른 인스턴스를 제공할 필요도 없습니다. 일부는 내부 작동을 숨기기 위한 추상화일 뿐입니다. 예를 들어 Calendar.getInstance() 및 NumberFormat.getInstance()는 항상 동일한 인스턴스를 반환하지만 정확한 세부 정보는 클라이언트 코드와 관련이 없습니다.
3. 추상 공장
추상 팩토리 패턴은 사용 된 공장 추상 기본 유형이이 넘어 단계입니다. 그런 다음 이러한 추상 유형의 관점에서 코드를 작성하고 런타임에 어떻게든 구체적인 팩토리 인스턴스를 선택할 수 있습니다.
먼저 인터페이스와 실제로 사용하려는 기능에 대한 몇 가지 구체적인 구현이 있습니다.
interface FileSystem {
// ...
}
class LocalFileSystem implements FileSystem {
// ...
}
class NetworkFileSystem implements FileSystem {
// ...
}
다음으로, 위의 것을 얻기 위한 팩토리에 대한 인터페이스와 몇 가지 구체적인 구현이 있습니다.
interface FileSystemFactory {
FileSystem newInstance();
}
class LocalFileSystemFactory implements FileSystemFactory {
// ...
}
class NetworkFileSystemFactory implements FileSystemFactory {
// ...
}
그런 다음 실제 인스턴스를 얻을 수 있는 추상 팩토리를 얻는 또 다른 팩토리 메소드가 있습니다.
class Example {
static FileSystemFactory getFactory(String fs) {
FileSystemFactory factory;
if ("local".equals(fs)) {
factory = new LocalFileSystemFactory();
else if ("network".equals(fs)) {
factory = new NetworkFileSystemFactory();
}
return factory;
}
}
여기에 두 가지 구체적인 구현 이 있는 FileSystemFactory 인터페이스가 있습니다. 우리는 런타임에 정확한 구현을 선택하지만 그것을 사용하는 코드는 실제로 어떤 인스턴스가 사용되는지 신경 쓸 필요가 없습니다 . 그런 다음 각각은 FileSystem 인터페이스 의 다른 구체적인 인스턴스를 반환 하지만 다시 말하지만 코드는 우리가 가지고 있는 이것의 인스턴스를 정확히 신경 쓸 필요가 없습니다.
종종 위에서 설명한 것처럼 다른 팩토리 메서드를 사용하여 팩토리 자체를 얻습니다. 여기에 우리의 예에서 getFactory () 메소드는 팩토리 메소드 자체가 그 반환 추상 FileSystemFactory 후 구성하기 위해 사용되는 파일 시스템 .
3.1. JVM의 예
JVM 전체에서 사용되는 이 디자인 패턴의 많은 예가 있습니다. 가장 일반적으로 볼 수 있는 것은 XML 패키지(예: DocumentBuilderFactory , TransformerFactory 및 XPathFactory ) 주변 입니다. 이것들은 모두 우리의 코드가 추상 팩토리의 인스턴스를 얻을 수 있도록 하는 특별한 newInstance() 팩토리 메소드를 가지고 있습니다.
내부적으로 이 방법은 시스템 속성, JVM의 구성 파일 및 서비스 공급자 인터페이스 와 같은 다양한 메커니즘 을 사용하여 사용할 구체적인 인스턴스를 정확히 시도하고 결정합니다. 그러면 원하는 경우 응용 프로그램에 대체 XML 라이브러리를 설치할 수 있지만 실제로 사용하는 코드에는 투명합니다.
코드에서 newInstance() 메서드를 호출 하면 적절한 XML 라이브러리의 팩토리 인스턴스가 생성됩니다. 그런 다음 이 팩토리는 동일한 라이브러리에서 사용하려는 실제 클래스를 구성합니다.
예를 들어 JVM 기본 Xerces 구현 을 사용하는 경우 com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl 인스턴스를 가져 오지만 대신 다른 구현을 사용하려면 다음을 호출합니다. newInstance() 는 대신 투명하게 반환합니다.
4. 빌더
Builder 패턴은 복잡한 객체를 보다 유연하게 구성하고자 할 때 유용합니다. 복잡한 객체를 구축하는 데 사용하는 별도의 클래스를 갖고 클라이언트가 더 간단한 인터페이스로 이를 생성할 수 있도록 함으로써 작동합니다.
class CarBuilder {
private String make = "Ford";
private String model = "Fiesta";
private int doors = 4;
private String color = "White";
public Car build() {
return new Car(make, model, doors, color);
}
}
이를 통해 make , model , door 및 color 에 대한 값을 개별적으로 제공 한 다음 Car 를 빌드할 때 모든 생성자 인수가 저장된 값으로 확인됩니다.
4.1. JVM의 예
JVM 내에서 이 패턴의 몇 가지 핵심적인 예가 있습니다. 의 StringBuilder 와 StringBuffer와 클래스는 우리가 오랫동안 구축 할 수 있도록 빌더있는 문자열을 여러 개의 작은 부품을 제공 . 최신 Stream.Builder 클래스를 사용하면 Stream 을 구성하기 위해 정확히 동일한 작업을 수행할 수 있습니다 .
Stream.Builder<Integer> builder = Stream.builder<Integer>();
builder.add(1);
builder.add(2);
if (condition) {
builder.add(3);
builder.add(4);
}
builder.add(5);
Stream<Integer> stream = builder.build();
5. 초기화 지연
Lazy Initialization 패턴을 사용하여 필요할 때까지 일부 값의 계산을 연기합니다. 때로는 여기에 개별 데이터 조각이 포함될 수 있고 다른 경우에는 전체 개체를 의미할 수 있습니다.
이는 여러 시나리오에서 유용합니다. 예를 들어, 객체를 완전히 구성하는 데 데이터베이스 또는 네트워크 액세스가 필요하고 이를 사용할 필요가 없는 경우 이러한 호출을 수행하면 애플리케이션의 성능이 저하될 수 있습니다 . 또는 필요하지 않은 많은 수의 값을 계산하는 경우 불필요한 메모리 사용이 발생할 수 있습니다.
일반적으로 이것은 하나의 객체가 우리가 필요로 하는 데이터에 대한 지연 래퍼가 되도록 하고 getter 메서드를 통해 액세스할 때 데이터를 계산하도록 함으로써 작동합니다.
class LazyPi {
private Supplier<Double> calculator;
private Double value;
public synchronized Double getValue() {
if (value == null) {
value = calculator.get();
}
return value;
}
}
파이 계산은 비용이 많이 드는 작업이며 수행할 필요가 없는 작업입니다. 위의 내용은 getValue()를 처음 호출할 때 수행되며 이전에는 수행 되지 않습니다.
5.1. JVM의 예
JVM에서 이에 대한 예는 비교적 드뭅니다. 그러나 Java 8에 도입된 Streams API 가 좋은 예입니다. 스트림에서 수행되는 모든 작업은 lazy 이므로 여기에서 값비싼 계산을 수행하고 필요할 때만 호출된다는 것을 알 수 있습니다.
그러나 스트림 자체의 실제 생성도 지연될 수 있습니다 . Stream.generate() 는 다음 값이 필요할 때마다 호출하는 함수를 사용하며 필요할 때만 호출됩니다. 이것을 사용하여 값비싼 값을 로드할 수 있습니다(예: HTTP API 호출). 그리고 새 요소가 실제로 필요할 때만 비용을 지불합니다.
Stream.generate(new BaeldungArticlesLoader())
.filter(article -> article.getTags().contains("java-streams"))
.map(article -> article.getTitle())
.findFirst();
여기에 기사를 로드하고 관련 태그를 기반으로 필터링한 다음 첫 번째 일치하는 제목을 반환하기 위해 HTTP 호출을 수행 하는 공급업체 가 있습니다 . 로드된 맨 처음 기사가 이 필터와 일치하는 경우 실제로 존재하는 기사의 수에 관계없이 네트워크 호출을 하나만 수행하면 됩니다.
6. 개체 풀
생성 비용이 많이 들 수 있지만 기존 인스턴스를 재사용하는 것이 허용되는 대안인 개체의 새 인스턴스를 구성할 때 개체 풀 패턴을 사용합니다. 매번 새로운 인스턴스를 생성하는 대신 이러한 세트를 미리 생성한 다음 필요에 따라 사용할 수 있습니다.
실제 개체 풀은 이러한 공유 개체를 관리하기 위해 존재합니다 . 또한 각각이 동시에 한 장소에서만 사용되도록 추적합니다. 어떤 경우에는 전체 개체 집합이 시작 시에만 구성됩니다. 다른 경우 풀은 필요한 경우 요청 시 새 인스턴스를 생성할 수 있습니다.
6.1. JVM의 예
JVM에서 이 패턴의 주요 예는 스레드 풀의 사용입니다 . ExecutorService입니다은 스레드의 집합을 관리하고 작업이 하나를 실행해야 할 때 우리가 사용할 수 있습니다. 이것을 사용하면 비동기 작업을 생성해야 할 때마다 모든 비용과 함께 새 스레드를 만들 필요가 없습니다.
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.execute(new SomeTask()); // Runs on a thread from the pool
pool.execute(new AnotherTask()); // Runs on a thread from the pool
이 두 작업에는 스레드 풀에서 실행할 스레드가 할당됩니다. 같은 스레드이거나 완전히 다른 스레드일 수 있으며 어떤 스레드가 사용되는지는 우리 코드에서 중요하지 않습니다.
7. 프로토타입
원본과 동일한 객체의 새 인스턴스를 생성해야 할 때 프로토타입 패턴을 사용합니다. 원본 인스턴스는 프로토타입 역할을 하고 원본과 완전히 독립적인 새 인스턴스를 구성하는 데 사용됩니다. 그런 다음 필요하지만 이를 사용할 수 있습니다.
Java는 Cloneable 마커 인터페이스 를 구현 한 다음 Object.clone() 을 사용하여 이에 대한 지원 수준이 있습니다. 이렇게 하면 개체의 얕은 복제가 생성되어 새 인스턴스가 생성되고 필드가 직접 복사됩니다.
이것은 더 저렴하지만 자체적으로 구조화된 객체 내부의 모든 필드가 동일한 인스턴스가 된다는 단점이 있습니다. 즉, 해당 필드의 변경 사항도 모든 인스턴스에서 발생합니다. 그러나 필요한 경우 항상 이를 직접 재정의할 수 있습니다.
public class Prototype implements Cloneable {
private Map<String, String> contents = new HashMap<>();
public void setValue(String key, String value) {
// ...
}
public String getValue(String key) {
// ...
}
@Override
public Prototype clone() {
Prototype result = new Prototype();
this.contents.entrySet().forEach(entry -> result.setValue(entry.getKey(), entry.getValue()));
return result;
}
}
7.1. JVM의 예
JVM에는 이에 대한 몇 가지 예가 있습니다. Cloneable 인터페이스 를 구현하는 클래스를 따라 가면 이를 확인할 수 있습니다. 예를 들어 PKIXCertPathBuilderResult , PKIXBuilderParameters , PKIXParameters , PKIXCertPathBuilderResult 및 PKIXCertPathValidatorResult 는 모두 복제 가능합니다.
또 다른 예는 java.util.Date 클래스입니다. 특히 이것은 Object를 재정의합니다 . clone() 메서드를 사용하여 추가 임시 필드에도 복사할 수 있습니다 .
8. 싱글톤
Singleton 패턴은 인스턴스가 하나만 있어야 하는 클래스가 있을 때 자주 사용되며 이 인스턴스는 애플리케이션 전체에서 액세스할 수 있어야 합니다. 일반적으로 정적 메서드를 통해 액세스하는 정적 인스턴스로 이를 관리합니다.
public class Singleton {
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
정확한 필요에 따라 몇 가지 변형이 있습니다. 예를 들어 인스턴스가 시작 시 생성되는지 또는 처음 사용할 때 생성되는지, 인스턴스에 액세스하는 것이 스레드로부터 안전해야 하는지, 스레드마다 다른 인스턴스가 있어야 하는지 여부 등입니다.
8.1. JVM의 예
JVM은 JVM 자체의 핵심 부품 대표 수업이 몇 가지 예제가 - 런타임, 바탕 화면, 그리고 Security 관리자를 . 이들 모두에는 해당 클래스의 단일 인스턴스를 반환하는 접근자 메서드가 있습니다.
또한 Java Reflection API의 대부분은 싱글톤 인스턴스에서 작동합니다 . 동일한 실제 클래스는 Class.forName() , String.class 를 사용하여 액세스했는지 또는 다른 리플렉션 메서드를 통해 액세스했는지에 관계없이 항상 동일한 Class 인스턴스를 반환합니다 .
비슷한 방식으로 현재 스레드를 나타내는 Thread 인스턴스를 싱글톤으로 간주할 수 있습니다 . 종종 이것의 많은 인스턴스가 있을 것이지만, 정의상 스레드당 하나의 인스턴스가 있습니다. 동일한 스레드에서 실행되는 모든 위치에서 Thread.currentThread() 를 호출 하면 항상 동일한 인스턴스가 반환됩니다.
9. 요약
이 기사에서는 개체의 인스턴스를 만들고 얻는 데 사용되는 다양한 디자인 패턴을 살펴보았습니다. 우리는 또한 코어 JVM 내에서 사용되는 이러한 패턴의 예를 살펴보았으므로 많은 애플리케이션에서 이미 이점을 얻고 있는 방식으로 패턴을 사용하고 있음을 알 수 있습니다.