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

susu·2022년 11월 2일
0
post-thumbnail

Connection Pool

💬 DB와 미리 연결해놓은 객체들을 받아놓는 웅덩이라는 의미에서 풀이라 한다.
DB와의 불필요한 I/O가 감소하므로 성능 향상을 기대할 수 있다.

Connection이나 PreparedStatement는 방식으로 운영된다.
미리 정해진 풀 안에 제한된 수의 리소스(Connection, Statement)를 만들어두고,
필요할 때 이를 할당한 뒤 반환하면 다시 풀에 넣는 방식으로 운영된다.

요청이 매우 많은 서버환경에서는 매번 새로운 리소스를 생성하는 대신 미리 만들어둔 리소스를 돌려가며 사용하는 편이 훨씬 유리하지만, 사용한 리소스는 빠르게 반환해야 한다.
(풀에 있는 리소스가 전부 고갈되면 문제가 발생하기 때문)

close() 메소드는 사용한 리소스를 풀로 다시 돌려주는 역할을 한다.

실제로 서비스 배포 후 DB 커넥션 풀이 꽉 차서 서버가 중단되는 사례도 있었다고 하니,
대규모 서비스를 구현한다면 놓쳐서는 안될 부분이다.

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

메소드 추출

코드의 한 부분을 메소드로 독립시키는 방법이 있다.
전체적인 구조를 고려해 자주 변하거나 변하지 않는 부분을 메소드로 감싸는 방식이다.

템플릿 메소드 패턴 적용

템플릿 메소드 패턴이란 상속을 통해 기능을 확장해서 사용하는 것.
변하지 않는 부분은 슈퍼클래스에 두고,
변하는 부분을 추상 메소드로 정의해서 서브클래스에서 오버라이드하여 새롭게 정의해 쓰도록 하는 것이다.

템플릿 메소드 패턴은 하나의 클래스가 전체 알고리즘을 보호하는 Framework의 역할을 하고,
재사용을 통해 코드의 중복성을 없애고 새로운 객체를 적용하기에 용이하다는 장점이 있다.

하지만 이렇게 될 경우

  • DAO 로직마다 상속을 통해 새로운 클래스를 만들어야 하고,
  • 확장구조가 이미 클래스를 설계하는 시점에서 고정되어 버린다

는 문제점이 있다.

전략 패턴 적용

전략 패턴은 객체를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 패턴이다.
확장 (변하는) 부분을 별도의 클래스로 만들어서, 추상화된 인터페이스를 통해 위임하는 방식.
개방 폐쇄 원칙(OCP)를 잘 지키는 구조이면서도 템플릿 메소드 패턴보다 유연하고 확장성이 뛰어나다.

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

전략 패턴에 따르면 Context가 어떤 전략을 사용하게 할 것인가는 Context를 사용하는 앞단의 Client가 결정하도록 하는 게 일반적이다.
→ Client가 구체적인 전략(Strategy)의 하나를 선택해 객체로 만들어서 Context에 전달하도록.

Context는 전달받은 그 전략 구현 클래스의 객체를 사용하게 된다.
따라서 이 구조에서 전략 객체 생성과 컨텍스트로의 전달을 담당하는 책임을 또 하나의 객체로 분리시키는 과정을 일반화해보면 그게 바로 DI가 된다.
즉, DI란 이러한 전략 패턴의 장점을 활용할 수 있도록 만든 것이다.

마이크로 DI

일반적으로 DI는

  • 의존관계에 있는 두 개의 객체와,
  • 이 관계를 다이나믹하게 설정해주는 객체 팩토리(DI 컨테이너),
  • 그리고 이를 사용하는 클라이언트

라는 4개의 객체 사이에서 일어나지만,
상황에 따라 이들의 구분이 명확하지 않을 수 있다.
이런 경우 매우 작은 단위의 코드와 메소드 사이에서도 DI가 일어날 수 있다.

이렇게 DI의 장점을 단순화해서 IoC 컨테이너의 도움 없이 코드 내에서 적용한 경우를 마이크로 DI,
또는 코드에 의한 DI라는 의미에서 수동 DI라고 부를 수도 있다.

📌 클래스를 이용한 JDBC 전략 패턴의 최적화

👽 중첩 클래스 ?

다른 클래스 내부에 정의하는 클래스를 중첩 (nested) 클래스라고 한다.
중첩 클래스는 독립적인 객체로 만들어질 수 있는 static 클래스와,
자신이 정의된 클래스의 객체 안에서만 만들어질 수 있는 inner 클래스로 구분된다.
그리고 이 inner 클래스는 다시 범위(scope)에 따라 멤버 필드처럼 객체 레벨에서 정의되는 멤버 내부 클래스와,
메소드 레벨에서 정의되는 로컬 클래스와,
그리고 이름을 갖지 않는 익명 내부 클래스로 구분된다.
cf. 익명 내부 클래스의 범위는 선언된 위치에 따라 다르다.

💬 중첩 클래스의 구분

  • 스태틱 클래스 (static class)
  • 내부 클래스 (inner class)
    • 멤버 내부 클래스 (member inner class)
    • 로컬 클래스 (local class)
    • 익명 내부 클래스 (anonymous inner class)

