Singleton , Strategy , Adapter , Template Method Pattern

Singleton 패턴

 

Singleton 은 ‘정확히 하나의 요소만 갖는 집합’ 이라는 뜻으로, 클래스가 하나의 객체만을 생성하도록 제한한다.

 

이 말은 즉, 생성자를 통해서 여러 번 호출이 되더라도 인스턴스를 새로 생성하지 않고 최초 호출 시에 만들어두었던 인스턴스를 재활용하는 패턴이라는 의미이다.

 

동일한 자원이나 데이터를 처리하는 객체가 불필요하게 여러 개 만들어질 필요가 없는 경우에 주로 사용한다.

 

 

 

굳이 하나의 객체만을 생성하도록 제한하는 이유는

객체를 많이 만드는 것은 시간이 많이 걸리고, 잉여 객체는 메모리 낭비이기 때문이다.

그 외에 여러 가지 다른 객체가 메모리에 떠도는 것은 유지보관에 골치 덩어리기도 하다.

 

 

 

 

싱글톤 패턴을 사용하면 그 타입의 객체는 유일하기 때문에, 유일한 인스턴스에 대하여 어디서든 접근할 수 있게 해야 한다.

 

싱글톤 패턴을 구현하기 위해서 우선, 생성자를 밖에서 부르지 못하도록 private으로 만들어야 한다.

클래스 안에 있는 클래스의 인스턴스를 static private 으로 선언하고

단일 인스턴스에 접근할 수 있는 public getInstance() 나 유사 메소드를 둔다.

 

public class RandomGenerator {
 	private static RandomGenerator gen = null;
 	public static synchronized RandomGenerator getInstance() {
 		if (gen == null) {
 			gen = new RandomGenerator();
 		}          
		return gen;
 	}
 	private RandomGenerator() {}
 	...
 }

( synchronized 를 통해 Locking 으로 병렬처리의 문제점을 해결하고, if문을 통해 필요할 때까지 객체를 만들지 않도록 한다.  )

 

 

 

 

Strategy pattern

 

strategy pattern을 알기 전에, 간단하게 여기에서 사용되는 인터페이스와 델리게이트의 개념을 짚어보자.

 

 

인터페이스는 키보드나 디스플레이처럼 사람과 컴퓨터를 연결하는 장치이다.

 

간단하게 기능에 대한 선언과 구현을 분리시키고, 기능을 호출하는 통로를 만들어두는 것으로 이해하면 된다.

 

 

이렇게 인터페이스를 사용한다면

해당 인터페이스 모양을 준수할 경우 통합에 신경 쓰지 않고 다양한 형태로 새로운 클래스를 개발할 수 있다.

또한 인터페이스로 다중 상속 효과를 간접적으로 얻을 수 있다.

인터페이스를 이용한 다중 상속 효과

 

public interface Ainterface {
     // //기능에 대한 선언
	public void funcA();
 }
 
 public class AinterfaceImpl implements Ainterface{
 	@Override
    public void funcA(){
    	System.out.println("AA");
    }
 }
 
public class Main {
     public static void main(String[] args) {
         Ainterface ainterface = new AinterfaceImpl();
         // 통로
	ainterface.funcA();
     }
 }

 

 

 

 

델리게이트란, 위임한다는 의미이다.

특정 객체의 기능을 사용하기 위해 다른 객체의 기능을 호출하는 것을 말한다.

 

public class AObj {
     Ainterface ainterface;
     public AObj() {
         ainterface = new AinterfaceImpl();
 	}
 
 public void funcAA() {
 
	// 위임한다.
	 ainterface.funcA();// System.out.println("AAA");
	ainterface.funcA();// System.out.println("AAA");
 	// ~ 기능이 필요합니다.
 	}
}
 
