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

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

 표준 통계(Standard statistics)

  다음은 과학 및 공학 응용프로그램에서 사용되지만 표준 자바 라이브러리에 구현되어 있지 않은, 수학 계산과 간단한 시각화 도구 라이브러리를 구성해볼까 합니다. 이는 통계 내에서 각 숫자들을 다루는데 사용되는 것들입니다. 현대 과학자들이 직면하고 있는 문제 중 하나는 자료들에 대한 분석입니다. 기초적인 자료 분석을 위해, 다음 API를 구현해 볼 것입니다.

자료 분석을 위한 정적 메소드들의 API


기초 통계

  N개의 수치가 있다고 해봅시다. 이 수치들의 평균(mean)은 모든 수치들의 합을 N으로 나누어서 계산됩니다. 우리는 평균을 보고 수치들을 짐작할 수 있습니다. 이렇게 우리가 갖고있는 수치 값들에 대해 평가할 수 있는 여러 함수들이 있습니다. 최솟값, 최댓값, 중간값, 분산, 표준편차와 같은 것입니다.

  

프로그램 2.2.4: Data analysis library

public final class StdStats
{
	public static double max(double[] a)
	{
		double max = Double.NEGATIVE_INFINITY;
		for (int i = 0; i < a.length; i++)
		{
			if (Double.isNaN(a[i]))
				return Double.NaN;
			if (a[i] > max)
				max = a[i];
		}
		return max;
	}

	public static double mean(double[] a)
	{
		if (a.length == 0)
			return Double.NaN;
		double sum = sum(a);
		return sum / a.length;
	}

	public static double var(double[] a)
	{
		if (a.length == 0)
			return Double.NaN;
		double avg = mean(a);
		double sum = 0.0;
		for (int i = 0; i < a.length; i++)
		{
			sum += (a[i] - avg) * (a[i] - avg);
		}
		return sum / (a.length - 1);
	}

	public static double stddev(double[] a)
	{
		return Math.sqrt(var(a));
	}

	public static void main(String[] args)
	{
		double[] a = StdArrayIO.readDouble1D();
		StdOut.printf(" min %7.3f\n", min(a));
		StdOut.printf(" mean %7.3f\n", mean(a));
		StdOut.printf(" max %7.3f\n", max(a));
		StdOut.printf(" std dev %7.3f\n", stddev(a));
	}
}

  해당 코드는 클래스의 일부만 발췌하였습니다. <StdRandom>과 같이, main()에서 각 메소드를 호출해 테스트를 실시합니다. 만약 라이브러리에 추가적인 메소드가 자리하게 된다면, 테스트 코드 역시 수정해 모든 메소드를 한 번에 테스트할 수 있게끔 만들어야 합니다. 관련된 내용은 코드 혹은 웹사이트를 참고해보세요.


그래프 그리기(Plotting)

  <StdDraw>는 표를 통해 숫자를 표시하는 것보다 좀 더 효과적으로 시각화를 할 수 있게 만들어줍니다. 무언가 실험을 했을 때, 해당 데이터를 단순히 표로 표현하는 것보다는 다양한 방식으로 시각화를 하는 것이 더 좋을 것입니다. 이런 처리를 신속하게 하기 위해, 일반적인 상황에서 사용할 수 있는 메소드들을 담아볼까 합니다.


프로그램 2.2.5: Plotting data values in an array

public static void plotPoints(double[] a) {
    int n = a.length;
    StdDraw.setXscale(-1, n);
    StdDraw.setPenRadius(1.0 / (3.0 * n));
    for (int i = 0; i < n; i++) {
        StdDraw.point(i, a[i]);
    }
}

public static void plotLines(double[] a) {
    int n = a.length;
    StdDraw.setXscale(-1, n);
    StdDraw.setPenRadius();
    for (int i = 1; i < n; i++) {
        StdDraw.line(i-1, a[i-1], i, a[i]);
    }
}

public static void plotBars(double[] a) {
    int n = a.length;
    StdDraw.setXscale(-1, n);
    for (int i = 0; i < n; i++) {
        StdDraw.filledRectangle(i, a[i]/2, 0.25, a[i]/2);
    }
}

  위 코드는 <StdStats>에 포함되어 있는 메소드들입니다. 각 자료값들이 있는 배열을 인자로 받아 점으로, 선으로, 막대로 시각화를 할 수 있습니다.


  <StdStats>는 여러분들에게 자료 분석을 어떤 식으로 할 수 있는지에 대해 알려주는 것일 뿐입니다. 일반적인 상황에서 사용할 수 있는 다양한 메소드들을 지원하지는 않으며, 원한다면 여러분들이 추가해볼 수도 있습니다. 


함수 그래프 그리기

  여러분은 StdStats.plot*() 메소드를 이용해 어떤 함수이든 그려낼 수 있습니다. x값의 간격을 선택하고, 그 간격에 맞게 y값을 계산해 배열에 저장 한 뒤, StdStats를 호출하면 됩니다. 하지만 그래프에 여백이 너무 많이 그려질 때가 있는데, 이 때에는 setScale 메소드들을 이용하면 됩니다.

  만약 여러분들이 삼각함수를 그래프로 표현하고 싶을 때, y값의 범위는 -1에서 +1 사이일 것입니다. 이를 메소드를 통해서 해결해보죠. 참고로, x값은 자동으로 그 여백을 처리해줍니다.


