토비의 스프링 3장 (템플릿)

템플릿

개방 폐쇄 원칙 (OCP)

어떤 부분은 변경을 통해 그 기능이 다양해지고 확장하려는 성질이 있고, 어떤 부분은 고정되어 있고 변하지 않으려는 성질이 있다. 변화의 특성이 다른 부분을 구분해주고, 각각 다른 목적과 다른 이유에 의해 다른 시점에 독립적으로 변경될 수 있는 효율적인 구조를 만들어주는 것이 개방 폐쇄 원칙이다.

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

일반적으로 서버에서는 제한된 개수의 DB 커넥션을 만들어서 재사용 가능한 풀로 관리한다. DB 풀은 매번 getConnection()으로 가져간 커넥션을 명시적으로 close()해서 돌려줘야지만 다시 풀에 넣었다가 다음 커넥션 요청이 있을 때 재사용할 수 있다. 그런데 이런 식으로 오류가 날 때마다 미처 반횐되지 못한 Connection이 계속 쌓이면 어느 순간에 커넥션 풀에 여유가 없어지고 리소스가 모자란다는 심각한 오류를 내며 서버가 중단될 수 있다. 그래서 JDBC 코드에서는 어떤 상황에서도 가져온 리소스를 반환하도록 try/catch/finally 구문 사용을 권장하고 있다. (finally는 try 블록을 수행한 후에 예외가 발생하든 정상적으로 처리되든 상관없이 반드시 실행되는 코드를 넣을 때 사용한다.)

어느 시점에서 예외가 발생했는지에 따라서 close()를 사용할 수 있는 변수가 달라질 수 있기 때문에 finally에서는 반드시 c(Connection)와 ps(PreparedStatment)가 null이 아닌지 먼저 확인한 후에 close() 메소드를 호출해야 한다.

변하는 것과 변하지 않는 것

이런 코드를 효과적으로 다룰 수 있는 방법은 없을까? 이 문제의 핵심은 변하지 않는, 그러나 많은 곳에서 중복되는 코드와 로직에 따라 자꾸 확장되고 자주 변하는 코드를 잘 분리해내는 작업이다.

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

메소드 추출

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void deleteAll() throws SQLException {
...
try {
c = dataSource.getConnection();

ps = makeStatement(c); // 변하는 부분을 메소드로 추출하고 변하지 않는 부분에서 호출하도록 만들었다.

ps.executeUpdate();
} catch(SQLException e)
...
}

private PreparedStatement makeStatement(Connection c) throws SQLException {
PreparedStatement ps;
ps = c.prepareStatement("delete from users");
return ps;
}

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

템플릿 메소드 패턴의 적용

템플릿 메소드 패턴은 상속을 통해 기능을 확장해서 사용하는 부분이다. 변하지 않는 부분은 슈퍼클래스에 두고 변하는 부분은 추상 메소드로 정의해둬서 서브클래스에서 오버라이드하여 새롭게 정의해 쓰도록 하는 것이다.

1
abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;
1
2
3
4
5
6
7
public class UserDaoDeleteAll extends UserDao {

protected PreparedStatement makeStatement(Connection C) throws SQLException {
PreparedStatment ps = c.prepareStatement("delete from users");
return ps;
}
}

이제 UserDao 클래스의 기능을 확장하고 싶을 때마다 상속을 통해 자유롭게 확장할 수 있고, 확장 때문에 기존의 상위 DAO 클래스에 불필요한 변화는 생기지 않도록 할 수 있으니 객체지향 설계의 핵심 원리인 개방 폐쇄 원칙(OCP)을 그럭저럭 지키는 구조를 만들어낼 수는 있는것 같다. 그렇지만 아직 문제가 있다. 가장 큰 문제는 DAO 로직마다 상속을 통해 새로운 클래스를 만들어야 한다는 점이다. 이래서는 장점보다 단점이 더 많아 보인다.

변하지 않는 코드를 가진 UserDao의 JDBC try/catch/finally 블록과 변하는 PreparedStatement를 담고 있는 서브클래스들이 이미 클래스 레벨에서 컴파일 시점에 이미 그 관계가 결정되어 있다. 따라서 그 관계에 대한 유연성이 떨어진다.

전략 패턴의 적용

개방 폐쇠 원칙(OCP)을 잘 지키는 구조이면서도 템플릿 메소드 패턴보다 유연하고 확장성이 뛰어난 것이, 오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 전략 패턴이다. 전략 패턴은 OCP 관점에서 보면 확장에 해당하는 변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방식이다.

