본문 바로가기
객체 지향 프로그래밍/스프링 입문을 위한 자바 객체 지향의 원리와 이해

[스프링 입문을 위한 자바 객체 지향의 원리와 이해] 스프링이 사랑한 디자인 패턴: 어댑터, 프록시, 데코레이터, 싱글턴, 템플릿 메서드, 팩터리 메서드, 전략, 템플릿 콜백

by 책 읽는 개발자_테드 2021. 10. 26.
반응형

학습목표

· 디자인 패턴이란?

· 어댑터 패턴

· 프록시 패턴

· 데코레이터 패턴

· 싱글턴 패턴

· 템플릿 메서드 패턴

· 팩터리 메서드 패턴

· 전략 패턴

· 템플릿 콜백 패턴

· 스프링이 사랑한 다른 패턴들


디자인 패턴이란?

· 프로그램을 작성하다 보면 비슷한 상황에 직면하게 되는 경우가 많은데,

  그러한 상황에서 이전의 많은 개발자들이 고민하고 정제한 사실상의 표준 설계 패턴

· 실제 개발 현장에서 비즈니스 요구 사항을 프로그래밍으로 처리하면서

  만들어진 다양한 해결책 중에서 많은 사람들이 인정한 베스트 프랙티스를 정리한 것

· 객체 지향을 도구로, 설계 원칙(SOLID)은 도구를 올바르게 사용하는 방법으로, 디자인 패턴은 레시피로 비유할 수 있음 

 

· 스프링은 객체 지향의 특정과 설계 원칙을 극한까지 적용한 프레임워크로, 

  스프링을 공부하다보면 자엽스럽게 객체 지향 설계의 베스트 프랙티스인 디자인 패턴을 만날 수 있고,

  이 때문에 스프링은 'OOP 프레임워크'라고 불림

 

어댑터 패턴

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

   - 객체를 속성으로 만들어서 참조하는 디자인 패턴(합성)

 

· SOLID의 개방 폐쇄 원칙(OCP)을 활용한 설계 패턴

· 어댑터: 변환기(converter)

     - 변환기의 역할은 서로 다른 두 인터페이스 사이에 통신이 가능하게 하는 것 ex) 충전기 (핸드폰-전원 콘센트 사이 통신)

     - 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있게됨

     - 인터페이스: 서로 다른 두 개 이상의 독립된 컴퓨터 시스템 구성 요소 간에 정보를 교환하는 공유 경계

· 사용예: 다양한 데이터베이스 시스템을 단일한 인터페이스로 조작할 수 있게 해주는 JDBC, 

              다양한 플랫폼의 프로그램을 단일한 자바 언어로 조작할 수 있도록 하는 JRE

 

▶ 예시 - 어댑터 패턴이 적용되지 않은 코드

 

public class ServiceA {
    void runServiceA(){
        System.out.println("ServiceA");
    }
}
public class ServiceB {
    void runServiceB(){
        System.out.println("ServiceB");
    }
}

 

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

        sa1.runServiceA();
        sb1.runServiceB();
    }
}

 

▶ 예시 - 어댑터 패턴 적용 

 

public class AdapterServiceA {
	ServiceA sa1 = new ServiceA();

	void runService() {
		sa1.runServiceA();
	}
}
public class AdapterServiceB {
	ServiceB sb1 = new ServiceB();

	void runService() {
		sb1.runServiceB();
	}
}

 

public class ClientWithAdapter {
    public static void main(String[] args){
        AdapterServiceA sa1 = new AdapterServiceA();
        AdapterServiceB sb1 = new AdapterServiceB();

        sa1.runService();
        sb1.runService();
    }
}

· ServiceA와 ServiceB의 메서드를 runService()라고 하는 같은 이름의 메서드로 호출해서 사용할 수 있게 해주는 변환기 예시

   - 변환기들이 인터페이스를 구현하게 해서 더 개선할 수도 있음