1. 로컬 클래스 이용하기

메소드 내에서 선언된 클래스.
이 메소드 안에서만 이 클래스를 사용하는 경우에 선택한다.
로컬 클래스는 선언된 메소드 내에서만 이용 가능하므로 그냥 로컬 변수를 선언하듯이 사용하면 된다.

로컬 클래스의 장점은, 내부 클래스이므로 자신이 선언된 메소드에 직접 접근할 수 있다는 것이다.
외부 클래스의 경우 생성자를 만들고 전달해주는 과정을 거쳐야 했지만 로컬 클래스를 사용하면 그럴 필요가 없다.
또한 클래스 파일을 하나 줄일 수 있다는 것도 큰 장점이다.

2. 익명 내부 클래스 이용하기

한 메소드 안에서 사용될 용도로 선언된 클래스라면 익명 내부 클래스를 사용하는 방법도 있다.
익명 내부 클래스는 선언과 동시에 객체를 생성한다.
이름이 없기 때문에 클래스 자신의 타입을 가질 수 없고,
자신이 구현한 인터페이스 타입의 변수에만 저장될 수 있다.
어차피 딱 한번만 사용할테니 굳이 변수에 담아두지 말고 바로 사용하라는 의미로 사용한다.

// 익명 내부 클래스 사용 패턴
public class Outer {
		
	...
	
	new 참조인터페이스형() {
		
		...
	}
}

cf. 자바 제네릭스(Generics)

🔗 자바 [JAVA] - 제네릭(Generic)의 이해
자료형을 클래스 내부에서 지정하는 것이 아닌, 외부에서 사용자에 의해 지정되는 것을 의미한다.

📌 템플릿/콜백 패턴

단일 전략 메소드를 갖는 전략 패턴이면서,
익명 내부 클래스를 사용해서 매번 전략을 새로 만들어 사용하고,
컨텍스트 호출과 동시에 전략 DI를 수행하는 방식을 템플릿/콜백 패턴이라 한다.

템플릿

고정된 모양이 있는 틀.
프로그래밍에서는 정해진 틀 안에서 내용을 수정할 수 있는 것을 템플릿이라 한다.
앞에서 봤던 템플릿 메소드 패턴에서는 고정된 틀의 로직을 가진 템플릿 메소드를 슈퍼클래스에 두고,
바뀌는 부분을 서브클래스에 두는 구조로 이뤄진다.

콜백

콜백은 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 객체를 의미한다.
파라미터로 전달되지만, 그 값을 꼭 참조하기 위해서가 목적이 아니다.
특정 로직을 가진 메소드를 실행시키는 것이 목적이다.
자바에서는 메소드 자체를 파라미터로 전달할 수 없으므로 메소드를 담은 객체를 전달해야 한다.
그래서 Functional Object 라고도 한다.

전략 패턴의 컨텍스트를 템플릿이라 부르고, 익명 내부 클래스로 만들어지는 객체를 콜백이라 부른다.

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

이해를 위해 DI 방식의 전략 패턴 구조라고 생각하고 보면 간단해진다.
클라이언트가 템플릿 메소드를 호출하면서 콜백 객체를 전달하는 것은 메소드 레벨에서 일어나는 DI라 할 수 있다.
템플릿이 사용할, 콜백 인터페이스를 구현한 객체를 메소드를 통해 주입해주는 DI 작업이,
클라이언트가 템플릿의 기능을 호출하는 것과 동시에 일어난다.

템플릿/콜백 방식의 특징은 다음과 같다.

  • 매번 메소드 단위로 사용할 객체를 새롭게 전달받는다.
  • 콜백 객체가 내부 클래스로서 자신을 생성한 클라이언트 메소드 내의 정보를 직접 참조한다.
  • 따라서 클라이언트와 콜백이 강력하게 결합된다.

따라서 템플릿/콜백 방식은 전략 패턴과 DI의 장점을 익명 내부 클래스 사용 전략과 결합한 독특한 활용 방법으로 볼 수 있다.
단순 전략 패턴이라기보단 템플릿/콜백 자체를 하나의 고유한 디자인 패턴으로 기억해두자.

📌 스프링의 JdbcTemplate

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

  • 거의 모든 종류의 JDBC 코드에 사용 가능한 템플릿과 콜백을 제공할 뿐만 아니라,
  • 자주 사용되는 패턴을 가진 콜백은 다시 템플릿에 결합시켜 간단한 메소드 호출만으로 사용이 가능

하므로 매우 편리하게 사용할 수 있다.

스프링이 제공하는 JDBC 코드용 기본 템플릿은 JdbcTemplate이다.
JdbcTemplate은 생성자의 파라미터로 DataSource를 주입하면 된다.

public class UserDao {
	...
	private JdbcTemplate jdbcTemplate;

	public void setDataSource(DataSource dataSource) {
		this.jdbcTemplate = new JdbcTemplate(dataSource);

		this.dataSource = dataSource;
	}
}

update()

