✏️[모던 자바 인 액션, 전문가를 위한 자바 8, 9, 10 기법 가이드] 스터디 관련 책 내용을 정리한 글입니다.
📌 이 장의 내용
- 스칼라 소개
- 자바와 스칼라의 관계
- 스칼라의 함수와 자바의 함수 비교
- 클래스와 트레이트
이 장에서는 스칼라 언어를 소개하고, 자바와 비교하며 함수형 프로그래밍의 다양한 기능을 살펴봅니다. 스칼라에서는 자바에 비해 더 다양하고 심화된 함수형 기능을 제공하며, 간결하고 가독성이 좋은 코드를 구현할 수 있다는 점을 확인할 수 있습니다. 이를 위해 스칼라로 간단한 프로그램을 구현하고 컬렉션을 다루는 방법, 함수, 클로저, 커링 등을 살펴보고, 인터페이스와 디폴트 메서드의 기능을 담당하는 트레이트라는 기능도 살펴봅니다.
20.1 스칼라 소개
이 절에서는 스칼라의 특징을 간단한 "Hello World" 예제를 통해 살펴보고, 스칼라가 지원하는 리스트, 집합, 맵, 스트림, 튜플, 옵션 등의 자료구조를 자바의 자료구조와 비교하며 소개합니다. 또한, 자바의 인터페이스를 대체하는 스칼라의 트레이트를 소개하면서 객체 인스턴스화할 때 메서드 상속 기능을 지원한다는 것을 설명합니다
20.1.1 Hello beer
스칼라와 자바로 각각 맥주 출력하는 'Hello World' 예제를 비교합니다.
Hello 2 bottles of beer
Hello 3 bottles of beer
Hello 4 bottles of beer
Hello 5 bottles of beer
Hello 6 bottles of beer
명령형 스칼라
다음은 명령형으로 위 문장을 출력하는 스칼라 프로그램입니다.
object Beer {
def main(args: Array[String] {
var n : Int = 2
while(n <= 6) {
println(s"Hello ${n} bottles ${n} bottles of beer")
n += 1
}
}
}
스칼라의 Hello World 예제는 자바와 매우 유사하며, 문자열 보간법과 object를 이용하여 싱글턴 객체를 만들 수 있음을 보여줍니다. 스칼라에서는 object를 이용하여 바로 싱글턴 객체를 생성할 수 있으며, object 내부에 선언된 메서드는 정적 메서드로 간주됩니다. 문자열 보간법은 문자열에 변수와 표현식을 직접 삽입하여 가독성을 높이는 기능입니다. 이를 통해 스칼라는 자바보다 더욱 간결하고 가독성이 높은 코드를 작성할 수 있습니다.
함수형 스칼라
이전 예제 코드를 자바의 함수형으로는 다음처럼 구현할 수 있습니다.
public class Foo {
public static void main(String[] args) {
IntStream.rangeClosed(2, 6)
.forEach(n -> System.out.println("Hello " + n +
" bottles of beer"));
}
}
스칼라로는 다음처럼 구현할 수 있습니다.
object Beer {
def main(args: Array[String]) {
2 to 6 foreach { n => println(s"Hello ${n} bottles of beer") }
}
}
스칼라에서는 자바와 비슷한 문법으로 코드를 작성할 수 있지만, 자바 코드보다 간결합니다. 모든 것이 객체이며, 기본형이 없는 완전한 객체지향 언어입니다. Int 객체는 to 메서드를 지원하여 범위를 반환하며, forEach 메서드는 람다 표현식을 인수로 받아 각 요소에 적용합니다. 람다 표현식 문법은 자바와 비슷하지만 -> 대신 =>를 사용합니다. 이를 통해 스칼라는 함수형 프로그래밍을 지원합니다.
20.1.2 기본 자료구조 : 리스트, 집합, 맵, 튜플, 스트림, 옵션
이번 절에서는 스칼라에서 컬렉션을 다루는 방법을 살펴보면서, 자바와의 차이점을 비교하고 있습니다.
컬렉션 만들기
불변과 가변
컬렉션 사용하기
튜플
스트림
옵션
20.2 함수
스칼라의 함수는 작업을 수행하는 명령어 그룹이며, 함수형 프로그래밍의 중요한 기초입니다. 자바에서는 함수에 대해 메서드라는 용어를 사용하며, 람다 표현식도 지원합니다. 스칼라는 자바보다 다양한 함수 기능을 제공합니다. 이번 절에서는 스칼라에서 제공하는 함수 기능들을 살펴봅니다.
- 함수 형식 : 함수 형식은 3장에서 설명한 자바 함수 디스크립터의 개념을 표현하는 편의 문법입니다.
- 익명 함수 : 익명 함수는 자바의 람다 표현식과 달리 비지역 변수 기록에 제한을 받지 않습니다.
- 커링 지원 : 커링은 여러 인수를 받는 함수를 일부 인수를 받는 여러 함수로 분리하는 기법입니다.
20.2.1 스칼라의 일급 함수
스칼라의 함수는 일급값으로, 인수로 전달하거나 결과로 반환하거나 변수에 저장할 수 있습니다. 자바의 메서드 참조와 람다 표현식도 일급 함수입니다. 스칼라의 일급 함수 예제로, 트윗 문자열 리스트에서 Java가 포함되어 있거나 짧은 문자열 등의 조건으로 트윗을 필터링할 수 있는 두 가지 프레디케이트를 살펴봅니다.
def isJavaMentioned(tweet: String) : Boolean = tweet.contains("Java")
def isShortTweet(tweet: String) : Boolean = tweet.length() < 20
이들 메서드를 스칼라에서 기본 제공하는 filter로 바로 전달할 수 있습니다.
val tweets = List(
"I love the new features in Java 8",
"How's it going?",
"An SQL query walks into a bar, sees two tables ands says 'Can I join you?'"
)
tweets.filter(isJavaMentioned).foreach(println)
tweets.filter(isShortTweet).foreach(println)
// 스칼라에서 제공하는 내장 메서드 filter의 시그니처를 확인합니다.
def filter[T](p: (T) => Boolean): List[T]
스칼라에서 함수형 인터페이스를 사용하는 대신, (T) => Boolean과 같은 함수 형식을 사용합니다. 이는 T 형식의 객체를 받아 Boolean 값을 반환하는 함수를 의미합니다. 이 함수 형식은 자바의 Predicate<T>나 Function <T, Boolean>과 유사한 역할을 합니다. 자바 언어 설계자는 기존 버전의 언어와의 일관성을 유지하기 위해 이와 같은 함수 형식을 지원하지 않기로 결정했습니다.
20.2.2 익명 함수와 클로저
스칼라에서는 람다 표현식과 비슷한 문법을 사용하여 익명 함수를 지원합니다. 예를 들어, 트윗이 긴지를 확인하는 isLongTweet이라는 변수에 익명 함수를 할당하는 방법이 있습니다.
val isLongTweet : String => Boolean =
(tweet : String) => tweet.length() > 60
스칼라에서도 자바와 마찬가지로 람다 표현식을 사용하여 함수형 인터페이스의 인스턴스를 생성할 수 있습니다. 이때 위 코드는 apply 메서드의 구현을 제공하는 scala.Function1 형식의 익명 클래스를 축약한 것입니다.
val isLongTweet : String => Boolean =
new Function1[String, Boolean] {
def apply(tweet: String): Boolean = tweet.length() > 60
}
isLongTweet 변수는 Function1 형식의 객체를 저장하므로 다음처럼 apply 메서드를 호출할 수 있습니다.
isLongTweet.apply("A very short tweet")
자바로는 다음처럼 구현할 수 있습니다.
Function<String, Boolean> isLongTweet = (String s) -> s.length() > 60;
boolean long = isLongTweet.apply("A very short tweet");
자바에서는 람다 표현식을 사용하기 위해 Predicate, Function, Consumer 등의 함수형 인터페이스를 제공합니다. 스칼라에서도 함수형 프로그래밍을 지원하기 위해 Function0부터 Function22까지의 트레이트를 제공합니다. 이를 사용하여 함수 호출과 같이 apply 메서드를 호출할 수 있습니다. 이러한 기능들은 스칼라의 강력한 함수형 프로그래밍 지원 기능 중 하나입니다.
isLongTweet("A very short tweet")
스칼라에서 컴파일러는 f(a)를 f.apply(a)로 변환하고, f(a1,..., an)도 f.apply(a1,..., an)으로 변환합니다. 이 변환은 apply 메서드를 지원하는 객체 f를 사용합니다.
클로저
3장에서는 자바의 람다 표현식이 클로저와 다른 점에 대해 설명하고 있습니다. 클로저는 함수의 인스턴스로서, 자유롭게 비지역 변수를 참조할 수 있습니다. 반면 자바의 람다 표현식은 람다가 정의된 메서드의 지역 변수를 고칠 수 없으며, 암시적으로 final로 취급됩니다. 즉, 람다는 변수가 아닌 값을 닫는다는 것입니다.
스칼라에서는 익명 함수가 변수를 캡처할 수 있습니다. 이를 통해 자유롭게 비지역 변수를 참조할 수 있으며, 이들 변수를 변경할 수도 있습니다.
def main(args: Array[String]) {
var count = 0
var inc = () => count += 1
inc()
println(count)
inc()
println(count)
}
하지만 자바로 구현한 아래 코드에서 count는 암시적으로 final이 되므로 컴파일 에러가 발생합니다.
public static void main(String[] args) {
int count = 0;
Runnable inc = () -> count += 1;
inc.run();
System.out.println(count);
inc.run();
}
7, 18, 19장에서는 프로그램의 유지보수와 병렬화를 위해 불필요한 변화를 피하라고 조언하며, 클로저 기능은 필요할 때에만 사용하는 것이 바람직하다는 것을 강조하고 있습니다.
20.2.3 커링
19장에서는 커링(Currying)이라는 함수형 프로그래밍 기법을 소개하고, 여러 인수를 받는 함수를 인수의 일부를 받는 여러 함수로 분할하여 일반화하는 방법을 설명하고 있습니다. 스칼라에서는 기존 함수를 쉽게 커링할 수 있는 방법을 제공합니다.
예를 들어, 이전에 자바 예제를 통해 보였던 두 정수를 곱하는 간단한 메서드를 커리화할 수 있습니다. 이를 통해 함수를 인자로 전달하는 고차 함수를 작성하거나, 함수를 부분 적용할 수 있습니다.
static int multiply(int x, int y) {
return x * y;
}
int r = multiply(2, 10);
이 함수는 전
static Function<Integer, Integer> multiplyCurry(int x) {
return (Integer y) -> x * y;
}
달된 모든 인수를 사용합니다. 다음 예제처럼 다른 함수를 반환하도록 위의 multiply 메서드를 분할할 수 있습니다.
20.3 클래스와 트레이트
클래스와 인터페이스는 애플리케이션 설계에서 중요한 역할을 하며, 스칼라의 클래스와 인터페이스는 자바보다 더 유연성이 높습니다.
20.3.1 간결성을 제공하는 스칼라의 클래스
스칼라는 자바와 비슷한 문법적 구조를 가지며 완전한 객체지향 언어로, 클래스를 만들고 인스턴스화할 수 있습니다.
class Hello {
def sayThankYou() {
println("Thanks for reading our book")
}
}
val h = new Hello()
h.sayThankYou()
게터와 세터
필드를 가진 자바 클래스에서는 생성자와 필드 수만큼의 게터와 세터를 선언하는 번거로운 작업이 필요합니다. 이는 대규모 자바 애플리케이션에서 흔한 일입니다. 예를 들어 다음과 같은 Student 클래스가 있습니다.
public class Student {
private String name;
private int id;
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public vid setId(int id) {
this.id = id;
}
}
자바에서는 간단한 클래스를 정의해도 생성자, 필드 초기화, 게터, 세터 등을 정의하는 데 20줄 이상의 코드가 필요합니다. 반면 스칼라에서는 이러한 코드들이 암시적으로 생성되어 클래스를 정의하는 과정이 훨씬 간단합니다.
class Student(var name: String, var id: Int)
val s = new Student("Raoul", 1)
println(s.name)
s.id = 1337
println(s.id)
20.3.2 스칼라 트레이트와 자바 인터페이스
스칼라의 트레이트는 자바의 인터페이스를 대체하며 추상 메서드와 기본 구현을 가진 메서드를 모두 정의할 수 있습니다. 다중 상속을 지원하므로 자바의 인터페이스와 디폴트 메서드 기능이 합쳐진 것으로 이해할 수 있지만, 추상 클래스와 같지는 않습니다. 자바에서는 디폴트 메서드를 통해 다중 상속이 가능하지만 상태는 다중 상속할 수 없습니다.
Sized 트레이트는 size 가변 필드와 isEmpty 메서드 기본 구현을 제공합니다.
trait Sized {
var size : Int = 0;
def isEmpty() = size == 0
}
Empty 클래스는 트레이트와 조합하여 항상 0의 크기를 갖도록 정의됩니다.
class Empty extends Sized
println(new Empty().isEmpty())
스칼라의 객체 트레이트는 인스턴스화 과정에서도 조합이 가능합니다. Box 클래스를 만들고 Box 인스턴스에 Sized 트레이트를 조합하여 동작을 지원하도록 결정할 수 있습니다.
class Box
val b1 = new Box() with Sized
println(b1.isEmpty())
val b2 = new Box()
b2.isEmpty()
'Study > Modern Java in Action[2회독]' 카테고리의 다른 글
[모던 자바 인 액션] 2장 동작 파라미터화 코드 전달하기 (0) | 2023.12.02 |
---|---|
[모던 자바 인 액션] 1장 Java 8, 9, 10, 11 : 무슨 일이 일어나고 있는가? (0) | 2023.11.24 |
[모던 자바 인 액션] 19장 함수형 프로그래밍 기법 (0) | 2023.04.02 |
[모던 자바 인 액션] 18장 함수형 관점으로 생각하기 (0) | 2023.04.02 |
[모던 자바 인 액션] 17장 리액티브 프로그래밍 (0) | 2023.04.02 |