티스토리 뷰

템플릿

객체지향 설계의 핵심 원칙인 개방 폐쇄 원칙(OCP)를 생각해보자. 이 원칙은 코드에서 어떤 부분은 변경을 통해 그 기능이 다양해지고 확장하려는 성질이 있고, 어떤 부분은 고정되어 있고 변하지 않으려는 성질이 있음을 말해준다.

템플릿이란 이렇게 바뀌는 성질이 다른 코드 중에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로부터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법이다.

예외처리 기능을 갖춘 DAO

DB Connection이라는 제한적인 리소스를 공유해 사용하는 서버에서 동작하는 JDBC 코드에는 반드시 지켜야 할 원칙이 있다. 바로 예외 처리다. 정상적인 JDBC 코드의 흐름을 따르지 않고 중간에 어떤 이유로든 예외가 발생했을 경우에도 사용한 리소스를 반드시 반환하도록 만들어야 하기 때문이다.

리소스 반환과 close()

JDBC의 Connection이나 PreparedStatement에는 close() 메소드가 있다. 단순하게 생각하면 만들어진 걸 종료하는 것이라고 볼 수도 있지만 보통 리소스를 반환한다는 의미로 이해하는 것이 좋다. Connection과 PreparedStatement는 보통 풀(pool) 방식으로 운영된다. 미리 정해진 풀 안에 제한된 수의 리소스를 만들어 두고 필요할 때 이를 할당하고, 반환하면 다시 풀에 넣는 방식으로 운영된다. close() 메소드는 사용한 리소스를 풀로 다시 돌려주는 역할을 한다.

try/catch/finally 구문을 활용해 예외 처리를 적용했지만, 아직 아쉬운 점이 남아 있다.

전략 패턴 / DI의 적용

deleteAll()이라는 메소드가 하는 일은 다음과 같다.

  • DB Connection 가져오기
  • PreparedStatement를 만들어줄 외부 기능 호출하기
  • 전달받은 PreparedStatement 실행하기
  • 예외가 발생하면 이를 다시 메소드 밖으로 던지기
  • 모든 경우에 만들어진 PreparedStatement와 Connection을 적절히 닫아주기

PreparedStatement를 상황에 맞게 만들어주는 작업을 제외하면 나머지 작업은 모든 컨텍스트가 동일하다. 두 번째 작업에서 사용하는 PreparedStatement를 만들어주는 외부 기능이 바로 전략 패턴에서 말하는 전략이라고 볼 수 있다. 이를 DI와 연결시켜서, 파라미터로 상황에 따라 변하는 전략을 넘겨주도록 하여 변하지 않는 컨텍스트를 고정할 수 있다.

public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dateSource.getConnection();

        ps = stmt.makePreparedStatement(c); // 파라미터로 받은 전략 사용

        ps.executeUpdate();
    } catch (SQLException e) {
        throw e;
    } finally {
        // 리소스 반환 작업
    }
}

익명 내부 클래스의 적용

전략 패턴을 적용한 지금까지의 구조에는 아직 두 가지 불만이 남아있다. 첫 번째는 DAO 메소드마다 전략 인터페이스인 StatementStrategy를 구현한 구현 클래스를 매번 만들어주어야 한다는 점, 두 번째는 DAO 메소드에서 StatementStrategy에 전달할 User 같은 데이터가 존재하는 경우, 이를 위해 오브젝트를 전달받는 생성자와 이를 저장할 인스턴스 변수를 번거롭게 만들어야 한다는 점이다.

메소드 내에 로컬 클래스를 선언해서 사용하면 인스턴스의 생성을 하지 않아도 된다.

중첩 클래스의 종류

