[Scala] Class와 객체

smlee·2023년 8월 8일
0

Scala

목록 보기
4/37
post-thumbnail

이 글은 Programming in Scala 4/e를 보고 정리한 글입니다.

(1) Class

Class는 class라는 키워드 뒤에 클래스명을 넣어 정의한다.
그 후, 해당 클래스를 new를 이용해 객체를 만들 수 있다.

class CheckSumAccumulator {
	// 클래스의 정의가 들어간다.
}

위와 같은 형태로 클래스를 정의한다. CheckSumAccumulator 타입의 객체를 만들고 싶다면 다음과 같이 정의하면 된다.

new CheckSumAccumulator

이제 클래스 내부를 작성해야 한다. 클래스 내부에는 필드메서드를 넣어 정의 한다. 이러한 메서드와 필드를 합쳐 멤버라고 부른다.

필드는 객체가 사용할 상태 혹은 데이터를 담으며, 메서드는 그 데이터를 사용해 객체가 실행할 업무를 담당한다. 클래스를 인스턴스화시킬 때, Scala runtime은 해당 객체의 상태(= 변수들의 내용)를 담아둘 메모리를 확보시킨다.

class CheckSumAccumulator {
	var sum:Int = 0
}

위의 예시에서 사용했던 CheckSumAccumulator의 클래스 정의가 위와 같다고 하자. 만약 CheckSumAccumulator를 2번 인스턴스화하면 Scala Runtime은 메모리에 다음과 같이 데이터를 저장할 것이다.

위와 같이 연결된 상황에서 acc 내부의 sum값을 재할당한다고 생각해보자. 그럼 다음과 같이 저장이 된다.

우리는 위의 2개의 그림에서 sum 변수가 각 클래스마다가 따로 존재한다는 사실을 알 수 있다. 즉, 필드는 인스턴스 변수인 것이다. 객체의 인스턴스 변수들은 한데 모여 해당 객체의 메모리상의 이미지를 만들어낸다.

그리고, 한 가지 주목할 점은 분명 acc라는 변수는 val을 통해 선언되었다. 하지만, acc가 참조하는 객체 내부의 인스턴스 변수는 변경되었다. 이 뜻은, 객체 내부의 값들은 변할 수 있지만, 다른 객체를 할당하지 못하도록 하는 것이다. 즉, acc를 다음과 같이 하면 실패하는 것이다.

acc = new CheckSumAccumulator

위는 acc에 새로운 객체를 할당하는 것인데, 이는 컴파일 오류가 발생한다. val에 재할당을 시도했기 때문이다. 따라서, acc가 항상 최초에 초기화한 CheckSumAccumulator 객체와 동일하다는 것을 알 수 있다.

객체의 강건성을 추구하는 한 가지 중요한 방법은 객체의 상태(인스턴스의 모든 변수)를 인스턴스가 항상 바르게 유지하는 것이다.

강건성 (robustness)
컴퓨터과학에서의 강건성은 컴퓨터 시스템이 실행 중 오류와 잘못된 입력에 대처하는 능력을 뜻한다.

클래스가 강건성을 유지하는 또 다른 방법은 필드에 접근할 수 있는 주체들을 클래스 내부로 한정하는 것이다. 즉, 캡슐화를 하는 것이다. 캡슐화를 위해 클래스 내부 필드에 private이라는 접근 제어자를 붙이면 된다.

캡슐화(Encapsulation)
클래스 안에 있는 연관된 속성 및 행위를 하나의 캡슐로 만들어 외부로부터 보호한다.

Scala의 접근 제어자(Access Modifier)
1. public (default) : 어디서든(내부 클래스, 상속 클래스, 다른 패키지) 접근 가능
2. protected : 동일 클래스, 상속 클래스 접근 가능
3. private : 동일 클래스 내에서만 접근 가능

이제 캡슐화를 적용하고 몇 가지 메서들을 추가했다고 생각해본다.

class CheckSumAccumulator {
	private var sum = 0
    
    def add(b:Byte): Unit = {
    	sum += b
    }
    
    def checksum():Int = {
    	return ~(sum & 0xFF) + 1
    }
}

위의 클래스 정의에서 주의해야 할 점은 무엇이 있을까? 클래스 내부 메서드 파라미터는 val이라는 점이다. 즉, 위 예제에서 add라는 메서드 내부로 전달되는 b라는 파라미터는 val이지 var이 아니다. 따라서 만약 add 메서드 내부가 다음과 같았다면 컴파일 오류가 났을 것이다.

class CheckSumAccumulator {
	private var sum = 0
    
    def add(b:Byte): Unit = {
    	b += 3 // 컴파일 오류 => b는 val이다.
    }
    
