3장 템플릿

soplia080 gyp·2022년 5월 18일
0

토비의 스프링

목록 보기
3/4
post-thumbnail

3.1 다시 보는 초난감 DAO

UserDao.java

import org.springframework.dao.EmptyResultDataAccessException;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class UserDao {

    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void add(User user) throws 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();
    }

    public User get(String id) throws ClassNotFoundException, SQLException{
        Connection c = dataSource.getConnection();
        PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
        ps.setString(1, id);

        ResultSet rs = ps.executeQuery();

        User user = null;
        if(rs.next()){
            user = new User();
            user.setId(rs.getString("id"));
            user.setName(rs.getString("Name"));
            user.setPassword(rs.getString("password"));
        }

        rs.close();
        ps.close();
        c.close();

        if(user == null) throw new EmptyResultDataAccessException(1);
        return user;
    }

    public void deleteAll() throws SQLException{
        Connection c = dataSource.getConnection();
        PreparedStatement ps = c.prepareStatement("delete from users");
        ps.executeUpdate();
        ps.close();
        c.close();
    }

    public int getCount() throws SQLException{
        Connection c = dataSource.getConnection();
        PreparedStatement ps = c.prepareStatement("select count(*) from users");
        ResultSet rs = ps.executeQuery();
        rs.next();
        int count = rs.getInt(1);

        rs.close();
        ps.close();
        c.close();

        return count;
    }
}
  • 현재 UserDao의 문제점은 예외상황에 대한 처리다.

3.1.1 예외 기능을 갖춘 DAO

UserDao.java

 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();
                }catch (SQLException e){
                    
                }
            }
            if (c!= null){
                try{
                    c.close();
                }catch (SQLException e){
                    
                }
            }
        }
    }

JDBC 조회 기능의 예외처리

UserDao.java

public int getCount() throws SQLException{

        Connection c = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        try {
            c = dataSource.getConnection();
            ps = c.prepareStatement("select count(*) from users");
            rs = ps.executeQuery();
            rs.next();
            return  rs.getInt(1);
        } catch (SQLException e){
            throw e;
        }finally {
            if(rs!=null){
                try{
                    rs.close();
                }catch (SQLException e){

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

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

                }
            }
        }

3.2 변하는 것과 변하지 않는 것

3.2.1 JDBC try/catch/finally 코드의 문제점

  • 복잡한 try/catch/finally 블록이 반복된다.
  • 유지보수가 헬이다.
  • 모든 DAO 메소드에 대해 테스트도 힘들고 비효율적이다.

문제의 핵심은 중복되는 코드와 로직에 따라 확장되며 자주 변하는 코드를 잘 분리하는 작업이다.


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

 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();
                }catch (SQLException e){

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

                }
            }
        }
    }
  • 위 코드에서는 ps = c.prepareStatement("delete from users"); 만 변하는 부분이다.
  • 나머지는 변하지 않는 부분이다.

그럼 변하지 않는 부분을 효율적으로 재사용할 수 있는 방법은 무엇일까?

1. 메소드 추출

  • 자주 바뀌는 부분을 메소드로 독립시키는 방법이다.
  • 이런 경우, 보통 분리시킨 메소드를 다른 곳에서 재사용이 필요한 부분인 경우에 하므로 이득이 없다.

지금 해야될 것은 변하지 않는 부분재사용이 필요하다.

2. 템플릿 메소드 패턴의 적용

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

UserDaoDeleteAll.java

public class UserDaoDeleteAll extends UserDao{
    protected PreparedStatement makeStatement(Connection c) throws SQLException{
        PreparedStatement ps = c.prepareStatement("delete from users");
        return ps;
    }
}

추상 클래스가 된 UserDao.java

public abstract class UserDao {

    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;
    ...
}
  • OCP는 지켜지는 구조가 됐으나, 큰 문제가 있다.
  • DAO 로직마다 상속을 통해 새로운 클래스를 만들어야 한다.

  • 확장구조가 클래스를 설계한 시점에서 고정되어 버린다.(PreparedStatement 로직에 의존적)
    - 유연성이 떨어진다...
    • 템플릿 메소드의 한계

