{{ label!='' ? 'Label : ' : (q!='' ? '검색 : ' : '전체 게시글') }} {{ label }} {{ q }} {{ ('('+(pubs|date:'yyyy-MM')+')') }}

[Java] 자바로 프로그래밍 입문하기: 2.2. 라이브러리와 클라이언트 (2)

 난수(Random numbers)

  우리는 Math.random()을 사용하는 프로그램을 작성해본 경험이 있습니다. 우리는Math.random()에서 제공하는 0부터 1 사이의 무작위 실수를, 우리 프로그램이 사용하고자 하는 숫자의 유형으로 바꿔서 사용했었습니다.(예를 들어 동전을 던지기 위해 난수를 boolean으로 바꾸었고, 카드를 섞기 위해 카드 뭉치 개수만큼의 무작위 정수로 바꾸었었죠) 

  이는 사실 효율적인 사용은 아닙니다. 우리 코드의 재사용성을 좀 더 늘리기 위해, '무작위 실수를 우리가 원하는 숫자 유형으로 바꾸는 것' 또한 라이브러리로 구성해볼까 합니다. <프로그램 2.2.1>은 <StdRandom> 라이브러리를 사용합니다. <StdRandom>은 다양한 무작위의 숫자를 생성하게끔 오버로딩 되어 있습니다.

난수에 대한 정적 메소드 라이브러리의 API


  이들은 API의 짧은 설명만으로도 각자 무엇을 하는 메소드인지 알기에 충분합니다. Math.random()을 통해 다양한 자료형의 난수를 생성하는 이 메소드들은 <StdRandom.java>로 통합되었으며, 많은 프로그램에서 사용되는 난수 생성의 방법을 이 파일 하나로 집중할 수 있게 되었습니다. 더 나아가, 이 메소드들을 이용하는 프로그램은 Math.random()을 사용하는 것보다 더 명쾌한데, <StdRandom>의 메소드를 사용함으로써 어떤 형태의 난수를 생성하는지 잘 드러나기 때문입니다.


프로그램 2.2.1: Random number library


public final class StdRandom {
	public static double uniform(double lo, double hi) { 
		return lo + Math.random() * (hi - lo); 
	}

	public static boolean bernoulli(double p) {
		if (!(p >= 0.0 && p <= 1.0))
			throw new IllegalArgumentException("probability p must be between 0.0 and 1.0: " + p);
		return uniform() < p;
	}
	
	public static double gaussian() {
		// use the polar form of the Box-Muller transform
		double r, x, y;
		do {
			x = uniform(-1.0, 1.0);
			y = uniform(-1.0, 1.0);
			r = x * x + y * y;
		} while (r >= 1 || r == 0);
		return x * Math.sqrt(-2 * Math.log(r) / r);

		// Remark: y * Math.sqrt(-2 * Math.log(r) / r)
		// is an independent random gaussian
	}
	
	public static double gaussian(double mu, double sigma) {
		return mu + sigma * gaussian();
	}

	public static String main(String[] args) {
		// Unit Testing
}
}

  해당 코드는 클래스의 일부만 발췌하였습니다.


API 설계(API design)

  우리는 <StdRandom>의 각 메소드에서 전달되는 값들에 대해 충분히 추측을 해볼 수 있습니다. 예를 들어 uniform(N)을 호출하면 양의 정수 N까지의 난수를 반환할테고, bernoulli(p)는 0부터 1사이의 실수, discrete()는 확률을 담고 있는 실수들의 배열에서 특정 인덱스를 확률에 맞게 추출해줄 것입니다.

  우리는 명료하며 오해가 없는 라이브러리를 설계하려고 노력해야 합니다. 이는 다른 프로그래밍 작업에서도 예외는 아닙니다. 좋은 API 설계는 다양한 가능성을 위한 시도의 반복에서 비로소 이루어집니다. 

  API를 설계할 때에는 특히 신경을 많이 써주어야 합니다. 만약 우리가 API를 변경하게 되면, 모든 구현과 클라이언트가 그 변경의 영향을 받을 것이기 때문입니다. API 설계에서 우리의 목표는 코드와 별개로 클라이언트가 기대하는 바를 명확히 밝히는 것입니다.


