토비의 스프링 3장- 템플릿

메이도·2023년 3월 21일

DB 풀: 제한된 개수의 DB 풀을 생성해 재사용 가능한 풀을 생성
개방폐쇄원칙: 어떤 부부는 변경을 통해 기능이 다양해지고 확장하려는 성질이 있고, 어떤 부분은 고정되어 있고 변하지 않으려는 성질이 있다. 변화의 특성이 다른 부분을 구분해주고, 다른 시점에 독립적으로 변경될 수 있는 효율적인 구조를 만드는 것.

중첩 클래스: 다른 클래스 내부에 정의되는 클래스
스태틱 클래스: 중첩클래스 중 독립적인 오브젝트로 생성 가능한 클래스
내부 클래스: 자신이 정의된 클래스 오브젝트 안에서만 만들어질 수 있는 클래
멤버 내부 클래스: 멤버 필드(로컬 변수)처럼 오브젝트 레벨에 정의(클래스 안에 구현 후 사용)
로컬 클래스: 메소드 레벨 정의
익명 내부 클래스: 이름 갖지 않음

템플릿: 어떤 목적을 위해 미리 만들어둔 모양이나 틀
콜백: 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트로 값 참조를 위함이 아닌 메소드 실행을 위해 사용한다. 자바에서는 메소드 자체를 전달하지 못해 메소드가 담긴 오브젝트를 전달한다.

db 연결 시의 예외 처리


PreparedStatement 실행 중 예외가 발생했어도 반드시 사용한 리소스를 반환하게 변경하자!-> 오류가 날 때마다 반환되지 못한 connection이 계속 쌓이면 커넥션 풀의 리소스가 모자란다.
try catch문을 사용해 SQL Exception을 잡고, finally 부분에서 ps와 c를 닫는다.

 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){//ps 수행 시 오류 날 시
                try{
                    ps.close();
                }catch (SQLException e){}
            }
            if(c!= null){//커넥션 할당 오류 날 시
                try {
                    c.close();
                }catch(SQLException e){}
            }
        }

        ps.close();
        c.close();
    }

위와 같이 getCount()에도 예외처리를 적용한다.

이와 같이 모든 메소드에 적용하면 모든 메소드마다 try catch 문이 반복된다.
많은 곳에서 중복되는 코드와 로직에 따라 확장되고 변하는 코드를 분리해야 한다.

분리와 재사용

템플릿 메소드 패턴

  1. userDao 함수에서 변하지 않는 부분은 그대로 두고 변하는 부분만 새로 메소드를 생성하기-> 분리한 메소드를 다른 곳에 재사용 불가
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;
 }
  1. 템플릿 메소드 패턴을 이용해 분리하기

템플릿 메소드 패턴: 상속을 통해 기능을 확장해서 사용하는 것. 변화하지 않는 부분을 부모에, 변화하는 부분을 자식이 구현한다.

abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;

UserDao를 상속한 UserDaoDeleteAll이라는 클래스를 생성해 makeStatement만 UserDaoDeleteAll만 구현하면 되지만, Dao 상속 클래스를 계속 생성해야 하며 클랫 설계 시점에 확장 구조가 고정되어 버려 유연성이 떨어진다.

전략 패턴

오브젝트를 둘로 분리해 인터페이스를 통해서만 의존하게 하는 것으로 템플릿 메소드 패턴보다 유연하고 확장성이 뛰어나다.

context에서 기본적인 구조(변하지 않는 맥락)를 갖고, strategy 인터페이스를 통해 확장해 외부의 독립된 전략 클래스에 위임하는 것

public interface StatementStrategy {
    PreparedStatement makePreparesStatement(Connection c) throws SQLException;
}

