스프링이 사랑한 디자인 패턴(1)

디자인 패턴은 실제 개발 현장에서 비즈니스 요구 사항을 프로그래밍으로 처리하면서 만들어진 다양한 해결책 중에서 많은 사람들이 인정한 베스트 프랙티스를 정리한 것이다. 디자인 패턴은 당연히 객체 지향 특성과 설계 원칙을 기반으로 구현돼 있다.

디자인 패턴은 객체 지향의 특성 중 상속(extends), 인터페이스(interface/implements), 합성(객체를 속성으로 사용)을 이용한다.

1. 어댑터 패턴(Adapter Pattern)

어댑터 패턴은 개방 폐쇄 원칙을 활용한 설계 패턴을 활용한 설계 패턴이라고 할 수 있다.

먼저 어댑터 패턴이 적용되지 않은 코드를 보자.

ServiceA.java

package adapterPattern;

public class ServiceA {
    voic runServiceA() {
        System.out.println("ServiceA");
    }
}

ServiceB.java

package adapterPattern;

public class ServiceB {
    voic runServiceB() {
        System.out.println("ServiceB");
    }
}

ClientWithNoAdapter.java

package adapterPattern;

public class ClientWithNoAdapter {
    public static void main(String[] args) {
        ServiceA sa1 = new ServiceA();
        ServiceB sb1 = new ServiceB();
        
        sa1.runServiceA();
        sb1.runServiceB();
    }
}

main() 메서드를 살펴보면 sa1 참조 변수와 sb1 참조 변수를 통해 호출하는 각 메서드가 비슷한 역할을 하지만 메서드명이 다르다는 것을 알 수 있다.

이제 어댑터를 적용해 메서드명을 통일해보자. ServiceA에 대한 변환기와 ServiceB에 대한 변환기를 아래 처럼 추가한다.

AdapterServiceA.java

package adapterPattern;

public class AdapterServiceA {
    ServiceA sa1 = new ServiceA();
	
    voic runService() {
        sa1.runServiceA();
    }
}

AdapterServiceB.java

package adapterPattern;

public class AdapterServiceB {
    ServiceB sb1 = new ServiceB();
	
    voic runService() {
        sb1.runServiceB();
    }
}

위의 예제는 기존의 ServiceA와 ServiceB의 메서드를 runService()라고 하는 같은 이름의 메서드로 호출해서 사용할 수 있게 해주는 변환기이다. 이제 ClientWithAdapter라는 클래스를 추가하고 main() 메서드에서 이 변환기들을 사용해보자.

ClientWithAdapter.java

package adapterPattern;

public class ClientWithAdapter {
    public static void main(String[] args) {
        AdapterServiceA asa1 = new AdapterServiceA();
        AdapterServiceB asb1 = new AdapterServiceB();
        
        asa1.runService();
        asb1.runService();
    }
}

클라이언트(ClientWithAdapter)가 변환기를 통해 runService()라는 동일한 메서드명으로 두 객체의 메서드를 호출하는 것을 볼 수 있다.

또한 어댑터 패턴은 합성, 즉 객체를 속성으로 만들어 참조하는 디자인 패턴으로, 한 문장으로 정리하면 다음과 같다.

"호출당하는 쪽의 메서드를 호출하는 쪽의 코드에 대응하도록 중간에 변환기를 통해 호출하는 패턴"

2. 프록시 패턴(Proxy Pattern)

프록시는 대리자, 대변인이라는 뜻을 가진 단어이다. 먼저 대리자를 사용하지 않고 직접 호출하는 구조를 살펴보자.

Service.java

package proxyPattern;

public Class Service {
    public String runSomthing() {
        return "서비스 짱!!!";
    }
}

프록시패턴을 적용하지 않은 ClientWithNoProxy.java

package proxyPattern;

public Class ClientWithNoProxy {
    public static void main(String[] args) {
        // 프록시를 이용하지 않은 호출
        Service service = new Service();
        System.out.println(service.runSomthing());
    }
}

이번에는 프록시 패턴이 적용된 경우를 살펴보자. 프록시 패턴의 경우 실제 서비스 객체가 가진 메서드와 같은 이름의 메서드를 사용하는데, 이를 위해 인터페이스를 사용한다. 인터페이스를 사용하면 서비스 객체가 들어갈 자리에 대리자 객체를 대신 투입해 클라이언트 쪽에서는 실제 서비스 객체를 통해 메서드를 호출하고 반환값을 받는지, 대리자 객체를 통해 메서드를 호출하고 반환값을 받는지 전혀 모르게 처리할 수 있다.

코드를 통해 프록시 패턴의 구성을 살펴보자.

IService.java

package proxyPattern;

public interface IService {
    String runSomthing();
}

IService 인터페이스를 구현한 Service.java

package proxyPattern;

