
템플릿이란?
JDBC 코드에서는 어떤 상황에서도 가져온 리소스를 반환하도록 try/catch/finally 구문 사용을 권장한다.
예외 발생 시에도 리소스를 반환하도록 수정한 deleteAll()
public void delteAll() throws SQLEXception {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = c.prepareStatement("delete from users");
ps.executeUpdate();
} catch (SQLException e) {
throw e;
/*
* 왜 예외를 다시 던질까?
* finally문 때문이다. 사실은 try-finally만 있어도 된다.
*/
} finally {
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
}
}
if (c != null) {
try {
c.close(); // try-with-resources를 사용하면 close가 따로 필요하지 않음
} catch (SQLException e) {
}
}
}
}
핵심! close할 때 예외처리를 해줘야 한다.
왜 예외를 다시 던지는가에 대해서 생각해 보았다.
finally문 때문이라고 했는 데 왜 일까?
여기서의 catch문은 try문에서 발생한 예외를 잡아주는 부분이다.
try문에서 예외가 발생하고 catch문에서 throw를 해주지 않는다면 finally(finally문은 try블록에서 예외가 발생하나 안하나 무조건 수행을 하는 블록) 안의 catch문에서 예외가 잡혀서 service단에서 어디서 예외가 발생했는지 알 수 없고, 잘 처리되었다고 생각하고 넘어갈 수 있기 때문이다.
finally 블럭 안의 try-catch문은 close에 대한 예외를 처리해 주기위한 블록이므로 각각의 예외를 잡아줘야된다고 생각한다.
try-finally만 있어도 된다는 부분은 어짜피 finally 블록에서 try-catch블럭이 존재하기 때문에 그렇게 말을 한 것이 아닐까 싶다.
close() 메소드
복잡한 try/catch/finally 블록이 2중으로 중첩되어 나오고, 모든 메소드마다 반복된다.
이런 복잡한 코드에서 당장 컴파일 에러가 나지 않더라도 메소드 호출이 되면서 커넥션이 하나씩 반환되지 않고 쌓이게 되면 리소스가 꽉 찼다는 에러가 나면서 서비스가 중단되는 상황이 발생할 수 있다.
이런 문제를 효과적으로 다룰 수 있는 방법은 많은 곳에서 중복되는 코드와 로직에 따라 자꾸 확장되고 자주 변하는 코드를 잘 분리해내는 작업이다.
단, DAO와 DB 연결 기능을 분리하는 것과는 성격이 다르므로 해결방법이 조금 다르다.
변하지 않는 부분
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) {} }
}
변하는 부분을 메소드로 뺀다.
public void deleteAll()throws SQLException {
...
try {
c = dataSource.getConnection();
ps = makeStatement(c); // 변하는 부분을 메소드로 추출하고 변하지 않는 부분에서 호출하도록 바꿈(전략패턴)
ps.executeUpdate();
} catch (SQLException e)
...
}
// 별도의 메소드로 빠진 변하는 부분
private PreparedStatement makeStatement(Connection c) thorws SQLException {
PreparedStatement ps;
ps = c.prepareStatement("delete from users");
return ps;
}
템플릿 메소드 패턴 : 상속을 통해 기능을 확장
상속을 통해 자유롭게 클래스의 기능을 확장할 수 있지만 제한이 많다. 만약 템플릿 메소드 패턴을 사용한다면 UserDao의 JDBC 메소드가 4개일 경우 4개의 서브클래스를 만들어서 사용해야 한다.

템플릿 메소드 패턴의 적용
또한 확장구조가 이미 클래스를 설계하는 시점에서 고정되어 버린다.
변하지 않는 코드를 가진 서브클래스들이 이미 클래스 레벨에서 컴파일 시점에 관계가 결정되버린다.
따라서 관계에 대한 유연성이 떨어진다.
전략 패턴 : 주입을 통해 기능을 위임
전략 패턴은 OCP(개방 폐쇄 원칙)을 잘 지키면서 오브젝트를 둘로 분리하고 클래스 레벨에서 인터페이스를 통해서만 의존하도록 만드는 점이 템플릿 메소드 패턴보다 유연하고 확장성이 뛰어나다고 볼 수 있다.

