HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
📖
공부한 책
/
토비의 스프링
토비의 스프링
/
3장

3장

3.3 JDBC 전략 패턴의 최적화

public class AddStatement implements StatementStrategy { User user; public AddStatement(User user) { this.user = user; } }
  • StatementStrategy 를 인터페이스로 하는 클래스를 전략마다 만들어야 한다는 점은 꽤나 번거롭다
  • 또, 전략마다 클래스를 만드는 식의 방법으로는 부가적인 정보를 넘겨주어야 할 때 인스턴스 변수와 생성자 파라미터를 추가해야 한다는 번거로움이 존재함

클래스를 전략마다 만들지 않는 방법

  • 클래스 안의 내부 클래스로 정의하는 방법
  • 특정 메서드에서만 사용된다면 로컬 클래스로 만들수도 있음
public void add(User user) throws SQLException { class AddStatement implements StatementStrategy { ... } }
  • 클래스파일이 하나 줄어들고, 생성로직도 함께 볼 수 있어 좋음
  • 또한, 로컬 클래스는 클래스가 내부 클래스이기에 생성자에 파라미터 추가할 필요 없이 메서드로 파라미터를 넘겨서 클래스 생성을 할 수 있다
  • 또는 익명 내부 클래스로 만들수도 있다
public void add(final User user) throws SQLException { jdbcContextWithStatementStrategy( new StatementStrategy() { public PreparedStatement makePreparedStatement(Connection c) { ...

3.4 컨텍스트와 DI

전략 패턴의 구조로 보자면
  • UserDao의 메소드가 클라이언트
  • 익명 내부 클래스로 만들어지는 것이 개별적인 전략
jdbcContextWithStatementStrategy() 메서드는 컨텍스트
public void jdbcContextWithStatementStrategy(StatementStrategy smt) 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) {} } } }
jdbcContextWithStatementStrategy() 메서드는 JDBC의 일반적인 작업 흐름을 담고 있기에 다른 DAO에서 사용 가능하니 UserDao 클래스 밖으로 독립시키자
 

JdbcContext의 특별한 DI

notion image
이 경우 JdbcContext는 그 자체로 독립적인 JDBC 컨텍스트를 제공해주는 서비스 오브젝트로서 의미가 있을 뿐 구현 방법이 바뀔 가능성은 없다.
따라서 인터페이스를 구현하도록 만들지 않았고, UserDao와 JdbcContext는 인터페이스를 사이에 두지 않고 DI를 적용하는 특별한 구조가 된다.
  • 인터페이스를 사용해서 클래스를 자유롭게 변경할 수 있게 하지는 않았지만, JdbcContext를 UserDao와 DI 구조로 만들어야 할 이유
    • JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 되기 때문
      • JdbcContext는 그 자체로 변경되는 상태정보를 갖고 있지 않음. 내부에서 사용할 dataSource라는 인스턴스 변수가 있지만, dataSource는 읽기전용이므로 JdbcContext가 싱글톤이 되는 데 아무런 문제가 없음
      JdbcContext가 DI를 통해 다른 빈에 의존하고 있기 때문(DataSource 오브젝트)
      DI를 위해서는 주입되는 오브젝트와 주입받는 오브젝트 양쪽 모두 스프링 빈으로 등록되어 있어야 함
      따라서 JdbcContext는 다른 빈을 DI 받기 위해서라도 스프링 빈으로 등록되어야 함
  • 여기서 중요한 것은 인터페이스의 사용 여부. 인터페이스가 없다는 건 UserDao와 JdbcContext가 매우 긴밀한 관계를 가지고 강하게 결합되어 있다는 의미임.
    • 비록 클래스는 구분되어 있지만 이 둘은 강한 응집도를 갖고 있음
    • UserDao가 JDBC 방식 대신 JPA나 하이버네이트 같은 ORM을 사용해야 한다면 JdbcContext도 통째로 바뀌어야 함
  • 단, 이런 클래스를 바로 사용하는 코드 구성을 DI에 적용하는 것은 가장 마지막 단계에서 고려해볼 사항임을 잊지 말기. 그저 인터페이스를 만들기 귀찮으니 그냥 클래스를 사용하자는 건 잘못된 생각이다.
 
