Programming/Spring

[스프링] 디자인 패턴

bu119 2023. 7. 2. 19:30
728x90
반응형

디자인 패턴의 개요

여러 사람이 협업을 하며 프로그램을 개발 할 때 다른 사람이 작성한 코드나 기존의 코드를 이해하는 것은 어렵다. 이런 코드들은 유지 보수를 함에 있어서 새로운 기능을 추가하거나 최적화를 하기에 힘든 구조적인 결함을 가지고 있다. 이를 최소화하기 위해서 디자인 패턴은 등장했다. 특수한 경우의 프로그래머가 아니라면 프로그래밍을 하면서 만나는 문제가 지구상에서 유일한 문제일 경우는 거의 없기 때문에 앞서 이미 많은 수많은 오류들을 피하는 방법을 패턴화하게 된다면 좀더 버그가 발생하는 폭을 줄이고 수정하는데(발생하는 숫자가 아니라 예외를 찾는데 조금더 덜 시간을 사용하게 됨) 최소한의 시간을 들일 수 있게 된다.

즉, 디자인 패턴은 프로그램 등을 개발하는 중에 발생했던 문제점들을 정리 및 특정한 ‘규약’을 통해 좀 더 쉽고 편리하게 쓸 수 있는 형태로 만든 것이다.

패러다임이나 알고리즘과는 다르며 오히려 코딩 방법론이나 코딩컨벤션에 가깝다고 볼 수 있다.

쉽게 말하자면 디자인 패턴은 이미 발견된 오류를 피해 깔려있는 포장 도로와도 같다고 할 수 있다. 우리는 이 포장된 도로를 이용하면서 오류를 최소화하고 시간과 예산 또한 줄일 수 있다.

 

디자인 패턴이란?

디자인 패턴은 소프트웨어 개발의 필수적인 부분이다. 디자인패턴은 반복되는 문제를 해결해주며, 개발자로 하여금 자주 사용되는 패턴을 통해 프레임워크의 설계를 이해하도록 도움을 준다.

Spring Framework에서는 여러 디자인 패턴을 적용하여 개발자의 부담을 덜어주고, 지루한 작업을 빠르게 수행할 수 있도록 도와준다.

 

디자인 패턴의 종류

디자인 패턴의 종류는 너무나도 많다. 디자인 패턴을 구체화하고 체계화한 GoF에 의해 디자인 패턴을 각각 생성, 구조, 행위 패턴으로 구분하고 그 하위에 옵저버, 프로토타입, 싱글톤, 프록시, 커맨드, 템플릿 메서드 등 다양한 패턴들이 포함되어 있다.

 

Spring에서 가장 많이 사용되는 패턴은 크게 네 가지이다.

  • 싱글톤 패턴 (Singleton pattern)
  • 팩토리 메소드 패턴 (Factory Method pattern)
  • 프록시 패턴 (Proxy pattern)
  • 템플릿 메소드 패턴 (Template Method Pattern)

 

 

싱글턴 패턴 (Singleton pattern)

싱글턴 패턴이란 클래스 인스턴스가 하나만 만들어지도록 하고, 그 인스턴스에 대한 전역 접근을 제공한다.
즉, 싱글턴 패턴을 정의한 클래스엔 인스턴스가 최대 1개까지만 생성되고, 어디서든지 이 인스턴스에 접근이 가능하다는 이야기이다. 따라서, 구현할 때는 private 생성자와, 정적 메소드, 정적 변수만을 사용한다.

 

Singleton pattern은 어플리케이션당 오직 하나의 인스턴스만 존재하도록 보장해주는 패턴이다. 공유 자원을 관리하거나 cross-cutting services(e.g. logging)를 제공할 때 유용하다.

 

즉, 싱글턴 패턴(Singleton pattern)을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다.

 

그렇다면 대체 왜 전역 인스턴스 하나에 메모리를 할당하여 사용할까?

