[CS-Desgin Pattern]생성패턴(싱글톤, 팩토리)

지영·2023년 9월 22일
1

CS

목록 보기
71/77

디자인패턴에는 생성패턴, 구조패턴, 행위패턴 총 3가지로 나눌 수 있습니다. 그 중 첫번째로 생성패턴에 대해서 알아보겠습니다.

🧐 생성패턴?

말 그대로 객체를 생성하는 것에 중점을 두는 패턴입니다.

따라서 객체 생성에 대한 로직은 캡슐화하고 이에 따라 재사용성을 증가시킬 수 있다는 장점이 있습니다. 객체를 독립적으로 생성하고 교체할 수 있게 되기 때문입니다.

생성패턴에는 위의 이미지에서처럼 싱글톤, 팩토리, 추상, 빌터, 프로토타입 패턴으로 꽤 많습니다!
주니어 입장이기 때문에 싱글톤패턴, 팩토리패턴만 자세히 알아보았습니다. (가장 일반적으로 쓰이는 생성패턴이라고 합니다.)

1. 싱글톤 패턴

클래스의 인스턴스를 '하나만' 생성되도록 보장하는 패턴이다. 즉, 인스턴스가 필요할 때, 똑같은 인스턴스를 만들지 않고 기존의 인스턴스를 활용하는 것이다.

  • 용도

    • 주로 공통된 객체를 여러 개 생성해야 하는 상황에서 사용
    • 예시 ) 데이터베이스의 커넥션 풀, 스레드풀, 캐시, 로그 기록 객체
    • 인스턴스가 절대적으로 한 개만 존재하는 것을 보증하고 싶을 때 사용.
  • 사용 방법 : JAVA에서는 생성자를 private하게 선언하여 다른 곳에서 생성하지 못하게 만들고, getInstance()메소드를 통해 사용할 수 있도록 구현한다.

  • 효과 및 장점

    • 한번의 new를 통해 만든 객체로 활용하면 메모리 낭비를 방지할 수 있다. ➡️ 리소스 절약
    • 싱글톤으로 구현한 인스턴스는 전역이다. 따라서 다른 클래스의 인스턴스들이 데이터를 공유하는 것이 가능하다.
  • 단점

    • 만약 싱글톤 인스턴스 너무 많은 용도로 쓰이면, 데이터의 공유가 지나치는 순간이 온다. 이렇게 되면 다른 클래스들 간의 결합도가 높아져 개방-폐쇄 원칙에 위반하게 된다.
    • 유지보수가 힘들고 테스트도 힘들어진다.
    • 멀티 스레드 환경에서 동기화 처리를 해주지 않으면 인스터스가 2개 생성되는 상황도 온다. (싱글톤 패턴의 정의에 위반)

❗❗❗ 이처럼 단점이 치명적이므로 꼭 필요한 상황이 아니라면 큰 프로젝트에서는 지양하는 것이 좋을 것 같다.


하지만 자바처럼 멀티스레드 환경에서도 안전하게 싱글톤 패턴을 구현할 수 있는 방식이 있습니다.

멀티스레드 환경(*JAVA)에서 안전한 싱글톤 만드는 법

1. Lazy Initialization(초기화지연)

  • private static으로 인스턴스 변수를 만든다.
  • private으로 생성자를 만들어 외부에서의 생성을 막는다.
  • synchronized 동기화를 활용하여 스레드를 안전하게 만든다. (단, synchronized는 성능 저하를 초래하므로 사용을 지양한다.)
public class ThreadSafe_Lazy_Initialization{
 
    private static ThreadSafe_Lazy_Initialization instance;
 
    private ThreadSafe_Lazy_Initialization(){}
     
    public static synchronized ThreadSafe_Lazy_Initialization getInstance(){
        if(instance == null){
            instance = new ThreadSafe_Lazy_Initialization();
        }
        return instance;
    }
 

2. Lazy Initialization + Double-checked Locking

