소스 코드를 기록하는 남자

템플릿 콜백 패턴(Template Callback Pattern)

디자인 패턴

템플릿 콜백 패턴은 전략 패턴의 변형이며 스프링 3대 프로그래밍 모델 중 하나인 DI (의존성 주입)에서 사용하는 특별한 형태의 전략 패턴이다. 템플릿 콜백 패턴은 전략 패턴과 모든 것이 동일하나 전략을 익명 내부 클래스로 정의해서 사용하는 것이 차이이다. 앞에서 봤던 내용을 템플릿 콜백 패턴으로 변경해보자. 

 

package 전략패턴;

public class Client {
    public static void main(String[] args) {
        전략 strategy = null;
        군인 rambo = new 군인();

        strategy = new 총();
        rambo.runContext(strategy);

        strategy = new 칼();
        rambo.runContext(strategy);

        strategy = new 활();
        rambo.runContext(strategy);

    }
}

 

위의 코드는 기존의 코드이다. 익명 내부 클래스를 사용하여 변경해보자.

 

 

package 전략패턴;

public class Client {
    public static void main(String[] args) {
        군인 rambo = new 군인();

        rambo.runContext(new 전략() {
        	@Override
            public void runStrategy() {
            	System.out.println("총 : 빵야");
            }
        });

        rambo.runContext(new 전략() {
        	@Override
            public void runStrategy() {
            	System.out.println("칼 : 슈욱");
            }
        });
        
                rambo.runContext(new 전략() {
        	@Override
            public void runStrategy() {
            	System.out.println("활 : 슈슉");
            }
        });

    }
}

 

위와 같이 익명 내부 클래스로 변경했다. 코드를 보자하니 중복되는 부분이 많다. 이를 리팩토링 해보자.

 

package 전략패턴;

public class 군인 {
    void runContext(String weaponSound) {
        System.out.println("전투 시작");
        executeWeapon(weaponSound).runStrategy();
        System.out.println("전투 종료");
    }
    
    private 전략 executeWeapon(final String weaponSound) {
        return new 전략() {
            @Override
            public void runStrategy() {
                System.out.println(weaponSound);
            }
        };
    }
}

public class Client {
    public static void main(String[] args) {
        군인 rambo = new 군인();

        rambo.runContext("총");

        rambo.runContext("칼");

        rambo.runContext("활");

    }
}

아래와 같이 리팩토링이 가능하다. 리팩토링의 예제를 보면 볼수록 신기하고 즐겁다.

이렇게 리팩토링하여 보니, 전략이 군인의 내부로 들어왔다. 스프링은 이와 같은 형식으로 템플릿 콜백 패턴을 DI에 적극 활용하고 있다. 따라서 전략 패턴과 템플릿 콜백 패턴, 리팩터링된 템플릿 콜백 패턴은 꼭 기억해둘 필요성이 있다.

 

마지막 한마디로 정리해보자.

 

"전략을 익명 내부 클래스로 구현한 전략 패턴"

 

guy-who-writes-sourcecode.tistory.com/30

 

스프링에 녹아있는 아름다운 디자인 패턴

프로그램을 개발하다 보면 많은 상황에 직면하게 되는데, 프로그래밍의 역사가 꽤 길지 않은가? 따라서 이와 비슷한 사례를 이미 경험한 선배들이 정리해 둔 표준 설계 패턴이 있다. 이를 디자

guy-who-writes-sourcecode.tistory.com

 

전략 패턴(Strategy Pattern)

디자인 패턴

전략 패턴은 디자인 패턴의 꽃이라 할 수 있다. 따라서 스프링을 공부하고자 하는 사람은 꼭 이해하고 넘어가야 할 부분이다. 주의깊게 보도록 해보자.

 

전략 패턴을 구성하는 요소는 세 가지다.

  1. 전략 메소드를 가진 전략 객체
  2. 전략 객체를 사용하는 컨텍스트 (전략 객체의 사용자/소비자)
  3. 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트