StdDraw.setYscale(StdStats.min(a), StdStats.max(a));

  배열의 크기가 클수록(점이 많을수록) 곡선은 부드러워집니다. 이전에 <StdDraw>를 통해 샘플링 해봤을 때와 동일합니다.


음파 그리기

  <StdAudio> 라이브러리와 <StdStats> 플롯 메소드는 균일한 간격으로 계산된 값의 배열으로 작동합니다. 이를 이용해서, 여러분들은 음파를 직접 그려낼 수도 있습니다. 


실험 결과 그리기

  여러분들은 여러 결과를 다른 방식으로 겹쳐 그릴 수도 있습니다. 예를 들어 남녀의 통계를 비교해야 할 때나 이론적 모델과 실제 수치를 비교해야 할 때, 두 그래프를 겹쳐서 한 눈에 확인할 수 있게 만듭니다. 다음 프로그램은, 동전을 N번 던졌을 때 앞면이 나온 횟수를 기록하며, 이를 T번 반복합니다. 이 결과를 시각화 하고, 정규(가우시안) 분포 함수와 비교합니다.


프로그램 2.2.6: Bernoulli trials

 
public class Bernoulli {
	public static int binomial(int N) { 
                // Simulate flipping a coin N times,
		int heads = 0;
		for (int i = 0; i < N; i++)
			if (StdRandom.bernoulli(0.5))
				heads++;
		return heads;
	}

	public static void main(String[] args) { 
                // Perform experiments, plot results and model,
		int N = Integer.parseInt(args[0]);
		int T = Integer.parseInt(args[1]);
		int[] freq = new int[N + 1];
		for (int t = 0; t < T; t++)
			freq[binomial(N)]++;
		double[] norm = new double[N + 1];
		for (int i = 0; i <= N; i++)
			norm[i] = (double) freq[i] / T;
		StdStats.plotBars(norm);

		double stddev = Math.sqrt(N) / 2.0;
		double mean = N / 2.0;
		double[] phi = new double[N + 1];
		for (int i = 0; i <= N; i++)
			phi[i] = Gaussian.pdf(i, mean, stddev);
		StdStats.plotLines(phi);
	}
}



모듈화 프로그래밍(Modular programming)

  우리가 개발했던 라이브러리의 구현은 모듈화 프로그래밍의 일환입니다. 크나큰 문제를 해결하기 위해서 새로운 프로그램을 작성하는 것 대신, 문제를 작게 나누어 각 작은 작업들을 독립적으로 해결하게 만드는 것입니다. 좋은 라이브러리는 모듈화 프로그래밍을 가능케 하며, 미래의 클라이언트들에게 작업의 해결책을 제시해줍니다. "프로그램에서 작업의 단위를 나눌 수 있다면, 그렇게 해야 합니다."

  자바는 독립적인 별도의 파일로 디버깅할 수 있게 해줍니다. 예로부터 프로그래머들은 각자 컴파일 되고 독립적으로 실행될 수 있는 코드들을 모듈이라고 칭했습니다. 즉, 자바의 각 클래스는 모듈에 해당합니다.


  <IFS, 프로그램 2.2.3>은 각 작은 모듈로부터 만족스러운 계산을 해내기 때문에, 모듈화 프로그래밍의 좋은 예라고 할 수 있습니다. <StdRandom>과 <StdIO>, <Integer>의 메소드와 <StdDraw>의 메소드까지 다양한 모듈들을 사용합니다. 만약 모든 코드를 <IFS> 파일 단 하나에 집어넣었다면, 나중에 유지보수나 디버깅을 할 때 굉장히 많은 노력이 필요했을 것입니다.

  모듈화 프로그래밍을 통해, 배열을 읽는 것과 적절한 분산값을 무작위로 생성하는 작업들이 이미 잘 작동한다는 확신을 갖고 더 큰 프로그램을 작성 할 수 있었습니다. 이들은 각자 모듈로 구성 되어 있고, 각자가 테스트를 끝마쳤기 때문입니다.

  <Bernoulli, 프로그램 2.2.6>을 보시죠. 이는 <Gaussian>, <Integer>, <Math>, <StdRandom>, StdStats>의 클라이언트입니다. 이 수많은 라이브러리들을 우리가 아무 걱정 없이 사용할 수 있는 이유는, 이것들이 모듈로서 잘 존재하기 때문입니다.


  모듈화 프로그래밍은 현대 프로그래밍에서 필수적인 것이며, 수많은 이점들을 가졌다는 점을 기억하세요.

  • 아무리 큰 시스템이더라도, 적절한 크기의 프로그램들의 조합으로 나눌 수 있습니다.
  • 디버깅 역시 적은 코드로 손쉽게 할 수 있습니다.
  • 재구현 없이 재사용 할 수 있습니다.
  • 유지보수와 확장이 훨씬 간단해집니다.


