토비의 스프링 정리 프로젝트 #3.1 템플릿과 다시보는 초난감 DAO

Jake Seo·2021년 7월 18일
1

토비의 스프링

목록 보기
17/29

템플릿이란?

1장에서는 초난감 DAO 코드에 DI를 적용하여, 코드를 분리하고 확장과 변경에 용이하게 대응할 수 있는 설계 구조로 개선하는 작업을 했다.

  1. UserDao 생성,
  2. UserDao에서 Connection을 만드는 부분과 쿼리를 수행하는 부분을 다른 관심사로 분리
  3. ConnectionMaker라는 클래스로 커넥션 생성 로직 분리
  4. ConnectionMaker를 인터페이스화하고, DConnectionMaker, NConnectionMaker 클래스 생성
  5. Client에게 어떤 ConnectionMaker를 사용할 것인지 책임을 위임
  6. DaoFactory라는 클래스에 의존관계에 대한 책임을 위임 (제어의 역전)
  7. 스프링 @Configuration을 이용하여 의존관계를 설정
  8. 스프링 빈에 UserDao 클래스를 등록하여 스프링 컨테이너에서 꺼내쓸 수 있게 만듦 (스프링을 이용한 제어의 역전)

확장에는 자유롭게 열려 있고 변경에는 굳게 닫혀 있다는 객체지향 설계의 핵심 원칙인 개방 폐쇄 원칙을 적용했다.

개방 폐쇄의 원칙에서의 전제조건은 코드의 성질을 두 부류로 나눌 수 있다는 가정에서 출발한다.

  • 변경을 통해 그 기능이 다양해지며 확장되는 성질을 가진 코드가 있다.
  • 고정되어 있고 변하지 않으려는 성질을 가진 코드가 있다.

변화의 특성이 다른 부분을 구분해주고, 각각 다른 목적과 다른 이유에 의해 다른 시점에 독립적으로 변경될 수 있는 효율적인 구조를 만들어주는 것이 개방 폐쇄의 원칙이다.

템플릿이란 코드 중 변경이 거의 일어나지 않으며, 일정한 패턴으로 유지되는 특성을 가진 부분자유롭게 변경되는 성질을 가진 부분으로부터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법이다.

다시 보는 초난감 DAO

초난감 DAO는 많은 개선을 했지만, 예외상황에 대한 처리를 하지 않았다.

예외 처리 기능을 갖춘 DAO

DB 커넥션은 DB의 소중한 자원이므로, 어떤 이유로든 예외가 발생하더라도 리소스를 반드시 반환해야 한다. 그렇지 않으면 시스템에 큰 문제를 일으킬 수 있다.

예외처리 없는 JDBC 코드

    public void deleteAll() throws SQLException {
        Connection c = dataSource.getConnection();
        
        // 여기서 예외가 발생되면 실행이 중단
        PreparedStatement ps = c.prepareStatement("delete from users");
        ps.executeUpdate();

        ps.close();
        c.close();
    }

위는 .deleteAll()의 코드다.

위 메소드에서는 정상적인 흐름의 경우에는 ps.close()c.close()가 잘 호출되어 리소스를 반환한다. 그런데, PreparedStatement를 처리하는 중에 예외가 발생하면 메소드 실행을 끝마치지 못하고 바로 메소드를 빠져나가게 되어 ConnectionPreparedStatementclose() 메소드가 실행되지 않아 제대로 리소스가 반환되지 않을 수 있다.

서버는 DB 커넥션을 풀로 관리하는데, 리소스가 반환되지 않으면, 서버는 언젠가 리소스가 모자란다는 에러를 내며 중단될 것이다.

리소스 반환과 close()

.close()라서 열린 것을 닫는 것으로 생각하기 쉽다. 하지만 엄밀히 말하면 리소스를 반환한다고 이해하는 것이 좋다.

예외 상황에서 리소스 반환하기

예외 상황에서도 리소스를 제대로 반환할 수 있도록 try/catch/finally를 적용해보자.

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

    try {
        // 예외가 발생할 수 있는 부분은 전부 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();
                // `ps.close()` 메소드에서도 `SQLException` 이 발생할 수 있다.
                // 이를 잡아주지 않으면, 아래 `Connection (c)`을 반환하는 로직이 수행되지 않을 수 있다.
            } catch (SQLException e) {
            }
        }

        if(c != null) {
            try {
                c.close(); // Connection 반환
            } catch (SQLException e) {
            }
        }
    }
}

예외가 발생할 수 있는 모든 시점을 고려해주어서 코드를 작성하였다.

cpsnull이 아니라면 .close()를 해주어야 한다.

  • getConnection()을 이용한 커넥션 생성 지점에서 예외가 나면, cps 모두 null 상태이다.
    • 이 경우 널체크를 하지 않고 .close()를 호출하면 NullPointerException이 발생한다.
  • PreparedStatement를 생성하다가 예외가 발생하면 그 때는 psnull 상태이다.
  • ps를 실행하다가 예외가 발생했다면 psc 모두 null이 아니다.
  • finally에서는 두 변수가 null이 아닌지 체크한 뒤에 .close()를 호출하면 된다.
  • 문제는 .close()SQLException이 발생할 수 있다. .close()를 수행하는 도중 SQLException이 발생하면 .close() 아래의 로직이 실행되지 않기 때문에, catch를 해주는 것이 좋다.
    • 마지막 close()try/catch는 혹시 예외처리가 필요할 수도 있으니 해주었다.

JDBC 조회 기능의 예외처리

조회를 위한 JDBC 코드는 더 복잡해진다. ResultSet이 더 추가되기 때문이다. getCount()의 예외처리를 해보자.

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 {
        // `ResultSet`의 `null`을 체크하고 닫아주는 부분 추가
        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) {
            }
        }
    }
}

테스트

코드를 바꾼 뒤에 정상적으로 동작하는지 확인

profile
풀스택 웹개발자로 일하고 있는 Jake Seo입니다. 주로 Jake Seo라는 닉네임을 많이 씁니다. 프론트엔드: Javascript, React 백엔드: Spring Framework에 관심이 있습니다.

0개의 댓글