public class Main {
     public static void main(String[] args) {
         Ainterface ainterface = new
         AinterfaceImpl();
         // 통로
	ainterface.funcA();
 	//위임
	AObj aObj = new AObj();
 	aObj.funcAA();
}

 

 

 

 

이제 Strategy 패턴을 알아보자.

 

Strategy Pattern은 여러 알고리즘의 하나의 추상적인 접근점(interface)을 만들어서

이 접근점에서 서로 교환 가능하도록 하는 패턴이다.

 

이름 그대로 전략을 쉽게 바꿀 수 있도록 해주는 디자인 패턴인데

여기에서 전략이란, 어떤 목적을 달성하기 위해 일을 수행하는 방식, 비즈니스 규칙, 문제를 해결하는 알고리즘 등으로 이해할 수 있다.

 

 

 

프로그램에서 전략을 실행할 때는 쉽게 전략을 바꿔야 할 필요가 있는 경우가 많이 발생한다.

특히 게임 프로그래밍에서 게임 캐릭터가 자신이 처한 상황에 따라 공격이나 행동 하는 방식을 바꾸고 싶을 때 스트래티지 패턴은 매우 유용하다.

 

 

게임에서 법칙은 모든 참가자에게 동등하기에 절대 바뀌지 않지만, 게임의 난이도는 바뀔 수 있다.

 

게임의 룰이 바뀌지 않으면서 게임의 난이도를 바꿀 수 있는 방법을 찾을 때

전략(strategy)을 위한 unique interface를 제공하여 전략을 쉽게 바꿀 수 있는 방법을 찾을 수 있다.

 

strategy 예시

더보기

 

로봇을 설계한다고 생각해보자.

 우리는 아마

Public abstract class Robot{}

Public class TeakwonV extends Robot{}

Public class Atom extends Robot{}

이렇게 Robot 클래스를 상속하여 다른 로봇 클래스를 만들 것이다.

 

만약 기존 로봇의 이동이나 공격을 수정하고 싶다면,

또는 새로운 로봇에 새로운 기능을 추가하고 싶다면 어떻게 해결해야 할까?

 

그냥 기존 클래스의 메소드를 수정하거나, 새로운 클래스에 로봇을 상속하게 하면 된다.

 

하지만 이 경우, 새로운 기능을변경하려면 기존 코드의 내용을 수정해야 하므로 OCP(개방폐쇄원칙)에 위배된다.

즉, 확장은 되는데 수정은안된다.

예를 들면 위의 로봇 클래스의 상속 클래스들에서 move() 로 기능이 중복되는 상황이 발생한다.

 

 

이런 문제점을 해결하기 위해서, 무엇이 변화되었는지 식별한 후에 이를 클래스로 캡슐화한다.

 

 

이를 반영하여 최종적으로 수정된 인터페이스는 다음과 같다.

 

 

 

 

위 그림에서 Strategy는 인터페이스나 추상 클래스로 외부에서 동일한 방식으로 알고리즘을 호출하는 방법을 명시한다.

 

ConcreteStrategy1, 2, 3은 strategy 패턴에서 명시한 알고리즘을 실제로 구현한 클래스다.

 

context는 strategy 패턴을 이용하는 역할을 수행한다. 필요에 따라 동적으로 구체적인 전략을 바꿀 수 있도록 setter 메서드를 제공한다.

 

 

전략 패턴의 순차 다이어그램

 

 

 

위 strategy 패턴의 예시에 실제로 적용해보면 다음과 같다.

 

robot, atom, teakwonV는 context 역할을 한다.

movingStrategy와 AttackStrategy는 각각 Strategy 역할을 한다.

나머지 클래스는 ConcreteStrategy 역할을 한다.

 

 

분명 방금 위에서 뭐가 뭔지 정의했었는데.. 너무 복잡하다.

 

 

간단하게 다시 보자면,

스트래티지 패턴에서 context는 특정 전략(알고리즘)을 사용하는 클라이언트를 의미한다.

context 클래스는 strategy 객체를 포함하여, 이 객체를 사용하는 것으로 클라이언트가 원하는 알고리즘을 수행한다.

 

그러니까 아톰과 태권브이 클래스는 context 클래스이고, 적절한 로봇 strategy 패턴을 선택하여 해당 객체를 포함한 상태이다.

아톰과 태권브이는 필요에 따라 전략 패턴을 교체할 수 있다.

 

이 패턴의 주요 목적은 서로 다른 알고리즘(또는 전략)을 정의하고, 이러한 알고리즘들을 캡슐화하여 클라이언트 코드에서 독립적으로 변경할 수 있도록 하는 것임을 염두에 두면 이해하기 쉽다.

여기에서 context는 이러한 전략을 사용하여 작업을 수행하는 역할을 한다.

 

strategy 패턴 코드 예시

public interface Weapon {
 public void attack();
}

public class Knife,Sword implements Weapon{
     @Override
     public void attack() {
     System.out.println("검 공격");
     }
}

public class GameCharacter {
 	//접근점 (무기)
 	private Weapon weapon;
 	//교환 가능
	public void setWeapon(Weapon weapon) {
 		this.weapon = weapon;
 	}
 	public void attack() {
 		if(weapon == null) {
 			System.out.println("맨손 공격");
 		}
 		else {
 			//델리게이트
			weapon.attack();
 		}
 	}
 }

 

 

 

 

 

Adapter Pattern

 

adapter란,

기계, 기구 등을 다목적으로 사용하기 위한 부가 기구를 말한다.

Ex) 220v 어뎁터를 이용하여 110v 로 사용

 

