본문 바로가기
도서기록/토비의 스프링

3장 템플릿

by 코엘리 2025. 3. 22.
반응형

3.1 다시보는 초난감 DAO

3.1.1 예외처리 기능을 갖춘 DAO

public void deleteAll() throws SQLException {
  Connection c = dataSource.getConnection();
  PreparedStatement ps = c.prepareStatement("delete from users") ;
  ps.executeUpdate(); // 여기서 예외가 발생하면 바로 메소드 실행이 중단된다.
  ps.close();
  c.close() ;
 }
  • close() 메서드를 제대로 실행하지 못하고 에러가 발생하면 리소스를 반환하지 못한다.
  • 그래서 JDBC 코드에서는 어떤 상황에서도 가져온 리소스를 반환하도록 try/catch/finally 구문 사용을 권장하고 있다.
public void deleteAll() throws SQLException {
    Connection c = dataSource.getConnection();
    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) { //  close()도 SQLException 이 발생할 수 있는 메소드다.

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

        }
      } 
    }
  }

JDBC 조회 기능 예외처리

  • JDBC는 Connection, PreparedStatement 외에도 ResultSet이 추가되기 때문에 더 복잡해진다.
  public int getCount() throws SQLException {
    Connection c = dataSource.getConnection();
    PreparedStatement ps = null;
    ResultSet rs = null;
    try {
      c = dataSource.getConnection();
      ps = c.prepareStatement("DELETE 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 변하는 것과 변하지 않는 것

  • try/catch/finally 블록이 2중 중첩에 메소드마다 반복되는 문제점이 있다.

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

  • 가장 먼저 변하는 성격이 다른 것을 찾아보자.
public void deleteAll() throws SQLException {
  Connection c = dataSource.getConnection();
  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) {} }
  }
}
  • 만약 업데이트용 쿼리를 실행하는 메서드를 생각해보면 deleteAll() 과 다 비슷하고 일정 부분만 변경된다.

메소드 추출

public void deleteAll() throws SQLException {
  ...
  try {
    c = dataSource.getConnection();
    ps = c.prepareStatement("DELETE FROM users");
    ps.executeUpdate();
  } catch (SQLException e) {
    ...
}

  private PreparedStatement makeStatement(Connection c) throws SQLException {
    PreparedStatement ps;
    ps = c.prepareStatement("delete from users'’) ;
    return ps;
  }
  • 분리시킨 메소드가 다른 곳에서 재사용해야하는데, 뭔가 반대로 됐다.

템플릿 메소드 패턴의 적용

  • 템플릿 메소드 패턴은 상속을 통해 기능을 확장해서 사용하는 부분이다.
  • 변하지 않는 부분은 슈퍼 클래스에 두고 변하는 부분은 추상 메소드로 정의해둬서 서브 클래스에서 오버라이드하여 새롭게 정의해 쓰도록 하는 것이다.
    abstract protected PreparedStatement makeStatement(Connection c) throws
    SQLException;
  • 추출한 메서드를 추상 메소드로 선언하고, UserDao 클래스를 추상 클래스로 변경한다.
public class UserDaoDeleteAll extends UserDao (
  protected PreparedStatement makeStatement(Connection c) throws SQLException (
    PreparedStatement ps = c.prepareStatement("delete from users");
    return ps;
  }
}
  • 위에처럼 필요에 따라서 상속을 통해 구체적인 PreparedStatement를 바꿔서 사용할 수 있게 만드는 서브 클래스로 분리할 수 있다.
  • OCP 는 그럭저럭 지키는 구조이지만, DAO 로직마다 상속을 통해 새로운 클래스를 만들어야 하는 단점이 생긴다.

전략 패턴의 적용

  • OCP 를 잘 지키면서 템플릿 메소드 패턴보다 유연하고 확장성이 뛰어난 것이, 오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 하는 것이 전략 패턴이다.
  • 전략 패턴은 확장에 해당하는 변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방식이다.
  • deleteAll()은 JDBC를 이용해 DB 를 업데이트하는 변하지 않는 맥락을 갖고 있다.
public interface StatementStrategy (
    PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}
  • PreparedStatement를 만들어주는 외부 기능이 전략패턴에서의 전략이다.
  • 전략 패턴 구조를 따라 이 기능을 인터페이스로 만들어두고, 인터페이스 메소드를 통해 PrepareStatement 생성 전략을 호출해주면 된다.
public void deleteAll() throws SQLException (
  try (
    c = dataSource.getConnection();

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

    ps.executeUpdate();
  } catch (SQLException e) {
  ...
}
  • 전략 패턴은 필요에 따라 컨텍스트는 유지되면서 전략을 바꿔 쓸 수 있다는 것인데, 이미 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있다는 문제가 있다.
  • 인터페이스 뿐 아니라 특정 구현 클래스를 알고 있는 것은 OCP 에 맞지 않다.

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

  • 어떤 전략을 사용할 것인가는 Context를 사용하는 앞단의 Client가 결정하는게 일반적이다.
// 클라이언트가 컨텍스트 호출 시 넘겨줄 전략 파라미터
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){}}
  }
}