public class DeleteAllStatement implements StatementStragegy{
	public PreparedStatement makePreparesStatement(Connection c) throws SQLException{
	 PreparedStatement ps = c.prepareStatement(
                    "delete from users");
     return ps;
}

public void deleteAll() throws SQLException{
...
    try{
        c = dataSource.getConnection();
        StatementStrategy strategy = new DeleteAllStatement();
        ps = strategy.makePrepareStatement(c);
        ps.executeUpdate();
    }catch(SQLException e){
        ...
 }

이와 같이 하면 구체적인 전략 클래스를 코드 안에 명시하게 되어 컨텍스트가 구현 클래스를 직접 알게 된다.(컨텍스트가 deleteAll 이 됨)
-> 전략 패턴에서는 context가 어떤 전략을 사용할지는 context를 사용하는 client가 결정한다.

** 따라서!
context를 가지는 함수를 생성한다

public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException{
        Connection c = null;
        PreparedStatement ps = null;
        
        try {
            c = dataSource.getConnection();
            ps = stmt.makePreparesStatement(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 deleteAll() throws SQLException {
        StatementStrategy st = new DeleteAllStatement();
        jdbcContextWithStatementStrategy(st);
    }

-> 컨텍스트는 jdbcContextWithStatementStrategy이고, strategy가 DeleteAllStatement, deleteAll() 메소드가 클라이언트가 된다.

최적화

addStatement도 생성해보자

public class AddStatement implements StatementStrategy {
        User user;
        public AddStatement(User user){
            this.user = user;
        }
        public PreparedStatement makePreparesStatement(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(User user) throws SQLException {
        StatementStrategy st = new AddStatement(user);
        jdbcContextWithStatementStrategy(st);
    }
  • 멤버 내부 클래스로 add 함수 안에 AddStatement를 구현하자! -> 관련 코드를 함수 안에서 한꺼번에 확인 가능 + 생성자 불필요
public void add(final User user) throws SQLException {//내부 클래스에서 외부 로컬 변수 접근 위해 final 설정
        class AddStatement implements StatementStrategy {
            public PreparedStatement makePreparesStatement(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();
        jdbcContextWithStatementStrategy(st);
    }
  • 익명 클래스로 add와 deleteAll 함수 안에 구현
public void add(final User user) throws SQLException {//내부 클래스에서 외부 로컬 변수 접근 위해 final 설정
        jdbcContextWithStatementStrategy(new StatementStrategy() {
            public PreparedStatement makePreparesStatement(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 deleteAll() throws SQLException {
        jdbcContextWithStatementStrategy(
            new StatementStrategy() {
                public PreparedStatement makePreparesStatement(Connection c) throws SQLException {
                return c.prepareStatement(
                        "delete from users");
            }
        });
    }

JdbcContext 분리

스프링 IoC에서 JdbcContext를 주입하기

jdbcContext를 다른 dao에서 쓸 수 있도록 분리시키자
1. jdbcContextWithStatementStrategy 함수를 가지고 있는 외부의 JdbcContext 클래스를 만든다.
2. UserDao이 add 와 deleteAll 함수에 jdbcContext의 함수를 적용한다.
이때 JdbcContext는 인터페이스가 아니라 구체 클래스로 인터페이스와는 달리 구현 방법이 달라질 가능성이 없다.

의존관계 주입: 인터페이스를 사이에 두고 클래스 레벨에서 의존관계가 고정되지 않고, 런타임 시에 의존할 오브젝트와의 관계를 다이내믹하게 주입해주는 것
JdbcContext가 인터페이스를 사용하지 않는다는 건 UserDao와 JdbcContext가 매우 긴밀하게 결합돼 있다는 것!
JdbcContext도 싱글톤으로 만들고 datasource를 스프링 IoC에서 주입해준다는 것을 이유로 UserDao에 Di되게 하는 것이 좋다.

수동으로 JdbcContext 주입하기

UserDao의 setDataSource 함수에서 JdbcContext의 dataSource까지 주입한다.

public void setDataSource(DataSource dataSource) {
        this.jdbcContext = new JdbcContext();
        this.jdbcContext.setDataSource(dataSource);//수동 di
        this.dataSource = dataSource;
    }

템플릿과 콜백

템플릿/콜백 패턴: 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름 존재, 일부분만 자주 바꿔서 재사용하는 구조

  • 단일 메소드 인터페이스를 사용한다
  • 클라이언트는 콜백 오브젝트를 생성, 콜백이 참조할 정보 제공. 클라이언트가 템플릿 메소드 호출할 때 파라미터로 콜백 전달(메소드 레벨의 di)
  • 템플릿은 콜백이 돌려준 정보로 작업 마저 수행.

일반 di라면 템플릿에 인스턴스 변수를 만들어두고 사용할 의존 오브젝트를 수정자 메소드로 받아서 저장하고 사용하지만, 템플릿 콜백에서는 매번 메소드 단위로 오브젝트를 새롭게 전달받는다.

콜백 분리

UserDao의 함수 안에서 반복되는 sql 실행 부분을 분리한다.

public void executeSql(final String query) throws SQLException{
    this.jdbcContext.workWithStatementStrategy(
             new StatementStrategy() {
                 public PreparedStatement makePreparesStatement(Connection c) throws SQLException {
                     return c.prepareStatement(
                             query);
                 }
             }
        );
}// 이 부분은 JdbcContext로 빼자
    
public void deleteAll() throws SQLException {
   this.jdbcContext.executeSql("delete from users");
    }
  1. 반복되는 부분을 함수로 분리(context)
  2. 분리한 함수에서 사용할 전략을 인터페이스에 생성
  3. 클라이언트에 적용
    인터페이스는 분리 시 구현체를 매개변수로 줄 때 사용한다.
    DI란 인터페이스를 통해 의존 클래스(구현체)를 변경해 사용하는 것

JdbcTemplate 적용

최종적으로 UserDao에는 User 정보를 DB에 넣거나 조작하는 방법에 대한 로직만 있다. 테이블과 필드 정보가 바뀌면 UserDao의 대부분의 코드가 바뀜으로 응집도가 높다.
JDBC API 사용 방식, 예외처리, 리소스 반납(close), db 연결 가져오는 법은 모드 JdbcTemplate에 있다. 변경이 일어나도 UserDao에 영향을 주지 않는다. 따라서 낮은 결합도를 유지한다. 하지만 템플릿을 직접 이용해 템플릿 콜백 구현에 대한 강한 결합을 갖는다.

0개의 댓글