PengTory

[Spring] 템플릿 본문

Spring

[Spring] 템플릿

펭토리 2022. 10. 21. 12:38

JDBC try/ catch/ finally 

JDBC 수정 기능의 예외처리 코드

앞서 만든 UserDao 코드에서는 많은 점을 개선 했지만 아직 예외상황에 대한 처리가 없다는 문제점이 있다.

기존의 deleteAll() 코드를 보자.

public void deleteAll() throws SQLException{
    Connection c = dataSource.getConnection();
    
    // 여기서 예외가 발생하면 바로 메소드 실행이 중단됨
    PreparedStatement ps = c.prepareStatement("delete from users");
    ps.executeUpdate();
    
    ps.close();
    c.close();
}

정상 처리가 된다면 메소드를 마치기 전 close() 를 호출해 리소드를 반환한다.

하지만 PreparedStatement를 처리하던 중 예외가 발생한다면 메소드 실행을 끝마치지 못하고 메소드를 빠져나간다.

이렇게 되면 Connection과 PreparedStatement의 close() 메소드가 실행되지 않아 제대로 리소스가 반환되지 않을 수 있다.

 

여기서 잠깐 리소스 반환과 close()에 대해 알아보자.

Connection이나 PreparedStatement에는 close() 메소드가 있다. 이 둘은 보통 풀(pool) 방식으로 운영되는데 미리 정해진 풀 안에 제한된 수의 리소스를 만들어 두고 필요할 때 할당하고, 반환하면 다시 풀에 넣는 방식으로 운영된다. 요청이 많은 서버 환경에서는 새로운 리소스를 생성하는 대신 미리 만들어둔 리소스르 돌려가며 사용해야하고 리소스는 빠르게 반환해야한다. 그렇지 않으면 풀에 있는 리소스가 고갈되고 문제가 발생하기 때문이다. 여기서 close() 메소드는 사용한 리소스를 풀로 다시 돌려주는 역할을 한다.

 

다시 본론으로 돌아와 위 deleteAll() 코드의 예외처리를 해주려면 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) {
                }
            }
        }
    }
}

 

JDBC 조회 기능의 예외처리

조회는 Connection, PreparedStatement 외에도 ResultSet이 추가되기 때문에 ResultSet의 close() 메소드도 반드시 호출되도록 만들면 된다.

아래 코드처럼 바꾸어주었다.

public int getCount() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;
    ResultSet rs = null;

    try {
        c = dataSource.getConnection();

        ps = c.prepareStatement("Select count(*) from users");

        rs = ps.executeQuery();
        rs.next();
        return rs.getInt(1);
    } catch (SQLException e) {
        throw e;
    } finally {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
            }
        }
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e) {
            }
        }
        if (c != null) {
            try {
                c.close();
            } catch (SQLException e) {
            }
        }
    }
}

 

JDBC try / catch / finaly 코드의 문제점

예외 처리를 해서 완성도가 높아지긴 했지만 try / catch / finaly 블록이 2중으로 중첩되고 모든 메소드마다 반복되는 문제가 있다.

1장에서 배운 것처럼 중복되는 로직과 코드를 잘 분리해보자

 

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

  • 메소드 추출 : deleteAll() 코드에서 변하는 부분을 메소드로 빼보자.

자주 바뀌는 뿐을 아래처럼 독립시켜보았다. 그러나 보통 메소드 추출 리팩토링을 적용하는 경우에는 분리시킨 메소드를 다른 곳에서 재사용할 수 있어야 하는데 이건 반대로 분리시키고 남은 메소드가 재사용이 필요하다. 다른 방법을 고려해보자.

public void deleteAll() throws SQLException {
    // ...

    try {
        c = 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;
    }
}

 

  • 탬플릿 메소드 패턴의 적용

탬플릿 메소드 패턴은 상속을 통해 기능을 확장해 사용하는 부분이다. 변하지 않는 부분은 슈퍼클래스에, 변하는 부분은 추상클래스로 정의해서 서브 클래스에서 오버라이드 해서 사용한다.

아래는 makeStatement()를 구현한 UserDao 서브 클래스이다.

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

그렇지만 이 방법도 제한이 많다. DAO 로직마다 상속을 통해 새로운 클래스를 만들어야 한다.

JDBC 메소드가 4개일 경우 아래 사진과 같은 구조가 되어버린다.

탬플릿 메소드 패턴의 적용

또한 확장구조가 이미 클래스를 설계하는 시점에서 고정되어 버린다. 따라서 관계에 대한 유연성이 현저히 떨어지는 것을 볼 수 있다.

 

  • 전략 패턴의 적용

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

전략 패턴의 구조

StatementStrategy 인터페이스

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

deleteAll() 메소드의 기능을 구현한 StatementStrategy 전략 클래스

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

하지만 이렇게 컨텍스트 안에서 이미 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있다면 이상하다. 컨텍스트가 StatementStrategy 인터페이스 뿐 아니라 특정 구현 클래스인 DeleteAllStatement를 직접 알고있는것은 전략패턴도, OCP도 아니다.

 

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

위 문제를 해결해보자. 전략 패턴에 따르면 Contect가 어떤 전략을 사용하게 할 것인가는 Context를 사용하는 앞단의 Client가 결정하는 것이 일반적이다.

전략 패턴에서 Client의 역할

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

컨텍스트에 해당하는 부분은 별도의 메소드로 독립시켜보겠다.

모든 JDBC 코드의 틀에 박힌 작업은 이 컨텍스트 메소드 안에 담겨있다. 

public void jdbcContectWithStatementStrategy (StatementStraregy stmt) throws
    SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSources.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() 메소드에 있던 변하지 않는 부분, 자주 변하는 부분을 전략 패턴을 사용해 깔끔히 분리했다.

다음 포스팅에는 add() 메소드에 적용해볼 예정이다.

 

 

[참고자료] 토비의 스프링 3.1 Vol.1 스프링의 이해와 원리

'Spring' 카테고리의 다른 글

JDBC와 JPA 그리고 Hibernate  (0) 2022.12.22
[Spring] 예외  (1) 2022.11.01
[Spring] 테스트  (0) 2022.10.14
[Spring] 오브젝트와 의존관계 (5) _ XML  (0) 2022.10.14
[Spring] 오브젝트와 의존관계 (5) _ DI  (0) 2022.10.14