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

[Java] 자바로 프로그래밍 입문하기: 3.2. 자료형 생성하기 (1)

자료형 생성하기

  원칙적으로는 사실 우리의 모든 프로그램들 8개의 기본 자료형들만으로 충분히 작성할 수 있습니다. 하지만 이전 절에서 체감했듯이, 높은 단계의 추상화를 담는 프로그램들을 만들기 위해 기본 자료형들만을 사용하는 것은 조금 버거운 일입니다. 그래서 자바 언어와 라이브러리에 내장된 수많은 자료형들을 사용했고, 이 또한 여전히 부족하죠. 나만의 자료형을 정의할 수 있는 능력이 필요합니다. 나만의 자료형, 이를 곧 자바에서는 클래스(class)라고 합니다.

  자바 클래스로 자료형을 구현하는 것은, 정적 메소드를 담은 함수 라이브러리를 만드는 것과 별반 다르지 않습니다. 차이점이라 하면, 자료와 메소드의 구현이 연결된다는 것이죠. 

  API는 우리가 구현해야 할 필요가 있는 메소드들을 명시합니다. 하지만 그것들을 어떻게 재현할 지에 대한 것은 우리의 자유죠. 이번 절에서 우리는 스톱워치, 히스토그램, 터틀 그래픽(직교 평면에서 상대 커서를 사용하는 벡터 그래픽-옮긴 이), 스피라 미라빌리스(Spira mirabilis, 로그 나선의 응용으로 만들어진 기하학적 형태-옮긴 이), 복소수, 망델브로 집합(프랙탈의 일종-옮긴 이), 주식 계좌 등 수많은 응용을 해볼 것입니다.

  자료형을 정의하는 절차를 자료 추상화라고 합니다. 이는 2장의 함수 추상화와 대응되는 용어죠. 자료와 자료에 구현되는 연산에 집중할 것입니다. "자료 및 해당 자료와 관련된 연산들을 명확하게 분리할 수 있다면, 그렇게 해야 합니다." 물리적 객체를 설계하거나, 수학적 추상화를 거치는 것은 간단하고 굉장히 유용하죠. 하지만 이러한 장점을 넘어선 진정한 자료 추상화의 힘은, 우리가 알고 있는 어떤 것이든 자료형으로 나타낼 수 있다는 것입니다.


자료형의 기본 요소들

  자바 클래스에서 자료형을 구현하는 방법에 대해 이야기해볼까 합니다. 3.1절에서 다루었던 <Charge> 자료형에 대해서죠. 우리는 이미 클라이언트 프로그램으로 이러한 것들을 다뤄보았습니다. 이제는 세부 사항을 구현하는 데 초점을 맞출 때가 왔네요. 앞으로 여러분들이 개발할 수많은 자료형들은 결국 간단한 요소들로 이루어져 있습니다.


API

  API는 모든 클라이언트들에게 드러나는 부분이며, 따라서 이곳이 곧 구현의 시작점이 될 것입니다. API가 구현에 있어 중요함을 강조하기 위해서, 다음 <Charge> API를 다시 한 번 돌아보죠.


  <Charge>를 구현하기 위해, 자료형의 값을 먼저 정의해야 합니다. 객체를 명시된 값으로 생성하는 생성자와, 값을 다루는 두 개의 메소드를 확인할 수 있습니다. 응용프로그램을 개발하다보면, 새로운 클래스를 만들어야 하는 문제에 직면하게 됩니다. 이 때 첫 단계는 바로 API를 만드는 것입니다. 이 단계는 3.3에서 설계 방법과 함께 더 자세하게 다룰 것입니다. API를 만들 때에는 심사숙고해야 합니다. API의 변경은 수많은 클라이언트에게 영향을 미치기 때문이죠. 우리는 API를 변경하지 않습니다.


클래스

  자료형의 구현은 자바 클래스입니다. 정적 메소드의 라이브러리로 사용해왔던 것처럼, 자료형에 관한 코드들 역시 집어넣을 수 있습니다. 물론 .java 확장자를 지닌 파일이어야 합니다. 우리는 여태껏 자바 클래스를 구현하면서, 자료형에서 핵심적인 부분들을 생략하고 있었습니다: 인스턴스 변수, 생성자, 인스턴스 메소드입니다. 

  인스턴스 변수는 여지껏 사용해왔던 변수와 비슷합니다. 생성자와 인스턴스 메소드 또한 함수와 비슷합니다. 하지만 이들의 쓰임은 조금 다른데요. 또한 접근 지정자라는 개념 역시 고려되어야 합니다. 


