3. 템플릿 - 변하는 것과 변하지 않는 것

이유석·2022년 5월 23일
0

Book - Toby's Spring

목록 보기
15/20
post-thumbnail

변하는 것과 변하지 않는 것

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

이전 장의 UserDao 예외 처리를 코드를 보면 문제점이 존재한다.

  • 복잡한 try / catch / finally 블록이 2중으로 중첩된다.
  • 모든 메소드마다 반복된다.

위의 코드를 작성하기에 효과적인 방법

  1. 복사해서 붙이기 : 인간이 수행하는 것 이기 때문에 빼먹는 부분이 있을 수 있다.

  2. 테스트를 통해 DAO마다 예외상황에서 리소스를 반납하는지 확인 : 테스트를 위한 예외상황 제작이 힘듦

문제 해결을 위한 위 2가지 방법은 한계가 존재한다.

해당 문제의 핵심

  • 변하지 않는, 그러나 많은 곳에서 중복되는 코드와 로직에 따라 자꾸 확장되고 변하는 코드를 잘 분리해내는 것

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

그렇다면 UserDao 의 코드를 수정해보자.

가장 먼저 할 일은 변하는 성격이 다른 것(변하는 부분 / 변하지 않는 부분)을 찾아내는 것이다.

UserDao 의 deleteAll() 메소드의 코드를 다시 살펴보자.

public void deleteAll() throws SQLException {
//*****************변하지 않는 부분*****************
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = this.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) {

            }
        }
    }
//*****************변하지 않는 부분.*****************
}

만약 add() 메소드라면, 위 deleteAll() 코드의 변하지 않는 부분만 아래와 같이 변경해주면 될 것 이다.

...

//*****************변하는 부분*********************
ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
//*****************변하는 부분*********************

...

나머지 코드는 전혀 수정하지 않아도 된다.

그렇다면 이 로직에서 변하는 부분을 변하지 않는 나머지 코드에서 분리하는 것이 어떨까?
그렇게 할 수 있다면 변하지 않는 부분을 재사용할 수 있는 방법이 있을 것 이다.


메소드 추출

변하지 않는 부분이 변하는 부분을 감싸고 있기 때문에 (변하지 않는 부분의)메소드 추출이 어려워 보인다.
반대로 수행해 보면 아래와 같은 코드가 된다.

public void deleteAll() throws SQLException {
	...
	try {
    	c = this.dataSource.getConnection();

		ps = makeStatement(c);
        
        ps.executeUpdate();
    } catch (SQLException e) {
    ...
}

private PreparedStatement makeStatement(Connection c) throws SQLException {
	PreparedStatement ps;
    ps = c.prepareStatement("delete from users");
    return ps;
}

자주 바뀌는 부분을 메소드로 독립 시켜보았지만, 당장 봐서는 별 이득이 없어보인다.

  • 분리시키고 남은 부분이 재사용이 필요한 부분이다.
  • 분리된 메소드는 DAO의 특정 로직마다 새롭게 만들어서 확장해야 하는 부분이다.

템플릿 메소드 패턴의 적용

  • 상속을 통해 기능을 확장해서 사용하는 부분이다.
  • 즉 변하지 않는 부분은 슈퍼클래스에 두고
    변하는 부분은 추상메소드로 정의해둬서 서브클래스에서 오버라이드하여 새롭게 정의해 쓰도록 하는 것 이다.

위에서 추출한 makeStatement() 메소드를 추상 메소드로 변경시켜 보자.
당연히 UserDao 클래스도 추상 클래스로 변경해줘야 한다.

public abstract class UserDao {
    ...
    abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;
	...
}

그런 다음 UserDao 의 서브 클래스를 생성하여, makeStatement 메소드를 구현해주자.

public class UserDaoDeleteAll extends UserDao{
    @Override
    protected PreparedStatement makeStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("delete from users");

        return ps;
    }
}

이제 UserDao 의 기능을 확장하고 싶을 때마다 상속을 통해 자유롭게 화장 가능하고, 확장으로 인하여 기존 UserDao 클래스에 불필요한 변화는 생기지 않도록 할 수 있으니, 개방폐쇄원칙을 지키는 구조를 만들어 낼 수 있었다.

하지만 템플릿 메소드 패턴으로의 접근은 제한이 많다.

  1. 각 DAO 로직 (add, deleteAll, get, getCount) 마다 상속을 통해 새로운 서브 클래스를 만들어줘야 한다.

  2. 확장구조가 이미 클래스를 설계하는 시점에서 고정되어 버린다.
    즉, 관계에 대한 유연성이 떨어진다.


전략 패턴의 적용

  • 개방 폐쇄 원칙을 잘 지키는 구조이면서도 템플릿 메소드 패턴보다 유연하고 확장성이 뛰어난 것
  • 오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만든 것

전략 패턴은 개방 폐쇄 원칙 관점에서 보면 확장에 해당하는 변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방식이다.

