토픽 : Design Pattern
소프트웨어 공학론 안의 좋은 코드를 설계하기 위한 일종의 디자인(설계) 패턴.
객체 간 응집도는 높이고, 결합도는 낮게.
요구 사항 변경 시, 코드 변경을 최소화 하는 방향으로.
즉, 프로그램을 개발하는 과정에서 빈번하게 발생하는 디자인(설계) 상의 문제를 정리해서 상황에 따라 간편하게 적용해서 쓸 수 있는 패턴이다.
디자인 패턴을 사용하면 아래와 같은 효과를 얻을 수 있기 때문에 필요하다.
개발자간의 원할한 의사소통
: 개발자들이 여러 가지 디자인 패턴을 알고있다면 문제가 발생했을때 그것을 해결하기위해 어떤 디자인 패턴을 사용할 것인지 논의할 수 있다.
소프트웨어 구조 파악 용이
: 디자인 패턴을 잘 알고 있다면 그 디자인 패턴이 적용된 소프트웨어의 전체적인 구조를 쉽게 파악이 가능하다.
재사용성을 통한 개발 기간 단축
: 이미 만들어진 디자인 패턴을 사용하면 처음부터 만들지 않아 개발 기간을 단축할 수 있다.
설계 변경 요청에 대한 유연한 대처
: 디자인 패턴을 사용하여 설계하면 각종 버그나 기능 추가, 운영체제 호환성 등의 이유로 설계를 변경하거나 유지보수를 할 때 짧은 시간 내 완료할 수 있다.
GoF(Gang of Four)에서는 23가지 디자인 패턴을 3가지 유형으로 분류한다.
A. 생성 패턴(Creational Patterns)
B. 구조 패턴(Structural Patterns)
C. 행동 패턴(Behavioral Patterns)
A. Spring - AOP
: 구조 패턴(Structural Patterns) > 프록시 패턴(Proxy Pattern)
어떤 객체를 직접적으로 참조하는 것이 아니라, 그 객체를 대행(대리,proxy)하는 객체를 통해 대상객체에 접근하는 방식이다.
이 방식을 사용하면 실제 대상 객체가 메모리에 존재하지 않아도 기본적인 정보를 참조하거나 설정할수 있고, 실제 객체의 기능이 반드시 필요한 시점까지 객체의 생성을 미룰 수 있다.
프록시 객체와 실제 객체는 같은 인터페이스를 구현하여, 프록시 객체는 실제 객체와 치환이 가능하다.
가상 프록시
사이즈가 큰 객체가 로딩되기 전에도 proxy를 통해 참조를 할 수 있다.
이미지, 동영상 등의 로딩에서 반응성 혹은 성능이 향상된다.
보호 프록시
실제 객체의 public, protected 메소드들을 숨기고 인터페이스를 통해 노출시킬수 있다.
안전성이 보장된다.
원격 프록시
로컬에 있지 않고 떨어져 있는 객체를 사용할 수 있다.
예) RMI, EJB 의 분산처리
스마트 프록시
원래 객체의 접근에 대해서 사전처리를 할수 있다.
예) 객체의 레퍼런스 카운트 처리
변형된 가상 프록시
원본 객체의 참조만 필요할때는 원복객체를 사용하다가, 최대한 늦게 복사가 필요한 시점에 원본객체를 예) Concurrent package의 CopyInWriteArrayList
객체를 생성할때 한단계를 거치게 되므로, 빈번한 객체 생성이 필요한 경우 성능이 저하될 수 있다.
프록시 내부에서 실제 객체 생성을 위해서 thread가 생성되고 동기화가 구현되야 하는 경우 성능이 저하되고 로직이 난해해진다.
스프링에서는 AOP(Aspect Oriented Programming)에 기반해 수많은 로깅처리, 트랜잭션 처리를 하고 있다. Spring의 AOP는 CGLIB이라는 런타임 프록시 생성 라이브러리를 사용해, AOP를 위한 프록시 객체를 생성한다. xml과 AspectJ 스크립트를 사용해 대상객체들에 대한 프록시를 생성하므로 코드로 적을수는 없다. 참고로 CGLIB을 사용하면 프록시의 대상이 되는 객체가 인터페이스를 구현안했더라도 프록시 방식으로 대상 객체의 메소드를 사용할수 있다.
B. 스프링의 XML 및 configuration 파일안에 설정된 Bean
: 생성 패턴(Creational Patterns) > 싱글턴 패턴(Singleton pattern)
자원이 단지 하나만 필요한 객체들이 있다.
그 객체는 다양한 클래스와 쓰레드 컨택스트 내에서 공유되어 사용될수 있어야 한다.
예를 들어 현재 JVM에 할당받은 힙메모리 크기를 가져오고 싶다고 할때, 여러 개의 객체를 만들 필요가 없다. 왜냐하면 어플리케이션 입장에서 바라보는 JVM은 단지 하나일 뿐이기 때문이다. 이렇게 단일 리소스 환경에 대해서 객체로서 표현하는 방법이 싱글턴(Singleton)이다.
스프링은 빈을 등록할 때 범위(scope)를 지정할 수 있는데 디폴트가 싱글턴(Singleton) 이다.
그 외에도 prototype, request, session 이 있습니다.
스프링에서 싱글턴을 저장하고 관리해주는 역할은 applicationContext 하는데,
Singleton Registry, IOC 컨테이너, 스프링 컨테이너, 빈 팩토리 등으로 불린다.
스프링의 핵심 컨테이너의 빈 관리를 담당하는 BeanFactory 의 핵심 구현 클래스는 DefaultListableBeanFactory이다.
대부분의 애플리케이션 컨텍스트는 바로 이 클래스를 BeanFactory 로 사용하는데, 핵심 구현 클래스인 DefaultListableBeanFactory 가 구현하고 있는 인터페이스의 한가지가 바로 SingletonRegistry 이다.
스프링에서 하나의 요청을 처리하기 위해서는 Presentation Layer, Business Layer, Data Access Layer 등 다양한 기능을 담당하는 객체들이 계층형을 이루고 있는데, 클라이언트 요청 마다 각 로직을 담당하는 객체를 만들어 사용한다면, GC가 있더라도 메모리 부하가 올 수 있다.
이 때문에 엔터프라이즈 분야에서는 서비스 오브젝트(Service Object) 라는 개념을 사용해 왔는데 서블릿은 Java 엔터프라이즈 기술의 가장 기본이 되는 서비스 오브젝트라고 할 수 있다.
서블릿은 대부분의 멀티 스레딩 환경에서 싱글턴을 동작하며, 서블릿 클래스 하나당 하나의 객체를 생성하여, 클라이언트 요청 처리를 담당하는 스레드 들이 해당 객체를 공유해서 사용한다.
스프링은 어노테이션 설정만으로 IoC 컨테이너(applicationContext) 에 제어권을 넘겨줌으로써 손쉽게 빈(Bean)을 싱글턴으로 생성하여 사용할 수 있다.
Component-scan 대상이 되는 어노테이션들 @Repository, @Service, @Controller, @Component 등 을 사용하면 된다.
C. 스프링의 XML 및 configuration 파일안에 설정된 Bean
: 행동 패턴(Behavioral Patterns) > 템플릿 메서드 패턴 (Template method pattern)
잘 구현되어 있는 알고리즘을 상황에 맞게 조금씩 다르게 확장해야 하는 경우에 유용한 패턴이다.
완성되지 않는 뼈대(Skeleton) 라는 개념으로, 뼈대에 피와 살을 붙여 살아있는 로직이 된다.
이렇게 하기 위해 하나의 객체를 추상클래스와 구현클래스로 나눈다.
로직의 기본 골격이 되는 메인 부분은 추상클래스의 일반 메소드로 둔다.(템플릿 메소드)
구현클래스별로 달라질수 있는 부분의 로직에 대해서만 구현클래스에 둔다.
결과적으로 유저가 추상클래스의 템플릿 메소드를 호출하면, 템플릿 메소드는 주 로직들을 처리하다가
구현별로 달라지는 부분에서 구현클래스의 함수를 호출하는 방식으로 구현된다.
유저 -> 추상클래스의 템플릿 메소드 -> 구현클래스의 abstract에 대한 구현 메소드
일반적인 클래스 상속구조에서는 구현클래스에서 추상클래스의 메소드를 이용하는 경우가 많은데, 템플릿 메소드에서는 반대로 추상클래스에서 구현클래스의 메소드를 참조한다.
전략패턴에서 알고리즘부분을 외부객체를 통해 수행하는 것과는 정반대의 방식이라고 볼수 있다.
따라서 핵심로직 처리 부분은 부모 클래스에 대부분 구현되어있고, 상속받은 자식 클래스는 최종적인 부분만 각기다르게 구현하는 방식이다.
jdbcTemplate는 커넥션 완전히 정리하기, 결과반복, 예외처리, 트랜잭션 처리등의 복잡하고 세부적인 사항들을 스프링 내부에서 알아서 처리해주는 코드가 미리 들어가 있고, 또한 다양한 generic을 지원한다.
최종적인 db binding( SELECT, UPDATE, INSERT, DELETE등의 쿼리를 전달하는 부분)을 jdbcTemplate이 템플릿이 호출한다.
객체의 상태 변화를 관찰하는 관찰자(옵저버) 목록을 객체에 등록하여 상태 변화가 있을 때마다
메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다.
주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다.
Observer 인터페이스 정의
public interface Observer {
public void update(String title, String news);
}
인터페이스 내에 update 메서드를 정의한다.
Publisher 인터페이스 정의
public interface Publisher {
public void registerObserver(Observer observer);
public void removeObserver(Observer observer);
public void notifyObservers();
}
인터페이스 내에 아래 3가지 메서드를 정의하여 publisher이 행동 할 수 있는 것들을 명시한다.
NewsPublisher 클래스 구현
public class NewsPublisher implements Publisher{
private ArrayList<Observer> observers;
private String title;
private String news;
public NewsPublisher() {
observers = new ArrayList<>();
title = null;
news = null;
}
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for(Observer observer : observers) {
observer.update(title, news);
}
}
public void setNews(String title, String news) {
this.title = title;
this.news = news;
notifyObservers();
}
}
Publisher 인터페이스를 직접 이용하는 클래스로 observers를 ArrayList에 모을 수 있게 한다.
NewsSubscriber 클래스 구현
public class NewsSubscriber implements Observer{
private String observerName;
private String news; private
Publisher publisher;
public NewsSubscriber(String subscriber, Publisher publisher) {
this.observerName = subscriber;
this.publisher = publisher;
publisher.registerObserver(this);
}
@Override
public void update(String title, String news) {
this.news = title + "!!! " + news; display();
}
private void display() {
System.out.println("=== " + observerName + " 수신 내용 ===\n" + news + "\n");
}
}
Observer 인터페이스를 직접 이용하는 클래스.
NewsSubscriber이 생성될 때 자신의 publisher이 누구인지 지정해준다.
그때 자신의 publisher의 registerObserver에 의해 등록이 될 것이고
추후 publisher의 notifyObservers()에서 publisher의 모든 observer가 내용을 전달받게 된다.
ObserverPatternMain 클래스 구현
public class ObserverPatternMain {
public static void main(String[] args) {
NewsPublisher newsPublisher = new NewsPublisher();
NewsSubscriber newsSubscriber1 = new NewsSubscriber("옵저버1", newsPublisher);
NewsSubscriber newsSubscriber2 = new NewsSubscriber("옵저버2", newsPublisher);
newsPublisher.setNews("특보", "옵저버 패턴이 만들어졌습니다.");
newsPublisher.setNews("정정", "옵저버 패턴으로 내용이 정정됨을 알립니다.");
newsPublisher.removeObserver(newsSubscriber1);
newsPublisher.setNews("속보", "누군가 구독 해제를 했습니다.");
}
}
실행 결과