전략 패턴의 맹점은 다음과 같다. 

 

"전략 패턴에서 클라이언트는 다양한 전략 중 하나를 선택해 생성한 후 컨텍스트에 주입한다."

 

예를 들어 군인이 있다고 가정해보자. 군인은 다양한 무기중 주어진 환경에 적합한 무기를 사용할 것이다. 또한 이를 보급해주는 보급 장교가 있다고 해보자. 그럼 여기서 무기는 전략이 되고, 군인은 컨텍스트, 보급 장교는 제 3자인 클라이언트가 된다.

 

다음은 코드를 볼 예정인데, 다양한 전략, 무기를 공통된 방법으로 사용하기 위해서 인터페이스를 정의할 것이다.

 

package 전략패턴;

public interface 전략 {
    public abstract void runStrategy();
}

public class 총 implements 전략{

    @Override
    public void runStrategy() {
        System.out.println("총 : 탕탕탕");
    }
}

public class 칼 implements 전략{
    @Override
    public void runStrategy() {
        System.out.println("칼 : 슝슝");
    }
}

public class 활 implements 전략{
    @Override
    public void runStrategy() {
        System.out.println("활 : 슉 슉 슈슝");
    }
}

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

public class Client {
    public static void main(String[] args) {
        전략 strategy = null;
        군인 rambo = new 군인();

        strategy = new 총();
        rambo.runContext(strategy);

        strategy = new 칼();
        rambo.runContext(strategy);

        strategy = new 활();
        rambo.runContext(strategy);

    }
}

 

위의 코드의 흐름은 클라이언트(=보급장교)가 군인인 람보에게 총, 칼, 활을 주고 전투가 시작되고 전투가 끝나는 흐름이 된다. 위에 보면 전략인 무기가 다양하게 변경되면서 컨텍스트를 실행할 수 있게 된 것이다. 전략 패턴은 디자인 패턴의 꽃이라 했던 것처럼, 다양한 문제 상황의 해결책으로 사용된다.

 

혹여라도 SOLID에 대한 부분이나 이전에 디자인 패턴이 공부한 기억이 있다면, 템플릿 메소드 패턴과 유사하다는 생각이 들 수도 있다. 같은 문제에 해결책으로 템플릿 메소드 패턴과 전략 패턴을 선택하여 사용할 수 있는데 상속이라는 제한이 있는 템플릿 메소드 패턴보다는 전략 패턴을 Java 진형에서 더 많이 사용한다고 한다.

 

마지막 한마디로 정리하자.

 

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

 

 

 

guy-who-writes-sourcecode.tistory.com/

 

소스 코드를 기록하는 남자

 

guy-who-writes-sourcecode.tistory.com

 

팩터리 메소드 패턴(Factory Method Pattern)

디자인 패턴

팩터리의 의미는 공장이다. 공장은 무언가를 생산하는 장소이다. 객체 지향에서의 팩터리는 객체를 생성하고, 팩터리 메소드는 객체를 생성 반환하는 메소드를 말한다.

 

팩터리 메소드 패턴은 무엇을 의미하는 것인가? 하위 클래스에서 팩터리 메소드를 오버라이딩해서 객체를 반환하는 것을 의미한다. 코드를 살펴보며, 이해를 해보자.

package 팩토리메소드패턴;

public abstract class 동물 {
    abstract 동물장난감 getToy();
}

public abstract class 동물장난감 {
    abstract void identify();
}

public class 강아지 extends 동물 {
    @Override
    동물장난감 getToy() {
    	return new 강아지장난감();
    }
}

public class 강아지장난감 extends 동물장난감 {
    public void identify() {
    	System.out.println("강아지 장난감입니다");
    }
}

public class 고양이 extends 동물 {
    @Override
    동물장난감 getToy() {
    	return new 고양이장난감();
    }
}