3. 전략 패턴의 적용

  • 전략 패턴은 OCP을 잘 지키는 구조이면서 템플릿 메소드보다 유연하고 확장성이 좋다.
  • 오브젝트를 둘로 분리하여 클래스 레벨에서는 인터페이스만 의족하도록 하는 패턴이다.

  • deleteAll() 메소드에서 변하지 않는 부분이 contextMethod()가 된다.

  • deleteAll()에서 변하는 부분은 PreparedStatement를 만들어주는 외부 기능이 전략(Strategy)가 된다.
    - ex) Connection.prepareStatement("delete from users"); 만 유연해야됨.

  • StatementStrategy.java

public interface StatementStrategy {
    PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}
  • DeleteAllStatement.java
public class DeleteAllStatement implements StatementStrategy{

    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("delete from users");
        return ps;
    }
}
  • UserDao.deleteAll()
public void deleteAll() throws SQLException{

        Connection c = null;
        PreparedStatement ps = null;

        try{
            c=dataSource.getConnection();

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


        }catch (SQLException e){
            throw e;
            ...
  • 컨텍스트(변하지 않는 코드의 틀)안에 구체 클래스가 있다.
    - DeleteAllStatement strategy = new DeleteAllStatement();

컨텍스트가 OCP잘 지키려면 특정 구현 클래스가 컨텍스트에 존재해서는 안된다.

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

  • 전략패턴에 따르면 Context가 어떤 전략을 사용할지는 Client가 결정

  • Client가 구체적인 전략 하나를 선택하여 오브젝트로 만들고 Context에 전달

  • UserDao.jdbcContextWithStatementStrategy

 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){} }
        }
    }
  • UserDao.deleteAll()
public void deleteAll() throws SQLException{
		// 선정한 전략 클래스의 오브젝트 생성
        DeleteAllStatement st = new DeleteAllStatement();
        // 컨텍스트 호출, 전략 오브젝트 전달
        jdbcContextWithStatementStrategy(st);
}
  • 비록 컨텍스트와 컨텍스트는 클래스를 분리하진 않았지만, 의존관계와 책임으로 볼때 이상적인 구조다
  • 클라이언트가 컨텍스트가 사용할 전략을 정해서 전달하는 면에서 DI구조라고 볼 수 있다.

마이크로 DI

  • 때로는 원시적인 전략 패턴 구조를 따라 클라이언트가 오브젝트 팩토리의 책임을 가질 수도 있다.
  • DI가 매우 작은 단위의 코드와 메소드 사이에 일어나기도 한다.
  • DI의 장점을 단순화하여 IoC 컨테이너의 도움 없이 코드내에 적용한 경우를 마이크로DI라고도 한다.

여기까지 링크
https://github.com/SpringFrameworkStudy/LeeJooHyun/tree/main/2week/problemDao/version3.2.2/src


3.3 JDBC 전략 패턴의 최적화

3.3.2 전략과 클라이언트의 동거

문제점

  1. DAO 메소드마다 새로운 StatementStrategy 구현 클래스(위의 DeleteAllStatement)를 만들어 하며, 기존의 UserDao때보다 클래스 파일의 갯수가 많이 늘어난다.

    • 런타임 시 다이나믹하게 DI를 해준다는 점 외에는 템플릿 메소드 패턴을 적용했던 그닥..
  2. DAO 메소드에서 StatementStrategy에 전달할 추가적인 오브젝트가 있는 경우, 오브젝트를 전달받는 생성자, 인스턴스 변수를 새롭게 만들어야 한다.

    • insert, update, delete

로컬 클래스(메소드 레벨에 정의)

  • 1번의 문제점인 클래스 파일이 많아지는 문제를 해결할 수 있다.

  • StatementStrategy 구현 클래스를 UserDao 안에 정의하는 방법이다.

  • DeleteAllStatement, AddStatement는 UserDao 밖에서는 사용되지 않고, 둘 다 UserDao에서만 사용되며, UserDao 로직과 강하게 결합되어 있으므로 적용할 수 있는 방법이다.

  • 변경 전 UserDao.add()