deleteAll()은 JDBC를 이용해 DB를 업데이트하는 작업이라는 변하지 않는 맥락(context)을 갖는다. deleteAll()의 컨텍스트를 정리해보면 다음과 같다.

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

두번째 작업에서 사용하는 PreparedStatement를 만들어주는 외부 기능이 바로 전략 패턴에서 말하는 전략이라고 볼 수 있다. 전략 패턴의 구조를 따라 이 기능을 인터페이스로 만들어두고 인터페이스 이 메소드를 통해 PreparedStatement 생성 전략을 호출해주면 된다. 여기서 눈여겨볼 것은 이 PreparedStatement를 생성하는 전략을 호출할 때는 이 컨텍스트 내에서 만들어둔 DB 커넥션을 전달해야 한다는 점이다.

PreparedStatement를 만드는 전략의 인터페이스는 컨텍스트가 만들어준 Connection을 전달받아서, PreparedStatement를 만들고 만들어진 PreparedStatement 오브젝트를 돌려준다. 이 내용을 인터페이스로 정의하면 다음과 같다.

1
2
3
public interface StatementStrategy {
PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}

StatementStrategy 인터페이스를 상속해서 실제 전략 클래스를 만들고 이 전략 클래스를 이용한 전략 패턴을 적용한 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public void deleteAll() throws SQLException {
...
try {
c = dataSource.getConnection();

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

ps.executeUpdate();
} catch (SQLException e) {
...
}
}

전략 패턴은 필요에 따라 컨텍스트는 그대로 유지되면서(OCP의 폐쇄 원칙) 전략을 바꿔 쓸 수 있다(OCP의 개방 원칙)는 것인데, 이렇게 컨텍스트 안에서 이미 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있다면 뭔가 이상하다. 컨텍스트가 StatementStrategy 인터페이스 뿐 아니라 특정 구현 클래스인 DeleteAllStatement를 직접 알고 있다는건, 전략 패턴에도 OCP에도 잘 들어맞는다고 볼 수 없기 때문이다.

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

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

결국 이 구조에서 전략 오브젝트 생성과 컨텍스트로의 전달을 담당하는 책임을 분리시킨 것이 바로 ObjectFactory이며, 이를 일반화한 것이 앞에서 살펴봤던 의존관계 주입(DI)이었다. 결국 DI란 이러한 전략 패턴의 장점을 일반적으로 활용할 수 있도록 만든 구조라고 볼 수 있다.

아무튼 여기서 이 패턴 구조를 코드에 적용해보자. 중요한 것은 컨텍스트에 해당하는 JDBC try/catch/finally 코드를 클라이언트 코드인 StatementStrategy를 만드는 부분에서 독립시켜야 한다는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException {
connection c = null;
PreparedStatement ps = null;

try {
c = dataSource.getConnection();

ps = stmt.makePreparedStatement(c);

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) {} }
}
}

이 메소드는 컨텍스트의 핵심적인 내용을 잘 담고 있다. 클라이언트로부터 StatementStrategy 타입의 전략 오브젝트를 제공받고 JDBC try/catch/finally 구조로 만들어진 컨텍스트 내에서 작업을 수행한다.

다음은 클라이언트에 해당하는 부분이다. 컨텍스트를 별도의 메소드로 분리했으니 deleteAll() 메소드가 클라이언트가 된다. deleteAll()은 전략 오브젝트를 만들고 컨텍스트를 호출하는 책임을 지고 있다.

1
2
3
4
public void deleteAll() throws SQLException {
StatementStrategy st = new DeleteAllStatement(); // 선정한 전략 클래스의 오브젝트 생성
jdbcContextWithStatementStrategy(st); // 컨텍스트 호출. 전략 오브젝트 전달
}

클라이언트가 컨텍스트가 사용할 전략을 정해서 전달하는 면에서 DI 구조라고 이해할 수도 있다.

마이크로 DI

의존관계 주입(DI)은 다양한 형태로 적용할 수 있다. DI의 가장 중요한 개념은 제3자의 도움을 통해 두 오브젝트 사이의 유연한 관계가 설정되도록 만든다는 것이다. 이 개념만 따른다면 DI를 이루는 오브젝트와 구성요소의 구조나 관계는 다양하게 만들 수 있다.

