Enjoy My Posts

토비의 스프링 3장

Posted on By Geunwon Lim

3장 템플릿

어떤 부분은 변경을 통해 그 기능이 다양해지고 확장하려는 성질이 있고, 어떤 부분은 고정되어 있고 변하지 않으려는 성질이 있다. 개방폐쇄원칙을 통해 변화의 특성이 다른 부분을 구분해주고, 각각 다른 목적과 다른 이유에 의해 다른 시점에 독립적으로 변경될 수 있는 효율적인 구조를 만들어주는 준다.

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

3.1 다시 보는 초난감 DAO

예외 처리를 할 때 try/catch 문이 많이 필요할 수 있는데, 이 try/catch는 많은 부분에서 공통적으로 사용된다. 그 코드들을 복사,붙여넣기로 계속 활용할 순 있지만, 코드가 지져분해질 뿐 아니라 잘못 복사하는 실수할 가능성이 높다. 같은 역할을 하지만, 여러 곳에서 반복 사용되면서 코드가 방대해짐에 따라, 누군가 코드를 수정하다가 실수할 가능성이 높아지는 시한폭탄이 돼버린다.

테스트를 통해 처리할 수도 있겠지만, 반복되는 코드가 테스트하기 쉽지 않은 코드일 수 있다. 책에 나온 예외상황 처리일 때에도 강제로 예외상황을 만들기가 쉽지 않고, 적용한다고 하더라도 테스트 코드의 양이 과도하게 많아질 것이다. 차라리 코드를 열심히 살펴보는 게 나을수도 있을 정도로 말이다.

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

이 문제의 핵심은 ‘변하지 않는, 그러나 많은 곳에서 중복되는 코드’와 ‘로직에 따라 자꾸 확장되고 자주 변하는 코드’를 잘 분리해내는 것이다. 이러한 문제를 개선할 때 가장 먼저 할 일은 ‘변하는 성격이 다른 것을 찾아내는 것’이다. 변하는 부분과 변하지 않는 코드를 찾아냈다면, 그 다음 할 것은 변하는 것과 변하지 않는 것을 분리하는 것이다.

  1. 메소드 추출

    먼저 생각할 수 있는 방법은 메소드로 빼는 것이다(변하지 않는 부분이 변하는 부분을 감싸고 있어, 변하지 않는 부분은 추출하기 어려워 반대로 해보는 것이다). 자주 바뀌는 부분을 메소드로 독립시켜도, 별 이득이 없어보인다. 왜냐하면 보통 메소드 추출 리팩토링을 적용하는 경우에 분리시킨 메소드를 다른 곳에서 재사용할 수 있어야 하는데, 이건 반대로 분리시키고 남은 메소드가 재사용할 수 잇는 것이고, 분리시킨 것은 로직마다 새롭게 만들어서 확장돼야 하는 부분이기 때문이다.

  2. 템플릿 메소드 패턴의 적용

    템플릿 메소드 패턴은 상속을 통해 기능을 확장해서 사용하는 것이다. 변하지 않는 부분은 슈퍼클래스에 두고 변하는 부분은 추상 메소드로 정의해둬서 서브클래스에서 오버라이드하여 새롭게 정의해 쓰도록 하는 것이다. 이 패턴을 적용하면 기능을 확장하고 싶을 때마다 상속을 통해 자유롭게 확장할 수 있고, 확장 때문에 기존의 상위 클래스에 불필요한 변화는 생기지 않으니 ocp를 그럭저럭 지키는 구조를 만들 수 있다. 하지만 템플릿 메소드 패턴은 안좋은 게 많다. 가장 큰 문제는 새로 생기는 로직마다 상속을 통해 새로운 클래스를 만들어야 한다는 점이다. 이러면 장점보다 단점이 많이 생길 수도 있다. 또한, 확장구조가 이미 클래스를 설계하는 시점에서 고정되어 버린다는 점이다. 변하지 않는 코드를 담고 있는 부분과 변하는 부분이 클래스 레벨에서 컴파일 시점에 그 관계가 결정된다. 따라서 그 관계의 유연성이 떨어진다.

  3. 전략 패턴의 적용

    OCP를 잘 지키면서 템플릿 메소드 패턴보다 유연하고 확장성이 뛰어난 것이 오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 전략패턴이다. 변하지 않는 부분은 맥락으로 두고, 상황에 따라 변하는 부분은 전략으로 두어 특정 기능을 전략에 맡기는 것이다.

    한편, 전략 패턴은 필요에 따라 컨텍스트는 그대로 유지되면서 전략을 바꿔쓸 수 있어야 하기 때문에, 컨텍스트 안에서 이미 구체적인 전략이 고정된다면 이상하다. 전략 패턴에 따르면 컨텍스트가 어떤 전략을 사용하게 할 것인가는 컨텍스트를 사용하는 앞단의 클라이언트가 결정하는 게 일반적이다. 따라서 di를 적용해줘야 한다. 그러기 위해 컨텍스트에 해당하는 부분을 별도의 메소드로 독립시킨다. 클라이언트는 전략을 컨텍스트 메소드에 전달해야 한다. 컨텍스트 메소드의 흐름을 살펴보면,

    1. 클라이언트로부터 전략 오브젝트를 제공받고, 반복되는 작업(try/catch/finally 등) 을 수행한다. 모든 틀에박힌 작업은 이 컨텍스트 메소드 안에 담겨있다.
    2. 제공받은 전략 오브젝트는 필요한 시점에 호출해서 사용한다.
    3. 클라이언트는 전략 오브젝트를 만들고, 컨텍스트를 호출하는 책임을 진다.

    전략 패턴을 적용하여 변하지 않는 부분은 여러 메소드들이 공유할 수 있게 됐다. 또한 상황에 따라 적절한 전략, 바뀌는 로직을 제공해줄 수 있게 됐다.

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