단위 테스팅(Unit testing)

  <StdRandom>을 다른 특정 클라이언트의 참조 없이 구현한다고 하더라도, main()에 테스트 클라이언트를 포함시키는 것은 좋은 프로그래밍 습관입니다.  클라이언트 클래스가 해당 라이브러리를 사용할 때에는 필요가 없겠지만, 여러분들이 라이브러리를 디버깅하고 테스팅 할 때 유용하게 사용될 것입니다.

  라이브러리를 만들 때에는, 단위 테스팅과 디버깅을 위한 main() 메소드를 포함하세요. 적절한 단위 테스팅은 그 자체로 프로그래밍의 중요한 문제가 될 수도 있습니다. 예를 들어, <StdRandom>와 같은 난수 생성의 방법이, 진정 난수와 동일한 특성을 갖는지 확인하는 방법에 대해서는 여전히 전문가들에게도 논쟁거리로 남아있습니다. 

  최소한 여러분은 라이브러리의 main() 메소드가 다음을 만족할 수 있도록 합시다.

  • 모든 코드를 실행해야 합니다.
  • 코드가 제대로 동작함을 확인할 수 있어야 합니다.
  • 명령행 인자를 통해 더 많은 테스팅을 할 수 있어야 합니다.

  그리고 여러분들이 라이브러리를 사용할 때보다 더 철저한 테스팅을 main() 메소드에서 이루어지게 해야합니다. 예를 들어, 다음 코드는 <StdRandom>의 코드입니다. (shuffle()의 테스팅은 코드에서 생략했습니다)

public static void main(String[] args)
{
	int n = Integer.parseInt(args[0]);
	double[] probabilities = { 0.5, 0.3, 0.1, 0.1 };

	StdOut.println("seed = " + StdRandom.getSeed());
	for (int i = 0; i < n; i++)
	{
		StdOut.printf("%2d ", uniform(100));
		StdOut.printf("%8.5f ", uniform(10.0, 99.0));
		StdOut.printf("%5b ", bernoulli(0.5));
		StdOut.printf("%7.5f ", gaussian(9.0, 0.2));
		StdOut.printf("%1d ", discrete(probabilities));
		StdOut.printf("%1d ", discrete(frequencies));
		StdOut.printf("%11d ", uniform(100000000000L));
		StdOut.println();
	}
}


  명령행 인자를 통해 여러 번 시행되는 결과를 볼 수도 있습니다. 우리는 더 확장된 테스팅으로 라이브러리의 main()이 아닌, 다른 클라이언트에서 숫자들이 해당 분포에 실제로 나타나는지 확인할 수 있습니다. 다음 코드는 gaussian()을 이용해 난수를 생성한 뒤, 해당 좌표로 점을 찍습니다. 이를 계속 시행하면, 가운데에 점이 더 많이 찍히는 것을 볼 수 있습니다. 이는 아주 직관적으로 프로그램을 테스팅 할 수 있죠.

 
public class RandomPoints
{
	public static void main(String[] args)
	{
		int N = Integer.parseInt(args[0]);
		for (int i = 0; i < N; i++)
		{
			double x = StdRandom.gaussian(.5, .2);
			double y = StdRandom.gaussian(.5, .2);
			StdDraw.point(x, y);
		}
	}
}

<StdRandom>의 테스트 클라이언트