일반적으로 DI는 의존관계에 있는 두 개의 오브젝트와 이 관계를 다이내믹하게 설정해주는 오브젝트 팩토리(DI 컨테이너), 그리고 이를 사용하는 클라이언트라는 4개의 오브직트 사이에서 일어난다. 하지만 때로는 원시적인 전략패턴 구조를 따라 클라이언트가 오브젝트 팩토리의 책임을 함께 지고 있을 수도 있다.

이런 경우에는 DI가 매우 작은 단위의 코드와 메소드 사이에서 일어나기도 한다. 이렇게 DI의 장점을 단순화해서 IoC 컨테이너의 도움 없이 코드 내에서 적용한 경우를 마이크로 DI라고도 한다. 또는 코드에 의한 의미로 수동 DI라고 부를 수도 있다.

JDBC 전략 패턴의 최적화

전략과 클라이언트의 동거

현재 구조에 두 가지 불만이 있다.

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

이 두가지 문제를 해결할 수 있는 방법을 생각해보자.

로컬 클래스

클래스 파일이 많아지는 문제는 간단한 해결 방법이 있다. StatementStrategy 전략 클래스를 매번 독립된 파일로 만들지 말고 UserDao 클래스 안에 내부 클래스로 정의해버리는 것이다. DeleteAllStatement나 AddStatement는 UserDao 밖에서는 사용되지 않는다. 둘 다 UserDao에서만 사용되고, UserDao의 메소드 로직에 강하게 결합되어 있다.

중첩 클래스의 종류

다른 클래스 내부에 정의되는 클래스를 중첩 클래스(nested class)라고 한다. 중첩 클래스는 독립적으로 오브젝트로 만들어질 수 있는 스태틱 클래스(static class)와 자신이 정의된 클래스의 오브젝트 안에서만 만들어질 수 있는 내부 클래스(inner class)로 구분된다.

내부 클래스는 다시 범위(scope)에 따라 세가지로 구분된다.

  • 멤버 내부 클래스 : 멤버 필드처럼 오브젝트 레벨에 정의된다.
  • 로컬 클래스 : 메소드 레벨에 정의된다.
  • 익명 내부 클래스 : 이름을 갖지 않는 익명 클래스이다. 익명 내부 클래스의 범위는 선언된 위치에 따라서 다르다.

로컬 클래스의 장점은 클래스가 내부 클래스이기 때문에 자신이 선언된 곳의 정보에 접근할 수 있다는 것이다. 내부 메소드는 자신이 정의된 메소드의 로컬 변수에 직접 접근할 수 있기 때문이다. 다만, 내부 클래스에서 외부의 변수를 사용할 때는 외부 변수는 반드시 final로 선언해줘야 한다.

로컬 클래스로 만들어두니, 메소드마다 추가해야 했던 클래스 파일을 하나 줄일 수 있고 내부 클래스의 특징을 이용해 로컬 변수를 바로 가져다 사용할 수 있다는 장점도 생겼다.

익명 내부 클래스

익명 내부 클래스

익명 내부 클래스(anonymous inner class)는 이름을 갖지 않는 클래스다. 클래스 선언과 오브젝트 생성이 결합된 상태로 만들어지며, 상속할 클래스나 구현할 인터페이스를 생성자 대신 사용해서 다음과 같은 형태로 만들어 사용한다. 클래스를 재사용할 필요가 없고, 구현한 인터페이스 타입으로만 사용할 경우에 유용하다.

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

익명 내부 클래스는 선언과 동시에 오브젝트를 생성한다. 이름이 없기 때문에 클래스 자신의 타입을 가질 수 없고, 구현한 인터페이스 타입의 변수에만 저장할 수 있다.

1
2
3
4
5
6
7
8
9
10
StatementStrategy st = new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());

return ps;
}
}

만들어진 익명 내부 클래스의 오브젝트는 딱 한 번만 사용할 테니 굳이 변수에 담아두지 말고 jdbcContextWithStatementStrategy() 메소드의 파라미터에서 바로 생성하는 편이 낫다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void add(final User user) throws SQLException {
jdbcContextWithStatementStrategy(
new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());

return ps;
}
}
);
}

컨텍스트와 DI

전략 패턴의 구조로 보자면 UserDao의 메소드가 클라이언트이고, 익명 내부 클래스로 만들어지는 것이 개별적인 전략이고, jdbcContextWithStatementStrategy() 메소드는 컨텍스트다. 그런데 JDBC의 일반적인 작업 흐름을 담고 있는 jdbcContextWithStatementStrategy()는 다른 DAO에서도 사용 가능하다. 그러니 jdbcContextWithStatementStrategy()를 UserDao 클래스 밖으로 독립시켜서 모든 DAO가 사용할 수 있게 해야한다.