3.3 JDBC 전략 패턴 최적화

3.3.1 전략 클래스의 추가 정보

  • add() 메서드에도 적용해본다.
public class AddStatement implements StatementStrategy {
  public PreparedStatement makePreparedStatement(Connection c)
      throws SQLException {
    PreparedStatement ps =
        c.prepareStatement("insert into users(id, name , password) values(?,?,?)");
    ps.setString(1, user.getld());
    ps.setString(2, user.getName());
    ps.setString(3, user.getPassword()); // user 는 어디서 가져오는 것일까?
    return ps;
  }
}
  • add() 메서드 실행하려면 클라이언트로부터 User 타입 오브젝트를 받을 수 있도록 해야한다.
public class AddStatement implements StatementStrategy {
  User user;
  public AddStatement(User user) {
    this.user = user;
  }

  public PreparedStatement makePreparedStatement(Connection c)
      throws SQLException {
    ...
    ps.setString(1, user.getld());
    ps.setString(2, user.getName());
    ps.setString(3, user.getPassword());
    ...
  }
}
public void add(User user) throws SQLException {
  StatementStrategy st = new AddStatement(user);
  jdbcContextWithStatementStrategy(st);
}

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

DAO 메소드마다 새로운 StatementStrategy 구현 클래스를 만들어야 한다는 점(파일 개수가 늘어나는 점)과 User 와 같은 부가적인 정보가 있는 경우, 이를 위해 오브젝트를 전달받을 생성자와 인스턴스 변수를 번거롭게 만들어야 한다는 단점이 있다.

로컬 클래스

  • StatementStrategy 전략 클래스를 UserDao 클래스 안에 내부 클래스로 정의해버리면 클래스 파일이 많아지는 문제점을 막을 수 있다.
  • 클래스 파일을 줄이고 PreparedStatement 생성 로직을 볼 수 있다는 장점이 있다.
  • 또 AddStatement는 User 정보를 필요로 한데, 내부 메소드는 자신이 정의된 메소드의 로컬 변수에 직접 접근할 수 있기 때문에 이를 위해서 add() 메소드에서 이를 전달해줄 필요가 없다.
    • 다만 내부 클래스에서 외부 변수를 사용할 때는 외부 변수는 반드시 final 로 선언해줘야 한다.
    • user 파라미터는 메소드 내부에서 변경될 일이 없으므로 final 로 선언해도 무방하다.
public void add(final User user) throws SQLException {

  class AddStatement implements StatementStrategy {
    public PreparedStatement makePreparedStatement(Connection c)
        throws SQLException {
    ...
      ps.setString(1, user.getld());
      ps.setString(2, user.getName());
      ps.setString(3, user.getPassword());
    ...
    }
  }
  StatementStrategy st = new AddStatement(user);
  jdbcContextWithStatementStrategy(st);
}

익명 내부 클래스

  • AddStatement 클래스는 add() 메서드에서만 사용할 용도로 만들어졌지만, 익명 내부 클래스로 만들어보자.
  StatementStrategy st = new StatementStrategy() {
    public PreparedStatement makePreparedStatement(Connection c)
        throws SQLException {
    ...
      ps.setString(1, user.getld());
      ps.setString(2, user.getName());
      ps.setString(3, user.getPassword());
    ...
    }
  }
public void add(final User user) throws SQLException {

  jdbcContextWithStatementStrategy(
      new StatementStrategy() {
      public PreparedStatement makePreparedStatement(Connection c)
          throws SQLException {
      ...
        ps.setString(1, user.getld());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());
      ...
      }
    }
  );
}
  • 익명 클래스 오브젝트는 딱 한번 사용할 테니 변수에 담지 않고 jdbcContextWithStatementStrategy() 메소드 파라미터에서 바로 생성하게 하면 더 간결해진다.

3.4 컨텍스트와 DI

