Gang of Four(GoF) 패턴 중 생성 패턴(Creational Patterns)을 정리해 보겠다.
생성 패턴
1. 생성 패턴의 개념
- 객체를 생성하는 것과 관련된 패턴이다.
- 객체의 생성과 변경이 전체 시스템에 미치는 영향을 최소화하도록 만들어주어 유연성을 높일 수 있고 코드를 유지하기 쉬워진다.
- 객체의 생성과 참조 과정을 추상화함으로써 시스템 개발할 때 부담을 덜어준다.
2. GoF 생성 패턴 5가지
1. Singleton Pattern
하나의 객체만 만들고 계속 돌려쓰는 패턴
❓ 언제 사용?
- 매번 객체를 만들 필요 없을 때
- 설정값, 캐시, 공용 서비스 등에 활용
✅SpringBoot에서의 활용
- Spring의 대부분 Bean(@Service, @Repositroy, @Component 등)은 기본적으로 Singleton Scope이다.
- 즉, 스프링은 객체를 싱글톤으로 생성하여 관리한다.
자바 예시
sigleton1~singleton3 변수는 모두 전역 메서드인 getInstance를 통해 객체를 반환받는다.
Singleton 클래스의 기본생성자는 private이므로 외부에서 생성자로 객체 생성 절대 불가능.
getInstance는 한 번만 호출되면 instance에 객체를 받게 된다.
package creational_pattern;
public class SingletonPatternMain {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
Singleton singleton3 = Singleton.getInstance();
System.out.println("세 개의 참조변수가 모두 같은 객체인가? " + (singleton1 == singleton2 && singleton2 == singleton3));
}
}
class Singleton {
private static Singleton instance;
private Singleton(){} //생성자를 private로 외부에서는 객체 생성 불가능 반드시 getInstance 메서드를 이용해서 생성
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
결과: "세 개의 참조변수가 모두 같은 객체인가? ture"
2. Factory Method Pattern
객체 생성 책임을 자식 클래스에게 위임하는 패턴
(어떤 객체를 만들지는 하위 클래스가 결정)
SpringBoot에서 활용
- BeanFactory, ApplicationContext 내부 구조
- 스프링이 객체 생성 로직을 스프링이 알아서 결정하는 방식 자체가 Factory Method 철학을 따르는 것이다.
❌ 나쁜 예 (Factory Method 패턴이 존재하지 않을 때)
(요구사항에서 AnimalService클래스에 run메서드 내부에 반드시 새로운 Animal 객체를 만들어서 인수로 받은 동물에 맞게 Animal객체를 생성해야 한다.)
package creational_pattern;
interface Animal{void sound();}
class Dog implements Animal{public void sound(){System.out.println("강아지 멍멍");}}
class Cat implements Animal{public void sound(){System.out.println("고양이 야옹");}}
class AnimalService {
public void run(Animal animal) {
Animal target;
if(animal instanceof Dog) {
target = new Dog();
}else if(animal instanceof Cat) {
target = new Cat();
}else {
target = null;
}
target.sound();
}
}
public class FactoryMethodPatternMain {
public static void main(String[] args) {
AnimalService animalService = new AnimalService();
animalService.run(new Dog());
}
}
🚨문제
현재 코드는 OCP, SRP 위반한다.
만약 새로운 Bird 클래스 추가되면 조건문쪽 수정을 해야 한다.
현재 AnimalService는 모든 동물 클래스들의 정보를 알아야 한다. 단일책임원칙(SRP) 위반.
여기서 FactoryMethodParttern 생성패턴을 적용한다면?
1. Animal 생성 책임을 팩토리로 분리한다. Animal 인터페이스를 구현한 클래스들 Dog, Cat 역시 구체 팩토리로 분리한다.
interface AnimalFactory {
Animal create();
}
class DogFactory implements AnimalFactory {
public Animal create() {
return new Dog();
}
}
class CatFactory implements AnimalFactory {
public Animal create() {
return new Cat();
}
}
class BirdFactory implements AnimalFactory{public Animal create(){return new Bird();}}
2. 서비스는 구체적인 동물을 몰라도 된다. (SRP 준수해짐, OCP 준수해짐) 추상화를 이용해서 어떤 자식이든 Service는 제대로 동작된다.
class AnimalService {
private final AnimalFactory animalFactory;
public AnimalService(AnimalFactory animalFactory) {
this.animalFactory = animalFactory;
}
public void run() {
Animal animal = animalFactory.create();
animal.sound();
}
}
3. 전체 코드
package creational_pattern;
interface Animal{void sound();}
class Dog implements Animal{public void sound(){System.out.println("강아지 멍멍");}}
class Cat implements Animal{public void sound(){System.out.println("고양이 야옹");}}
class Bird implements Animal{public void sound(){System.out.println("새 짹짹");}}
interface AnimalFactory {
Animal create();
}
class DogFactory implements AnimalFactory {
public Animal create() {
return new Dog();
}
}
class CatFactory implements AnimalFactory {
public Animal create() {
return new Cat();
}
}
class BirdFactory implements AnimalFactory{public Animal create(){return new Bird();}}
class AnimalService {
private final AnimalFactory animalFactory;
public AnimalService(AnimalFactory animalFactory) {
this.animalFactory = animalFactory;
}
public void run() {
Animal animal = animalFactory.create();
animal.sound();
}
}
public class FactoryMethodPatternMain {
public static void main(String[] args) {
AnimalService animalService = new AnimalService(new CatFactory());
animalService.run();
}
}
3. Abstact Factory Pattern
관련된 객체들 "세트"를 생성하는 패턴
package creational_pattern;
interface SideDish{void eat();}
interface Soup {void drink();}
interface MainDish{void eatMain();}
class Kimchi implements SideDish {
public void eat(){
System.out.println("김치를 먹는다.");
}
}
class DoenjangSoup implements Soup {
public void drink(){
System.out.println("된장국을 마신다.");
}
}
class Bibimbap implements MainDish {
public void eatMain(){
System.out.println("비빔밥 먹는중.");
}
}
//추상공장
interface MealFactory {
SideDish createSide();
Soup crateSoup();
MainDish createMain();
}
class KoreanMealFactory implements MealFactory {
@Override
public SideDish createSide() {
return new Kimchi();
}
@Override
public Soup crateSoup() {
return new DoenjangSoup();
}
@Override
public MainDish createMain() {
return new Bibimbap();
}
}
public class AbstractFactoryPatternMain {
static class MealService {
private final MealFactory factory;
MealService(MealFactory factory) {
this.factory = factory;
}
public void service(){
factory.createSide().eat();
factory.crateSoup().drink();
factory.createMain().eatMain();
}
}
public static void main(String[] args) {
MealService mealService = new MealService(new KoreanMealFactory());
mealService.service();
}
}
💡참고 (FactoryMethod Pattern VS Abstract Factory Pattern)
둘 다 생성 책임을 다른 곳을 넘기는 패턴이다.
하지만, Factory Method는 "하나의 객체 생성만 위임하는 패턴" Abstract Factory는 "여러 객체가 함께 구성되는 제품군 생성을 위임하는 패턴이다"
결론적으로
Factory Method는 단일 객체를 어떤 클래스로 만들지 결정하는 패턴
Abstract Factory는 서로 관련된 여러 객체를 세트로 만들도록 묶어둔 패턴이다.
4. Builder Pattern
특정 클래스가 멤버변수가 많고, 생성자의 파라미터도 복잡하면 복잡한 생성자를 대신하여 객체를 단계별로 만들 때 사용하는 패턴이다.
SpringBoot의 활용 예
- Lombok @Builder
- HttpEntity, WebClient, MockMvc 등 빌더 스타일 메서드 사용한다.
자바 예시
핵심
- 일반적으로 빌더패턴을 적용하는 대상 클래스(User) 내부에 빌더클래스(Builder)를 정적 중첩 클래스로 선언한다.
- 그냥 내부클래스로 구현하면 메모리낭비, 빌더패턴의 의미가 사라진다. 그래서 static을 붙인 정적 중첩 클래스로 선언
- 빌더패턴의 핵심은 메서드 체이닝이다. return this;로 계속해서 자기 자신(객체)을 반환해서 메서드 체이닝의 이점을 이용한다.
package builder;
public class UserMain {
public static void main(String[] args) {
User user1 = new User.Builder()
.name("테스트")
.age(100)
.email("test@gmail.com")
.build();
System.out.println(user1);
}
}
class User {
private String name;
private Integer age;
private String email;
static class Builder {
private String name;
private Integer age;
private String email;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(Integer age) {
this.age = age;
return this;
}
public Builder email(String email) {
this.email = email;
return this;
}
public User build() {
return new User(this);
}
}
private User(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.email = builder.email;
}
@Override
public String toString() {
return "이름: " + name + " 나이: " + age + " 이메일: " + email;
}
}

5. Prototype Pattern
*복제(clone)*로 객체를 만들어 생성 비용을 줄이는 패턴
객체를 복사해서 새로운 객체를 만드는 패턴이다.
📌핵심은 new로 객체 생성을 안 하고, 기존 객체를 복사해서 새로운 객체를 만드는 것.
SpringBoot에서 언제 사용될까?
- 스프링은 거의 사용하지 않는다. (스코프 문제 때문에)
- 대신 프로토타입 스코프 Bean이 비슷한 개념
왜 필요할까?
- 복잡한 DB 조회가 필요한 객체
- 객체 생성 비용이 매우 비쌀 때 (객체의 크기가 클 때)
- 큰 이미지/리소스를 로딩해야 하는 객체일 때
- 네트워크 작업이 필요한 객체일 때
자바 예시 (문서 템플릿 객체 복사하기)
자바에서 제공하는 Cloneable을 사용한다.
여기서 clone 메서드는 Object 클래스의 clone메서드를 오버라이딩하는 것이라는 점.
interface Document extends Cloneable {
Document clone();
void print();
}
class Resume implements Document {
private String name;
private String career;
public Resume(String name, String career) {
this.name = name;
this.career = career;
}
@Override
public Document clone() {
try{
return (Resume)super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
@Override
public void print() {
System.out.println("[이력서] 이름: "+name+", 경력: "+career);
}
}
public class PrototypePatternMain {
public static void main(String[] args) {
//원본 객체
Resume template = new Resume("홍길동","5년차");
//복사 객체
Resume copyTemplate = (Resume) template.clone(); //런타임 기준에서는 Resume 객체가 맞지만 컴파일러는 Object로 판단하기에 형변환함
Resume copyTemplate2 = (Resume) template.clone();
template.print();
copyTemplate.print();
copyTemplate2.print();
}
}