3. 템플릿

이유석·2022년 12월 11일
1

Book - Toby's Spring

목록 보기
18/20
post-thumbnail

이전까지 책을 읽으며 작은 챕터 하나(ex - 3.1)가 끝날때 마다 게시글을 작성하여 정리하였다.
이렇다 보니, 책 읽는 속도도 느려지고 너무 깊게 이해하려고 한다.
앞으로는 챕터 하나(ex - 3)가 끝날때 마다 게시글을 작성하여 정리하도록 하며, 내용도 더 함축적으로 하여 핵심 적인 내용만 정리하도록 하겠다.

3장에서는 스프링에 적용된 템플릿 기법을 살펴보고, 이를 적용해 완성도 있는 DAO 코드를 만드는 방법을 알아보았다.

템플릿

  • 개방폐쇄원칙에서 고정되어 있는 성질을 갖고, 일정한 패턴으로 유지되는 특성을 가진 부분을 독립시켜 효과적으로 활용할 수 있도록 하는 방법이다.

개방 폐쇄 원칙

  • 확장에는 자유롭게 열러있고, 변경에는 굳게 닫혀 있다는 객체지향 설계의 핵심 원칙

    • 어떤 부분은 변경을 통해 그 기능이 다양해지고 확장하려는 성질을 갖는다.
    • 어떤 부분은 고정되어 있고 변하지 않으려는 성질을 갖는다.

3.1 다시 보는 초난감 DAO

2장에서 제작한 UserDao 는 DB 연결과 관련하여 예외상황에 대한 처리가 되어있지 않아있다.

예외상황은 아래와 같다.

  • 일반적으로 서버에서는 제한된 개수의 DB 관련 공유 자원을 만들어 풀에서 관리한다.
  • 이때 오류가 발생하면 해당 공유 자원을 반환하지 못하고, 반환되지 못한 공유 자원이 계속해서 쌓이다 보면 결국 풀은 가득차게 되며 심각한 오류를 내며 서버가 중단될 수 있다.

그러므로 아래의 조치를 해주어야 한다.

  • JDBC와 같은 예외가 발생할 가능성이 있으며 공유 자원의 반환이 필요한 코드는 반드시 try/catch/finally 블록으로 관리해주어야 한다.

즉, Connection, PrerparedStatement, ResultSet 등과 같은 공유 자원들을 사용할 때 마다 try/catch/finally 블록을 통해 안전하게 생성하고, 반환할 수 있도록 하여 DB 연결 기능을 자유롭게 확장할 수 있는 이상적인 DAO 를 완성하였다.

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

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

3.1 에서 작성한 코드를 살펴보면, 각 DB 관련 공유 자원을 사용하는 곳 마다, try/catch/finally 블록을 생성하여 코드의 복잡성이 증가했다.

이렇게 코드를 작성하다 보면, 분명히 어느 부분에서 공유 자원의 반환이 이루어지지 않는 곳이 존재하게 되며 서버가 중지될 수 있다.

해당 문제를 해결하기 위한 방법

  1. 테스트를 통해 DAO마다 예외 상황에서 자원을 반납하는지 체크한다. - 적용하기 어려움
    예외 상황을 테스트 하는것은 매우 어렵고, 모든 상황마다 테스트를 만드는 것은 어렵다.
    또한, 테스트를 위해 개발한 공유자원 을 구현한 클래스가 필요하지만 이를 만드는 것 또한 어렵다.

  2. 변하지 않는, 그러나 많은 곳에서 중복되는 코드와 로직에 따라 자주 변하는 코드를 분리한다.
    해당 방법은 3.2.2 에서 알아보도록 하겠다.

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

전략 패턴 적용

  • 일정한 작업 흐름이 반복(DB 공유자원 생성 및 반환)되면서 일부 기능만 바뀌는(각 기능을 위한 PreparedStatement 생성) 코드가 존재한다면, 전략패턴을 적용한다.

  • 컨텍스트 : 바뀌지 않는 부분 - DB 공유자원 생성 및 반환
  • 전략 : 바뀌는 부분 - 각 기능을 위한 PreparedStatement 생성
    전략으로 만든 부분은 인터페이스를 통해 각 기능마다 전략을 유연하게 변경할 수 있도록 한다.

  • 같은 애플리케이션 안에서 여러 가지 종류의 전략을 다이나믹하게 구성하고 사용해야 한다면, 컨텍스트를 이용하는 클라이언트 메서드에서 직접 전략을 정의하고 제공하게 만든다.

