[1일차] 디자인 패턴, Singleton, Factory, Strategy 패턴

sani·2023년 6월 4일

CS스터디

목록 보기
1/7

🧐 동기

  • CS 스터디에서 공유할 지식을 정리하기위해 글을 쓰게 되었다. 앞으로 네트워크, DB, OS 등 여러 주제들을 다룰 것이고 그 첫번째 주제인 디자인 패턴에 대해 다뤄보자.
  • 사실 이전부터 디자인 패턴에 대한 이해가 필요함을 느꼈다. BE Spring 개발자로서 디버깅에 어려움을 겪던 내게 백엔드 개발자인 친구가 조언을 해줬다. 디자인 패턴을 알면 Spring과 같은 방대한 코드의 흐름을 이해하는데에 도움된다고 조언해줬기에 언제 한번 공부를 해보자 마음먹었으나, 드디어 시작하게 되었다. 비록 개론에 불과한 수준이겠지만, 알고나면 Spring framework 코드를 뜯어볼 수 있지 않을까..? 라는 자그마한 기대를 해본다.

🌱 사전 지식

class와 object의 차이

  • class의 instace화 시킨것이 object. 즉, 메모리(heap area)에 적재된 것이 object. object 생성을 위한 설계도가 class. object와 instace는 혼용하기도 함.

Thread-safe 의미

  • 여러 thread가 해당 method를 동시에 사용해도 안전한 경우.
int a;

int inc(int n)
{
	a += n;
	return a;
}

위 코드는 A thread가 실행 도중에 B thread가 schedule된다면 엉뚱한 값을 return하게 된다. 따라서 전역변수를 제거하거나, 아래와 같이 mutex를 이용해야한다.

int a;
pthread_mutex_t a_lock = PTHREAD_MUTEX_INITIALIZER;

int inc(int n)
{
	pthread_mutex_lock(&a_lock);
	/* critical section */
	a += n;
	pthread_mutex_unlock(&a_lock);
}

디자인 패턴이란?

  • 디자인 패턴은 SW분야 뿐만아니라 다른 분야에서도 적용되던 기법이다. 재발되던 문제를 해결하기위한 솔루션이란 관점은 다른 분야에서도 쉽게 찾아볼 수 있다.
  • SW 측면에서 살펴보자면, 특정 맥락에서 반복적으로 발생하는 고질적인 문제들에 대한 해결책을 '제시'하는 기법이다.

왜 필요한가?

  • 재사용성 : 자주 반복되는 문제들에 대한 해결책을 쉽게 구현 가능.
  • 확장성 : 요구사항 변경에 따른 code 변경 최소화.
  • 공통 언어 : 패턴에 대해 정해뒀기에 부가적인 설명 없이 개발자들 간의 소통 원활.
  • 유지보수성 : SW 구조 파악에 용이. (내가 가장 필요성을 느꼈던 부분이다. Spring은 Singleton, Factory Method, Proxy, Template 패턴 등으로 이뤄져있다 한다)

단점

  • 복잡성 : 추상화와 구조화를 통해 복잡성이 높아지고, 이를 이해하기위해 개발자들은 사전공부가 필요하다. 따라서 나와 같은 초보자들은 단순한 문제에 과도한 해결책을 적용하려하다 코드가 더더욱 복잡해질 수 있다... 조심해야겠다. 이로인해 장점이었던 유지보수성이 오히려 무색해질수도 있다.
  • 낮은 효율성 : 추상화와 계층화 하느라 도입하는데에 오버헤드 초래할 수 있어 초기 투자 비용이 부담될 수 있다.
  • 객체 지향 : 객체 지향적 설계 및 구현 위주로 사용된다.

디자인 패턴 구조

  • Context : 문제 발생하는 여러 상황들 기술.
  • Problem : 패턴이 적용되어 해결될 수 있는 여러 이슈들 기술.
  • Solution : 문제 해결에 필요한 요소들과 그들간의 관계, 책임 등을 기술. 구체적인 구현 방법이나 언어에 비의존적이기에 다양한 상황에 적용될 수 있는 template이 될 수 있다.