· 이러한 방식을 오브젝트(객체) 어댑터 패턴이라함

   - 클래스 어댑터 방식이 존재하지만, 다중상속을 이용하기 때문에 자바에서는 쓸 수 없음         (https://lalwr.blogspot.com/2016/03/adapter-vs-facade-vs-decorator.html)

 

프록시 패턴

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

   - 실제 서비스 메서드의 반환값에 가감하는 것을 목적으로 하지 않고,

      제어의 흐름을 변경하거나 다른 로직을 수행하기 위해 사용

· 프록시: 대리자/대변인, 누군가를 대신해 그 역할을 수행

· 개방 폐쇄 원칙(OCP), 의존 역전 원칙(DIP)이 적용된 설계 패턴

 

▶ 예시 - 프록시 패턴이 적용되지 않은 코드

 

public class Service {
    public String runSomething(){
        return "서비스 짱!!!";
    }
}

 

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

 

 

▶ 예시 - 프록시 패턴 적용 

 

public interface IService {
    String runSomething();
}

 

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

 

public class Proxy implements IService{
    IService service1;

    public String runSomething(){
        System.out.println("호출에 대한 흐름 제어가 주목적, 반환 결과는 그대로 전달");

        service1 = new Service();
        return service1.runSomething();
    }
}

 

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

· 프록시 패턴의 중요 포인트:

   - 대리자는 실제 서비스와 같은 이름의 메서드를 구현하며, 이를 위해 인터페이스를 사용

   - 대리자는 실제 서비스에 대한 참조 변수를 갖음(합성)

   - 대리자는 실제 서비스의 같은 이름을 가진 메서드를 호출하고, 그 값을 클라이언트에게 돌려줌

   - 대리자는 실제 서비스의 메서드 호출 전후에 별도의 로직을 수행할 수 있음

 

·  서비스 객체가 들어갈 자리에 대리자 객체를 대신 투입해,

    클라이언트 쪽에서는 실제 서비스 객체를 통해 메서드를 호출하고 반환값을 받는지 전혀 모르게 처리할 수 있음

 

https://velog.io/@jsj3282/스프링이-사랑한-디자인-패턴1

 

데코레이터 패턴

· 원본에 장식을 더하는 패턴

· 프록시 패턴과 구현 방법이 같지만,

  프록시 패턴은 클라이언트가 최종적으로 돌려 받는 반환값을 조작하지 않고 그래로 전달하는 반면,

  데코레이터 패턴은 클라이언트가 받는 반환값에 장식을 덧입힘

 

▶ 예시 - 데코레이터 패턴을 적용한 코드

 

public interface IService {
    String runSomething();
}

 

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

 

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

 

public class ClientWithDecorator {
    public static void main(String[] args){
        IService decorator = new Decorator();
        System.out.println(decorator.runSomething());
    }
}

 

· 데코레이터 패턴의 중요 포인트:

   - 장식자는 실제 서비스와 같은 이름의 메서드를 구현하며, 이를 위해 인터페이스를 사용

   - 장식자는 실제 서비스에 대한 참조 변수를 갖음(합성)

   - 장식자는 실제 서비스의 같은 이름을 가진 메서드를 호출하고, 그 값에 장식을 더해 클라이언트에게 돌려줌

   - 장식자는 실제 서비스의 메서드 호출 전후에 별도의 로직을 수행할 수 있음

 

싱글턴 패턴

· 오직 인스턴스 하나만 만들고 그것을 계속 재사용하는 패턴

· 용도: 커넥션 풀, 스레드 풀, 디바이스 설정 객체 등 인스턴스를 여러 개 만들면 불필요한 자원을 사용하게 되고,

          프로그램이 예상치 못한 결과를 낳을 수 있는 경우 사용함

 

 

▶ 예시 - 싱글턴 패턴을 적용한 코드 

 

public class Singleton {
    static Singleton singletonObject; //정적 참조 변수
    private Singleton() {}; //private 생성자

    //객체 반환 정적 메서드
    public static Singleton getInstance(){
        if(singletonObject == null){
            singletonObject = new Singleton();
        }
        return singletonObject;
    }
}

 

public class Client {
    public static void main(String[] args){
        //private 생성자이므로 new를 통해 인스턴스 생성 불가
        //Singleton s = new Singleton();

        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        Singleton s3 = Singleton.getInstance();

        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s3);

        s1 = null;
        s2 = null;
        s3 = null;
    }
}

 

결과 - toString() 메서드를 별도로 오버라이딩하지 않았으므로 객체의 고유 값인 hashcode 반환

· 싱글턴 패턴을 적용하기 위한 방법:

   - new를 생성할 수 없도록 생성자에 private 접근 제어자를 지정

   - 유일한 단일 객체를 반환할 수 있는 정적 메서드가 필요

   - 유일한 단일 객체를 참조할 정적 참조 변수가 필요

 

· s1, s2, s3 참조 변수에 null을 입력하기전 T 메모리 스냅샷