3.3 JDBC 전략 패턴의 최적화

3.3.1 전략 클래스의 추가 정보

  1. 위와 같은 방법으로 진행하면 각 기능들 마다, 전략을 생성하는 클래스 파일을 만들어 주어야 한다.
    즉, 클래스 파일이 많아지게 되는 문제점이 발생한다.

  2. 또한 해당 기능에 부가적인 정보가 필요하다면, 클래스에 추가 필드를 생성하여 생성자를 통해서 값을 받아오도록 해주어야 한다.
    예시) add() 기능은 User 의 정보가 추가적으로 필요하다.

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

내부 클래스 활용

위와같은 불편 사항을 해결해주기 위하여 아래와 같은 방법을 적용하 수 있다.

  • 첫번째 문제는 내부 클래스를 활용하는 것 이다.
    즉, 전략 클래스를 매번 독립된 파일로 만들지 말고 Client 의 메서드 안에 내부 클래스로 정의해버리는 것 이다.

  • 두번째 문제는 내부 클래스를 활용하며 자연스럽게 해결이 가능해진다.
    내부 클래스는 자신이 선언된 곳의 정보에 접근할 수 있다는 점을 이용하는 것 이다.
    즉, add() 메서드의 User 변수를 내부 클래스에서 직접 사용할 수 있다.

    • 이때 add메서드는 User 를 파라미터로 받기 때문에, 이를 사용하기 위해 외부 변수인 파라미터는 final 로 선언해워쟈 한다.

익멱 내부 클래스 활용
하나의 전략 내부 클래스는 해당 메서드에서만 사용할 용도로 만들어졌다.
그렇다면 익명 내부 클래스를 활용하여 코드를 단축시킬 수 있다.

익명 내부 클래스 : 클래스 선언과 오브젝트 생성이 결합된 형태로 만들어진다.
클래스를 재사용할 필요가 없고, 구현한 인터페이스 타입으로만 사용할 경우에 유용하다.
new 인터페이스이름() {추상 메서드를 구현한 클래스 본문};

3.4 컨텍스트와 DI

3.4.1 JdbcContext 의 분리

현재 코드 상황에 대해서 다시 한번 생각해보고 가겠다.

  • 클라이언트 : UserDao
  • 개별적인 전략 : 익명 내부 클래스가 선언되고 사용되어 지고 있는 메서드
  • 컨텍스트 : DB 연결 및 쿼리 실행, 공유 자원 반환을 담당하고 있는 jdbcContextWithStatementStrategy() 메서드

여기서 JDBC의 일반적인 작업 흐름을 담당하는 컨텍스트 부분은 다른 DAO 에서도 사용 가능하다.
즉, 해당 메서드를 UserDao 클래스 밖으로 독립시키겠다.

  • 해당 메서드를 담고있도록 하는 새로운 JdbcContext 클래스를 생성해주겠다.
    이러면, JdbcContext가 DataSource에 의존하고 있으므로 DataSource 타입 빈을 DI 받을 수 있게 해줘야 한다.

  • 또한, UserDao 는 JdbcContext 를 DI 받아서 사용하도록 만들어줘야 한다.

    • JdbcContext는 구현 방법이 바뀔 가능성이 없기 때문에, 인터페이스를 따로 두지 않고 DI를 적용하도록 하겠다.
      (Spring DI 의 기본 방법은 인터페이스를 두고, 의존 클래스를 바꾸는 것이 목적이다.)

빈 의존관계 변경


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

3.4.2 JdbcContext의 특별한 DI

JdbcContext를 인터페이스를 사용자히 않고, DI 한 부분을 다시 살펴보자

Spring DI의 넓은 개념 : 객체의 생성과 관계설정에 대한 제어권한을 오브젝트에서 제거하고, 외부로 위임했다는 IoC라는 개념을 포괄한다.

위 개념에 따라서, JdbcContext의 의존성 주입 방식은 DI의 기본을 따르고 있다고 볼 수 있다.