안드로이드 앱 같은 경우에는 각 액티비티나 클래스 별로 주요 클래스들을 일일이 전달하기가 번거롭기 때문에 싱글톤 클래스를 만들어서 어디서나 접근하도록 설계하는 것이 작업하기 더 편하기 때문에 싱글톤 패턴을 자주 사용한다. 또 DBCP(DataBase Connection Pool)처럼 공통된 객체를 여러개 생성해서 사용해야하는 상황에서 많이 사용된다.

캐시나 로그 기록, 사용자 설정 같이 자주 사용되는 데이터들을 같이 공유하며 저장해두는 것들을 생각해보면 이해가 쉬울 것이다.

 

싱글턴 패턴의 장점

  • 두 번째 이용시부터는 (이미 인스턴스 내에 가지고 있기 때문에)객체 로딩 시간이 현저하게 줄어 성능이 좋아진다.
  • 싱글톤으로 만들어진 클래스의 인스턴스는 전역 인스턴스이기 때문에 다른 클래스들과 데이터를 공유하기가 쉽다.
  • 인스턴스가 절대적으로 한 개라는 것을 보증한다.

 

싱글턴 패턴의 단점

  • 너무 많은 데이터를 공유할 경우에 인스턴스들 간에 결합도가 높아져 수정이 어려워지고 테스트하기 어려워진다.(객체 지향적인 설계를 지향하기 어려워짐)
  • 멀티쓰레드 환경에서 동기화 처리를 하지 않으면 인스턴스가 두 개가 생성되는 경우가 발생한다.

 

1. 싱글턴 빈 (Singleton Beans)

일반적으로 Singleton Object는 어플리케이션에서 글로벌하게 유일해야하지만, Spring에서는 이러한 제약이 완화된다.  Spring에서는 하나의 Spring IoC Container 당 하나의 Singleton Object를 갖도록 제한한다. 실제로 이것은 Spring Framework가 하나의 Application Context당 하나의 Bean을 생성하는 것을 의미한다.

 

따라서, 엄밀히 따지면, Spring Framework에서는 Application에서 여러 Spring Container를 가질 수 있기 때문에 Singleton의 정의와는 다르다. 즉, 여러 Container를 가진 Application에서는 같은 클래스의 객체가 여러개 존재할 수 있다.

 

 

2.  Autowired 싱글턴 (Autowired  Singletons)

Spring에서 @Autowired 어노테이션을 통해 컨테이너 내 Bean을 주입받아 사용할 수 있다. 이 말인 즉슨 최초 어플리케이션 구동시 Bean을 컨테이너에 등록하고 @Autowired 구문을 찾아 해당 변수에 등록한 Bean을 주입하여 사용한다.

 

여러곳에서 동일한 객체를 autowired 해도 이 둘의 객체 id를 비교하면, 동일한 객체를 주입 받았음을 알 수 있다.

객체 주입 시 새로운 객체를 만들도록 설정하고 싶다면, bean scope을 프로토타입으로 변경해야한다.

 

기본적으로, Spring Framework는 모든 Bean들을 Singleton으로 생성다.

예를 들어, 단일 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();
    }
}

그 후, Application을 실행하고 두 요청을 실행하면,

$ curl -X GET http://localhost:8080/count
$ curl -X GET http://localhost:8080/book/1

아래와 같이 같은 Object ID를 가진 repository 객체를 볼 수 있다.

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

즉, Spring Framework이 LibraryController와 BookController에 같은 BookRepository Bean을 주입했다는 것을 증명한다.

 

이와 다르게, Bean Scope를 singleton에서 prototype으로 변경함으로써 서로 다른 BookRepository Bean을 생성할 수 있다.

ex) @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE

 

 

팩토리 메소드 패턴 (Factory Method pattern)

팩토리 메서드 패턴(Factory method pattern)은 객체지향 디자인 패턴이다. Factory method는 부모(상위) 클래스에 알려지지 않은 구체 클래스를 생성하는 패턴이며. 자식(하위) 클래스가 어떤 객체를 생성할지를 결정하도록 하는 패턴이기도 하다. 부모(상위) 클래스 코드에 구체 클래스 이름을 감추기 위한 방법으로도 사용한다.

 