전략 패턴의 구조
// Statementstrategy stmt : 클라이언트가 컨텍스트를 호출할 때 넘겨줄 전략 파라미터
publc void jbcContextWihStatementStrategy(Statementstrategy stmt)
throws SQLExcepti n {
Connecti n c = nul;
PreparedStatement ps = nul;
try {
c = dataSource.getConnection();
ps = stmt.makePreparedStatement(c); // 매개변수로 주입 받았음
ps.executeUpdate();
} catch (SQLExcepti n e) {
throw e;
} finaly {
i (ps != nul) { try { ps.close(); } catch (SQLException e) {} }
i (c != nul) { try {c.close(); } catch (SQLException e) {} }
}
}
위의 메소드는 클라이언트로부터 StatementStrategy 타입의 전략 오브젝트를 제공받고 JDBC try/catch/finally 구조로 만들어진 컨텍스트 내에서 작업을 수행한다.
제공받은 전략 오브젝트는 PreparedStatement 생성이 필요한 시점에 호출해서 사용한다.
public void deleteAll() throws SQLException {
StatementStrategy st = new DeleteAllStatement(); // 선정한 전략 클래스의 오브젝트 생성
jdbcContextWithStatementStrategy(st); // 컨텍스트 호출. 전략 오브젝트 전달
}
전략은 사용하는 쪽(밖에서 결정)에서 결정해서 주도록 한다.
추가정보를 받기위해서 클라이언트가 부가정보를 제공해줘야한다.
클라이언트로부터 오브젝트를 받을 수 있도록 생성자를 통해 제공받게 한다.
package springbook.user.dao;
...
public class AddStatement implements StatementStrategy {
User user;
public AddStatement(User user) {
this.user = user;
}
public PreparedStatement makePreparedStatement(Connection c) {
...
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
..
}
}
public void add(User user) throws SQLException {
StatementStrategy st = new AddStatement(**user**); // user를 받아와서 넘겨줌
jdbcContextWithStatementStragey(st);
}
DAO 메소드마다 새로운 StatementStrategy 구현 클래스를 만들게 되면 클래스 파일의 개수가 많이 늘어나게 된다. 또 부가 정보가 있는 경우, 생성자와 저장할 인스턴스 변수를 번거롭게 만들어야 한다는 단점이 있다.
이러한 문제를 해결할 수 있는 방법으로 로컬 클래스, 익명 내부 클래스가 있다.
전략 클래스를 매번 독립된 파일로 만들지 않고 Dao 클래스 안에 내부 클래스로 정의하는 방법이다.
내부클래스를 이용해서 처리한다는 말은 Builder 패턴을 뜻한다.
밖에서 사용되지 않고 UserDao에서만 사용되는 클래스와 같이 특정 메소드에서만 사용되는 것이라면 로컬 클래스로 만들 수 있다. → 밖에서 사용하지 않고 서로 관련이 깊기 때문에 전략을 안으로 넣는 것이 가능 함
익명 내부 클래스는 이름을 갖지 않는 클래스로
클래스의 선언과 동시에 객체를 생성하므로, 단 하나의 객체만을 생성하는 일회용 클래스다.
익명 내부 클래스를 이용해서 처리한다는 말은 전략을 바로 만들어서 넣어두고 일회용으로 사용하겠다는 뜻이다.
컨텍스트란? 특정작업 한 단위를 유지 또는 수행하기 위해 필요한 모든일 들이다. 즉, 공유하는 환경을 말한다.
컨텍스트를 분리해야되며, 분리 한 뒤 연결하는 작업을 binding이라고 한다.
binding은 필요한대로 조합하여 재사용성을 높이기 위해 사용한다.
binding을 하는 방법은 2가지로 상속과 주입이 있다.
package springbook.user.dao;
...
public class JdbcContext {
// DataSource 타입 빈을 DI 받을 수 있게 준비한다
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
// JdbcContext 클래스 안으로 옮겼으므로 이름도 그에 맞게 수정
public void workWithStatementStrategy(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){}}
}
}
}
public class UserDao {
...
// JdbcContext를 DI 받도록 만듦(전략 패턴, 브릿지 패턴)
private JdbcContext jdbcContext; // iv
public void setJdbcContext(JdbcContext jdbcContext) {
this.jdbcContext = jdbcContext;
}
public void add(final User user) throws SQLException {
// DI 받은 JdbcContext의 컨텍스트 메소드를 사용하도록 변경
this.jdbcContext.workWithStatementStrategy(
new StatementStrategy() { ... }
);
}
public void deleteAll() throws SQLException {
this.jdbcContext.workWithStatementStrategy(
new StatementStrategy() { ... }
);
}
}
UserDao와 JdbcContext는 인터페이스를 사이에 두지 않고 DI를 적용하는 특별한 구조가 됨

JdbcContext를 적용한 UserDao의 의존관계
스프링의 빈 설정은 클래스 레벨이 아니라 런타임 시에 만들어지는 오브젝트 레벨의 의존관계에 따라 정의 된다.
userDao 빈이 dataSource 빈을 직접 의존했지만 이제 jdbcContext 빈이 사이에 끼게 된다.