위 이미지를 통해 감이 오겠지만

Adapter Pattern은 연관성없는 두 객체 묶어서 사용하는 것이다.

이에 따라서 우리는 알고리즘을 요구사항에 맞춰서 사용할 수 있다.

 

 

이미 주어진 기능을 원하는 기능으로 정의하고, 원하는 기능을 구현해서 사용한다.

 

 

adapter 패턴의 순차 다이어그램

 

 

그냥 직접 코드를 보는 것이 가장 이해에 빠르다.

 

숫자를 입력하면 수의 두 배, 반을 반환하는 연산을 수행하는 객체를 만들어보자.

 

Math에서 기능은 이미 구현되어 있다고 하자.

이미 주어진 함수를 바로 사용할 수 없기 때문에 요구사항에 맞춰 변경하는 로직이 필요하다.

이때 adapter를 사용하게 된다.

 

 

추가로 위 조건에서 알고리즘 변경을 하고싶은 경우,

Math 클래스에 새롭게 두 배를 구할 수 있는 함수를 추가했고

절반을 구하는 기능에서 로그를 찍는 기능을 추가했다.

public interface Adapter {
	public Float twiceOf(Float f);
	public Float halfOf(Float f);
}

 public class Math {
 	public static double twoTime(double num) {
 		return num*2;
 	}
	public static double half(double num) {
		return num/2;
	}
	public static Double doulbed(double d) {
		return d*2;
	}
}

public class AdapterImpl implements Adapter {
     @Override
     public Float twiceOf(Float f) {
         return Math.doulbed(f.doubleValue()).floatValue(); 
	}
     @Override
     public Float halfOf(Float f) {
         System.out.println("half 함수 로그 호출");
         return (float) Math.half(f.doubleValue());
     }
}

public class MainClass {
     public static void main(String[] args) {
         Adapter adapter = new AdapterImpl();
         System.out.println(adapter.twiceOf(100f));
         System.out.println(adapter.halfOf(100f));
     }
}

 

 

 

 

 

Template Method pattern

 

알고리즘의 구조를 메소드에 정의하고 하위 클래스에서 알고리즘 구조의 변경없이 알고리즘을 재정의 하는 패턴이다.

 

 

 

템플릿 메소드 패턴은

구현하려는 알고리즘이 일정한 프로세스가 있을 때와, 구현하려는 알고리즘이 변경 가능성이 있는 경우 사용한다.

 

 

 

이 패턴을 사용하기 위해선 먼저

  1. 알고리즘을 여러 단계로 나눈다.
  2. 나눠진 알고리즘의 각 단계를 메소드로 선언하고
  3. 알고리즘을 수행할 템플릿 메소드를 만든다.
    (여러 단계로 나눠진메소드를 호출)
  4. 그러면 하위 클래스에서 나눠진 메소드들을 구현한다.

 

 

 

 

아래와 같은 클래스 다이어그램을 가정해보자.

 

HyundaiMotor 클래스는 모터를 제어하여 엘리베이터를 이동시키는 클래스,

Door 클래스는 문을열거나 닫는기능을 제공하는 클래스이다.

 

만약 LG와 같은 다른 회사의 모터를 제어하고 싶다면

 

move 메서드에서 공통적인 부분을 상위 클래스 Motor로 이동하여 move 메서드의 중복 코드를 최소화하여 설계한다.

 

 

 

템플릿 메서드 패턴은

전체적인 알고리즘을 구현하면서 상이한 부분은 하위 클래스에서 구현할 수 있도록 하여 메소드의 코드 중복을 최소화해주는 디자인 패턴으로서 전체적인 알고리즘의 코드를 재사용하는데 유용하다.

위 예시의 move 메소드

 

 

 

 

아래는 이 템플릿 메서드 패턴의 순차 다이어그램이다.

 

 

 

 

최종적으로 템플릿 메서드 패턴을 모터 예제에 적용시킨다면 다음의 결과를 얻을 수 있다.