Factory Method pattern은 원하는 객체를 생성하기 위한 추상 메서드가 있는 팩토리 클래스를 생성한다.

종종 우리는 특정 컨텍스트를 기반으로 다른 객체를 생성한다.

 

예를 들어, 아래와 같이 Vehicle 객체를 생성하고자 할 때, 해상 환경에서는 Boat 객체를, 항공 환경에서는 Airplain 객체를 만들고 싶다고 가정하겠다. 이를 위해, 각 환경별 Factory 구현체를 구현할 수 있고, 구체화한 Factory Method로부터 원하는 객체를 반환할 수 있다.

 

 

팩토리 메소드 패턴의 장점

  • 각 객체를 인터페이스를 통해 하나로 관리 가능
  • 비슷한 객체 추가시 추가 구현 용이
  • 공통코드 무결성 보장

 

팩토리 메소드 패턴 사용법

  • 공통 interface 정의
  • interface 상속 및 Class 구현
  • 구현한 Class 객체 반환용 Factory Class 정의
  • Factory Class를 통해 객체를 받아 사용

 

1. 어플리케이션 컨텍스트 (Application Context)

스프링 부트를 쓰면서 사실 잊고 살기는 하지만, 기본적으로 bean을 주입 받으려면 어플리케이션 컨텍스트 객체의 getBean() 팩토리 메소드를 사용할 수 있다. (ApplicationContext가 BeanFactory를 상속하기 때문에)

이때 어플리케이션 컨텐스트 객체가 팩토리 역할을 한다. 이는 스프링 DI 프레임워크의 근간이다.

 

Spring Framework는 이 기술을 Dependency Injection(DI)에서 사용한다.

기본적으로, Spring Framework는 Bean Container를 Bean을 생성하는 Factory로 취급한다.

따라서, Spring Framework는 BeanFactory Interface를 Bean Container의 추상화로 정의다.

public interface BeanFactory {

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

    // ...
}

각 getBean(...) 메서드는 팩토리 메서드로 간주되어 메서드에 제공된 기준(namerequiredType, ...)과 일치하는 Bean을 반환한다.

 

그 다음 Spring Framework는 BeanFactory를 상속하여 추가적인 Application 설정을 다루는 ApplicationContext Interface를 구현한다. Spring Framework는 XML file 또는 Java Annotation과 같은 일부 외부 설정을 기반으로 Bean Container를 시작하기 위해 ApplicationContext를 사용한다.

 

AnnotationConfigApplicationContext와 같은 ApplicationContext의 구현체를 사용하여, BeanFactory Interface로부터 상속한 다양한 Factory Method를 통해 Bean을 생성할 수 있다.

 

먼저, 간단한 Application Configuration을 생성한다.

@Configuration
@ComponentScan(basePackageClasses = ApplicationConfig.class)
public class ApplicationConfig {
}

다음으로 Foo라는 간단한 클래스를 생성한다.

@Component
public class Foo {
}

그 뒤 Bar라는 다른 클래스를 생성한다.

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Bar {
 
    private String name;
     
    public Bar(String name) {
        this.name = name;
    }
     
    // Getter ...
}

마지막으로, ApplicationContext의 구현체인 AnnotationConfigApplicationContext를 통해 Bean들을 생성한다.

@Test
public void whenGetSimpleBean_thenReturnConstructedBean() {
    
    ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
    
    Foo foo = context.getBean(Foo.class);
    
    assertNotNull(foo);
}

@Test
public void whenGetPrototypeBean_thenReturnConstructedBean() {
    
    String expectedName = "Some name";
    ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
    
    Bar bar = context.getBean(Bar.class, expectedName);
    
    assertNotNull(bar);
    assertThat(bar.getName(), is(expectedName));
}

