[토비의 스프링] - 템플릿

✔ 템플릿


이 포스팅은 토비의 스프링을 읽고 개인적으로 정리하는 글입니다.

템플릿

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

다시 보는 초난감 DAO

예외처리 기능을 가진 DAO

public void deleteAll() throws SQLException {
	Connection c =dataSource.getConnection();
    
	PreparedStatement ps =c.prepareStatement("delete from users");
	ps.executeUpdate();
	
	ps.close();
	c.close();
}
  • 위의 코드는 .close()를 통해 자원을 반납하기 전에 예외가 발생하면 리소스가 정상적으로 반환되지 않을 수 있다. 이렇게 Pool에 있던 모든 리소스를 다 사용하고 반납하지 못하게 되면 리소스가 모자라다는 심각한 오류를 내며 서버가 중단될 수 있음.
  • 따라서 예외가 발생해도 정상적으로 자원을 반납할 수 있도록 아래와 같은 코드를 작성해야 한다.(책에서는 JDK 1.6 기준이라 try-catch-finally문법을 사용하지만 현재는 <AutoCloseable> 인터페이스의 등장으로 인해 try-with-resources문법을 사용하는 것이 더 효율적이다)
public void deleteAll() throws SQLException { 
	Connection c =null;
	PreparedStatement ps =null;
	try {
		rc =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이 추가된다.
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.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/finally 코드의 문제점

  • 위의 코드처럼 예외처리를 하면 기능상에는 전혀 문제가 없는 코드가 완성되었다. 하지만 코드를 자세히 살펴보면 중복도 많고, 읽기 어려운 코드가 되었다.
  • 또한 수정을 원할때도 어느 부분을 수정해야하는지, 수정했을 때 기능이 정상적으로 작동할지 보장할 수 없다.

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

  • 변하는 부분을 먼저 메소드 추출로 분리해보자
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;
}    
  • 메소드 분리를 통해 변하는 부분을 추출해보았지만 이를 다른곳에서 재활용할 수 없어보인다.
  • 분리하고 남은 부분들이 재사용이 필요하고, 분리한 메소드가 쿼리마다 새로 만들어져야 해서 뭔가 반대로 되었다.

템플릿 메소드 패턴의 적용

  • 템플릿 메소드 패턴이란?
    • 상속을 통해 기능을 확장해서 사용하는 부분
    • 즉, 변하지 않는 부분은 슈퍼클래스에 두고 변하는 부분은 추상 메소드로 정의해둬서 서브클래스에서 오버라이드하여 새롭게 정의해 쓰도록 하는 것
public class UserDaoDeleteAll extends UserDao {
	protected PreparedStatement makeStatement(Connection c) throws SQLException { 
    	PreparedStatement ps = c.prepareStatement("delete from users");
		return ps;
	}
}
  • 템플릿 메소드 패턴의 적용을 통해 어느정도 OCP를 지키면서 구조를 개선했지만 필요한 쿼리마다 계속 클래스를 만들어야 한다는 것은 부담스럽다.
  • 또한 확장구조가 이미 클래스를 설계하는 시점에서 고정되어 버린다.

전략 패턴의 적용

  • 전략 패턴이란?
    • OCP를 잘 지키면서 템플릿 메소드 패턴보다 유연하고 확장성이 뛰어난 것이, 오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 전략
    • 클라이언트가 전략을 생성해 전략을 실행할 컨텍스트에 주입하는 패턴
    • 전략 패턴을 구성하는 3가지 요소
      • 전략 메소드를 가진 전략 객체
      • 전략 객체를 사용하는 컨텍스트
      • 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트

컨텍스트와 DI

JdbcContext의 분리

  • 전략 패턴의 구조로 본다면
    • UserDao의 메소드는 클라이언트
    • 익명 내부 클래스로 만들어지는 것은 개별적인 전략
    • jdbcContextWithStatementStrategy() 메소드는 컨텍스트
  • 모든 DAO에서 사용할 수 있도록 jdbcContextWithStatementStrategy()를 독립시켜보자!

클래스 분리

  • JdbcContext 클래스로 분리해서 DI에 활용하자!

빈 의존관계 변경

  • UserDao는 이제 JdbcContext에 의존하고 있다. 하지만 이는 인터페이스가 아닌 구현체임.
  • 보통의 스프링 DI는 인터페이스를 두고 구현체를 여러개 만들어 바꿔 사용하는 것이 목적이었음

스프링 빈으로 DI

  • 이렇게 인터페이스를 사용하지 않고 DI를 적용하는 것은 문제가 있지 않을까?
  • 그렇게 해도 상관없지만 꼭 그럴 필요는 없다!
  • 의존관계 주입이라는 개념을 충실히 따르면, 인터페이스를 사이에 둬서 클래스레벨에서는 의존관계가 고정되지 않게 하고, 런타임 시에 의존할 오브젝트와의 관계를 동적으로 주입해주는 것이 맞다!
  • 하지만 위의 코드에서는 객체의 생성과 관계설정에 대한 제어권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC의 개념을 포괄하고 있으므로 DI의 기본을 따르고 있다고 볼 수 있다.
  • JdbcContextDI구조로 만들어야 할 이유
    • JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 되기 때문!
    • JdbcContextDI를 통해 다른 빈에 의존하고 있기 때문!
      • DI를 위해서는 양쪽 클래스 모두 빈으로 등록되어야 한다!
  • 현재는 인터페이스가 없어서 UserDaoJdbcContext가 매우 강하게 결합되어 있음.

코드를 이용하는 DI

  • UserDao 내부에서 직접 DI를 적용하는 방법!
  • 이 방법에서는 싱글톤으로 만드는 것을 포기해야 한다. 왜냐하면 DAO마다 하나의 JdbcContext를 갖고 있게 해야하기 때문이다.
  • JdbcContext에 대한 제어권을 스프링이 아닌 사용자가 갖고 생성과 관리를 담당하는 UserDao에게 DI까지 맡기는 것!
  • 위와 같이 한 오브젝트의 수정자 메소드에서 다른 오브젝트를 초기화하고 코드를 이용해 DI하는 것은 스프링에서도 종종 사용되는 기법

템플릿과 콜백

  • 지금까지 UserDaoStatementStrategy, JDbcContext를 이용해 만든 코드는 일종의 전략 패턴이 적용된 것!
  • 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고 그중 일부분만 자주 바꿔서 사용한다면 템플릿/콜백 패턴이라고 부른다.

    콜백은 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트를 말한다.

템플릿 / 콜백의 특징

  • 여러 개의 메소드를 가진 일반적인 인터페이스를 사용할 수 있는 전략 패턴의 전략과 달리 템플릿 / 콜백 패턴의 콜백은 보통 단일 메소드 인터페이스를 사용한다. 이는 템플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출되는 것이 일반적이기 때문이다.
  • 콜백 인터페이스의 메소드에는 보통 파라미터가 있다. 이는 템플릿의 작업 흐름 중에 만들어지는 컨텍스트 정보를 전달받을 때 사용!
  • 템플릿 / 콜백 패턴에서는 매번 메소드 단위로 사용할 오브젝트를 새롭게 전달받는다는 것이 특징이며 또한 콜백 오브젝트가 내부 클래스로서 자신을 생성한 클라이언트 메소드 내의 정보를 직접 참조한다는 것도 고유한 특징이다.

콜백의 분리와 재활용

  • 복잡한 익명 내부 클래스의 사용을 최소화할 수 있는 방법!
  • 위의 코드는 바뀌는 sql부분만 파라미터로 받아서 사용한다.

콜백과 템플릿의 결합

  • 위에서 만든 executeSql() 메소드를 JdbcContext 내부로 옮겨 여러곳에서 사용할 수 있도록 수정한다.
  • 성격이 다른 코드들은 가능한 한 분리하는 편이 낫지만, 이 경우에는 반대다!
  • 하나의 목적을 위해 서로 긴밀하게 연관되어 동작하는 응집력이 강한 코드들이기 때문이다!

템플릿 / 콜백의 응용

  • 스프링을 사용하는 개발자라면 당연히 스프링이 제공하는 템플릿 / 콜백 기능을 잘 사용할 수 있어야 한다.
  • 고정된 작업 흐름을 갖고 있으면서 여기저기서 자주 반복되는 코드가 있다면, 중복되는 코드를 분리할 방법을 생각해보는 습관을 기르자.
  • 가장 전형적인 템플릿 / 콜백 패턴의 후보는 try/catch/finally 블록을 사용하는 코드다.

템플릿/콜백은 스프링이 객체지향 설계와 프로그래밍에 얼마나 가치를 두고 있는지를 잘 보여주는 예다! 우리는 이를 잘 사용하는 것은 물론이고 필요하면 직접 만들어서 활용할 수도 있어야 한다!

좋은 웹페이지 즐겨찾기