그림 3-2는 전략 패턴의 구조를 나타낸다.

  • 좌측에 있는 Context의 contextMethod()에서는 일정한 구조를 가지고 동작한다.
  • 특정 확장 기능은 Strategy 인터페이스를 통해 외부의 독립된 전략 클래스에 위임하는 것이다.

deleteAll() 메소드의 Context에 해당하는 부분을 정리해보자.

  • DB connection 가져오기
  • PreparedStatement를 만들어줄 외부 기능 호출하기
  • 전달받은 PreparedStatement 실행하기
  • 예외가 발생하면 이를 다시 메소드 밖으로 던지기
  • 모든 경우에 만들어진 PreparedStatement와 Connection을 적절히 닫아주기

이 중에서 Strategy(전략)에 해당하는 부분은 두 번째인 PreparedStatement를 만들어주는 외부 기능이다.

전략 패턴에 따라 이 기능을 인터페이스로 만들어두고, 인터페이스의 메소드를 통해 PreparedStatement 생성 전략을 호출해주면 된다.

PreparedStatement 생성 전략의 인터페이스를 작성해보자.

public interface StatementStrategy {
	PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}

해당 인터페이스를 상속하여 실제 전략, 즉 바뀌는 부분인 PreparedStatement를 생성하는 클래스를 만들어보자.

public class DeleteAllStatement implements StatementStrategy{
    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("delete from users");
        return ps;
    }
}

이제 확장된 PreparedStrategy 전략인 DeleteAllStatement가 만들어졌다.

이것을 UserDao의 deleteAll() 메소드에서 사용하면 전략 패턴을 적용했다고 볼 수 있을까?

public void deleteAll() throws SQLException {
	...
    try {
    	c = dataSource.getConnection();
        
        StatementStrategy strategy = new DeleteAllStatement();

		ps = strategy.makePreparedStatement(c);

		ps.executeUpdate();
    } catch (SQLException e) {
    ...
}

아니다.
전략 패턴은 필요에 따라 컨텍스트는 유지되면서 전략을 바꿔 쓸 수 있다는 것이다.
하지만 위와 같이, 이미 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있다면 뭔가 잘못된 것 이다.


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

위 문제를 해결하기 위해, 전략 패턴의 실제적인 사용 방법을 좀 더 살펴보자.

전략 패턴에 따르면 Context가 어떤 Strategy(전략)을 사용하게 할 것인가는 Context를 사용하는 앞단의 Client가 결정해야 한다.

즉 Client가 구체적인 전략의 하나를 선택하고 해당 오브젝트를 만들어서 Context에 전달하는 것 이다.

위 구조는 이전 1장에서 처음 UserDao와 ConnectionMake를 독립 시킨 후, UserDao가 구체적인 ConnectionMaker 구현 클래스를 만들어 사용하는 데 문제가 있다고 판단했을 때 적용했던 방법과 동일하다.

즉 전략 오브젝트 생성과 컨테스트로의 전달을 담당하는 책임을 분리시킨 것이 바로 Object Factory이며, 이를 일반화한 것이 앞에서 살펴봤던 의존관계 주입(DI) 이다.

결국 DI란 이러한 전략 패턴의 장점을 일반적으로 활용할 수 있도록 만든 구조라고 볼 수 있다.

위 패턴 구조를 코드에 적용시켜 보자.

컨텍스트에 해당하는 JDBC try / catch / finally 코드를 클라이언트 코드인 StatementStrategy를 만드는 부분에서 독립시켜야 한다는 것이 가장 중요하다.

컨텍스트에 해당하는 부분을 별도의 메소드로 독립시켜보자.
이때 클라이언트는 DeleteAllStatement 오브젝트 같은 전략 클래스의 오브젝트를 컨텍스트 메소드로 전달해야 한다.

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) {}}
    }
}

위 코드는 컨텍스트를 메소드로 분리한 것이다.

다음은 클라이언트에 해당하는 부분을 살펴보자.
컨텍스트를 별도의 메소드로 분리했으니 deleteAll() 메소드가 클라이언트가 된다.

deleteAll()은 전략 오브젝트를 만들고 컨텍스트에게 해당 오브젝트를 전달함과 동시에 컨텍스트를 호출하는 책임을 갖고있다.

public void deleteAll() throws SQLException {
    StatementStrategy st = new DeleteAllStatement();
    jdbcContextWithStatementStrategy(st);
}

위 코드는 클라이언트 책임을 갖도록 재구성한 deleteAll() 메소드이다.

이제 구조로 볼 때 완벽한 전략 패턴의 모습을 갖췄다.

특히 클라이언트가 컨텍스트가 사용할 전략을 정해서 전달한다는 면에서 DI 구조라고 이해할 수도 있다.

소스코드 : github

profile
https://github.com/yuseogi0218

0개의 댓글