토비의 스프링 | 3장 템플릿 (독서메모)

주싱·2022년 9월 25일
0

토비의 스프링

목록 보기
12/30

토비의 스프링을 읽고 핵심적인 내용을 정리합니다.

3. 템플릿

개방 폐쇄 원칙

개방 폐쇄 원칙 (OCP, Open-Closed Principle)이란 자유로운 확장에는 열려있고 변경에는 굳게 닫혀 있다는 객체지향 설계의 핵심 원칙이다. 이 원칙은 코드에서 어떤 부분은 변경을 통해 그 기능이 다양해지고 확장하려는 성질이 있고, 어떤 부분은 고정되어 있고 변하지 않으려는 성질이 있음을 말해준다. 변경의 특성이 다른 부부을 구분해주고, 각각 다른 목적과 다른 이유에 의해 다른 시점에 독립적으로 변경될 수 있는 효율적인 구조를 만들어주는 것이 바로 이 개방 폐쇄 원칙이다.

어제 고민하던 ID와 Function간의 매핑 테이블이 개방 폐쇄 원칙을 지키는 객체지향 설계에 가깝구나! 서비스 코드에는 둘이 연관된다는 변하지 않는 사실만 드러나고, 새로운 ID와 Function간의 매핑은 서비스 코드의 변경 없이 얼마든지 확장되어 질 수 있게 된다.

템플릿

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

3.1 다시 보는 초난감 DAO

3.1.1 예외처리 기능을 갖춘 DAO

DB 풀(Pool)의 빠른 반환 요구

  • 일반적으로 서버는 제한된 개수의 DB 커넥션을 만들어서 재사용 가능한 풀로 관리한다. DB 풀은 매번 getConnection()으로 가져간 커넥션을 명시적으로 close()해서 돌려줘야만 다시 풀에 넣었다가 다음 커넥션 요청이 있을 때 재사용할 수 있다. 그래서 여기서 close()는 보통 리소스를 반환한다는 의미로 이해하는 것이 좋다.
  • DB 풀을 사용한다면 사용한 리소스를 빠르게 반환해야 한다. 그렇지 않으면 풀에 있는 리소스가 고갈되고 결국 문제가 발생한다.

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

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

  • 복잡한 try/catch/finally 블록이 2중으로 중첩까지 되어 나오는데다, 모든 메서드마다 반복된다.
  • 실수로 한 줄을 빼먹고 복사했거나, 몇 줄을 잘못 삭제했다면 어떻게 할까? 괄호를 잘못 닫은 게 아니라면 당장 컴파일 에러가 나지 않을 것이다. 기능도 잘 동작하는 것 처럼 보일 것이다. 만약 연결의 close() 코드를 실수로 빼먹었다면 시간이 한 참지나 운영중 DB 연결풀이 가득차고 서버가 죽으면서 문제를 인지하게 될 것이다.
  • 이 문제의 핵심은 변하지 않는, 그러나 많은 곳에서 중복되는 코드와 로직에 따라 자꾸 확장되고 자주 변하는 코드를 잘 분리해 내는 것이다.

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

  • 변하는 성격이 다른 것을 찾아서 분리해서 재사용 해야한다.
  • 일반적인 1) 메서드 추출 리팩토링을 적용하면 분리시킨 메서드를 다른 곳에서 재사용할 수 있어야 하는데 이건 반대로 분리시키고 남은 메서드가 재사용이 필요한 부분이 된다. 분리된 메서드는 DAO 로직마다 새롭게 만들어서 확장돼야 하는 부분이기 때문이다.
  • 2) 템플릿 메서드 패턴을 사용해 변하지 않는 부분은 슈퍼클래스에 두고 변하는 부분은 추상 메서드로 정의해둬서 서브 클래스에서 오버라이드하여 새롭게 정의해 쓰도록 할 수 있다. 이 방법을 사용하면 DAO 로직마다 상속을 통해 새로운 클래스를 만들어야 한다는 점과 확장구조가 클래스를 설계하는 시점에 고정된다는 문제점이 남는다.
  • 이제 마지막으로 3) 전략(Strategy) 패턴을 적용할 차례다. 변하지 않는 부분을 Context라는 클래스로 분리하고, 변하는 부분을 Strategy 클래스로 구현하도록 한다. Context 클래스는 Strategy 클래스를 직접 의존하는 대신 Strategy 인터페이스에만 의존하도록 한다. 그래서 Context에서는 일정한 구조를 가지고 동작하다가 특정 확장 기능은 Strategy 인터페이스를 통해 외부의 독립된 전략 클래스에 위임하는 것이다. 이렇게 되면 필요에 따라 컨텍스트는 그대로 유지되면서 전략을 바꿔 쓸 수 있는 개방과 폐쇄 원칙이 완벽하게 적용되게 된다.

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

  • 전략 패턴에 따르면 Context가 어떤 전략을 사용하게 할 것인가는 Context를 사용하는 앞단의 Client가 결정하는게 일반적이다.
  • 결국 DI란 이러한 전략 패턴의 장점을 일반적으로 활용할 수 있도록 만든 구조라고 볼 수 있다.
  • 컨텍스트를 별도의 메서드로 분리했으니 기존의 deleteAll() 메서드가 클라이언트가 된다. deleteAll()은 전략 오브젝트를 만들고 컨텍스트를 호출하는 책임을 지고 있다.

