[GoF] Strategy 패턴

패턴명칭

Strategy

필요한 상황

변경될 가능성이 높은 어떤 알고리즘을 쉽고 효과적으로 교체할 수 있도록 하는 패턴이다.

예제 코드

예시를 위해 이 글에서는 변경될 가능성이 높은 알고리즘을 1부터 N까지의 합계를 구하는 것으로 한다. 위의 클래스 다이어그램에서 NSumStrategy는 이 알고리즘의 연산 결과를 얻기 위한 인터페이스만을 정의하는 인터페이스이며 Strategy 패턴의 핵심이다. 코드는 아래와 같다.

package pattern;

public interface NSumStrategy {
	long sum(long N);
}

Calculator 클래스는 어떤 복잡한 연산을 수행하는 기능을 하는데, 복잡한 연산 속에 1부터 N까지 합계를 내는 연산이 필요하다. 코드는 아래와 같다.

package pattern;

public class Calculator {
	private NSumStrategy strategy;
	
	public Calculator(NSumStrategy strategy) {
		this.strategy = strategy;
	}
	
	public double run(int N) {
		return Math.log(strategy.sum(N));	
	}
}

이제 1부터 N까지 합계를 내는 연산에 대한 구체적인 클래스를 정의해보자. 먼저 SimpleSumStrategy 클래스는 다음과 같다. 가장 흔하게 사용되며 매우 직관적인 코드이지만, 반복문을 사용함으로써 수행속도는 느린 방법이다.

package pattern;

public class SimpleNSumStrategy implements NSumStrategy {
	@Override
	public long sum(long N) {
		long sum = N;
		
		for(long i=1; i<N; i++) {
			sum += i;
		}
		
		return sum;
	}
}

다음은 가우스 방식을 사용하는 GaussSumStrategy 클래스이다. 반복문을 사용하지 않아 매우 속도가 빠르다.

package pattern;

public class GaussSumStrategy implements NSumStrategy {
	@Override
	public long sum(long N) {
		return (N+1)*N/2;
	}
}

아래는 실제 1부터 N까지의 합을 필요로 하는 복잡한 연산을 실제로 수행하는 코드이다.

package pattern;

public class Main {
	public static void main(String[] args) {
		Calculator cal1 = new Calculator(new SimpleNSumStrategy()); 
		Calculator cal2 = new Calculator(new GaussSumStrategy());
		
		double result1 = cal1.run(10000000);
		double result2 = cal2.run(10000000);
		
		System.out.println(result1 + " " + result2);
		
	}
}

복잡한 연산 중 일부분인 1부터 N까지의 합을 구하는 방식을 분리하여 쉽게 교체가 가능한 것을 볼 수 있다.

이 글은 소프트웨어 설계의 기반이 되는 GoF의 디자인패턴에 대한 강의자료입니다. 완전한 실습을 위해 이 글에서 소개하는 클래스 다이어그램과 예제 코드는 완전하게 실행되도록 제공되지만, 상대적으로 예제 코드와 관련된 설명이 함축적으로 제공되고 있습니다. 이 글에 대해 궁금한 점이 있으면 댓글을 통해 남겨주시기 바랍니다.

[GoF] Iterator 패턴

패턴명칭

Iterator

필요한 상황

배열과 같이 동일한 형태의 데이터를 여러 개 저장하고 있는 저장소(Aggregator)가 있다. 이 저장소에 저장된 데이터를 순서대로 접근하기 위한 패턴이다. 순서대로 접근하기 위한 방법은 저장소의 종류와는 상관없이 동일한 방식(API)을 제공한다.

예제 코드

Aggregator와 Iterator 인터페이스는 저장소의 형태와 종류에 상관없이 저장소를 구성하는 데이터를 순차적으로 모두 접근하기 위한 API를 제공한다. 이 인터페이스를 구성하는 Table과 RowIterator는 저장소에 대한 특화된 구현 클래스이다. Row 역시 저장소를 구성하는 특화된 데이터 구조에 대한 클래스이다. 이들 클래스를 사용하는 예제는 다음과 같다.

package pattern;

public class Main {
	public static void main(String[] args) {
		Table table = new Table();
		
		table.add(new Row("Jane", 27));
		table.add(new Row("Suji", 35));
		table.add(new Row("Tom", 19));
		table.add(new Row("Robin", 43));
		table.add(new Row("Robert", 58));
		
		Iterator it = table.iterator();
		while(it.next()) {
			Row row = (Row)it.current();
			System.out.println(row);
		}
	}
}

Aggregator 인터페이스는 다음과 같다.

package pattern;

public interface Aggregator {
	Iterator iterator();
}

Itertor 인터페이스는 다음과 같다.

package pattern;

public interface Iterator {
	boolean next();
	Object current();
}

next는 다음 구성 데이터로 이동하고 true를 반환한다. 만약 다음 데이터가 존재하지 않으면 false를 반환한다. current는 현재 Itertor가 가르키고 있는 현재의 데이터를 반환한다. Aggregator를 구현하는 Table 클래스는 다음과 같다.

