함수형 객체는 변경 불가능한 객체를 의미합니다.

그래서 변경 불가능한 객체의 장/단점에 대해 살펴보면  일단 객체 생명주기 동안 상태가 변하지 않기 때문에 객체의 내부 값이 어떤지 바로 알 수 있습니다.

변경 가능한 객체는 내부 상태가 변하기 때문에 객체를 사용한 곳을 추적해야만 하는 번거로움이 있습니다.

전달이 자유롭습니다. 메소드의 인자로 전달할 때 자바의 경우 메서드 내부에서 객체 상태 변경을 막기 위해 복사를 합니다.

깊은 복사를 할 경우 cloneable 을 상속해서 clone 메서드를 구현해야 합니다. 귀찮습니다. 하지만 변경 불가능한 객체는 애초에 내부 상태 변경이 불가능하기 때문에 자유롭게 전달할 수 있습니다.

당연히 내부 상태 변경이 불가능하기 때문에 여러 스레드에서 접근해도 괜찮습니다.

HashSet에 변경 가능한 객체를 키로 사용했을 경우 나중에 변수 값을 바꿀 때 HashSet에서 해당 객체를 찾을 수 없을 때가 있는 데 이를 방지해 줍니다.

단점은 단순한 상태 변경만 할 때에도 객체를 그대로 복사하기 때문에 복사하는 객체가 복잡하다면 성능이 떨어질 수 있다는 단점이 존재합니다.

 

책에서는 분수를 표현하는 클래스를 구현하는 예제를 통해서 함수형 객체를 설명합니다.

분수 클래스 명세는 위와 같습니다.

 

변경 불가능한 객체는 객체 생성 후에 값을 변경할 수 없기 때문에 인스턴스에 필요한 정보를 생성할 때 제공해야 합니다.

Rational 객체는 분자와 분모 값이 필요하기 때문에 인스턴스를 생성할 때 분자, 분모의 값을 인스턴스에 전달해 줘야 합니다.

그래서 아래와 같이 클래스를 정의할 수 있습니다. 클래스 이름 주고 괄호 안에 nd 식별자가 있습니다. nd는 클래스 파라미터라고 하고

괄호 안에 있는 것을 주 생성자라고 합니다. 그래서 객체를 생성할 때 아래와 같이 하면 n10이 들어가고 d2 들어갑니다.

이렇게 자바와 달리 별도 생성자 메서드 없이 생성할 수 있습니다.

객체를 생성할 시 별도의 로직을 넣고 싶다면 아래와 같이 클래스 내부에 넣으면 됩니다.

그러면 알아서 컴파일러가 클래스 내부에 있으면서 필드나, 메서드가 아닌 것들은 주 생성자 내부로 넣습니다.

그래서 아래와 같이 출력이됩니다.

 

인스턴스를 생성했을 때 빨간 상자와 같이 인터프리터가 메시지를 출력했습니다. 이 출력은 객체의 toString 메서드를 호출한 겁니다.

기본적으로 toString은 클래스 이름 골뱅이, 16진수 숫자를 출력합니다.

나중에 디버그나 로그 메시지, 테스트 할 시 프로그래머에게 객체에 대한 명확한 정보를 제공해 주기 위해 toString을 오버라이딩에 재정의할 수 있습니다.

분수와 분자를 출력해주는 toString으로 바꾸는 예제가 가운데와 같습니다.

기존 객체에 toString 메서드를 메서드 앞에 override 키워드를 통해 오버라이딩해서 구현할 수 있습니다.

 

선결 조건이란 메소드나 생성자가 전달받은 값에 대한 제약, 호출하는 이가 지켜야할 요구 조건을 의미합니다.

저희 분수 클래스는 분모가 절대 0일 수 없습니다. 그래서 객체를 생성할 때 분모에 0이 들어오지 않도록 해 분모가 0이 되지 않도록  차단하는 선결조건을 만들 수 있습니다.

선결조건은 주생성자 안에 require 키워드를 이용해 정의할 수 있습니다.

아래와 같이 만약에 분모가 0Rational 객체를 생성하려고 한다면 아래와 같이 예외가 발생해서 객체 생성을 막습니다.

 

클래스 파라미터 n d는 자신의 클래스 내부에서만 접근 가능하다. 외부에서 접근이 불가능합니다.

 

lessThan 메서드는 호출한 분수 객체가 인자로 받은 분수 객체 보다 작을시 True리턴합니다.

이 때 호출한 분수 객체의 numerdonom에 접근하기 위해 this를 쓴 것을 볼 수 있습니다. 물론 여기서는 this를 생략해도 괜찮습니다.