클래스 분리

분리해서 만들 클래스의 이름을 JdbcContext라고 하자. JdbcContext에 UserDao에 있던 컨텍스트 메소드를 workWithStatementStrategy()라는 이름으로 옮겨놓는다. 그런데, 이렇게 하면 DataSource가 필요한 것은 UserDao가 아니라 JdbcContext가 돼버린다. DB 커넥션을 필요로 하는 코드는 JdbcContext 안에 있기 때문이다. 따라서 JdbcContext가 DataSource에 의존하고 있으므로 DataSource 타입 빈을 DI 받을 수 있게 해줘야 한다.

빈 의존관계 변경

UserDao는 이제 JdbcContext에 의존한다. 그런데 JdbcContext는 인터페이스인 DataSource와는 달리 구체 클래스다. 스프링의 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꿔서 사용하도록 하는게 목적이다. 하지만 이 경우 JdbcContext는 그 자체로 독립적인 JDBC 컨텍스트를 제공해주는 서비스 오브젝트로서 의미가 있을 뿐이고 구현 방법이 바뀔 가능성은 없다. 따라서 인터페이스를 구현하지 않고, UserDao와 JdbcContext는 인터페이스를 사이에 두지 않고 DI를 적용하는 특별한 구조가 된다.

스프링 빈으로 DI

인터페이스를 사용해서 클래스를 자유롭게 변경할 수 있게 하지는 않았지만, JdbcContext를 UserDao와 DI 구조로 만들어야 할 이유는 다음과 같다.

  1. JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 되기 때문이다.
  2. JdbcContext가 DI를 통해 다른 빈에 의존하고 있기 때문이다. JdbcContext는 dataSource 프로퍼티를 통해 JdbcContext 오브젝트를 주입받도록 되어 있다. DI를 위해서는 주입되는 오브젝트와 주입받는 오브젝트 양쪽 모두 스프링 빈으로 등록돼야 한다. 스프링이 생성하고 관리하는 IoC 대상이어야 DI에 참여할 수 있기 때문이다.

템플릿과 콜백

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

템플릿

템플릿은 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀을 가리킨다. 템플릿 메소드 패턴은 고정된 틀의 로직을 가진 템플릿 메소드를 슈퍼클래스에 두고, 바뀌는 부분을 서브클래스의 메소드에 두는 구조로 이뤄진다.

콜백

콜백은 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트를 말한다. 자바에서는 메소드 자체를 파라미터로 전달할 방법이 없기 때문에 메소드가 담긴 오브젝트를 전달해야 한다. 그래서 펑서녈 오브젝트(functional object)라고도 한다.

템플릿/콜백의 동작원리

템플릿은 고정된 작업 흐름을 가진 코드를 재사용한다는 의미에서 붙인 이름이다. 콜백은 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트를 말한다.

템플릿/콜백의 특징

여러 개의 메소드를 가진 일반적인 인터페이스를 사용할 수 있는 전략 패턴의 전략과 달리 템플릿/콜백 패턴의 콜백은 보통 단일 메소드 인터페이스를 사용한다. 템플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출되는 경우가 일반적이기 때문이다. 콜백은 일반적으로 하나의 메소드를 가진 인터페이스를 구현한 익명 내부 클래스로 만들어진다고 보면된다.

템플릿/콜백 패턴의 일반적인 작업 흐름은 다음과 같다.

  • 클라이언트의 역할은 템플릿 안에서 실행될 로직을 담은 콜백 오브젝트를 만들고, 콜백이 참조할 정보를 제공하는 것이다. 만들어진 콜백은 클라이언트가 템플릿의 메소드를 호출할 때 파라미터로 전달된다.
  • 템플릿은 정해진 작업 흐름을 따라 작업을 진행하다가 내부에서 생성한 참조정보를 가지고 콜백 오브젝트의 메소드를 호출한다. 콜백은 클라이언트 메소드에 있는 정보와 템플릿이 제공한 참조정보를 이용해서 작업을 수행하고 그 결과를 다시 템플릿에 돌려준다.
  • 템플릿은 콜백이 돌려준 정보를 사용해서 작업을 마저 수행한다. 경우에 따라 최종 결과를 클라이언트에 다시 돌려주기도 한다.