StatementStrategy 인터페이스의 makePreparedStatement() 메소드는
JdbcTemplate 콜백 중 PreparedStatementCreator 인터페이스의 createPreparedStatement() 메소드이다.
템플릿으로부터 Connection을 제공받아 PreparedStatement를 리턴한다는 점에서 구조는 똑같다.
PrepareStatementCreator 타입의 콜백을 받아서 사용하는 JdbcTemplate의 템플릿 메소드는 update()다.

...
	public void deleteAll() {
		this.jdbcTemplate.update("delete from users");; // 파라미터로 쿼리문을 준다
	}

update() 메소드를 사용하면 SQL 쿼리만으로 PreparedStatement를 만들 수 있다.
쿼리문 다음에 이어지는 파라미터는 입력되는 순서로 바인딩된다.

		...
		this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)", user.getId(), user.getName(), user.getPassword());

query()

🗣 익명 내부 클래스로 콜백을 작성하는 과정 잘 읽어보기!

템플릿/콜백 방식을 적용하지 않았던 메소드에 JdbcTemplate를 적용하는 과정을 살펴보자.

queryForInt()

SQL 쿼리를 실행하고 ResultSet을 통해 결과 값을 가져오는 getCount() 코드를 jdbcTemplate의 query() 메소드를 이용해 작성해본다.

  • query() 메소드는 PreparedStatementCreator 콜백과 ResultSetExtractor 콜백, 두 개의 콜백을 파라미터로 받는다.
  • 첫번째 콜백(PreparedStatementCreator)은 템플릿으로부터 Connection을 받아 PreparedStatement를 돌려준다.
  • 두번째 콜백(ResultSetExtractor)은 템플릿으로부터 ResultSet을 받고 거기서 추출한 결과를 돌려준다.
  • 두 콜백은 익명 내부 클래스로 작성되었다.
// queryForInt() 적용 전
public int getCount() {
	return this.jdbcTemplate.query(new PreparedStatementCreator() {
		public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
			return con.preparedStatement("select count(*) from users");
		}
	}, new ResultSetExtractor<Integer> {
		public Integer extractData(ResultSet rs) throws SQLException, DataAccessException {
			rs.next();
			return rs.getInt(1);
		}
	});
}

위와 같이 이중 콜백을 사용하는 코드의 경우 JdbcTemplate이 제공하는 queryForInt() 메소드를 이용해 개선할 수 있다.

public int getCount() {
	return this.jdbcTemplate.queryForInt("select count(*) from users");
}

queryForObject()

앞에서처럼 쿼리를 날리고 ResultSet을 받아올텐데,
이번에는 ResultSetExtractor 대신 RowMapper 콜백을 이용해 하나의 객체(User)에 매핑하여 리턴하도록 한다.
RowMapper는 ResultSet의 첫 번째 Row에 담긴 정보를 하나의 User 객체에 매핑해준다.

public User get(String id) {
	return this.jdbcTemplate.queryForObject("select * from users where id = ?", 
			new Object[] {id}, 
			new RowMapper<User>() {
				public User mapRow(Resultset rs, int rowNum) throws SQLException {
					User user = new User;
					user.setId(rs.getString("id"));
					
					...

					return user;
				}
	}
}

이때, 조회 결과가 없는 것에 대한 예외 상황은 어떻게 처리할까?

→ 이를 위해 특별히 해줄 것은 없다.
이미 queryForObject()에서 SQL을 실행해서 받은 로우의 개수가 하나가 아니라면 예외를 던지도록 만들어져 있다,
이때 던져지는 예외는 EmptyResultDataAccessException이다.
이를 처리하면 된다.

마무리

  • 스프링의 JDBC 기술에 대한 내용은 11장에서 이어진다.
  • 스프링은 JdbcTemplate 외에도 십여 가지의 템플릿/콜백 패턴을 적용한 API가 존재한다.
    클래스 이름이 Template으로 끝나거나, 인터페이스 이름이 Callback으로 끝난다면 템플릿/콜백이 적용된 것이라 보면 된다.

📌 정리

  • JDBC같이 예외가 발생할 가능성이 있고 + 공유 리소스의 반환이 필요한 경우 반드시 try-catch-finally 블록으로 감싸야 한다.
  • 일정한 작업 흐름이 반복되면서, 그중 일부 기능만 바뀌는 코드가 존재한다면 전략 패턴을 적용하자.
    바뀌지 않는 부분은 컨텍스트로, 바뀌는 부분은 전략으로 만들고 인터페이스를 통해 유연하게 전략을 변경할 수 있도록 구성한다.
  • 컨텍스트가 하나 이상의 클라이언트 객체에서 사용된다면 클래스를 분리하여 공유하도록 한다.
  • 콜백의 코드에도 일정한 패턴이 반복된다면 콜백을 템플릿에 넣고 재활용하는 것이 편리하다.
  • 템플릿과 콜백의 타입이 다양하게 바뀔 수 있다면 제네릭스를 이용한다.
profile
블로그 이사했습니다 ✈ https://jennairlines.tistory.com

0개의 댓글