아래 maxlessThan을 이용해서 더 큰 분수 객체를 리턴합니다. 여기서는 this를 무조건 서야합니다. 만약 인자로 들어온 객체보다 크다면 자기 자신을 리턴해야 하는데 이를 할 방법이 this 말곤 없기 때문입니다.

 

보조 생성자에 대해 알아보겠습니다. 보조생성자는 주생성자가 아닌 모든 생성자를 말하고코드와 같이 def this(…)으로 시작합니다.

위 코드는 def this로 시작했으므로 보조 생성자가 됩니다. 이 생성자의 역할은 분모가 1인 경우 분자만을 이용해 간결하게 표현하기 위해서

해동 보조 생성자는 분자만 입력으로 받고 내부적으로 주생성자를 호출하되 분모가 1이 되도록 두번째 인자로 1을 전달합니다.

그래서 오른쪽과 같이 3만 인자에 넣으면 3/1 객체가 생성됩니다.

생성자를 사용할 때 주의할 점은 모든 보조생성자는 다른 생성자를 호출하는 코드로 시작해야 합니다. 즉 첫 구문이 this가 됩니다.

 

이번에는 분자에 6, 분모에 4를 넣었을 대 6/4 가 아니라 약분된 상태인 3/2로 깔끔하게 내부 객체 상태를 관리하고 싶어졌습니다.

그래서 주 생성자 내부에서 분자와 분모의 최대공약수를 구해 각 분자와 분모를 최대 공약수로 나눠서 기약분수로 만들어 관리하고자 합니다.

그래서 맨 아래 gcd라는 최대공약수를 구하는 메서드를 정의합니다.(유클리드 알고리즘) gcd 메서드는 최대공약수를 구하지 분수 자체에 대한 메서드가 아니라서 외부에 공개할 필요가 없습니다.

그래서 private라는 키워드를 붙여서 외부에 공개되지 않도록 합니다.

그래서 주생성자가 호출되면 먼저 클래스 파라미터인 nd의 최대공약수를 구합니다. 마찬가지로 최대공약수를 공개할 필요가 없어 private 키워드를 붙였습니다.
약분 하기 위해서 nd를 최대공약수로 나눠 줍니다.

그러면 오른쪽 과 같이 2/3가 나옵니다.

 

스칼라는 라이브러리를 언어가 기본 지원하는 것처럼 정의하는 것을 추구합니다.

그래서 add 메서드를 +로 바꿔서 기본 연산자 같이 바꿀 수 있습니다.

또한 앞장에서 보았듯이 스칼라는 연산자의 가장 앞 글자로 연산자 우선순위를 결정합니다.

+ 보다 * 글자가 우선순위가 높으므로 오른족 코드와 같이 *먼저 계산 되는 것을 볼 수 있습니다 .

 

 

싱글톤 객체 같은 경우 컴파일러가 $클래스이름으로 클래스를 생성한다
사용해도 되긴 하지만 컴파일러가 만들어낸 식별자와 충돌할 가능성이 생긴다.

여기서 상수는 val로 된 모든 것을 의미하는 것이 아닙니다. Val 또한 array 타입일 경우 내부 원소 값이 변경 가능하기 때문이다.

여기서 말하는 val은 항상 동일한것 3.14 pi 같은 걸 의미합니다.

 

스칼라는 라이브러리를 언어가 기본 지원하는 것처럼 정의하는 것을 추구합니다.

메소드 호출 시점에 컴파일러는 오버로드한 메소드 중 인자의 타입이 일치하는 메소드를 선택한다.

 

 

스칼라의 기본 타입은 자바와 동일하게 byte short int 등이 있습니다. 이들의 숫자 범위도 자바와 동일합니다.

자바와 다른 점은 기본 타입이 클래스로 정의되어 있다는 점입니다.

여기서 보이는 기본 타입은 모두 scala라는 패키지 내부에 잇고 Striingjava lang패키지 안에 있는 것을 사용합니다.

스칼라는 자동으로 scalajava.lang 패키지를 import 하기 때문에 저희는 어떠한 import 구문 없이 기본형 타입과 String을 사용할 수 있습니다.

또한 스칼라와 자바 기본 타입 범위가 같기 때문에 스칼라 컴파일러는 기본 타입을 자유롭게 자바의 primitive 타입으로 변환 가능합니다.

 

리터럴은 상수 값을 코드에 직접 적는 것을 의미합니다.