또한 이렇게 해야하는 이유는 아래와 같다.

  1. JdbcContext가 Spring 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 되기 때문이다.
    JdbcContext는 변경되는 상태정보를 갖고 있지 않기 때문에 싱글톤이 되는 데 아무런 문제가 없다.

  2. JdbcContext가 DI를 통해 다른 빈(dataSource)에 의존하고 있기 때문이다.
    DI를 위해서는 양쪽(주입 받는, 주입 되는) 모두 Spring 빈으로 등록되어 있어야 한다.

그렇다면, 인터페이스를 사용하지 않은 이유는 무엇일까??

  • 인터페이스가 없다는 것은 UserDao 와 JdbcContext가 매우 긴밀한 관계를 가지고 강하게 결합되어 있다는 의미다.

장점 : 오브젝트 사이의 실제 의존관계가 설정 파일에 명확하게 드러난다.
단점 : DI의 근본적인 원칙에 부합하지 않는 구체적인 클래스와의 관계가 설정에 직접 노출된다.

위 단점을 해결하기 위해, JdbcContext를 UserDao 의 코드를 이용해 수동으로 DI하는 방법이 있다.
즉, 각 DAO 마다 JdbcContext 오브젝트를 만들어 사용하는 방식이다.

이렇게 하면, JdbcContext가 의존하고 있는 dataSource 는 어떻게 DI 할까?

  • DAO 가 dataSource 에 대한 의존성을 주입받아, 직접 JdbcContext에 dataSource 의 빈을 주입시켜주면 된다.

장점 : 구체적인 클래스와의 관계가 노출되지 않는다.
단점 : JdbcContext를 싱글톤으로 만들 수 없고, DI 작업을 위한 부가적인 코드가 필요하다.

위 방법은 상황에 따라 적절하다고 판단되는 방법을 선택해서 사용하면 된다.
일반적으로는 인터페이스를 만들어서 평범한 DI 구조로 만드는게 나을 수도 있다.

3.5 템플릿과 콜백

템플릿 / 콜백 패턴

  • 단일 전략 메서드를 갖는 전략 패턴이면서 익명 내부 클래스를 사용해서 매번 전략을 새로 만들어 사용하고, 컨텍스트 호출과 동시에 전략 DI를 수행하는 방식
  • 템플릿 : 고정된 작업 흐름을 가진 코드를 재사용한다는 의미에서 붙인 이름이다.
  • 콜백 : 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트(익명 내부 클래스 - 단일 메서드 인터페이스)이다.

3.5.1 템플릿 / 콜백의 특징

  1. 콜백에서 매번 메서드 단위로 사용할 오브젝트를 새롭게 전달받는다.
    일반적인 DI 방식은 인스턴스 변수를 생성하고, DI 를 받아 지속적으로 사용한다.

  2. 콜백 오브젝트가 내부 클래스로서 자신을 생성한 클라이언트 메서드 내의 정보를 직접 참조한다.

  3. 클라이언트와 콜백이 강하게 결합되어있다.

JdbcContext에 적용된 템플릿 / 콜백

  • 템플릿과 클라이언트가 메서드 단위인 것이 특징이다.

3.5.2 편리한 콜백의 재활용

장점 : 기존의 여러곳에서 반복적으로 사용하는 코드를 템플릿 한곳에서 관리한다.

현재 코드는 DAO 메서드에서 매번 익명 내부 클래스를 사용하기 때문에 코드의 작성과 읽기가 불편하다.

콜백의 분리와 재활용
익명 내부 클래스에서 변하지 않는 부분과 변하는 부분을 분리하여, 익명 내부 클래스를 사용한 코드를 간결하게 만들어 볼 수 있다.

  • 변하지 않는 부분 : 콜백 클래스 정의 및 PreparedStatement 오브젝트 생성
  • 변하는 부분 : SQL 문장

변하지 않는 부분을 메서드(executeSql())로 정의하고, 변하는 부분인 SQL 문장을 파라미터로 전달받을 수 있도록 하자.