스트레스 테스팅(Stress testing)

  <StdRandom>과 같이 광범위하게 사용되는 라이브러리들도, 클라이언트가 무분별하게 사용하거나 API와 관련 없이 사용될 때 충돌이 일어나지는 않는지 확인하는 스트레스 테스팅을 해야 합니다. 자바 라이브러리 또한 이러한 테스팅을 거쳤고, 각 코드의 라인 하나하나가 혹여 문제가 되지는 않을지 확인했을 것입니다. 

  만약 discrete()에 주어진 배열의 실수 합이 정확히 1이 아니라면 어떻게 될까요? 배열의 크기가 0이라면 어떻게 될까요? unifrom()에 주어지는 두 인자 중 하나가 NaN 혹은 Infinity라면 어떻게 될까요? 이러한 경우들을 코너 케이스(corner case)라고 합니다. 

  경험 상, 대부분의 프로그래머들은 나중에 디버깅을 하는 수고를 덜기 위해서는 이러한 것들을 최대한 빨리 규정하고 찾아내는 것이 중요하다는 것을 배우게 됩니다. 이런 테스팅에 대한 합리적인 접근법은 분리된 클라이언트로서 테스팅을 하는 것입니다.


배열 입출력

  우리가 봐오기도 했으며, 곧 보게 될 많은 예제들은 자료들을 배열에 저장합니다. 고로 <StdIn>과 <StdOut>을 보완해 배열로도 입력을 받거나 출력을 할 수 있는 정적 메소드를 만들어보죠. 

API for out library of static methods for array input and output

참고:

  1. 1차원 형식은 정수 N, 그리고 N 개의 값을 입력으로 받습니다.
  2. 2차원 형식은 두 정수 M과 N, 그리고 M*N 개의 값을 행 우선 배열 입력으로 받습니다.
  3. int와 boolean 메소드 역시 구현되어 있습니다.(오버로딩)


  앞의 두 참고사항은 파일 형식을 결정해야 한다는 생각에서 반영된 사항입니다. 보통 우리가 무언가를 입력할 때에는, 맨 처음에 크기를 적고 뒤에 순서에 맞게 자료를 입력합니다. 이는 보편적인 방식이므로, 우리 메소드들도 이를 따르기로 합니다. print() 역시 해당 형식을 따라서 출력됩니다.


프로그램 2.2.2: Array I/O library 

public class StdArrayIO {

    public static double[][] readDouble2D() {
        int m = StdIn.readInt();
        int n = StdIn.readInt();
        double[][] a = new double[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                a[i][j] = StdIn.readDouble();
            }
        }
        return a;
    }
	
    public static void print(double[][] a) {
        int m = a.length;
        int n = a[0].length;
        StdOut.println(m + " " + n);
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                StdOut.printf("%9.5f ", a[i][j]);
            }
            StdOut.println();
        }
    }
}

  <프로그램 2.2.2>는 1차원과 2차원 배열을 표준 입력과 표준 출력으로 다루는 정적 메소드를 제공합니다. 해당 코드는 클래스 일부만 발췌하였습니다.


반복적인 함수 시스템(Iterated function systems)

  과학자들은 간단한 계산만으로도 상상 이상의 복잡한 이미지들을 만들 수 있다는 사실을 깨달았습니다. StdRandom, StdDraw, StdArrayIO 등, 우리가 쉽게 배워왔던 것들을 이용하는 것이죠.


시에르핀스키 삼각형(Sierpinski triangle)

  첫번째 예로, 다음 간단한 절차를 생각해보세요: 정삼각형의 한 꼭짓점에서 시작합니다. 이후, 정삼각형의 세 꼭짓점 중 하나를 무작위로 계속 선택합니다. 이전에 선택한 점과 선을 이어 그 중간에 점을 찍습니다. 이렇게 무작위로 선택하고, 이전 점과 선택된 점 가운데에 점을 찍는 것을 반복합니다. 다음 그림을 참고해보세요.

무작위 절차

 이는 단순해보이지만, 이를 행하는 프로그램을 작성해보는 것은 꽤나 도움이 됩니다. 