정수 리터럴로는 Int, Long, Short, Byte가 있으며 스칼라는 10진수, 16진수를 표현할 수 있습니다. 16진수를 표현하려면 값 앞에 “0x”를 붙이면 됩니다.

정수 값 뒤에 L이나 l을 붙이면 Long 타입이 되고 아무 것도 없으면 Int 형이 됩니다. , 숫자 값은 디폴트로 Int형을 나타내고 위 코드와 같이 변수 선언 시 Short로 명시하면 Int형인 숫자는 Short 형으로 바뀝니다.

부동 소수점 리터럴은 Float, Double이 있으며 지수 부분은 “e”로 표현하는 데 10의 제곱승을 의미합니다. 값 뒤에 Ff를 붙이면 Float형이 되고 아무것도 없으면 Double 형이 됩니다.

문자 리터럴은 작은 따옴표로 둘러싼 문자로 표현합니다. 큰 따옴표로 둘러쌀 경우 String 타입이 됩니다. 문자 값은 “\u” 붙여 유니코드 형식으로 표현가능합니다.

또한 자바와 비슷하게 특수 문자를 ‘\’ 통해 표현할 수 있습니다.

문자열 리터럴은 String 타입 상수를 의미하며 대괄호로 둘러싸면 됩니다. 스칼라의 문자열 리터럴 중 독특한 Raw 문자열이라는 개념이 있습니다. 

기존에 특수문자, 따옴표, 개행문자는 “\”를 앞에 붙여서 넣을 수 있는 데 Raw 문자열을 사용하면 “\” 없이 개행 문자, 따옴표, 특수 문자 없이 타이핑한 문자열 그대로 표현 가능합니다.

Raw 문자열을 사용하려면 큰 따옴표 세 개로 둘러쌓으면 됩니다.  그러면 왼쪽 코드와 같이 큰 따옴표와 개행이 “\”없이 그대로 이루어 진 것을 볼 수 있습니다.

, “Type” 문자열 앞에 띄어쓰기도 그대로 표현하는 데 이 띄어쓰기를 조절해주려면 오른쪽과 같이 “|”를 넣어주면 됩니다.  그러면 해당 줄의 띄어쓰기가 “|” 기준 뒤부터 적용됩니다.

 

 

심볼 리터럴은 작음 따옴표 뒤에 알파벳과 숫자를 혼합한 식별자로 표현합니다.

아래 코드와 같이 a라는 변수에 “’hi” 심볼 리터럴을 넣어주면 스칼라 컴파일러는 ‘hi 심볼에 대해 Symbol(“hi”) 팩토리 메소드를 호출해 a 변수에 Symbol 인스턴스를 매핑해줍니다.

또한 동일한 식별자를 가진 심볼 리터럴은 동일한 Symbol 객체를 참조하게 됩니다. 아래 코드와 같이 ab가 같은 객체를 참조하는 것을 볼 수 있습니다.

 

문자열 인퍼폴레이션을 이용하면 문자열 리터럴 내부에 표현식을 내장해 문자열을 가독성 있게 표현 가능합니다.

S 문자열 인퍼폴레이션은 내장된 각 표현식을 평가/계산하고 toString으로 대치시킵니다. 각 표현식 앞에는 “$”를 붙어야 합니다.

코드를 보면 println 내부에 s를 붙이고 문자열을 입력합니다. 그러면 스칼라 컴파일러에서 s 문자열 인퍼폴레이션으로 인식하고 $ 뒤에 붙은 표현식을 찾아 표현식을 평가한 후 toString을 호출합니다.

그래서 최종적으로 “Hello, reader!” 가 출력됩니다.

S 문자열 인터폴레이션에서 $ 뒤에 오는 표현식이 변수 하나라면 변수 이름만 적으면 되지만 그 외의 경우에는 중괄호로 묶어서 하나의 표현식이라는 것을 알려줘야 합니다.

Raw 문자열 인터폴레이션은 s와 동일하게 동작하고 문자열 이스케이프 시퀀스를 인식하지 않습니다.

F 문자열 인터폴레이션 또한 s와 동일하고 추가적으로 printf 스타일 형식 지정이 가능합니다.

이 문자열 인터폴레이션은 컴파일 시점에 코드를 재작성하는 형태로 구현되어 있고 컴파일러에서 식별자 바로 뒤에 문자열 리터럴이 오면 인터폴레이터로 취급하게 되어 있고 s, f, raw 처럼 커스텀하게 구현도 가능합니다.

 

 

스칼라에서 모든 연산자는 메소드입니다.