마이크로 DI

  • DI의 가장 중요한 개념은 제3자의 도움을 통해 두 오브젝트 사이의 유연한 관계가 설정되도록 만든다는 것이다.
  • 일반적으로 DI는 의존관계에 있는 두 개의 오브젝트와 이 관계를 다이나믹학 설정해주는 오브젝트 팩토리(DI 컨테이너), 그리고 이를 사용하는 클라이언트라는 4개의 오브젝트 사이에서 일어난다.
  • 하지만 때때로 원시적인 전략 패턴 구조를 따라 클라이언트가 오브젝트 팩토리의 책임을 함께 지고 있을 수도 있다.
  • DI의 장점을 단순화해서 IoC 컨테이너의 도움 없이 코드 내에서 적용한 경우를 마이크로 DI 또는 수동 DI라고 부를 수 있다.

3.3 JDBC 전략 패턴의 최적화

3.3.1 전략 클래스의 추가 정보

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

전략 패턴을 완벽하게 적용하였지만 여전히 남아 있는 문제가 있다.

  • 클래스 파일의 개수가 많아진다. 런타임 시에 DI해준다는 점을 제외하면 로직마다 상속을 사용하는 템플릿 메서드 패턴을 적용했을 때보다 그다지 나을 게 없다.
  • Strategy 구체 클래스에 전달하는 부가 정보가 있는 경우 오브젝트를 전달받는 생성자와 이를 저장해둘 인스턴스 변수를 번거롭게 만들어야 한다는 점이다. 이 오브젝트가 사용되는 시점은 컨텍스트가 전략 오브젝트를 호출할 때이므로 잠시라도 어딘가에 다시 저장해둘 수밖에 없다.

