3장-템플릿

다람·2024년 6월 16일

토비의 스프링 3.1

목록 보기
2/3
post-thumbnail

템플릿이란?

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

3.1 다시보는 초난감 DAO

JDBC 코드에서는 어떤 상황에서도 가져온 리소스를 반환하도록 try/catch/finally 구문 사용을 권장한다.

예외 발생 시에도 리소스를 반환하도록 수정한 deleteAll()

public void delteAll() 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문 때문이다. 사실은 try-finally만 있어도 된다.
		*/
	} finally {
		if (ps != null) {
			try {
				ps.close();
			} catch (SQLException e) {
			}
		}
		if (c != null) {
			try {
				c.close(); // try-with-resources를 사용하면 close가 따로 필요하지 않음
			} catch (SQLException e) {
			}
		}
	}
}

핵심! close할 때 예외처리를 해줘야 한다.

왜 예외를 다시 던지는가에 대해서 생각해 보았다.

finally문 때문이라고 했는 데 왜 일까?

여기서의 catch문은 try문에서 발생한 예외를 잡아주는 부분이다.

try문에서 예외가 발생하고 catch문에서 throw를 해주지 않는다면 finally(finally문은 try블록에서 예외가 발생하나 안하나 무조건 수행을 하는 블록) 안의 catch문에서 예외가 잡혀서 service단에서 어디서 예외가 발생했는지 알 수 없고, 잘 처리되었다고 생각하고 넘어갈 수 있기 때문이다.

finally 블럭 안의 try-catch문은 close에 대한 예외를 처리해 주기위한 블록이므로 각각의 예외를 잡아줘야된다고 생각한다.

try-finally만 있어도 된다는 부분은 어짜피 finally 블록에서 try-catch블럭이 존재하기 때문에 그렇게 말을 한 것이 아닐까 싶다.

close() 메소드

  • close()도 SQLException이 발생할 수 있는 메소드이므로 try-catch문으로 처리해 줘야 한다.
  • close() 메소드는 만들어진 순서의 반대로 하는 것이 원칙이다.

3.2 변하는 것과 변하지 않는 것

3.2.1 JDBC try/catch/finally 코드의 문제점

복잡한 try/catch/finally 블록이 2중으로 중첩되어 나오고, 모든 메소드마다 반복된다.

이런 복잡한 코드에서 당장 컴파일 에러가 나지 않더라도 메소드 호출이 되면서 커넥션이 하나씩 반환되지 않고 쌓이게 되면 리소스가 꽉 찼다는 에러가 나면서 서비스가 중단되는 상황이 발생할 수 있다.

이런 문제를 효과적으로 다룰 수 있는 방법은 많은 곳에서 중복되는 코드와 로직에 따라 자꾸 확장되고 자주 변하는 코드를 잘 분리해내는 작업이다.

단, DAO와 DB 연결 기능을 분리하는 것과는 성격이 다르므로 해결방법이 조금 다르다.

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

변하지 않는 부분

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

메소드 추출

변하는 부분을 메소드로 뺀다.

public void deleteAll()throws SQLException {
	...
	try {
		c = dataSource.getConnection();
		
		ps = makeStatement(c); // 변하는 부분을 메소드로 추출하고 변하지 않는 부분에서 호출하도록 바꿈(전략패턴)
		
		ps.executeUpdate();
	} catch (SQLException e)
	...
}

// 별도의 메소드로 빠진 변하는 부분
private PreparedStatement makeStatement(Connection c) thorws SQLException {
	PreparedStatement ps;
	ps = c.prepareStatement("delete from users");
	return ps;
}

템플릿 메소드 패턴의 적용

템플릿 메소드 패턴 : 상속을 통해 기능을 확장

상속을 통해 자유롭게 클래스의 기능을 확장할 수 있지만 제한이 많다. 만약 템플릿 메소드 패턴을 사용한다면 UserDao의 JDBC 메소드가 4개일 경우 4개의 서브클래스를 만들어서 사용해야 한다.

템플릿 메소드 패턴의 적용

템플릿 메소드 패턴의 적용

또한 확장구조가 이미 클래스를 설계하는 시점에서 고정되어 버린다.