https://velog.io/@jsj3282/스프링이-사랑한-디자인-패턴2

· 단일 객체는 공유 객체로 사용되므로 속성을 갖지 않는 게 정석

   - 단일 객체가 속성을 갖게 되면 하나의 참조 변수가 변경한 단일 객체의 속성이 다른 참조 변수에 영향을 미침

   - 단, 읽기 전용 속성 다른 단일 객체에 대한 참조 속성을 갖는 것은 예외

  

 

템플릿 메서드 패턴

· 상위 클래스 템플릿(견본) 메서드에서 하위 클래스 오버라이딩한 메서드를 호출하는 패턴

 

▶ 예시 - 템플릿 메서드 패턴이 적용되지 않은 코드: 강아지, 고양이와 재미있는 시간을 보내는 프로그램

 

public class Dog {
    public void playWithOwner(){
        System.out.println("귀염둥이 이리온...");
        System.out.println("멍! 멍!");
        System.out.println("꼬리 살랑 살랑~");
        System.out.println("잘했어");
    }
}

 

public class Cat {
    public void playWithOwner(){
        System.out.println("귀염둥이 이리온...");
        System.out.println("야옹~ 야용~");
        System.out.println("꼬리 살랑 살랑~");
        System.out.println("잘했어");
    }
}

 

▶ 예시 - 템플릿 메서드 패턴 적용 

 

· 상속을 통해 중복되는 부분은 상위 클래스로, 달라지는 부분은 하위 클래스로 분할

 

public abstract class Animal {
    //템플릿 메서드
    public void playWithOwner(){
        System.out.println("귀염둥이 이리온...");
        play();
        runSomething();
        System.out.println("잘했어");
    }
    
    //추상 메서드
    abstract void play();
    
    //추상 메서드
    
    //Hook(갈고리) 메서드
    void runSomething(){
        System.out.println("꼬리 살랑 살랑~");
    }
}

 

public class Cat extends Animal{

    @Override
    //추상 메서드 오버라이딩
    void play(){
        System.out.println("야옹~ 야옹~");
    }

    @Override
    // Hook(갈고리) 메서드 오버라이딩
    void runSomething() {
        System.out.println("야옹~ 야옹~ 꼬리 살랑 살랑~");
    }
}

 

public class Dog extends Animal {
    @Override
        //추상 메서드 오버라이딩
    void play() {
        System.out.println("멍! 멍!");
    }

    @Override
        // Hook(갈고리) 메서드 오버라이딩
    void runSomething() {
        System.out.println("멍! 멍! 꼬리 살랑 살랑~");
    }
}

 

public class Driver {
    public static void main(String[] args){
        Animal bolt = new Dog();
        Animal kitty = new Cat();

        bolt.playWithOwner();

        System.out.println();
        System.out.println();

        kitty.playWithOwner();
    }
}

· 상위 클래스인 Animal에는 템플릿(공통 로직)을 제공하는 playWithOwner() 메서드,

  하위 클래스에게 구현을 강제하는 play() 추상 메서드,

  하위 클래스가 선택적으로 오버라이딩할 수 있는 훅(Hook) 메서드인 runSomething()가 존재

 

· 템플릿 메서드 클래스 다이어그램

https://velog.io/@jsj3282/스프링이-사랑한-디자인-패턴2-hev91vgr

· 클래스 다이어그램에서 보이듯이 의존 역전 원칙(DIP)를 활용하는 패턴

 

팩터리 메서드 패턴

· 하위 클래스에서 팩터리 메서드를 오버라이딩해서 객체를 반환하는 패턴

· 팩터리 메서드: 객체를 생성 반한하는 메서드

 

▶ 예시 - 팩터리 메서드 패턴을 적용한 코드: 강아지와 고양이가 각자 장난감을 가져오는 프로그램

 

public abstract class Animal {
    //추상 팩터리 메서드
    abstract AnimalToy getToy();
}

 

//팩터리 메서드가 생성할 객체의 상위 클래스
public abstract class AnimalToy {
     abstract void identify();
}

 

public class Dog extends Animal{
    //추상 팩터리 메서드 오버라이딩
    @Override
    AnimalToy getToy(){
        return new DogToy();
    }
}

 

//팩터리 메서드가 생성할 객체
public class DogToy extends AnimalToy{
    public void identify(){
        System.out.println("나는 테니스공! 강아지의 친구!");
    }
}

 

public class Cat extends Animal{
    //추상 팩터리 메서드 오버라이딩
    @Override
    AnimalToy getToy(){
        return new CatToy();
    }
}

 

