개요
아주 간단하지만 아주 끔찍한(?) 클래스를 하나 보겠습니다.
class MyClass {
void myMethod1() {
System.out.println("A");
System.out.println("B1");
System.out.println("C");
}
void myMethod2() {
System.out.println("A");
System.out.println("B2");
System.out.println("C");
}
}
public class Main {
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.myMethod1();
myClass.myMethod2();
}
}
위의 MyClass는 비슷한 로직을 수행하는 메소드를 2개 가지고 있습니다. B1, B2를 출력하는 부분 이외에는 A와 C부분은 똑같은 행위를 하고 있습니다. (아, 그리고 출력하는 한줄짜리 코드를 조금은 복잡한 로직을 수행하는 것이라고 상상해주세요.)
이 끔찍한 MyClass를 조금씩 리팩토링해보겠습니다.
1. 중복되는 부분 메소드로 추출하기
먼저 중복되는 A와 C출력 로직을 메소드로 만들어서 처리하도록 하겠습니다.
class MyClass {
void myMethod1() {
a();
System.out.println("B1");
c();
}
void myMethod2() {
a();
System.out.println("B2");
c();
}
void a() {
System.out.println("A");
}
void c() {
System.out.println("C");
}
}
public class Main {...}
중복되는 부분을 메소드로 추출하는 방법은 아주 간단하고 효과적입니다. 초보개발자들도 쉽게 실천할 수 도 있구요.
하지만, 비슷한 메소드인 myMethod3을 만들어야 한다면 어떨까요? 다시 MyClass의 새로운 메소드를 추가하고, B3를 출력하는 코드를 작성해야 합니다. 그리고 이건 myMethod4, myMethod5... 가 나오게 될 때 마다 발생될 문제입니다.
2. 전략패턴 적용하기
객체지향의 설계원칙에 따르면, 클래스가 외부의 요구에 맞추어서 계속해서 변하게 되는 것은 좋지 않습니다. (개방-폐쇄 원칙; OCP). 이런 경우 전략 패턴을 사용하게 되면, 기능 확장에는 개방되어 있고, 외부 요구에 대한 변화에 폐쇄되어진 형태를 만들 수 있습니다.
전략 패턴은, 변화 될 수 있는 외부의 요구사항들을 하나의 인터페이스로 만들어서 대응 할 수 있도록 만드는 디자인 패턴입니다.
(전략패턴에 대한 자세한 포스팅은 이전 포스팅을 참고하시기 바랍니다.)
class MyClass {
void myMethod(PrintB printB) {
a();
printB.b();
c();
}
void a() {
System.out.println("A");
}
void c() {
System.out.println("C");
}
}
interface PrintB {
void b();
}
class B1 implements PrintB {
@Override
public void b() {
System.out.println("B1");
}
}
class B2 implements PrintB {
@Override
public void b() {
System.out.println("B2");
}
}
public class Main {
public static void main(String[] args) {
MyClass myClass = new MyClass();
B1 b1 = new B1();
B2 b2 = new B2();
myClass.myMethod(b1);
myClass.myMethod(b2);
}
}
PrintB라는 인터페이스를 선언하고, 이를 구현한 B1, B2를 만들었습니다. 그리고 MyClass가 변화될 수 있는 부분은 PrintB라는 인터페이스를 통해 의존하고 있기 때문에, 변화되는 로직마다 MyClass가 수정될 위험이 없습니다. 즉, 이런 구조를 가지고 있다면, B3 B4 B5 ... 처럼 계속된 변화에도 MyClass 내부적 코드는 전혀 바뀌지 않습니다.
3. 템플릿-콜백 패턴 적용하기
결론부터 말하자면, 템플릿-콜백패턴이란 결국 전략패턴의 변형된 형태라고 봐야합니다. 전략패턴의 기본적 구조에 변화되는 부분을 매번 클래스로 만들지 않고, '익명 내부 클래스' 바로 생성하여 이용합니다. (자바 8버전의 람다식을 이용해서 익명 내부 클래스를 구현하겠습니다.)
class MyClass {
void myMethod(PrintB printB) {
a();
printB.b();
c();
}
void a() {
System.out.println("A");
}
void c() {
System.out.println("C");
}
}
interface PrintB {
void b();
}
public class Main {
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.myMethod(()-> System.out.println("B1"));
myClass.myMethod(()-> System.out.println("B2"));
}
}
myClass객체를 사용하는 Client 쪽에서 (여기 코드상에서의 main), 변화되는 로직을 바로 구현해서 매개변수로 넘겨줍니다. 이런식의 구현 방법을 템플릿-콜백 패턴 이라고 합니다.
템플릿은 뭐고, 콜백은 뭐죠?
템플릿 : '템플릿'이라는 키워드는 개발 영역에서도 자주 사용합니다. (이름이 비슷한 '템플릿'-메소드 패턴이나, view단에서 사용되는 '템플릿' 언어에서도 사용되죠). 템플릿은 정해져 있는 '틀'이라고 생각하시면 됩니다.
콜백 : 콜백은 실행되기 위한 특정 로직을 말합니다. 자바에서는 콜백을 매개변수로 주고 받기 위해 '오브젝트'에 담아서 전달합니다. (예제에선 PrintB를 구현한 오브젝트의 b(); 메소드에 원하는 로직을 담아 전달합니다.)
즉, 템플릿-콜백 패턴이란 정해진 틀안에서, 변화되는 부분만 유동적으로 받아 수행하는 패턴입니다. 위의 예제로 말하자면 템플릿은 'MyClass', 콜백은 B1,B2.. 같은 실행되기 위한 로직을 말합니다.
템플릿-콜백 패턴의 장점
전략패턴은 사용하는 객체와 전략 객체간의 의존성을 설정해주는 팩토리객체가 필요합니다. 그러나 템플릿-콜백 패턴은 팩토리 객체 없이, 해당 객체를 사용하는 메소드에서 인터페이스의 전략을 선택합니다. (그래서 수동 DI, 메소드 수준의 DI 라고도 볼 수 있습니다.)
그러므로, 외부에서 어떤 전략을 사용하는지 감추고 중요한 부분에 집중할 수 있게 됩니다.
템플릿-콜백 패턴의 단점
스프링 프레임워크에서 DI를 사용하지 않게 되면, Bean으로 등록되지 않아 싱글톤 객체가 되지 않습니다.
인터페이스를 사용하지만, 실제 사용할 클래스를 직접 선언하기 때문에 결합도가 증가합니다. (하지만, 결합도를 낮추는 행위는 필요한 곳에만 하면 됩니다. 강한 결합도와 확장성이 무관한 곳에 무리하게 결합도를 고려하지 않아도 된다는 겁니다.)