변하지 않는 코드를 가진 서브클래스들이 이미 클래스 레벨에서 컴파일 시점에 관계가 결정되버린다.

따라서 관계에 대한 유연성이 떨어진다.

전략 패턴의 적용

전략 패턴 : 주입을 통해 기능을 위임

전략 패턴은 OCP(개방 폐쇄 원칙)을 잘 지키면서 오브젝트를 둘로 분리하고 클래스 레벨에서 인터페이스를 통해서만 의존하도록 만드는 점이 템플릿 메소드 패턴보다 유연하고 확장성이 뛰어나다고 볼 수 있다.

전략 패턴의 구조

전략 패턴의 구조

DI 적용을 위한 클라이언트/컨텍스트 분리

// Statementstrategy stmt : 클라이언트가 컨텍스트를 호출할 때 넘겨줄 전략 파라미터
publc void jbcContextWihStatementStrategy(Statementstrategy stmt)
		throws SQLExcepti n {
	Connecti n c = nul;
	PreparedStatement ps = nul;

	try {
		c = dataSource.getConnection();
		
		ps = stmt.makePreparedStatement(c)// 매개변수로 주입 받았음
		
		ps.executeUpdate()} catch (SQLExcepti n e) {
		throw e;
	} finaly {
		i (ps != nul) { try { ps.close()} catch (SQLException e) {} }
		i (c != nul) { try {c.close()} catch (SQLException e) {} }
	}
}

위의 메소드는 클라이언트로부터 StatementStrategy 타입의 전략 오브젝트를 제공받고 JDBC try/catch/finally 구조로 만들어진 컨텍스트 내에서 작업을 수행한다.

제공받은 전략 오브젝트는 PreparedStatement 생성이 필요한 시점에 호출해서 사용한다.

public void deleteAll() throws SQLException {
	StatementStrategy st = new DeleteAllStatement(); // 선정한 전략 클래스의 오브젝트 생성
	jdbcContextWithStatementStrategy(st); // 컨텍스트 호출. 전략 오브젝트 전달
}

전략은 사용하는 쪽(밖에서 결정)에서 결정해서 주도록 한다.

3.3 JDBC 전략 패턴의 최적화

3.3.1 전략 클래스의 추가 정보

추가정보를 받기위해서 클라이언트가 부가정보를 제공해줘야한다.

클라이언트로부터 오브젝트를 받을 수 있도록 생성자를 통해 제공받게 한다.

package springbook.user.dao;
...
public class AddStatement implements StatementStrategy {
	User user;
	
	public AddStatement(User user) {
		this.user = user;
	}
 
 public PreparedStatement makePreparedStatement(Connection c) {
	 ...
	 ps.setString(1, user.getId());
	 ps.setString(2, user.getName());
	 ps.setString(3, user.getPassword());
	 ..
 }
}
public void add(User user) throws SQLException {
	StatementStrategy st = new AddStatement(**user**); // user를 받아와서 넘겨줌
	jdbcContextWithStatementStragey(st);
}

3.3.2 전략과 클라이언트의 동거

DAO 메소드마다 새로운 StatementStrategy 구현 클래스를 만들게 되면 클래스 파일의 개수가 많이 늘어나게 된다. 또 부가 정보가 있는 경우, 생성자와 저장할 인스턴스 변수를 번거롭게 만들어야 한다는 단점이 있다.

이러한 문제를 해결할 수 있는 방법으로 로컬 클래스, 익명 내부 클래스가 있다.

로컬 클래스

전략 클래스를 매번 독립된 파일로 만들지 않고 Dao 클래스 안에 내부 클래스로 정의하는 방법이다.

내부클래스를 이용해서 처리한다는 말은 Builder 패턴을 뜻한다.

밖에서 사용되지 않고 UserDao에서만 사용되는 클래스와 같이 특정 메소드에서만 사용되는 것이라면 로컬 클래스로 만들 수 있다. → 밖에서 사용하지 않고 서로 관련이 깊기 때문에 전략을 안으로 넣는 것이 가능 함

익명 내부 클래스

익명 내부 클래스는 이름을 갖지 않는 클래스로