public class 고양이장난감 extends 동물장난감 {
    @Override
    public void identify() {
    	System.out.println("고양이 장난감입니다");
     }
}


 

위 코드는 매우 억지처럼 보일 수 있지만, 그래도 중점에 대해서 파악해보자.

여기서 동물의 팩토리 메소드를 구현하여 각 동물이 각자의 장난감을 반환하도록 구현되는 모습을 볼 수 있다.

 

위와 같은 코드 방식으로 구성된다. 아직 미비된 부분이 많은 것 같다. 내가 공부한 책에서 다루는 팩토리 메소드 패턴은 매우 간단하다. 한 마디로 정의된다

"오버라이드된 메소드가 객체를 반환하는 패턴"

 

guy-who-writes-sourcecode.tistory.com/30

 

템플릿 메소드 패턴 (Template Method Pattern)

디자인 패턴

템플릿 메소드는 템플릿을 제공하는 메소드, 하위 클래스에게 구현을 강제하는 추상 메소드, 하위 클래스가 선택적으로 오버라이딩할 수 있는 Hook 메소드를 두는 패턴을 템플릿 메소드 패턴이라 한다.

 

이해가 되지 않는다면, 코드를 보는 것이 좋다.

 

억지스럽게 코드를 한번 작성해보겠다. 너무 불편해하지 않았으면 좋겠다.

 

public class Bmw {
    public void driveOnRoad() {
    	System.out.println("자동차 시동 부릉");
        System.out.println("수동 기어로 시작");
        System.out.println("정지");
        System.out.println("자동차 시동 끄기");
    }
 }
 
 public class Audi {
    public void driveOnRoad() {
    	System.out.println("자동차 시동 부릉");
        System.out.println("자동 기어로 시작");
        System.out.println("정지");
        System.out.println("자동차 시동 끄기");
    }
 }

위와 같은 코드가 있다고 하자. 객체 지향의 특징을 조금이나마 이해하고 있다면, 위에서 반복적하는 코드에 대한 리팩토링 의지가 불타오를 것인다. 따라서 이를 템플릿 메소드 패턴을 적용하여 개선해보자.

 

package 템플릿메소드패턴;

public abstract class Car {
    public void driveOnRoad() {
        System.out.println("자동차 시동 부릉");
        drive();
        stop();
        System.out.println("자동차 시동 끄기");
    }
    
    abstract drive();
    
    void stop() {
    	System.out.println("정지");
    }
}
        
        
package 템플릿메소드패턴;

public class Bmw extends Car {
    @Override
    void drive() {
    	System.out.println("자동 주행");
    }
    
    @Override
    void stop() {
    	System.out.println("Bmw 정지");
    }
 }
 
 
package 템플릿메소드패턴;

public class Audi extends Car {
    @Override
    void drive() {
    	System.out.println("수동 주행");
    }
    
    @Override
    void stop() {
    	System.out.println("Audi 정지");
    }
 }

 

코드만 봐서 이해가 안될 것인다. 하나 하나 짚어서 설명해보겠다.

 

템플릿 메소드 패턴 구성 요소 상위 클래스 Car 하위 클래스 (Bmw, Audi)
템플릿 메소드는 공통 로직을 수행하는 부분, 공통 로직 안에서 하위 클래스에서 오버라이딩한 추상 메소드/훅 메소드를 호출 driveOnRoad()  
템플릿 메소드에서 호출하는 추상 메소드, 하위 클래스가 반드시 오버라이딩하도록 만든다. drive() 오버라이딩 필수
템플릿 메소드에서 호출하는 훅 메소드를 하위 클래스에서 선택적으로 오버라이딩합니다. stop() 오버라이딩 선택

 

"상위 클래스의 견본 메서드에서 하위 클래스가 오버라이딩한 메소드를 호출하는 패턴"

 

