본문 바로가기
BackEnd/Spring

스프링의 템플릿/콜백 패턴

by 규난 2023. 4. 2.
728x90

이번 포스트에서는 스프링에서 자주 등장하는 템프릿/콜백 패턴에 대해서 알아보도록 하겠습니다.

 

템플릿/콜백 패턴을 설명하기 전에 확장에는 자유롭게 열려있고 변경에는 굳게 닫혀 있다는 객체지향 설계의 핵심 원리인 개방 폐쇄 원칙(OCP)을 다시 한번 생각해 봅시다. 이 원칙은 코드에서 어떤 부분은 변경을 통해 그 기능이 다양해지고 확장하려는 성질이 가지고, 어떤 부분은 고정이 되어있어 변하지 않으려는 성질을 가지고 있습니다. 변화의 특성이 다른 부분을 구분 해주고(변하는 것과 변하지 않는 것), 각각 다른 목적과 다른 이유에 의해 다른 시점에 독립적으로 변경될 수 있는 효율적인 구조를 만들어주는 것이 개방 폐쇄 원칙입니다.

 

템플릿/콜백 패턴은 이렇게 바뀌는 성질이 다른 코드 중 거의 변경이 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분(템플릿)을 자유롭게 변경되는 성질을 가진 부분(콜백)으로부터 독립시켜서 효과적으로 활용할 수 있도록 하는 패턴입니다. 디자인 패턴 중 전략 패턴과 굉장히 비슷하지만 다른 부분이 있습니다. 다른 부분은 밑에서 설명하도록 하고 이제 부터 코드로 변하지 않는 것과 변하는 것을 분리하도록 하겠습니다.

 

변하지 않는 것과 변하는 것 분리

밑의 코드는 java 7 버전부터 쓸 수 있는 try-with-resource 보다 조금 더 향상된 java 9의 try-with-resource를 사용한 리소스를 반납하는 코드입니다. 이 코드는 데이터 베이스의 커넥션을 가져오고 쿼리를 만들고 실행 후 사용한 자원을 잘 반납하는 문제없는 코드입니다.

하지만 UserDao의 메소드를 만들 때마다 한 가지 아쉬운 점이 있는데 바로 데이터 베이스의 커넥션을 가져오고 사용한 자원에 대해서 반납하는 코드가 중복된다는 점입니다. UserDao 말고도 다른 역할을 하는 Dao가 생겼을 때도 이 부분은 중복이 될 것입니다.

public void deleteAll() throws ClassNotFoundException, SQLException {
    try(Connection connection = connectionMaker.makeConnection();
    	PreparedStatement ps = connection.prepareStatement("delete from users")) {
        ps.executeUpdate();
    } catch (SQLException e) {
        throw e;
    }
}

위 코드에서 변하지 않는 것과 변하는 것을 찾아보자면 변하지 않는 코드는 데이터 베이스의 커넥션을 가져오고 자원을 반납하는 부분과 

변하는 코드는 쿼리를 만들고 생성하는 부분입니다. 이렇게 변하는 것과 변하지 않는 것을 분리해서 템플릿/콜백 패턴을 적용하면 개방 폐쇄 원칙을 지키면서 유연하고 확장성이 뛰어난 코드로 만들 수 있게 됩니다.

템플릿/콜백 패턴 적용

밑의 workWithStatementStrategy 메소드를 보시면

PreparedStatement ps = statementStrategy.makePreparedStatement(connection) 이 부분이 핵심입니다.

UserDao의 deleteAll 메소드와 동일해 보이지만 workWithStatementStrategy 메소드는 데이터 베이스의 커넥션과 만들어진 쿼리를 실행하는 부분과 자원을 반납하는 변하지 않은 부분은 템플릿으로 고정을 시키고 쿼리를 생성하는 부분(콜백)은 유연하게 확장이 가능하도록 인터페이스를 호출하고 있습니다.

package one;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class JdbcContext {

    private ConnectionMaker connectionMaker;

    public void setConnection(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }

    public void workWithStatementStrategy(StatementStrategy statementStrategy) throws ClassNotFoundException, SQLException {
      try(Connection connection = connectionMaker.makeConnection();
            PreparedStatement ps = statementStrategy.makePreparedStatement(connection)) {

            ps.executeUpdate();

        } catch (SQLException e) {
            throw e;
        }
    }

}