  • 1번과의 차이점은 조건문에서부터 synchronized를 걸지 않고, 먼저 인스턴스의 존재여부를 확인한다. 따라서 두 번째 조건문에 있는 synchronized로 동기화를 시켜 인스턴스를 생성한다.
  • 스레드를 안전하게 만들면서 처음 생성이후에는 synchronized를 실행하지 않아 1번만큼의 성능저하는 일어나지 않는다. (하지만 여전히 synchronized로 인한 성능저하는 해결되지 않음)
public class ThreadSafe_Lazy_Initialization{
    private volatile static ThreadSafe_Lazy_Initialization instance;

    private ThreadSafe_Lazy_Initialization(){}

    public static ThreadSafe_Lazy_Initialization getInstance(){
    	if(instance == null) {
        	synchronized (ThreadSafe_Lazy_Initialization.class){
                if(instance == null){
                    instance = new ThreadSafe_Lazy_Initialization();
                }
            }
        }
        return instance;
    }
}

3. Initialization on demand holer idiom (holder에 의한 초기화)

  • 실제로 가장 많이 사용되고 있는 싱글톤 클래스 사용방법
  • 클래스 안에 Holer라는 클래스를 두고, JVM의 Class Loader매커니즘과 Class가 로드되는 시점을 이용한 방법이다. 1번 방식에서 동기화 문제를 해결할 수 있다.
  • Holder클래스는 getInstance()메소드가 호출되기 전에는 참조되지 않는다. 최초로 getInstance()메소드가 호출될 때 클래스 로더에 의해 싱글톤 객체가 생성되어 리턴된다.
package SingleTonExample;

public class InitializationOnDemandHolderIdiom {

	private InitializationOnDemandHolderIdiom(){}
	
	private static class SingleTonHolder{
    // static이므로 클래스 로딩 시점에 한번만 호출됨
    // final값으로 다시 값이 할당받지 않도록 함 
		private static final InitializationOnDemandHolderIdiom instance = new InitializationOnDemandHolderIdiom();
	}
	
	public static InitializationOnDemandHolderIdiom getInstance(){
		return SingleTonHolder.instance;
	}
}

✅ 싱글톤 패턴 예시

싱글톤의 특성을 이용하여 데이터베이스, 캐시, 로깅 등으로 용도가 자주 쓰인다고 위에서 언급했었습니다! 모두 상태를 공유해야 하고 리소스 사용의 최적화가 필요한 예시들입니다.
그 중 로깅에 쓰이는 Logger 클래스를 만든다고 생각해 봅시다.

public class Logger {
	private static Logger instance;
    private String logData = "";
    
    private Logger(){
    	// private Constructor
	}
    
    public static synchronized Logger getInstance(){
    	if (instance == null){
        	instance = new Logger();
        }
        return instance;
    }
   
}

public void log(String message){
	logData += message + "\n";
}

public void printLog(){
	System.out.println(logData);
}

위와 같이 private으로 생성되는 인스턴스를 클라이언트 입장에서 어떻게 이용할 수 있을까요?

public class Client{
	public static void main(String[] args){
    	Logger logger = Logger.getinstance();
        
        logger.log("message1");
        logger.log("message2...");
        
        logger.printLog();
    }
}

위와 같이 클라이언트는 getInstance()를 한 번만 호출하여 log를 공유하여 사용할 수 있게 됩니다. 🎉

2. 팩토리 패턴

팩토리 Class가 있으며, 해당 Class에서 객체생성 로직캡슐화한 패턴을 말합니다. 클라이언트가 직접 객체를 생성하지 않아, 코드간의 결합도가 높습니다.

  • 용도

    • 다양한 타입의 객체를 생성해야 할 때
    • 데이터베이스 연결, GUI 툴킷, 파일 포맷 변환등에 자주 쓰임
  • 사용 방법 : Creator라는 최상위 클래스가 있어, 팩토리 메서드를 추상화하여 서브 클래스인 ConcreteCreator에서 구현하게끔 만듭니다.

    위의 이미지 예시를 들면, 바리스타가 Creator로서 최상위 클래스로 있습니다. recipe()라는 팩토리 메서드를 만들어, 라떼와 아메리카노 레시피를 재정의하게 됩니다.

  • 효과 및 장점