getBean Factory Method를 사용해, class type과 생성자 파라미터만을 가지고 설정된 Bean들을 생성할 수 있다.

 

2. 외부 설정 (External Configuration)

이러한 Factory Method pattern은 Application의 동작을 외부 설정에 맞게 변경할 수 있기 때문에 다양하게 사용 가능하다.

즉, 외부 설정을 통해서도 완전히 바꿀 수 있다는 점에서 매우 유연하다. 바라보는 설정파일만 달라져도 전혀 다른 방식으로 활용 가능하다는 이야기이다.

 

예를 들어, 우리는 AnnotationConfigApplicationContext을 ClassPathXmlApplicationContext로 변경할 수 있다.

@Test 
public void givenXmlConfiguration_whenGetPrototypeBean_thenReturnConstructedBean() { 

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

 

 

프록시 패턴 (Proxy pattern)

일반적으로 프록시는 다른 무언가와 이어지는 인터페이스의 역할을 하는 클래스이다. 프록시는 어떠한 것(이를테면 네트워크 연결, 메모리 안의 커다란 객체, 파일, 또 복제할 수 없거나 수요가 많은 리소스)과도 인터페이스의 역할을 수행할 수 있다. 하나의 개체(프록시)가 다른 개체(주체 또는 서비스)에 대한 액세스를 제어할 수 있도록 하는 패턴이다.

Proxy pattern은 한 객체(proxy)가 다른 객체(subject or service)로의 접근을 제어하도록 하는 기술이다.

 

 

 

일반적으로, Spring에서는 두 가지 타입의 Proxy를 사용한다.

  • CGLib Proxy - Class들을 프록싱 할 때 사용한다.
  • JDK Dynamic Proxy - Interface들을 프록싱할 때 사용한다.

 

프록시패턴 장점

  • 사이즈가 큰 객체가 로딩되기 전에도 프록시를 통해 참조를 할 수 있다.
  • 실제 객체의 public, protected 메소드를 숨기고 인터페이스를 통해 노출시킬 수 있다.
  • 로컬에 있지 않고 떨어져있는 객체를 사용할 수 있다.
  • 원래 객체에 접근에 대해 사전처리를 할 수 있다.

 

프록시패턴 단점

  • 객체를 생성할 때 한 단계를 거치게 되므로, 빈번한 객체 생성이 필요한 경우 성능이 저하될 수 있다.
  • 프록시 내부에서 객체 생성을 위해 스레드가 생성, 동기화가 구현되어야 하는 경우 성능이 저하될 수 있다.
  • 로직이 난해해져 가독성이 떨어질 수 있다.

 

 프록시 패턴의 종류

  • 가상 프록시
    꼭 필요로 하는 시점까지 객체의 생성을 연기하고, 해당 객체가 생성된 것 처럼 동작하도록 만들고 싶을 때 사용하는 패턴이다. 프록시 클래스에서 작은 단위의 작업을 처리하고 리소스가 많이 요구되는 작업들이 필요할 경우만 주체 클래스를 사용하도록 구현한다.
  • 원격 프록시
    원격 객체에 대한 접근을 제어 로컬 환경에 존재하며, 원격 객체에 대한 대변자 역할을 하는 객체 서로 다른 주소 공간에 있는 객체에 대해 마치 같은 주소 공간에 있는 것 처럼 동작하게 하는 패턴이다.(예: Google Docs)
  • 보호 프록시
    주체 클래스에 대한 접근을 제어하기 위한 경우에 객체에 대한 접근 권한을 제어하거나 객체마다 접근 권한을 달리하고 싶을 경우 사용하는 패턴으로 프록시 클래스에서 클라이언트가 주체 클래스에 대한 접근을 허용할지 말지 결정하도록 할 수 있다.

 

1. @Transactional

 

Proxy를 생성하기 위해 주제(Subject)와 동일한 인터페이스(Interface)를 구현하고 주제(Subject)에 대한 참조를 포함하는 객체를 생성한다. 이로써 Subject 대신 Proxy를 사용할 수 있다.

 

메소드에 @Transactional 어노테이션을 명시해 해당 메소드의 원자성을 보장시키게 한다. 한마디로 메소드 내부에서 오류가 발생하거나 문제가 되는 경우 호출전 상태로 되돌리기 때문에 안정성이 보장된다.

 

Spring Framework에서 Bean들은 Underlying Bean에대한 접근을 제어하기 위해 프록싱된다.

대표적인 예가 Transaction이다.

@Service
public class BookManager {
    
    @Autowired
    private BookRepository repository;

    @Transactional
    public Book create(String author) {
        System.out.println(repository.getClass().getName());
        return repository.create(author);
    }
}

위 BookManager class에서 create(...) 메서드에 @Transactional 어노테이션을 추가했다. @Transactional 어노테이션은 Spring Framework에게 create(...) 메서드를 원자적(Atomacally)으로 실행하도록 지시한다. Proxy 없이는 Spring Framework가 BookRepository Bean에 접근해서 트랜잭션 일관성(Consistency)을 보장할 수 없다.

 

2. CGLib 프록시 (CGLib Proxies)

Code Generator Library의 약자로, 클래스의 바이트코드를 조작하여 Proxy 객체를 생성해주는 라이브러리이다.

 

CGLib Proxies는 Spring Framework가 BookRepository를 감싸고 Bean들에게 create(...) 메서드를 원자적으로 실행하도록 지시하는 Proxy를 생성한다.

즉, 스프링은 CGLib을 통해 프록시를 만들고 bean을 래핑해 메소드가 원자적으로 실행되도록 한다.

 

 BookManager#create를 호출하면 아래와 같은 출력을 볼 수 있다. 

com.baeldung.patterns.proxy.BookRepository$$EnhancerBySpringCGLIB$$3dc2b55c

 일반적으로, 기본 BookRepository의 Object ID가 출력될 것을 기대하지만, 그 대신, EnhancerBySpringCGLIB의 Object ID가 출력되는 것을 볼 수 있다.

 

뒷단에서 Spring Framework는 BookRepository객체를 EnhancerBySpringCGLIB객체로 wrapping한다. 그럼으로써 Spring Framework는 BookRepository객체(트랜잭션 일관성을 보장하는)에 대핸 접근을 제어할 수 있게 된다.

즉, Spring은 CGLib을 사용하여 인터페이스가 아닌 타깃의 클래스에 대해서도 Proxy를 생성한다.

 

 

4. 템플렛 메소드 패턴 (Template Method pattern)

어떤 작업을 처리하는 일부분을 서브 클래스로 캡슐화해 전체 일을 수행하는 구조는 바꾸지 않으면서 특정 단계에서 수행하는 내역을 바꾸는 패턴이다.

 

다수의 프레임워크에는 상당량의 코드가 boilerplate code이다.

예를 들어, DB에서 쿼리를 실행하기 위해선 다음 단계들이 필수적이다.

  1. Connection 생성
  2. 쿼리 실행
  3. cleanup 실행
  4. Connection 종료

이런 단계들은 Template Method pattern에 이상적인 시나리오다.

 

 

템플렛 메소드 패턴의 장점

  • 중복코드를 줄일 수 있다.
  • 자식 클래스의 역할을 줄여 핵심 로직의 관리가 용이하다.
  • 좀더 코드를 객체지향적으로 구성할 수 있다.

 

템플렛 메소드 패턴의 단점

  • 추상 메소드가 많아지면서 클래스 관리가 복잡해진다.
  • 클래스간의 관계와 코드가 꼬여버릴 염려가 있다.

 

1. Templates & Callbacks

Template Method pattern은 일부 작업에 필요한 단계들을 정의하고 boilerplate 단계들을 구현하고 사용자 정의 단계를 추상적으로 남겨두는 기술이다. 그러면 subclass들은 이 추상클래스를 구현하고, 누락된 단계들에 대한 구체적인 구현을 제공할 수 있다.

public abstract DatabaseQuery {

    public void execute() {
        Connection connection = createConnection();
        executeQuery(connection);
        closeConnection(connection);
    } 

    protected Connection createConnection() {
        // Connect to database...
    }

    protected void closeConnection(Connection connection) {
        // Close connection...
    }

    protected abstract void executeQuery(Connection connection);
}

또는 Callback method를 통해 누락된 단계를 제공할 수 있다. Callback method는 원하는 작업이 완료되었음을 클라이언트에 알릴 수 있는 메서드이다.

 

 

예를 들어, executeQuery 메서드를 갖는 대신, execute 메서드에 query와 callback method를 제공하여 결과를 처리할 수 있다.

 

먼저, Result 객체를 받아 T 타입의 객체에 매핑해주는 Callback method를 생성한다.

public interface ResultsMapper<T> {
    public T map(Results results);
}

그 다음, 이 Callback method를 사용하도록 DatabaseQuery를 변경한다.

public abstract DatabaseQuery {

    public <T> T execute(String query, ResultsMapper<T> mapper) {
        Connection connection = createConnection();
        Results results = executeQuery(connection, query);
        closeConnection(connection);
        return mapper.map(results);
    ]

    protected Results executeQuery(Connection connection, String query) {
        // Perform query...
    }
}

이러한 Callback method방식이 정확히 Spring framework가 JdbcTemplate을 사용하는 접근 방식이다.

 

2. JDBC Template

데이터베이스에 질의할때, 아래 단계를 거치게 된다.

JdbcTemplate의 query 메소드는 템플렛 메소드 패턴으로 동작한다.

  1. 커넥션 연결
  2. 쿼리 실행
  3. 클린업
  4. 커넥션 해제

 

JdbcTemplate class는 query(String, ResultSetExtractor) 메서드를 제공한다.

public class JdbcTemplate {

    public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
        // Execute query...
    }

    // Other methods...
}

