-
3장) 3.4 컨텍스트와 DI ~ 3.5 템플릿과 콜백Java & Spring/토비의 스프링 3.1 2021. 6. 11. 16:44반응형
3장 템플릿
3.4 컨텍스트와 DI
JdbcContext의 분리
- 전략 패턴 구조
- UserDao의 메소드가 클라이언트
- 익명 내부 클래스로 만들어진 것이 개별적인 전략
jdbcContextWithStatementStrategy
메소드가 Context
- JDBC의 일반적인 작업 흐름을 가지고 있는 컨텍스는 다른 DAO에서도 사용 가능
~> 분리 시켜보자 - 클래스 분리
- JdbcContext가 DataSource에 의존 ~> DataSource 타입 빈을 DI 받게 변경
- 생성자를 통해 DataSource 주입
- JdbcContext가 DataSource에 의존 ~> DataSource 타입 빈을 DI 받게 변경
- 빈 의존관계 변경
- 클래스 분리로 인해 UserDao는 JdbcContext에 의존하고 있지만, JdbcContext는 구체 클래스
- 스프링 DI는 인터페이스를 사이에 두고 의존 클래스를 바꿔 사용하는게 목적
- 하지만, 이 경우 서비스 오브젝트로서 의미가 있을 뿐이고 구현 방법이 바뀔 가능성이 없기 때문에 인터페이스를 사용하지 않아도 괜찮다.
- 전략 패턴 구조
jdbcContext의 특별한 DI
지금까지 적용했던 DI는 클래스 레벨에서 구체적인 의존관계가 만들어지지 않도록 인터페이스를 사용했지만, UserDao는 바로 JdbcContext 클래스를 사용
- 인터페이스를 사용하지 않았기 때문에 온전한 DI라고 볼 수는 없음
- 하지만, DI의 기본을 따름
- 객체의 생성과 관계설정에 대한 제어 권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC라는 개념을 포괄
스프링 빈으로 DI
- JdbcContext를 UserDao와 DI 구조로 만들어야하는 이유
- JdbcContext가 스프링 컨테이너 싱글톤 레지스트리에서 관리되는 싱글톤 빈임
- 그 자체로 변경되는 상태정보를 가지고 있지 않음
- JdbcContext가 DI를 통해 다른 빈에 의존하고 있음
- 다른 빈을 DI 받기 위해서라도 스프링 빈으로 등록돼야함
- JdbcContext가 스프링 컨테이너 싱글톤 레지스트리에서 관리되는 싱글톤 빈임
- 스프링에는 드물지만 이렇게 인터페이스를 사용하지 않는 클래스를 직접 의존하는 DI가 있음
- 그 이유는, 책임과 목적을 따졌을 때, 강한 응집도를 가지고 있기 때문
- 단, 클래스를 바로 사용하는 코드 구성을 DI에 적용하는 것은 가장 마지막에 고려해야함
- JdbcContext를 UserDao와 DI 구조로 만들어야하는 이유
코드를 이용하는 수동 DI
UserDao 내부에서 직접 DI
- JdbcContext를 스프링 빈으로 등록해서 사용했던 첫 번째 이유인 싱글톤으로 만드려는 것을 포기해야함
JdbcContext를 스프링 빈으로 등록하지 않았으므로 누군가 JdbcContext의 생성과 초기화를 책임져야함
- JdbcContext의 제어권은 UserDao가 갖는 것이 적당
JdbcContext를 스프링 빈으로 등록해서 사용했던 문제를 해결해야함
- JdbcContext는 다른 빈을 인터페이스를 통해 간접적으로 의존 ~> 자신도 빈으로 등록되어 있었어야함
- JdbcContext에 대한 제어권을 갖고 생성과 관리를 담당하는 UserDao에게 DI 까지 맡기며 해결 가능
- UserDao가 임시로 DI 컨테이너처럼 동작하게 만들면 해결
jdbcContext 빈을 제거한 설정파일만 보면 UserDao가 직접 DataSource를 의존하고 있는 것 같지만, 내부적으로는 JdbcContext를 통해 간접적으로 DataSource를 사용
- 빈 레벨에서는 userDao 빈이 dataSource 빈에 의존하고 있다고 말할 수도 있음
public class UserDao { // ... private JdbcContext jdbcContext; public void setDataSource(DataSource dataSource) { this.jdbcContext = new JdbcContext(); this.jdbcContext.setDataSource(dataSource); this.dataSource = dataSource; } }
인터페이스를 두지 않아도 될 만큼 긴밀한 관계를 갖는 DAO 클래스와 JdbcContext를 어색하게 따로 빈으로 분리하지 않고, 내부에서 직접 사용하면서도 다른 오브젝트에 대한 DI를 적용할 수 있다는 장점이 있음
- 최근 들어 수정자 주입을 하는 코드를 많이 보지는 못함 (개인 의견)
스프링 DI를 위한 빈 등록 vs 수동 DI
- 스프링 DI를 위한 빈 등록
- 장점) 의존관계가 설정파일에 명확하게 드러남
- 단점) DI의 근본 원칙에 부합하지 않는 구체적 클래스와의 관계가 설정에 집적 노출됨
- 수동 DI
- 장점) 관계를 외부에 드러내지 않음
- 단점) 싱글톤으로 만들 수 없고, DI를 위한 부가적인 코드가 필요함
- 스프링 DI를 위한 빈 등록
3.5 템플릿과 콜백
템플릿/콜백 패턴
- 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식
- 전략 패턴의 컨텍스트 ~> 템플릿
익명 내부 클래스 오브젝트 ~> 콜백
템플릿/콜백의 동작 원리
- 템플릿/콜백의 특징
- 단일 메소드 인터페이스를 사용 ~> 전략 패턴은 여러 메소드를 가진 일반 인터페이스 사용 가능
- 콜백은 일반적으로 하나의 메소드를 가진 인터페이스를 구현한 익명 내부 클래스로 만들어진다.
- 클라이언트 역할
- 템플릿 안에 실행될 로직을 담은 콜백 오브젝트를 생성, 참조할 정보 제공
- 콜백은 템플릿 메소드를 호출할 때 파라미터로 전달
- 템플릿
- 정해진 작업 흐름을 따라 작업 진행 ~> 콜백 오브젝트의 메소드 호출
- 콜백은 클라이언트 메소드에 있는 정보와 템플릿의 참조정보를 이용해 작업 수행 ~> 다시 템플릿에게 결과를 리턴
- 템플릿은 콜백이 돌려준 정보로 작업을 마저 수행 ~> 최종 결과를 클라이언트에 리턴
- DI 방식의 전략 패턴 구조라고 생각하면 이해하기 수월
- 일반적인 DI ~> 템플릿에 인스턴수 변수 생성 ~> 주입
- 템플릿/콜백 패턴 ~> 매번 새롭게 오브젝트를 전달받음, 콜백 오브젝트가 자신을 생성한 클라이언트 메소드 내의 정보를 직접 참조
- 전략 패턴과 DI 장점을 익명 내부 클래스 사용 전략과 결합한 활용법
- JdbcContext에 적용된 템플릿/콜백
- UserDao, JdbcContext를 템플릿/콜백의 구조로 살펴보자
- 템플릿과 클라이언트가 메소드 단위인 것이 특징
- UserDao, JdbcContext를 템플릿/콜백의 구조로 살펴보자
- 템플릿/콜백의 특징
편리한 콜백의 재활용
템플릿/콜백의 장단점
- 장점
- 클라이언트 DAO의 메소드가 간결해짐
- 최소한의 데이터 엑세스 로직만 가짐
- 단점
- DAO 메소드에서 매번 익명 내부 클래스를 사용 ~> 코드를 작성하고 읽기가 불편
- 장점
콜백의 분리와 재활용
deleteAll() 메소드의 내용을 통틀어 바뀔 수 있는 것은 오직 쿼리 뿐
- 파라미터로 SQL 문장을 받아 바꾸도록 설정하면 분리 가능
public void deleteAll() throws SQLException { executeSql("delete from users"); } private void excuteSql(final String query) throws SQLException { this.jdbcContext.workWithStatementStrategy( new StatementStrategy() { public PreparedStatement makePreparedStatement(Connection c) throws SQLException { return c.prepareStatement(query); } } ); }
바뀌지 않는 모든 부분을 빼내 executeSql() 메소드로 만들고, 바뀌는 부분인 SQL 문장만 파라미터로 받아 사용하게 설정
- 파라미터는 final로 선언
콜백과 템플릿의 결합
excuteSql을 UserDao만 사용하기는 아까움 ~> 재활용 해보자
템플릿은 JdbcContext 클래스가 아니라 workWithStatementStrategy() 메소드 ~> JdbcContext 클래스로 콜백 생성과 템플릿 호출이 담긴 executeSql() 메소드를 옮겨도 문제 X
public class JdbcContext { //... public void excuteSql(final String query) throws SQLException { workWithStatementStrategy( new StatementStrategy() { public PreparedStatement makePreparedStatement(Connection c) throws SQLException { return c.prepareStatement(query); } } ); } }
- deleteAll은
this.jdbcContext.executeSql("delete from users");
로 바꿔주기만 하면 사용 가능
- deleteAll은
수정으로 인해 JdbcContext 안에 클라이언트와 템플릿, 콜백이 모두 함께 공존하면서 동작하는 구조로 바뀜
성격이 다른 코드들은 가능한 한 분리하는 편이 낫지만, 이 경우는 하나의 목적을 가지고, 응집력이 강한 코드기 때문에 한 군데 모여있는게 유리
템플릿/콜백 응용
스프링은 기본적으로 OCP를 지키고, 전략 패턴과 DI를 바탕에 깔고 있으니, 언제든 확장해서 편리한 방법으로 사용 가능
- 중복 코드를 분리할 방법을 생각해보는 습관을 기르자
템플릿/콜백 응용 예시
Calculator라는 클래스를 만들어보자.
초기에는 덧셈, 뺄셈 등과 같은 수식을 만든다고 가정해보자.
이후에, 곱셈 및 나눗셈 등이 추가될 수 있다.
수식의 공통점을 생각해보면 계산 값들이 있고, 해당 값을 저장할 초기 값(결과 값을 담을 변수), 수식 등이 있다.
public interface LineCallback { Integer doSomthingWithLine(String line, Integer value); }
public Integer lineReadTemplate(String filepath, LineCallback callback, int initVal) throws IOException { try (BuffredReader br = new BufferedReader(new FileReader(filepath));) { Integer res = initVal; String line = null; while(line = br.readLine() != null) { res = callback.doSomethingWithLine(line, res); } return res; } catch (IOException e) { //... } }
- 익명 내부 클래스 때문에 라인 수가 많아보이지만 핵심 코드는 딱 한줄 뿐이다.
public Integer calcSum(String filepath) throws IOException { LineCallback sumCallback = new LineCallback() { public Integer doSomethingWithLine(String line, Integer value) { return value + Integer.valueOf(line); }}; return lineReadTemplate(filepath, sumCallback, 0); } public Integer calcMultiply(String filepath) throws IOException { LineCallback muliplyCallback = new LineCallback() { public Integer doSomethingWithLine(String line, Integer value) { return value * Integer.valueOf(line); }}; return lineReadTemplate(filepath, muliplyCallback, 1); }
위와 같이 재사용을 수월하게 할 수 있음
코드의 특성이 바뀌는 경계를 잘 살피고, 인터페이스를 사용해 분리한다는 객체지향 원칙만 충실하면, 어렵지 않게 템플릿/콜백 패턴을 만들어 활용할 수 있음
제네릭스를 이용한 콜백 인터페이스
파일 각 라인의 문자를 모두 연결해서 하나의 스트링으로 돌려주는 기능을 추가한다고 생각해보자.
public interface LineCallback<T> { T doSomethingWithLine(String line, T value); }
public <T> T lineReadTemplate(String filepath, LineCallback<T> callback, T initVal) throws IOException{ try (BuffredReader br = new BufferedReader(new FileReader(filepath));) { T res = initVal; String line = null; while((line = br.readLine()) != null) { res = callback.doSomethingWithLine(line, res); } return res; } catch (IOException e) { //... } }
public String concatenate(String filepath) throws IOException { LineCallBack<String> concatenateCallback = new LineCallback<String>() { public String doSomthingWithLine(String line, String value) { return value + line; }}; return lineReadTemplate(filepath, concatenateallback, ""); } public Integer calcSum(String filepath) throws IOException { LineCallback<Integer> sumCallback = new LineCallback() { // ... } }
위와 같이 제네릭으로 인터페이스를 설정해주고, 콜백 함수에서 타입을 지정해주면 확장성을 고려하고, 코드 중복을 줄여줄 수 있음
용어 정리
템플릿/콜백
- 템플릿
- 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀
- 콜백
- 다른 오브젝트 메소드에 전달되는 오브젝트
- 펑셔널 오브젝트(functional object) 라고도 함
- 템플릿
반응형'Java & Spring > 토비의 스프링 3.1' 카테고리의 다른 글
4장) 4.1 예외 (0) 2021.06.17 3장) 3.6 스프링의 JdbcTemplate~ 3.7 정리 (0) 2021.06.17 3장) 3.1 다시 보는 초난감 DAO ~ 3.3 JDBC 전략 패턴의 최적화 (0) 2021.06.11 2.4 스프링 테스트 적용 ~ 2.6 정리 (0) 2021.06.03 2장) 2.3 개발자를 위한 테스팅 프레임워크 JUnit (0) 2021.06.01