Spring/[책] 토비의 스프링

[토비의 스프링] 3장. 템플릿

궝대 2022. 1. 5. 22:49
반응형

이 포스팅은 토비의 스프링 3.1을 읽고 글쓴이가 좀 더 이해하기 쉽도록 정리한 내용입니다.

이 포스팅의 대상은 토비의 스프링을 읽어보신 분들이 한 번쯤 다시 기억을 상기시키고자 하는 분들입니다. 

글쓴이가 이미 알고 있는 내용에 대해서는 생략합니다. 또한 소스코드도 생략이 많으니 유의해서 읽어주시기 바랍니다.


1장에서 봤던 디자인 패턴 중 하나인 템플릿 메서드 패턴과 같이 템플릿이란 코드 중에서 변경이 자주 일어나는 부분과 코드를 독립시켜 변경이 거의 일어나지 않는 부분을 만들어 활용하는 방법을 칭한다.

 

3.1 다시 보는 초난감 DAO

 

우리는 DAO에서 1. 커넥션을 얻고, 2. PreparedStatement로 쿼리를 수행하고 3. 리소스를 회수했다.

public void deleteAll() throws SQLException, ClassNotFoundException {
		// 1. 커넥션 얻기
		Connection c = dataSource.getConnection();

		// 2. 쿼리 수행
		PreparedStatement ps = c.prepareStatement("delete from users");
		ps.executeUpdate();

		// 3. 리소스 회수
		ps.close();
		c.close();
	}

그런데 왜 리소스를 회수해야 할까?

 

일반적으로 DB를 사용할 때 커넥션을 매번 생성하지 않는다. 만약 DB 요청이 많은 상황에서 DB 연결을 위한 커넥션을 매번 생성한다면 다른 작업도 수행해야 하는데 커넥션도 계속 생성해야 하므로 더욱 악순환만 반복되게 된다.

 

위와 같은 문제점을 해결하기 위해 바구니와 같은 풀(Pool) 안에 커넥션을 미리 만들어 두고, 요청이 들어올 때 풀에서 꺼내 해당 커넥션을 사용하도록 한다. 풀 방식을 사용하면 많은 요청이 들어와도 미리 만들어 둔 커넥션을 사용하게 하면 되기 때문에 다른 작업을 더 빠르게 할 수 있다. 또한 풀 안에 더 이상 사용 가능한 커넥션이 없다면 나머지 요청은 대기하게 할 수 있기 때문에 부하를 줄일 수 있다.

 

다만 아무도 풀에서 가지고 간 커넥션을 반환하지 않으면 어떻게 될까? 더 이상 요청을 처리하지 못하고 결국 문제가 발생한다. 따라서 모든 작업을 완료했다면 리소스를 풀로 반드시 돌려줘야 한다. 이러한 이유로 우리는 3. 리소스 회수를 수행했던 것이다.

 

하지만 위 코드에서 3. 리소스 회수가 수행되지 못할 수가 있다. 3. 리소스 회수 전에 있는 코드에서 예외가 발생하면 메서드 실행을 끝마치지 못하고 메서드를 빠져나가게 되므로 리소스를 반환하지 못한다.

 

그래서 일반적으로 JDBC 코드에서는 아래와 같이 try/catch/finally 코드로 작성한다.

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

		try {
			// 1. 커넥션 얻기
			c = dataSource.getConnection();
			
			// 2. 쿼리 수행
			ps = c.prepareStatement("delete from users");
			ps.executeUpdate();
		} catch (SQLException e) {
			throw e;
		} finally {
			// 3. 리소스 회수
			try {
				if (ps != null) {
					ps.close();
				}
			} catch (SQLException e) { 

			}

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

			}
		}
	}

finally는 정상 수행하든 예외가 발생하든 반드시 수행되므로 해당 블록에서 리소스 반환을 하면 된다.

예외가 어느 위치에서 발생하냐에 따라 ps와 c가 null이 될 수 있다. 따라서 NPE를 방지하기 위해 null 체크를 해준다.

 

close() 메서드도 예외가 발생할 수 있다. 만약 아래와 같이 작성하였을 때 문제점은 무엇일까?

public void deleteAll() throws SQLException {
		...

		} finally {
			if (ps != null) {
				ps.close();
			}

			if (c != null) {
				c.close();
			}
			
		}
	}

ps.close에서 예외가 발생한다면 c.close()가 호출되지 않고 끝나게 된다. 따라서 c의 리소스는 반환하지 않은 채로 남게 되는 문제가 발생한다. 따라서 이를 방지하기 위해 close() 메서드도 try/catch로 묶어서 예외가 발생해도 아래 코드가 수행되도록 한다.

 

지금까지 봤던 delete 코드는 리소스를 Connection과 PreparedStatement를 사용하여 반환하였다. 데이터를 조회하는 코드는 두 리소스뿐만 아니라 ResultSet 리소스를 추가적으로 사용하기 때문에 해당 리소스도 반환해줘야 한다.

예를 들어 데이터 단건을 조회하는 기존 코드는 다음과 같다.

public User get(String id) throws ClassNotFoundException, SQLException {
		Connection c = dataSource.getConnection();

		PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
		ps.setString(1, id);

		ResultSet rs = ps.executeQuery();
		rs.next();

		User user = new User();
		user.setId(rs.getString("id"));
		user.setName(rs.getString("name"));
		user.setPassword(rs.getString("password"));

		rs.close();
		ps.close();
		c.close();

		return user;
	}

이제 이 코드도 try/catch/finally로 바꾸면 아래와 같다.

public User get(String id) throws ClassNotFoundException, SQLException {
		Connection c = null;
		PreparedStatement ps = null;
		ResultSet rs = null;

		try {
			c = dataSource.getConnection();

			ps = c.prepareStatement("select * from users where id = ?");
			ps.setString(1, id);

			rs = ps.executeQuery();
			rs.next();

			User user = new User();
			user.setId(rs.getString("id"));
			user.setName(rs.getString("name"));
			user.setPassword(rs.getString("password"));

			return user;
		} catch (SQLException e) {
			throw e;
		} finally {
			try {
				if (rs != null) {
					rs.close();
				}
			} catch (SQLException e) {

			}

			try {
				if (ps != null) {
					ps.close();
				}
			} catch (SQLException e) {

			}

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

			}
		}

	}

완벽하게 동작하는 JDBC 코드를 완성하였다. 하지만 코드가 좀 그렇지 않은가..?

반응형