클래스의 선언과 동시에 객체를 생성하므로, 단 하나의 객체만을 생성하는 일회용 클래스다.

익명 내부 클래스를 이용해서 처리한다는 말은 전략을 바로 만들어서 넣어두고 일회용으로 사용하겠다는 뜻이다.

3.4 컨텍스트와 DI

컨텍스트란? 특정작업 한 단위를 유지 또는 수행하기 위해 필요한 모든일 들이다. 즉, 공유하는 환경을 말한다.

컨텍스트를 분리해야되며, 분리 한 뒤 연결하는 작업을 binding이라고 한다.

binding은 필요한대로 조합하여 재사용성을 높이기 위해 사용한다.

binding을 하는 방법은 2가지로 상속과 주입이 있다.

3.4.1 JdbcContext의 분리

클래스 분리

package springbook.user.dao;

...
 
public class JdbcContext {
	
	// DataSource 타입 빈을 DI 받을 수 있게 준비한다
	private DataSource dataSource;
 
	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}
	
	
	// JdbcContext 클래스 안으로 옮겼으므로 이름도 그에 맞게 수정
	public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
		
		Connection c = null;
		PreparedStatement ps = null;
		
		try {
			c = dataSource.getConnection();
			
			ps = stmt.makePreparedStatement(c);
			
			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){}}
		}		
	}
}
public class UserDao {
	...
  
  // JdbcContext를 DI 받도록 만듦(전략 패턴, 브릿지 패턴)
  private JdbcContext jdbcContext; // iv
 
  public void setJdbcContext(JdbcContext jdbcContext) {
      this.jdbcContext = jdbcContext;
  }
	
  public void add(final User user) throws SQLException {
	  // DI 받은 JdbcContext의 컨텍스트 메소드를 사용하도록 변경
		this.jdbcContext.workWithStatementStrategy(
			new StatementStrategy() { ... }
		);  
  }
    
  public void deleteAll() throws SQLException {
    this.jdbcContext.workWithStatementStrategy(
	    new StatementStrategy() { ... }
    );
  }
}

빈 의존관계 변경

UserDao와 JdbcContext는 인터페이스를 사이에 두지 않고 DI를 적용하는 특별한 구조가 됨

JdbcContext를 적용한 UserDao의 의존관계
JdbcContext를 적용한 UserDao의 의존관계

스프링의 빈 설정은 클래스 레벨이 아니라 런타임 시에 만들어지는 오브젝트 레벨의 의존관계에 따라 정의 된다.

userDao 빈이 dataSource 빈을 직접 의존했지만 이제 jdbcContext 빈이 사이에 끼게 된다.

JdbcContext가 적용된 빈 오브젝트 관계
JdbcContext가 적용된 빈 오브젝트 관계

3.4.2 JdbcContext의 특별한 DI

UserDao는 인터페이스를 거치지 않고 코드에서 바로 JdbcContext 클래스를 사용하고 있다.

UserDao와 JdbcContext는 클래스 레벨에서 의존관계가 결정된다. 비록 런타임 시에 DI 방식으로 외부에서 오브젝트를 주입해주는 방식을 사용하긴 했지만, 의존 오브젝트의 구현 클래스를 변경할 수 는 없다.

스프링 빈으로 DI

인터페이스를 사용해서 클래스를 자유롭게 변경할 수 있게 하진 않았지만, JdbcContext를 UserDao와 DI 구조로 만들어야 할 이유는 무엇일까?

  1. JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 되기 때문이다. JdbcContext는 그 자체로 변경되는 상태정보를 갖고 있지 않다. dataSource는 읽기 전용이므로 JdbcContext가 싱글톤이 되는데 아무 문제가 없다.
  2. JdbcContext가 DI를 통해 다른 빈에 의존하고 있다.
    DI를 위해서 주입되는 오브젝트와 주입받는 오브젝트 양쪽 모두 스프링 빈으로 등록돼야 한다.
    스프링이 생성하고 관리하는 IoC 대상이어야 DI에 참여할 수 있다.

그런데 왜 인터페이스를 사용하지 않았을까? UserDao와 JdbcContext가 매우 긴밀한 관계를 가지고 강하게 결합되어 있기 때문이다.(강한 응집도)