public Class Service implements IService {
    public String runSomthing(){
        return "서비스 짱!!!";
    }
}

IService 인터페이스를 구현한 Proxy.java

package proxyPattern;

public Class Proxy implements IService {
    IService serivce1;
    
    public String runSomthing(){
        System.out.println("호출에 대한 흐름 제어가 주목적, 반환 결과를 그대로 전달");
        
        service1 = new Service();
        return service1.runSomthing();
    }
}

프록시를 사용하는 ClientWithProxy.java

package proxyPattern;

public Class ClientWithProxy {
    public static void main(String[] args) {
        // 프록시를 이용한 호출
        IService proxy = new Proxy();
        System.out.println(proxy.runSomthing());
    }
}

이제 프록시 패턴의 중요 포인트를 짚어보자.

  • 대리자는 실제 서비스와 같은 이름의 메서드를 구현한다. 이때 인터페이스를 사용한다.
  • 대리자는 실제 서비스에 대한 참조 변수를 갖는다. (합성)
  • 대리자는 실제 서비스의 같은 이름을 가진 메서드를 호출하고 그 값을 클라이언트에게 돌려준다.
  • 대리자는 실제 서비스의 메서드 호출 전후에 별도의 로직을 수행할 수도 있다.

프록시 패턴은 실제 서비스 메서드의 반환값에 가감하는 것을 목적으로 하지 않고 제어의 흐름을 변경하거나 다른 로직을 수행하기 위해 사용한다. 프록시 패턴을 한 문장으로 변경해보자.

"제어 흐름을 조정하기 위한 목적으로 중간에 대리자를 두는 패턴"

위의 예제의 구조를 보면 프록시 패턴이 인터페이스를 중간에 두고 이에 의존하기 때문에 이를 구현한 클래스들로 교체해주어도 변경을 받지 않는 개방 폐쇄 원칙(OCP)와 의존 역전 원칙(DIP)을 적용한 설계 패턴이라는 것을 알 수 있다.

3. 데코레이터 패턴(Decorator Pattern)

데코레이터 패턴은 프록시 패턴과 구현 방법이 같다. 다만 프록시 패턴은 클라이언트가 최종적으로 돌려 받는 반환값을 조작하지 않고 그대로 전달하는 반면 데코레이터 패턴은 클라이언트가 받는 반환값에 장식을 덧입힌다.

프록시 패턴

  • 제어의 흐름을 변경하거나 별도의 로직처리를 목적으로 한다.
  • 클라이언트가 받는 반환값을 특별한 경우가 아니면 변경하지 않는다.

데코레이터 패턴

  • 클라이언트가 받는 반환값에 장식을 더한다.

데코레이터 패턴을 적용한 코드는 다음과 같다.

IService.java

package decoratorPattern;

public interface IService {
    public abstract String runSomthing();
}

IService 인터페이스를 구현한 Service.java

package decoratorPattern;

public Class Service implements IService {
    public String runSomthing(){
        return "서비스 짱!!!";
    }
}

IService 인터페이스를 구현한 Decorator.java

package decoratorPattern;

public Class Decorator implements IService {
    IService service;
    
    public String runSomthing(){
        System.out.println("호출에 대한 장식 주목적, 클라이언트에게 반환 결과에 장식을 더하여 전달");
        
        service = new Service();
        return "정말" + service.runSomething();
    }
}

데코레이터를 사용하는 ClientWithDecorator.java

package decoratorPattern;

public Class ClientWithDecolator {
    public static void main(String[] args) {
        IService decorator = new Decorator();
        System.out.println(decorator.runSomthing());
    }
}

데코레이터 패턴의 중요 포인트를 짚어보자. 반환값에 장식을 더한다는 것을 빼면 프록시 패턴과 동일하다.

  • 정삭자는 실제 서비스와 같은 이름의 메서드를 구현한다. 이때 인터페이스를 사용한다.
  • 장식자는 실제 서비스에 대한 참조 변수를 갖는다. (합성)
  • 장식자는 실제 서비스의 같은 이름을 가진 메서드를 호출하고 그 반환값에 장식을 더해 클라이언트에게 돌려준다.
  • 장식자는 실제 서비스의 메서드 호출 전후에 별도의 로직을 수행할 수도 있다.

마지막으로 데코레이터 패턴을 한 문장으로 정리하면 다음과 같다.

"메서드 호출의 반환값에 변화를 주기 위해 중간에 장식자를 두는 패턴"

데코레이터 패턴이 프록시 패턴과 동일한 구조를 갖기에 데코레이터 패턴도 개방 폐쇄 원칙(OCP)와 의존 역전 원칙(DIP)이 적용된 설계 패턴임을 알 수 있다.

참고

  • 스프링 입문을 위한 자바 객체지향의 원리와 이해

좋은 웹페이지 즐겨찾기