    def checksum():Int = {
    	return ~(sum & 0xFF) + 1
    }
}

add 내부가 b의 값을 변경하는 연산이었다면 val을 변경할 수 없다고 오류가 뜰 것이다.

위의 예시에서 스칼라는 타입 추론이 가능한데 굳이 명시를 하는 이유는 무엇일까?

스칼라 컴파일러가 메서드의 타입을 제대로 추론하기는 하지만, 코드를 읽는 사람도 결과 타입을 추론하기 위해서 코드를 읽어야 할 것이다. 따라서 유지/보수를 용이하기 위해 명시적으로 리턴 타입을 남겨두는 것이 더 나은 경우가 자주 있다.

또한, add나 checksum의 경우 하나의 표현식만으로 계산할 수 있으므로 중괄호를 없앨 수 있다. 그리고 메서드 작성 시 return을 명시적으로 사용하지 않는 것이 권장되므로 위의 클래스를 최적화하면 밑과 같은 코드가 된다.

class CheckSumAccumulator {
	private var sum = 0
    
    def add(b:Byte): Unit = { sum += b }
    
    def checksum():Int = ~(sum & 0xFF) + 1

}

프로시저 (procedure)
위 예시의 CheckSumAccumulator의 add처럼 Unit이 결과 타입인 메서드는 부수 효과를 위해 실행된다. 위의 메소드처럼 부수 효과 만을 위해 실행되는 메서드를 프로시저라고 한다.

(2) 싱글톤 객체

스칼라가 자바보다 더 객체지향인 이유 중 하나는 Scala 클래스는 정적 멤버가 없다는 것이다. 대신, 스칼라는 싱글톤 객체를 제공한다. 싱글톤 객체는 클래스 정의와 같아 보이지만, class 키워드 대신 object 키워드를 사용한다는 차이점이 있다.

import scala.collection.mutable

object ChecSumAccumulator {
	private val cache = mutable.Map.empty[String, Int]
    
    def calculate(s: String) :Int = {
    	if (cache.contains(s)) {
        	cache(s)
        }
        else {
        	val acc = new CheckSumAccumulator
            s.map(char => acc.add(char.toByte))
            
            val cs = acc.checkSum()
            cache += (s->cs)
            cs
        }
    ]
}

위는 예제 싱글톤 객체이다. 위의 코드는 object로 선언했으므로 싱글톤 객체인게 분명하다.

그런데 만약 같은 파일 내에 (1)의 예시에서 사용한 class CheckSumAccumulator가 있다고 가정해보자. 즉, 같은 클래스명을 가진 싱글톤 객체와 클래스가 있으면, 싱글톤 객체를 클래스의 동반 객체(companion object)라고 부르며, 클래스는 싱글톤 객체의 동반 클래스(companion class)라고 부른다.

동반 클래스와 동반 객체는 상대방의 비공개 멤버(private member)에 접근이 가능하다.

자바에서 넘어온 사람이라면 싱글톤을 자바의 정적 메서드를 담아두는 집처럼 생각하는 것도 한 가지 방법이다. 싱글톤 객체의 메서드는 정적 메서드를 호출하는 것과 유사한 방식으로 호출한다.

CheckSumAccumulator.calculate("Every value is an object")

위와 같이 싱글톤 객체 내부에 있는 메서드를 자바의 정적 메소드처럼 사용한다. 하지만, 정적 메소드만 보관하는 것은 아니다. 싱글톤 객체는 1급 계층이다.

1급 계층 (first class)
1급 계층이란 말은 언어에서 제약 없이 다룰 수 있는 대상이란 뜻으로, 언어에서도 제약 없이 쓰일 수 있는 값이라는 의미이다. 인자로 넘길 수 있고, 변수에 저장 가능하고, 함수에서 반환 가능한 특징을 지닌 대상을 1급 계층 객체나 값이라고 말한다.

싱글톤 객체의 정의는 타입을 정의하지 않는다. CheckSumAccumulator라는 싱글톤 객체 정의만 있다면 객체를 만들 수 없다. 객체를 만드려면 동반 클래스가 반드시 존재해야 한다. 하지만, 싱글톤은 슈퍼 클래스를 확장하거나 trait를 믹스인할 수 있다.

클래스와 싱글톤 객체의 한 가지 차이는 싱글톤 객체는 파라미터를 받을 수 없고 클래스는 받을 수 있다는 점이다. 싱글톤은 인스턴스화할 수 없으므로 파라미터를 싱글톤에 넘길 방법이 없기 때문이다.

만약 동반 클래스가 없는 싱글톤 클래스는 독립 개체(standalone object)라고 한다. 독립개체는 스칼라 앱 진입점을 만들거나 도구 메서드를 모아둘 때 사용된다.

📚 Reference

0개의 댓글