코드를 이용하는 수동 DI

자동 : @Autowired

수동 : 생성자, setter, getBean()

JdbcContext를 스프링의 빈으로 등록해서 UserDao에 DI하는 대신 사용할 수 있는 방법으로 UserDao 내부에서 직접 DI를 적용하는 방법이다.

하지만 이 방법을 사용하려면 문제점 두 가지가 존재한다.

  1. 싱글톤으로 만드는 것을 포기해야한다.
    하지만 싱글톤을 포기하여 수만, 수백만의 JdbcContext 오브젝트가 만들어지더라고 DAO마다 하나 씩 만든다면 DAO의 개수만큼, 수백 개 정도면 충분히 가능하다. 또한 JdbcContext에는 내부에 두는 상태정보가 없으므로 메모리에 주는 부담이 거의 없다. 또한 자주 만들어졌다 제거되는 것이 아니므로 GC에 대한 부담도 없다.
  2. JdbcContext를 스프링 빈으로 등록해서 사용하여 다른 빈을 인터페이스를 통해 간접적으로 의존하고 있다.
    이 경우 JdbcContext에 대한 제어권을 갖고 생성과 관리를 담당하는 DAO에 DI까지 맡기는 방법이 있다.

코드를 통한 JdbcContext DI 구조

코드를 통한 JdbcContext DI 구조

기존 설정파일에 등록했던 JdbcContext 빈을 제거하고, Dao의 jdbcContext 프로퍼티도 제거해준다. 그리고 DAO는 DataSource 타입 프로퍼티만 갖도록 한다.

<beans> 
	<bean id="userDao" class="springbook.user.dao.UserDao">
	    <property name="dataSource" ref="dataSource"/> // 주입하는 것
	</bean>
	
	<bean id="dataSource"
		class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
		...
	</bean>
</beans>

UserDao는 이제 JdbcContext를 외부에서 주입받을 필요가 없으니 setJdbcContext()를 제거하고 setDataSource() 메소드를 수정해준다.

public class UserDao {
	...
  
  private JdbcContext jdbcContext;
 
	// 수정자 메소드이면서 jdbcContext에 대한 생성, DI 작업을 동시에 수행 
  public void setDataSource(DataSource dataSource) {
      this.jdbcContext = new JdbcContext(); // JdbcContext 생성(IoC) : dataSource에 대한 흐름을 넘겨 줬기 때문
      
      this.jdbccontext.setDataSource(dataSource); // 의존 오브젝트 주입(DI)
  
  // 위의 코드를 없애려고 jdbcContext 빈을 제거한 설정파일 xml을 작성하는 것
      
      this.dataSource = dataSource; // 아직 JdbcContext를 적용하지 않은 메소드를 위해 저장해 둠
  }
}

setDataSource() 메소드는 DI 컨테이너가 DataSource 오브젝트를 주입해줄 때 호출된다. 이때 JdbcContext에 대한 수동 DI 작업을 진행하면 된다.

DAO와 밀접한 관계를 갖는 클래스를 DI에 적용하는 방법 두 가지 모두 장단점이 존재한다.

  1. 인터페이스를 사용하지 않는 클래스와의 의존관계지만 스프링의 DI를 이용하기 위해 빈으로 등록해서 사용하는 방법
    • 장점 : 오브젝트 사이의 실제 의존관계가 설정파일에 명확하게 드러난다.
    • 단점 : DI의 근본적인 원칙에 부합하지 않는 구체적인 클래스와의 관계가 설정에 직접 노출된다.
  2. DAO의 코드를 이용해 수동으로 DI를 한다.
    • 장점 : JdbcContext가 UserDao의 내부에서 만들어지고 사용되면서 그 관계를 외부에는 드러내지 않는다. 필요에 따라 내부에서 은밀하게 DI를 수행하고 전략을 외부에 감출 수 있다.
    • 단점 : JdbcContext를 여러 오브젝트가 사용하더라도 싱글톤으로 만들 수 없고, DI 작업을 위한 부가적인 코드가 필요하다.

3.5 템플릿과 콜백

템플릿/콜백 패턴 : 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식

  • 템플릿 : 전략 패턴의 컨텍스트
  • 콜백 : 익명 내부 클래스로 만들어지는 오브젝트

