- 디자인 패턴이란 설계 패턴이다. 스프링은 다양한 설계 패턴을 활용한다. 그렇다면 과연 스프링이란 무엇인가?
- 스프링이란 자바 엔터프라이즈 개발을 편하게 해주는 오픈 소스 경량급 애플리케이션 프레임 워크이다. 책의 필자는 OOP 프레임 워크라고 정의한다.
- 스프링 프레임워크를 이해하는데 도움이 되는 디자인 패턴을 살펴본다
어댑터 패턴(Adapter Pattern)
-
어댑터는 변환기이며 변환기는 서로 다른 두 인터페이스 사이에 통신이 가능하도록 한다
-
어댑터 패턴은 개방폐쇄원칙을 활용한 설계 패턴이며 객체를 속성으로 만들어서 참조하는 디자인 패턴이다
-
호출당하는 쪽의 메서드를 호출하는 쪽의 코드에 대응하도록 중간에 변환기를 통해 호출하는 패턴이라고 할 수 있다
-
장점
- 기존의 클래스를 수정하지 않고도 클라이언트에서 새로운 인터페이스를 사용할 수 있다
- 기존 코드의 재사용을 수월하게 해주고 코드 중복을 줄여준다
- 클래스 간의 결합도를 줄여주어 소스 코드 변경이 필요할 때 쉽게 수정할 수 있다
-
단점
- 어댑터 클래스를 추가로 작성해야 하기 때문에 소스 코드가 늘어난다
- 코드의 복잡성이 증가되며 유지보수를 어렵게 만들수도 있다
- 어댑터가 중간에 데이터를 변환하는 과정에서 추가적인 처리 시간과 오버 헤드(특정 기능을 수행하는데 드는 간접적인 시간, 메모리 등의 자원)가 발생할 수 있다
-
어댑터 패턴이 필요한 경우
- 호환되지 않는 인터페이스를 가진 클래스들이 함께 작동해야 하거나 이미 존재하는 클래스의 인터페이스가 요구 사항과 맞지 않거나 기존 클래스에 원하는 인터페이스가 없는 경우 사용한다
프록시 패턴
프록시란 대리자, 대변인을 뜻하며 누군가를 대신해서 역할을 수행하는 사람이다
-
프록시 패턴을 사용하면 사용하고자 하는 객체를 직접 호출하지 않고 해당 객체를 대변하는 객체를 통해 대상에 접근한다
-
그렇기에 해당 객체가 메모리에 존재하지 않아도 기본적인 정보를 참조하거나 설정할 수 있으며 실제 객체의 필요 시점까지 객체 생성을 미루는 지연 초기화가 가능하다
-
장점
- 원본 객체에 직접 접근하지 않기 때문에 보안성이 향상된다
- 원본 객체에 간접적으로 접근하기 때문에 결합도 낮춰서 유연성이 향상된다
- 지연 초기화를 통해 객체가 필요한 순간에 초기화하여 사용, 불필요한 객체 생성을 줄이기 때문에 성능이 향상된다
-
단점
- 객체 간의 중간 계층이 추가되어 코드의 복잡성이 증가된다
- 프록시 객체를 추가로 사용하기 때문에 객체 생성이 빈번하면 성능 저하가 올 수 있다
-
구현을 위해서는 실제 서비스 객체가 가진 메서드와 똑같은 이름의 메서드를 사용하는데 이를 위해 인터페이스를 사용한다
-
인터페이스를 사용하면서 서비스 객체가 들어갈 자리에 대리자 객체를 투입한다
-
개방 폐쇄 원칙과 의존 역전 원칙이 적용된 설계 패턴이다
싱글톤 패턴
public class Singleton() {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
public void say() {
System.out.println("hi");
}
}
- 인스턴스를 하나만 만들어서 사용하는 패턴으로 두개의 객체를 만들지 않기 위해서 new에 제약을 걸어둔다
- 단일 객체를 반환할 수 있는 메서드가 필요하다
- new 실행이 불가하도록 생성자에 private 접근제어자를 사용한다
- 단일 객체를 참조할 수 있는 정적 참조 변수 필드가 필요하다
- 장점
- 최초 한번의 new 연산자를 통해서 고정된 메모리 영역을 사용하기 때문에 추후 해당 객체에 접근할 때 메모리 낭비를 방지하기 때문에 메모리 낭비를 막을 수 있다
- 이미 생성된 인스턴스를 활용하기 때문에 속도 측면에서 이점이 있다
- 싱글톤으로 구현한 인스턴스는 전역 인스턴스이기 때문에 다른 클래스의 인스턴스들이 데이터를 공유하는 것이 가능하다
- 단점
- 전역 인스턴스이기 때문에 동시성 문제, 자원공유 문제가 발생한다
- 애플리케이션 전역에서 상태를 공유하기 때문에 매번 인스턴스의 상태를 초기화시켜주어야 하며 테스트가 어렵다
- 의존 관계상 클라이언트가 구체 클래스에 의존(new 키워드를 직접 사용하여 클래스 안에서 객체를 생성하기 때문)하기 때문에 SOLID 중 DIP(의존 역전 원칙)을 위반하게 된다 또한 OCP(개방 폐쇄 원칙) 역시 위반할 가능성이 있다
- 자식 클래스를 만들 수 없으며 내부 상태를 변경하기 어려워 유연성이 많이 떨어지는 패턴이다
- 싱글톤 패턴의 사용 목적
- 정보를 보관하고 공유하는 것을 편하게 하기 위해서, 즉 동기화에 용이하기 때문에 사용한다
템플릿 메서드 패턴
- 기능의 뼈대(템플릿)와 실제 구현을 분리하는 패턴
- 중복이 있는 클래스가 있으면 상속을 통해 중복을 상위 클래스로, 달라지는 부분만 하위 클래스로 분할하고 싶어진다
- 상위 클래스에 공통 로직을 수행하는 템플릿 메서드와 하위 클래스에 오버라이딩을 강제하는 추상 메서드 또는 선택적으로 오버라이딩 할 수 있는 Hook 메서드를 두고, 상위 클래스의 템플릿 메서드에서 하위 클래스가 오버라이딩한 메서드를 호출하는 패턴을 템플릿 메서드 패턴이라고 한다
- 즉 상위 클래스에는 어떤 기능의 템플릿을 정의하고 변경되는 로직은 자식 클래스에서 재정의한다
- 상속과 다형성을 통해 변경되는 부분을 관리한다
- DIP(의존 역전 원칙)을 활용하였다
- 단점
- 상속을 사용하기 때문에 하위 클래스는 상위 클래스에 강하게 결합된다
- 상속의 단점을 없애고 템플릿 메서드 패턴과 유사한 장점을 가지는 패턴이 전략 패턴이다
- 참고(https://engineering.linecorp.com/ko/blog/templete-method-pattern)
팩터리 메서드 패턴
- 객체 지향에서 팩터리는 객체를 생성한다
- 팩터리 메서드란 객체를 생성해서 반환하는 메서드를 말한다
- 팩터리 메서드 패턴이란 하위 클래스에서 팩터리 메서드를 오버라이딩 해서 객체를 반환하게 하는 것을 말한다
- 즉, 오버라이드 된 메서드가 객체를 반환하는 패턴이다
- 하위 클래스에서 어떤 객체를 생성할지 결정하게 되며 객체 생성 처리를 서브 클래스로 분리해 처리하도록 캡슐화하는 패턴이라고도 할 수 있다
- DIP(의존 역전 원칙)을 활용한다
- 직접 객체를 생성해 사용하는 것을 방지하고 하위 클래스에 위임하기 때문에 의존성을 제거하여 결합도를 낮춘다
전략 패턴
- 전략 패턴은 여러 전략을 쉽게 교환할 수 있도록 만드는 패턴이다
- 템플릿 메서드 패턴은 변경되는 부분을 부모 클래스의 추상 메서드로 만들었지만 전략 패턴은 이를 인터페이스로 완전히 분리하였다
- 즉 템플릿 메서드는 상속을 통해 문제를 해결하지만 전략 패턴은 객체 주입을 통해 해결한다
- 전략 패턴에는 세가지 요소가 필요하다
- 전략 메서드를 가진 전략 객체
- 전략 객체를 사용하는 컨텍스트(전략 객체 사용자/소비자)
- 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트(제3자, 전략객체 공급자)
- 클라이언트가 전략을 생성해 전략을 실행할 컨텍스트에 주입(파라미터로 넘겨줌)하는 패턴이다
- DIP(의존 역전 원칙)과 OCP(개방 폐쇄 원칙)을 활용하였다
- 전략 패턴의 구현 방식
- 전략을 컨텍스트의 필드에 선언하여 컨텍스트 클래스가 생성될 때 초기화되도록 했다
- 선 조립, 후 실행이라고 부르며 이는 필요한 전략이 실행 시점에 반드시 존재한다는 것을 보장받을 수 있다
- 단점으로는 전략을 변경하기 위해서는 컨텍스트 객체를 새로 만들어야 하기 때문에 들어가는 비용이 크다
- 위 단점을 해결하기 위해서 전략의 위치를 컨텍스트의 필드가 아닌 실제로 사용하는 메서드의 매개변수로 받을 수 있다
- 이 경우 비용은 줄일 수 있으나 전략이 존재하는지에 대해서 보장받을 수 없다
- 한 전략의 구현체가 여러곳에서 사용하는 것이 아닌 한 곳에서만 필요한 경우라면 클래스 파일로 따로 분리해서 선언하는 것보다 익명 클래스 또는 람다를 사용해서 구현할 수 있다
템플릿 콜백 패턴
- 전략 패턴의 변형
- 스프링의 DI(의존성 주입)에서 사용하는 특별한 형태의 전략 패턴
- 전략을 익명 내부 클래스로 정의해서 사용한다
- DIP(의존 역전 원칙), OCP(개방 폐쇄 원칙)을 활용한다
- 콜백 코드는 호출한 곳에서 실행되지 않고 호출을 요청한 곳에서 실행되는 코드를 말한다
질문사항
어댑터 패턴 vs 프록시 패턴
- 두 개의 객체를 이어준다는 역할적인 측면에서 두 패턴은 유사하다
- 어댑터 패턴은 서로 다른 인터페이스를 맞춰주는 반면 프록시는 동일한 인터페이스를 유지
- 어댑터 패턴은 인터페이스를 전환하는 것이 목적이며 프록시 패턴은 객체에 대한 접근을 제어하는 것이 목표
전략 패턴 vs 템플릿 메서드 패턴
- 전략 패턴과 템플릿 메서드 패턴은 사용 목적으로 구분이 가능하다
- 전략 패턴은 알고리즘을 숨기고 쉽게 교체할 수 있게 만드는 데에 목적이 있으며 템플릿 메서드 패턴은 알고리즘의 특정 단계만 제어하고 싶을 때 사용한다
팩터리 메서드 패턴 vs 템플릿 메서드 패턴
- 팩터리 메서드 패턴과 템플릿 메서드 패턴의 공통점은 추상 메서드를 정의하고 서브 클래스가 추상 메서드를 구현화한다는 점이다
- 둘의 차이점으로는 팩터리 메서드 패턴의 구현 대상은 객체 생성 알고리즘이고 템플릿 메서드 패턴은 실행할 전략 알고리즘이다