템플릿/콜백 방식은 전략 패턴과 DI의 장점을 익명 내부 클래스 사용 전략과 결합한 독특한 활용법이라고 이해할 수 있다. 단순히 전략 패턴으로만 보기엔 독특한 특징이 많으므로 템플릿/콜백을 하나의 고유한 패턴으로 기억해두면 좋다.

편리한 콜백의 재활용

템플릿/콜백 방식에서 한 가지 아쉬운 점이 있다. DAO 메소드에서 매번 익명 내부 클래스를 사용하기 때문에 상대적으로 코드를 작성하고 읽기가 조금 불편하다는 점이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public void deleteAll() throws SQLException {
executeSql("delete from users"); // 변하는 SQL 문장
}
------------------------------------------------------------
private void executeSql(final String query) throws SQLException {
this.jdbcContext.workWithStatementStrategy(
new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.prepareStatement(query);
}
}
)
}

바뀌지 않는 모든 부분을 빼내서 executeSql() 메소드로 만들었다. 바뀌는 부분인 SQL 문장만 파라미터로 받아서 사용하게 만들었다. SQL을 담은 파라미터를 final로 선언해서 익명 내부 클래스인 콜백 안에서 직접 사용할 수 있게 하는 것만 주의하면 된다.

이렇게 재사용 가능한 콜백을 담고 있는 메소드라면 DAO가 공유할 수 있는 템플릿 클래스 안으로 옮겨도 된다. 그 결과 결국 JdbcContext 안에 클라이언트와 템플릿, 콜백이 모두 함께 공존하면서 동작하는 구조가 됐다.

템플릿/콜백의 응용

고정된 작업 흐름을 갖고 있으면서 여기저기서 자주 반복되는 코드가 있다면, 중복되는 코드를 분리할 방법을 생각해보는 습관을 기르자. 중복된 코드는 먼저 메소드로 분리하는 간단한 시도를 해본다. 그중 일부 작업을 필요에 따라 바꾸어 사용해야 한다면 인터페이스를 사이에 두고 분리해서 전략패턴을 적용하고 DI로 의존관계를 관리하도록 만든다. 그런데 바뀌는 부분이 한 애플리케이션 안에서 동시에 여러 종류가 만들어질 수 있다면 이번엔 템플릿/콜백 패턴을 적용하는 것을 고려해볼 수 있다.

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

템플릿/콜백을 적용할 때는 템플릿과 콜백의 경계를 정하고 템플릿이 콜백에게, 콜백이 템플릿에게 각각 전달하는 내용이 무엇인지 파악하는게 가장 중요하다. 그에 따라 콜백의 인터페이스를 정의해야 하기 때문이다.

클래스 이름이 Template으로 끝나거나 인터페이스 이름이 Callback으로 끝난다면 템플릿/콜백이 적용된 것이라고 보면 된다.

정리

  • JDBC와 같은 예외가 발생할 가능성이 있으며 공유 리소스의 반환이 필요한 코드는 반드시 try/catch/finally 블록으로 관리해야 한다.
  • 일정한 작업 흐름이 반복되면서 그중 일부 기능만 바뀌는 코드가 존재한다면 전략 패턴을 적용한다. 바뀌지 않는 부분을 컨텍스트로, 바뀌는 부분은 전략으로 만들고 인터페이스를 통해 유연하게 전략을 변경할 수 있도록 구성한다.
  • 클라이언트 메소드 안에 익명 내부 클래스를 사용해서 전략 오브젝트를 구현하면 코드도 간결해지고 메소드의 정보를 직접 사용할 수 있어서 편리하다.
  • 컨텍스트가 하나 이상의 클라이언트 오브젝트에서 사용된다면 클래스를 분리해서 공유하도록 만든다.
  • 단일 전략 메소드를 갖는 전략 패턴이면서 익명 내부 클래스를 사용해서 매번 전략을 새로 만들어 사용하고, 컨텍스트 호출과 동시에 전략 DI를 수행하는 방식을 템플릿/콜백 패턴이라고 한다.
  • 콜백의 코드에도 일정한 패천이 반복된다면 콜백을 템플릿에 넣고 재활용하는 것이 편리하다.
  • 템플릿과 콜백의 타입이 다양하게 바뀔 수 있다면 제네릭스를 이용한다.
  • 템플릿은 한 번에 하나 이상의 콜백을 사용할 수도 있고, 하나의 콜백을 여러 번 호출할 수도 있다.
  • 템플릿/콜백을 설계할 때는 템플릿과 콜백 사이에 주고받는 정보에 관심을 둬야한다.

참고

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