package pattern;

import java.util.LinkedList;

public class Table implements Aggregator {
	private LinkedList<Row> table = new LinkedList<Row>();
	
	public Table() {
		//.
	}
	
	public Row get(int index) {
		return table.get(index);
	}
	
	public void add(Row row) {
		table.addLast(row);
	}
	
	public int getLength() {
		return table.size();
	}
	
	@Override
	public Iterator iterator() {
		return new RowIterator(this);
	}
}

Iterator를 구현하는 RowIterator 클래스는 다음과 같다.

package pattern;

public class RowIterator implements Iterator {
	private Table table;
	private int index;
	
	public RowIterator(Table table) {
		this.table = table;
		this.index = -1;
	}
	
	@Override
	public boolean next() {
		index++;
		return index < table.getLength();
	}

	@Override
	public Object current() {
		return table.get(index);
	}
}

저장소를 구성하는 실제 데이터에 대한 Row 클래스는 다음과 같다.

package pattern;

public class Row {
	private String name;
	private int age;
	
	public Row(String name, int age) {
		this.name = name;
		this.age = age;
	}
	
	public String getName() {
		return name;
	}
	
	public int getAge() {
		return age;
	}
	
	public String toString() {
		return "(" + name + ", " + age + ")";
	}
}

실행 결과는 다음과 같다.

(Jane, 27)
(Suji, 35)
(Tom, 19)
(Robin, 43)
(Robert, 58)
이 글은 소프트웨어 설계의 기반이 되는 GoF의 디자인패턴에 대한 강의자료입니다. 완전한 실습을 위해 이 글에서 소개하는 클래스 다이어그램과 예제 코드는 완전하게 실행되도록 제공되지만, 상대적으로 예제 코드와 관련된 설명이 함축적으로 제공되고 있습니다. 이 글에 대해 궁금한 점이 있으면 댓글을 통해 남겨주시기 바랍니다.

Kotlin, Null 검사

이미 20년 내외로 개발자들에게 포인터나 참조에 대한 null은 악몽이라고 했다. 사람은 정확하게 작동하는 기계가 아니므로, 그날의 컨디션이나 스트레스 따위로 실수는 언제든 할 수 있다는 점에서 null에 대한 참조는 분명 악몽의 작은 불씨임에는 틀림이 없다.

Kotlin은 참조에 대한 null 값을 최대한 방지하고자 값에 대해 null 값을 가질 수 있는 경우와 null 값을 가지지 못하는 경우로 분명하게 분리를 했으며, 각각에 대해 Nullable과 Non-Null이라는 타입이며 기본적이 Kotlin에서의 값들은 Non-Null 타입이다.

var a:Int = null

위의 코드는 Null can not be a value of a non-null type Int라는 에러가 발생한다. 이를 방지하려면 다음처럼 추가적인 작은 코드가 필요하다.

var a:Int? = null

Null 검사에 대한 Kotlin의 문법에는 ?., ?:, !!, ?.let{} 등이 있다. 하나씩 간략하게 정리한다.

?. 연산자

?.는 값이 null이면 null을 반환하고 구문을 종료하고 null이 아니면 계속 구문을 실행한다는 것이다.

var a:Int? = 10
println(a?.toString())

a = null
println(a?.toString())

실행 결과는 다음과 같다.

10
null

>?: 연산자

엘비스 프레슬리의 헤어 스타일을 닮았다고해서 엘비스 연산자랜다. 장난하냐? 여튼, 이 연산자는 값이 null일 경우에 대한 대체값을 지정하기 위한 연산자이다.

var a:Int? = 10
println(a?:"NULL")

a = null
println(a?:"NULL")

결과는 다음과 같다.

10
NULL

!! 연산자

이 연산자는 값이 null일 수 없으니 걱정말고 실행하라는 의미이다.

var a:Int? = 10
println(a!!.toString())

a = null
println(a!!.toString())

첫번째 출력은 10이고, 두번째에서는 값이 null인데 toString() 함수를 호출하고 있어 NullPointerException이 똭! 악몽의 작은 불씨~ 퍽!

?.let{} 구문

이 구문은 값이 null이 아닐 경우 여러 문장을 실행할 수 있는 구문을 제공한다.

var a:Int? = 10
a?.let {
    println("The world is good place to live.")
    println("are you kidding me?")
}  

a = null
a?.let {
    println("The world is good place to live.")
    println("I agree with you.")
}  

실행 결과는 다음과 같다.

The world is good place to live.
are you kidding me?

Safe Termination 패턴

패턴 명칭

Safe Termination

필요한 상황

스레드의 종료를 안전하게 하기 위한 패턴이다. 스레드는 기본적으로 stop 매서드를 제공하지만, 이 매서드는 안정성 문제로 deprecated 되었다. 스레드는 자신의 코드가 모두 실행되고 종료되는 것이 가장 이상적이지만, 실행 중간에 종료되어야 할 상황에서 안전하게 종료될 수도 있어야 한다.

예제 코드

