토비의 스프링 (3)


3장 템플릿

확장에는 자유롭게 열려 있고 변경에는 굳게 닫혀 있다는 객체지향 설계의 핵심 원칙인
개방 폐쇄 원칙을 다시 생각해보자.

이 원칙은 코드에서 어떤 부분은 변경을 통해 그 기능이 다양해지고 확장하려는 성질이 있고,
어떤 부분은 고정되어 있어 변하지 않으려는 설징이 있음을 말해준다.

변화의 특성이 다른 부분을 구분해주고, 각각 다른 목적과 다른 이유에 의해 다른 시점에 독립적으로 변경될 수 있는
효율적인 구조를 만들어주는 것이 바로 이 개방 폐쇄 원칙이다.

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


분리와 재상용을 위한 디자인 패턴 적용

다음은 개선할 deleteAll() 메서드이다.

Connection c = null;
PreparedStatement ps = null;
 


try {
    c = dataSource.getConnection();

    ps = c.prepareStatement("delete from users"); // 변하는 부분 

    ps.executeUpdate();
} catch (SQLException e) {
    throw e;
} finally {
    if(ps != null) { try { ps.close(); }  catch (SQLException e) {} }
    if(c != null) { try { c.close(); }  catch (SQLException e) {} }
}

변하는 부분을 재외하면 나머지 코드는 변하지 않는다.

이 로직에 따라 변하는 부분을 변하지 않는 나머지 코드에서 분리하는 것이 어떨까?
변하지 않는 부분을 재사용할 수 있는 부분이 있지 않을까?


메소드 추출

먼저 생각해볼 수 있는 방법은 변하는 부분을 메소드로 빼는 것이다.

자주 바뀌는 부분을 메서드로 독립시키는 것은 별 이득이 없어 보인다.
왜냐면 보통 메소드 추출 리팩토링을 적용하는 경우에는 분리시킨 메서드를 다른 곳에서 재사용할 수 있어야 하는데
반대로 분리시키고 남은 메서드가 재사용이 필요한 부분이기 때문이다.
분리된 메서드는 DAO 로직마다 새롭게 만들어서 확장돼야 하는 부분이기 때문에 뭔가 반대로 됐다.


템플릿 메소드 패턴의 적용

템플릿 메서드 패턴은 상속을 통해 기능을 확장해서 사용하면 된다.
변하는 부분을 추상 메서드로 정의해두고 서브클래스에서 재정의하여 사용하면 된다.

하지만 템플릿 메서드 패턴으로서의 접근은 제한이 많다.

가장 큰 문제는 DAO 로직마다 상속을 통해 새로운 클래스를 만들어야 한다는 점이다.
만약 이런 방식으로 구현한다면 JDBC 메서드가 4개일 경우 4개의 서브 클래스를 만들어서 사용해야 한다.

두 번째 문제는 확장구조가 이미 클래스를 설계하는 시점에서 고정되어 버린다는 점이다.
변하지 않는 코드와 서브 클래스들(변하는 코드)이 이미 클래스 레벨에서 컴파일 시점에 이미 그 관계가 결정되어 있다.
따라서 그 관계에 대한 유연성이 떨어져 버린다.
상속을 통해 확장을 꾀하는 템플릿 패턴의 단점이 고스란히 드러난다.


전략 패턴 사용

개방 폐쇄 원칙을 잘 지키는 구조이면서도 템플릿 메서드 패턴보다 유연하고 확장성이 뛰어난 것이,
오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 전략 패턴이다.

전략 패턴은 OCP 관점에 보면 확장에 해당하는
변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방식이다.

c = dataSource.getConnection();

StatementStrategy strategy = new DeleteAllStatement();
ps = strategy.makePreparedStatement(c);

위 코드는 그럭저럭 전략 패턴을 적용한 것으로 볼 수 있다.

하지만 전략 패턴은 필요에 따라 컨텍스트는 그대로 유지되면서 전략을 바꿔쓸 수 있어야 한다.
이렇게 컨텍스트 안에서 이미 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있다면 뭔가 이상하다.
전략 패턴과 OCP에 잘 들어맞지 않는다.


DI 적용을 위한 클라이언트 / 컨텍스트 분리

문제를 해결하기 위해 전략 패턴의 실제적인 사용 방법을 더 살펴보자.

전략 패턴에 따르면 Context가 어떤 전략을 사용하게 할 것인지는 Context를 사용하는 앞단의 Client가 결정하는 게 일반적이다.
Client가 구체적인 전략의 하나를 선택하고 오브젝트를 만들어서 Context에 전달하는 것이다.

결국 전략 오브젝트 생성과 컨텍스트로의 전달을 담당하는 책임을 분리시킨 것이 바로 ObjectFactory이며
이를 일반화한 것이 이전 장에서 살펴봤던 의존관계 주입(DI)이었다.