//팩터리 메서드가 생성할 객체
public class CatToy extends AnimalToy{
    @Override
    public void identify(){
        System.out.println("나는 캣타워! 고양이의 친구!");
    }
}

 

public class Driver {
    public static void main(String[] args){
        //팩터리 메서드를 보유한 객체들 생성
        Animal bolt = new Dog();
        Animal kitty = new Cat();

        //팩터리 메서드가 반환하는 객체들
        AnimalToy boltBall = bolt.getToy();
        AnimalToy kittyTower = kitty.getToy();

        //팩터리 메서드가 반환한 객체들을 사용
        boltBall.identify();
        kittyTower.identify();
    }
}

 

· 팩터리 메서드 클래스 다이어그램

https://velog.io/@jsj3282/스프링이-사랑한-디자인-패턴2-hev91vgr

· 클래스 다이어그램 위와 아래에 보이듯이 의존 역전 원칙(DIP)를 활용함

 

 

전략 패턴

· 클라이언트 전략을 생성해 전략을 실행할 컨텍스트에 주입하는 패턴

· 템플릿 메서드 패턴과 유사하여 같은 문제의 해결책으로 상속을 이용하는 템플릿 패턴

  객체 주입을 통한 전략 패턴 중에서 선택/적용할 수 있음

     - 단일 상속만 가능한 자바에서는 상속이라는 제한이 있는 템플릿 메서드 패턴보다 전략 패턴을 더 많이 활용

· 전략 패턴을 구성하는 세 요소

   - 전략 메서드를 가진 전략 객체

   - 전략 메서드를 사용하는 컨텍스트(전략 객체의 사용자/소비자)

   - 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트(제3자, 전략 객체의 공급자)

 

▶ 예시 - 전략 패턴을 적용한 코드 

 

· 무기(전략)를 조달(생성)해서 군인(컨텍스트)에게 지급(주입)해 줄 보급 장교(클라이언트, 제3자)를 구현

public interface Strategy {
      void runStrategy();
}

 

public class StrategyGun implements Strategy{
    @Override
    public void runStrategy(){
        System.out.println("탕, 타당, 타다당");
    }
}

 

public class StrategySword implements Strategy{
    @Override
    public void runStrategy(){
        System.out.println("챙.. 채쟁챙 챙챙");
    }
}

 

public class StrategyBow implements Strategy{
    @Override
    public void runStrategy(){
        System.out.println("슝.. 쐐액.. 쉑, 최종 병기");
    }
}

 

public class Soldier {
    void runContext(Strategy strategy){
        System.out.println("전투 시작");
        strategy.runStrategy();
        System.out.println("전투 종료");
    }
}

 

public class Client {
    public static void main(String[] args){
        Strategy strategy = null;
        Soldier rambo = new Soldier();

        // 총을 람보에게 전달해서 전투를 수행하게 한다.
        strategy = new StrategyGun();
        rambo.runContext(strategy);

        System.out.println();

        // 검을 람보에게 전달해서 전투를 수행하게 한다.
        strategy = new StrategySword();
        rambo.runContext(strategy);

        System.out.println();

        // 활을 람보에게 전달해서 전투를 수행하게 한다.
        strategy = new StrategyBow();
        rambo.runContext(strategy);
    }
}

 

· 전략 패턴의 클래스 다이어그램

https://velog.io/@jsj3282/스프링이-사랑한-디자인-패턴4

 

TODO:  템플릿 콜백 패턴

 

▶ 예시 - 템플릿 콜백 패턴을 적용한 코드 

public interface Strategy {
    void runStrategy();
}

 

public class Soldier {
    void runContext(final String weaponSound){
        System.out.println("전투 시작");
        executeWeapon(weaponSound);
        System.out.println("전투 종료");
    }

    private Strategy executeWeapon(final String weaponSound){
        return new Strategy() {
            @Override
            public void runStrategy() {
                System.out.println(weaponSound);
            }
        };
    }
}

 

public class Client {
    public static void main(String[] args){
        Soldier rambo = new Soldier();
        rambo.runContext("총! 총초종총 총! 총!");

        System.out.println();

        rambo.runContext("칼! 카가갈 칼! 칼!");

        System.out.println();

        rambo.runContext("도끼! 독독..도도독 독끼!");
    }
}

 

 

TODO: 스프링이 사랑한 다른 패턴들

 

 

 

반응형

댓글