로컬 클래스

  • Strategy 구체 클래스인 DeleteAllStatement나 AddStatement는 UserDao 밖에서는 사용되지 않는다. 둘 다 UserDao에서만 사용되고, UserDao의 메서드 로직에 강하게 결합되어 있다.
  • 그래서 메서드 내의 로컬 클래스로 이전해 줄 수 있다.
  • 중첩 클래스의 종류
    • 독립성
      • Static Class - 독립적으로 오브젝트로 만들어질 수 있다
      • Inner Class - 자기를 정의한 클래스의 오브젝트 안에서만 만들어질 수 있다.
    • 범위 기준
      • Member inner class - 멤버 필드처럼 정의된다.
      • Local clsss - 메서드 레벨에서 정의된다.
    • 이름 기준
      • Anonymous inner class - 이름을 갖지 않는다.
  • 중첩 클래스가 좋은 이유
    • AddStatement가 사용될 곳이 add() 메서드뿐이라면 사용하기 전에 바로 정의해서 쓰는 것도 나쁘지 않다.
    • 덕분에 클래스 파일 개수가 줄어든다.
    • add() 메서드 안에서 PreparedStatement 생성 로직을 함께 볼 수 있으니 코드를 이해하기도 좋다.
    • 로컬 클래스는 자신이 선언된 메서드의 로컬 변수에 직접 접근할 수 있다. 그래서 Strategy 구체 클래스에 전달해야 하는 부가 정보를 위해 번거롭게 생성자를 통해 전달하고 멤버 변수를 통해 저장하고 있는 코드를 작성할 필요가 없다.
  • 로컬 클래스로 인해 좋아진 점
    • AddStatement는 복잡한 클래스가 아니므로 메서드 안에서 정의해도 그다지 복잡해 보이지 않는다.
    • 메서드마다 추가해야 했던 클래스 파일을 줄일 수 있다.
    • 내부 클래스의 특징을 이용해 로컬 변수를 바로 가져다 사용할 수 있다.

익명 내부 클래스

  • 좀 더 간결하게 클래스 이름도 제거해 보자.
  • new 인터페이스이름() { 클래스 본문 }; 과 같은 형식으로 필요한 곳에 클래스 이름 없이 직접 선언과 정의를 한 번에 할 수 있다.

3.4 컨텍스트와 DI

  • UserDao 클래스 외부에서도 일반적으로 사용할 수 있는 jdbcContextWithStatementStrategy() 메서드는 밖으로 독립시켜서 모든 DAO가 사용할 수 있게 해보자.

클래스 분리

  • 프로그래밍 요소 네이밍에 문맥활용하기
    • 분리해서 만든 클래스의 이름은 JdbcContext라고 하자.
    • 메서드는 workWithStatementStrategy()라고 옮겨본다.

빈 의존관계 변경

  • UserDao 객체는 JdbcContext를 인터페이스 없이 직접 의존관계 주입을 받고 있다. 이게 왜 괜찮은가?
  • JdbcContext는 그 자체로 독립적인 JDBC 컨텍스트를 제공해주는 서비스 오브젝트로서 구현 방법이 바뀔 가능성은 없다.

3.4.2 JdbcContext의 특별한 DI

  • 지금까지 DI에서는 클래스 레벨에서 구체적인 의존관계가 만들어지지 않도록 인터페이스를 사용했다. 그러나 UserDao는 JdbcContext를 직접 주입받고 있다. 그래서 비록 런타임 시에 외부에서 객체를 주입받고 있지만 의존 객체의 구현 클래스를 바꾸어 줄 수는 없다.

스프링 빈으로 DI

  • 스프링의 DI는 넓게 보자면 객체의 생성과 관계설정에 대한 제어권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC라는 개념을 포함한다.
  • 인터페이스를 통해서 클래스를 자유롭게 변경할 수 있게 하지는 않았지만 JdbcContext를 UserDao와 DI 구조로 만들어야 하는 이유를 생각해보자.
    • JdbcContext는 상태정보를 갖지 않는다. 뿐만아니라 내부 멤버 모두 역시 상태정보를 갖지 않는다. 일반적인 서비스를 제공함으로 싱글톤으로 등록돼서 여러 오브젝트에서 공유해 사용되는 것이 이상적이다.
    • JdbcContext 역시 DI를 통해 다른 빈에 의존하고 있기 때문이다. DI 컨테이너 서비스를 받기 위해서는 주입되는 오브젝트와 주입받는 오브젝트 모두 스프링 빈으로 등록돼어야 한다.
  • 반대로 UserDao에서 JdbcContext를 인터페이스 없이 직접 생성하고 의존관계를 연결해도 괜찮은 이유도 생각해 보자.
    • 인터페이스가 없다는 것은 두 객체가 매우 긴밀한 관계를 가지고 강하게 결합되어 있다는 의미이다. UserDao는 항상 JdbcContext와 함께 사용돼야 한다. JDBC 방식 대신 JPA나 하이버네이트 같은 ORM을 사용해야 한다면 JdbcContext도 통째로 바뀌어야 한다. JdbcContext는 DataSource와 달리 테스트에서도 다른 구현으로 대체해서 사용할 이유가 없다.
    • 강력한 결합을 가진 관계를 허용하면서 위에서 말한 두 가지 이유인, 싱글톤으로 만드는 것과 JdbcContext에 대한 DI 필요성을 위해 스프링의 빈으로 등록해서 UserDao DI 되도록 만들어도 좋다.