GoF 디자인 패턴

  • GoF 디자인 패턴이란, Gang of Four라고 불리는 Erich Gamma, Richard Helm, Ralph Johnson, John Vissides가 처음으로 구체화한 SW공학에서 가장 많이 사용되는 23개의 패턴들을 일컫는다. 이들을 범위에 따라 클래스, 객체로 분류하거나, 목적에 따라 생성, 구조, 행동 3가지로 분류할 수 있다.
  • 생성 : object 생성과 관련된 패턴으로 객체의 instance과정을 추상화하는 방법이다. 객체의 생성과 참조과정을 capsulation하여 객체가 새로 생성되거나 변경되더라도 프로그램 구조에 영향받지 않도록한다.
  • 구조 : 클래스나 객체들을 조합해 더 큰 구조로 만들 수 있게 해주는 패턴이다. 상속을 통해 class나 interface를 합성하거나, object를 합성한다.
  • 행위 : class나 object들이 서로 상호작용하는 방법이나 어떤 태스크, 어떤 알고리즘을 어떤 객체에 할당하는 것이 좋을지를 정의하는 패턴이다. 한 object가 혼자 수행할 수 없는 작업을 여러 object로 어떻게 분배하고 그들의 결합도를 최소화할 수 있는지에 대해 다룬다.

1️⃣ Singleton 패턴

  • 어떤 class의 instance는 하나임을 보장.
  • 어디서든 참조할 수 있음.
  • 여러 thread가 동시에 해당 instance를 공유할 수 있기에 요청이 많다면 효율적이겠지만, 이는 당연히 Concurrency 문제가 동반되기에 주의해야한다.
  • singleton 구현시 중요한점은 multi-thread 환경에서도 singleton을 보장해야하므로 Thread-safe가 보장되어야 한다.

1. Java에서의 Singleton 패턴

  • Java에서 Singleton 패턴이 가지는 공통적 특징은 private constructor를 가지는 것과 static method를 사용하는 것이다. 아래에서 Singleton in Java의 몇가지 이디엄을 소개한다.

1-1 Eager Initialization(Thread-safe)

public class Singleton {
    // Eager Initialization
    private static Singleton uniqueInstance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
      return uniqueInstance; 
    } 
}

static이기에 class loader가 초기화하는 시점에서 정적 바인딩을 통해 해당 instance를 메모리에 등록하는 방식이다. 컴파일 시점에 object를 생성하므로 추후 실행될 일이 없으므로 thread-safe하다.

1-2 Lazy Initialization with synchronized (Thread-safe)

public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton() {}

    // Lazy Initailization
    public static synchronzied Singleton getInstance() {
      if(uniqueInstance == null) {
         uniqueInstance = new Singleton();
      }
      return uniqueInstance;
    }
}

synchronized를 통해 다른 thread들의 접근을 막을 수 있으므로 instance 생성후, 더는 실행되지 않는다. 덕분에 instance가 필요한 시점에 들어온 요청에 의하여 생성되는 동적 바인딩으로 객체를 생성할 수 있다.

그러나 이는 무조건 instance가 생성됐는지, 아닌지를 확인하는 동기화 block을 거쳐야하므로 성능이 떨어진다.(synchronied 키워드를 사용하면 성능이 약 100배 떨어진다한다..!)

1-3 Lazy Initialization. Double Checking Locking(DCL, Thread-safe)

public class Singleton {
    private volatile static Singleton uniqueInstance;

    private Singleton() {}

    // Lazy Initialization. DCL
    public static Singleton getInstance() {
      if(uniqueInstance == null) {
         synchronized(Singleton.class) {
            if(uniqueInstance == null) {
               uniqueInstance = new Singleton(); 
            }
         }
      }
      return uniqueInstance;
    }
}

1-2의 단점을 보완하여 instance가 생성되지 않았을때만 동기화 블럭을 실행한다.

