이 글은 Programming in Scala 4/e
를 읽고 정리한 내용입니다.
앞의 포스트들에서는 스칼라의 기본적인 내용을 공부했었다. 지금 포스트부터는 좀 더 완전한 기능을 갖춘 클래스 작성법을 정리하려고 한다.
이론적인 내용들보다 예시를 통해 클래스 작성을 정리하려 한다.
분수를 나타내는 클래스를 작성하며 클래스 파라미터, 생성자, 메서드, 연산자, 비공개 멤버, 오버라이드 등과 같은 스칼라의 객체지향 프로그래밍 요소 역시 정리할 예정이다.
분수(rational number)는 n
과 d
가 정수이고, d
가 0이 아닐 때 n/d
로 표시된다.
분수 클래스에서 덧셈, 뺄셈, 곱셈, 나눗셈과 같은 분수 클래스를 만드려고 한다. 우리는 위의 기능들을 포함할 클래스명을 Rational
이라고 명명할 것이다.
우리는 위에서 어떤 클래스를 만들 것인지 구상을 해놓았다. 우리는 불변하는 객체를 만들려고 한다. 따라서 인스턴스에 필요한 정보를 생성 시점에 제공해야 한다. 따라서 다음과 같이 설계를 한다.
class Rational(n:Int, d:Int)
이 코드에서 우리는 Java와의 차이점을 볼 수 있다. Java는 클래스에 인자를 받는 생성자가 있지만, 스칼라에서는 클래스가 바로 인자를 받는다.
위와 같이 클래스 파라미터들을 종합해서 주 생성자를 스칼라 컴파일러가 만들어 준다.
class Rational (n:Int, m:Int){
println("Created "+n+"/"+d)
}
간단하게 디버그 메시지를 넣어주었고, 이를 스칼라 인터프리터에서 실행시키면 다음과 같이 출력된다.
이 예시에서 생성자를 통해 값들이 잘 출력되는 것을 알 수 있다.
위의 인터프리터 결과를 보면 val res0: Rational = Rational@ce2eaa7
와 같은 문자열을 출력하는 것을 확인할 수 있다. 왜 이런 문자열이 출력되는 것일까?
기본적으로 생성된 클래스들은 java.lang.Object
클래스에 있는 toString 구현을 물려 받는다. java.lang.Object 클래스 내부에 있는 toString
은 클래스명, @ 표시, 16진수 숫자를 출력한다. 즉, 디버그 출력문, 로그 메시지, 테스트 실패 보고와 같이 프로그래머에게 정보를 제공해 도움을 주기 위함이다.
하지만 위와 같은 정보들은 우리가 정의할 객체에 대해서는 유용하지 않은 정보이다. 따라서 우리는 우리가 직접 생성할 객체에 알맞는 정보를 출력하도록 재정의 하고 싶다. 이를 위해서는 toString 메서드를 Rational 클래스에 추가하여 오버라이드
시켜 원하는 정보를 출력하도록 한다.
class Rational(n:Int, d:Int){
override def toString = s"$n/$d"
}
위와 같이 작성을 하면 1번 객체 생성에서 디버그용으로 생성하였던 println 메서드를 굳이 사용할 필요가 없다.
1번과 같이 스칼라 인터프리터에서 Rational 클래스를 만들고, toString을 재정의해서 객체를 만들면, java.lang.Object의 toString과는 다르게, 내가 원하는 데이터 형태로 출력되는 것을 확인할 수 있다.
우리는 분수 객체를 설계할 때, 분모인 d
는 0이 올 수 없게 설계해야한다고 구상했다. 하지만, 2번까지의 정의로는 분모의 값에 0이 전달되는 것이 가능하다.
위와 같이 분모가 0임에도 객체가 생성되는데, 우리는 이렇게 잘못된 값이 들어오는 것을 방지하고 싶다.
이를 위해서는 0이 아니라는 선결 조건(precondition)을 정의해야 한다. 선결 조건은 메서드나 생성자가 전달 받은 값에 대한 제약이고, 호출하는 이가 지켜야 하는 요구 조건이다. require
문을 통해 선결 조건을 받을 수 있다.
위의 코드처럼 require 메서드를 추가하면, 올바르지 않은 값에 대하여 IllegalArgumentException
을 발생시키면서 객체 생성에 실패를 한다.
require(조건식)
조건식에 Boolean값이 될 수 있는 인자를 받는다. 이 값이 참이면, require문이 정상적으로 끝나고 다음 명령문들이 정상적으로 진행된다. 전달 받은 값이 참이 아니라면IllegalArgumentException
예외가 발생해 객체의 생성을 막는다.
선결 조건까지 작성했으므로, 덧셈 기능을 추가할 것이다. Rational은 변경 불가능한 객체로 유지되려면, add 메서드가 객체 자체의 값을 수정하는 것이 아닌, 새로운 객체를 생성하여 반환해야 한다.
따라서 위와 같은 코드를 작성하였는데, reference error가 발생한다. 그 이유는 무엇일까?
that
Scala에서 that은 현재 클래스의 인스턴스를 가리키는 변수이다.
클래스 파라미터 n
과 d
는 작성한 add 메서드의 스코프에 있다. 하지만, add 메서드가 호출된 대상 인스턴스에 속한 n과 d만 접근이 가능하다. 즉, 위의 add 메서드는 다른 Rational 객체를 호출했기 때문에 불가능하다. 따라서 올바르게 선언을 하려면 다음과 같이 선언해야 한다.
위와 같이 클래스 내부 필드를 선언해주어야 정상적으로 선언이 되는 것을 알 수 있다.
위와 같이 선언되면 의도한 대로 코드가 동작함을 알 수 있다.
하나의 클래스에는 여러 개의 생성자가 필요한 경우가 있다. 스칼라에서 주 생성자가 아닌 생성자를 보조 생성자라고 한다.
만약 위의 예시에서 1개의 인자만을 받아온 경우 분모의 default값은 1로 정의하고 싶으면 어떻게 하면 될까?
this
를 선언하면 된다. this는 자기 참조
이다. 즉, 자기 자신의 생성자를 선언하는 것으로, def this(...)
로 보조 생성자를 정의한다.
우리는 기약분수를 사용한다. 즉, 66/42
라는 형태보다는 최대공약수를 나눈 11/7
을 선호하는 것이다. 이를 구현하기 위해서는 private
키워드를 통해 비공개로 연산을 진행한다. 즉, 외부에서는 최대공약수가 얼마인지 안 보이게 하는 것이다.
외부에서 사용자가 최초에 입력한 값을 알지 못하도록 private
키워드를 사용해서 선언한 결과이다. 외부에서 g의 값을 조회할 수 없으므로 최초 입력값을 알지 못하도록 막아놓았다.
add
를 사용하여 분수의 연산을 정의해도 되지만, +
나 *
와 같은 수학 연산 기호를 사용하는 것이 더 직관적일 것이다. Scala는 수학 연산 기호를 정의할 수 있다.
즉, 위처럼 add
라는 메서드명 대신 +
를 정의하면 원하는 대로 사용할 수 있다.이때 주의해야할 점은, 연산자 우선 순위 역시 기본 Scala의 연산 우선순위가 적용된다.
오버라이드가 가능했던 것처럼, 오버로드 역시 가능하다. 따라서 다양한 연산을 진행할 수 있다. 스칼라에서 오버로드 메서드를 처리하는 방법은 Java와 거의 유사하다. 오버로드한 메서드 중 인자의 정적인 타입과 가장 잘 일치하는 버전을 선택하여 실행된다.