package one;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public interface StatementStrategy {

    PreparedStatement makePreparedStatement(Connection connection) throws SQLException;
}

그리고 클라이언트 역할을 하는 deleteAll()메소드를 보시면 템플릿 메소드 안에서 즉, jdbcContext의 workWithStatementStrategy 메소드 안에서 실행될 로직을 담은 콜백 오브젝트를 익명 내부 클래스로 생성해서 템플릿 메소드의 파라미터로 넣어주고 있습니다.

package one;

import org.springframework.util.ObjectUtils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class UserDao {

    private JdbcContext jdbcContext;
    
    public UserDao(ConnectionMaker connectionMaker) {
        this.jdbcContext = new JdbcContext();
        this.jdbcContext.setConnection(connectionMaker);
    }
    
    public void deleteAll() throws ClassNotFoundException, SQLException {
        this.jdbcContext.workWithStatementStrategy(
                new StatementStrategy() {
                    @Override
                    public PreparedStatement makePreparedStatement(Connection connection) throws SQLException {
                        return connection.prepareStatement("delete from users");
                    }
                }
        );
    }
}

템플릿은 클라이언트에 의해 호출이 되면 정해진 작업 흐름을 따라 작업을 진행하다가 콜백 오브젝트의 메소드가 참조할 참조 정보를 생성 후(위 예제에서는 connetion을 말함) 콜백 오브젝트의 메소드를 호출합니다. 그리고 콜백 오브젝트의 메소드는 템플릿이 제공한 참조 정보와 콜백 오브젝트를 생성한 메소드의 정보(deleteAll에는 없지만 메소드의 파라미터나 로컬 변수를 말함)를 이용해서 작업을 수행하고 그 결과를 다시 템플릿에게 돌려주게 됩니다.

 

이렇게 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식을 스프링에서는 템플릿/콜백 패턴이라고 부릅니다.

 

전략 패턴과 템플릿/콜백 패턴의 차이점과 템플릿/콜백 패턴의 특징

여러 개의 메소드를 가진 일반적인 인터페이스를 사용할 수 있는 전략 패턴과 달리 보통 단일 메소드 인터페이스를 사용하는 것이 전략 패턴과 템플릿/콜백 패턴의 차이입니다. 또 한 템플릿/콜백 패턴은 매번 템플릿 메소드 안에서 사용할 콜백 오브젝트를 익명 내부 클래스로 새롭게 생성해서 전달받고 콜백 오브젝트는 익명 내부 클래스로서 자신을 생성한 클라이언트 메소드 내의 정보를 직접 참조할 수 있다는 것이 특징입니다.

 

좋은 코드로 리팩토링 하는 법

  1. 고정된 작업 흐름을 갖고 반복되는 코드가 있다면 반복되는 코드를 먼저 메소로 간단하게 분리하기.(메소드 추출 법)
  2. 그중 일부 작업을 필요에 따라 바꾸어야 한다면 인터페이스를 사이에 두고 분리하는 방법이 전략 패턴을 적용하고 DI로 의존 관계를 관리하도록 하기.
  3. 마지막으로 바뀌는 부분이 한 애플리케이션에서 동시에 여러 종류가 만들어질 수 있다면 템플릿/콜백 패턴 적용을 고려해 보기.(인터페이스의 구현체가 많아지게 되면 그만큼 클래스 파일의 개수가 늘어나게 되고 콜백 오브젝트 메소드에서 클라이언트 메소드의 정보를 참조해야 하는 경우 구현체에 생성자와 멤버 변수를 만들어야 하는 번거로움이 있기 때문인 것 같습니다.)

이번에 토비의 스프링 3.1 Vol 1의 3장을 읽으면서 다시 한번 스프링은 너무너무 어렵다고 느꼈습니다...

점점 이해하는 속도가 느려지기도 하고 이해를 해도 내가 실무에서 이런 패턴들을 적용하려면 공부를 얼마나 더 많이 해야 가능할까??라는 생각도 들기도 하였습니다. 지금 당장은 스프링이 어떻게 만들어졌고 내부적으로 어떻게 돌아가는지 몰라도 쓰는데 문제는 없지만 제 성격상 그러기도 힘들고 스프링이 재미있기도 하고 잘 하고 싶기 때문에 끝까지 포기하지 않고 달려보겠습니다!

 

출처 - 토비의 스프링 3.1 Vol 1 스프링의 이해와 원리

728x90