1장에서는 초난감 DAO 코드에 DI를 적용하여, 코드를 분리하고 확장과 변경에 용이하게 대응할 수 있는 설계 구조로 개선하는 작업을 했다.
UserDao
생성,UserDao
에서 Connection
을 만드는 부분과 쿼리를 수행하는 부분을 다른 관심사로 분리ConnectionMaker
라는 클래스로 커넥션 생성 로직 분리ConnectionMaker
를 인터페이스화하고, DConnectionMaker
, NConnectionMaker
클래스 생성Client
에게 어떤 ConnectionMaker
를 사용할 것인지 책임을 위임DaoFactory
라는 클래스에 의존관계에 대한 책임을 위임 (제어의 역전)@Configuration
을 이용하여 의존관계를 설정UserDao
클래스를 등록하여 스프링 컨테이너에서 꺼내쓸 수 있게 만듦 (스프링을 이용한 제어의 역전)확장에는 자유롭게 열려 있고 변경에는 굳게 닫혀 있다는 객체지향 설계의 핵심 원칙인 개방 폐쇄 원칙을 적용했다.
개방 폐쇄의 원칙에서의 전제조건은 코드의 성질을 두 부류로 나눌 수 있다는 가정에서 출발한다.
변화의 특성이 다른 부분을 구분해주고, 각각 다른 목적과 다른 이유에 의해 다른 시점에 독립적으로 변경될 수 있는 효율적인 구조를 만들어주는 것이 개방 폐쇄의 원칙이다.
템플릿이란 코드 중 변경이 거의 일어나지 않으며, 일정한 패턴으로 유지되는 특성을 가진 부분
을 자유롭게 변경되는 성질을 가진 부분
으로부터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법이다.
초난감 DAO는 많은 개선을 했지만, 예외상황에 대한 처리를 하지 않았다.
DB 커넥션은 DB의 소중한 자원이므로, 어떤 이유로든 예외가 발생하더라도 리소스를 반드시 반환해야 한다. 그렇지 않으면 시스템에 큰 문제를 일으킬 수 있다.
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
를 처리하는 중에 예외가 발생하면 메소드 실행을 끝마치지 못하고 바로 메소드를 빠져나가게 되어 Connection
과 PreparedStatement
의 close()
메소드가 실행되지 않아 제대로 리소스가 반환되지 않을 수 있다.
서버는 DB 커넥션을 풀로 관리하는데, 리소스가 반환되지 않으면, 서버는 언젠가 리소스가 모자란다는 에러를 내며 중단될 것이다.
.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) {
}
}
}
}
예외가 발생할 수 있는 모든 시점을 고려해주어서 코드를 작성하였다.
c
와ps
가null
이 아니라면.close()
를 해주어야 한다.
getConnection()
을 이용한 커넥션 생성 지점에서 예외가 나면, c
와 ps
모두 null
상태이다..close()
를 호출하면 NullPointerException
이 발생한다.PreparedStatement
를 생성하다가 예외가 발생하면 그 때는 ps
만 null
상태이다.ps
를 실행하다가 예외가 발생했다면 ps
와 c
모두 null
이 아니다.finally
에서는 두 변수가 null
이 아닌지 체크한 뒤에 .close()
를 호출하면 된다..close()
도 SQLException
이 발생할 수 있다. .close()
를 수행하는 도중 SQLException
이 발생하면 .close()
아래의 로직이 실행되지 않기 때문에, catch
를 해주는 것이 좋다.close()
의 try/catch
는 혹시 예외처리가 필요할 수도 있으니 해주었다.조회를 위한 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) {
}
}
}
}
코드를 바꾼 뒤에 정상적으로 동작하는지 확인