3.4.1 JdbcContext의 분리

  • 전략 패턴 구조에서 UserDao 메소드가 클라이언트이고, 익명 내부 클래스로 만들어지는 것이 개별적인 전략이고, jdbcContextWithStatementStrategy() 메소드는 컨텍스트다.
  • 컨텍스트 메소드는 UserDao 내의 PreparedStatement를 실행하는 기능을 가진 메소드에서 공유할 수 있다.
  • jdbcContextWithStatementStrategy() 를 다른 DAO 에서도 사용할 수 있도록 UserDao 클래스 밖으로 독립시켜본다.

클래스 분리

  • 분리할 클래스는 JdbcContext이다.
  • JdbcContext에 UserDao에 있던 컨텍스트 메소드를 workWithStatementStrategy() 라는 이름으로 옮겨 놓는다.
  •  
  • 이렇게 하면 DataSource 가 필요한 것은 UserDao 가 아니라 JdbcContext가 되어버린다.
  • JdbcContext가 DataSource에 의존하므로 Datasource 타입 빈을 DI 받을 수 있게 해줘야 한다.
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 =this.dataSource.getConnection() ;
    ps = stmt.makePreparedStatement(c);
    ps.executeUpdate();
  } catch(
  SQLException e)

  {
    throw e;
  }finally

  {
  }
}
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() {...}
    );
  }

  public void deleteAll() throws SQLException{
    this.jdbcContext.workWithStatementStrategy(
        new StatementStrategy() {...}
    );
  }
}

빈 의존관계 변경

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

3.4.2 JdbcContext의 특별한 DI

  • UserDao 와 JdbcContext 사이에는 인터페이스를 사용하지 않고 DI 를 적용했다.
  • 의존관계 주입이라는 개념을 충실히 따르면, 인터페이스를 두고 클래스 레벨에서는 의존관계가 고정되지 않게 하고 런타임 시 의존할 오브젝트 관계를 다이나믹하게 주입해주는 것이 맞다.
  • 드물지만 이렇게 인터페이스를 사용하지 않는 클래스를 직접 의존하는 DI 가 등장하는 경우도 있다.

JdbcContext 가 빈이 되어야 하는 이유

  • JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이기 때문에 여러 오브젝트에서 공유해 사용되는 것이 이상적이다.
  • JdbcContet 가 DI 를 통해 다른 빈에 의존하고 있기 때문에

3.5 템플릿과 콜백

전략 패턴의 컨텍스트를 템플릿이라 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 불려서 이런 패턴을 템플릿/콜백 패턴이라고 부른다.

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

템플릿/콜백의 특징

  • 템플릿/콜백 패턴의 콜백은 보통 단일 메소드 인터페이스를 사용한다.
  • 콜백 인터페이스의 메소드에는 보통 파라미터가 있다.
  • 클라이언트는 템플릿 안에서 실행될 로직을 담은 콜백 오브젝트를 만들고, 콜백이 참조할 정보를 제공한다.
  • 콜백은 클라이언트가 템플릿 메소드를 호출할 때 파라미터로 전달한다.
  • 콜백은 클라이언트 메소드에 있는 정보와 템플릿이 제공한 참조정보를 이용해서 작업을 수행하고 그 결과를 다시 템플릿에게 돌려준다.
  • 템플릿은 콜백이 돌려준 정보를 사용해서 작업을 마저 수행한다.

3.5.2 편리한 콜백의 재활용

  • 익명 내부 클래스를 사용해서 상대적으로 코드를 읽기가 조금 불편하다.
  • StatementStrategy 인터페이스의 makePreparedStatement() 메소드를 구현한 콜백 오브젝트 코드를 보면 SQL 문장만 바뀌고 나머지는 동일하다.
  • 따라서 SQL 문장만 파라미터로 받게하고 익명 클래스를 포함하여 나머지 부분을 메소드로 바꾼다.
  • 이렇게 하면 익명 내부 클래스인 콜백을 직접 만들 필요조차 사라진다.

콜백과 템플릿 결합

  • 위에 분리한 메서드를 JdbcContext로 옮기면 모든 DAO 메소드에서 재사용할 수 있다.
    public class JdbcContext {
    	...
        public void executeSql(final String query) throws SQLException {
          this.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");
    }

 

3.5.3 템플릿/콜백의 응용

  • 코드가 한 두번 사용되는 것이 아니라 여기저기 자주 반복된다면 템플릿/콜백 패턴을 적용하기 좋다.
  • 템플릿과 콜백을 찾아낼 때는 변하는 코드 경계를 찾고, 그 경계를 사이에 두고 주고받는 일정한 정보가 있는지 확인하면 된다.

반응형

'도서기록 > 토비의 스프링' 카테고리의 다른 글

5장 서비스 추상화 - 1  (0) 2025.04.04
1장 오브젝트와 의존관계  (0) 2025.03.08