변하는 것과 변하지 않는 것을 분리하고 변하지 않는 건 유연하게 재활용할 수 있게 만든다는 간단한 원리를 계속 적용했을 때, 이렇게 단순하면서도 안전하게 작성 가능한 Jdbc 활용 코드가 완성된다. 바로 이런 게 객체지향 언어와 설계를 사용하는 매력이 아닐까

스프링 빈으로 DI

  • JdbcContext를 빈으로 만들어, UserDao에 빈으로 주입하는 방법
  • 오브젝트 사이의 실제 의존관계가 설정파일에 명확하게 드러난다는 장점
  • 하지만 DI의 근본적인 원칙에 부합하지 않는 구체적인 클래스와의 관계가 설정에 직접 노출되는 단점이 있음
    • 이것이 중요한 이유는, 나중에 변경이 필요할 때 수정하지 않고 변경하기 위함. 결국 변경에 용이하기 위하여

코드를 이용하는 수동 DI

  • JdbcContext를 빈으로 만들지 않고 UserDao의 내부에서 직접 DI를 적용하는 방식
  • DAO 클래스마다 하나의 JdbcContext 오브젝트를 갖고 있게 하는 방식으로
public class UserDao { private JdbcContext jdbcContext; public void setDataSource(DataSource dataSource) { this.jdbcContext = new JdbcContext(); this.jdbcContext.setDataSource(dataSource); this.dataSource = dataSource; } }
  • 이 방법의 장점은 굳이 인터페이스를 두지 않아도 될 만큼 긴밀한 관계를 갖는 DAO 클래스와 JdbcContext를 어색하게 따로 빈으로 분리하지 않고 내부에서 직접 만들어 사용하면서도 다른 오브젝트에 대한 DI를 적용할 수 있다는 점.
    • 한 오브젝트의 수정자 메소드에서 다른 오브젝트를 초기화하고 코드를 이용해 DI 하는 것은 스프링에서도 종종 사용되는 기법임
  • 관계를 외부에 드러내지 않는다는 장점이 있음
  • 하지만 JdbcContext를 여러 오브젝트가 사용하더라도 싱글톤으로 만들 수 없고, DI 작업을 위한 부가적인 코드가 필요하다는 단점이 있음
 

템플릿 콜백 패턴

  • 가장 전형적인 템플릿/콜백 패턴의 후보는 try/catch/finally 블록을 사용하는 코드다.
  • 일정한 리소스를 만들거나 가져와 작업하면서 예외가 발생할 가능성이 있는 코드는 보통 try/catch/finally 구조로 코드가 만들어질 가능성이 높다.

스프링의 JdbcTemplate

스프링에는 JdbcTemplate 외에도 십여 가지의 템플릿/콜백 패턴을 적용한 API가 존재함.
클래스 이름이 Template으로 끝나거나 인터페이스 이름이 Callback으로 끝난다면 템플릿/콜백이 적용된 것이라고 보면 됨

테스트 보완

  • 성공적인 테스트 결과를 보면 빨리 다음 기능으로 넘어가고 싶겠지만 너무 서두르는 것은 좋지 않다. 항상 꼼꼼하게 빠진 것은 없는지 더 개선할 부분은 없는지 한 번쯤 생각해보자
  • 네거티브 테스트라 불리는, 예외상황에 대한 테스트는 항상 빼먹기 쉬움. get()이라면 ID가 없을 때는 어떻게 되는지, getAll() 이라면 결과가 하나도 없는 경우에는 어떻게 되는 지 등.
  • 스프링의 개발자인 로드 존슨은 테스트를 작성할 때 항상 네거티브 테스트부터 만드는 습관이 있다고 함. 정상적인 조건의 테스트부터 만들면 테스트가 성공하는 것을보고 쉽게 만족해서 예외적인 상황은 빼먹고 넘어가기 쉽기 때문