[토비의 스프링 3.1] 3.1 다시 보는 초난감 DAO

2022. 7. 5. 07:32토비의 스터디

1장에서 초난감 DAO 코드에 DI를 적용하면서 관심사가 다른 코드를 분리하였으며, 변경과 확장에 유연한 구조로 변경하였다. 이는 개방 폐쇄 원칙(OCP)을 잘 준수했다고 볼 수 있다.

3장에서는 템플릿에 대해 학습한다. 

템플릿이란 변경이 거의 일어나지 않는 부분자유롭게 변경되는 부분과 독립시켜서 효과적으로 활용할 수 있도록 하는 방법이다.

3장에서 스프링에 적용된 템플릿 기법을 살펴보자.

3.1 다시 보는 초난감 DAO

아직 UserDao의 코드에는 문제점이 남아 있다. 예외 상황에 대한 처리가 부족하다. 한번 살펴보자.

3.1.1 예외처리 기능을 갖춘 DAO

JDBC 코드에는 예외 처리를 반드시 해주어야 한다. 정상적인 JDBC 코드의 흐름을 따르지 않고 중간에 예외가 발생한 경우 사용한 리소스를 반.드.시 반환하도록 만들어야 하기 때문이다. 

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();
}

만약 PreparedStatement를 처리하는 중에 예외가 발생하면, 두 개의 공유 리소스를 반환하지 못한 채로 메소드를 빠져나간다. 

일반적으로 서버는 제한된 개수의 DB 커넥션을 만들어서 재사용 하는 pool로 관리한다. 따라서 close()로 반환해주어야 다음 커넥션 요청이 있을 때 재사용 가능하다. 반환되지 못한 커넥션이 쌓이면 결국 리소스가 모자란다는 심각한 오류를 내며 서버가 중단된다.

따라서 JDBC 코드는 try/catch/finally 사용을 권장하고 있다. 아래는 try/catch/finally를 적용한 deleteAll() 메소드다.

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

close()로 반환하는 과정에서도 SQLException이 발생할 수 있기 때문에 이 역시도 try catch로 한번 더 감싸주는 모습이다.
만약 ps.close()에서 예외가 발생하면, 그 뒤에 실행되는 c.close()는 실행되지 못하고 빠져나갈 수 있기 때문이다.

ps와 c의 널체크를 해주고 있는데, 이는 NullPointerException이 발생할 수 있기 때문이다. 만약 커넥션을 가져오다가 DB 서버에 문제가 생긴다면 ps는 물론 c도 null 상태다. 
커넥션을 갖고 있는 상태에서 PreparedStatement를 생성하던 중 예외가 발생하면 c는 close() 호출이 가능한 반면 ps는 아니다.

위와 같은 코드를 작성함으로써 예외상황에서도 안전한 코드가 되었다. 

JDBC 조회 기능의 예외처리

조회를 위한 JDBC 코드는 조금 더 복잡하다. ResultSet이 추가되기 때문이다. 코드로 바로 살펴보자.

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

이제 UserDao의 모든 메소드에 try/catch/finally를 적용했다. 

다음 장에서는 이 복잡한 코드를 하나씩 개선해보자!