개요
1. 예외처리 기능 적용
2. 변하는 것과 변하지 않는 것
3. 템플릿 메소드 패턴
4. 전략 패턴
본문
들어가기 전에
이전 장인 테스트의 학습 테스트 부분은 배운다기보다는 테스트를 통해서 이루어가야할 개발자의 지침과도 같은 부분이라서 정리하지 않고 곧장 3장을 포스팅하게 되었음을 알린다.
1. 예외처리 기능 적용
기존에 작성하고 있던 초난감 DAO를 보면 예외처리가 없다는 문제점을 발견할 수 있다. JDBC코드를 처리할 때는 예외처리가 기본적으로 요구된다. 시스템에 문제를 일으키지 않기 위해서 특히 데이터베이스의 상태에 문제가 생기지 않게 하기위해서는 말이다.
UserDao의 가장 단순한 메소드인 deleteAll()을 통해서 예외처리 기능을 적용시켜보겠다.
public void deleteAll() throws SQLException {
Connection c = null;
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) {//ps.close()메소드에서도 예외가 발생할 수 있기에 이를 잡아줘야 한다. 그렇지 않으면 connection을 close하지 못하고 빠져나갈 수 있다.
}
}
if (c!= null) {
try {
c.close();
} catch (SQLException e) {
}
}
}
}
예외가 발생할 수 있는 코드를 try{}블럭으로 묶어주고 예외 발생시 잡아주는 catch{}블럭에서 예외를 throw 그러니까 밖으로 던지도록 처리를 해둔 코드를 만들었다. finally에는 예외가 발생했을 때나 안 했을 때나 동작해야 하는 코드를 적어둔다. prepareStatement와 connection의 반환은 언제나 이루어져야하기에 finally에 묶어준다.
이처럼 예외처리를 하는 것은 그렇게 어렵지 않다. 하지만 개선할 지점은 존재한다. 그 지점이 이 3장 템플릿에서 배우게 될 핵심이 될 부분이다.
2. 변하는 부분과 변하지 않는 부분
위의 예외처리는 다른 메소드에서도 적용되어야 한다. getCount메소드에서도 add메소드에도 이후 추가될지 모를 어떤 데이터베이스 연계 메소드에서도 어지간하면 예외처리가 필요하다. 이 코드들에서 예외처리를 하는 방법은 대게 비슷할 것이다. 이것을 일일이 만들 때마다 쓰는 것은 오류가 생길지도 빼먹는 부분이 생길지도 모르는 일이다. 토비의 스프링에서는 c.close를 빼먹은 회사의 사례를 들면서 connection이 반환되지 않으면서도 문제없이 계속 동작하다가 뻗어버린 서버 이야기를 한다. 이때 회사의 코드에서 connection 반환이 빠진 코드를 일일이 찾아가면서 고쳤다는 이야기를 한다. 이러한 비효율성을 극복하기 위한 방법을 생각해볼 필요가 있다.
이것도 넓게 보면 1장에서 다루었던 분리의 방법과 개념상 다르지는 않다. 분리는 코드를 효율적으로 만드는 열쇠와도 같은 것이니 말이다. 하지만 이번 코드는 기능 단계에서의 분리와는 성격이 다르기에 조금 다른 해결방법이 요구된다.
그 방법은 '변하는 부분'과 '변하지 않는 부분'의 구분에서 이루어진다. 다시 deleteAll()의 코드를 살펴보면서 변하는 부분과 변하지 않는 부분을 나누어서 바라보자.
public void deleteAll() throws SQLException {
Connection c = null;
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) {
}
}
}
}
ps = c.prepareStatement("delete from users"); 이 부분만이 변하는 코드이고 나머지 부분은 변하지 않는 부분이다. prepareStatement를 만들어서 업데이트용 쿼리를 실행하는 메소드라면 위의 코드와 거의 비슷할 것이다. 이때 먼저 생각해 볼 수 있는 방법은 변하는 부분을 따로 메소드로 빼서 사용하는 것이다. 코드로 만들면 아래와 같이 된다.
public void deleteAll() throws SQLException {
...
try {
c = dataSource.getConnection();
ps = makeStatement(c);//변하는 부분을 메소드로 추출하고 변하지 않는 부분에서 호출하도록 만들었다.
ps.executeUpdate();
} catch (SQLException e) {
throw e;
} finally {
if(ps != null) {
try {
ps.close();
} catch (SQLException e) {
...
}
private PreparedStatement makeStatement(Connection c) throws SQLException {
PreparedStatement ps;
ps = c.prepareStatement("delete from users");
return ps;
}
그런데 이 코드에는 그다지 장점을 발견할 수가 없다. 변하지 않는 부분을 재활용해야 다른 코드에서도 써먹을 수 있는 게 아니었던가? 원하던 상황과 반대가 되었다.
3. 템플릿 메소드 패턴
다음은 템플릿 메소드 패턴을 이용한 분리이다. 템플릿 메소드 패턴은 상속을 통해서 기능을 확장하는 것으로 1장에서도 다루어 보았다. UserDao클래스를 추상 클래스로 변경하고 makeStatement()메소드를 추상 메소드로 선언을 하고 상속한 클래스를 아래와 같이 작성해보자.
public class UserDaoDeleteAll extends UserDao {
protected PreparedStatement makeStatement(Connection c) throws SQLException {
PreparedStatement ps;
ps = c.prepareStatement("delete from users");
return ps;
}
}
이렇게 되면 UserDao의 기능을 확장하고 싶을 때마다 상속을 통해 확장이 가능하면서 확장을 위해 원 코드를 건드릴 필요가 없는 장점을 지닌 코드를 만들 수 있다. 하지만 확장 구조가 고정된다든지, 런타임 시점이 아닌 컴파일 시점에서 서브 클래스의 코드와 슈퍼 클래스의 활용 코드의 관계가 결정되어 유연성이 떨어진다든지 하는 단점을 지니고 있다. 이는 1장에서 다루었던 단점과 같다.
4. 전략 패턴
따라서 1장에서 다루었던 것처럼 전략 패턴을 활용하여 보다 유연성 있는 코드를 만드는 작업을 해볼 수 있다. 현재 변하는 부분을 따로 빼내기로 한 deleteAll() 메소드의 코드는 다음과 같은 컨텍스트를 가진다.
1. DB커넥션 가져오기
2. PreparedStatement를 만들어줄 외부 기능 호출하기
3. 전달받은 PreparedStatement 실행하기
4. 예외 발생시 메소드 밖으로 던지기
5. 모든 경우에 PreparedStatement와 Connection 닫아주기
이때 2번 외부 기능이 전략 패턴에서 말하는 전략에 해당하는 부분이라고 할 수 있다. PreparedStatement를 만들기 위해서는 connection이 필요하다는 것을 고려하여 PreparedStatement를 생성하는 인터페이스를 만들면 다음과 같다.
public interface StatementStrategy {
PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}
구현 코드는 아래와 같이 만들 수 있다.
public class DeleteAllStatement implements StatementStrategy {
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("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) {
...
}
전략패턴을 구현했지만 아직도 활용도가 높다고는 할 수 없다. DeleteAllStatement를 이미 메소드에서 알고서 사용하고 있기에 분리도가 높지 않고 이는 전략 패턴을 제대로 활용하고 있다고 볼 수가 없다. 이 문제를 해결하기 위해서는 전략 패턴에 대해서 보다 실제적인 사용을 이해할 필요가 있다. 전략 패턴이란 기본적인 문맥, 컨텍스트가 존재하고 그 문맥에 여러가지 있을 수 있는 경우의 수, 즉 전략을 제공해주는 것이다. 이 전략은 인터페이스를 통해서 컨텍스트가 그 존재를 알지 못하는 상태로 존재하고 그 존재를 클라이언트가 선택해서 제공해주면 전략에서 컨텍스트의 정보를 받아서 런타임시 코드가 수행될 수 있게끔 하는 것이다. 요는 변하지 않는 부분이 컨텍스트로서의 역할을 하며 전략을 제공 받을 수 있는 위치에 있어야 한다는 것인데, 현재의 코드는 전략을 제공해주어야 할 클라이언트가 만들어지는 부분에 공존하고 있기에 문제가 생기는 것이다. 따라서 이를 다른 메소드로 분리해줄 필요가 있다.
너무 어렵게 적었는데 정말 해야할 것을 적자면 변하지 않는 부분 try/catch/finally코드를 다른 메소드로 분리해야한다는 것이다.
//try/catch/finally컨텍스트 코드 메소드로 분리
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 ( ps != null) {try {c.close();} catch (SQLException e) {}}
}
}
이러면 컨텍스트가 따로 독립적으로 존재할 수 있게 되고 deleteAll()은 클라이언트로서 기능만 수행하며 위 메소드에게 전략을 제공하기만하면 된다.
public void deleteAll() throws SQLException {
StatementStrategy st = new DeleteAllStatement(); //선정한 전략 클래스의 오브젝트 생성
jdbcContextWithStatementStrategy(st); //컨텍스트 호출 전략 오브젝트 전달
}
이제 정말 구조적 문제가 없는 전략 패턴이 되었다.
정리
1. 변하는 부분과 변하지 않는 부분의 분리로 코드를 개선할 수도 있다.
2. 템플릿 메소드, 전략 패턴을 복습해보았다.
3. 전략 패턴은 클라이언트 - 컨텍스트- 전략의 구분이 잘 이루어져야 한다는 것과 컨텍스트에 대해서 깊이 이해할 수 있었다.
소스 깃허브
https://github.com/cholongbul/Tobyspring
cholongbul/Tobyspring
토비의 스프링 연습. Contribute to cholongbul/Tobyspring development by creating an account on GitHub.
github.com
'스프링' 카테고리의 다른 글
토비의 스프링 - 3.4 컨텍스트와 DI (0) | 2021.06.18 |
---|---|
토비의 스프링 - 3.3 JDBC 전략 패턴의 최적화 (0) | 2021.06.03 |
토비의 스프링 - 2.4 스프링 테스트 적용 (0) | 2021.05.30 |
토비의 스프링 - 2.3 개발자를 위한 테스팅 프레임워크 JUnit (0) | 2021.05.28 |
토비의 스프링 - 2.1 UserDaoTest다시 보기//2.2UserDaoTest개선 (0) | 2021.05.27 |