[Scala] 스칼라의 계층 구조

smlee·2023년 8월 16일
0

Scala

목록 보기
15/37
post-thumbnail

이 포스트는 Programming in Scala 4/e를 읽고 작성한 내용입니다.


스칼라의 모든 클래스는 공통의 슈퍼 클래스 Any를 상속한다. 즉, 모든 클래스가 Any의 서브클래스이므로 Any가 정의해둔 메서드는 모두 '보편적인' 메서드이다.

어느 객체에서든 Any의 메서드를 호출할 수 있는 반면, 스칼라 클래스 계층의 맨 밑바닥의 Null과 Nothing 같은 클래스는 모든 클래스의 서브 클래스의 역할을 한다.
이러한 스칼라의 계층 구조를 자세히 공부할 예정이다.

스칼라의 클래스 계층 구조


스칼라의 계층 구조는 개략적으로 위의 이미지로 정리할 수 있다.

루트 클래스에 있는 Any에는 2개의 서브 클래스가 있다. 이는 각각 AnyValAnyRef이다. 이때, AnyVal은 위의 이미지에 나와있듯, Double, Float, Long, Int, Short, Byte를 하위 클래스로 가지고 있다. 즉, Java의 원시타입을 서브 클래스로 가지고 있는 것이다. 이렇게 AnyVal의 모든 인스턴스는 리터럴로 선언할 수 있다. 이들은 new를 이용해 인스턴스화할 수 없다.

루트 클래스 Any의 또다른 서브 클래스는 AnyRef가 존재한다. 이 클래스는 스칼라의 모든 참조 클래스의 기반 클래스이다. 즉, AnyRef는 java.lang.Object에 별칭을 붙인것에 지나지 않는다. 따라서 자바로 작성한 클래스나 스칼라로 작성한 클래스는 모두 AnyRef를 상속받는다.

자바 플랫폼의 스칼라 프로그램에서는 AnyRef나 Object를 서로 바꿔서 사용할 수 있지만, 어디서나 AnyRef를 사용하는 것이 권장된다.


우리는 위의 이미지에서 Any 클래스가 가장 최상위 클래스인 것을 알 수 있는데, 그렇다면 모든 클래스에서 사용할 수 있는 클래스 Any의 메서드들에는 무엇이 있을까?

Any의 메서드

  1. final def == (that: Any): Boolean
  2. final def != (that: Any): Boolean
  3. def equals(that: Any): Boolean
  4. def ##:Int : 이 메서드 역시 해시값을 얻을 수 있는 메서드이다.
  5. def hashCode:Int
  6. def toString: String

    ## vs hashCode
    교재에는 ##와 hasCode 모두 해시값을 리턴하는 메서드라고 나와있다. 그래서 둘의 차이를 알아보았다. ## null 안정성을 가지고 있다. 따라서, null.##을 실행하면 0을 리턴하는 반면, null.hashCode를 실행하면 null pointer exception을 리턴한다.

Any의 메서드에는 final로 선언되어 변경할 수 없는 ==!= 메서드가 존재하며, 오버라이드할 수 있는 equals, ##, hashCode, toString이 있다.


hash cons

val x:String = "abcd".substring(2)
val y:String = "abcd".substring(2)

x == y // 출력값은?

만약 위의 코드가 Java 코드였으면 x == y는 false가 나와야 한다. 두 변수 내에 저장된 값들은 같지만, 참조하는 값은 다르기 때문에 equals를 사용하지 않으므로 false가 나와야 한다.
하지만, Scala에서 위의 코드는 true를 반환한다. 하지만, 가끔 참조 동일성이 필요한 경우가 있다. 이 경우 hash cons를 사용한다. eq를 사용하여 hash cons를 비교할 수 있다.

실제 Scala 터미널에서 실행한 코드이다. ==를 통해 동등성을 확인한다. 하지만 eq 메서드를 사용하면 두 변수의 참조값이 같은지를 확인하므로 false를 리턴한다. 또한, ne라는 메서드 역시 존재하는데, 이 메서드는 not eq로 생각하면 편하다. 즉, 같은 값을 참조하고 있지 않다에 대한 것을 묻는 메서드이다.

최하위 타입

Scala 계층 구조를 그린 맨 위의 그림에서 최 하위에 scala.Nothing과 Scala.Null이 존재하는 것을 확인할 수 있다. 이들은 스칼라 객체지향 타입 시스템의 일부 "특이한 경우를 처리하기 위한 특별 타입이다.

scala.Null

Null 클래스는 null 참조의 타입이다. 이 클래스는 모든 참조 타입의 서브 클래스이다. Null은 값 타입과는 호환성이 없다. 즉, AnyRef의 모든 서브타입의 최하위 클래스이며, AnyVal과는 전혀 상관없다.

위와 같이 AnyVal의 하위 타입인 Int형 변수에 Null을 넣으면 오류가 나는 것을 알 수 있다.

scala.Nothing

Nothing 타입은 스칼라 클래스 계층의 맨 밑바닥에 존재한다. 이 타입은 다른 모든 타입의 서브타입이다. 하지만, 이 타입의 값은 존재하지 않는다. 값이 없는 타입은 어떤 의미로 이해해야 할까?
Nothing의 쓸모 중 하나는 비정상적인 종료를 표시하는 것이다.

def error(message:String): Nothing =
	throw new RuntimeException(message)

위와 같이 비정상적인 종료를 나타내기 위하여 사용한다.

자신만의 값 클래스 정의

스칼라가 제공하는 값 클래스를 보조하는 자신만의 값 클래스를 정의할 수 있다. 내장된 값 클래스와 마찬가지로, 클래스의 인스턴스도 보통은 래퍼 클래스를 사용하지 않고 자바 바이트코드로 컴파일될 것이다.

제네릭 코드처럼 래퍼가 필요한 문맥에서는 자동으로 박싱과 언박싱이 이루어진다.

class Dollars(val amount:Int) extends AnyVal {
	override def toString() = "$ "+amount
}

위는 사용자 정의 값 클래스이다. 값 클래스를 정의하기 위해서는 AnyVal 클래스를 상속하면 된다. 그리고, 유일한 파라미터 앞에 val을 넣으면 된다.

한 가지 타입만 남용하는 것을 막기

스칼라의 클래스 계층을 가장 잘 활용하고 싶다면, 문제 영역에 잘 들어맞는 새로운 클래스를 정의하면 된다. 심지어 동일한 클래스를 다른 목적에 재활용할 수 있는 경우에도 가능하면 새로운 클래스를 정의하는 편이 좋다. 그렇게 만든 클래스 내에 메서드나 필드가 없어서 소위 아주 작은 타입이라고 불릴 만한 것이라 할지라도, 클래스를 하나 더 정의하면 컴파일러가 더 잘 돕게 만들 수 있다.

📚 Reference

1개의 댓글

comment-user-thumbnail
2023년 8월 16일

많은 것을 배웠습니다, 감사합니다.

답글 달기