접근 지정자(Access modifier, Visibility modifier)

  public, private, final 등의 키워드는 클래스와 변수 이름 앞에 위치하게 됩니다. 이를 접근 지정자라고 합니다. public과 private 지정자는 클라이언트 코드로부터 접근을 제어하는 방법입니다. 우리는 클래스 안의 모든 인스턴스 변수와 메소드는 public 혹은 private으로 지정할 것입니다. 

  public 개체는 클라이언트가 접근할 수 있으며, private 개체는 클라이언트가 접근할 수 없습니다. final 지정자는 한 번 초기화되면 더이상 그 값이 변하지 않습니다. 읽기 전용인 셈입니다. 우리는 public을 생성자와 API에 있는 메소드에만 사용하기로 약속합시다. 그 외에는 private을 부여하는 것이죠. private 메소드는 보통 헬퍼 메소드라고 해서, 클래스 내의 다른 메소드들을 간소화하는 역할을 합니다. 이와 관련된 이야기들은 3.3절에서 좀 더 자세하게 다뤄보죠.


인스턴스 변수(Instance variables)

  자료형 값을 생성하는 메소드들을 작성할 때, 우리가 해야할 첫 번째 일은 그러한 값들을 참조하기 위한 변수들을 선언하는 것입니다. 이 변수들은 어떤 자료형이든 상관 없습니다. 지역 변수를 선언했을 때처럼, 인스턴스 변수들 역시 선언할 수 있습니다. <Charge>의 경우, 3개의 double 값을 사용했습니다. 두 개는 평면 위의 위치를 표현하기 위해서, 나머지 하나는 전하량을 표현하기 위해서였습니다. 

인스턴스 변수

  이러한 선언은 클래스의 첫번째 구문들에 나타나며, main()이나 다른 메소드들 안에 작성하지 않습니다. 인스턴스 변수와 지역 변수는 아주 중요한 차이가 있습니다. 지역 변수는 그 순간에 단 하나의 값만 존재하지만, 인스턴스 변수는 수도 없이 존재할 수 있다는 것입니다. 자료형의 인스턴스 하나 당 하나씩 존재하는 것이죠. 

  셀 수 없이 많다고 해서 접근하기 어려운 것은 아닙니다. 우리가 인스턴스 메소드를 호출할 때에는, 해당하는 객체의 이름을 함께 명시해서 접근하기 때문이죠.


생성자(Constructors)

  생성자는 객체를 생성함과 동시에 해당 객체에 대한 참조를 제공합니다. 클라이언트 프로그램이 new 키워드를 사용할 때, 자바는 자동으로 생성자를 호출해줍니다. 자바가 대부분의 일을 해주기 때문에, 우리의 코드는 단지 인스턴스 변수들을 의미 있는 값들로 초기화해주기만 하면 됩니다. 

생성자

  생성자는 언제나 클래스의 이름과 동일합니다. 여러 개의 생성자를 오버로딩 할 수 있습니다. 우리가 정적 메소드를 사용했을 때, 시그니처를 달리하여 같은 이름의 메소드를 여러개 만들 수 있었습니다. 생성자 역시 동일합니다. 

  클라이언트에게는 new 뒤에 쓰이는 생성자 이름(과 괄호 안의 인자값)이 단순 함수 호출과 별 다를 것 없습니다. 해당하는 자료형의 참조를 반환해주는 함수인 셈이죠. 생성자 시그니처는 반환 타입을 쓰지 않습니다. 왜냐하면 모든 생성자는 항상 해당 자료형 객체의 참조를 반환하기 때문입니다. 자료형, 클래스, 생성자의 이름은 전부 동일합니다.

  클라이언트가 생성자를 호출하였을 때, 자바는 자동으로 다음을 수행합니다.

  • 객체의 메모리 공간을 할당합니다.
  • 생성자 코드를 호출해 인스턴스 변수들을 초기화합니다.
  • 객체의 참조를 반환합니다.

  <Charge>의 생성자는 전형적인 경우입니다: 클라이언트로부터 인자를 받아서, 인스턴스 변수들을 초기화합니다.


인스턴스 메소드(Instance methods)

  인스턴스 메소드들을 구현하기 위해, 챕터 2에서 정적 메소드를 구현한 기억을 떠올려 코드를 작성하면 됩니다. 각 메소드는 시그니처와 바디로 구성됩니다. 시그니처는 지정자와 반환형, 메소드 이름과 매개변수들로 이루어집니다. 바디는 메소드의 구문들이며, 메소드의 반환형에 따른 return문을 포함합니다.

인스턴스 메소드의 구조

  클라이언트가 메소드를 호출할 때, 시스템은 클라이언트가 제공하는 값들로 매개변수들을 초기화합니다. 또한 return문을 만날 때까지 구문들을 실행하며, 계산된 값이 클라이언트에게 반환됩니다. 이는 해당 메소드의 호출 부분이 마치 그 메소드의 반환 값으로 대체되는 것과 동일합니다. 이는 정적 메소드와 동일하죠. 단 하나만 빼면요: 인스턴스 메소드는 인스턴스 변수를 이용할 수 있습니다.


