본문 바로가기
Spring&SpringBoot/<토비의 스프링 3.1 Vol1.1>, 이일민

반복되는 예외처리, 전략 패턴으로 개선하기

by 민휘 2023. 3. 13.

문제 상황

Dao의 JDBC를 이용하는 메소드에서 예외 처리를 해주었더니 유사한 try-catch-finally 문이 반복된다. 계속 이렇게 복붙해서 쓰다가 실수라도 하면 예외 처리가 제대로 안돼서 시스템에 문제가 발생할 수도 있고, 무엇보다 너무 지저분하다. 유지 보수도 어려울 것이다. 반복되는 코드를 캡슐화해서 중복을 줄여보자!

// 중복되는 예외 코드
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) {}
            }
        }
}

public int add() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();
        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());
        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) {}
            }
            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {}
            }            
}

 

 

 

접근 방법

중복되고 있는 코드 속에서 변경되는 부분과 변경되지 않는 부분을 분리해보자. 변경되지 않는 부분을 분리해서 재사용하면 중복을 없앨 수 있을 것이다. 변경되지 않는 부분은 리소스를 준비하고 sql을 실행하는 부분이다. 변경되는 부분은 PreparedStatement를 생성하고 파라미터를 세팅하는 부분이다. 우선 변경되는 부분부터 분리해본다.

PreparedStatement를 준비하는 부분이 변경되므로, 해당 책임을 외부로 옮기고 다음과 같이 사용하기로 했다.

public void deleteAll() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();

        // 변경되는 부분
        StatementStrategy strategy = new DeleteAllStatement();
        ps = strategy.makePreparedStatement(c);

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

 

변경되는 부분은 인터페이스를 두고 SQL문마다 구현체를 만든다.

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

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

 

그런데 deleteAll() 메소드는 여전히 DeleteAllStatement라는 구현체에 의존하고 있다. 이래서는 변하지 않는 부분만을 분리할 수 없다. DI를 사용해 구현체 선택을 외부의 책임으로 돌리자.

// 매개변수로 추가 (메소드 주입)
public void deleteAll(StatementStrategy strategy) throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();

        // 변경되는 부분 호출
        ps = strategy.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()는 변경되는 부분으로부터 자유롭다. 이 메소드의 이름을 jdbcContextWithStatementStrategy 로 변경해 모든 메소드에서 호출 가능하도록 추출하자. dao 내부 메소드에서만 사용할거니까 private으로 선언했다.

private 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()이 변하지 않는 부분을 담은 메소드를 사용하고, 삭제 SQL을 생성하는 구현체를 생성해 주입하도록 만들자. 매개변수는 필요하지 않으니 지워버리자.

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

 

반복되는 부분이 동일한 add() 메소드도 동일하게 리팩토링할 수 있다. SQL을 만들 때 User가 필요하므로 인스턴스 필드로 두고 생성자로 받는다.

public void add(User user) throws SQLException {
    StatementStrategy addStatementStrategy = new AddStatement(user);
    jdbcContextWithStatementStrategy(addStatementStrategy);
}

public class AddStatement implements StatementStrategy {

    User user;

    public AddStatement(User user) {
        this.user = user;
    }

    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement 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());
        return ps;
    }
}

이제 dao에는 흉측한 try-catch-finally문이 딱 한번 나온다. 그리고 dao 메소드는 이 복잡한 코드의 내부를 알 필요 없이 가져다 쓰면 된다. 중복도 없어지고 실수할 가능성도 없어졌다. 예쁜 코드 완성이다!

 

전략 패턴

위의 개선 과정에서 적용한 접근 방법은 전략 패턴이다.

목적은 변경되는 부분을 변경되지 않는 부분으로부터 캡슐화하는 것이다.

이렇게 되면 변경되지 않는 부분은 재사용이 가능해지므로 유연하고 생산적인 설계를 만들 수 있다.

전략 패턴은 변경 분리 목적을 위해 합성 방식을 사용한다는 점이 특징이다.

이 협력을 만들기 위해 필요한 책임은 크게 네 가지이다.

변하지 않는 책임, 변하는 책임, 의존성을 주입할 책임, 그리고 연결된 두 객체를 사용할 책임.

전략 패턴은 이 책임을 가진 역할을 각각 컨텍스트, 개별 전략, 팩토리, 클라이언트이다.

위의 예제에서 컨텍스트는 jdbcContextWithStatementStrategy가, 전략은 StatementStrategy가, 의존성 주입 책임과 클라이언트는 Dao의 메소드가 하고 있다.

책임만 명확하게 가질 수 있다면 구현을 꼭 클래스 단위로 쪼개지 않아도 된다.