double[] cx = { 0.000, 1.000, 0.500 };
double[] cy = { 0.000, 0.000, 0.866 };
double x = 0.0, y = 0.0;
for (int t = 0; t < T; t++)
{
	int r = StdRandom.uniform(3);
	x = (x + cx[r]) / 2.0;
	y = (y + cy[r]) / 2.0;
	StdDraw.point(x, y);
}

   위 코드는 cx와 cy 배열에 삼각형의 꼭짓점을 저장합니다. StdRandom.uniform(3)을 통해 꼭짓점 하나를 선택하며, 선택된 꼭짓점과 기존의 x, y값을 더해 2로 나누어 가운데 점을 찾습니다. 마지막으로, StdDraw.point()를 이용해 화면에 점을 찍게 됩니다.

  놀랍게도, 그림은 꽤 그럴듯한 규칙을 가지고 그려집니다. 이를 시에르핀스키 삼각형이라고 합니다. 어떻게 이런 그림을 그려낼 수 있을까요?


확장하기

  우리는 다른 규칙으로, 같은 형태의 놀이를 해볼 수 있습니다. 바로 Barnsley fern입니다. 이를 생성하기 위해서는 좀 더 확장된 형태의 처리가 필요합니다. 다음 표를 참고해보세요. 각 확률에 따라, x와 y값이 적힌대로 변하게 됩니다.


  예를 들면, 1%의 확률로 x값은 0.5, y값은 0.16y로 대체되게 됩니다. (첫번째 행의 y-update 열에서 y가 아닌 x로 써져있는 것은 오타로 보입니다)

  이를 프로그램에서 입력값으로 설정할 수 있도록 하면 어떨까요? <프로그램 2.2.3>을 참고해보세요.


프로그램 2.2.3: Iterated function systems

 
public class IFS
{
	public static void main(String[] args)
	{
		// Plot T iterations of IFS on StdIn.
		int T = Integer.parseInt(args[0]);
		double[] dist = StdArrayIO.readDouble1D();
		double[][] cx = StdArrayIO.readDouble2D();
		double[][] cy = StdArrayIO.readDouble2D();
		double x = 0.0, y = 0.0;

		for (int t = 0; t < T; t++)
		{	// Plot 1 iteration.
			int r = StdRandom.discrete(dist);
			double x0 = cx[r][0] * x + cx[r][1] * y + cx[r][2];
			double y0 = cy[r][0] * x + cy[r][1] * y + cy[r][2];
			x = x0;
			y = y0;
			StdDraw.point(x, y);
		}
	}
}
% more sierpinski.txt
3
.33 .33 .34
3 3
.50 .00 .00
.50 .00 .50
.50 .00 .25
3 3
.00 .50 .00
.00 .50 .00
.00 .50 .433

% java IFS 10000 < sierpinski.txt



  시에르핀스키 삼각형을 위한 코드처럼 단순히 그것만을 위해 코드를 작성할 수도 있지만, <프로그램 2.2.3, IFS>는 행렬을 이용해서 좀 더 보편적으로 사용할 수 있게 작성되었습니다. 첫번째 입력은 각 확률을 받습니다. 두번째 입력은 x값의 변화에 대한 행렬을 받습니다. 세번째 입력은 y값의 변화에 대한 행렬을 받습니다.

  이렇게 작성한 결과 어떤 규칙이든 쉽게 만들어낼 수 있고, 놀라운 이미지들을 생성할 수 있게 됐죠. <IFS>는 자료주도(data-driven) 컴퓨팅을 해냅니다. 어떤 벡터값을 입력하느냐에 따라, 수도 없이 다양한 그림을 만들어낼 수 있습니다. 

  짧은 프로그램, 그리고 몇 안되는 입력이 이런 그림들을 만들어낼 수 있다는 것은 참으로 놀랍습니다. 이런 방식의 처리는 영화나 게임에서 응용되어 유용하게 사용되고 있습니다. 더 중요한 것은, 이런 간단한 계산이 현실적인 그림을 만들어내는 것이 우리에게 철학적인 질문을 던진다는 것입니다: 컴퓨팅이 자연에 대해서 무엇을 말해주는가? 또 자연은 컴퓨팅에 대해서 무엇을 말해주는가?


계속.

댓글

이 블로그의 인기 게시물

[코딩의탑] 4층: 툰 쉐이딩

[코딩의탑] 3층: 바다 렌더링

[코딩의탑] 5층: 포탈(Portal), 더 나아가기