먼저 수를 계속 카운팅하는 스레드 Worker를 기동시키는 아래의 코드가 있다.

package tstThread;

public class Main {
	public static void main(String[] args) {
		System.out.println("BEGIN");
		
		Worker worker = new Worker();
		worker.start();
		
		try {
			Thread.sleep(2000);
			
			System.out.println("[stopRequest BEGIN]");
			worker.stopRequest();
			System.out.println("[stopRequest END]");
			worker.join();
		} catch(InterruptedException e) {
			//.
		}
		
		System.out.println("END");
	}
}

Worker 스레드는 다음과 같다.

package tstThread;

public class Worker extends Thread {
	private volatile boolean bStop = false;
	
	public void stopRequest() {
		bStop = true;
		interrupt();
	}
	
	public void run() {
		try {
			for(int i=0; true; i++) {
				if(bStop) break;
				System.out.println("Counting: " + i);
				Thread.sleep(100);
			}
		} catch(InterruptedException e) {
			//.
		} finally {
			release();
		}
	}
	
	private void release() {
		System.out.println("SAFE TERMINATION");
	}
}

bStop 변수가 스레드를 안전하게 종료시키는 장치인데, volatile로 선언되었다. 이 변수는 stopRequest 매서드로 인해 true로 설정되며, interrupt 매서드의 호출을 동반한다. interrupt 매서드의 호출은 wait, sleep로 인해 스레드가 대기하는 상황에서도 안전하게 스레드를 종료시키기 위함이다. 실행 결과는 다음과 같다.

BEGIN
Counting: 0
Counting: 1
Counting: 2
Counting: 3
Counting: 4
Counting: 5
Counting: 6
Counting: 7
Counting: 8
Counting: 9
Counting: 10
Counting: 11
Counting: 12
Counting: 13
Counting: 14
Counting: 15
Counting: 16
Counting: 17
Counting: 18
Counting: 19
[stopRequest BEGIN]
[stopRequest END]
SAFE TERMINATION
END

Future 패턴

패턴 명칭

Future

필요한 상황

스레드는 코드의 실행에 초점이 맞춰져 있고, 그 결과를 받는 시점이 불분명하다. 스레드가 단순히 코드를 실행하는 것에서 끝나는 것이 아니라 그 실행의 결과를 다른 스레드에서 받기 위한 패턴이다.

예제 코드

Proxy 클래스는 어떤 결과를 얻기 위한 코드의 실행을 스레드로 실행해 주는 대리자이다. Proxy는 어떤 코드의 실행을 스레드로 실행해주고 바로 Result 클래스 타입의 객체를 반환한다. 바로 이 Result를 통해 스레드의 결과를 얻을 수 있다. 스레드의 실행 결과를 얻을 수 있는 상태가 되면 Result 객체의 setRealResult를 통해 실제 결과를 담는 RealResult 객체를 Result 클래스의 필드값에 담는다. 추후 적당한 시점에서 Result 클래스의 get 함수를 호출해서 실제 결과를 얻는다. 만약 실제 결과를 얻을 수 없을 때는 실제 결과가 완성될때까지 get 함수는 동기화(Blocking) 된다. 이러한 클래스들을 사용하는 코드는 아래와 같다.

package tstThread;

public class Main {
	public static void main(String[] args) {
		System.out.println("START");
		
		Proxy proxy = new Proxy();
		Result result1 = proxy.run(10, 'A');
		Result result2 = proxy.run(30, 'B');
		Result result3 = proxy.run(20, 'C');
				
		System.out.println("result1 = " + result1.get());
		System.out.println("result2 = " + result2.get());
		System.out.println("result3 = " + result3.get());
		
		System.out.println("END");
	}
}

Proxy 클래스는 다음과 같다.

package tstThread;

public class Proxy {
	public Result run(final int count, final char c) {
		final Result result = new Result();
		
		new Thread() {
			public void run() {
				RealResult realData = new RealResult(count, c);
				result.setRealResult(realData);
			}
		}.start();
		
		return result;
	}
}

Result 클래스는 다음과 같다.

package tstThread;

public class Result {
	private RealResult real = null;
	
	public synchronized void setRealResult(RealResult real) {
		if(this.real != null) {
			return;
		}
		
		this.real = real;
		
		notifyAll();
	}
	
	public synchronized String get() {
		while(real == null) {
			try {
				wait();
			} catch(InterruptedException e) {
				//.
			}
		}
		
		return real.get();
	}
}

RealResult 클래스는 다음과 같다.

package tstThread;

public class RealResult extends Result {
	private final String resultData;
	
	public RealResult(int count, char c) {
		char[] buffer = new char[count];
		for(int i=0; i<count; i++) {
			buffer[i] = c;
			try {
				Thread.sleep(100);
			} catch(InterruptedException e) {
				//.
			}
		}

		this.resultData = new String(buffer);
	}
	
	@Override
	public String get() {
		return resultData;
	}
}

실행 결과는 다음과 같다.

START
result1 = AAAAAAAAAA
result2 = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
result3 = CCCCCCCCCCCCCCCCCCCC
END