DI란 이러한 전략 패턴의 장점을 일반적으로 활용할 수 있도록 만든 구조라고 볼 수 있다.

의존관계 주입(DI)은 다양한 형태로 적용할 수 있다.
가장 중요한 개념은 제 3자의 도움을 통해 두 오븢게트 사이의 유연한 관계가 설정되도록 만든다는 것이다.


전략과 클리이언트의 동거

지금까지 공부한 내용으로 구현한 코드에 더 개선할 부분이 있다.

  1. DAO 메서드마다 새로운 StatementStrategy 구현 클래스를 만들어야 한다는 점이다.
    이렇게 되면 기존 UserDao 때보다 클래스 파일의 개수가 더 늘어난다.

이래서는 런타임 시에 다이내믹하게 DI해준다는 점을 제외하면 로직마다 상속을 사용하는
템플릿 메서드 패턴을 적용했을 때보다 그다지 나을 게 없다.

  1. DAO 메서드에서 StatementStrategy에 전달할 User와 같은 부가적인 정보가 있는 경우,
    이를 위해 오브젝트를 전달받는 생성자와 이를 저장해둘 인스턴스 변수를 번거롭게 만들어야 한다는 점이다.

이 오브젝트가 사용되는 시점은 컨텍스트가 전략 오브젝트를 호출할 때라서 잠시라도 어딘가에 다시 저장해둘 수밖애 없다.

이 문제점들을 해결해보자.


로컬 클래스

클래스 파일이 많아지는 문제점은 간단하게 해결할 수 있다.
내부 클래스로 정의해버리는 것이다.

마치 로컬 변수를 선언하듯이 사용하면 된다.
로컬 클래스는 선언된 메서드 내에서만 사용할 수 있다.

메서드 안에서 클래스 생성 로직을 함께 볼 수 있어 코드를 이해하기 좋고,
내부 클래스이기 때문에 자신이 선언된 곳의 정보에 접근할 수 있다.

다만, 내부 클래스에서 외부의 변수를 사용할 때는 외부 변수는 반드시 final로 선언해야 한다.


익명 내부 클래스

더 욕심을 내보자면, 더 간결하게 클래스 이름도 제거할 수 있다.

new 인터페이스이름() { 클래스 본문 };  


컨텍스트와 DI

JdbcContext의 분리

전략 패턴의 구조로 보자면 UserDao의 메소드가 클라이언트고, 익명 내부 클래스로 만들어지는 것이 개별적인 전략이고
JdbcContextWithStatementStrategy() 메서드는 컨텍스트이다.
컨텍스트 메서드는 UserDao 내의 PreparedStatement를 실행하는 기능울 가진 메서드에서 공유할 수 있다.

그런데 JdbcContextWithStatementStrategy()는 다른 DAO에서도 사용 가능하다.
밖으로 독립시켜 모든 DAO가 사용할 수 있게 해보자.

분리해서 만든 클래스는 JdbcContextfkrh gkwk.
JdbcContext에 UserDao에 있던 컨텍스트 메서드를 workWithStatementStrategy()라는 이름으로 옮겨놓는다.

JdbcContext가 DataSource에 의존하고 있으므로 DataSource 타입 빈을 DI 받을 수 있게 해줘야 한다.


빈 의존관계 변경

새롭게 변경된 의존관계를 살펴보자.
UserDao는 이제 JdbcContext에 외존한다. 그런데 JdbcContext는 인터페이스인 DataSource와는 달리 구체 클래스다.

스프링 DI는 기본적으로 인터페이스를 사에이 두고 의존 클래스를 바꿔서 사용할 수 있도록 하는 게 목적이다.
하지만 이 경우 JdbcContext는 그 자체로 독립적인 JDBC 컨텍스트를 제공해주는 서비스 오브젝트로서 의미가 있을 뿐이고
구현 방법이 바뀔 가능성은 없다.

따라서 인터페이스를 구현하도록 하지 않았고, UserDaodjl JdbcContext는 인터페이스를 사이에 두지 않고 DI를 적용하는 특별한 구조가 되었다.


스프링 빈으로 DI

JdbcContext를 UserDao와 DI 구조로 만들어야 할 이유를 생각해보자.

  1. JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 되기 때문이다.

  2. JdbcContext가 DI를 통해 다른 빈에 의존하고 있기 때문이다.
    JdbcContext는 dataSource 프로퍼티를 통해 DataSource 오브젝트를 주입받도록 되어 있다.
    DI를 위해서는 주입되는 오브젝트와 주입받는 오브젝트 양쪽 모두 스프링 빈으로 등록돼야 한다.

스프링이 생성하고 관리하는 IoC 대상이어야 DI에 참여할 수 있기 떄문이다.
따라서 JdbcContext가 다른 빈을 받기 위해서라도 스프링 빈으로 등록돼야 한다.