1+21 객체의 + 메소드를 호출한 것으로 + 메소드는 2라는 Int 형 객체를 파라미터를 받는 것과 같습니다.

이렇게 2라는 Int 형 객체를 파라미터로 받을 수 있는 것은 Int 클래스 내부에 Int형 인자를 받는 + 메소드가 정의되어 있기 때문입니다.

따라서 1+2 뿐만 아니라 1+2.0 또는 1+ 2L 과 같이 다른 타입의 리터럴에 대해 더하기를 하려면 해당 타입의 리터럴을 인자로 받는 메소드가 존재해야 합니다.

, Int 클래스에는 파라미터 타입에 따라 오버로드한 + 메소드가 여러 개 존재하게 됩니다. 그래서 1+2L도 가능합니다.

+라는 메소드를 일반적으로 생각해 보면 파라미터가 있는 메소드는 중위연산자처럼 표현가능 하다고 할 수 있습니다.

코드와 같이 indexOf 라는 하나의 파라미터를 받는 메서드는 중위 연산자 처럼 사용할 수 있습니다. 또한 여러 인자를 받는 다면 괄호로 묶어줍니다.

 

전위 연산자는 “unary_연산자" 메소드로 변경 가능합니다. 그래서 ! 연산자는 unary_! 되겠습니다. 만약 커스텀하게 연산자를 만들고 싶다면 unary_연산자 이름으로 메소드를 정의하면 됩니다.

인자를 취하지 않는 메소드는 후위 연산자처럼 사용가능 합니다.

코드를 보면 toLowerCases toLowerCase로 바꿀 수 있습니다.

 

두 객체가 같은 지 비교할 때는 “==“을 사용합니다. “==“연산자는 단순히 객체의 주소를 비교하는 지 않습니다.

동작 방식을 살펴보면 좌항이 null 인지 검사하고 null 이 아니라면 해당 객체의 equals 메소드를 호출합니다. 

자바의 경우와 다른 것을 알 수 있습니다. 자바 참조타입의 “==“연산자는 scala에서 “eq” 메소드가 되겠습니다.

그래서 아래 코드와 같이 참조가 다르지만 List 내부에 equals 메소드를 호출해 내부 값을 비교하기 때문에 결과값이 true가 됩니다.

또한 오른쪽과 같이 equals로 값을 비교하기 때문에 다른 타입간 비교도 가능합니다.

 

스칼라에서는 연산자 우선순위를 연산자의 첫 글자를 보고 정합니다.

왼쪽 표, 위쪽으로 갈수록 연산자 우선순위가 높습니다. 그래서 2+2*7 의 경우 첫 글자인 *하고 + * 의 우선순위가 높기 때문에 *가 먼저 수행됩니다.

+++, ***도 같습니다. 메소드 이름이 +++, ***이고 파라미터를 받으면 중위 연산자처럼 사용 가능하기 때문에 a+++b***c 라는 표현이 가능하고

메소드의 제일 앞 글자인 +* *의 우선순위가 높기 때문에 a+++(b***c) 로 계산됩니다.

첫 글자로 우선순위를 결정하는 방법에서 예외가 있는 데 “=“로 끝나는 메소드(할당 연산자)이고 비교 연산자가 아닐 경우입니다. 그 때는 해당 연산자 우선순위는 “=“과 같습니다.

그래서 x*=y+1 의 경우 단순히 첫 글자만 보면 x*=y 연산이 먼저 이루어져야 하지만 *=는 예외 사항이기 때문에 =과 우선순위가 같습니다.

그래서 x*= (y+1) 이 됩니다.

그리고 동일한 우선 순위가 여러 개일 때는 왼쪽부터 계산하지만 연산자가 “:”로 끝나면 오른쪽부터 계산합니다.

“:” 연산자는 보통 리스트를 추가할 때하는 것으로 “:” 연산자는 뒤에 있는 객체의 메소드로 호출되기 때문에 “:”로 끝나면 오른쪽부터 계산하게 됩니다.

 

왼쪽 표를 보면 기본 타입 객체에 대해 max, min, abs 등 유용한 메소드를 호출할 수 있습니다.

그런데 이 메소드들은 기본 타입 Int, String 같은 객체에 정의되어 있지 않습니다.

이들은 각 기본 타입 클래스의 래퍼 클래스에 정의되어 있습니다. 스칼라에서는 내부적으로 max, min 같은 메소드를 호출하면 암시적 변환을 해서 래퍼 클래스에 있는 메소드를 호출하게 합니다.

이에 대해서는 나중에 더 알아볼 예정입니다.

+ Recent posts