전략 패턴을 적용한다고 해도 개선할 점이 있다. 1. 기능이 추가될 때마다 새로운 클래스를 구현해야한다는 점이다. 이래서는 런타임 시 다이내믹하게 di해준다는 점을 제외하면 로직마다 상속을 사용하는 템플릿 메소드 패턴을 적용했을 때보다 그다지 나을 게 없다. 2. 전략에 부가적인 정보를 전달해야 할 경우, 이를 위해 생성자나 인스턴스 변수를 번거롭게 만들어야 한다는 점이다. 이 두 문제를 해결하는 방법을 생각하자.

로컬 클래스

클래스 파일이 많아지는 문제는 전략 클래스를 매번 독립된 파일로 만들지 말고 내부 클래스로 정의해버리면 된다. 특정 메소드에서만 사용되는 것이라면 로컬 클래스로 만들 수 있다. 이렇게 하면 클래스 파일이 하나 줄고, 메소드 안에서 로직을 함께 볼 수 있으니 이해하기 좋다. 또한 부가적인 정보를 전달할 때도 좋다. 나아가 익명 클래스로 만들면, 클래스 이름을 제거할 수도 있다.

3.4 컨텍스트와 DI

3.4.1 JdbcContext의 분리

컨텍스트의 변하지 않는 부분은 여러 메소드에 공유될 수 있다. 따라서 변하지 않는 부분을 아예 밖으로 빼서 활용도를 높일 수 있다(클래스 분리). 이렇게 바꿀 경우 기능을 수행하는 클래스에서는 컨텍스트를 주입받아야 하는데, 이 때 일반적으로는 인터페이스를 사이에 두고 의존 클래스를 바꿔서 사용한다(di의 목적상). 하지만 컨텍스트는 그 자체로 서비스 오브젝트로서의 의미가 있을 뿐이고 구현 방법이 바뀔 가능성은 없다. 따라서 인터페이스를 구현하지 않아도 큰 문제가 없다. 이렇게 되면 클래스 레벨에서 의존관계가 결정되고, 즉 런타임 시에 의존관계를 주입받더라도 의존 오브젝트를 변경할 수 없다.

3.4.2 JdbcContext의 특별한 DI

스프링 빈으로 DI

일반적인 di에서는 인터페이스를 사이에 둬서 클래스레벨에서는 의존관계가 고정되지 않게 하고, 런타임 시에 의존할 오브젝트와의 관계를 다이내믹하게 주입해주는 것이 맞다. 하지만 di를 넓게 보면 객체 생성과 관계 설정의 권한을 오브젝트에서 제거하고 외부로 위임한다는 IoC의 개념을 포괄한다. 인터페이스를 사용하지 않아도 di구조를 만들어야 하는 이유를 생각해보자.

  1. 컨텍스트가 싱글톤 빈으로 관리되기 때문이다.
  2. 컨텍스트가 di를 통해 다른 빈에 의존하고 있기 때문이다. 스프링으로 di를 하려면 주입되는 오브젝트와 주입받는 오브젝트 양쪽 모두 스프링 빈으로 등록돼야 한다. 즉 다른 빈을 di받기 위해서라도 빈으로 등록돼야 한다.