템플릿 메소드 패턴이 의존 역전 법칙(DIP)을 활용하고 있음을 알 수 있다. 이 패턴을 통해서 중복되는 공통 로직을 리팩토링하고, 개별로 다르게 진행되는 로직은 추상 메소드와 훅 메소드를 사용하여 강제로 오버라이딩하거나 선택적으로 오버라이딩할 수 있습니다.

 

guy-who-writes-sourcecode.tistory.com/30

 

싱글톤 패턴 (Singleton Pattern)

디자인 패턴

싱글톤 패턴이 무엇인가?

  • 싱글톤 패턴은 인스턴스를 딱 하나만 만들어 사용하기 위한 패턴이다.
  • 커넥션 풀, 스레드 풀, 디바이스 설정 객체 등과 같이 여러 개를 만들었을 때 불필요한 자원 낭비가 일어나는 부분을 방지할 수 있다.

싱글톤 패턴을 어떻게 적용해야 하나?

  • new를 실행할 수 없도록 생성자에 private 접근 제한자를 사용한다.
  • 유일한 단일 객체를 반환할 수 있는 정적 메소드를 생성한다.
  • 유일한 단일 객체를 참조할 정적 참조 변수를 사용한다.

코드를 보면서 어떻게 사용하는 것인가 다시 확인하자.

package 싱글톤패턴;

public class Singleton {

    static Singleton sigletonObject;
    
    private Singleton() {};
    
    public static Singleton getInstance() {
    	if (singtonObject == null) {
        	singletonObject = new Singleton();
        }
    	return singletonObject;
    }
}

 

new를 실행할 수 없도록 Singleton 생성자에 private을 사용했다. 이렇게 한다면 외부에서 new Singleton() 메소드를 호출할 수가 없다. 

 

유일한 단일 객체를 참조할 정적 참조 변수인 singletonObject를 사용한다. 추후 getInstance 메소드를 호출하게 되면 이 참조 객체를 반환한다.

 

유일한 단일 객체를 반환할 수 있는 정적 메소드가 필요하며 여기서는 getInstance라는 메소드를 사용했다. 일반적으로 싱글톤 패턴을 사용할 때 이름은 getInstance로 하는 듯하니 싱글톤 패턴을 사용할 때 고민하지말고 getInstance를 사용해도 무방하다.

 

마지막으로, 싱글톤 패턴 특징을 간략하게 설명하고, 마무리한다.

 

싱글톤 패턴

기본적으로 쓰기 가능한 속성을 갖지 않는 것을 대상으로 적용하며, private 생성자를 가지며, static 키워드 단일 객체 참조 변수를 사용하고, 단일 객체를 반환할 수 있는 getInstance 메소드를 사용한다.

 

guy-who-writes-sourcecode.tistory.com/30

 

스프링에 녹아있는 아름다운 디자인 패턴

프로그램을 개발하다 보면 많은 상황에 직면하게 되는데, 프로그래밍의 역사가 꽤 길지 않은가? 따라서 이와 비슷한 사례를 이미 경험한 선배들이 정리해 둔 표준 설계 패턴이 있다. 이를 디자

guy-who-writes-sourcecode.tistory.com

 

데코레이터 패턴(Decorator Pattern)

디자인 패턴

장식하는 사람이란 입장에서 접근해보자. 데코레이터 패턴의 구현 방법은 프록시 패턴과 동일하다. 다만 프록시 패턴과 다른 점은 프록시 패턴이 반환값을 조작하지 않고 그대로 전달하는 것과 다르게 데코레이션을 한다.

 

바로 코드를 확인해보자.

 

[IService]

package decoratorPattern;

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

[Service]

package decoratorPattern;

public class Service implements IService {
    @Override
    public String runProcess() {
        return "process";
    }
}

[Decorator]

package decoratorPattern;

public class Decorator implements IService {
    IService service;

