2022. 7. 6. 08:48ㆍ토비의 스터디
3.2.1 JDBC try/catch/finally 코드의 문제점
바로 이전 장에서는 try/catch/finally 블록을 적용해서 완성도 높은 UserDao를 작성했지만, 여전히 복잡한 것은 사실이다.
try/catch/finally 블록이 2중으로 중첩되어 나오며(finally 블록에서 리소스 반환할 때에도 try/catch/finally가 한번 더 사용된다), 모든 메소드에서 try/catch/finally 블록이 반복된다.
만약 finally 블록에서 커넥션을 닫아주는 메소드를 생략한 경우에는, 테스트에 별 문제 없어보여도 애플리케이션을 운영하던 중에 서버에서 리소스가 꽉 찼다는 에러가 발생하며 서비스가 중단될 것이다.
이런 DAO는 폭탄이 될 가능성을 지니고 있다.
이 경우에는
변하지 않는, 그러나 많은 곳에서 중복되는 코드 <-> 로직에 따라 자꾸 확장되고 자주 변하는 코드를 잘 분리하면 효과적으로 개선할 수 있다.
3.2.2 분리와 재사용을 위한 디자인 패턴 적용
UserDao의 메소드를 개선해보자. 성격이 다른 코드를 찾아내는 작업을 가장 먼저 해야한다.
public deleteAll() {
Connetion c = null;
PreparedStatement ps = null;
try{
c = dataSource.getConnetion();
ps = c.prepareStatment("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() 메소드에서 변하는 부분은 단 한줄이다. "delete from users" 쿼리문은 변경되기 때문이다. 나머지는 PreparedStatement를 사용해서 업데이트용 쿼리를 실행하는 메소드라면 공통 부분이다.
메소드 추출
가장 쉽게 떠올릴 수 있는 분리 방법은, 메소드로 추출해 내는 것이다. 변하는 부분을 메소드로 추출해보자.
try{
c = dataSource.getConnetion();
ps = makeStatement(c); //변하는부분을 메소드로 추출하고 변하지 않는 부분에서 호출하도록 만들었다.
ps.executeUpdate();
}
....
...
private PreparedStatement makeStatement(Connection c) throws SQLException{
PreparedStatment ps ;
ps = c.prepareStatement("delete from users");
return ps;
}
변하는 부분을 메소드로 추출하였으나 별 이득이 없어 보인다. 왜냐하면 분리시킨 메소드는 다른 곳에서 재사용이 용이해야 하는데, 지금은 반대로 되었다. 추출한 부분은 재사용이 불가능하며, 분리시키고 남은 본체가 재사용이 필요한 부분이다.
즉, 분리된 makeStatement 메소드는 DAO 로직마다 새롭게 만들어서 확장해야 한다. 뭔가 반대로 되었다.
템플릿 메소드 패턴의 적용
다음은 변하는 것과 공통 코드를 템플릿 메소드 패턴을 이용해서 분리해보자.
템플릿 메소드 패턴은 상속을 통해 기능을 확장해서 사용하는 부분이다. 따라서 변하지 않는 부분은 슈퍼 클래스에 그대로 두고, 변하는 부분은 추상 메소드로 정의해서 각 서브클래스에서 입맛에 맞게 오버라이드 하여 새롭게 정의해 쓰는 것이다.
abstract protected PreparedStatement makeStatement(Connection c) throws SQLException ;
위와 같이 추출한 메소드를 추상 메소드로 변경한다. UserDao도 당연히 추상 클래스가 된다.
그리고 이를 상속하는 서브클래스를 만들어서 이 추상메소드를 구현한다.
public calss UserDaoDeleteAll extends UserDao{
protected PreparedStatement makeStatement(Conncetion c) throws SQLException {
PreparedStatement ps = c.prepareStatment("delete from users");
return ps;
}
}
서브클래스에서 상속을 통해 자유롭게 확장이 가능하다. 만약 다른 쿼리문을 사용할 경우 위와 같이 추상 클래스를 구현하면 되기 때문이다.
하지만 UserDao의 기능을 확장하고 싶을 때마다 상속을 통해 새로운 클래스를 만들어야 한다는 문제점이 있다.
만약 UserDao의 JDBC 메소드가 4개라면? 4개의 서브클래스를 만ㄷ르어서 사용해야 한다.
또, 확장구조가 이미 클래스를 설계하는 시점에서 고정되어 버린다. UserDao와 그 서브클래스들이 이미 컴파일 시점에서 관계가 결정된다. 둘 간의 관계에 대한 유연성이 떨어진다.
전략 패턴의 적용
전략패턴은 템플릿 메소드 패턴보다 더 유연하고 확장성이 뛰어나다. 오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하는 것이 전략패턴이다.
확장에 해당하는 변하는 부분을 별도의 클래스로 만들어서 추상화된 인터페이스를 통해 위임하는 방식이다.
좌측 Context의 contextMethod()는 일정한 구조를 갖고 동작하다가, 특정 확장 기능은 Strategy 인터페이스를 통해 외부 독립된 전략 클래스에 위임하는 것이다. 우리가 작성한 deleteAll() 메소드에 대입해보면, 변하지 않는 부분(공통 부분)이 contextMethod()가 되며, 추상 메소드로 선언한 PreparedStatement를 만들어주는 기능이 전략에 해당한다.
public interface StatementStrategy{
PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}
이제 이 인터페이스를 상속해서 실제 전략, 즉 바뀌는 부분인 PreparedStatement를 생성하는 클래스를 만들어보자.
public class DeleteAllStatement implements StatementStrategy{
public PreparedStatement makePreparedStatement (Connection c) throws SQLException{
PreparedStatement ps = c.preparedStatement ("delete from users");
return ps;
}
}
템플릿 메소드 패턴과 유사하지만, 인터페이스를 상속한다는 차이가 있어 둘 간의 관계가 느슨하다.
이제 다시 deleteAll()을 완성하면
public void deleteAll() throws SQLException{
...
try{
c = dataSource.getConnection();
StatementStrategy strategy = new DeleteAllStatement();
ps = strategy.makePreparedStatement(c);
ps.executeUpdate();
}catch(SQLException e){
...
}
위와 같이 작성할 수 있다. 하지만 컨텍스트(공통 부분) 안에서 구체적인 전략 클래스를 사용하도록 고정하고 있다. 이러면 전략 패턴이 의미가 없다. 컨텍스트가 StatementStrategy 인터페이스 뿐만아니라 특정 구현 클래스인 DeleteAllStatement를 직접 의존하고 있다. 이것은 OCP를 위배했다.
전략을 사용하는 컨텍스트는 그 전략을 구현한 클래스가 무엇인지 알 필요가 없어야 한다.
DI 적용을 위한 클라이언트/컨텍스트 분리
전략패턴에 따르면 컨텍스트가 어떤 전략을 사용하게 할 것인가는 컨텍스트 앞단인 Client가 결정하는 것이 일반적이다.
Client가 구체적인 전략을 선택한 뒤 Context에 전달하는 것이다.
위의 패턴을 코드에 적용해보자. 컨텍스트에 해당하는 JDBC try/catch/finally 코드를 클라이언트로부터 독립시켜야 한다. 그리고 구체적인 전략을 결정하는 다음 코드는 클라이언트 코드에 들어가야 한다.
StatementStrategy strategy = new DeleteAllStatement();
컨텍스트에 해당하는 부분은 별도의 메소드로 독립시키고, 클라이언트는 전략 클래스의 오브젝트를 컨텍스트 메소드로 전달해야 한다. 설명만 들으면 어렵게 들릴 수 있다. 일단 아래 코드를 보자.
컨텍스트에 해당하는 부분을 아래 jdbcContextWithStatementStrategy()로 독립시켰다. 이 메소드는 클라이언트 코드에서 호출할 것이다.
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) { } }
}
}
다음은 클라이언트 코드다.
public void deleteAll() throws SQLException {
StatementStrategy strategy = new DeleteAllStatement(); // 선정한 전략 클래스의 오브젝트 생성
jdbcContextWithStatementStrategy(strategy); // 컨텍스트 호출, 전략 오브젝트 전달
}
클라이언트에서 구체적인 전략 클래스를 결정하며, 그 전략을 컨텍스트에 전달한다.
드디어 전략 패턴의 모습을 갖췄다. 클라이언트가 컨텍스트가 사용할 전략을 정해서 전달한다는 면에서 DI 구조라고 이해할 수도 있다.
'토비의 스터디' 카테고리의 다른 글
전략 패턴이란? (0) | 2022.10.29 |
---|---|
[토비의 스프링 3.1] 3.3 JDBC 전략 패턴의 최적화 (0) | 2022.07.07 |
[토비의 스프링 3.1] 3.1 다시 보는 초난감 DAO (0) | 2022.07.05 |
[토비의 스프링 3.1] 2.5 학습 테스트로 배우는 스프링 (0) | 2022.07.04 |
[토비의 스프링 3.1] 2.4 스프링 테스트 적용 (0) | 2022.06.30 |