그렇다면 왜 인터페이스를 사용하지 않았을까? 인터페이스가 없다는 건 컨텍스트를 사용하는 클래스와 컨텍스트가 매우 긴밀한 관계를 가지고 결합되어 있다는 의미다. 컨텍스트를 사용하는 클래스는 항상 컨텍스트와 함께 사용돼야 하고, 클래스는 구분돼있지만 강한 응집도를 갖고 있는 것이다. 이런 경우는 굳이 인터페이스를 두지 말고 강력한 결합을 가진 관계를 허용하면서 빈으로 등록해 di되도록 해도 좋다.

수동 DI

스프링 빈에 등록하지 않고 직접 di할 수도 있다. 이 방법을 쓰려면 스프링 빈으로 등록하는 처ㅏㅅ번째 이유인 싱글톤으로 만드는 것은 포기해야 한다. 그렇다고 무식하게 메소드가 호출될 때마다 오브젝트를 생성하는 건 아니다. dao당 하나씩 만든다고 생각할 경우, 그렇게 많지 않다. 즉 크게 문제되지 않는다. 그렇다면, 컨텍스트가 di를 받기 위해서라도 빈으로 등록돼야 한다는 문제는 어떨까? 이 때는 컨텍스트에 대한 제어권을 갖고 관리하는 부분에 di까지 맡기는 것이다. 즉, 책에서는 dao가 임시로 di컨테이너처럼 동작하게 하면 된다. 이 방법의 장점은 굳이 인터페이스를 두지 않아도 될 만큼 긴밀한 관계를 갖는 클래스들을 어색하게 빈으로 분리하지 않고 내부에서 직접 만들어 사용하면서도 다른 오브젝트에 대한 di를 적용할 수 있다는 점이다.

인터페이스를 사용하지 않고 di를 적용하는 방법 두가지를 알아봤다. 각각 장단점이 있는데, 인터페이스를 사용하지 않는 클래스와의 의존관계이지만 스프링의 di를 이용하기 위해 빈으로 등록해 사용하는 방법은 실제 의존관계가 설정파일에 명확히 드러나는 장점이 있다. 하지만 di의 근본적인 원칙에 부합하지 않는 구체적인 클래스와의 관계가 설정에 직접 노출되는 단점이 있다. 반면 수동 di는 사용되는 클래스가 사용하는 클래스 내부에서 만들어지고 사용되면서 그 관계를 외부에 드러내지 않는 장점이 있다. 필요에 따라 은밀히 di를 수행하고 그 전략을 외부에 감추는 것이다. 하지만 여러 오브젝트가 만들어져도 싱글톤으로 만들 수 없고, di를 위한 부가적인 코드가 필요하다는 단점이 있다.

3.5 템플릿과 콜백

전략패턴을 적용해봤는데, 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고, 그 중 일부만 자주 바꿔 사용해야 하는 경우 적합하다. 이런 방식을 템플릿/콜백 패턴이라고 부른다.전략 패턴의 컨텍스트를 템플릿이라고 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 부른다(전략 = 콜백).

3.5.1 템플릿/콜백의 동작원리

템플릿은 고정된 작업 흐름을 가진 코드를 재사용한다는 의미에서 붙인 이름이다. 콜백은 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트를 말한다.

여러 개의 메소드 를 가진 일반적 인터페이스를 사용할 수 있는 전략패턴의 전략과 달리, 콜백은 보통 단일 메소드 인터페이스를 사용한다. 템플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출되는 경우가 많기 때문이다. 즉, 콜백은 일반적으로 하나의 메소드를 가진 인터페이스를 구현한 익명 내부 클래스로 만들어진다고 보면 된다.

콜백 인터페이스의 메소드에는 보통 파라미터가 있다. 이 파라미터는 템플릿의 작업 흐름 중 만들어지는 컨텍스트 정보를 전달받을 때 사용된다.

일반적인 di라면 템플릿에 인스턴스 변수를 만들어두고 사용할 의존 오브젝트를 수정자 메소드로 받아서 사용할 것이다. 반면 템플릿/콜백 방식에서는 매번 메소드 단위로 사용할 오브젝트를 새롭게 전달받는 것이 특징이다. 콜백 오브젝트가 내부 클래스로서 자신을 생성한 클라이언트 메소드 내의 정보를 직접 참조한다는 것도 템플릿/콜백의 고유한 특징이다. 클라이언트와 강하게 결합된다는 면에서도 일반적인 di와 다르다. 템플릿/콜백 방식은 전략패턴과 di의 장점을 익명 내부 클래스 사용 전략과 결합한 독특한 활용법이라고 이해할 수 있다. 이 패턴에 녹아있는 전략 패턴과 수동 di를 이해해야 한다.

