Search
Duplicate

3.2 변하는 것과 변하지 않는 것

태그

3.2.1 JDBC try/catch/finally 코드의 문제점

try~catch~finally 코드를 통해 예외처리까지 수행하게 되었지만 복잡한 try~catch~finnaly 블록이 2중으로 중첩되까지 되어 나오는데다, 모든 메서드마다 반복되어 나타난다는 문제가 존재한다.
이런 코드를 작성할 때 Copy&Pates 를 통해서 작성할 수 있다. 하지만 어느 순간 한 줄을 빼먹고 복사했거나, 몇 줄을 잘못 삭제하는 경우에는 문제가 발생할 수 있다. 당장 컴파일 에러가 존재하지 않는다고 하더라도, 이전 코드와 같이 close() 가 제대로 호출되지 않아 커넥션이 쌓인다던 하는 상항이 발생할 수 있다는 것이다.
초기에 실수를 하지 않고 완벽하게 작성했더라도 이런 코드는 계속 문제가 될 가능성을 지니고 있다. 누군가 DAO 로직을 수정하려고 했을 때 복잡한 try~catch~finally 블록 안에서 필요한 부분을 찾아서 수정해야 하고, 언젠가 꼭 필요한 부분을 잘못 삭제해버리면 역시 같은 문제가 반복된다.
이 문제의 핵심은 변하지 않는, 그러나 많은 곳에서 중복되는 코드와 로직에 따라 자꾸 확장되고 자주 변하지 않는 코드를 잘 분리해내는 방법이다. 1장에서 살펴봤던 것과 비슷한 방법으로 접근하면 되지만, DAO와 DB 연결 기능을 분리하는 것과는 성격이 다르기 때문에 해결 방법이 조금 다르다.

3.2.2 분리와 재사용을 위한 디자인 패턴 적용

UserDao의 메서드를 개선하는 작업을 위해 변하지 않는 부분을 먼저 찾아보자.
//////////////////////////////변하지 않는 부분////////////////////////////// Connection c = null; PreparedStatement ps = null; try{ c = dataSource.getConnection(); //////////////////////////////변하는 부분/////////////////////////////////// ps = c.prepareStatement("delete from users"); /////////////////////////////변하지 않는 부분///////////////////////////// ps.executeUpdate(); } catch (SQLException e){ throw e; } finally { // finally 블록은 예외의 발생 여부와 관계없이 항상 실행된다. if(ps != null){ try{ ps.close(); } catch (SQLException e){} if(c != null){ try{ c.close(); } catch(SQLException e){} } }
Java
복사
PreparedStatement를 만들어서 업데이트용 쿼리를 실행하는 메서드라면 deleteAll() 메서드와 구조는 거의 비슷할 것이다. 비슷한 기능의 메서드에서 동일하게 나타날 수 있는 변하지 않고 고정되는 부분과, 각 메서드마다 로직에 따라 변하는 부분을 위와 같이 구분해 볼 수 있다.

메서드 추출