    @Override
    public String runProcess() {
        System.out.println("호출에 대한 장식이 주목적이며, 클라이언트에게 장식이 달린 반환 결과를 전달");
        service = new Service();
        return "장식" + service.runProcess();
    }
}

 

기존의 프록시 패턴과 다르게 Decorator의 runProcess가 반환하는 문자열에 "장식" 이 추가되지 않았는가?

이제 데코레이터 패턴의 핵심들을 살펴보자.

 

  • 장식자는 실제 서비스와 같은 이름의 메소드를 사용한다.

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

  • 장식자는 실제 서비스와 같은 이름을 가진 메소드를 호출하고, 반환값에 장식을 붙여서
    클라이언트에게 전달한다.

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

 

하나만 기억하자!

메소드 호출 반환값에 변화를 주기 위해 중간에 데코레이터를 두는 패턴!

 

스프링에 녹아있는 디자인 패턴들

guy-who-writes-sourcecode.tistory.com/30

 

프록시 패턴(Proxy Pattern)

디자인 패턴

프록시라는 말은 대변인이란 의미를 가진다. 누군다를 대신해서 수행한다는 의미로서 디자인 패턴에서도 이와 같은 방식으로 적용된다. 객체지향스럽지 않은가? 현실 고증이 오진다.

 

먼저 프록시가 적용되지 않은 코드를 보자.

 

[Service]

package proxyPattern;

public class Service {
    public String runProcess() {
        return "Process";
    }
}

[ClientWithNoProxy]

public class ClientWithNoProxy {
    public static void main(String[] args)
    {
        Service service = new Service();
        System.out.println(service.runProcess());
    }
}

위와 같은 코드가 있다면, Client 에서 runProcess() 메소드를 직접 호출하는 것을 볼 수 있다.

그럼 프록시 패턴이 적용된다면 어떻게 될까? 프록시 패턴의 경우 실제 서비스 객체가 가진 메서드와 같은 이름의 메서드를 사용하고, 이 목적을 달성하기 위해 인터페이스를 사용한다.

 

인터페이스를 사용하면 서비스 객체가 들어갈 자리에 대리자 객체를 대신 투입하여 클라이언트 쪽에서 실제 서비스 객체를 통해서 메소드를 호출하고 반환값을 받는지, 대리자 객체를 통해 메소드를 호출하고 반환값을 받는지 전혀 모르게 처리할 수도 있다.

 

코드를 한번 살펴보자.

 

[IService]

package proxyPattern;

public interface IService {
    String runProcess();
}

[Service]

package proxyPattern;

public class Service implements IService{
    @Override
    public String runProcess() {
        return "Process";
    }
}

[Proxy]

package proxyPattern;

public class Proxy implements IService {
    IService service1;

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

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

[ClientWithProxy]

package proxyPattern;

public class ClientWithProxy {
    public static void main(String[] args)
    {
        IService proxy = new Proxy();
        System.out.println(proxy.runProcess());
    }
}

 

이전 코드에서는 Service service로 객체를 만들어 직접 선언했지 않았는가? 하지만 프록시 패턴을 사용하게 되면 대리자 호출이 가능해진다. 자! 프록시 패턴의 중요 포인트를 확인하고 마무리하겠다.

  • 대리자는 실제 서비스와 같은 이름의 메서드를 구현한다. 인터페이스를 사용한다.