multi-thread 환경에서는 기본적으로 변수 값을 읽어올때, CPU cache에 저장하여 이를 읽어오나, thread마다 읽어오는 cache가 다르므로 변수값 불일치가 발생한다.

따라서 volatile로 instance를 정의하게되면, cache가 아닌 main memory에 저장 후 읽어오므로 불일치가 발생하지 않는다. 물론 cache가 아닌 main memory에서 읽어야하므로 성능저하가 있긴 하겠지만, 1-2의 동기화 블럭을 모든 thread들이 실행하는 것보다는 성능상 유리하다.

1-4 Lazy Initailization. Enum(Thread-safe)

public enum Singleton {
    INSTANCE; 
}

기본적으로 Enum instance의 생성은 Thread-safe하다. 따라서 코드가 간결해진다. 그러나 enum class 내의 다른 method들이 thread-safe한지는 개발자가 책임져야한다.

그러나 만들려는 Singleton이 Enum 외의 class를 상속한다면 사용할 수 없는 기법이다.

1-5 Lazy Initialization. LazyHolder(Thread-safe)

public class Singleton {

    private Singleton() {}

    /**
     * static member class
     * 내부클래스에서 static변수를 선언해야하는 경우 static 내부 클래스를 선언해야만 한다.
     * static 멤버, 특히 static 메서드에서 사용될 목적으로 선언
     */
    private static class InnerInstanceClazz {
        // 클래스 로딩 시점에서 생성
        private static final Singleton uniqueInstance = new Singleton();
    }

    public static Singleton getInstance() {
        return InnerInstanceClazz.uniqueInstance;
    }
    
}

1-1에서 살펴봤듯이 static 변수로 생성하면 정적 바인딩을 통해 다시 생성될일이 없으므로 깔끔하게 thread-safe가 가능하다. 그러나 lazy loading을 지원하기위해 생성 함수를 동적 바인딩으로 만드는 것이 문제였다.

위 코드에서는 생성함수의 호출이 class가 필요한 시점에 static class를 호출하며 이뤄지기에 lazy loading을 지원한다. 또한 static 변수를 final을 통해 재할당하지 않음으로써 Singleton을 보장할 수 있다.

위 방법의 경우 volatile이나 synchronized 없이 concurrency를 해결할 수 있어 성능이 뛰어나다.

2. Spring에서의 Singleton 패턴

  • Spring에서 Singleton을 저장하고 관리하는 객체가 applicationContext 이다.(IoC Container, Spring container, Bean Factory 등으로 불림) 이 객체에 직접적으로 접근할 일은 없지만, 필요하다면 여기를 참고바란다.
  • Spring은 왜 Singleton으로 Bean을 생성할까?
    • Spring에서는 하나의 요청을 처리하기까지 Presentation Layer, Business Layer, Data Access Layer 등 기능별 다양한 layer들을 거쳐야하므로 각 로직을 담당하는 객체를 layer마다 만들어 사용한다면, GC가 있더라도 메모리 부하가 올 수 있기 때문이다.
  • 더 자세한 내용은 여기를 참고바란다.

2-1 Singleton Beans

원래라면 한 application에서 Singleton Object는 global하게 유일해야하지만, Spring에서는 약간 완화하여 Spring IoC Container당 하나의 Singleton Object 갖도록 제한한다. 즉, 같인 class의 object가 container가 여러개라면, Singleton Object도 여러개 존재할 수 있다.

Spring에서는 모든 bean을 singleton으로 생성하는것이 default이다.

2-2 Autowired Singletons

단일 Application Context내의 두 Controller에는 동일한 Bean을 주입할 수 있다.

@RestController
public class LibraryController {
    
    @Autowired
    private BookRepository repository;

    @GetMapping("/count")
    public Long findCount() {
        System.out.println(repository);
        return repository.count();
    }
}
@RestController
public class BookController {
     
    @Autowired
    private BookRepository repository;
 
