3장은 '템플릿'에 관련 내용이다. 예외처리와 안전한 리소스 반환을 보장해주는
DAO
코드를 만들고 이를 객체지향 설계 원리와 디자인 패턴, DI 등을 적용해서 유연하며 단순한 코드로 만드는 방법을 살펴본다.
템플릿이란 바뀌는 성질이 다른 코드 중에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로부터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법이다.
DB 커넥션이라는 제한적인 리소스를 공유해 사용하는 서버에서 동작하는 JDBC 코드에서는 반드시 지켜야 할 원칙이 있다. 바로 예외처리이다.
일반적으로 서버에서 제한된 개수의 DB 커넥션을 만들어서 재사용 가능한 풀로 관리하기 때문에 명시적으로close()
를 해서 돌려줘야지만 다시pool
에 넣었다가 다음 커넥션 요청이 있을 때 재사용할 수 있다.
위의 이유로 인해 예외 발생 가능성이 있는 코드에 대해 try/catch/finally를 통해 예외 발생 시에도 리소스를 반환하도록 수정해야 한다.
👿 위의 코드는 try/catch/finally 블록이 모든 메소드마다 반복된다.
변하지 않지만, 그러나 많은 곳에서 중복되는 코드와 로직에 따라 자꾸 확장되고 자주 변하는 코드를 분리해보자.
👿 분리시키고 남은 메소드가 재사용이 필요한 부분이고, 분리된 메소드가 확장돼야 하는 부분이다.
템플릿 메소드 패턴은 상속을 통해 기능을 확장해서 사용하는 부분이다.
변하지 않는 부분은 슈퍼클래스에 두고 변하는 부분은 추상 메소드로 정의해서 사용하자.
👿 DAO 로직마다 상속을 통해 새로운 클래스를 만들어야 한다. 또한, 클래스 레벨에서 컴파일 시점에 이미 그 관계가 결정된다.(delete를 사용하기 위해 UserDeleteAll
을 선언하여 사용해야 한다.) 유연성이 떨어지게 된다.
전략패턴은 오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 패턴이다.
변하는 부분은 전략으로, 변하지 않는 부분은 컨텍스트로 정의하자.
👿 컨텍스트 안에서 구체적인 전략 클래스인 DeleteAllStatement
를 사용하도록 고정되어 있다면(특정 구현 클래스를 직접 알고 있다는 것은) 전략패턴에도, OCP에도 잘 들어맞는다고 볼 수 없다.
StatementStrategy strategy = new DeleteAllStatement(); // 고정되어 있다!!
ps = srategy.makePreparedStatement(c);
전략 패턴에 따르면 Context가 어떤 전략을 사용하게 할 것인가는 Context를 사용하는 앞단의 Client가 결정하는 게 일반적이다.
클라이언트가 컨텍스트가 사용할 전략을 정해서 전달한다는 면에서 DI 구조
라고 이해할 수 있다.
👿 DAO 메소드마다 새로운 StatementStrategy
구현 클래스를 만들어야 한다.
👿 DAO 메소드에서 전달할 부가정보가 있는 경우, 이를 위해 오브젝트를 전달받는 생성자와 인스턴스 변수를 만들어야 한다.
DI의 가장 중요한 개념은 제 3자의 도움을 통해 두 오브젝트 사이의 유연한 관계가 설정되도록 만든다는 것이다.
🤔 클래스 파일이 많아지는 문제를 해결할 간단한 해결하려면 ?
UserDao에서만 사용하고, UserDao의 메소드에 강하게 결합되어 있기 때문에 로컬클래스로 변환해보자.
😇 코드를 이해하기 좋아진다. (관련 코드를 한 곳에서 볼 수 있기 때문)
😇 로컬 클래스는 클래스가 내부 클래스이기 때문에 자신이 선언된 곳의 정보에 접근할 수 있다는 점이다.
👿 여태까지 개선한 코드는 다른 dao에서는 사용할 수 없다. (사용하는 class 내부에 있기 때문!)
전략 패턴의 구조로 보자면 UserDao의 메소드가 클라이언트이고, 익명 내부 클래스로 만들어지는 것이 개별적인 전략이고, jdbcContextWithStatementStrategy()
메소드는 컨텍스트이다.
모든 DAO가 사용할 수 있게 jdbcContextWithStatementStrategy()
메소드를 클래스로 분리해보자. (스프링 빈으로 DI)
의존관계는 아래와 같이 변했다.
UserDao
---> DataSource
UserDao
---> JdbcContext
---> DataSource
UserDao
와 JdbcContext
는 클래스 레벨에서 의존관계가 결정된다. 비록 런타임 시에 DI 방식으로 외부에서 오브젝트를 주입해주는 방식을 사용하긴 했지만, 의존 오브젝트의 구현 클래스를 변경할 수 없다. (인터페이스가 아니므로~)
JdbcContext
는 스프링을 이용해UserDao
객체에서 사용하게 주입했다는 것은 DI의 기본을 따르고 있다고 볼 수 있다. (DI라는 개념은 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않게 하고, 런타임 시에 의존할 오브젝트와의 관계를 다이나믹 하는게 맞지만! DI는 IoC(관계설정을 외부로 위임)라는 개념을 포괄하기 때문에)
🤔 왜 인터페이스를 사용하지 않았을까 ?
UserDao
와 JdbcContext
는 강한 응집도를 가지고 있다. 따라서, 다른 구현으로 대체해서 사용할 이유가 없다. 이런 경우엔 굳이 인터페이스로 두지말고 강력한 결합 관계를 허용하면 된다.
🤔 인터페이스를 사용하진 않았지만 JdbcContext
와 UserDao
를 DI 구조로 만들어야 할 이유는 무엇일까 ?
1. JdbcContext
가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이기 때문이다. (JdbcContext
는 싱글톤으로 등록돼서 여러 오브젝트에서 공유해 사용되는 것이 이상적이다.)
2. JdbcContext
가 DI를 통해 다른 빈(DataSource
)에 의존하고 있기 때문이다. (DI를 위해서는 둘 다 스프링 빈이어야 한다.)
이렇게 되면 싱글톤으로 만드는 것은 포기하고, DAO마다 JdbcContext
오브젝트를 갖고 있게 한다. 👉 따라서, DataSource
를 주입받을 수 없게되므로, UserDao
의 setDataSource
메소드에서 주입받게 된다.
public class UserDao {
private JdbcContext jdbcContext;
public void setDataSource(DataSource dataSource) {
this.jdbcContext = new JdbcContext();
this.jdbcContext.setDataSource(dataSource);
}
}
스프링DI
😇 오브젝트의 의존관계가 설정파일에 명확하게 드러난다.
👿 DI의 근본원칙(인테페이스 구현)에 부합하지 않는 구체적인 클래스와의 관계가 직접 노출된다.
수동 DI
😇 관계가 외부에 드러나지 않는다. (UserDao
의 set()
메소드를 통해 주입하기 때문) 따라서, DI 전략을 외부에는 감출 수 있다.
👿 싱글톤으로 만들 수 없고, DI 작업을 위한 부가적인 코드가 필요하다.
둘의 방법 중 어느 방법이 더 낫다고 말할 순 없다. 상황에 따라 적절하다고 판단되는 방법을 선택해서 사용하면 된다.
변하는 것 : 쿼리
변하지 않는 것 : 그 외 코드
변하는 것과 변하지 않는 코드를 분리하고 변하지 않는 것은 유연하게 재활용할 수 있게 만들자
복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고, 그중 일부분만 자주 바꿔서 사용해야 하는 경우에 적합한 구조다. 전략패턴의 기본 구조에 익명 내부 클래스를 활용한 방식이다.
전략패턴의 컨텍스트를 템플릿(변하지 않는 부분)이라고 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백(변하는 부분)이라고 부른다.
클라이언트가 템플릿 메소드를 호출하면서 콜백 오브젝트를 전달하는 것은 메소드 레벨에서 일어나는 DI이다.
바뀌는 부분이 한 어플리케이션 안에서 동시에 여러 종류가 만들어질 수 있다면 템플릿/콜백 패턴을 적용하는 것을 고려해보자.
일가장 전형적인 템플릿/콜백 패턴의 후보는 try/catch/finally
블록을 사용하는 코드이다.
스프링에서 제공하는 jdbc
코드에도 템플릿 콜백 기술이 사용된다.
일정한 작업 흐름이 반복되면서 그중 일부 기능만 바뀌는 코드가 존재한다면 전략 패턴을 적용하다. 전략 메소드를 갖는 전략패턴이면서 익명 내부 클래스를 사용해서 매번 전략을 새로이 만들어 사용하고, 컨텍스트 호출과 동시에 전략 DI를 수행하는 방식을 템플릿/콜백 패턴이라고 한다.