앞서 적용한 템플릿/콜백 패턴의 구조를 살펴볼 때 템플릿과 클라이언트가 메소드 단위인 것이 특징이다.

템플릿의 작업 흐름이 좀 더 복잡한 경우에는 한 번 이상 콜백을 호출하기도 하고 여러 개의 콜백을 클라이언트로부터 받아 사용하기도 한다.

템플릿/콜백 방식은 템플릿에 담긴 코드를 여기저기서 반복하는 원시적인 방법에 비해 많은 장점이 있다. 위에서 얘기한 반복 사용함에 따라 생기는 문제들을 해결할 수 있고, 클라이언트의 메소드가 간결해진다.

콜백의 분리와 재활용

하지만 아쉬운 점이 한 가지 있다. 익명 내부 클래스를 사용하기 때문에 코드를 작성하고 읽기가 조금 불편하다는 점이다. 따라서 복잡한 익명 내부 클래스의 사용을 최소화할 수 있는 방법을 찾아보자. 만약 분리를 통해 재사용이 가능한 코드를 찾아낼 수 있다면 익명 내부 클래스를 사용한 코드를 간결하게 만들 수 있다. 책에서 콜백 오브젝트 코드를 살펴보면, 내용이 간단하다. 이러한 콜백이 적지 않을 것이고, 유사한 내용의 콜백 오브젝트가 반복될 가능성이 크다. 여기에서도 중복될 가능성이 있는 자주 바뀌지 않는 부분을 분리하자. 바뀌지 않는 모든 부분을 빼내서 메소드로 만들고, 바뀌는 부분만 파라미터로 받아서 사용하게 만든다. 이렇게 하면 코드가 간결해지고, 복잡한 익명 내부 클래스인 콜백을 직접 만들 필요조차 없어진다.

콜백과 템플릿의 결합

콜백 중 바뀌지 않는 부분을 모아놓은 메소드를 특정 클래스에서만 사용하기는 아깝다. 이렇게 재사용 가능한 콜백을 담고 있는 메소드라면 템플릿 클래스 안으로 옮겨서 더 공유 가능하게 할 수 있다. 엄밀히 말해서 템플릿은 클래스가 아니라 그 안에 있는 메소드이므로 콜백의 변하지 않는 부분을 템플릿 클래스로 옮겨도 된다. 일반적으로 성격이 다른 코드들은 가능한 분리하는 편이 낫지만, 이 경우는 반대다. 하나의 목적을 위해 서로 긴밀하게 연관돼 동작하는 응집력 강한 코드들이기 때문에 한 군데 모여 있는 게 유리하다. 구체적인 구현과 내부의 전략, 코드에 의한 di, 익명 내부 클래스 등의 기술은 최대한 감춰두고, 외부에는 꼭 필요한 기능을 제공하는 단순한 메소드만 노출해주는 것이다.

3.5.3 템플릿/콜백의 응용

고정된 작업 흐름을 갖고 있으면서 여기저기서 자주 반복되는 코드가 있다면, 중복되는 코드를 분리할 방법을 생각하자. 중복된 코드는 먼저 메소드로 분리하는 시도를 해보고, 그 중 일부 작업을 필요에 따라 바구어 사용해야 한다면 인터페이스를 사이에 두고 분리해 전략패턴을 적용하고 di로 의존관계를 관리하도록 만든다. 바뀌는 부분이 한 애필리케이션 안에서 여러 종류가 만들어질 수 잇다면 템플릿/콜백 패턴을 적용하는 것을 고려할 수 있다.

중복의 제거와 템플릿/콜백 설계

템플릿/콜백 패턴을 적용할 때는 다음을 고려하자. 먼저 템플릿에 담을 반복되는 작업의 흐름은 어떤 것인지 살펴보자. 템플릿이 콜백에게 전달해줄 내부의 정보는 무엇이고, 콜백이 템플릿에게 돌려줄 내용은 무엇인지 생각해보자. 그리고 템플릿이 작업을 마친 뒤 클라이언트에게 전달해줘야 할 것도 있다. 템플릿/콜백을 적용할 때는 템플릿과 콜백의 경계를 정하고 템플릿이 콜백에게, 콜백이 템플릿에게 각각 전달하는 내용이 무엇인지 파악하는 게 가장 중요하다. 그에 따라 콜백의 인터페이스를 정의해야 하기 때문이다.