왜 인터페이스를 사용하지 않았을까?

인터페이스가 없다는 건 UserDao와 JdbcContext가 매우 긴밀한 관계를 가지고
강하게 결합되어 있다는 것이다.

클래스는 구분되어 있지만 강한 응집도를 갖고 있다.
JDBC 방식 대신 JPA나 하이버네이트 같은 ORM을 사용해야 한다면 JdbcContext도 통쨰로 바뀌어야 한다.

이런 경우는 굳이 인터페이스를 두지 말고 강력한 결합을 가진 관계를 허용하면서 위에서 말한 두 가지 이유인,
싱글톤으로 만드는 것과 JdbcContext에 대한 DI 필요성을 위해 스프링의 빈으로 등록해서 UserDao에 DI되도록 만드는 것도 좋다.

단, 이런 클래스를 바로 사용하는 코드 구성을 DI에 적용하는 것은 가장 마지막 단계에서 고려해볼 사항이다.


템플릿과 콜백

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


템플릿/콜백의 동작원리

템플릿은 고정된 작업 흐름을 가진 코드를 재사용한다는 의미에서 붙인 이름이다.
콜백은 템플릿 안에서 값을 참조하기 위한 것이 아니라 특정 로직을 담는 것을 목적으로
호출되는 오브젝트를 말한다.

템플릿 메서드 패턴은 고정된 틀의 로직을 가진 템플릿 메소드를 슈퍼클래스에 두고,
바뀌는 부분을 서브 클래스의 메서드에 두는 구조로 이뤄진다.

전략 패턴의 전략은 여러 개의 메소드를 가진 일반적인 인터페이스를 사용할 수 있다.
템플릿/콜백 패턴의 콜백은 보통 단일 메소드 인터페이스를 사용한다.

작업 흐름 중 특정 기능을 위해 한 번 호출되는 경우가 일반적이기 때문이다.
하나의 템플릿 안에서 여러 전략을 사용한다면 하나 이상의 콜백 오브젝트를 사용할 수도 있다.

콜백은 일반적으로 하나의 메서드를 가진 인터페이스를 구현한 익명 내부 클래스로 만들어진다고 보면된다.

콜백 인터페이스의 메서드에는 보통 파라미터가 있다.
이 파라미터는 템플릿의 작업 흐름 중에 만들어지는 컨텍스트 정보를 전달받을 때 사용된다.

  1. 클아이언트의 역할은 템플릿 안에서 실행될 로직을 담은 콜백 오브젝트를 만들고,
    콜백이 참조할 정보를 제공하는 것이다.
    만들어진 콜백은 클라이언트가 템플릿의 메소드를 호출할 때 파라미터로 전달된다.

  2. 템플릿은 정해진 작업 흐름을 따라 작업을 진행하다가 내부에서 생성한 참조정보를 가지고 콜백 오브젝트의 메소드를 호출한다.
    콜백은 클라이언트 메서드에 있는 정보와 템플릿이 제공한 참조정보를 이용해서 작업을 수행하고 그 결과를 다시 템플릿에 돌려준다.

  3. 템플릿은 콜백이 돌려준 정보를 사용해서 작업을 마저 수행한다.
    경우에 따라 최종 결과를 클라이언트에 다시 돌려주기도 한다.

복잡한 과정같지만 DI 방식의 전략 패턴 구조로 보면 간단하다.
클라이언트가 템플릿 메소드를 호출하면서 콜백 오브젝트를 전달하는 것은 메소드 레벨에서 일어나는 DI이다.
일반적인 DI는 의존 오브젝트를 수정자 메서드로 받아서 사용할 것인데
템플릿/콜백 방식에서는 매번 메서드 단위로 사용할 오브젝트를 받는 것이 특징이다.

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

이 패턴에 녹아있는 전략 패턴과 수동 DI 를 이해할 수 있어야 한다.


템플릿/콜백의 응용

고정된 작업 흐름을 갖고 있으면서 여기저기서 자주 반복되는 코드가 있다면,
중복되는 코드를 분리할 방법을 생각해보는 습관을 기르자.

먼저 메소드로 분리할 방법을 생각해보고,
그중 일부 작업을 필요에 따라 바꾸어 사용해야 하나면
인터페이스를 사이에 두고 분리해서 전략 패턴을 적용하고 DI로 의존관계를 관리하도록 만든다.

그런데 바뀌는 부분이 한 애플리케이션 안에서 동시에 여러 종류가 만들어질 수 있다면
템플릿/콜백 패턴을 적용하는 것을 고려해볼 수 있다.

가장 전형적인 템플릿/콜백 패턴의 후보는 try/catch/finally 블록을 사용하는 코드다.


정리