템플릿 메서드 패턴은 생각보다 우리 주변에서 굉장히 많이 사용되고 있습니다.
그 중에서 백엔드에서 스프링을 사용한다면 데이터베이스와 연동해서 어떠한 작업을 할 때 JdbcTemplate 라고 들어봤을 거에요.
JdbcTemplate는 템플릿 메서드 패턴의 단점을 해결한 전략패턴과 스프링의 템플릿 메서드 콜백 패턴을 이용한 방식입니다.
템플릿 메서드 패턴이란
GOF에서 말하는 템플릿 메서드 패턴의 정의는 다음과 같습니다.
알고리즘의 골격을 정의하고 일부 단계를 하위 클래스로 이전한다.
하위 클래스가 알고리즘의 구조를 변경하지 않고 알고리즘의 특정 단계를 재정의 할 수 있다.
어떤 얘기인지 잘 이해가 안될텐데 다음 예시 코드를 한 번 살펴봅시다.
@Test
void templateMethodV0() {
logic1();
logic2();
}
private void logic1() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
log.info("비즈니스 로직1 실행");
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
private void logic2() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
log.info("비즈니스 로직2 실행");
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
코드에서 코드의 실행 시간을 표현하는 부분의 코드는 똑같지만, 비즈니스 로직을 실행하는 부분이 다릅니다.
여기서 객체지향의 좋은 설계 원칙 중에 하나를 알 수 있는데 다음과 같습니다.
바뀌는 부분과 바뀌지 않는 부분을 분리하여 캡슐화한다.
정의에 따라 위의 코드를 살펴보면 바뀌지 않는 부분은 코드의 실행시간을 표현하는 부분의 코드이고, 바뀌는 부분은 비즈니스 로직입니다. 이제 위에서 템플릿 메서드 패턴의 정의에 적용해봅시다. 알고리즘의 골격이란 위처럼 바뀌지 않는 부분에 대해서 얘기하는 것이고, 일부 단계란 바뀌는 부분을 얘기하는 것입니다.
템플릿 메서드 패턴 구현
템플릿 메서드 패턴을 구현 할 때에는 주로 execute()와 call()을 사용해서 구현합니다.
위의 코드를 템플릿 메서드 패턴을 적용할 수 있는데 적용하면 다음과 같이 구현을 할 수 있습니다.
public abstract class AbstractTemplate {
public void execute() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
call(); // 상속
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
protected abstract void call();
}
추상 클래스를 선언하여, execute()에 기존의 알고리즘 군의 골격을 적어놓습니다.
다만, 비즈니스 로직이 실행하는 부분이 달라지기 때문에 call 메소드는 추상 메서드로 선언하여 실행 될 수 있도록 합니다.
추상 클래스는 무조건 어딘가에서 상속을 받아서 구현해야 사용할 수 있기 때문에, 추상 메서드가 아닌 메서드에서 추상 메서드를 호출하더라도 문제가 없습니다. 만약 이 부분이 제대로 이해가 되지 않는다면, 추상 클래스에 대해서 따로 학습을 하는 것이 좋을 것 같습니다.
템플릿 메서드 패턴의 골격을 정하기 위해 추상 클래스를 사용했으니 해당 템플릿 메서드 패턴을 결국에 사용하려면 추상 클래스를 구현을 해야겠지요?
public class SubClassLogic1 extends AbstractTemplate {
@Override
protected void call() {
log.info("비즈니스 로직1 실행");
}
}
public class SubClassLogic2 extends AbstractTemplate {
@Override
protected void call() {
log.info("비즈니스 로직2 실행");
}
}
이렇게 2개의 추상클래스를 상속받는 클래스를 만들어주었고, 추상 메서드를 구현한 것을 볼 수 있습니다.
템플릿 메서드의 장점
위에서 보게 된 예시 코드를 템플릿 메서드 패턴을 적용하면 어떤 모습이 나올지 한 번 살펴봅시다.
@Test
void templateMethodV1() {
AbstractTemplate template1 = new SubClassLogic1();
template1.execute();
AbstractTemplate template2 = new SubClassLogic2();
template2.execute();
}
위에서 길었던 코드가 정말 간단하게 줄어든 모습을 볼 수 있습니다.
만약 똑같은 패턴인데 패턴을 바꿔야 한다면, 템플릿 메서드 패턴을 적용하지 않았을 때 해당 패턴들을 찾아 코드를 하나 하나 일일히 변경을 해주어야 했을 것입니다. 하지만, 우리는 템플릿 메서드 패턴을 적용해서 알고리즘의 골격을 정해주었기 때문에, 템플릿 메서드의 추상 클래스에서 알고리즘 골격에 해당 하는 코드 하나만 수정하면 다른 부분에 다 동일하게 적용됨을 알 수가 있습니다.
어디선가 많이 들어본 것 같지요? 바로 객체지향의 좋은 설계 원칙 SOLID 중에 하나인 S 단일 책임 원칙을 준수 할 수 있게 되었습니다.
참고로 단일 책임 원칙이란, 객체는 단 하나의 책임만 가져야 한다는 원칙입니다.
해당 부분이 실제로 객체를 생성하는 과정에서는 하나의 책임만 가지는 것 같은데, 단일 책임 원칙이 준수됐는지 안됐는지는 수정을 할 때 잘 드러납니다. 만약에 어떠한 서비스 로직을 수정하는 과정에서 여러 객체들에 대한 코드를 수정해야 하는 상황이 발생한다면, 이는 단일 책임 원칙에 위배 된 것이지요. 사실 이 부분을 추상화해서 바로 적용하긴 쉽지 않습니다. 리팩토링 해나가는 과정에서 지킬 수 있도록 하는 것이 중요합니다.
템플릿 메서드 패턴을 사용한 모습을 볼 수 있었는데요. 보다보니 다음과 같은 생각이 들 수 있습니다.
그럼 템플릿 메서드 패턴을 사용하기 위해 일부 단계들에 대해서 추상 클래스를 다 상속받아서 구현을 해야 하는 건가?
만약 해당 템플릿 메서드 패턴을 다른 부분에서 똑같은 코드를 사용해야 한다면 구현을 해놓는 것이 좋을 것입니다.
하지만, 만약에 이번 한 번만 실행하고 다시 사용되지 않는다면 다시 구현하는 것은 무리가 있겠지요.
우리는 객체지향의 특징은 재사용과 유지보수를 용이하게 하기 위해서 객체를 생성하여 재사용 할 수 있도록 하는데, 만약 한 번 쓰고 사용되지 않음에도 불구하고 생성한다면, 어떠한 패키지안에 한 번 사용하고 다시 사용하지 않을 더미 클래스들이 쌓이게 될 것 입니다.
따라서, 이런 문제를 해결하기 위한 방식 중 익명 클래스를 활용하는 방법이 있습니다.
익명 클래스에 대한 설명은 해당 포스팅 에서 확인 할 수 있습니다.
이제 익명 클래스를 적용한 테스트 코드를 한 번 살펴보도록 합시다.
@Test
void templateMethodV2() {
AbstractTemplate template1 = new AbstractTemplate() {
@Override
protected void call() {
log.info("비즈니스 로직1 실행");
}
};
template1.execute();
AbstractTemplate template2 = new AbstractTemplate() {
@Override
protected void call() {
log.info("비즈니스 로직2 실행");
}
};
template2.execute();
}
위에서 얘기한대로 객체를 생성하지 않아, 더미 클래스들이 쌓이는 문제점은 사라졌습니다. 하지만 코드에 대한 가독성이 좀 떨어지는 문제가 있습니다. 이것만 보면 "아 나는 그냥 클래스를 생성해서 쓸래." 라는 생각이 들 수도 있겠지만, 장기적인 관점으로 봤을 때 더미 클래스가 100 ~ 120개가 쌓이게 된다면, 파일 사이즈가 커지므로 그거 나름대로 문제가 많을 것 입니다.
따라서 이 부분은 프로젝트의 크기가 작아서 만약에 더미 클래스가 많이 생성될 것 같지 않으면 가독성을 위해 생성하는 것을 택하고, 만약 프로젝트의 규모가 크다면 클래스들이 상당히 많을 것이기 때문에 더미 클래스를 생성하는 일은 자제하는 것이 좋을 것 같습니다.
즉, 장단점을 알고 상황에 따라 달리 선택을 하는 것이 중요 할 것 같습니다.
템플릿 메서드 패턴의 단점
템플릿 메서드 패턴이 무조건 좋고 만능은 아닙니다. 바로 다음과 같은 단점이 존재하기 때문인데요.
상속받은 클래스는 부모의 기능을 추가적으로 사용하지 않는데, 부모 클래스의 기능을 무조건 사용해야 하는 상황이 발생합니다. 결국 템플릿 메서드의 추상 클래스를 상속 받은 자손 클래스는 부모 클래스의 알고리즘 골격을 수정할 수도 없고, 부모 메서드의 기능을 추가적으로 사용하지 않는 상황이지만 무조건 상속을 받아야 하는 상황이 된 것 입니다. 결론적으로 자손 클래스가 부모 클래스에게 강하게 의존을 하게 된 상황이지요. 강하게 의존을 하게 되면 다음과 같은 상황이 발생 할 수 있습니다. 예를 들어서, 추상 클래스에 일부 알고리즘 요소를 추가적으로 구현해야 한다면 해당 요소를 또 추상 메서드로 구현하고 execute()에서 실행 될 수 있도록 추가해야하는데, 이렇게 되면 추상 클래스를 구현한 모든 자손들이 일부 알고리즘 요소를 구현해야 하는 상황에 직면하게 됩니다. 따라서 이렇게 부모에게 강하게 의존하는 설계는 좋은 설계가 아니지요.
객체지향의 좋은 설계 원칙 중에는 추가적으로 다음과 같은 특징이 있습니다.
상속보다는 구성(Composite)을 활용한다.
템플릿 메서드 패턴의 이러한 상속에 대한 단점을 구성으로 변경하여 사용하는 방법이 있는데, 이러한 디자인 패턴이 전략패턴입니다.
전략 패턴은 추후에 추가적으로 올려보도록 하겠습니다.
'Language' 카테고리의 다른 글
[UML] 클래스 다이어그램 (2) | 2023.05.20 |
---|---|
gradle build vs Intellij IDEA build (0) | 2023.05.20 |
Lombok ToString 순환참조 방지하기 (1) | 2023.05.12 |
전략 패턴 (0) | 2023.04.16 |