    @GetMapping("/book/{id}")
    public Book findById(@PathVariable long id) {
        System.out.println(repository);
        return repository.findById(id).get();
    }
}

위와 같이 2개의 controller는 Context내의 동일한 Bean을 주입받기에 아래와 같이 동일한 repository object임을 확인할 수 있다.

com.baeldung.spring.patterns.singleton.BookRepository@3ea9524f
com.baeldung.spring.patterns.singleton.BookRepository@3ea9524f

2️⃣ Factory 패턴

  • 객체 생성을 sub class에 위임하여 capsulation.

    위와 같이 객체의 생성하기위해 이에 대한 추상 메서드가 있는 factory class를 통해 생성하는 방식이다.
  • 객체 생성 로직을 분리함으로써 client 코드는 객체 생성 방법 알 필요없이 생성 요청이 가능하다. 따라서 코드의 가독성, 유지보수성을 높인다.
  • 객체 생성의 중앙 집중화를 통해 객체 생성의 중복을 방지하고 일관된 방식의 객체 생성이 가능하다. 또한 객체 생성 방식을 변경한다면, client 코드의 수정 없이, factory의 생성 코드만 수정하면 된다.

1. Python 예시

class Character:
    def __init__(self, name):
        self.name = name

class Warrior(Character):
    def __init__(self, name):
        super().__init__(name)
        # Warrior-specific initialization

class Mage(Character):
    def __init__(self, name):
        super().__init__(name)
        # Mage-specific initialization

class CharacterFactory:
    def create_character(self, character_type, name):
        if character_type == "Warrior":
            return Warrior(name)
        elif character_type == "Mage":
            return Mage(name)
        else:
            raise ValueError("Invalid character type")
            
# 클라이언트 코드
factory = CharacterFactory()
character1 = factory.create_character("Warrior", "John")
character2 = factory.create_character("Mage", "Emma")

2. Spring에서의 Factory 패턴

들어가기에 앞서....

  • Spring에서는 Bean Container를 Bean을 생성하는 Factory로 취급.
  • Bean Container의 추상화가 BeanFactory interface.
  • ApplicationContext interface는 BeanFactory interface를 상속.

2-1 Application Context

Spring에서 Factory 패턴은 Dependency Injection(DI)를 위한 Bean을 생성할때 이용된다.

Spring은 아래의 코드와 같이 BeanFactory 추상체의 getBean method를 통해 제공된 기준(name, requiredType, ...)이 일치하는 Bean을 반환한다. getBean method는 내부적으로 코드를 보면 Bean을 가져오는게 아닌, 생성해 반환하는 함수이다.

public interface BeanFactory {

    getBean(Class<T> requiredType);
    getBean(Class<T> requiredType, Object... args);
    getBean(String name);

    // ...
}

그럼 이 BeanFactory의 구현체는 누구일까?

먼저 Application 설정을 다루는 ApplicationContext Interface가 BeanFactory를 상속받는다. 왜냐면 Spring의 XML 이나 Java Annotation과 같은 외부설정을 반영한 Bean을 생성하기 위함이다.

이후 Bean Container를 시작하기 위해 ApplicationContext를 사용해야하는데, 이의 구현체가 AnnotationConfigApplicationContext이며, 상속받았던 BeanFactory의 Factory method(getBean)를 통해 Bean을 생성할 수 있다.

2-2 External Configuration

위에서 언급했듯 Spring이 실행되려면 Application의 설정을 담당하는 ApplicationContext 또한 생성되어야한다. 이때 다른 외부 설정파일에 맞게 ApplicationContext를 생성하는것 또한 Factory 패턴이라 볼 수 있다.

다음과 같이 AnnotationConfigApplicationContext를 ClassPathXmlApplicationContext로 변경할 수 있다.

@Test 
public void givenXmlConfiguration_whenGetPrototypeBean_thenReturnConstructedBean() { 

    String expectedName = "Some name";
    ApplicationContext context = new ClassPathXmlApplicationContext("context.xml");
 
    // Same test as before ...
}