코드를 이용하는 수동 DI

  • JdbcContext의 제어권은 UserDao가 갖는 것이 적당하다. 자신이 사용할 오브젝트를 직접 만들고 초기화하는 전통적인 방법을 사용하는 것이다. 어차피 JdbcContext 클래스의 정체도 알고 있으니 문제 될 것은 없다.
  • DI 컨테이너의 도움을 받는다면 의존관계가 설정 파일에 명확하게 드러난다. 반면에 애플리케이션 객체 내부에서 수동으로 DI를 수행한다면 둘의 관계를 숨기는 효과가 있을 수 있다.
  • 그러나 여러 DAO에서 사용하더라도 싱글톤으로 만들 수 없고, DI 작업을 위한 부가 코드를 필요로 한다.

분명하게 설명할 자신이 없다면 차라리 인터페이스를 만들어서 평범한 DI 구조로 만드는 게 나을 수 있다.

3.5 템플릿과 콜백

  • 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고 그중 일부만 자주 바꿔서 사용해야 하는 경우에 적합한 구조다. 이런 방식을 스프링에서는 템플릿/콜백 패턴이라고 부른다.
  • 전략 패턴의 컨텍스트를 템플릿이라 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 부른다.
  • 중복될 가능성이 있는 자주 바뀌지 않는 부분을 분리한다.
  • 일반적으로 성격이 다른 코드들은 가능한 한 분리하는 편이 낫지만, 이 경우는 반대다. 하나의 목적을 위해 서로 긴밀하게 연관되어 동작하는 응집력이 강한 코드들이기 때문에 한 군데 모여 있는게 유리하다.
  • 템플릿과 콜백을 찾아낼 때는, 변하는 코드의 경계를 찾고 그 경계를 사이에 두고 주고받는 일정한 정보가 있는지 확인하면 된다.

3.6 스프링의 JdbcTemplate

  • 겨우 두 번 나왔는데 이것도 중복이라고 생각하고 분리할 필요가 있을까? 만약 두 개가 전부이고 UserDao 기능이 더 추가되지 않을 것이라면 그냥 넘어가도 문제 될 것은 없다. 하지만 UserDao에 앞으로 추가될 기능을 예상해보면 RowMapper의 사용이 여기서 끝날 것 같지는 않다.
  • 만약 사용할 테이블과 필드정보가 바뀌면 UserDao의 거의 모든 코드가 함께 바뀐다. 따라서 응집도가 높다고 볼 수 있다.
  • 반면에 JDBC API를 사용하는 방식, 예외처리, 리소스의 반납, DB 연결을 어떻게 가져올지에 관한 책임과 관심은 모두 JdbcTemplate에 있다. 따라서 변경이 일어난다고 해도 UserDao 코드에는 아무런 영향을 주지 않는다. 그런 면에서 책임이 다른 코드와는 낮은 결합도를 유지하고 있다.
  • 더 생각해 볼 문제
    • userMapper 빈으로 등록해 사용하기
    • UserDao에 있는 SQL 쿼리 문장도 별도로 구분해서 관리하기
profile
소프트웨어 엔지니어, 일상

0개의 댓글