변하지 않는 부분을 재사용할 수 있는 방법으로 생각해볼 수 있는 첫번째 방법은 변하지 않는 부분을 메서드로 빼는 것이다.
public void deleteAll() throws SQLException{ ... try{ // 예외가 발생할 수 있는 코드 부분을 모두 try 블록으로 묶어줌 c = dataSource.getConnection(); ps = makeStatment(c); ps.executeUpdate(); } catch (SQLException e){ ... } private PreparedStatement makeStatement(Connection c) throws SQLException{ PreparedStatement ps; ps = c.prepareStatement("delete from users"); return ps; }
Java
복사
자주 바뀌는 부분을 메서드로 독립시켰는데 당장 봐서는 별 이득이 없어 보인다. 왜냐하면 보통 메서드 추출 리팩토링을 적용하는 경우에는 분리시킨 메서드를 다른 곳에서 재사용 할 수 있어야 하는데, 이건 반대로 분리시키고 남은 메서드가 재사용이 필요한 부분이고, 분리한 메서드는 DAO 로직마다 새롭게 만들어서 확장돼야 하는 부분이기 때문이다.

템플릿 메서드 패턴의 적용

템플릿 메서드 패턴은 상속을 통해 기능을 확장해서 사용하는 부분이다. 변하지 않는 부분은 슈퍼클래스에 두고 변하는 부분은 추상 메서드로 정의해둬서 서브클래스에서 오버라이드하여 새롭게 정의해서 쓰도록 하는 것이다.
추출해서 별도의 메서드로 독립시킨 메서드를 다음과 같이 추상 메서드로 변경한다. 그리고 이를 상속하는 서브클래스를 만들어서 거기서 이 메서드를 구현한다. 고정된 JDBC try/catch/finally 블록을 가진 슈퍼클래스 메서드와 필요에 따라 상속을 통해 구체적은 PreparedStatement를 바꿔서 사용할 수 있게 만드는 서브클래스로 깔끔하게 분리할 수 있다.
public class UserDaoDeleteAll() extends UserDao{ abstract protected PreparedStatement makeStatement(Connection c) throws SQLException{ PreparedStatement ps = c.prepareStatement("delete from users"); return ps; } }
Java
복사
하지만 템플릿 메서드 패턴으로의 접근은 제한이 많다. 가장 큰 문제는 DAO 로직마다 상속을 통해 새로운 클래스를 만들어야 한다는 점이다. 만약 이 방식을 사용한다면 UserDao의 JDBC 메서드가 4개일 경우 4개의 클래스를 만들어서 사용해야 한다.
또 확장구조가 이미 클래스를 설계하는 시점에서 고정되어 버린다는 단점도 존재한다. 따라서 그 관계에 대한 유연성이 떨어져 버린다.

전략 패턴의 적용

개방 폐쇄 원칙(OCP)을 잘 지키는 구조이면서도 템플릿 메서드 패턴보다 유연하고 확장성이 뛰어난 것이, 오브젝트를 아얘 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 전략 패턴이다.
전략 패턴은 OCP 관점에서 보면 확장에 해당하는 변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방식이다. 아래 그림은 전략 패턴의 구조를 나타낸 그림이다.
좌측에 있는 Context의 contextMethod() 에서 일정한 구조를 가지고 동작하다가 특정 확장 기능은 Strategey의 인터페이스를 통해 외부의 독립된 전략 클래스에 위임하는 것이다.
deleteAll() 메서드에서 변하지 않는 부분이라고 명시한 것이 바로 이 contextMethod()가 된다. deleteAll() 은 JDBC를 이용해 DB를 업데이트하는 작업이라는 변하지 않는 맥락(context)를 가진다.
deleteAll()의 컨텍스트
DB 커넥션 가져오기
PreparedStatement를 만들어줄 외부 기능 호출하기
전달받은 PreparedStatement 실행하기
예외가 발생하면 이를 다시 메서드 밖으로 던지기
모든 경우에 만들어진 PreparedStatement와 Connection을 적절히 닫아주기
두 번째 작업에서 사용하는 PreparedStatement를 만들어주는 외부 기능이 바로 전략 패턴에서 말하는 전략이라고 볼 수 있다. 전략 패턴의 구조에 따라 이 기능을 인터페이스로 만들어두고 인터페이스의 메서드를 통해 PreparedStatement 생성 전략을 호출해주면 된다.
public inteface StatementStrategy{ PreparedStatement makeStatement(Connection c) throws SQLException; } public class DeleteAllStatement implements StatementStrategy{ public PreparedStatement makeStatement(Connection c) throws SQLException{ PreparedStatement ps = c.prepareStatement("delete from users"); return ps; } } public void deleteAll() throws SQLException{ ... try{ c = dataSource.getConnection(); StatementStrategy strategy = new DeleteAllStatement(); ps = strategy.makeStatment(c); ps.executeUpdate(); } catch (SQLException e){ ... }
Java
복사
하지만 전략 패턴은 필요에 따라 컨텍스트는 그대로 유지되면서(OCP의 폐쇄 원칙) 전략을 바꿔 쓸 수 있다(OCP의 개방 원칙)는 것인데, 이렇게 컨텍스트 안에서 이미 구체적인 전략 클래스인DeleteAllState를 사용하도록 고정되어 있다면 OCP의 개방 폐쇄 원칙을 지킬 수 없다.
컨텍스트가 인터페이스뿐 아니라 특정 구현 클래스인 DeleteAllStatement를 직접 알고 있다는 건, 전략 패턴에도 OCP에도 잘 들어맞는다고 볼 수 없다.

DI 적용을 위한 클라이언트/컨텍스트 분리

전략 패턴에 따르면 Context가 어떤 전략을 사용하게 할 것인가는 Context를 사용하는 앞단의 Client가 결정하는 게 일반적이다. Client가 구체적인 전략의 하나를 선택하고 오브젝트로 만들어서 Context에 전달하는 것이다.
위의 구조는 1장에서 처음 UserDao 와 ConnectionMaker를 독립시켰을 때 적용했던 방법과 동일하다. 결국 이 구조에서 전략 오브젝트 생성과 컨텍스트로의 전달을 담당하는 책임을 분리시킨 것이 바로 ObjectFactory이며, 이를 일반화한 것이 앞에서 살펴봤던 의존관계 주입(DI)이었다.
결국 DI란 이러한 전략 패턴의 장점을 일반적으로 활용할 수 있도록 만든 구조라고 볼 수 있다.
이러한 패턴 구조를 코드에 적용시켜 보자. 가장 중요한 것은 이 컨텍스트에 해당하는 JDBC try/catch/finally 코드를 클라이언트 코드인 StatementStrategy를 만드는 부분에서 독립시켜야 한다는 점이다.
분리된 try/catch/finally 컨텍스트 코드
public ovid jdbcContextWithStatementStrategy(StatementStrategy stmt) throw 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){} } }
Java
복사
클라이언트 책임을 담당할 deleteAll() 메서드
public void deleteAll() throws SQLException{ StatementStrategy st = new DeleteAllStatement(); // 선정한 전략 클래스의 오브젝트 생성 jdbcContextWithStatementStrategy(st); }
Java
복사
컨텍스트를 별도의 메서드로 분리했으니 deleteAll() 메서드가 클라이언트가 된다. deleteAll()은 전략 오브젝트를 만들고 컨텍스트를 호출하는 책임을 지고 있다.
비록 클라이언트와 컨텍스트는 클래스를 분리하진 않았지만, 의존관계와 책임으로 볼 때 이상적인 클라이언트/컨텍스트 관계를 가지고 있다. 특히 클라이언트가 컨텍스트가 사용할 전략을 정해서 전달한다는 면에서 DI 구조라고 이해할 수도 있다.
이 과정을 통해서 관심사를 분리하고 유연한 확장관계를 유지하도록 만들 수 있다.