Domain/Spring

[Spring] 템플릿-콜백

by Donghwan 2021. 7. 23.

Spring은 DB 연결과 관련된 상황에서 예외처리에 대한 상황에서 심각한 문제점을 가지고 있었습니다. JDBC 코드의 흐름을 따르지 않고 중간에 어떤 이유로든 예외가 발생했을 경우에도 사용한 리소스를 반드시 반환하도록 만들어야 하기 때문입니다. 그렇지 않으면 시스템에 심각한 문제를 일으킬 수 있습니다. 이러한 부분을 해결하기 위해 사용하는 것이 JdbcTemplate입니다. JdbcTemplate는 스프링이 제공하는 템플릿-콜백 기술입니다.

기본적으로 Spring에서는 DB에 접근할 때, Connection과 PreparedStatement 두개의 공유 리소스를 이용하여 처리를 하게 됩니다. 이때, 사용을 마친 후에 close()를 통해 접속을 끊어주어야 합니다. 

  public void sample(String query) throws SQLException {
    Connection conn = dataSource.getConnection();

    PreparedStatement ps = conn.prepareStatement(query);
    ps.executeUpdate();

    ps.close();
    conn.close();
  }

하지만 PreparedStatement를 실행하는 과정에서 Exception이 발생하게 된다면 sample() 메소드가 끝까지 실행되지 못하고 종료됩니다. 이때 close() 메소드가 미처 실행되지 못하여 리소스가 반환되지 못하게 되는데, 일반적으로 서버에서는 제한된 개수의 DB 커넥션을 만들어서 재사용 가능한 풀로 관리합니다. DB 풀은 매번 getConnection()으로 가져간 커넥션을 명시적으로 close()해서 돌려줘야지만 다시 풀에 넣었다가 다음 커넥션 요청이 있을 때 재사용할 수 있습니다. 그런데 이런 식으로 오류가 날 때마다 미처 반환되지 못한 Connection이 계속 쌓이면 어느 순간에 커넥션 풀에 여유가 없어지고 리소스가 모자란다는 심각한 오류를 내며 서버가 중단될 수 있습니다. 따라서 위의 코드는 한 번 실행되고 애플리케이션 전체가 종료되는 간단한 예제에서는 괜찮겠지만, 장시간 운영되는 다중 사용자를 위한 서버에 적용하기에는 치명적인 위험을 내포하고 있습니다.

그래서 이런 JDBC 코드에서는 어떤 상황에서도 가져온 리소스를 반환하도록 try/catch/finally 구문 사용을 권장하고 있습니다. 하지만 이러한 코드는 모든 예외에 대해 처리하게 되고 상황에 따라 매우 복잡해지기도 합니다. 

public void sample(String query) throws SQLException {
    Connection conn = null;
    PreparedStatement ps = null;

    try {
      conn = dataSource.getConnection();
      ps = conn.prepareStatement(query);
      ps.executeUpdate();
    } catch (SQLException e) {
      throw e;
    } finally {
      if(ps != null) {
        try {
          ps.close();
        } catch (SQLException e) {
          //...
        }
      }

      if(conn != null) {
        try {
          conn.close();
        } catch (SQLException e) {
          //...
        }
      }
    }
  }

이렇게 코드를 작성하게 된다면 finally 블록의 close() 라인 하나 빼먹은 것과 같은 실수를 했어도 테스트를 돌려보면 별문제가 없어 보입니다. 그러나 해당 메소드가 호출되고 나면 커 넥션이 하나씩 반환되지 않고 쌓여가게 됩니다.

이런 문제를 해결하기 위해 고정적인 부분과 변화가 있는 부분을 분리하여 재사용성을 높이는 방법으로 개선을 시도하게 됩니다. 가장 간단한 메소드로 추출하여 사용하는 방법, 템플릿 메소드 패턴을 사용하는 방법, 전략 패턴을 사용하는 방법이 있습니다.

메소드 추출 방법은 재사용성이 떨어지고, 템플릿 메소드 패턴은 메소드마다 서브 클래스를 만들어야 한다는 단점과 확장구조가 이미 클래스를 설계하는 시점에서 고정되어 버린다는 단점 그리고 서브클래스들이 이미 클래스 레벨에서 컴파일 시점에 이미 그 관계가 결정되어 있기 때문에 관계에 대한 유연성이 떨어져 버린다는 단점이 있습니다. 그렇기 때문에 템플릿/콜백을 사용하여 앞선 문제를 효율적으로 해결할 수 있습니다. 

public interface Strategy {
    PreparedStatement createStatement(Connection conn) throws SQLException;
}

public class SampleTemplate {
    private DataSource dataSource;

    public SampleTemplate(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void executeSql(String query) throws SQLException {
        strategy(new Strategy() {
            @Override
            public PreparedStatement createStatement(Connection conn) throws SQLException {
                return conn.prepareStatement(query);
            }
        });
    }

    public void strategy(Strategy strategy) throws SQLException {
        Connection conn = null;
        PreparedStatement ps = null;

        try {
            conn = dataSource.getConnection();
            ps = strategy.createStatement(conn);
            ps.executeUpdate();
        } catch (SQLException e) {
            throw e;
        } finally {
            if(ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {

                }
            }

            if(conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {

                }
            }
        }
    }
}

public class DAO {
    private SampleTemplate sampleTemplate;

    public void setSampleTemplate(SampleTemplate sampleTemplate) {
        this.sampleTemplate = sampleTemplate;
    }

    public void add() throws SQLException {
        this.sampleTemplate.executeSql("");
    }
}

이러한 형태는 Spring에서 제공하는 JdbcTemplate의 기본형태라고 볼 수 있습니다.


출처

  • 토비의 스프링 3.1
728x90
반응형

댓글