    • 객체 간의 겹합도가 낮아 유지보수에 용이하고 확장성이 높아집니다.
    • 단일 책임 원칙 준수 : 객체 생성을 한 클래스로 정리합니다.
    • 개발/폐쇄 원칙 준수 : 새로운 유형을 생성할 때도 기존 코드는 수정하지 않고 도입이 가능합니다.
    • 라이브러리 혹은 프레임워크 사용자에게 구성 요소를 확장하는 방법을 제공할 때도 유용합니다.
  • 단점

    • 각 유형마다 객체들을 모두 구현해야 하기 때문에 서브 클래스의 수가 급증하게 됩니다. ➡️ 코드의 복장성 증가

♻️ 팩토리 패턴으로 더티코드 리팩토링 하기

class Animal{
	String name, color;
    
    @Override
    public String toString(){
    	return String.format("my friend is called {name : '%s'}, and she is {color : '%s'} one", name, color);
    }
}

public static Cat adoptCat(String name, String color){
	if (name == null){
    	throw new IllegalArgumentException("반려동물의 이름을 지어주세요");
    }
    
    // 동물 객체 생성
    Animal animal = new Animal();
    
    // 객체 생성 후 처리
    animal.name = name;
    
    if name.equalsIgnoreCase("고양이"){
    	animal.color = "white";
    }else if (animal.equalsIgnoreCase("강아지"){
    	animal.color = "black";
    
    }
        System.out.println(animal.name + " 이를 입양하였습니다. ");

    return animal;
}

public static void main(String[] args){
	Animal 소금 = adoptAnimal("고양이");
    
    Animal 흑임자 = adoptAnimal("강아지");
	
}

💥 위의 방식은 강아지/말/새 등 객체의 유형이 늘어난다면 코드도 복잡해지고 유지보수가 힘들어질 것입니다. 이를 개선하기 위해 아래와 같이 리팩토링이 가능합니다.

// 동물 객체
Class Animal{
	String name, color;
    
    @Override
    public String toString{
        	return String.format("my friend is called {name : '%s'}, and she is {color : '%s'} one", name, color);

    }
}

class Cat extends Animal{
	Cat(String name, String color){
    	this.name = name;
        this.color = color;
    } 
}

class Dog extends Animal{
	Dog(String name, String color){
    	this.name = name;
        this.color = color;
    }
}
// 공장 객체 : 입양보호소
class AdoptFactory{
	final animal adopt(String email){
    	validate(email);
        Animal animal = adoptProcess(); // 입양 절차 
        sendEmail(email, animal);
        
        return animal;
    }
        
   // 팩토리 메서드
   abstract protected Animal adoptProcess();
    
   class CatAdoptFactory extends AdoptFactory{
   		@Override
        protected Animal adoptProcess(){
        	return new Cat("고양이", "yellow") 
        }
   } 
 
   class DogAdoptFactory extends AdoptFactory{
   		@Override
        protected Animal adoptProcess(){
        	return new Dog("강아지", "black") 
        }
   } 
   
    
   
    
    private void validate( String email){
   	    if (email == null) {
            throw new IllegalArgumentException("이메일을 남겨주세요");
        }
    }
    
    private void sendEmail(String email, Animal animal){
        System.out.println(animal.name + " 이를 입양정보를 "+ email + "을 통해 보내드렸습니다." );
    }
}
class Client {
	public static void main(String[] args){
    	Animal cat = new CatAdoptFactory().adopt("aaa@naver.com);
        Animal dog = new DogAdoptFactory().adopt("bbb@naver.com);

이처럼, 고양이 입양 절차를 통해 입양팩토리 객체를 생성함으로서 수정에는 닫혀있고 확장에는 열려있는 구조로 리팩토링이 가능해진다.

👻 참고 | Spring Framework의 BeanFactory도 팩토리 패턴을 이용했다고 한다.

  • Creator : Bean Factory
  • ConcreteCreator : ClassPathXmlApplicationContext, AnnotationConfigApplicationContext
    위의 ConcreteCreator에서 넘기는 Object타입을 넘겨받는 인스턴스가 ConcreteProduct가 된다고 한다.
  • ConcreteProduct : 컴포넌트 스캔, bean설정 어노테이션...



profile
꾸준함의 힘을 아는 개발자📍

0개의 댓글