public void add(User user) throws SQLException {
        AddStatement st = new AddStatement(user);
}
  • 변경 후 UserDao.add()
    public void add(User user) throws SQLException {
        class AddStatement implements StatementStrategy{
            User user;

            public AddStatement(User user) {
                this.user = user;
            }

            @Override
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                PreparedStatement ps = c.prepareStatement("insert into users() values(?,?,?)");
                ps.setString(1, user.getId());
                ps.setString(2, user.getName());
                ps.setString(3, user.getPassword());
                return ps;
            }
        }
        AddStatement st = new AddStatement(user);
        jdbcContextWithStatementStrategy(st);
}
  • 로컬 클래스는 클래스가 내부 클래스이기 때문에 자신이 선언된 곳의 정보에 바로 접근이 가능하다.
public void add(User user) throws SQLException {
        class AddStatement implements StatementStrategy{
            User user;

            public AddStatement(User user) {
                this.user = user;
            }
  • 위와 같은 User 정보를 전달하는 코드를 없앨 수 있다.
  • 다만, 내부 클래스에서 외부 변수를 사용할 때는 외부 변수는 반드시 final로 선언해야 한다.
public void add(final User user) throws SQLException {
        class AddStatement implements StatementStrategy{

            @Override
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                PreparedStatement ps = c.prepareStatement("insert into users() values(?,?,?)");
                
                // 로컬 클래스의 코드에서 외부의 메소드 로컬 변수에 접근 가능.
                ps.setString(1, user.getId());
                ps.setString(2, user.getName());
                ps.setString(3, user.getPassword());
                return ps;
            }
        }
        
        // 생성자 파라미터로 user를 전달하지 않아도 된다.
        AddStatement st = new AddStatement();
        jdbcContextWithStatementStrategy(st);
    }

메소드마다 추가해야 했던 클래스 파일을 줄일 수 있고,
로컬 변수를 바로 가져다 쓸 수 있다.

익명 내부 클래스

  • 클래스를 재사용할 필요가 없고, 구현한 인터페이스 타입으로만 사용할 경우 유용하다.

  • AddStatement 클래스는 add() 메소드에서만 사용할 용도로 만들어졌다. 클래스 이름도 굳히 필요 없으므로 제거해 보자.

  • 변경 후 UserDao.add()

public void add(final User user) throws SQLException {
        jdbcContextWithStatementStrategy(
                new StatementStrategy() {
                    @Override
                    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                        PreparedStatement ps = c.prepareStatement("insert into users() values(?,?,?)");
                        ps.setString(1, user.getId());
                        ps.setString(2, user.getName());
                        ps.setString(3, user.getPassword());
                        return ps;
                    }
                }
        );
    }

3.4 컨텍스트와 DI

3.4.1 JdbcContext의 분리

  • 전략패턴의 구조
    • UserDao 메소드 -> 클라이언트
    • 익명 내부 클래스 -> 개별적인 전략
    • jdbcContextWithStatementStrategy() 메소드 -> 컨텍스트

jdbcContextWithStatementStrategy() 는 다른 DAO에서도 사용이 가능하므로 UserDao 클래스 밖으로 독립시켜 모든 DAO 클래스가 사용할 수 있도록 해보자.

클래스 분리

  • JdbcContext.java
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){} }
        }
    }
}
  • UserDao.java
public class UserDao {

    private JdbcContext jdbcContext;

    public void setJdbcContext(JdbcContext jdbcContext) {this.jdbcContext = jdbcContext;}

    public void add(final User user) throws SQLException {

        this.jdbcContext.workWithStatementStrategy(
                new StatementStrategy() { .. } 
        );
}
  • JDBC 로직을 따로 클래스로 뺀다.

빈 의존관계 변경