  • 대리자는 실제 서비스에 대한 참조 변수를 갖는다.(합성을 사용)

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

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

 

프록시 패턴을 아까 뭐라고 했는가? 대변인이라 하지 않았는가? 대변인이 하는 일이 뭔가를 생각해보면 본인의 의견을 이야기하는 것이 아니라 어떤 회사, 정부, 기관 등의 의견을 대변할 뿐 자신의 의견을 더하거나 빼거나 가미하지 않는다. 프록시 패턴실제 서비스 메서드의 반환값에 아무런 가감하지 않는 것을 보면 알 것이다.

 

프록시 패턴의 목적은 아래와 같다.

 

 

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

 

 

그럼 프록시 패턴을 한 문장으로 정의해보자.

 

 

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

 

 

이해가 됐으면 좋겠다. 이 예제에서 사용된 프록시 패턴은 이전의 SOLID를 기억나게 할 것임이 분명하다.

여기에는 개방 폐쇄 원칙과 의존 역전 원칙이 적용된 설계 패턴이기 때문이다.

어댑터 패턴(Adapter Pattern)

디자인 패턴

어댑터라 하면 뭐가 가장 먼저 떠오르는가? 이 글을 쓰면서 막 떠오른 것은 dp to hdmi 선이다. 이 선을 생각해보면 모니터와 컴퓨터 서로 다른 기기 사이에서 통신을 가능하도록 해주지 않는가? 모니터가 읽을 수 있는 신호로 변환해주는 역할을 하고 있다.

 

위 글을 읽으면서 Java의 JDBC가 떠올랐다면 아주 잘했다. JDBC 또한 어댑터 패턴을 이용해서 다양한 데이터베이스 시스템을 단일한 인터페이스로 조작할 수 있게 해주기 때문이다.

 

혹시 JRE도 떠올랐는가? 아주 잘 이해하고 있다. Java를 구동하는 JRE도 어댑터 패턴이라고 할 수 있다. 단순히 Java 코드만 작성한다면 어느 운영 체제에 상관없이 동작할 수 있기 때문이다.

 

혹시 OCP가 떠올랐는가? 그럼 SOLID에 대해서 한 발자국 나아가고 있다는 사실이다. 어댑터 패턴은 결국 OCP(개방-폐쇄 원칙)과 의존 역전 원칙(DIP)가 활용한 설계 패턴인 것이다.

 

이제 코드를 보자. 어댑터가 적용된 코드와 적용되지 않은 코드를 비교해보겠다.

 

어댑터가 적용되지 않은 코드

[Adapter ServiceA]

package adapterPattern;

public class ServiceA {
    void processServiceA() {
        System.out.println("this is Service A");
    }
}

[Adapter ServiceB]

package adapterPattern;

public class ServiceB {
    void processServiceB() {
        System.out.println("this is Service B");
    }
}

[ClientWithNoAdapter]

package adapterPattern;

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

        sa1.processServiceA();
        sb1.processServiceB();
    }
}

위에서 main() 메서드를 살펴보면 sa1이 호출하는 메소드와 sb1이 호출하는 메소드의 역할이 매우 비슷한 것을 알 수가 있다. 하지만 메서드명이 다르다.

 

어댑터 패턴을 적용하면 메소드명을 통일 시킬 수 있다. 각 ServiceA, ServiceB 변환기를 만들어보자.

 

[AdapterServiceA]

package adapterPattern;

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

    void processService() {
        sa1.processServiceA();
    }
}

[AdapterServiceB]

package adapterPattern;

public class AdapterServiceB {
    ServiceB sb1 = new ServiceB();

    void processService()
    {
        sb1.processServiceB();
    }
}

[ClientWithAdapter]

package adapterPattern;

public class ClientWithNoAdapter {
    public static void main(String[] args) {
        AdapterServiceA asa1 = new AdapterServiceA();
        AdapterServiceB asb1 = new AdapterServiceB();

        asa1.processService();
        asb1.processService();
    }
}

어댑터 패턴을 적용하여 비슷한 서비스를 수행하는 메소드 이름을 통일하였다. 왜 사용하는가에 대한 의문이 든다면, 아직 객체 지향적인 이해가 부족하다고 생각한다. 예를 하나 들어서 데이터베이스 연결하는 메소드 connect() 이 있다고 하자. 데이터베이스가 다르다고 해서 connectMongo(), connectMySQL(), connectPostgreSQL() 이렇게 메소드를 짓는거보다 connect()가 좋지 않은가?

 

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

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