3장-템플릿
3장 템플릿
바뀌는 성질이 다른 코드 중에서 변경이 거의 일어나지 않으며
일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로부터 독립
시켜서 효과적으로 활용할 수 있도록 하는 방법
3.1 다시 보는 초난감 DAO
- 초난감 DAO는 많은 개선을 했지만, 예외상황에 대한 처리를 하지 않았다.
3.1.1 예외처리 기능을 갖춘 DAO
- DB커넥션은 DB의 소중한 자원이므로, 어떤 이유로든 예외가 발생하더라도 리소스를 반드시 반환해야한다. 그렇지 않으면 시스템에 큰 문제를 일으킬 수 있다.
JDBC 수정 기능의 예외처리 코드
JDBC API를 이용한 DAO 코드인 deleteAll()
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()
.close()
라서 열린 것을 닫는 것으로 생각하기 쉽다.
하지만 엄밀히 말하면 리소스를 반환한다고 이해하는 것이 좋다.
예외 발생 시에도 리소스를 반환하도록 수정한 deleteAll()
- 예외 상황에서도 리소스를 제대로 반환할 수 있도록
try/catch/finally
를 적용해보자.
public void deleteAll() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = c.prepareStatement("delete from users");
ps.executeUpdate();
// 예외가 발생할 가능성이 있는 코드를 모두 try 블록으로 묶어준다.
} catch (SQLException e) {
throw e;
} finally {
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
// ps.close() 메서드에서도 SQLException이 발생할 수 있기 때문에 이를 잡아줘야 한다.
// 그렇지 않으면 Connection을 close() 하지 못하고 메소드를 빠져나갈 수 있다.
}
}
if (c != null) {
try {
c.close() //Connection 반환
} catch (SQLException e) {
}
}
}
}
예외가 발생할 수 있는 모든 시점을 고려해주어서 코드를 작성하였다.
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 조회 기능의 예외처리
- 조회를 위한 JDBC 코드는 더 복잡해진다.
ResultSet
이 더 추가되기 때문이다.getCount()
의 예외처리를 해보자.
JDBC 예외처리를 적용한 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 {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
// 만들어진 ResultSet을 닫아주는 기능. close()는 만들어진 순서의 반대로 하는 것이 원칙이다.
}
}
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
}
}
if (c != null) {
try {
c.close();
} catch (SQLException e) {
}
}
}
}
3.2 변하는 것과 변하지 않는 것
3.2.1 JDBC try/catch/finally 코드의 문제점
-
복잡한
try/catch/finally
블록이 2중으로 중첩돼서 나오며, 모든 메소드마다 반복된다. 보통 이렇게 반복되는 코드에 대한 나쁜 습관은 따로 분리하지 않고 복사 붙여넣기로 처리하는 것이다. -
그러다가 어느 순간 한 줄을 빼먹고 복사하거나 몇 줄을 잘못 삭제하면 어떻게 될까? 괄호를 잘못 닫은게 아니라면 당장에 컴파일 에러가 나지는 않는다. 하지만 이렇게 별 문제 없어보이면서 리소스를 반환하지 않으면 더욱 심각한 문제가 된다.
-
정상 동작하는 것처럼 보이는 서버가 며칠 주기로 리소스가 꽉찼다는 에러를 내면서 중지되는 상황이 올 수 있다.
-
누군가 DAO 로직을 수정하려고 했을 때, 복잡한
try/catch/finally
블록 안에서 필요한 부분을 찾아서 수정해야하고, 언젠가 필요한 부분을 잘못 삭제했는데 코드가 정상적으로 동작하면 역시 같은 문제가 반복될 것이다. -
언제 터질지 모르는 폭탄과 같은 코드가 될 수 있다. 우리는 이전에 1장에서 이러한 코드를 효과적으로 분리하는 방법을 배웠었다.
3.2.2 분리와 재사용을 위한 디자인 패턴 적용
UserDao
의 .deleteAll()
메소드를 먼저 개선해보자.
개선할 deleteAll() 메소드
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) {} }
}
deleteAll()
에서 변경될 수 있는 부분은 단 한줄이다.
ps = c.prepareStatement("delete from users"); // 변할 수 있는 부분
저 부분만 유연하게 변경될 수 있으면 ResultSet
이 필요없는 어떠한 쿼리도 같은 템플릿으로 처리할 수 있게 될 것이다.
그러면 우리가 정의한 변할 수 있는 부분
과 변하지 않는 부분
을 나누어보자.
add() 메소드에서 수정할 부분
...
ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
메소드 추출
변하는 부분을 메소드로 추출한 후의 deleteAll()
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;
}
}
- 보통 메서드 추출 리팩토링을 적용하는 경우에는 분리시킨 메소드를 다른 곳에서 재사용할 수 있어야 하는데, 이건 반대로 분리시키고 남은 메소드가 재사용이 필요한 부분이고, 분리된 메소드는 DAO 로직마다 새롭게 만들어서 확장돼야 하는 부분이기 때문이다.
템플릿 메소드 패턴의 적용
- 이번엔 템플릿 메소드 패턴을 이용해서 분리해본다.
- 템플릿 메소드 패턴은 상속을 통해 기능을 확장해서 사용하는 부분이다.
makeStatement()를 구현한 UserDao 서브클래스
public class UserDaoDeleteAll extends UserDao {
protected PreparedStatement makeStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("delete from users");
return ps;
}
}
-
템플릿 메소드 패턴
은 위와 같이추상 클래스를 만들고 변화가 필요한 부분을 추상 메소드로 빼서, 해당 추상 클래스를 상속 받은 뒤 추상 메소드를 구현하는 방식
이었다. -
확장 때문에 기존의 상위 DAO 클래스에 불필요한 변화는 생기지 않도록 할 수 있으니 객체지향 설계의 핵심 원리인
개방 폐쇄 원칙(OCP)
은 그럭저럭 지킬 수 있지만, 이렇게 구현하면 각 DAO 로직마다 상속을 통해 새로운 클래스를 만들어야한다. UserDao의 JDBC메소드가 4개일 경우, 4개의 서브 클래스가 필요해진다. -
또, 확장구조가 이미 클래스를 설계하는 시점에서 고정된다. 변하지 않는 코드를 가진
UserDao
의 JDBCtry/catch/finally
블록과 변하는PreparedStatement
를 담고 있는 서브 클래스들이 이미 클래스 레벨에서 컴파일 시점에서 그 관계가 결정되어 있다. 따라서 관계에 대한 유연성이 떨어져버린다.
전략 패턴의 적용
개방 폐쇄 원칙(OCP)
을 잘 지키면서도 템플릿 메소드 패턴보다 유연하고 확장성이 뛰어난 것이, 오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 전략 패턴이다.전략 패턴
은 OCP관점에서 보면확장에 해당하는 변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방식
이다.
-
좌측에 있는
Context
의contextMethod()
에서 일정한 구조를 가지고 동작하다가 특정 확장 기능은 Strategy 인터페이스를 통해 외부의 독립된 전략을 클래스에 위임하는 것이다. deleteAll()
에서 변하지 않는 부분이라고 명시한 것이 바로 이contextMethod()
가 된다.deleteAll()
은 JDBC를 이용해 DB를 업데이트하는 작업이라는 변하지 않는 컨텍스트를 갖는다.- DB 커넥션 가져오기
PreparedStatement
를 만들어줄 외부 기능 호출하기- 전달받은
PreparedStatement
실행하기 - 예외가 발생하면 이를 다시 메소드 밖으로 던지기
- 모든 경우에 만들어진
PreparedStatement
와Connection
을 적절히 닫아주기
PreparedStatement
를 생성하는 전략을 호출할 때는 이 컨텍스트 내에서 만들어둔 DB커넥션을 전달해야한다는 점을 눈여겨봐야 한다.
StatementStrategy 인터페이스
package springbook.user.dao;
...
public interface StatementStrategy {
PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}
deleteAll() 메소드의 기능을 구현한 StatementStrategy 전략 클래스
package springbook.user.dao;
...
public class DeleteAllStatement implements StatementStrategy {
public PreparedStatement makePreparedStatement(Conneciton c) throws SQLException {
PreparedStatement ps = c.preparedStatement("delete from users");
return ps;
}
}
전략 패턴을 따라 DeleteAllStatement가 적용된 deleteAll() 메소드
public void deleteAll() throws SQLException {
...
try {
c = dataSource.getConnection();
StatementStrategy strategy = new DeleteAllStatement();
ps = strategy.makePreparedStatement(c);
ps.executeUpdate();
} catch (SQLException e) {
...
}
}
- 전략 패턴은 적용했는데, 전략 패턴은 필요에 따라 컨텍스트는 그대로 유지되면서(
OCP 폐쇄 원칙
) 전략을 바꿔쓸 수 있다는 것인데, 이렇게 컨텍스트 안에서 이미 구체적인 전략 클래스인DeleteAllStatement
를 사용하도록 고정되어 있다면 뭔가 이상하다. - 컨텍스트가
StatementStrategy
인터페이스 뿐만 아니라 특정 구현 클래스인DeleteAllStatement
를 직접 알고 있다는건, 전략 패턴에도 OCP에도 잘 들어맞는다고 볼 수 없기 때문이다.
DI 적용을 위한 클라이언트/컨텍스트 분리
전략 패턴에 따르면 Context
가 어떤 전략을 사용하게 할 것인가는 앞단의 Client
가 결정하는 것이 일반적이다.
이전 ConnectionMaker
에 전략 패턴을 적용했을 때와 동일한 그림이 나왔다. 결국 이 구조에서 전략 오브젝트 생성과 컨텍스트로의 전달을 담당하는 책임을 분리시킨 것이 ObjectFactory
이며, 이를 일반화한 것이 앞에서 살펴봤던 의존관계 주입(DI)
이었다. 결국 DI란 이러한 전략 패턴의 장점을 일반적으로 활용할 수 있도록 만든 구조라고 볼 수 있다.
StatementStrategy strategy = new DeleteAllStatement();
-
컨텍스트에 해당하는 부분은 별도의 메소드로 독립시켜보자. 클라이언트는
DeleteAllStatement
오브젝트 같은 전략 클래스의 오브젝트를 컨텍스트 메소드로 전달해야 한다. -
이를 위해 전략 인터페이스인
StatementStrategy
를 컨텍스트 메소드 파라미터로 지정할 필요가 있다.
메소드로 분리한 try/catch/finally 컨텍스트 코드
public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException {
Connection c = null;
PreparedStatement ps = null;
tyr {
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) {} }
}
}
클라이언트 책임을 담당할 deleteAll() 메소드
public void deleteAll() throws SQLException {
StatementStrategy st = new DeleteAllStatement(); // 선정한 전략 클래스의 오브젝트 생성
jdbcContextWithStatementStrategy(st); // 컨텍스트 호출. 전략 오브젝트 전달
}
-
위의 구조로 볼 때, 완벽한 전략 패턴의 모습을 갖췄다. 비록 클라이언트와 컨텍스트를 분리하진 않았지만, 의존관계와 책임으로 볼 때는 이상적인 클라이언트/컨텍스트 관계를 갖고 있다.
-
특히 클라이언트가 컨텍스트가 사용할 전략을 정해서 전달한다는 면에서 DI구조라고 이해할 수도 있다. 아직까지는 이렇게 분리한 것에서 크게 장점이 보이지 않지만, 지금까지 해온 관심사 분리와 유연한 확장관계를 유지하도록 만드는 작업은 매우 중요하다.
마이크로 DI
-
의존관계 주입(DI)
는 다양한 형태로 적용할 수 있는데, 가장 중요한 개념은 제 3자의 도움을 통해 두 오브젝트 사이의 유연한 관계가 설정되도록 만든다는 것이다. 이 개념만 따르면 DI를 이루는 오브젝트와 구성요소의 구조나 관계는 다양하게 만들 수 있다. -
일반적으로 DI는 의존관계에 있는 두개의 오브젝트와 이 관계를 다이내믹하게 설정해주는 오브젝트 팩토리(DI 컨테이너), 그리고 이를 사용하는 클라이언트라는 4개의 오브젝트 사이에서 일어난다. 때로는 원시적인 전략 패턴 구조를 따라 클라이언트가 오브젝트 팩토리의 책임을 함께 지고 있을 수도 있다. 또는 클라이언트와 전략(의존 오브젝트)이 결합될 수도 있다. 심지어는 클라이언트와 DI 관계에 있는 두 개의 오브젝트가 모두 하나의 클래스 안에 담길 수도 있다.
-
DI는 꼭 클래스 단위로 일어나지 않고, 매우 작은 단위의 코드와 메소드 사이에서 일어나기도 한다. 얼핏보면 DI 같아보이지 않지만, 세밀하게 관찰해보면 작은 단위지만 엄연히 DI가 이뤄지고 있음을 알 수 있다. 이렇게 DI의 장점을 단순화해서 IoC 컨테이너의 도움 없이 코드 내에서 적용한 경우를
마이크로 DI
라고도 한다. 또는 코드에 의한 DI라는 의미로 수동 DI라고 부를 수도 있다.
3.3 JDBC 전략 패턴의 최적화
기존까지의 컨텍스트와 전략
- 자주 변하는 부분
- 변하지 않는 부분
두 부분을 전략 패턴을 이용해 분리해냈다. 독립된 JDBC 작업 흐름이 담긴 jdbcContextWithStatementStrategy()는 DAO 메소드들이 공유할 수 있는 메소드다. 해당 메소드에 바뀌는 전략들에 대한 클래스를 추가하여 재활용할 수 있다.
여기서
- 컨텍스트는 PreparedStatement를 실행하는 JDBC의 작업 흐름이다.
- 전략은 PreparedStatement를 생성하는 것이다.
3.3.1 전략 클래스의 추가 정보
add() 메소드의 PreparedStatement 생성 로직을 분리한 클래스
public void AddStatement implements StatementStrategy {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword()); // 그런데 user는 어디서 가져올까?
return ps;
}
}
User 정보를 생성자로부터 제공받도록 만든 AddStatement
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());;
...
}
}
user정보를 AddStatement에 전달해주는 add() 메소드
public void add(User user) throws SQLException {
StatementStrategy st = new AddStatement(user);
jdbcContextWithStatementStrategy(st);
}
- add() 메소드에서는 deleteAll() 메소드와 다르게, User라는 정보가 필요했다. 그래서 생성자에 User 정보를 받는 부분을 추가했다.
- add() 메소드의 코드가 훨씬 개선되었다. 테스트를 통해 테스트해보자. 앞으로 비슷한 기능의 DAO 메소드가 필요할 때마다 이 Statement 전략과 jdbcContextWithStatementStrategy() 컨텍스트를 활용할 수 있으니 try/catch/finally로 범벅된 코드를 만들다가 실수할 염려는 사라졌다.
- DAO 코드도 간결해졌다. DAO 코드의 양을 많게는 70~80%까지 줄일 수 있다.
3.3.2 전략과 클라이언트의 동거
- 현재 만들어진 구조에 두가지 불만이 있다.
- 메소드마다 새로운 StatementStrategy 구현 클래스를 만들어야한다.
- 클래스 파일의 개수가 너무 많아진다.
- StatementStrategy에 전달할 부가 정보가 있는 경우, 클래스에 번거롭게 인스턴스 변수를 만들어줘야한다.
- 메소드마다 새로운 StatementStrategy 구현 클래스를 만들어야한다.
로컬 클래스
- 클래스를 매번 독립된 파일로 만들지 않고 UserDao 클래스 내부 클래스로 정의하면 클래스 파일이 많아지는 문제를 해결할 수 있다.
add() 메소드 내의 로컬 클래스로 이전한 AddStatement
public void add(User user) throws SQLException {
class AddStatement implements StatementStrategy {
User user;
public AddStatement(User user) {
this.user = user;
}
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
return ps;
}
}
}
-
위처럼 로컬 클래스로 구성하면, 따로 클래스 파일을 만들지 않아도 된다.
-
AddStatement가 사용될 곳이 add() 메소드 뿐이라면, 이렇게 사용하기 전에 바로 정의해서 쓰는 것두 나쁘지 않다. 덕분에 클래스 파일이 하나 줄고, add() 메소드 안에서 PreparedStatement 생성 로직을 함께 볼 수 있으니 코드를 이해하기도 좋다.
-
로컬클래스에는 또 한가지 장점이 있는데, 내부 클래스이기 때문에 자신을 선언한 메소드의 로컬 변수에 접근할 수 있다는 점이다. 그래서 User를 생성자로 받을 필요가 사라진다.
add() 메소드의 로컬 변수를 직접 사용하도록 수정한 AddStatement
public void add(User user) throws SQLException {
class AddStatement implements StatementStrategy {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
//로컬(내부) 클래스의 코드에서 외부의 메소드 로컬 변수에 직접 접근할 수 있다.
return ps;
}
}
StatementStrategy st = new AddStatement(); // 생성자 파라미터로 user를 전달하지 않아도 된다.
jdbcContextWithStatementStrategy(st);
}
- 단, final로 선언된 외부 로컬 변수에만 접근할 수 있다. 위 예제에서는 final을 붙이지 않았지만, Effectively final(참조링크1, 참조링크2) 때문에 자동으로 final이 붙은 효과가 나타난다. 내부 클래스 선언 이후에 User 객체가 가리키는 곳을 다른데로 바꾸면 에러가 날 것이다.
중첩 클래스의 종류
- 다른 클래스 내부에 정의되는 클래스를 중첩 클래스(nested class)라고 한다.
- static으로 선언되면, static class가 되어 독립적으로 오브젝트로 만들어질 수 있다.
- static이 아니면, inner class로 내부에서만 생성될 수 있다.
- inner class는 다시 범위에 따라 3가지로 나뉜다.
- 오브젝트 레벨에 정의되는 멤버 내부 클래스(Memeber inner class)
- 메소드 레벨에 정의되는 로컬 클래스(local class)
- AddStatement는 여기에 해당된다.
- 이름을 갖지 않는 익명 내부 클래스(anonymous inner class)
- 익명 내부 클래스의 범위는 선언된 위치에 따라 다른다.
익명 내부 클래스 한가지 더 욕심을 내서 AddStatement 클래스의 이름도 제거해보자. 익명 내부 클래스를 이용하면 된다.
AddStatement를 익명 내부 클래스로 전환
StatementStrategy st = new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
return ps;
}
};
메소드 파라미터로 이전한 익명 내부 클래스
public void add(final User user) throws SQLException {
jdbcContextWithStatementStrategy (
new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
return ps;
}
}
)
};
-
메소드가 하나밖에 없는 인터페이스를 익명 내부 클래스로 구현하는 경우, 위와 같이 람다로 구현할 수 있다.
-
deleteAll() 메소드도 위와 같은 방식으로 간단히 정리해보면,
익명 내부 클래스를 적용한 deleteAll() 메소드
public void deleteAll() throws SQLException {
jdbcContextWithStatementStrategy (
new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c)
throws SQLException {
return c.prepareStatement("delete from users");
}
}
);
}
위와 같이 줄일 수 있다.
이제 많은 클래스 파일이 생기지도 않고 메소드의 로컬 변수를 직접 이용하는데에 문제도 없어졌다.
3.4 컨텍스트와 DI
3.4.1 JdbcContext의 분리
이전의 방식을 전략패턴 구조로 보자면, 다음과 같다.
- UserDao.deleteAll(), UserDao.add() : 클라이언트
- 어떤 전략을 사용할지 의존성을 결정
- 익명 내부 클래스 : 전략
- 구체적인 전략
- UserDao.jdbcContextWithStatementStrategy() : 컨텍스트
- PreparedStatement를 실행하는 변하지 않는 부분
JDBC의 일반적인 작업 흐름을 담고 있는 jdbcContextWithStatementStrategy()는 다른 DAO에서도 사용 가능하다. jdbcContextWithStatementStrategy()를 UserDao 클래스 밖으로 독립시켜서 모든 DAO가 사용할 수 있게 해본다.
클래스 분리
-
JdbcContext 라는 클래스를 생성하여 UserDao에 있던 컨텍스트 메소드를 workWithStatementStrategy()라는 이름으로 옮겨놓는다. DataSource가 필요한 것은 UserDao가 아니라 JdbcContext가 된다.
-
JdbcContext가 DataSource에 의존하게 되므로 JdbcContext에 DataSource 타입 빈을 DI 받을 수 있게 해줘야 한다.
JDBC 작업 흐름을 분리해서 만든 JdbcContext 클래스
package springboo.user.dao;
...
public class JdbcContext {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
Conneciton c = null;
PreparedStatement ps = null;
try {
c = this.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) {} }
}
}
}
JdbcContext를 DI 받아서 사용하도록 만든 UserDao
public class UserDao {
...
private JdbcContext jdbcContext;
public void setJdbcContext(JdbcContext jdbcContext) {
this.jdbcContext = jdbcContext; // JdbcContext를 DI 받도록 만든다.
}
public void add(final User user) throws SQLException {
this.jdbcContext.workWithStatementStrategy (
new StatementStrategy() { ... }
);
}
public void deleteAll() throws SQLException {
this.jdbcContext.workWithStatementStrategy(
new StatementStrategy() { ... }
);
}
}
빈 의존관계 설정
-
UserDao는 이제 JdbcContext에 의존하고 있다. 그런데, JdbcContext는 인터페이스인 DataSource와 달리 구체 클래스이다.
-
스프링의 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꿔서 사용하도록 하는게 목적이다. 하지만 이 경우 JdbcContext는 그 자체로 독립적인 JDBC 컨텍스트를 제공해주는 서비스 오브젝트로서 의미가 있을 뿐이고, 구현 방법이 바뀔 가능성은 없다.
-
따라서 인터페이스를 구현하도록 만들지 않았고, UserDao와 JdbcContext는 인터페이스를 사이에 두지 않고 DI를 적용하는 특별한 구조가 된다.
위 그림은 JdbcContext를 적용한 UserDao의 의존관계이다.
위 그림은 JdbcContext가 적용된 빈 오브젝트 관계이다. xml 파일은 아래와 같이 변경되면 된다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="connectionMaker" class="toby_spring.chapter1.user.connection_maker.DConnectionMaker" />
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="username" value="postgres" />
<property name="password" value="iwaz123!@#" />
<property name="driverClass" value="org.postgresql.Driver" />
<property name="url" value="jdbc:postgresql://localhost/toby_spring" />
</bean>
<bean id="jdbcContext" class="toby_spring.chapter1.user.jdbc_context.JdbcContext">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="userDao" class="toby_spring.chapter1.user.dao.UserDao">
<property name="dataSource" ref="dataSource" />
<property name="jdbcContext" ref="jdbcContext" />
</bean>
</beans>
- 아직은 UserDao 클래스의 모든 메소드가 jdbcContext를 사용하는 것은 아니라서 dataSource에 대한 의존성도 필요하다.
3.4.2 JdbcContext의 특별한 DI
JdbcContext의 인터페이스 없는 DI
- UserDao와 JdbbcContext 사이에는 인터페이스를 사용하지 않고 DI를 적용했다. UserDao와 JdbcContext는 클래스 레벨에서 의존관계가 발생한다. 런타임 시에 DI방식으로 외부에서 오브젝트를 주입해주는 방식을 사용하긴 했지만, 의존 오브젝트의 구현 클래스를 변경할 수는 없다.
인터페이스 없이 스프링 빈으로 DI하면 문제가 있을까
-
인터페이스를 사용하지 않는다면 엄밀히 말해서 온전한 DI라고 볼 수는 없다. 하지만 스프링의 DI는 넓게 보자면 객체의 생성과 관계설정에 대한 제어권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC라는 개념을 포괄한다.
-
JdbcContext를 스프링을 이용해 UserDao 객체에서 사용하게 주입했다는건 DI의 기본을 따르고 있다고 볼 수 있다.
JdbcContext를 UserDao와 DI구조로 만들어야 할 이유를 꼽자면 어떤 것이 있을까?
- 싱글톤 레지스트리에 등록된 싱글톤 빈으로 만든다는 것에 의미가 있다.
- 많은 DAO에서 사용되더라도 JdbcContext는 1개의 빈으로 관리가 가능하다.
- 변경되는 상태정보가 없기 때문에 서비스 오브젝트로서 의미가 있고 여러 오브젝트에 공유해 사용되는 것이 이상적이다.
-
JdcbcContext가 dataSource라는 다른 빈에 의존해야 하기 때문이다.
- DI를 위해서 주입되는 오브젝트와 주입받는 오브젝트 양쪽 모두 스프링 빈으로 등록되어야 한다.
- 다른 빈을 주입받기 위해서라도 스프링 빈에 등록되어야 한다.
-
인터페이스가 없다는건 UserDao는 JdbcContext 클래스와 강한 결합을 갖고 있다는 의미이다. OOP의 설계 원칙에는 위배되지만, JdbcContext는 테스트에서도 다른 구현으로 대체해서 사용할 이유가 없다.
-
이런 경우는 굳이 인터페이스를 두지 않아도 상관 없다.
- 단, 이런 클래스를 바로 사용하는 코드 구성을 DI에 적용하는 것은 가장 마지막 단계에서 고려해볼 사항임을 잊지 말자.
스프링 빈으로 DI
코드를 이용하는 수동 DI
-
JdbcContext를 빈으로 등록하지 않고, UserDao 내부에서 직접 DI를 적용할 수도 있다. 이 방법을 쓰려면 JdbcContext를 스프링 빈으로 등록해서 사용했던 첫번째 이유인 싱글톤으로 만드려는 것은 포기해야 한다.
-
하지만 JdbcContext 자체는 싱글톤이 아니더라도, DAO 객체들은 빈으로 등록되어 싱글톤으로 관리될 것이기 때문에 JdbcContext도 DAO와 1:1로 형성될 것이다. 웬만큼 대형 프로젝트라도 수백개면 충분할 것이다.
-
UserDao가 직접 JdbcContext에 DataSource를 DI해주도록 코드를 변경해보자.
JdbcContext 생성과 DI 작업을 수행하는 setDataSource() 메소드
public class UserDao {
...
private JdbcContext jdbcContext;
public void setDataSource(DataSource dataSource) { // 수정자 메소드이면서 JdbcContext에 대한 생성, DI 작업을 동시에 수행한다.
this.jdbcContext = new JdbcContext(); // JdbcContext 생성(IoC)
this.jdbcContext.setDataSource(dataSource); // 의존 오브젝트 투입 (DI)
this.dataSource = dataSource; // 아직 JdbcContext를 적용하지 않은 메소드를 위해 저장해둔다.
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="connectionMaker" class="toby_spring.chapter1.user.connection_maker.DConnectionMaker" />
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="username" value="postgres" />
<property name="password" value="iwaz123!@#" />
<property name="driverClass" value="org.postgresql.Driver" />
<property name="url" value="jdbc:postgresql://localhost/toby_spring" />
</bean>
<bean id="userDao" class="toby_spring.chapter1.user.dao.UserDao">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
-
이 방법의 장점은 굳이 인터페이스를 두지 않아도 될만큼 긴밀한 관계를 갖는 DAO 클래스와 JdbcContext를 어색하기 따로 빈으로 분리하지 않고 내부에서 직접 만들어 사용하면서도 다른 오브젝트에 대한 DI를 적용할 수 있다는 점이다.
-
이렇게 한 오브젝트의 수정자 메소드에서 다른 오브젝트를 초기화하고 코드를 이용해 DI하는 것은 스프링에서도 종종 사용되는 기법이다.
댓글남기기