3️⃣ Strategy 패턴

  • 동일 계열의 알고리즘군을 정의하고 capsulation하여 상호교환이 가능하도록 함.

1. Java 예시

public interface Movable {
    public void move();
}

public class Train implements Movable{
    public void move(){
        System.out.println("선로를 통해 이동");
    }
}

public class Bus implements Movable{
    public void move(){
        System.out.println("도로를 통해 이동");
    }
}

public class Client {
    public static void main(String args[]){
        Movable train = new Train();
        Movable bus = new Bus();

        train.move();
        bus.move();
    }
}

위 코드에서 Bus의 move method를 수정해야한다 하자. 이때 Bus의 move 함수 자체를 수정한다면, 기존의 코드를 수정하지않고 행위가 수정되어야 하는 OCP 원칙을 위배하게 된다. 또한 추후 Bus외의 다른 교통수단들이 추가되다가 모두 공통적으로 구현된 move 함수들을 수정해야한다면, 이들을 일일이 수정해야할 뿐더러 method의 중복이 발생하게 된다.

이를 해결하기위해 Strategy 패턴에서는 method의 목적에 따라 전략 class를 capsulation하여 method를 구현한다. 먼저 필요한 메서드를 목적에 맞게 class로 구현한다.

public interface MovableStrategy {
    public void move();
}

public class RailLoadStrategy implements MovableStrategy{
    public void move(){
        System.out.println("선로를 통해 이동");
    }
}

public class LoadStrategy implements MovableStrategy{
    public void move() {
        System.out.println("도로를 통해 이동");
    }
}

그후 다음과 같이 이동에 관한 method를 object마다 직접 구현하지 않고 전략을 구현하여 이를 object마다 set 해주는 방식으로 구현한다.

public class Moving {
    private MovableStrategy movableStrategy;

    public void move(){
        movableStrategy.move();
    }

    public void setMovableStrategy(MovableStrategy movableStrategy){
        this.movableStrategy = movableStrategy;
    }
}
public class Bus extends Moving{

}
public class Train extends Moving{

}

그 결과, client에서는 다음과 같이 move()를 기존 object의 method를 수정하지 않고 전략을 수정하는 방식으로 구현할 수 있다.

public class Client {
    public static void main(String args[]){
        Moving train = new Train();
        Moving bus = new Bus();

        /*
            기존의 기차와 버스의 이동 방식
            1) 기차 - 선로
            2) 버스 - 도로
         */
        train.setMovableStrategy(new RailLoadStrategy());
        bus.setMovableStrategy(new LoadStrategy());

        train.move();
        bus.move();

        /*
            선로를 따라 움직이는 버스가 개발
         */
        bus.setMovableStrategy(new RailLoadStrategy());
        bus.move();
    }
}

😇 느낀점

  • 이론을 인터넷 블로그들로만 정리해서 한계가 있다. 아직 책이 배송되려면 멀었기에 당분간은 공식문서의 코드위주로 정리해야겠다.
  • 첫 글작성에 거의 6시간이 걸렸는데, CS 전공지식이 부족하여 디자인 패턴 외에 여러 부수개념들을 찾아보느라 애먹었지만, 그만큼 많은 것들을 얻어갈 수 있었다.
  • Spring의 Bean의 생성에 대해 알 수 있었다. 주입되는 로직은 그냥 가져오는거니 간단(?)하지 않을까?!

출처

https://velog.io/@dongvelop/Java-클래스-객체-인스턴스의-차이
https://developercc.tistory.com/17
https://codedragon.tistory.com/8988
https://4z7l.github.io/2020/12/25/design_pattern_GoF.html
https://loginfo.tistory.com/2
https://www.baeldung.com/spring-framework-design-patterns
https://devmoony.tistory.com/43
https://jsonobject.tistory.com/131
https://medium.com/webeveloper/싱글턴-패턴-singleton-pattern-db75ed29c36
https://victorydntmd.tistory.com/292

profile
블로그 이전했습니다. https://devsan.tistory.com/

0개의 댓글