적절한 크기의 프로그램(Programs of a resonable size)

  좋은 프로그램은 결코 거대하지 않습니다. 만약 여러분 프로그램의 페이지가 점점 늘어나고 있다면, 작은 작업들로 나눌 수는 없는지 다시 생각해보아야 합니다. 또 미래에 다른 클라이언트가 이 프로그램을 이용할 수 있는지에 대해 생각해보는 것도 좋겠죠. 여러분이 어떤 프로그램을 작성하던 간에, 그곳에는 굉장히 많고 작은 모듈들이 얽혀있다는 것을 알게 될 것입니다. 여러분 역시 그런 규칙을 따라야겠죠?


디버깅(Debugging)

  구문이 많고 상호작용하는 변수들이 많을 수록 프로그램을 추적하기는 어려워집니다. 100개의 변수와 그 변수들에 영향을 끼치는 수많은 구문들이 있는프로그램을 상상해보세요. 모듈화 프로그래밍에서는 이런 변수들의 스코프를 적절히 조절할 수 있게 해주고, 디버깅 역시 쉽게 만들어줍니다.


코드 재사용(Code reuse)

  우리가 <StdStats>나 <StdRandom>과 같은 라이브러리를 한 번 구현만 한다면, 이후에는 더이상 평균과 분산을 계산하고, 난수를 생성하는 데 걱정할 필요가 없습니다. 단순히 재사용만 하면 될 뿐입니다. 같은 코드를 계속 복사해낼 필요가 없다는 의미죠.


유지보수(Maintenance)

  모듈화 프로그래밍은 지속적으로 여러분의 프로그램을 향상시킬 수 있게 합니다. 모듈을 향상시킨다는 것은, 결국 해당 모듈을 사용하는 클라이언트를 전부 향상시키는 셈이죠. 가령 특정 문제를 두고 서로 다른 방법으로 해결하는 경우는 굉장히 흔합니다. 모듈화 프로그래밍을 적용한다면, 각자 모듈을 만들어 다양한 방법으로 접근해보고 독립적으로 시도해볼 수도 있습니다. 

  더욱 중요한 사실은, 여러분의 모듈에 버그가 발생할 수 있다는 것입니다. 모듈화 프로그래밍은 고쳐야 할 버그의 양을 상당수 줄여주게 됩니다. 모듈만 고치면, 모든 클라이언트의 버그가 고쳐지는 셈이니까요.


오래된 프로그램을 직면해본다면...

  오래된 프로그램을 보면, 상상 이상으로 긴 코드를 볼 수 있을 것입니다. 수 페이지가 넘어가는 길고 긴 구문들, 어떤 구문에서든지 접근할 수 있는 수많은 변수들이 있는 것들이죠. 이렇게 코드 어느 곳에서든지 접근할 수 있는 변수를 전역 변수(global variables)라고 합니다.

  모듈화 프로그램에서는 전역 변수의 사용을 최대한 피합니다. 하지만 저레벨이고 오래된 프로그래밍 언어에서는 여전히 많이 사용되고 있습니다. 전역 변수를 사용하는 거대한 모듈은 이해하기도 어렵고, 유지보수와 디버깅하기도 어렵습니다. 

  오래된 프로그램은 굉장히 중요한 사회 기반 시설들에서 사용됩니다. 가령 핵발전 시설이나, 은행 프로그램과 같은 것들이죠. 이것들이 여전히 남아있는 이유는 여러가지 이유가 있겠지만, 그 중 하나는 현대 프로그래머들이 현대 언어로 다시 작성하기엔 코드가 너무 난해하기 때문이기도 합니다! 여러분들은 이런 상황을 만들지 마세요.


마치며

  이번 절에서 우리는 Std* 라이브러리들을 살펴보고, 직접 사용해보기도 했습니다. 이런 간단한 수학 도구들을 소개해주는 이유는, 여러분들에게 이 도구들의 사용법을 알려주기 위함이 아닙니다. 중요한 것은, 여러분들이 이런 라이브러리들을 원하면 언제든지 만들 수 있다는 것입니다. 

  현대의 프로그래머가 복잡한 문제를 해결하기 위해서 던지는 첫번째 질문은 다음과 같습니다. "어떤 도구가 필요할까?". 필요한 도구들이 아직 구현되어있지 않다면, "그것들을 구현하는 것이 얼마나 어려울까?"에 대한 질문을 던져봐야 할 것입니다. 좋은 프로그래머가 되기 위해서는, 어떤 라이브러리를 사용할지 결정하는 지혜와, 원하는 소프트웨어 도구를 만들 수 있는 확신을 가져야합니다.

  라이브러리와 모듈화 프로그래밍 이후, 여러분들은 완전한 현대 프로그래밍 모델을 배우기 위해 한 걸음 내딛을 것입니다. 바로 객체 지향 프로그래밍(object-oriented programming)입니다. 객체 지향 프로그래밍으로 진입하기 이전에 마지막으로, 다음 절에서 자기 자신을 호출하는 함수에 대해 알아보도록 합시다.


2.2. 라이브러리와 클라이언트 끝.

댓글

이 블로그의 인기 게시물

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

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

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