  • JdbcContext는 JDBC 컨텍스트를 제공해주는 서비스 오브젝트로 바뀔일이 없으므로 인터페이스로 구현하지 않고 DI를 적용한다.
    <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3307/problem"/>
        <property name="username" value="root"/>
        <property name="password" value="1234"/>
    </bean>
    <bean id="userDao" class="UserDao">
        <property name="dataSource" ref="dataSource"/>
        <property name="jdbcContext" ref="jdbcContext"/>
    </bean>
    <bean id="jdbcContext" class="JdbcContext">
        <property name="dataSource" ref="dataSource"/>
    </bean>

3.4.2 JdbcContext의 특별한 DI

  • 인터페이스를 적용한 DI 경우, 코드에서 직접 클래스를 사용하지 않고, 설정을 변경하는 것만으로도 다양한 의존 오브젝트를 사용할 수 있다.
  • JdbcContext 같은 경우는 클래스로 바로 DI를 적용했으므로 DI방식을 적용해도 구현 클래스를 변경하기는 힘들다.

인터페이스를 적용하지 않고 DI를 하는것.. 문제가 있지 않나?

상관은 없다. 상황에 따라 사용하지만 인터페이스를 적용한 DI를 권고한다. 하지만 클래스로 DI를 적용해야 하는 상황도 있다. 그 방법들을 알아보자.

스프링 빈으로 DI

  • 인터페이스를 사용하지 않았으므로 온전한 DI라고 보기는 힘들지만, 객체의 생성, 관계설정에 대한 제어권을 외부로 위임했다는 점에서는 넓은 의미로 DI라고 할 수 있다.

그럼 굳이 왜 DI 구조로 만들어야 했을까?

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

    • JdbcContext의 필드인 DataSource는 읽기 전용이므로 싱글톤이되도 문제없다.
    • JdbcContext는 JDBC 컨텍스트 메소드를 제공해주는 역할로 여러 오브젝트에서 공유되어 사용되는것이 이상적이다.
  2. JdbcContext가 DI를 통해 다른 빈에 의존하고 있기 때문이다.

    • JdbcContext는 DataSource빈을 주입 받도록 설계되어 있다.
    • DI의 성립조건은 주입되는 오브젝트, 주입받는 오브젝트 모두 스프링 빈으로 등록 되어야 한다.
    • JdbcContext는 다른 빈을 DI 받기 위해서 스프링 빈으로 등록되어야 한다.

위 2가지 이유는 인터페이스를 사용해도 해결되는데, 왜 굳이 인터페이스를 사용하지 않는가?

  • UserDao, JdbcContext는 긴밀한 관계를 가지며, 강한 응집도를 가진다.
  • 둘다 JDBC를 사용해야만 하며, JPA, MyBatis를 써야하는 경우 둘다 코드를 바꿔야 한다.

코드를 이용하는 수동 DI

인터페이스가 아닌 구현체를 스프링 빈으로 등록하는게 맘에 안들면 해당 방법이 있다.

UserDao내부에서 직접 DI를 적용하는 방법이다.

  • 대신 싱글톤으로 만드는 것은 포기해야 한다. DAO마다 JdbcContext 오브젝트가 꼭 하나씩 있어야 한다.
  • 또한, JdbcContext를 스프링 빈으로 등록하지 않았으므로 JdbcContext의 생성, 초기화를 UserDao가 담당하는 것이 적당하다.
  • JdbcContext는 Datasource 인터페이스를 통해 간적으로 의존하고 있는데, 문제는 DI를 제공받으려면 자신도 빈이 되야한다.

이런 경우, JdbcContext에 대한 제어권을 가진 UserDao에게 DI도 맡겨야 한다.

  • JdbcContext에 주입해줄 DataSource는 UserDao가 대신 DI 받도록 하고 내부적으로 UserDao 오브젝트를 JdbcContext에 DI한다.

  • UserDao.java

public class UserDao {

    private JdbcContext jdbcContext;
	