JdbcContext가 적용된 빈 오브젝트 관계
UserDao는 인터페이스를 거치지 않고 코드에서 바로 JdbcContext 클래스를 사용하고 있다.
UserDao와 JdbcContext는 클래스 레벨에서 의존관계가 결정된다. 비록 런타임 시에 DI 방식으로 외부에서 오브젝트를 주입해주는 방식을 사용하긴 했지만, 의존 오브젝트의 구현 클래스를 변경할 수 는 없다.
인터페이스를 사용해서 클래스를 자유롭게 변경할 수 있게 하진 않았지만, JdbcContext를 UserDao와 DI 구조로 만들어야 할 이유는 무엇일까?
그런데 왜 인터페이스를 사용하지 않았을까? UserDao와 JdbcContext가 매우 긴밀한 관계를 가지고 강하게 결합되어 있기 때문이다.(강한 응집도)
자동 : @Autowired
수동 : 생성자, setter, getBean()
JdbcContext를 스프링의 빈으로 등록해서 UserDao에 DI하는 대신 사용할 수 있는 방법으로 UserDao 내부에서 직접 DI를 적용하는 방법이다.
하지만 이 방법을 사용하려면 문제점 두 가지가 존재한다.
코드를 통한 JdbcContext DI 구조
기존 설정파일에 등록했던 JdbcContext 빈을 제거하고, Dao의 jdbcContext 프로퍼티도 제거해준다. 그리고 DAO는 DataSource 타입 프로퍼티만 갖도록 한다.
<beans>
<bean id="userDao" class="springbook.user.dao.UserDao">
<property name="dataSource" ref="dataSource"/> // 주입하는 것
</bean>
<bean id="dataSource"
class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
...
</bean>
</beans>
UserDao는 이제 JdbcContext를 외부에서 주입받을 필요가 없으니 setJdbcContext()를 제거하고 setDataSource() 메소드를 수정해준다.
public class UserDao {
...
private JdbcContext jdbcContext;
// 수정자 메소드이면서 jdbcContext에 대한 생성, DI 작업을 동시에 수행
public void setDataSource(DataSource dataSource) {
this.jdbcContext = new JdbcContext(); // JdbcContext 생성(IoC) : dataSource에 대한 흐름을 넘겨 줬기 때문
this.jdbccontext.setDataSource(dataSource); // 의존 오브젝트 주입(DI)
// 위의 코드를 없애려고 jdbcContext 빈을 제거한 설정파일 xml을 작성하는 것
this.dataSource = dataSource; // 아직 JdbcContext를 적용하지 않은 메소드를 위해 저장해 둠
}
}
setDataSource() 메소드는 DI 컨테이너가 DataSource 오브젝트를 주입해줄 때 호출된다. 이때 JdbcContext에 대한 수동 DI 작업을 진행하면 된다.
DAO와 밀접한 관계를 갖는 클래스를 DI에 적용하는 방법 두 가지 모두 장단점이 존재한다.
템플릿/콜백 패턴 : 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식
템플릿/콜백 패턴의 콜백은 단일 메소드 인터페이스를 사용한다.
콜백 인터페이스의 메소드에 있는 파라미터는 템플릿의 작업 흐름 중 만들어지는 컨텍스트 정보를 전달받을 때 사용된다.
콜백? A가 호출하는 데 호출 받은 B는 A를 호출 할 수 없다.
A가 B를 호출했는데 B는 A의 전화번호가 없어서 호출을 못하기 때문에 값을 주거나 객체를 줘서 정보를 알려줘야 된다.(B가 A를 호출할 방법을 주는 것 ⇒ 나중에 호출할 함수를 주는 것)
A는 B의 연락을 무작정 기다리지 않고 다른 일을 하면서 기다린다.
A가 호출하는 쪽에 호출항 방법을 줘야되고, 보통 함수를 준다. B가 할 일을 다 끝내면 함수를 호출한다.
위의 예시 설명처럼 바뀌는 부분만 주면 JDBC 템플릿이 돌아가게 된다.

템플릿/콜백의 일반적인 작업 흐름
콜백을 클라이언트가 전달하는 것은 State 패턴,
바뀌는 부분을 밖에서 주는 것은 Strategy 패턴이다.
SQL만 넘겨주면 된다.
public class JdbcContext {
...
public void executeSql(final String query) throws SQLException {
workWithStatementStrategy(
new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c)
throws SQLException {
return c.prepareStatement(query);
}
}
);
}
public void deleteAll() throws SQLException {
this.jdbcContext.executeSql("delete from users");
}
위 처럼 코드를 수정하게 되면 모든 DAO 메소드에서 executeSql() 메소드를 사용할 수 있게 된다.

콜백 재활용을 적용한 JdbcContext
구체적인 구현과 내부의 전략 패턴, 코드에 의한 DI, 익명 내부 클래스 등의 기술은 최대한 감춰두고, 외부에는 꼭필요한 기능을 제공하는 단순한 메소드만 노출해주는 것이다.
JdbcTemplate : 스프링이 제공하는 JDBC 코드용 기본 템플릿이다. JDBC를 이용하는 DAO에서 사용할 수 있도록 준비된 다양한 템플릿과 콜백을 제공한다.
public class UserDao {
...
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.dataSource = dataSource;
}
Template을 주입받고 → dataSource를 넘겨줘서 → CRUD를 하면 된다.