토비의 스프링 - 템플릿
다시 보는 초난감 DAO
UserDao 는 예외상황 처리에 대한 문제가 남아있다.
3.1.1 예외처리 기능을 갖춘 DAO
DB 커넥션이라는 제한적인 리소스를 공유해 사용하는 서버에서 동작하는 JDBC 코드에는 반드시 사용한 리소스를 반환하도록 만들어야 한다.
일반적으로 서버에서는 제한된 개수의 DB 커넥션을 만들어서 재사용 가능한 풀로 관리한다.
DB 풀은 매번 가져간 커넥션을 명시적으로 close() 해서 돌려줘야지만 다시 풀에 넣었다가 다음 커넥션 요청이 있을 때 재사용할 수 있다.
JDBC 수정 기능의 예외 처리 코드
@Throws(SQLException::class, ClassNotFoundException::class)
fun deleteAll() {
var connection: Connection? = null
var ps: PreparedStatement? = null
try {
connection = dataSource.connection
ps = connection.prepareStatement("DELETE FROM users")
ps.executeUpdate()
} catch (e: SQLException) {
throw e
} finally {
try {
ps?.close() // 여기서도 예외가 발생할 수 있다. 잡아주지 않으면 Connection close가 실행되지 않는다.
} catch (e: SQLException) {
}
try {
connection?.close()
} catch (e: SQLException) {
}
}
}
JDBC 조회 기능의 예외처리
@Throws(SQLException::class, ClassNotFoundException::class)
fun getCount(): Int {
val connection = dataSource.connection
var ps: PreparedStatement? = null
var rs: ResultSet? = null
try {
ps = connection.prepareStatement("SELECT COUNT(*) FROM users")
rs = ps.executeQuery()
rs.next()
return rs.getInt(1)
} catch (e: SQLException) {
throw e
} finally {
try {
rs?.close()
} catch (e: SQLException) {
}
try {
ps?.close() // 여기서도 예외가 발생할 수 있다. 잡아주지 않으면 Connection close가 실행되지 않는다.
} catch (e: SQLException) {
}
try {
connection.close()
} catch (e: SQLException) {
}
}
}
3.2 변하는 것과 변하지 않는 것
3.2.1 JDBC try/catch/finally 코드의 문제점
- 중복 코드가 많다.
- 핵심 로직이 try/catch/finally 코드에 묻혀서 잘 보이지 않는다.
- 실수로 인해 자원 반환 코드가 누락될 수 있다.
3.2.2 분리와 재사용을 위한 디자인 패턴 적용
로직에 따라서 변하는 부분을 변하지 않는 나머지 코드에서 분리하는 것이 어떨까?
- 메소드 추출
makeStatement(connection: Connection): PreparedStatement로 추출한다.- 메소드 추출 리팩토링을 적용하는 경우에는 분리시킨 메소드를 다른 곳에서 재사용할 수 있어야 하는데, 이건 반대로 분리시키고 남은 메소드가 재사용이 필요한 부분이 되었다.
- 템플릿 메소드 패턴의 적용
abstract protected fun makeStatement(connection: Connection): PreparedStatement선언으로 변경한다.- 그리고 이를 상속하는 서브 클래스를 만들어서 거기서 이 메소드를 구현한다.
- 하지만 이 방법은 제한이 많다.
- DAO 로직마다 상속을 통해 새로운 클래스를 만들어야 한다.
- 확장구조가 이미 클래스를 설계하는 시점에서 고정되어 버린다.
전략 패턴의 적용
개방 폐쇄 원칙을 잘 지키는 구조이면서도 템플릿 메소드 패턴보다 유연하고 확장성이 뛰어난 것이 인터페이스를 통해서만 의존하도록 만드는 전략 패턴이다.
DI 적용을 위한 클라리언트/컨텍스트 분리
전략 패턴에 따르면 Context 가 어떤 전략을 사용하게 할 것인가는 Context를 사용하는 앞단의 Client가 결정하는게 일반적이다.
Client가 구체적인 전략의 하나를 선택하고 오브젝트를 만들어서 Context에 전달하는 것이다.
Context는 전달받은 그 Strategy 구현 클래스의 오브젝트를 사용한다.
interface StatementStrategy {
@Throws(SQLException::class)
fun makePreparedStatement(connection: Connection): PreparedStatement
}
class DeleteAllStatement : StatementStrategy {
@Throws(SQLException::class)
override fun makePreparedStatement(connection: Connection): PreparedStatement {
return connection.prepareStatement("DELETE FROM users")
}
}
class UserDao(private val dataSource: DataSource) {
@Throws(SQLException::class)
fun deleteAll() {
jdbcContextWithStatement(DeleteAllStatement())
}
@Throws(SQLException::class)
fun jdbcContextWithStatement(stmt: StatementStrategy) {
var connection: Connection? = null
var ps: PreparedStatement? = null
try {
connection = dataSource.connection
ps = stmt.makePreparedStatement(connection)
ps.executeUpdate()
} catch (e: SQLException) {
throw e
} finally {
try {
ps?.close()
} catch (e: SQLException) {
throw e
}
}
}
}
3.3 JDBC 전략 패턴의 최적화
3.3.1 전략 클래스의 추가 정보
3.3.2 전략과 클라이언트의 동거
현재 만들어진 구조에 두 가지 불만이 있다.
- DAO 메소드마다 새로운
StatementStrategy구현 클래스를 만들어야 한다. - DAO 메소드에서
StatementStrategy에 전달할 부가적인 정보가 있는 경우, 이를 위해 오브젝트를 전달받는 생성자와 이를 저장해둘 인스턴스 변수를 번거롭게 만들어야 한다는 점이다.
로컬 클래스
클래스 파일이 많아지는 문제는 간단한 해결 방법이 있다. 바로 로컬 클래스를 사용하는 것이다. 로컬 클래스를 사용하면 해당 클래스가 필요한 메소드 안에 클래스를 정의할 수 있으므로, 클래스 파일의 수를 줄일 수 있다.
익명 내부 클래스
AddStatement 클래스는 StatementStrategy 인터페이스를 구현한 클래스이므로, 이 클래스의 오브젝트를 만들어서 jdbcContextWithStatement() 메소드에 전달하는 부분을 익명 내부 클래스로 바꿀 수 있다.
class UserDao(private val dataSource: DataSource) {
@Throws(SQLException::class)
fun add(user: User) {
jdbcContextWithStatement(object : StatementStrategy {
@Throws(SQLException::class)
override fun makePreparedStatement(connection: Connection): PreparedStatement {
val ps = connection.prepareStatement("INSERT INTO users (name, email) VALUES (?, ?)")
ps.setString(1, user.name)
ps.setString(2, user.email)
return ps
}
})
}
}
3.4 컨텍스트와 DI
- 전략 패턴의 구조로 보자면
UserDao메소드가 클라이언트이고 - 익명 내부 클래스로 만들어지는 것이 개별적인 전략이고
jdbcContextWithStatement()메소드가 컨텍스트 역할을 한다.- 컨텍스트 메소드는
UserDao내의PreparedStatement를 실행하는 기능을 가진 메소드에서 공유할 수 있다. UserDao클래스 밖으로 독립시키면 모든DAO클래스에서 사용할 수 있다.
3.4.2 JdbcContext의 특별한 DI
스프링 빈으로 DI
JdbcContext 처럼 인터페이스를 사용하지 않고 DI를 적용하는 것은 문제가 있지 않을까?
스프링 DI의 기본 의도에 맞게 JdbcContext의 메소드를 인터페이스로 뽑아내어 정의해두고, 이를 UserDao에서 사용하게 해야 하지 않을까?
꼭 그럴 필요는 없다.
스프링의 DI는 넓게 보자면 객체의 생성과 관계설정에 대한 제어권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC라는 개념을 포괄한다. 그런 의미에서 JdbcContext 를 스프링을 이용해 UserDao 객체에서 사용하게 주입했다는 건 DI의 기본을 따르고 있다고 볼 수 있다.
인터페이스를 사용해서 클래스를 자유롭게 변경할 수 있게 하지는 않았지만, JdbcContext를 UserDao와 DI 구조로 만들어야 할 이유를 생각해보자.
JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 되기 때문이다.JdbcContext는 JDBC 컨텍스트 메소드를 제공해주는 일종의 서비스 오브젝트로서 의미가 있고, 그래서 싱글톤으로 등록돼서 여러 오브젝트에서 공유해 사용되는 것이 이상적이다.JdbcContext가 DI를 통해 다른 빈에 의존하고 있기 때문이다.DataSource오브젝트를 주입받도록 되어있는데, DI를 위해서는 주입되는 오브젝트와 받는 오브젝트 둘 다 스프링 빈으로 등록돼야 한다.
왜 인터페이스를 사용하지 않았을까?
UserDao는 항상JdbcContext클래스와 함께 사용돼야 한다. 비록 클래스는 구분되어 있지만 이 둘은 강한 응집도를 갖고 있다.
코드를 이용하는 수동 DI
스프링 빈에 등록하지 않고 UserDao에서 JdbcContext를 직접 생성해서 사용하는 방법도 있다.
-
인터페이스를 사용하지 않는 클래스와 의존관계이지만 스프링의 DI를 이용하기 위해 빈으로 등록해서 사용하는 방법은 오브젝트 사이의 의존관계가 설정파일에 명확하게 드러난다는 장점이 있다.
-
하지만 DI의 근본적인 원칙에 부합하지 않는 구체적인 클래스와의 관계가 설정에 직접 노출된다는 단점이 있다.
-
반면에 DAO 코드를 이용해 수동으로 DI를 하는 방법은
JdbcContext가UserDao의 내부에서 만들어지고 사용되면서 그 관계를 외부에 드러내지 않는다는 장점이 있다. -
필요에 따라서 내부에서 은밀히 DI를 수행하고 그 전략을 외부에는 감출 수 있다.
-
하지만
JdbcContext를 여러 오브젝트가 사용하더라도 싱클톤으로 만들 수 없고, DI 작업을 위한 부가적인 코드가 필요하다는 단점도 있다.
일반적으로 어떤 방법이 더 낫다고 말할 수는 없다. 상황에 따라 적절하다고 판단되는 방법을 선택해서 사용하면 된다.
3.5 템플릿과 콜백
템플릿/콜백 패턴은 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고 그 중 일부분만 자주 바꿔서 사용해야하는 경우에 적합하다.
전략 패턴의 컨텍스트를 템플릿이라 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라 부른다.
3.5.1 템플릿/콜백의 동작원리
- 전략 패턴의 전략과 달리 템플릿/콜백 패턴의 콜백은 보통 단일 메소드 인터페이스를 사용한다.
- 템플릿 콜백 방식은 전략 패턴과 DI의 장점을 익명 내부 클래스 사용 전략과 결합한 독특한 활용법이라고 이해할 수 있다.
- 템플릿 콜백 패턴의 한 가지 아쉬운 점은 DAO 메소드에서 매번 익명 내부 클래스를 사용하기 때문에 상대적으로 코드를 작성하고 읽기가 조금 불편하다.
3.5.2 변하지 않는 부분을 분리시킨 deleteAll 메소드 (p245) 3.5.2 변하지 않는 부분을 분리시킨 deleteAll 메소드 (p246)
**고차 함수(Higher-Order Function)**와 람다(Lambda) 활용
fun workWithStatement(makeStatement: (Connection) -> PreparedStatement) {
var connection: Connection? = null
var ps: PreparedStatement? = null
try {
connection = dataSource.connection
ps = makeStatement(connection)
ps.executeUpdate()
} catch (e: Exception) {
throw RuntimeException(e)
} finally {
try {
ps?.close()
} catch (e: Exception) {
}
try {
connection?.close()
} catch (e: Exception) {
}
}
}
fun executeSql(sql: String) {
workWithStatement { connection -> connection.prepareStatement(sql) }
}
3.6 스프링의 JdbcTemplate
스프링은 JDBC를 이용하는 DAO에서 사용할 수 있도록 다양한 템플릿과 콜백을 제공한다.
거의 모든 종류의 JDBC 코드에 사용 가능한 템플릿과 콜백을 제공할 뿐만 아니라, 자주 사용되는 패턴을 가진 콜백은 다시 템플릿에 결합시켜서 간단한 메소드 호출만으로 사용이 가능하도록 만들어져 있기 때문에 템플릿/콜백 방식의 기술을 사용하고 있는지 모르고도 쓸 수 있을 정도로 편리하다.