    // 수정자 메소드이면서, JdbcContext에 대한 생성, DI작업을 동시에 수행한다.
    public void setDataSource(DataSource dataSource) {
        this.jdbcContext = new JdbcContext();
        
        // 의존 오브젝트 DI
        this.jdbcContext.setDataSource(dataSource);
    }
  • setDataSource() 메소드는 DI컨테이너가 DataSource 오브젝트를 주입해줄 때 호출된다.
  • applicationContext.xml
 <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3307/problem"/>
        <property name="username" value="root"/>
        <property name="password" value="1234"/>
    </bean>
    <bean id="userDao" class="UserDao">
        <property name="dataSource" ref="dataSource"/>
    </bean>
  • 이 방법을 적용하면 굳이 인터페이스를 두지 않아도 DI가 가능하며, 내부에서 직접 다른 오브젝트에 대한 DI가 가능해진다.

위 2가지 방법 모두 장단점이 존재하며, 알잘딱으로 적용하면 되것다.


3.5 템플릿과 콜백

템플릿/콜백 패턴 - 전략 패턴 + 익명 내부 클래스를 활용한 방식

전략 패턴의 컨텍스트 - 템플릿
익명 내부 클래스로 만들어지는 오브젝트 - 콜백

템플릿

  • 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀
  • 프로그래밍에서는 고정된 틀 안에서 바꿀 수 있는 부분을 넣어서 사용하는 경우

콜백

  • 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트
  • 파라미터로 전달되나, 값을 참조하기 위함이 아닌 특정 로직을 담은 메소드를 실행시키기 위해 사용
  • 자바에서는 메소드 자체를 파라미터로 전달하는 방법이 없기 때문에 메소드가 담긴 오브젝트를 전달 -> functional object

3.5.1 템플릿/콜백의 동작 원리

구조

  • DI 방식의 전략 패턴 구조
  • DI 작업이 클라이언트가 템플릿의 기능을 호출함과 동시에 발생

3.5.2 편리한 콜백의 재활용

콜백의 분리와 재활용

  • 기존의 UserDao.java
public class UserDao {
	private DataSource dataSource;
		
	public void setDataSource(DataSource dataSource) {
		this.jdbcContext = new JdbcContext();
		this.jdbcContext.setDataSource(dataSource);

		this.dataSource = dataSource;
	}
	
	private JdbcContext jdbcContext;
	
	public void add(final User user) throws SQLException {
		this.jdbcContext.workWithStatementStrategy(
				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;
					}
				}
		);
	}


	public User get(String id) throws SQLException {
		Connection c = this.dataSource.getConnection();
		PreparedStatement ps = c
				.prepareStatement("select * from users where id = ?");
		ps.setString(1, id);
		
		ResultSet rs = ps.executeQuery();

		User user = null;
		if (rs.next()) {
			user = new User();
			user.setId(rs.getString("id"));
			user.setName(rs.getString("name"));
			user.setPassword(rs.getString("password"));
		}

		rs.close();
		ps.close();
		c.close();
		
		if (user == null) throw new EmptyResultDataAccessException(1);

		return user;
	}

	public void deleteAll() throws SQLException {
		this.jdbcContext.workWithStatementStrategy(
			new StatementStrategy() {
				public PreparedStatement makePreparedStatement(Connection c)
						throws SQLException {
					return c.prepareStatement("delete from users");
				}
			}
		);
	}

	public int getCount() throws SQLException  {
		Connection c = dataSource.getConnection();
	
		PreparedStatement ps = c.prepareStatement("select count(*) from users");

		ResultSet rs = ps.executeQuery();
		rs.next();
		int count = rs.getInt(1);

		rs.close();
		ps.close();
		c.close();
	
		return count;
	}
}
  • 위의 코드에서 메소드별로 변하는 부분은 SQL부분이다.

  • deleteAll() 메소드에서는 "delete from users"라는 문자열로 된 SQL 문장만 파라미터로 넘기면 원하는 기능이 동작할 것이다.

  • 최종적으로는 아래의 형태가 될 것이다.

public void deleteAll() throws SQLException {
		executeSql("delete from users");
}

변하지 않는 부분을 분리

  • JdbcContext.java
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) {} }
		}
	}
  • UserDao.java

public void deleteAll() throws SQLException {
		executeSql("delete from users");
}