3.5.1 템플릿/콜백의 동작원리

  • 템플릿 : 고정된 작업 흐름을 가진 코드를 재사용
  • 콜백 : 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트

템플릿/콜백의 특징

템플릿/콜백 패턴의 콜백은 단일 메소드 인터페이스를 사용한다.

콜백 인터페이스의 메소드에 있는 파라미터는 템플릿의 작업 흐름 중 만들어지는 컨텍스트 정보를 전달받을 때 사용된다.

콜백? A가 호출하는 데 호출 받은 B는 A를 호출 할 수 없다.

A가 B를 호출했는데 B는 A의 전화번호가 없어서 호출을 못하기 때문에 값을 주거나 객체를 줘서 정보를 알려줘야 된다.(B가 A를 호출할 방법을 주는 것 ⇒ 나중에 호출할 함수를 주는 것)

A는 B의 연락을 무작정 기다리지 않고 다른 일을 하면서 기다린다.

A가 호출하는 쪽에 호출항 방법을 줘야되고, 보통 함수를 준다. B가 할 일을 다 끝내면 함수를 호출한다.

위의 예시 설명처럼 바뀌는 부분만 주면 JDBC 템플릿이 돌아가게 된다.

템플릿/콜백의 일반적인 작업 흐름
템플릿/콜백의 일반적인 작업 흐름

  • 템필릿/콜백의 작업 흐름
    • 클라이언트의 역할은 템플릿 안에서 실행될 로직을 담은 콜백 오브젝트를 만들고, 콜백이 참조할 정보를 제공하는 것이다. 만들어진 콜백은 클라이언트가 템플릿의 메소드를 호출할 때 파라미터로 전달된다.
    • 템플릿은 정해진 작업 흐름을 따라 작업을 진행하다가 내부에서 생성한 참조정보를 가지고 콜백 오브젝트의 메소드를 호출한다. 콜백은 클라이언트 메소드에 있는 정보와 템플릿이 제공한 참조정보를 이용해서 작업을 수행하고 그 결과를 다시 템플릿에 돌려준다.
    • 템플릿은 콜백이 돌려준 정보를 사용해서 작업을 마저 수행한다. 경우에 따라 최종 결과를 클라이언트에 다시 돌려주기도 한다.

콜백을 클라이언트가 전달하는 것은 State 패턴,

바뀌는 부분을 밖에서 주는 것은 Strategy 패턴이다.

3.5.2 편리한 콜백의 재활용

콜백과 템플릿의 결합

SQL만 넘겨주면 된다.

public class JdbcContext {
	...
	public void executeSql(final String query) throws SQLException {
		workWithStatementStrategy(
			new StatementStrategy() {
				public PreparedStatement makePreparedStatement(Connection c)
					throws SQLException {
						return c.prepareStatement(query);
				}
		}
	);
}
public void deleteAll() throws SQLException {
	this.jdbcContext.executeSql("delete from users");
}

위 처럼 코드를 수정하게 되면 모든 DAO 메소드에서 executeSql() 메소드를 사용할 수 있게 된다.

콜백 재활용을 적용한 JdbcContext

콜백 재활용을 적용한 JdbcContext

구체적인 구현과 내부의 전략 패턴, 코드에 의한 DI, 익명 내부 클래스 등의 기술은 최대한 감춰두고, 외부에는 꼭필요한 기능을 제공하는 단순한 메소드만 노출해주는 것이다.

3.6 스프링의 JdbcTemplate : 사용법

JdbcTemplate : 스프링이 제공하는 JDBC 코드용 기본 템플릿이다. JDBC를 이용하는 DAO에서 사용할 수 있도록 준비된 다양한 템플릿과 콜백을 제공한다.

public class UserDao {
	...
	private JdbcTemplate jdbcTemplate;
	
	public void setDataSource(DataSource dataSource) {
		this.jdbcTemplate = new JdbcTemplate(dataSource);
		
		this.dataSource = dataSource;
	}

Template을 주입받고 → dataSource를 넘겨줘서 → CRUD를 하면 된다.

profile
개발하는 다람쥐

0개의 댓글