메소드 내의 변수

  곧, 인스턴스 메소드를 구현하는 자바 코드는 다음 세 종류의 변수를 사용합니다:

  • 매개변수(Argument variables, 인자 변수)
  • 지역 변수(Local variables)
  • 인스턴스 변수(Instance variables)

  앞의 두 변수는 정적 메소드와 동일합니다. 매개변수는 메소드의 시그니처에 명시되어 있으며, 메소드가 호출되었을 때 클라이언트가 제공하는 값으로 초기화됩니다. 지역 변수는 메소드의 바디에서 선언 및 초기화됩니다. 매개변수의 스코프는 메소드 전체이며, 지역 변수의 스코프는 해당 변수가 선언된 블록 안입니다. 

  반면 인스턴스 변수는 완전히 다릅니다. 인스턴스 변수는 해당 클래스 객체 내의 자료형 값을 쥐고 있는 것입니다. 또한 인스턴스 변수의 스코프는 클래스 전체죠. 우리가 사용하고 싶은 객체의 값을 어떻게 특정할 수 있을까요? 아마 조금만 생각해보면, 금방 답이 나올 것입니다. 메소드를 호출하는 데 사용한 객체의 값을 사용하면 되는 것이죠. c1.potentialAt(x, y)라고 작성했을 때, potentialAt()은 c1의 인스턴스 변수를 참조하게 되는 것입니다.

인스턴스 메소드 내의 변수

  세 종류의 변수의 차이를 반드시 이해하도록 하세요! 이 차이를 이해하는 것이 곧 객체지향 프로그래밍의 핵심입니다.


프로그램 3.2.1: Charged-particle implementation


public class Charge {
	private final double rx, ry;
	private final double q;

	public Charge(double x0, double y0, double q0) {
		rx = x0;
		ry = y0;
		q = q0;
	}

	public double potentialAt(double x, double y) {
		double k = 8.99e09;
		double dx = x - rx;
		double dy = y - ry;
		return k * q / Math.sqrt(dx * dx + dy * dy);
	}

	public String toString() {
		return q + " at (" + rx + ", " + ry + ")";
	}
	
	public static void main(String[] args) {
		double x = Double.parseDouble(args[0]);
		double y = Double.parseDouble(args[1]);
		Charge c1 = new Charge(0.51, 0.63, 21.3);
		Charge c2 = new Charge(0.13, 0.94, 81.9);
		StdOut.println(c1);
		StdOut.println(c2);
		double v1 = c1.potentialAt(x, y);
		double v2 = c2.potentialAt(x, y);
		StdOut.printf("%.2e\n", (v1 + v2));
	}
}
% java Charge .2 .5
2.2e+12
% java Charge .51 .94
2.5e+12



  자바에서 자료형을 만들기 위해 이해해야 할 기본적인 구성이 있습니다. 우리가 고려할 모든 자료형의 구현은 인스턴스 변수, 생성자, 인스턴스 메소드, 테스트 클라이언트와 함께할 것입니다. 우리가 개발할 각 자료형은 전부 같은 단계를 거칠 것입니다. 전산적 목표를 달성하기 위해서 다음 행동을 생각하는 것보다, 클라이언트의 필요를 확인하고 해당하는 자료형을 만들어볼 것입니다. 즉, 실용적으로 다가가보자는 의미입니다.

  자료형을 생성하는 첫 번째 단계는 API를 명시하는 것입니다. API의 목표는 클라이언트를 구현으로부터 분리해내기 위함입니다. 모듈화 프로그래밍을 실현하는 것이죠. API 명세에는 두 가지의 목표가 있습니다. 첫째, 정확하고 명료한 클라이언트 코드를 사용하기 위함입니다. 몇몇 클라이언트 코드를 미리 작성해보고 API를 마무리하는 것이, 실제 클라이언트가 필요한 자료형의 연산들을 명세하는데 도움이 됩니다. 둘째, 연산들을 실제로 구현하기 위함입니다. 어떻게 구현할지 모르겠는 연산들을 명시하는 것은 의미가 없죠.

  두 번째 단계는 API에 맞게 자료형을 구현하는 것입니다. 먼저 인스턴스 변수들을 선정합니다. 그리고 명세된 메소드를 구현하기 위해 인스턴스 변수들을 사용합니다.

  세 번째 단계는 테스트 코드를 작성하는 것입니다. 앞의 두 단계를 검증하기 위함입니다.

  이번 절에서는 API와 함께 각 예제들을 살펴볼 것입니다. 구현을 먼저 생각하고, 그 다음 클라이언트를 생각할 것입니다. 수많은 자료형들을 살펴볼 예정입니다.

  자료형을 정의하는 값은 무엇이고, 클라이언트가 그러한 값들을 다루는데 필요한 연산들은 무엇일까요? 이 의문에 대한 대답은 곧 여러분들이 새로운 자료형을 생성하고, 더 나아가 우리가 정의한 자료형들을 이제껏 내장 자료형을 사용했던 것과 같은 방법으로 사용하는 클라이언트를 작성할 수 있게 만듭니다.

클래스의 구조

계속.

댓글

이 블로그의 인기 게시물

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

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

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