콜백과 템플릿의 결합
재사용 가능한 콜백을 담고있는 메서드라면, DAO가 공유할 수 있는 템플릿 클래스 안으로 옮겨도 된다.
즉, JdbcContext 클래스로 콜백 생성과 템플릿 호출이 담긴 executeSql() 메서드를 옮겨도 된다.

일반적으로는 성격이 다른 코드들은 가능한 분리하는게 좋지만,
하나의 목적을 위해 서로 긴밀하게 연관되어 동작하는 응집력이 강한 코드들은 한군데에 모아두는게 유리하다.

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

3.5.3 템플릿 / 콜백의 응용

템플릿 / 콜백 패턴 적용 과정

  1. 중복되는 코드가 있다면 → 메서드를 통해 간단하게 분리해본다.
  2. 일부 작업을 필요에 따라 바꾸어 사용해야 한다면 → 인터페이스를 사이에 두고 분리해서 전략 패턴을 적용 후, DI로 의존관계를 관리하도록 만든다.
  3. 바뀌는 부분이 한 애플리케이션 안에서 동시에 여러 종류가 만들어질 수 있다면 → 템플릿 / 콜백 패턴을 적용하는 것 을 고려해볼 수 있다.

템플릿 / 콜백 패턴 적용시 주의할 점

  • 템플릿과 콜백의 관계를 정의하고 템플릿이 콜백에게, 톨백이 템플릿에게 각각 전달하는 내용이 무엇인지 파악하는 것 이 중요하다.

  • 템플릿과 콜백을 분리 시 : 변하는 코드의 경계를 찾고, 그 경계를 사이에 두고 주고받는 일정한 정보가 있는지 확인하면 된다.

제네릭스를 이용한 콜백 인터페이스

  • 콜백의 함수형 인터페이스 메서드의 리턴값을 다양하게 사용하고 싶을 때 제네릭스 기법을 이용하면 된다.

3.6 스프링의 JdbcTemplate

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

쿼리 실행 콜백 : PreparedStatementCreater 인터페이스의 createPreparedStatement() 메서드
해당 메서드는 템플릿으로부터 Connection을 제공받아서 PreaparedStatement를 만들어 돌려준다.

update(String query)

  • PreaparedStatement 타입의 콜백 결과를 받아서 사용하는 메서드이다.

update(String query, Object...args)

  • 파라미터를 추가로 설정하여 쿼리에 인자를 설정해줄 수 있다.

<T> T query(PreparedStatementCreator psc, ResultSetExtractor<T> rse)

  • 쿼리 결과 반환 콜백 : ResultSetExtractor 인터페이스의 extractData() 메서드이다.
    쿼리가 반환하는 결과값에 대해 접근할때 사용한다.

  • 이때, 반환하는 결과값의 타입이 다양할 수 있기 때문에 제네릭스를 활용한 것을 볼 수 있다.

int queryForInt(String sql)

  • 쿼리의 결과가 int 값 일때, 편하게 사용할 수 있는 템플릿 이다.

<T> T queryForObject(String sql, Object[] args, RowMapper<T> rowMapper)

  • 쿼리의 결과가 int, String 과 같은 객체가 아닌 복잡한 Object 일때 사용한다.
  • 쿼리의 결과값이 한 개의 로우만 얻을 것이라고 기대한다.
    ex) select * from users where id = ?
    쿼리의 결과값이 한 개가 아니라면, EmptyResultDataAccessException 예외를 던진다.
  • SQL 에 바인딩할 파라미터 값으로 가변 인자 대신 배열을 사용한다.
  • 쿼리 결과 반환 콜백 : RowMapper 인터페이스의 mapRow() 메서드이다.
    ResultSet의 로우 하나를 매핑하기 위해 사용되기 때문에 여러 번 호출될 수 있다.

<T> List<T> query(String sql, Object[] args, RowMapper<T> rowMapper)

  • 쿼리의 결과값이 다수일때 사용한다.
    ex) select * from users

스프링에서는 JdbcTemplate 외에도 십여가지의 템플릿 / 콜백 패턴을 적용한 API 가 존재합니다.
클래스 이름이 Template 으로 끝나거나 인터페이스 이름이 Callback 으로 끝난다면 템플릿 / 콜백 패턴이 적용된 것이라고 보면 된다.

소스코드

profile
https://github.com/yuseogi0218

0개의 댓글