다른 클래스 내부에 정의되는 클래스를 중첩 클래스(nested class) 라고 한다. 중첩 클래스는 독립적으로 오브젝트로 만들어질 수 있는 스태틱 클래스(static class) 와 자신이 정의된 클래스의 오브젝트 안에서만 만들어질 수 있는 내부 클래스(inner class) 로 구분된다.
내부 클래스는 다시 범위에 따라 세 가지로 구분된다. 멤버 필드처럼 오브젝트 레벨에 정의되는 멤버 내부 클래스(member inner class) 와 메소드 레벨에 정의되는 로컬 클래스(local class), 그리고 이름을 갖지 않는 익명 내부 클래스(anonymous inner class) 다.

조금 더 나아가서 다음과 같이 익명 내부 클래스로 선언할 수 있다.

public void add(final User user) throws SQLException {
    jdbcContextWithStatementStrategy(
            new StatementStrategy() { // 익명 내부 클래스
                public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                    PreparedStatement ps = c.preparedStatement("INSERT INTO users(id, name, password) VALUES(?,?,?)");
                    ps.setString(1, user.getId());
                    ps.setString(2, user.getName());
                    ps.setString(3, user.getPassword());

                    return ps;
                }
            }
    );
}

익명 내부 클래스

익명 내부 클래스는 이름을 갖지 않는 클래스다. 클래스 선언과 오브젝트 생성이 결합된 형태로 만들어지며, 상속할 클래스나 구현할 인터페이스를 생성자 대신 사용해서 다음과 같이 new 인터페이스 이름() { 클래스 본문 };의 형태로 만들어 사용한다. 클래스를 재사용할 필요가 없고, 구현한 인터페이스 타입으로만 사용할 경우에 유용하다. 익명 내부 클래스는 자바8에서 더 간단한 람다식으로 변환하여 사용할 수 있다.

템플릿/콜백

지금까지 진행했던 코드는, 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고 그중 일부분만 자주 바꿔서 사용해야 하는 경우에 적합한 구조다. 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식이다. 이런 방식을 템플릿/콜백 패턴이라고 한다. 전략 패턴의 컨텍스트를 템플릿이라 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 부른다.

템플릿

템플릿은 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀을 가리킨다. 프로그래밍에서는 고정된 틀 안에 바꿀 수 있는 부분을 넣어서 사용하는 경우에 템플릿이라고 부른다.

콜백

콜백(callback)은 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트를 말한다. 파라미터로 전달되지만 값을 참조하기 위한 것이 아니라 특정 로직을 담은 메소드를 실행시키기 위해 사용한다. 펑셔널 오브젝트(functional object)라고도 한다.

여러 개의 메소드를 가진 일반적인 인터페이스를 사용할 수 있는 전략 패턴의 전략과 달리 템플릿/콜백 패턴의 콜백은 보통 단일 메소드 인터페이스를 사용한다. 콜백은 일반적으로 하나의 메소드를 가진 인터페이스를 구현한 익명 내부 클래스로 만들어진다.

템플릿/콜백 방식에서는 매번 메소드 단위로 사용할 오브젝트를 새롭게 전달받는다는 것이 특징이다. 콜백 오브젝트가 내부 클래스로서 자신을 생성한 클라이언트 메소드 내의 정보를 직접 참조한다는 것도 템플릿/콜백의 고유한 특징이다. 클라이언트와 콜백이 강하게 결합된다는 면에서도 일반적인 DI와 조금 다르다.

템플릿과 콜백을 찾아낼 때는, 변하는 코드의 경계를 찾고 그 경계를 사이에 두고 주고받는 일정한 정보가 있는지 확인하면 된다.

스프링의 많은 API나 기능을 살펴보면 템플릿/콜백 패턴을 적용한 경우를 많이 발견할 수 있다. 스프링을 사용하는 개발자라면 당연히 스프링이 제공하는 템플릿/콜백 기능을 잘 사용할 수 있어야 한다. 동시에 템플릿/콜백이 필요한 곳이 있으면 직접 만들어서 사용할 줄도 알아야 한다. 고정된 작업 흐름을 갖고 있으면서 여기저기서 자주 반복되는 코드가 있다면, 중복되는 코드를 분리할 방법을 생각해보는 습관을 기르자.


참고

토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리