ResultSetExtractor는 ResultSet객체를 T타입의 도메인 객체로 변환해준다.

@FunctionalInterface
public interface ResultSetExtractor<T> {
    T extractData(ResultSet rs) throws SQLException, DataAccessException;
}

Spring framework는 보다 구체적인 Callback interface를 생성해 boilerplate code를 더욱 줄인다.

예를 들어, RowMapper interface는 단일행의 SQL 데이터를 T 타입의 도메인객체로 변환하는데 사용된다.

@FunctionalInterface
public interface RowMapper<T> {
    T mapRow(ResultSet rs, int rowNum) throws SQLException;
}

RowMapper interface를 예상되는 ResultSetExtractor에 적용하기 위해 Spring framework는 RowMapperResultSetExtractor class를 생성한한다.

public class JdbcTemplate {

    public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
        return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper)));
    }

    // Other methods...
}

 row들을 반복하며 전체 ResultSet 객체를 전환하는 로직을 제공하는 대신, 단일행을 변환하는 방법에 대한 로직을 제공할 수 있다.

public class BookRowMapper implements RowMapper<Book> {

    @Override
    public Book mapRow(ResultSet rs, int rowNum) throws SQLException {

        Book book = new Book();
        
        book.setId(rs.getLong("id"));
        book.setTitle(rs.getString("title"));
        book.setAuthor(rs.getString("author"));
        
        return book;
    }
}

이 converter를 통해 JdbcTemplate을 이용해 DB에 질의하고 각 결과 row를 매핑할 수 있다.

JdbcTemplate template = // create template...
template.query("SELECT * FROM books", new BookRowMapper());

JdbcTemplate 외에도 Spring은 다양한 Template들을 제공다


참고 자료

https://loginfo.tistory.com/2

https://velog.io/@sangmin7648/Spring%EC%9D%98-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EB%93%A4

https://metafor.notion.site/54d624628a634c879cc93d94f54cd2d1#0c4b1ec1ee49475ebebbf3897e5d7818

728x90
반응형