log.info

Spring Framework의 디자인 패턴들 본문

Spring Framework

Spring Framework의 디자인 패턴들

log.info 2021. 9. 14. 17:32

(본 포스트는 Design Patterns in the Spring Framework | Baeldung을 번역하였습니다)

 


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

 

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

 

 이번 포스트에서는 Spring Framework에서 가장 자주 사용되는 네 가지 디자인 패턴을 다룹니다.

  • Singleton pattern
  • Factory Method pattern
  • Proxy pattern
  • Template pattern

1. Singleton pattern

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

1-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에서는 같은 클래스의 객체가 여러개 존재할 수 있습니다.

by https://www.baeldung.com/spring-framework-design-patterns

1-2. Autowired Singletons

기본적으로, 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이 LibraryControllerBookController에 같은 BookRepository Bean을 주입했다는 것을 증명합니다.

 

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

ex) @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE

 

 

(Spring Application에서 여러 Spring Container를 사용하는 예시? Spring Batch's Scope?)


2. Factory Method pattern

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

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

 

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

by https://www.baeldung.com/spring-framework-design-patterns

2-1. Application Context

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(...) 메서드는 팩토리 메서드로 간주되어 메서드에 제공된 기준(name, requiredType, ...)과 일치하는 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-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 ...
}

3. Proxy pattern

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

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

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

3-1. Transactions

 Proxy를 생성하기 위해 Subject와 동일한 Interface를 구현하고 Subject에 대한 참조를 포함하는 객체를 생성합니다. 이로써 Subject 대신 Proxy를 사용할 수 있게 됩니다.

 

 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)을 보장할 수 없습니다.

3-2. CGLib Proxies

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

 

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

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

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

 

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

sms


4. Template pattern

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

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

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

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

4-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을 사용하는 접근 방식입니다.

4-2. JdbcTemplate

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...
}

ResultSetExtractorResultSet객체를 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들을 제공합니다


The code from this article can be found over on GitHub.

'Spring Framework' 카테고리의 다른 글

Spring Data  (0) 2021.09.14