public void executeSql(final String query) throws SQLException {
		this.jdbcContext.workWithStatementStrategy(
			new StatementStrategy() {
				public PreparedStatement makePreparedStatement(Connection c)
						throws SQLException {
					return c.prepareStatement(query);
				}
			}
		);
	}
  • SQL을 담은 파라미터(String query)를 final로 선언하여 익명 내부 클래스인 콜백 안에서 직접 사용할 수 있게만 하면 된다.

콜백과 템플릿의 결합

  • executeSql()을 UserDao 뿐만 아니라 다른 Dao에서도 사용고자 한다.

  • executeSql(final String query) 메소드도 JdbcContext로 옮기면 아래의 형태가 된다.

  • JdbcContext.java

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 void executeSql(final String query) throws SQLException {
		this.jdbcContext.workWithStatementStrategy(
			new StatementStrategy() {
				public PreparedStatement makePreparedStatement(Connection c)
						throws SQLException {
					return c.prepareStatement(query);
				}
			}
		);
}
  • 하나의 목적을 위해 서로 긴밀하게 연관되어 동작하는 응집력이 강한 코드들은 위와 같이 한 군데로 모여 있는 것이 낫다.
  • UserDao.java
public void deleteAll() throws SQLException {
		this.jdbcContext.executeSql("delete from users");
}
  • 이제 모든 Dao 메소드에서 jdbcContext 클래스의 executeSql()메소드로 SQL문장을 넘기면 간단히 JDBC API를 사용할 수 있게 된다.

3.6 스프링의 JdbcTemplate

  • 스프링이 제공하는 JDBC 코드용 기본 템플릿이다.
  • 앞에서 배웠던 템플릿/콜백 기술로 구현되어있다고 보면 된다.

jdbcConext -> JdbcTemplate로 변경

public class UserDao{
	...
    private JdbcTemplate jdbcTemplate;
    
    
	public void setDataSource(DataSource dataSource){
    	this.jdbcTemplate = new JdbcTemplate(dataSource);
        this.dataSource = dataSource;
    }

3.6.1 update()

위 링크를 보면 템플릿/콜백 패턴으로 sql을 전달하는 로직을 볼 수있다.

public void deleteAll() {
		this.jdbcTemplate.update("delete from users");
}

ex)

  • UserDao.add()
public void add(final User user) throws SQLException {
		this.jdbcContext.workWithStatementStrategy(
				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;
					}
				}
		);
	}
public void add(final User user) {
		this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
						user.getId(), user.getName(), user.getPassword());
}

관련 메소드 코드다.

3.6.2 queryForInt()

  • Integer 타입의 결과를 가져올 수 있는 SQL문장을 전달하면 ResultSet을 통해 결과 값을 가져온다.

    스프링 3.2.2 버전부터 deprecated 되었고, queryForObject()로 사용을 권장한다고 한다.

3.6.3 queryForObject()

  • ResultSet에서 getCount() 같은 단순한 값 외에 복잡한 오브젝트도 반환한다.
  • 로우의 개수가 하나가 아닐 시 예외를 던진다.
    ex) select * from users where id = ? 같은 sql문 return값 반환시 사용

3.6.4 query()

  • queryForObject()는 쿼리의 결과가 로우 하나일 시 사용
  • query() 는 여러 개의 로우가 결과로 나오는 일반적인 경우에 사용

UserDao.getAll()

public List<User> getAll() {
		return this.jdbcTemplate.query("select * from users order by id",
				new RowMapper<User>() {
					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;
					}
				});
}
  • 첫번째 파라미터는 실행할 SQL, 마지막 파라미터는 RowMapper 콜백이다.
  • ResultSet의 모든 row를 열람하면서 각각 RowMapper 콜백을 호출해 row의 내용을 User타입 오브젝트로 매핑해서 돌려준다.

3.6.5 재사용 가능한 콜백의 분리

  • UserDao의 get(), getAll() 을 보면 RowMapper의 내용이 비슷한 것을 볼 수 있다.

  • 이 RowMapper를 분리하는 내용이다.

profile
천방지축 어리둥절 빙글빙글 돌아가는

0개의 댓글