3장 템플릿
객체지향 설계의 핵심 원칙인 OCP원칙은 확장에는 열려 있고 변경에는 닫혀있도록 설계하는 원칙이다.
이 원칙은 코드에서 고정되어 변하지않으려는 성질을 가진 코드와, 변경을 통해 다양해지고 확장하려는 성질을
나누어서 효율적인 구조로 만들어 주는 방식이다.
템플릿이란 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 독립적으로 추려내서 활용하는 방법을 의미한다.
3.1 다시보는 초난감 DAO
public class UserDao {
private DataSource dataSource;
public UserDao() {}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = dataSource.getConnection();
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());
ps.executeUpdate();
ps.close();
c.close();
}
}
- UserDao의 코드는 DB 연결과 관련된 개선 작업은 진행하였지만, 예외 상황에 대한 처리가 남아 있다.
3.1.1 예외처리 기능을 갖춘 DAO
JDBC 코드에는 반드시 예외처리를 구현해서 리소스를 반환하도록 만들어줘야 한다. 그렇지 않으면 예외 발생으로 리소스 반환을 하지 못하고 과부화 등 시스템에 심각한 문제를 가져올 수 있다.
public void deleteAll() throws SQLException {
Connection c = dataSource.getConnection();
// 예외 발생 시 Connection과 PreparedStatement 리소스 반환 불가!
PreparedStatement ps = c.prepareStatement("delete from users");
ps.executeUpdate();
ps.close();
c.close();
}
- 다음 코드에서 예외가 발생한다면
close()
를 호출하지 않고, 메서드를 빠져나간다.
서버는 일반적으로 제한된 DB 커넥션을 만들어 재사용 가능한 풀로 만들어서 관리한다.
즉, 매번 사용한 커넥션을 반환해줘야지 다음 커넥션 요청에 재사용할 수 있다. 그렇지 않으면, 리소스 부족 현상이 발생하며 오류로 직결하게 된다. 예외처리를 통해 리소스를 반드시 반환하도록 만들어보자.
public void deleteAll() 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 {
if (ps != null) {
try {
ps.close(); // close() 도 SQLExeption 발생 가능성 존재
} catch (SQLException e) {
}
}
if (c != null) {
try {
c.close();
} catch (SQLException e) {
}
}
}
}
finally
는 try 블록을 수행한 후 반드시 수행하는 코드를 넣을 때 사용한다.
코드를 살펴보면 null 체크를 하고 있다. 그 이유는 어디서 예외가 발생할지 모르기 때문이다. getConnection()
에서 예외가 발생한다면, c는 null 상태일 것이고, null을 close하면 NPE
가 발생한다.
3.2 변하는 것과 변하지 않는 것
위에서 예외처리를 적용해보면, 한숨부터 나올 것이다. 추가로 ResultSet
까지 사용하는 경우라면 복잡한 코드로만 만들어질 뿐이다. 다양한 방법으로 변하는 것과 변하지 않는 것을 분리하여 개선해보자.
메서드 추출 문제점
먼저 가장 간단하게 생각해볼 부분은 변하는 부분을 메서드로 빼는 것이다.
public void deleteAll() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = makeStatement(c); // 메서드 추출
} catch (SQLException e) {
...
}
}
private PreparedStatement makeStatement(Connection c) throws SQLException {
PreparedStatement ps;
ps = c.prepateStatement("delete from users");
return ps;
}
- 자주 바뀌는 부분을 메서드로 독립시켜도 재사용이 불가능하기 때문에 별 이득이 없어보인다.
템플릿 메서드 패턴 문제점
템플릿 메서드 패턴은 상속을 통해 기능을 확장해서 사용하는 패턴이다.
변하지 않는 부분은 슈퍼 클래스에 두고, 변하는 부분은 추상 메서드로 정의해서 재정의하도록 하는 것이다.
public abstract class AbstractUserDao {
abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;
}
- UserDao를 다음과 같이 추상클래스로 만들고, 자식 클래스에서 재정의하도록 구현하자.
public class UserDaoDeleteAll extends AbstractUserDao {
@Override
protected PreparedStatement makeStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("delete from users");
return ps;
}
}
- 이제 추상클래스를 통해 사용 용도에 맞춰서
makeStatement
를 구현하여 확장에 용이해졌다.
뭔가 찜찜하다. UserDao건, MemberDao건 입맛대로 구현하여 확장에 열려있는 OCP 원칙을 그럭저럭 지켰지만,
JDBC 메서드 마다 새로운 서브 클래스를 만들어서 구현해야 된다. 장점보다 단점이 많아보인다.
템플릿 메서드의 단점은 하나 더 있다. 이미 클래스를 설계하는 시점에 확장 구조가 고정되어 버린다는 것이다.
서브클래스들이 이미 그 관계가 결정되어 있어, 컴파일 시점에 그 관계가 결정되어 있기 때문에 유연성이 떨어진다.
전략 패턴 적용
OCP 원칙을 잘 지키는 구조이면서도, 템플릿 메서드 패턴보다 유연하고 확장성이 뛰어난 것이 전략 패턴이다.
전략 패턴은 변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방식이다.
- 컨텍스트에는 일정한 구조를 가지고 동작하다가, 특정 확장 기능은 전략 인터페이스의 독립된 전략 클래스에 위임하는 것이다.
deleteAll 기능을 구현하기 위해 필요한 구조를 생각해보자.
- DB 커넥션 가져오기
- PrepareStatement를 만들어줄 외부 기능 호출하기
- 전달받은 PreparedStatement 실행하기
- 예외가 발생하면 이를 다시 메서드 밖으로 던지기
- 모든 경우에 만들어진 PreparedStatement와 Connection을 반환하기
여기서 두 번째 작업이 바로 전략 패턴에서 사용되는 전략이라고 볼 수 있다.
전략 패턴의 구조를 따라 인터페이스로 만들어두고, 생성 전략을 호출해주면 된다.
public interface StatementStrategy {
PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}
- 이 인터페이스를 상속해서 실제 바뀌는 부분인
PreparedStatement
를 생성하는 클래스를 만들어보자.
public class DeleteAllStatement implements StatementStrategy {
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("delete from users");
return ps;
}
}
- 이제 확장된
PreparedStategy
전략인DeleteAllStatement
를 만들었다. 이어서 전략 패턴을 적용해보자.
public void deleteAll() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
StatementStrategy strategy = new DeleteAllStatement(); // 구현체가 명시되어있네?
ps = strategy.makePreparedStatement(c);
ps.executeUpdate();
} catch (SQLException e) {
...
}
- 전략 패턴 적용을 통해 필요에 따라 컨텍스트는 유지되면서, 전략을 바꿔 사용할 수 있다고 생각했지만 뭔가 이상하다.
- 컨텍스트가 전략 인터페이스 뿐아니라, 구현체까지 알고 있는 것은 OCP에 잘 들어맞는다고 볼 수 없다.
마이크로 DI 적용 클라이언트/컨텍스트 분리
전략 패턴의 문제를 보완하기 위해 마이크로 DI 적용을 고민해보자.
전략 패턴에 따르면, Context가 어떤 전략을 사용하게 할 것인지는 앞단에 Client의 역할이다.
- 다음과 같이 Client가 전략을 제공해서 필요로 하는 구현체를 제공해주는 방법을 사용하자.
public void jdbcContextWithStratementStrategy(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) {
}
}
}
}
- 클라이언트로부터 전략 오브젝트를 제공 받고, try/catch/finally 구조로 만들어진 컨텍스트 내에서 작업을 수행한다.
// 클라이언트 책임을 담당하게 된 deleteAll() 메서드
public void deleteAll() throws SQLException {
StatementStrategy st = new DeleteAllStatement(); // 전략 클래스와 오브젝트 생성
jdbcContextWithStratementStrategy(st); // 전략 오브젝트 전달
}
- 클라이언트가 된
deleteAll()
메서드에서 사용할 전략 오브젝트를 만들고 호출하자.
일반적으로 DI는 의존관계에 있는 두 개의 오브젝트와 DI 컨테이너, 클라이언트 사이에서 일어난다.
하지만 때로는 지금과 같이 원시적인 전략 패턴 구조를 따라 클라이언트가 오브젝트 팩토리의 책임을 함께 지고 있을 수 있다.
이런 경우 DI 같아보이지 않지만, 세밀하게 관찰해보면 작은 단위지만 DI가 이뤄지고 있음을 알 수 있다.
이렇게 DI의 장점을 단순화해서 IoC 컨테이너의 도움 없이 적용한 경우를 마이크로 DI 또는 수동 DI 라고 한다.
이제 구조적으로 완벽한 전략 패턴의 모습을 갖추게 되었다.
클라이언트와 컨텍스트는 클래스를 분리하지 않았지만, 이상적인 관계를 갖고 있다.
이 구조를 기반으로 UserDao의 본격적인 개선 작업의 사전 준비가 완료되었다.
3.3 JDBC 전략 패턴의 최적화
전략 패턴을 사용해 독립된 JDBC 작업 흐름이 담긴 jdbcContextWithStratementStrategy
메서드를 만들었고,
DAO 메서드들이 서로 공유할 수 있게 되었다. 이어서 다른 메서드에도 적용해보자.
3.3.1 전략 클래스의 추가 정보
add() 메서드에도 적용을 해보자. 메서드에서 변하는 부분인 PreparedStatement
를 AddStatemet 클래스로 옮겨 담자.
- 클래스를 분리해보니 User 객체에 대한 정보가 없어서 컴파일 에러가 발생한다.
public class AddStatement implements StatementStrategy {
User user;
public AddStatement(User user) {
this.user = user;
}
}
- 생성자를 통해 User 정보를 받아 넣어주도록 처리하면 컴파일 에러를 해결할 수 있다.
public void add(User user) throws ClassNotFoundException, SQLException {
StatementStrategy st = new AddStatement(user);
jdbcContextWithStratementStrategy(st);
}
- UserDao에서 add 메서드를 전략패턴으로 변경하자.
이어서 전략패턴으로 변경한 UserDao가 테스트 통과하는지 확인해보자.
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.junit.Before;
import org.junit.Test;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import springbook.user.dao.UserDao;
import springbook.user.domain.User;
public class UserDaoTest2 {
private UserDao dao;
private User user1;
private User user2;
private User user3;
@Before
public void setUp() {
dao = new UserDao();
DataSource dataSource = new SingleConnectionDataSource("jdbc:mysql://localhost/tobi", "root", "", true);
dao.setDataSource(dataSource);
this.user1 = new User("test1", "a", "123");
this.user2 = new User("test2", "b", "123");
this.user3 = new User("test3", "c", "123");
}
@Test
public void addAndGet() throws SQLException, ClassNotFoundException {
User user = new User();
user.setId("sas1232");
user.setName("jalgayo");
user.setPassword("jalgayo");
dao.add(user);
User user2 = dao.get(user.getId());
assertThat(user2.getName(), is(user.getName()));
assertThat(user2.getPassword(), is(user.getPassword()));
}
@Test
public void count() throws SQLException, ClassNotFoundException {
dao.deleteAll();
assertThat(dao.getCount(), is(0));
dao.add(user1);
dao.add(user2);
assertThat(dao.getCount(), is(2));
User userget1 = dao.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
User userget2 = dao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
dao.deleteAll();
}
@Test(expected = EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException, ClassNotFoundException {
dao.deleteAll();
assertThat(dao.getCount(), is(0));
dao.get("unknown_id");
}
}
- add(), deleteAll() 메서드를 실행 시켜주고, 매번 실행마다 데이터소스를 주입해주자.
- 테스트가 정상 통과되는 것을 확인할 수 있다.
3.3.2 전략과 클라이언트의 동거
현재 만들어진 구조에 두 가지 문제가 있다.
- DAO 메서드마다 새로운 전략 구현 클래스를 만들어야 한다.
- User와 같은 전략 오브젝트에 전달할 부가 정보가 있을 때 번거롭게 생성자와 인스턴스 변수를 만들어야 한다.
로컬 클래스
클래스 파일이 많아지는 문제는 간단히 해결할 방법이 있다.
UserDao 클래스 안에 내부 클래스로 정의해버리는 방법이다.
add() 메서드 내의 로컬 클래스로 이전한 AddStatement
public void add(final User user) throws ClassNotFoundException, SQLException {
class AddStatement implements StatementStrategy {
@Override
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;
}
}
StatementStrategy st = new AddStatement(); // 생성자 없어도 user 정보 사용 가능
jdbcContextWithStratementStrategy(st);
}
- AddStatement 클래스를 로컬 클래스로 메서드 안에 집어 넣었다.
- add(final User user) 메서드안에 User 정보를 받아와서 로컬 클래스에서 사용할 수 있어 직접 생성자를 만들어주지 않아도 사용 가능하다.
익명 내부 클래스
로컬 클래스는 메서드 안에서만 사용된다. 따라서 익명 내부 클래스를 사용하면 더 간결하게 만들 수 있다.
public void add(final User user) throws ClassNotFoundException, SQLException {
jdbcContextWithStratementStrategy( new StatementStrategy() {
@Override
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;
}
});
}
- 익명 내부 클래스로 처리하여 로컬클래스를 더욱 간결하게 사용할 수 있다.
3.4 컨텍스트와 DI
전략 패턴의 구조로 보자면 UserDao
의 메서드가 클라이언트이고, 익명 내부 클래스로 만드는 것이 개별적 전락이며,
jdbcContextWithStratementStrategy
메서드는 컨텍스트가 된다.
여기서의 컨텍스트는 다른 DAO에서도 재사용이 가능하다. 따라서 UserDao 클래스 밖으로 독립시켜 재사용할 수 있도록 만들자.
클래스 분리
분리해서 만들 클래스의 이름은 JdbcContext
라고 하자. UserDao에 있던 메서드도 새로 만들어주자.
JdbcContext 클래스
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import javax.sql.DataSource;
public class JdbcContext {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
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) {
}
}
}
}
}
- 이렇게 밖으로 분리한다면, 다른 DAO에서도 재사용할 수 있다. 먼저 UserDao에 적용하자.
JdbcContext를 DI 받아서 사용하도록 만든 UserDao
public class UserDao {
private JdbcContext jdbcContext;
public void setJdbcContext(JdbcContext jdbcContext) {
this.jdbcContext = jdbcContext;
}
public void add(final User user) throws ClassNotFoundException, SQLException {
this.jdbcContext.workWithStatementStrategy( new StatementStrategy() {
@Override
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;
}
});
}
}
- UserDao 클래스에게 JdbcContext를 DI받도록 만들어서 사용하자.
빈 의존관계 변경
새롭게 작성된 오브젝트 간의 의존관계를 살펴보고 스프링 설정에 적용해보자.
- JdbcContext를 적용한 후 UserDao의 의존관계는 다음과 같다.
applicationContext.xml 설정
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost/tobi?autoReconnect=true"></property>
<property name="username" value="root"></property>
<property name="password" value=""></property>
</bean>
<bean id="userDao" class="springbook.user.dao.UserDao">
<property name="jdbcContext" ref="jdbcContext"/>
</bean>
<bean id="jdbcContext" class="springbook.user.dao.JdbcContext">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
- 위의 다이아그램과 같이 UserDao가 JdbcContext를 바라보고, JdbcContext가 DataSouce를 바라보도록 빈 설정을 등록하자.
3.4.2 JdbcContext의 특별한 DI
JdbcContext에 사용한 DI는 인터페이스를 사용하지 않고 적용했다.
그렇기에 UserDao의 입장에선 클래스 레벨에서 의존관계가 결정 되었다.
이렇게 인터페이스를 사용하지 않고 DI를 적용하는 것은 어색하기도하다. 먼저 인터페이스를 사용하는 이유를 살펴보자.
인터페이스가 없다는 것은 UserDao와 JdbcContext가 항상 높은 결합도를 갖고 있다는 의미이다.
UserDao가 JDBC 방식이 아닌 다른 ORM 등을 사용한다면, JdbcContext를 통째로 변경해야 한다는 점도 고려하자.
3.5 템플릿과 콜백
지금까지 UserDao와 StatementStrategy, JdbcContext를 이용해 만든 코드는 일종의 전략 패턴이 적용된 것이라고 볼 수 있다.
복잡하지만 바뀌지 않는 패턴을 갖는 흐름이 존재하고, 일부분만 바꿔서 사용해야하는 경우 적합한 구조다.
스프링에서 이러한 방식을 템플릿/콜백 패턴이라고 부른다.
전략 패턴의 컨텍스트를 템플릿이라 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 부른다.
3.5.1 템플릿/콜백의 동작원리
템플릿은 고정된 작업 흐름을 가진 코드를 재사용한다는 의미이고, 콜백은 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트를 의미한다.
얼핏보면 전략 패턴과 동일해보이지만, 차이점이 존재한다. 템플릿/콜백 패턴의 콜백은 보통 단일 메서드 인터페이스를 사용한다.
템플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출되는 경우가 일반적이기 때문이다.
- 클라이언트의 역할은 템플릿 안에서 실행될 콜백 오브젝트를 만들고 제공하는 것이다.
- 템플릿은 콜백이 돌려준 정보를 사용해 작업을 수행하고, 최종 결과를 클라이언트에게 제공해준다.
JdbcContext에 적용된 템플릿/콜백
JdbcContext에 적용된 템플릿/콜백 구조는 다음과 같다. 템플릿의 작업 흐름이 복잡한 경우에는 한 번 이상의 콜백을 호출하기도 하고, 여러 개의 콜백을 클라이언트로부터 받아서 사용하기도 한다.
콜백의 분리와 재활용
템플릿/콜백 메서드를 적용하는 과정에서 코드의 중복이 발생한다. 이를 간결하게 변경해보자.
JdbcContext에 메서드 추가
public void executeSql(final String query) throws SQLException {
workWithStatementStrategy(new StatementStrategy() {
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.prepareStatement(query);
}
});
}
변경후 deleteAll() 메서드
public void deleteAll() throws SQLException {
this.jdbcContext.executeSql("delete from users");
}
- 다음과 같이 변경하면서, 모든 DAO 메서드에서 단순하게 사용할 수 있다.
3.6 스프링의 JdbcTemplate
템플릿과 콜백의 기본적인 원리와 동작과정에 대해 알아봤다.
스프링은 JDBC를 이용하는 DAO에 맞게 다양한 템플릿과 콜백을 제공한다. 그 중 기본 템플릿이 jdbcTemplate
이다.
UserDao에 JdbcTemplate을 적용시켜보자.
public class UserDao {
private DataSource dataSource;
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.dataSource = dataSource; // 적용을 안한 메서드를 위해 생성
}
}
- 다음과 같이 설정하면 템플릿을 사용할 준비가 완료된다.
3.6.1 JdbcTemplate update()
public void deleteAll() throws SQLException {
this.jdbcTemplate.update("delete from users");
}
public void add(final User user) throws ClassNotFoundException, SQLException {
this.jdbcTemplate.update("insert into users(id, name, password) values(?, ?, ?)",
user.getId(), user.getName(), user.getPassword());
}
- jdbcTemplate의 update() 메서드를 사용하면, SQL 쿼리를 바로 실행하도록 만들어준다.
3.6.2 queryForInt()
public int getCount() throws SQLException {
return this.jdbcTemplate.queryForInt("select count(*) from users");
}
- queryForInt 메서드를 사용하여 단 한줄로 결과값을 받아올 수 있다.
- 내부적으로는 ResultSet을 전달받아 ResultSetExtractor 콜백을 사용하여 값을 전달해준다.
3.6.3 queryForObject()
get()
메서드에 JdbcTemplate을 적용해보자.
JdbcTemplate은 ResultSet에 받아오는 객체를 처리하기 위한 queryForObject()를 제공한다.
public User get(String id) throws ClassNotFoundException, SQLException {
return this.jdbcTemplate.queryForObject("select * from users where id = ?",
new Object[] {id},
new RowMapper<User>() {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
return user;
}
});
}
public List<User> getAll() {
return this.jdbcTemplate.query("select * from users order by id",
new RowMapper<User>() {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
return user;
}
});
}
- 첫 번째 파라미터는 PreparedStatement를 만들기 위한 SQL을 넣어준다.
- 두 번째 파라미터에는 id 값에 바인딩할 값들을 넣어준다.
- 세 번째 파라미터에는 ResultSet을 전달받아 매번 콜백을 통해 여러 로우 결과를 가져올 수 있다.
- 만약 결과가 없는 경우 자동으로
EmptyResultDataAccessException
예외를 던져준다.
package springbook.user.dao;
import springbook.user.domain.User;
import java.sql.*;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
public class UserDao {
private DataSource dataSource;
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void add(final User user) throws ClassNotFoundException, SQLException {
this.jdbcTemplate.update("insert into users(id, name, password) values(?, ?, ?)",
user.getId(), user.getName(), user.getPassword());
}
public void deleteAll() throws SQLException {
this.jdbcTemplate.update("delete from users");
}
public User get(String id) throws ClassNotFoundException, SQLException {
return this.jdbcTemplate.queryForObject("select * from users where id = ?",
new Object[] {id},
userMapper);
}
public int getCount() throws SQLException {
return this.jdbcTemplate.queryForInt("select count(*) from users");
}
public List<User> getAll() {
return this.jdbcTemplate.query("select * from users order by id", userMapper);
}
private RowMapper<User> userMapper = new RowMapper<User>() {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
return user;
}
};
}
- 중복된 RowMapper를 메서드 분리해서 추출한다면, 다음과 같이 간결한 코드로 만들 수 있다.
3.7 정리
- 예외의 가능성이 있으면서 공유 리소스 반환이 필요한 코드는 반드시 try/catch/finally 블록으로 관리해야 한다.
- 일정한 작업 흐름이 반복되면서, 일부 기능만 바뀌는 코드가 존재한다면 전략 패턴을 적용하자. 바뀌지 않는 부분은 컨텍스트, 바뀌는 부분을 전략으로 만들고 인터페이스를 통해 유연하게 전략을 변경할 수 있도록 하자.
- 단일 전략 메서드를 갖는 전략 패턴이면서, 익명 내부 클래스를 사용하는 방식을 템플릿/콜백 패턴이라 한다.
- 스프링 JDBC 코드를 간결하게 사용하기 위해 스프링은 JdbcTemplate 기반으로 템플릿/콜백 패턴을 지원한다.
- 템플릿은 한 번에 하나 이상의 콜백을 사용하거나 호출할 수 있다.
- 템플릿/콜백을 설계할 때는 템플릿과 콜백 사이에 주고 받는 정보에 관심을 둬야 한다.
REFERENCES
- 토비스프링 3.1