입문자부터 고급 개발자까지, 모두가 기다린 실전 안내서 P r o g r a m m i n g
S c a l a
2 n d
실용적인 스칼라 활용법을 익히는 가장 확실한 실전 바이블
E d i t i o n
스칼라 2.11.x 버전 기준
Programming
이 책은 다양한 코드 예제가 포함된 실전 바이블이다. 초보자와 고급 사용자를 한데 아우를 뿐 아니라, 실 제 개발자들의 실용적 관심사에 초점을 맞추어 실전 활용법을 안내한다. 최신 스칼라 언어의 특징부터 패턴 매칭, for 내장, 고급 함수형 프로그래밍 등의 새로운 내용을 소개한다. 또한 스칼라 명령행 도구와 서드파티
Scala
도구, 라이브러리, IDE의 스칼라 지원에 대해 살펴보고, 그 과정에서 생산성을 발휘하는 방법을 제시한다.
•간결하고 유연한 문법으로 빠르게 프로그래밍하기 •함수형 프로그래밍의 기초와 고급 기법 익히기 •함수 컴비네이터로 빅데이터 애플리케이션 작성하기 •트레이트 혼합을 활용하고, 데이터 추출에 패턴 매칭 사용하기 •함수형과 객체 지향 프로그래밍 개념을 한데 묶는 정교한 타입 시스템 배우기 •액터 기반 동시성 모델 아카와, 비동기 코드 작성에 유용한 퓨처 다루기
2nd Edition
관련 도서
프로그래밍 스칼라
•도메인 특화 언어(DSL) 개발법 이해하기 •확장성이 좋으면서 튼튼한 애플리케이션 설계 기법 배우기
이 책은 매우 실용적이라는 점에서 나를 흥분시킨다. 저자들은 재미있는 예제와 설명으로 스칼라 언어를 소개할 뿐 아니라, 스칼라를 실제 어떻게 활용할지 보여준다는 측면에서도 멋진 작품을 만들어냈다. 실제 목표를 달성하 고자 하는 프로그래머를 위한 책이다.
관련 도서 브루스 테이트의 세븐 랭귀지
폴리글랏 프로그래밍
누구나 쉽게 배우는 스칼라 e-Book
딘 왐플러, 알렉스 페인 지음 오현석 옮김
요나스 보네르, Typesafe 공동 창립자
프로그래밍 언어 기타
예제 소스 http://www.hanbit.co.kr/exam/2275
정가 48,000원
딘 왐플러, 알렉스 페인 지음 오현석 옮김
| 표지 설명 |
표지에 있는 동물은 말레이 테이퍼(학명: Tapirus indicus )다. 다른 이름으로 아시아 테이퍼라고도 부른다. 이 동물은 검은색과 흰색 털이 난 굽이 있는 동물 로, 돼지와 비슷한 둥글고 다부진 체격을 하고 있다. 몸길이 약 2~2.4m, 몸무게 약 250~320kg인 말레이 테이퍼는 네 종의 테이퍼 중에서 가장 큰 종이며, 동남아시아의 열대우림지역에서 서식한다. 말레이 테이퍼의 모습은 눈에 띈다. 몸의 전반부와 네 다리는 검은빛을 띤 갈 색이고, 몸의 중앙부는 흰 안장 모양으로 되어 있다. 이런 패턴은 달빛이 비치는 정글에 서는 완벽한 위장이 된다. 다른 신체 특징으로는 두꺼운 가죽, 땅딸막한 꼬리, 짧고 유연
한 코를 들 수 있다. 생김새와 달리 말레이 테이퍼는 나무를 잘 타며, 달리는 속도도 빠르다. 테이퍼는 단독으로 서식하며 야행성 동물이다. 시력이 매우 나쁘기 때문에 주로 후각과 청각에 의존하여 광범위한 지역에 서 먹이를 탐색하고, 다른 테이퍼의 체취를 추적하여 고음의 날카로운 소리로 서로 의사소통한다. 말레이 테이퍼의 포식자 로는 사자, 표범, 인간 등을 들 수 있으며, 서식지 파괴와 남획으로 멸종 위기에 처했다.
프로그래밍 스칼라 실용적인 스칼라 활용법을 익히는 가장 확실한 실전 바이블 초판발행 2016년 6월 1일 지은이 딘 왐플러, 알렉스 페인 / 옮긴이 오현석 / 펴낸이 김태헌 펴낸곳 한빛미디어 (주) / 주소 서울시 마포구 양화로 7길 83 한빛미디어(주) IT출판부 전화 02 – 325 – 5544 / 팩스 02 – 336 – 7124 등록 1999년 6월 24일 제10 – 1779호 / ISBN 978 – 89 – 6848 – 275 – 5
93000
총괄 전태호 / 책임편집 김창수 / 기획·편집 박지영 / 교정·조판 김철수 디자인 표지 강은영, 내지 김미현 영업 김형진, 김진불, 조유미 / 마케팅 박상용, 송경석, 변지영 / 제작 박성우 이 책에 대한 의견이나 오탈자 및 잘못된 내용에 대한 수정 정보는 한빛미디어(주)의 홈페이지나 아래 이메일로 알려주십시오. 잘못된 책은 구입하신 서점에서 교환해드립니다. 책값은 뒤표지에 표시되어 있습니다. 한빛미디어 홈페이지 www.hanbit.co.kr / 이메일 ask@hanbit.co.kr © 2016 Hanbit Media Inc.
Authorized Korean translation of the English edition of Programming Scala , 2nd Edition ISBN 9781491949856 © 2015 Dean Wampler and Alex Payne This translation is published and sold by permission of O’Reilly Media, Inc., which owns or controls all rights to publish and sell the same. 이 책의 저작권은 오라일리와 한빛미디어(주)에 있습니다. 저작권법에 의해 한국내에서 보호를 받는 저작물이므로 무단전재와 복제를 금합니다.
지금 하지 않으면 할 수 없는 일이 있습니다. 책으로 펴내고 싶은 아이디어나 원고를 메일 ( writer@hanbit.co.kr ) 로 보내주세요. 한빛미디어(주)는 여러분의 소중한 경험과 지식을 기다리고 있습니다.
프로그래밍 스칼라
에버렛 로런스 왐플러Everette Lawrence Wampler에게 바침
1931년 8월 38일 ~ 2013년 5월 9일 _딘
지은이·옮긴이 소개
지은이
딘 왐플러 Dean Wampler
타입세이프Typesafe 사의 빅데이터 제품 아키텍트다. 그는 스칼라와 함수형 프로그래밍이 빅데 이터 애플리케이션에 이상적인 도구라고 늘 주장해온 옹호자다. 딘은 『하이브 완벽 가이드』의 공저자며, 『자바 개발자를 위한 함수형 프로그래밍』(이상 한빛미디어)의 저자다. 그는 여러 오 픈 소스 프로젝트에 기여했으며, 다양한 기술 콘퍼런스와 시카고 지역의 여러 사용자 그룹을 함 께 조직했다. @deanwampler를 통해 트위터에서 그를 만날 수 있다.
지은이
알렉스 페인 Alex Payne
초기 단계의 스타트업과 주로 작업하는 개발자이자 저술가며 엔젤 투자가다. 그는 트위터의 플 랫폼 리드로 스칼라를 사용했고, 온라인 뱅킹 서비스인 심플Simple 사의 CTO로 스칼라 제품을 출시했다. 알렉스는 새로운 프로그래밍 언어와 개발 도구를 보여주는 연례 ‘최신 언어 콘퍼런 스Emerging Language conference ’를 조직했다. 트위터 계정 @al3x나 웹사이트 https://al3x.net에서 그를 찾을 수 있다.
옮긴이
오현석
KAIST에서 전산학 학사와 석사 학위(프로그래밍 언어 연구실)를 취득했다. 삼성메디슨, 비트 앤펄스 등에서 UI 개발자와 개발 팀장을 지냈고, 호주에서 프리랜서 C++/풀스택 개발자로 일 하고 있다. 웹이나 모바일 등의 분야에서 값 중심의 프로그래밍을 통해 오류 발생 가능성이 더 적으면서 유지보수가 편한 프로그램을 작성하는 방법과 이를 지원하는 여러 도구를 만드는 일 에 관심이 많다. 최근에는 스칼라와 파이썬을 사용한 대규모 병렬 처리나 액터를 활용한 분산 처 리 등을 공부하는 중이다. 『Programming in Scala (Second Edition ) 한국어판』, 『스칼라 동시성 프로그래밍』(이상 에이콘), 『시스템 성능 분석과 최적화』(위키북스) 등을 번역했다.
5
추천의 글
여러분은 소프트웨어 개발 과정에서 맞닥뜨리는 여러 가지 문제로 인해 한계에 막혀 해결책을 모색하는 길에서 스칼라를 만났을 수 있습니다. 어쩌면 그 과정에서 스칼라에 강렬하게 매료되 어 차근차근 정복해나갈 채비를 마쳤을 수도 있습니다. 반면, 어떤 분들은 스칼라 자체에는 아 직 큰 기대가 없을 수 있습니다. 어쩌면 스칼라라는 언어와 그 생태계가 개발에 도움이 될지도 모른다는 막연한 기대감 정도는 품고 있을지도 모르겠습니다. 소프트웨어 개발에서 가장 도전적인 부분은 복잡도를 다루는 것입니다. 복잡도를 다룰 때는 단 순히 코드의 양을 줄이는 것만이 능사는 아닙니다. 소스 코드 구석구석에 녹아 있는 보편적이 며 간결한 법칙이 프로그래머에게 편하게 받아들여지는가의 문제입니다. 그 일이 너무 어렵게 느껴져서 이제는 한계라는 느낌을 받은 경험이 있다면, 어쩌면 그것은 여러분의 탓이 아닌 언 어의 한계 때문일 수 있습니다. 실제로 스칼라는 여러분에게 해결책이 될 수 있습니다. 그러한 판단은 온전히 여러분의 몫이지 만, 이를 실천에 옮기기 위해서는 스칼라를 공부해야 한다는 문제가 꼬리를 물고 이어집니다. 보통 스칼라의 기본적인 기능을 다루는 것만으로도 펼치기 부담스러운 책의 두께가 되어버립 니다. 객체지향 구현 방식, 함수형 프로그래밍, 타입 시스템 각각은 그리 단순한 주제가 아니기 때문입니다. 따라서 그 참맛을 알기까지는 약간의 시간이 필요할 수 있습니다. 그런 점에서 이 책이 가치 있다고 생각합니다. 스칼라의 유용성을 파악하는 효과적인 방법은 각 주제에 대한 개념을 익히고, 실제 코드를 실행한 후 결과를 확인하며, 실제 사례에서 어떻게 응 용될 수 있는지 경험에 비춰 반추해보고 개선 방법을 상상해보는 것입니다. 이 책의 예제 코드 는 짧고 간결하며 대부분 독립적으로 빠르게 실행해볼 수 있습니다. 그래서 책에 나오는 모든 코드를 직접 실행해보기를 강하게 권장합니다. 물론 눈으로만 훑어보는 것도 가능합니다. 글의 흐름이나 문체가 부드럽게 구성되어 있으므로, 흐름만 파악하는 수준으로 짧은 시간에 단숨에 읽어낼 수 있습니다.
6
이 책의 역자인 오현석 님이 번역을 진행하면서 고민했던 용어 선택의 문제, 그리고 번역 전반에 대한 고민과 토론의 흔적을 곳곳에서 확인할 수 있었고, 그러한 노력과 열정을 바탕으로 이 책이 완성되었다는 것을 느낄 수 있었습니다. 보통 번역서를 읽다 도무지 이해가 되지 않는 부분을 만 나면, 번역에 문제가 있는 것은 아닐까 하는 의심이 들곤 합니다. 적어도 이 책을 읽을 때는 그런 의심 없이 내용에만 집중할 수 있었다는 개인적 경험을 강조하고 싶습니다. 최정열 라 스칼라 코딩단 운영자
7
추천의 글
프로그래머로서의 내 경력에 일관된 주제가 있다면 소프트웨어 작성을 지원하는 더 나은 추상 화와 도구를 찾기 위한 노력이라고 말할 수 있다. 여러 해에 걸쳐 나는 다른 무엇보다 상호 조 합성이란 특성에 더 많은 가치를 부여해왔다. 조합성이 좋은 코드를 작성할 수 있다면 소프트 웨어 개발자들이 높이 평가하는 다른 특성(직교성, 느슨한 결합, 높은 응집도)은 이미 그 코드 에 들어 있기 마련이다. 모든 것은 서로 연관되어 있다. 몇 년 전 스칼라를 처음 봤을 때 나는 그 상호 조합성에 가장 큰 감동을 받았다. 설계 시 아주 우아한 결정을 내리고, 객체지향과 함수형 프로그래밍 세계로부터 단순하지만 강 력한 추상화를 도입함으로써, 마틴 오더스키Martin Odersky 는 응집도와 직교성이 높고 소프트웨 어 설계의 모든 방면에서 높은 조합성을 이끄는 깊은 추상화가 가능한 언어를 만들 수 있었다. 스칼라는 쓰는 사람에 따라 정말로 규모 확장이 가능한 언어SCAlable LAnguage (SCALA )로, 스크 립팅부터 대규모 기업 애플리케이션이나 미들웨어에 이르기까지 사용 가능하다. 스칼라는 대학에서 생겨난 언어지만, 실제 환경에서 사용하기에 충분한 실용적이고 실제적인 언어로 자라왔다. 이 책은 매우 실용적이라는 점에서 나를 흥분시킨다. 딘은 재미있는 예제와 설명을 통해 스칼 라 언어를 소개할 뿐 아니라, 스칼라를 실제 어떤 맥락에서 활용할지 보여준다는 측면에서도 멋 진 작품을 만들어냈다. 이 책은 실제 목표를 달성하고자 하는 프로그래머를 위해 쓴 책이다. 딘과 나는 몇 년간 함께 관점 중심aspect oriented 프로그래밍 커뮤니티의 일원이었다. 깊은 학문적 인 사고와 목표를 달성하기 위한 실용성을 잘 조화시키는 사람이 드문데, 딘은 그런 일을 해냈다. 여러분은 믹스인mixin 과 함수 합성을 활용해서 재사용 가능한 구성 요소를 만드는 방법, 아카 Akka
를 사용해서 반응형 애플리케이션reactive application 을 작성하는 방법, 매크로나 상류 타입higher
kinded type
등 스칼라의 고급 기능을 효율적으로 활용하는 방법, 풍부하고 유연성과 표현력이 큰
스칼라 구문 구조를 활용해서 도메인 특화 언어domain specific language (DSL )를 만드는 방법, 스칼
8
라 코드를 효율적으로 테스트하는 방법, 스칼라를 활용해서 빅데이터Big Data 문제에 쉽게 접근 하는 방법 등 수많은 내용을 살펴보게 될 것이다. 모쪼록 이 책을 즐기기 바란다. 나 자신도 이 책을 읽는 과정이 정말 즐거웠다. 요나스 보네르Jonas Bonér 타입세이프 공동 창립자, CTO, 2014년 8월
9
옮긴이의 말
스칼라가 만들어진 지 12년이 지났다. 그동안 스칼라는 차근차근 세를 불려왔다. 최근에 스파 크Spark 가 관심을 끌면서 스칼라를 사용하거나 스칼라에 관심을 갖는 개발자가 빠르게 늘고 있 다. 또한 자바 8에 아주 기본적인 함수형 프로그래밍 기능이 추가되면서, 자바 개발자 중에서도 더 나은 함수형 프로그래밍 언어를 원하는 경우가 생겨났고, 그에 따라 클로저나 스칼라 등의
JVM 기반 언어에 눈길을 주는 경우가 생겨나고 있다. 스칼라를 배울 때 가장 좋은 방법은 무엇일까? 스칼라에는 REPL이 있으므로 이를 활용해서 대화식으로 코드를 입력하고 바로 컴파일러로 컴파일해서 실행한 결과를 살펴볼 수 있다. 이런
REPL을 활용하면서 스칼라의 각종 개념을 바로바로 실행해가며 배우는 것이 가장 좋은 방식 이라고 할 수 있다. 책을 백 번 읽는 것보다 한 번 실행해보는 것이 더 낫다는 것은 프로그래머 들 사이에 널리 받아들여지는 속담이 아니던가! 이 책은 그런 면에서 최고의 스칼라 자습서라고 할 수 있다. 이 책은 코딩하는 법을 가르쳐주지 는 않는다. 하지만 이미 어느 정도 프로그래밍을 할 줄 아는 독자는 스칼라의 여러 개념을 REPL 을 통해 바로바로 시험해보면서 몸으로 체험할 수 있다. 짧지만 핵심을 찌르는 예제와 예제에 대 한 간결하고 정확한 설명을 제공하므로, REPL에서 예제를 하나하나 실행하면서 설명을 이해 하다 보면 어느덧 스칼라의 기초가 탄탄히 다져져 있다는 사실을 알게 될 것이다. 비록 이 책의 원서가 스칼라 2.11.2를 기준으로 쓰였지만, 2016년 5월 기준 최신 스칼라 버전인 2.11.8과 스칼라 2.11.2가 언어적으로 차이가 없는 만큼, 이 책이 종합적인 스칼라 서적 중 가장 최신 서 적이라고 감히 말할 수 있다. 새로운 언어를 배우면, 과거부터 사용해왔던 언어를 더 잘 이해할 수 있게 되고, 새로운 언어를 통해 문제에 접근하는 다른 방법을 배움으로써 더 나은 프로그래머가 될 수 있는 발판이 마련 될 것이다. 스칼라를 배우고, 또한 스칼라 책을 번역하면서 나 자신도 그런 경험을 할 수 있었 다. 독자 여러분도 이 책을 통해 스칼라를 배울 뿐 아니라, 과거의 언어를 이해하는 새로운 눈
10
을 뜨고, 문제를 해결하는 또 다른 방법을 익히며, 코딩이 주는 기쁨을 배가하는 행복한 경험을 하기 바란다. 마지막으로 베타리딩에 참여해주신 강민우, 강인철, 김경범, 김나헌, 김도남, 김지수, 김현준 님 께 감사드린다. 무엇보다 옆에서 함께해준 아내 계영과 세 아이 혜원, 성원, 정원에게 사랑한다 는 말을 남기고 싶다. 오현석
11
이 책에 대하여
『프로그래밍 스칼라』는 현대적인 객체 모델, 함수형 프로그래밍functional programming (FP ), 고급 타입 시스템의 모든 이점을 제공하면서 그동안 업계에서 발전시켜온 자바 가상 머신Java Virtual Machine
(JVM )의 기능을 활용하는 흥미롭고 강력한 언어를 소개한다. 이 책은 종합적으로 스칼
라를 다루고, 다양한 코드 예제를 제공하며, 여러분이 빠르게 스칼라를 생산적으로 활용할 수 있 도록 돕고, 스칼라의 어떤 특성이 분산 처리와 동시성을 지원하는 최근의 확장 가능한 분산 컴포 넌트 기반의 애플리케이션에 이상적인 언어로 만들어주는지 설명한다. 또한 JVM의 고급 기능 을 스칼라가 프로그래밍 언어를 위한 플랫폼으로 어떻게 활용하는지 배우게 될 것이다. 프로그래밍 언어가 유명해지는 이유는 다양하다. 때로 프로그래머가 사용하는 플랫폼에 따라 선 호하는 언어가 정해지거나, 회사에서 지정한 언어를 사용하는 경우도 있다. 대부분의 맥 OS 프 로그래머는 오브젝티브-C를 사용한다. 대부분의 윈도우 사용자는 C++와 닷넷을 사용한다. 대부분의 임베디드 시스템 개발자는 C와 C++를 사용한다. 때로는 기술적인 매력에서 비롯된 유명세가 유행이나 광신을 낳기도 한다. C++, 자바, 루비는 프로그래머 사이에 환상적인 헌신의 대상으로 자리 잡아왔다. 어떤 경우에는 언어가 시대와 맞아떨어져서 유명해지기도 한다. 자바는 처음에 브라우저 기반 의 풍부한 클라이언트 애플리케이션rich client application에 아주 적합한 언어로 여겨졌다. 스몰토 크Smalltalk 는 객체지향이 프로그래밍 모델의 주류로 부상하면서 객체지향 프로그래밍의 정수 essence
로 자리 잡았다.
요즘에는 동시성, 플랫폼 다양성, 무중단 서비스, 결코 줄어들지 않는 개발 스케줄 등으로 인해 함수형 프로그래밍에 대한 관심이 늘고 있다. 객체지향이 주도하던 시대가 어쩌면 끝날 수 있 을 것 같아 보인다. 여러 패러다임을 혼합하는 일이 흔하며, 때로는 필수적이기까지하다. 우리가 다른 언어보다 스칼라에 매력을 느끼는 이유는, 오늘날 구축해야 하는 여러 인터넷과 엔 터프라이즈 애플리케이션을 위한 범용 언어에 필요한 가장 좋은 특성을 여럿 포함하기 때문이 다. 신뢰성, 고성능, 높은 수준의 동시성 지원 등이 바로 그런 특성이다.
12
스칼라는 다중 패러다임multiparadigm 언어다. 객체지향과 함수형 프로그래밍 접근 방법을 모두 지 원한다. 스칼라는 짧은 스크립트로부터 대규모 컴포넌트 기반 애플리케이션에 이르기까지 적합 한 규모 확장성을 제공한다. 스칼라는 전 세계 전산 학계의 최신 아이디어를 아우르는 복잡한 언 어다. 그렇지만 스칼라는 실용적이다. 스칼라의 창시자인 마틴 오더스키는 여러 해에 걸쳐 자바 개발에 참여해왔고, 프로 개발자의 필요를 잘 이해하고 있다. 두 저자 모두 스칼라에 매료되었다. 스칼라의 간결성, 우아함, 표현성 높은 문법, 사용 가능한 도 구가 폭넓다는 점 등이 그 이유다. 이 책에서 우리는 이러한 특성이 어떻게 스칼라를 매혹적이 고 필수적인 언어로 만들어주는지 보여주기 위해 노력할 것이다. 이 책은 스칼라에 대한 전반적인 지식을 빠르게 얻고 싶어 하는 경험 많은 개발자에게 적합하 다. 스칼라가 현재 사용 중인 언어를 보완하거나 대체할 만한 언어인지 고려하는 중인지도 모 르겠다. 어쩌면 이미 스칼라를 사용하기로 결정해서, 그 특성을 배우고 제대로 사용하는 법을 익 힐 필요가 있을지도 모르겠다. 어느 경우이더라도 이 책이 스칼라라는 강력한 언어를 이해하기 쉬운 방식으로 조명할 수 있었으면 한다. 우리는 독자 여러분이 객체지향 프로그래밍을 잘 알고 있다고 가정한다. 하지만 함수형 프로 그래밍을 접해봤다는 가정은 하지 않는다. 또한 프로그래밍 언어를 하나 이상 사용해본 것으로 간주한다. 자바, C#, 루비, 또는 다른 언어의 특성과 스칼라의 특성을 나란히 비교할 것이다. 이런 언어를 알고 있다면 스칼라에서 기존 언어와 유사한 기능을 발견할 수 있을 것이다. 하지 만 보지 못했던 전혀 새로운 기능도 찾게 될 것이다. 여러분이 객체지향이나 함수형 프로그래밍 중 어느 배경으로부터 왔건, 스칼라는 두 패러다임 을 우아하게 조합하는 방법과 두 패러다임의 상호 보완적 특성을 보여줄 것이다. 예제를 통해
OOP와 FP 기법을 다양한 설계 문제에 언제 어떻게 적용할지 이해할 수 있을 것이다. 여러분도 스칼라에 매료되길 바란다. 스칼라가 여러분이 매일 사용할 언어로 자리 잡지는 못한 다고 할지라도, 여러분이 실제 사용하는 언어가 무엇인지와 관계없이 스칼라로부터 통찰을 얻 기 바란다.
13
이 책을 쓰는 현재, 스칼라 버전은 2.11.2이고 스칼라의 창시자인 마틴 오더스키와 액터actor 기 반 동시성 프레임워크인 아카Akka를 만든 요나스 보네르Jonas Bonér 는 스칼라와 스칼라로 만든 도 구의 사용을 촉진하기 위해 타입세이프 사를 공동 창립했다. 스칼라에 대한 책도 많이 나왔다. 스칼라 초보자를 위한 훌륭한 안내서가 많이 나왔으며, 고급 개발자를 위한 책도 몇 가지 나왔다. 사전처럼 사용할 수 있는 참고서로는 『Programming in
Scala (Second Edition ) 한국어판』(에이콘, 2014 )이 있다. 하지만 나는 이 책이 독특한 위치를 차지하리라 본다. 이 책은 스칼라와 그 생태계에 대한 종합 안내서며, 초보자와 고급 사용자를 아우르고, 실제 프로 개발자들의 실용적인 관심사에 초점을 맞추고 있기 때문이다. 점점 더 많은 조직이 스칼라를 사용 중이다. 또한 이제는 대부분의 자바 개발자가 스칼라에 대해 들어본 적이 있다. 몇 가지 질문을 계속 들어왔다. 스칼라가 복잡한가? 스칼라에서 볼 수 있는 여러 주요 특징이 자바 8에 이미 이식된 지금도 스칼라 언어로 바꿔야 할까? 나는 이런 질문에 답하고 실제 적용에 대해 다룰 것이다. 종종 나는 “스칼라에 매료됐어, 단점까 지 좋아해”라고 말하곤 한다. 독자 여러분도 이 책을 읽고 같은 느낌을 가졌으면 한다.
이 책의 구성 이 책은 종합적인 책이다. 그러므로 초보자가 스칼라를 사용해서 생산적이 되기 위해 모든 내 용을 다 읽을 필요는 없다.
1부 스칼라와의 만남 스칼라로 코드를 직접 작성해가면서 스칼라를 만나본다. 1장 ‘빠른 출발: 스칼라 소개’, 2장 ‘입 력은 조금만, 일은 더 많이’, 3장 ‘기초를 튼튼히’를 통해 스칼라의 핵심적인 특징을 정리한다.
14
2부 기본기 다지기 스칼라의 기본기를 다진다. 4장 ‘패턴 매칭’과 5장 ‘암시’에서는 스칼라 코드를 작성하면서 매일 사용할 필수 도구 두 가지를 깊이 설명한다. 중요한 소프트웨어 개발 방식인 함수형 프로그래밍(FP )을 처음 접하는 독자라면, 6장 ‘스칼라 함수형 프로그래밍’에서 스칼라가 구현한 방식을 통해 FP를 이해할 수 있다. 7장 ‘for 내장’에 서는 명성이 자자한 for 루프를 스칼라가 어떻게 확장했는지 설명하고, 복잡한 관용적인 함수 형 코드를 스칼라에서는 간결한 문법으로 다룰 수 있음을 보여준다.
8장 ‘스칼라 객체지향 프로그래밍’에서는 관심을 스칼라의 객체지향 프로그래밍object-oriented language
(OOP ) 지원으로 돌린다. 최근의 다양한 소프트웨어 개발 문제를 해결할 때 함수형 프
로그래밍의 중요성을 강조하기 위해 일부러 객체지향에 대한 내용을 함수형 프로그래밍 다음 에 배치했다. 스칼라를 ‘더 객체지향적인 자바’로 활용할 수도 있다. 하지만 그런 짓은 스칼라가 제공하는 가장 강력한 도구를 무시하는 것이다! 스칼라에서 클래스, 생성자 등을 정의하는 방 식이 자바와 유사하기 때문에 8장에서 다룬 대부분의 개념은 이해하기 쉬울 것이다.
9장 ‘트레이트’에서는 트레이트trait 를 사용해서 스칼라가 여러 동작을 조합하는 방식을 배운다. 자바 8은 인터페이스를 개선함으로써 이런 기능 중 일부를 도입했다. 그중 일부는 스칼라의 영 향을 받은 것이다. 경험 많은 자바 프로그래머도 이 내용을 잘 이해할 필요가 있다.
10장 ‘스칼라 객체 시스템 I ’, 11장 ‘스칼라 객체 시스템 II ’, 12장 ‘스칼라 컬렉션 라이브러리’, 13장 ‘가시성 규칙’에서는 스칼라의 객체 모델과 라이브러리 타입을 자세히 다룬다. 10장은 빨 리 통달하면 좋은 필수적인 내용을 다루므로 주의 깊게 읽어야 한다. 하지만 11장은 덜 필수적 인 정보로, 단순하지 않은 스칼라 타입 계층구조를 제대로 구현하는 방법을 다룬다. 이 책을 처 음 읽는 독자는 11장을 대강 읽고 넘어가도 된다. 12장은 컬렉션의 설계를 다루고 컬렉션을 지 혜롭게 사용하기 위해 필요한 유용한 정보를 제공한다. 스칼라를 처음 접하는 독자라면 12장 도 대강 읽고 넘어갔다가, 나중에 컬렉션 API를 제대로 알고 싶을 때 다시 돌아와도 된다. 13
15
장은 스칼라가 자바의 공개public, 보호protected, 비공개private 가시성을 어떻게 세밀하게 확장했 는지 알려준다.
3부 기초를 넘어서 지금까지 배운 내용을 더 잘 활용하는 데 필요한 여러 가지 중급 스칼라 기능에 관해 공부한다. 이제부터 더 어려운 영역으로 들어간다. 14장 ‘스칼라 타입 시스템 I ’과 15장 ‘스칼라 타입 시 스템 II ’에서는 스칼라의 복잡한 타입 시스템을 설명한다. 14장에서는 새로운 스칼라 프로그래 머가 상대적으로 빠르게 이해할 수 있는 개념을 다루고, 15장에서는 좀 더 고급 주제를 다룬다.
15장은 일단 건너뛰고 나중에 다시 도전해도 좋다. 16장 ‘고급 함수형 프로그래밍’에서는 보통의 스칼라 개발자라면 처음엔 몰라도 될, 카테고리 이론category theory 에서 따온 모나드monad 나 펑터functor 같은 좀 더 어려운 수학적 개념을 다룬다.
4부 고급 주제 및 실전 응용 실전에서 문제를 해결할 수 있도록 스칼라 프로그래밍 언어의 다양한 라이브러리와 도구의 사 용법을 익힌다. 17장 ‘동시성 프로그래밍 도구’는 회복성과 규모 확장성이 필요한 대규모 서비 스를 개발하는 개발자(우리 중 대부분)에게 유용할 것이다. 17장은 풍부한 액터 기반의 동시 성 모델인 아카와 비동기 코드를 작성할 때 유용한 Future에 대해 다룬다.
18장 ‘스칼라를 활용한 빅데이터’에서는 빅데이터나 데이터 중심 계산이 스칼라(더 나아가 실 제로는 일반적인 함수형 프로그래밍 전체)의 킬러 애플리케이션이 되리라는 사실을 보여줄 것 이다.
19장 ‘스칼라 동적 호출’과 20장 ‘스칼라 도메인 특화 언어’는 서로 잘 어울린다. 이들은 조금 어 려운 주제며, 여기서는 풍부한 도메인 특화 언어domain-specific language 를 만들기 위한 도구를 설명 한다.
16
21장 ‘스칼라 도구와 라이브러리’에서는 IDE 툴과 서드파티 라이브러리에 대해 다룬다. 스칼 라를 처음 접한 개발자라면 IDE와 에디터 지원에 대한 부분과 SBT에 대한 내용을 살펴보라.
SBT는 스칼라 프로젝트에서 사실상의 업계 표준인 빌드 도구다. 라이브러리 목록은 나중에 참 고 자료로 활용할 수 있다. 22장 ‘자바 상호 운용성’은 자바와 스칼라 코드를 함께 운용하는 팀 에 유용하다. 좋은 애플리케이션 설계에 대한 내 생각을 아키텍트나 소프트웨어 리드 개발자들과 나누기 위 해 23장 ‘애플리케이션 설계’를 썼다. 나는 복잡한 객체 그래프를 구성하는 상대적으로 비대한
JAR 파일로 이루어진 전통적인 소프트웨어 모델은 문제가 많으며 이를 더 나은 새로운 모델로 바꿀 필요가 있다고 생각한다. 마지막으로 24장 ‘메타프로그래밍: 매크로와 리플렉션’에서는 이 책에서 가장 고급 주제를 다 룬다. 초보자라면 24장을 생략해도 된다. 이 책은 부록 A ‘참고 문헌’에서 추가로 읽을 만한 여러 내용을 제시하면서 끝난다.
이 책이 다루지 않은 내용 최근 스칼라 2.11 배포판의 주 초점은 라이브러리를 모듈화해서 더 작은 JAR 파일로 나누는 것이다. 그러면 메모리 사용량이 중요한 환경(예를 들면 모바일 디바이스)에 배포할 때 불필 요한 코드를 더 쉽게 제외할 수 있다. 물론 라이브러리에서 예전에 사용 중단 안내deprecated 된 패키지나 타입도 삭제했다. 2.11에서 새로 사용 중단 안내된 것도 있다. 더 이상 유지보수가 이 루어지지 않고 있거나, 더 나은 서드파티 라이브러리가 있기 때문이다.
2.11에서 사용 중단 안내된 다음 패키지에 대해서는 설명하지 않을 것이다. scala.actors
액터 라이브러리다. 대신 아카를 사용하라(이에 대해서는 17.3절 ‘액터를 활용해서 튼튼하 고 확장성 있는 동시성 프로그래밍하기’에서 설명할 것이다).
17
scala.collection.script
컬렉션을 관찰하고 변경하는 ‘스크립트’를 작성하기 위한 라이브러리다. scala.text
예쁘게 출력하기pretty printing 위한 라이브러리다. 다음은 2.10에서 사용 중단 안내되었고, 2.11에서 제거된 패키지다. scala.util.automata
정규 표현식으로부터 결정적 유한 상태 오토마타deterministic finite automata (DFA )를 만들어내는 패키지다. scala.util.grammar
구문분석 라이브러리의 일부다. scala.util.logging
이 패키지 대신 JVM에서 사용할 수 있고 활발히 프로젝트가 진행 중인 여러 서드파티 라이 브러리를 활용하라. scala.util.regexp
정규 표현식 파싱 라이브러리다. 이 패키지 대신 scala.util.matching 패키지를 정규 표 현식을 지원하도록 확장했다. 닷넷(.Net ) 컴파일러 백엔드
과거에는 스칼라 팀에서도 닷넷 런타임 환경을 위한 컴파일러 백엔드를 개발했다. 하지만 닷넷 스칼라에 대한 관심이 사그라졌기 때문에 프로젝트를 중단했다.
18
라이브러리가 제공하는 모든 패키지나 타입을 다루지는 않을 것이다. 다음은 한정된 지면이나 여러 다른 이유로 인해 제외한 패키지다. scala.swing
자바 스윙 라이브러리에 대한 래퍼wrapper 다. 계속 관리는 되지만 사용하는 일은 많지 않다. scala.util.continuations
컨티뉴에이션 전달 방식continuation passing style (CPS ) 코드 생성을 위한 컴파일러 플러그인이 다. 적용 범위가 한정된 특별한 도구다. App과 DelayedInit 트레이트
이 두 타입은 전통적인 main (프로그램 진입 지점) 타입을 구현하기 위해 쓰였다. 즉, 자바의 static main 메서드와 비슷하다. 하지만 때로 예측을 벗어나는 동작을 보이곤 하기 때문에
사용을 권장하진 않는다. 대신 스칼라에서 널리 사용하는 관용적인 방식으로 main을 정의할 것이다. scala.ref java.lang.ref.WeakReference에 상응하는 WeakReference와 같은 자바 타입을 감싸는
래퍼다. scala.runtime
라이브러리 구현 일부에서 사용하는 타입이다. scala.util.hashing
해시 알고리즘이다.
19
코드 예제 내려받기 코드 예제를 깃허브(http://bit.ly/prog-scala-code )에서 내려받을 수 있다. 또한 한빛미디 어 웹페이지에서도 내려받을 수 있다.
http://www.hanbit.co.kr/exam/2275 압축 파일을 원하는 곳에 풀라. 배포 파일의 README 파일에 자세한 빌드 방법과 사용법이 들 어 있다(같은 내용을 1장에 요약해두었다). 예제 파일 중 일부는 scala 명령을 사용해서 실행할 수 있는 스크립트다. 다른 파일들은 클래스 파일로 컴파일해야 한다. 일부 파일의 코드에는 의도적으로 오류를 포함시켰기 때문에 컴파일 되지 않을 것이다. 이런 각각의 경우를 표현하기 위해 파일 명명 규칙을 도입했다. 물론 스칼라 를 배운 다음에는 대부분의 경우 파일 내용을 보고 세 가지 경우를 구분할 수 있게 될 것이다. *.scala
스칼라의 표준 파일 확장자는 .scala다. 하지만 한 가지 확장자만 사용하면 scalac를 사용 해서 컴파일해야 하는 코드 파일과 직접 scala를 사용해서 실행하는 스크립트 파일 그리고 이 책에서 일부러 오류가 나도록 만든 코드 파일을 구분할 수 없다. 따라서 예제 코드에서는 자바와 마찬가지로 컴파일을 거쳐야만 하는 코드 파일을 표시하기 위해 .scala 확장자를 사 용할 것이다. *.sc
확장자 .sc로 끝나는 파일은 명령행에서 scala를 사용해서 스크립트로 실행할 수 있는 파일 이다. 예를 들면 scala foo.sc와 같이 실행할 수 있다. scala를 인터프리터 모드로 시작하 고, ‘:load 파일이름’ 명령을 사용해서 인터프리터 안에서 스크립트 파일을 로드할 수도 있 다. 주의할 점은 이런 명명 규칙은 스칼라 커뮤니티의 표준이 아니라는 것이다. 하지만 이 책에서는 SBT 빌드 시 스크립트 파일을 무시하게 만들기 위해 이런 확장자를 사용한다. 또 한 이 책 뒷부분에서 설명하는 새로운 IDE 기능인 워크시트worksheet 의 경우에도 같은 확장
20
자를 사용한다. 본 확장자는 편의상 정한 것일 뿐이다. 명확히 하자면, 보통은 .scala를 스 크립트나 코드 파일 모두에 사용한다. *.scalaX와 *.scX
일부 예제 파일에는 컴파일 오류가 발생하는 잘못된 코드가 들어 있는 경우도 있다. 그런 예 제로 다른 빌드가 깨지는 것을 방지하기 위해 코드 파일에는 .scalaX 확장자를, 스크립트 파일에는 .scX 확장자를 사용한다. 물론 이것은 업계에서 일반적으로 사용하는 방식은 아니 다. 각 파일에는 어떤 부분이 잘못됐는지 설명하는 주석이 들어 있다.
21
감사의 글
이 책을 쓰는 동안 많은 사람이 원고를 읽고 여러 가지 개선 사항을 제안했다. 그에 대해 심심 한 감사를 표한다. 특히 광범위한 조언을 해준 스티브 젠슨Steve Jensen, 램니바스 라다드Ramnivas Laddad
, 마르셀 몰리나Marcel Molina, 빌 베너스Bill Venners, 요나스 보네르Jonas Bonér 에게 감사드린다.
우리가 받았던 피드백은 대부분 사파리 러프 컷Safari Rough Cuts과 http://programmingscala.
com에 있는 온라인 버전을 통한 것이었다. 피드백을 제공해준(특별한 순서는 없음) 율리안 드 라고스Iulian Dragos, 니콜라 린드버그Nikolaj Lindberg, 맷 헬리지Matt Hellige, 데이비드 비드라David Vydra, 리키 클락슨Ricky Clarkson, 알렉스 크루즈Alex Cruise, 조시 크론미어Josh Cronemeyer, 타일러 제닝스 Tyler Jennings
, 앨런 수피넉Alan Supynuk, 토니 힐러슨Tony Hillerson, 로저 본Roger Vaughn, 아비 수카지안
Arbi Sookazian
, 브루스 레이들Bruce Leidl, 대니얼 솝랄Daniel Sobral, 에데르 안드레스 아빌라Eder Andres
Avila
, 마렉 쿠비카Marek Kubica, 헨릭 후투넨Henrik Huttunen, 브사카 마달라Bhaskar Maddala, 제드 번Ged
Byrne
, 데릭 마하르Derek Mahar, 제프리 와이즈먼Geoffrey Wiseman, 피터 로스손Peter Rawsthorne, 조 보
비어Joe Bowbeer, 알렉산더 바티스티Alexander Battisti, 롭 디킨스Rob Dickens, 팀 매키천Tim MacEachern, 제이슨 해리스Jason Harris, 스티븐 그래디Steven Grady, 밥 폴렉Bob Follek, 아리엘 오티즈Ariel Ortiz, 파 스 말완카Parth Malwankar, 레이드 호츠스테들러Reid Hochstedler, 제이슨 자우그Jason Zaugg, 존 핸슨 Jon Hanson
, 마리오 글리츠만Mario Gleichmann, 데이비드 게이츠David Gates, 제프 헤멜Zef Hemel, 마이
클 이Michael Yee, 마리우스 크리스Marius Kreis, 마틴 쉬스크라우트Martin Süsskraut, 자비에 베가스Javier Vegas
, 토비아스 하우스Tobias Hauth, 프란체스코 보치치오Francesco Bochicchio, 스티븐 던컨 주니어
Stephen Duncan Jr.
, 패트릭 두디츠Patrik Dudits, 얀 니후스만Jan Niehusmann, 빌 버딕Bill Burdick, 데이빗 홀
브룩David Holbrook, 샬롬 디치Shalom Deitch, 제스퍼 노르덴버그Jesper Nordenberg, 에사 레인Esa Laine, 글렙 프랭크Gleb Frank, 사이먼 앤더슨Simon Andersson, 크리스 루이스Chris Lewis, 줄리언 호와스 Julian Howarth
, 더크 구젬작Dirk Kuzemczak, 헨리 게리츠Henri Gerrits, 존 하이츠John Heintz, 스튜어트 로버크
Stuart Roebuck
, 김정호Jungho Kim에게 감사드린다. 사용자명만 사용해서 피드백을 제공한 독자도
있다. Zack, JoshG, ewilligers, abcoates, brad, teto, pjcj, mkleint, dandoyon, Arek,
rue, acangiano, vkelman, bryanl, Jeff, mbaxter, pjb3, kxen, hipertracker, ctran, Ram R., cody, Nolan, Joshua, Ajay, Joe에게 감사드린다. 혹시 실수로 여기 적지 못한 분
22
이 있다면 정말 죄송하다.1 편집자 마이크 루키데스Mike Loukides 는 부드럽게 독촉하거나 등을 떠미는 방법을 잘 안다. 이 책 을 쓰는 모든 험난한 과정에서 그가 큰 도움을 줬다. 오라일리의 다른 분들도 여러 질문에 답해 주고 우리가 앞으로 나아갈 수 있도록 항상 그 자리를 지켜줬다. 추천글을 써 준 요나스 보네르에게 감사드린다. 그는 관점 중심 프로그래밍 커뮤니티를 통한 오랜 친구이자 동료다. 그는 여러 해에 걸쳐 자바 커뮤니티에서 선구자적인 작업을 진행해왔 다. 이제는 그 에너지를 스칼라를 장려하고 스칼라 커뮤니티를 키우는 데 사용하고 있다. 빌 베너스Bill Venners 는 기꺼이 뒤표지에 쓸 추천 문구를 써 줬다. 그가 마틴 오더스키와 렉 스 스푼Lex Spoon 과 함께 쓴, 스칼라에 대한 최초의 책인 『Programming in Scala (Second
Edition ) 한국어판』 (에이콘, 2014 )는 스칼라 개발자들의 필독서다. 빌은 또한 훌륭한 스칼라 테스트ScalaTest 라이브러리를 만들었다. 우리는 전 세계의 여려 개발자로부터 많은 것을 배울 수 있었다. 요나스Jonas 와 빌Bill 외에도 드 바시시 고시Debasish Ghosh, 제임스 아이리James Iry, 대니얼 스피와크Daniel Spiewak, 데이비드 폴락 David Pollack
, 폴 스니블리Paul Snively, 올라 비니Ola Bini, 대니얼 소브랄Daniel Sobral, 조시 수에레스Josh
Suereth
, 로비 포인터Robey Pointer, 네이선 햄블렌Nathan Hamblen, 호르헤 오르티스Jorge Ortiz 등이 블로
그 포스트, 포럼 토론, 개인적인 대화 등을 통해 우리가 잘 모르던 부분을 밝혀주었다. 딘은 오브젝트 맨토Object Mentor 의 동료들에게 감사드리고, 언어, 소프트웨어 설계, 그리고 업계 에 종사하는 개발자가 직면한 실질적인 문제에 대해 흥미진진한 토론을 함께했던 여러 고객 개 발자에게 감사드린다. 시카고 지역 스칼라 애호가Chicago Area Scala Enthusiasts (CASE ) 그룹도 귀중 한 피드백과 영감의 근원이 돼왔다. 또한 이 책의 얼리 액세스early access 버전을 리뷰한 독자들의 귀중한 피드백과 타입세이프에 있는 여러 동료의 지속적인 조언이 큰 도움이 되었다. 특히 초고를 검토해준 램니바스 라다드Ramnivas 1 역주_ 이름 번역시 사용자명( ID )은 따로 음차 표기하지 않았다.
23
Laddad
, 케빈 킬로이Kevin Kilroy, 루츠 휭켄Lutz Huehnken, 토머스 로크Thomas Lockney 에게 감사드린다.
특히 우리들의 소중한 시간 중 상당량을 이 프로젝트에 투입하도록 허락해준 앤Ann 에게 감사하 고 싶다. 앤, 사랑해! 알렉스는 트위터에 있는 동료들의 지지와 스칼라의 효율성을 보여주기 위한 뛰어난 작업에 감 사드린다. 그는 또한 샌프란시스코만 지역 스칼라 애호가Bay Area Scala Enthusiasts (BASE ) 그룹이 제공해준 자극과 커뮤니티에 감사드린다. 다른 무엇보다도 우리는 마틴 오더스키와 그의 팀에 감사드린다.
24
CONTENTS
지은이·옮긴이 소개 ��������������������������������������������������������������������������������������������������������� 5
추천의 글 ����������������������������������������������������������������������������������������������������������������������� 6
옮긴이의 말 ������������������������������������������������������������������������������������������������������������������ 10
이 책에 대하여 �������������������������������������������������������������������������������������������������������������� 12
감사의 글 ��������������������������������������������������������������������������������������������������������������������� 22
Part
1
CHAPTER
스칼라와의 만남
1 빠른 출발: 스칼라 소개 1.1 왜 스칼라인가? ����������������������������������������������������������������������������������������������������������� 43
1.1.1 스칼라의 매력 ������������������������������������������������������������������������������������������������� 45
1.1.2 자바 8이 나왔지만 여전히 유용한 스칼라 ������������������������������������������������������������� 45
1.2 스칼라 설치하기 ���������������������������������������������������������������������������������������������������������� 46
1.2.1 SBT 사용하기 ������������������������������������������������������������������������������������������������� 48
1.2.2 스칼라 명령행 도구 실행하기 ����������������������������������������������������������������������������� 50
1.2.3 IDE에서 스칼라 REPL 실행하기 ������������������������������������������������������������������������ 53
1.3 스칼라 맛보기 ������������������������������������������������������������������������������������������������������������ 54
1.4 동시성 맛보기 ������������������������������������������������������������������������������������������������������������ 67
1.5 마치며 ���������������������������������������������������������������������������������������������������������������������� 82
CHAPTER
2 입력은 조금만, 일은 더 많이
2.1 세미콜론 ������������������������������������������������������������������������������������������������������������������� 83
2.2 변수 정의 ������������������������������������������������������������������������������������������������������������������ 84
25
CONTENTS
2.3 범위 ������������������������������������������������������������������������������������������������������������������������� 87
2.4 부분 함수 ������������������������������������������������������������������������������������������������������������������ 88
2.5 메서드 선언 ��������������������������������������������������������������������������������������������������������������� 90
2.5.1 메서드의 기본 인자와 이름 붙은 인자 ������������������������������������������������������������������ 90
2.5.2 인자 목록이 여럿 있는 메서드 ���������������������������������������������������������������������������� 91
2.5.3 Future 맛보기 ������������������������������������������������������������������������������������������������ 94
2.5.4 내포된 메서드 정의와 재귀 �������������������������������������������������������������������������������� 98
2.6 타입 정보 추론하기 ���������������������������������������������������������������������������������������������������� 101
2.7 예약어 �������������������������������������������������������������������������������������������������������������������� 108
2.8 리터럴 값 ���������������������������������������������������������������������������������������������������������������� 111
2.8.1 정수 리터럴 �������������������������������������������������������������������������������������������������� 111
2.8.2 부동소수점 리터럴 ����������������������������������������������������������������������������������������� 112
2.8.3 불린 리터럴 �������������������������������������������������������������������������������������������������� 113
2.8.4 문자 리터럴 �������������������������������������������������������������������������������������������������� 114
2.8.5 문자열 리터럴 ����������������������������������������������������������������������������������������������� 115
2.8.6 심벌 리터럴 �������������������������������������������������������������������������������������������������� 117
2.8.7 함수 리터럴 �������������������������������������������������������������������������������������������������� 117
2.8.8 튜플 리터럴 �������������������������������������������������������������������������������������������������� 118
2.9 Option, Some, None : null 사용 피하기 ���������������������������������������������������������������������� 120
2.10 봉인된 클래스 계층 ���������������������������������������������������������������������������������������������������� 123
2.11 파일과 이름공간으로 코드 구조화하기 ��������������������������������������������������������������������������� 123
2.12 타입과 멤버 임포트하기 ���������������������������������������������������������������������������������������������� 126
2.12.1 임포트는 상대적이다 �������������������������������������������������������������������������������������� 127
2.12.2 패키지 객체 �������������������������������������������������������������������������������������������������� 128
2.13 추상 타입과 매개변수화한 타입 ������������������������������������������������������������������������������������ 129
2.14 마치며 �������������������������������������������������������������������������������������������������������������������� 133
26
CHAPTER
3 기초를 튼튼히 3.1 연산자 오버로딩? ������������������������������������������������������������������������������������������������������ 135
3.1.1 편의 구문 ����������������������������������������������������������������������������������������������������� 139
3.2 빈 인자 목록이 있는 메서드 ����������������������������������������������������������������������������������������� 139
3.3 우선순위 규칙 ���������������������������������������������������������������������������������������������������������� 141
3.4 도메인 특화 언어 ������������������������������������������������������������������������������������������������������� 143
3.5 스칼라 if 문 �������������������������������������������������������������������������������������������������������������� 144
3.6 스칼라 for 내장 �������������������������������������������������������������������������������������������������������� 145
3.6.1 for 루프 ������������������������������������������������������������������������������������������������������� 145
3.6.2 제너레이터 식 ����������������������������������������������������������������������������������������������� 146
3.6.3 가드: 값 걸러내기 ������������������������������������������������������������������������������������������ 146
3.6.4 yield로 값 만들어내기 ������������������������������������������������������������������������������������ 148
3.6.5 확장 영역과 값 정의 ��������������������������������������������������������������������������������������� 149
3.7 다른 루프 표현 ���������������������������������������������������������������������������������������������������������� 151
3.7.1 스칼라 while 루프 ����������������������������������������������������������������������������������������� 151
3.7.2 스칼라 do -while 루프 ������������������������������������������������������������������������������������ 152
3.8 조건 연산자 ������������������������������������������������������������������������������������������������������������� 152
3.9 try, catch, finally 사용하기 ���������������������������������������������������������������������������������������� 153
3.10 이름에 의한 호출과 값에 의한 호출 ������������������������������������������������������������������������������� 156
3.11 지연값 �������������������������������������������������������������������������������������������������������������������� 161
3.12 열거값 �������������������������������������������������������������������������������������������������������������������� 163
3.13 문자열 인터폴레이션 �������������������������������������������������������������������������������������������������� 166
3.14 트레이트: 스칼라 인터페이스와 혼합 ����������������������������������������������������������������������������� 169
3.15 마치며 �������������������������������������������������������������������������������������������������������������������� 173
27
CONTENTS
Part
2
CHAPTER
기본기 다지기
4 패턴 매칭
4.1 단순 매치 ���������������������������������������������������������������������������������������������������������������� 177
4.2 매치 내의 값, 변수, 타입 ��������������������������������������������������������������������������������������������� 179
4.3 시퀀스에 일치시키기 �������������������������������������������������������������������������������������������������� 184
4.4 튜플에 일치시키기 ����������������������������������������������������������������������������������������������������� 189
4.5 케이스 절의 가드 ������������������������������������������������������������������������������������������������������� 190
4.6 케이스 클래스에 일치시키기 ���������������������������������������������������������������������������������������� 191
4.6.1 unapply 메서드 �������������������������������������������������������������������������������������������� 193
4.6.2 unapplySeq 메서드 �������������������������������������������������������������������������������������� 198
4.7 가변 인자 목록과 일치시키기 ��������������������������������������������������������������������������������������� 201
4.8 정규 표현식과 일치시키기 ������������������������������������������������������������������������������������������� 203
4.9 케이스 절의 변수 바인딩에 대해 더 살펴보기 ������������������������������������������������������������������ 204
4.10 타입 일치에 대해 더 살펴보기 �������������������������������������������������������������������������������������� 205
4.11 봉인된 클래스 계층과 매치의 완전성 ����������������������������������������������������������������������������� 207
4.12 패턴 매칭의 다른 사용법 ��������������������������������������������������������������������������������������������� 210
4.13 패턴 매칭에 대한 설명을 마치며 ����������������������������������������������������������������������������������� 215
4.14 마치며 �������������������������������������������������������������������������������������������������������������������� 216
CHAPTER
5 암시 5.1 암시적 인자 ������������������������������������������������������������������������������������������������������������� 217
5.2 암시적 인자를 사용하는 시나리오 ��������������������������������������������������������������������������������� 221
28
5.1.1 implicitly 사용하기 ���������������������������������������������������������������������������������������� 220
5.2.1 실행 맥락 제공하기 ���������������������������������������������������������������������������������������� 221
5.2.2 사용 가능한 기능 제어하기 ������������������������������������������������������������������������������ 222
5.2.3 사용 가능한 인스턴스 제한하기 ������������������������������������������������������������������������ 223
5.2.4 암시적 증거 제공하기 ������������������������������������������������������������������������������������� 229
5.2.5 타입 소거 우회하기 ���������������������������������������������������������������������������������������� 231
5.2.6 오류 메시지 개선하기 ������������������������������������������������������������������������������������� 234
5.2.7 유령 타입 ����������������������������������������������������������������������������������������������������� 235
5.2.8 암시적 인자를 처리하기 위한 규칙 �������������������������������������������������������������������� 239
5.3 암시적 변환 ������������������������������������������������������������������������������������������������������������� 241
5.3.1 자신만의 문자열 인터폴레이션 만들기 ��������������������������������������������������������������� 246
5.3.2 표현력 문제 �������������������������������������������������������������������������������������������������� 249
5.4 타입 클래스 패턴 ������������������������������������������������������������������������������������������������������� 251
5.5 암시와 관련된 기술적 문제 ������������������������������������������������������������������������������������������ 254
5.6 암시 해결 규칙 ���������������������������������������������������������������������������������������������������������� 256
5.7 스칼라가 기본 제공하는 암시 ��������������������������������������������������������������������������������������� 257
5.8 암시를 현명하게 활용하기 ������������������������������������������������������������������������������������������� 267
5.9 마치며 �������������������������������������������������������������������������������������������������������������������� 267
CHAPTER
6 스칼라 함수형 프로그래밍 6.1 함수형 프로그래밍이란 무엇인가? �������������������������������������������������������������������������������� 270
6.1.1 수학 함수 ����������������������������������������������������������������������������������������������������� 271
6.1.2 값이 바뀌지 않는 변수 ������������������������������������������������������������������������������������ 272
6.2 스칼라 함수형 프로그래밍 ������������������������������������������������������������������������������������������� 276
6.2.1 익명 함수, 람다, 클로저 ����������������������������������������������������������������������������������� 277
6.2.2 안에서 보는 순수성과 밖에서 보는 순수성 ���������������������������������������������������������� 280
6.3 재귀 ����������������������������������������������������������������������������������������������������������������������� 281
6.4 꼬리 호출과 꼬리 호출 최적화 �������������������������������������������������������������������������������������� 282
6.4.1 꼬리 호출 트램펄린 ���������������������������������������������������������������������������������������� 284
29
CONTENTS
6.5 부분 적용 함수와 부분 함수 ����������������������������������������������������������������������������������������� 285
6.6 함수의 커링과 다른 변환 ��������������������������������������������������������������������������������������������� 287
6.7 함수형 데이터 구조 ���������������������������������������������������������������������������������������������������� 293
6.7.1 시퀀스 ��������������������������������������������������������������������������������������������������������� 294
6.7.2 맵 ��������������������������������������������������������������������������������������������������������������� 300
6.7.3 집합 ������������������������������������������������������������������������������������������������������������ 302
6.8 순회하기, 연관시키기, 걸러내기, 접기, 축약하기 ������������������������������������������������������������� 303
6.8.1 순회 ������������������������������������������������������������������������������������������������������������ 303
6.8.2 연관시키기 ��������������������������������������������������������������������������������������������������� 305
6.8.3 펼치면서 연관시키기 �������������������������������������������������������������������������������������� 308
6.8.4 걸러내기 ������������������������������������������������������������������������������������������������������ 310
6.8.5 접기와 축약시키기 ����������������������������������������������������������������������������������������� 312
6.9 왼쪽 순회와 오른쪽 순회 ��������������������������������������������������������������������������������������������� 318
6.9.1 꼬리 재귀와 무한 컬렉션에 대한 순회 ���������������������������������������������������������������� 322
6.10 콤비네이터: 가장 뛰어난 소프트웨어 컴포넌트 추상화 ������������������������������������������������������ 327
6.11 복사에 드는 비용은 어떤가? ���������������������������������������������������������������������������������������� 331
6.12 마치며 �������������������������������������������������������������������������������������������������������������������� 334
CHAPTER
7 for 내장
7.1 돌아보기: for 내장의 기본 요소 ������������������������������������������������������������������������������������ 337
7.2 for 내장: 내부 동작 ��������������������������������������������������������������������������������������������������� 341
7.3 for 내장의 변환 규칙 ������������������������������������������������������������������������������������������������� 344
7.4 Option과 다른 컨테이너 타입 ������������������������������������������������������������������������������������� 349
7.4.1 컨테이너로서의 Option ���������������������������������������������������������������������������������� 349
7.4.2 Either : Option의 논리적 확장 ������������������������������������������������������������������������� 354
7.4.3 Try : 할 수 있는 일이 없을 때 ���������������������������������������������������������������������������� 360
30
CHAPTER
7.4.4 스칼라제드의 Validation �������������������������������������������������������������������������������� 362
7.5 마치며 �������������������������������������������������������������������������������������������������������������������� 366
8 스칼라 객체지향 프로그래밍
8.1 클래스와 객체의 기초 ������������������������������������������������������������������������������������������������� 368
8.2 참조 타입과 값 타입 ��������������������������������������������������������������������������������������������������� 371
8.3 값 클래스 ���������������������������������������������������������������������������������������������������������������� 373
8.4 부모 타입 ���������������������������������������������������������������������������������������������������������������� 377
8.5 스칼라에서의 생성자 �������������������������������������������������������������������������������������������������� 378
8.6 클래스의 필드 ���������������������������������������������������������������������������������������������������������� 384
8.6.1 단일 접근 원칙 ���������������������������������������������������������������������������������������������� 386
8.6.2 단항 메서드 �������������������������������������������������������������������������������������������������� 387
8.7 입력 검증하기 ���������������������������������������������������������������������������������������������������������� 388
8.8 부모 클래스 생성자 호출하기(그리고 좋은 객체지향 설계) ������������������������������������������������ 391
8.8.1 여담: 좋은 객체지향 설계 �������������������������������������������������������������������������������� 392
8.9 내포된 타입 ������������������������������������������������������������������������������������������������������������� 398
8.10 마치며 �������������������������������������������������������������������������������������������������������������������� 400
CHAPTER
9 트레이트
9.1 자바 8의 인터페이스 �������������������������������������������������������������������������������������������������� 402
9.2 믹스인으로서의 트레이트 �������������������������������������������������������������������������������������������� 402
9.3 트레이트 쌓기 ���������������������������������������������������������������������������������������������������������� 409
9.4 트레이트 만들기 �������������������������������������������������������������������������������������������������������� 415
9.5 클래스를 쓸 것인가 트레이트를 쓸 것인가? �������������������������������������������������������������������� 418
9.6 마치며 �������������������������������������������������������������������������������������������������������������������� 418
31
CONTENTS
CHAPTER
10 스칼라 객체 시스템 I 10.1 매개변수화한 타입: 상속에 따른 변성 �������������������������������������������������������������������������� 419
10.1.1 함수 내부 들여다보기 ��������������������������������������������������������������������������������� 421
10.1.2 변경 가능한 타입의 변성 ����������������������������������������������������������������������������� 426
10.1.3 스칼라의 변성과 자바의 변성 비교 ���������������������������������������������������������������� 429
10.2 스칼라 타입 계층구조 ����������������������������������������������������������������������������������������������� 430
10.3 Nothing (그리고 Null )에 대한 더 많은 내용 ����������������������������������������������������������������� 432
10.4 Product , 케이스 클래스, 튜플 ���������������������������������������������������������������������������������� 438
10.5 Predef 객체 ���������������������������������������������������������������������������������������������������������� 440
10.5.1 암시적 변환 ���������������������������������������������������������������������������������������������� 440
10.5.2 타입 정의 ������������������������������������������������������������������������������������������������� 444
10.5.3 조건 검사 메서드 ��������������������������������������������������������������������������������������� 445
10.5.4 입출력 메서드 ������������������������������������������������������������������������������������������� 446
10.5.5 기타 메서드 ���������������������������������������������������������������������������������������������� 448
10.6 객체의 동등성 ��������������������������������������������������������������������������������������������������������� 449
10.6.1 equals 메서드 ������������������������������������������������������������������������������������������ 450
10.6.2 ==와 != 메서드 ��������������������������������������������������������������������������������������� 451
10.6.3 eq와 ne 메서드 ���������������������������������������������������������������������������������������� 452
10.6.4 Array의 동등성과 sameElements 메서드 ���������������������������������������������������� 452
CHAPTER
10.7 마치며 ������������������������������������������������������������������������������������������������������������������ 453
11 스칼라 객체 시스템 II 11.1 클래스와 트레이트의 멤버 오버라이딩하기 ������������������������������������������������������������������� 455
11.1.1 구체적 멤버를 오버라이딩하는 일 피하기 ������������������������������������������������������� 456
11.1.2 final 선언을 오버라이딩하려 시도하기 ����������������������������������������������������������� 459
32
11.1.3 추상적 메서드와 구체적 메서드 오버라이딩하기 ���������������������������������������������� 460
11.1.4 추상적 필드와 구체적 필드 오버라이딩하기 ���������������������������������������������������� 462
11.1.5 추상 타입 오버라이딩하기 ��������������������������������������������������������������������������� 470
11.1.6 접근자 메서드와 필드를 구별할 수 없을 때: 단일 접근 원칙 ������������������������������� 471
11.2 객체의 상속 계층을 선형화하기 ���������������������������������������������������������������������������������� 474
11.3 마치며 ������������������������������������������������������������������������������������������������������������������ 481
CHAPTER
12 스칼라 컬렉션 라이브러리 12.1 제네릭, 변경 가능, 변경 불가능, 동시성, 병렬 컬렉션, 아이고! ������������������������������������������ 483
12.1.1 collection 패키지 ������������������������������������������������������������������������������������� 484
12.1.2 collection.concurrent 패키지 �������������������������������������������������������������������� 486
12.1.3 collection.convert 패키지 ������������������������������������������������������������������������� 487
12.1.4 collection.generic 패키지 ������������������������������������������������������������������������� 487
12.1.5 collection.immutable 패키지 ��������������������������������������������������������������������� 487
12.1.6 collection.mutable 패키지 ������������������������������������������������������������������������ 489
12.1.7 collection.parallel 패키지 �������������������������������������������������������������������������� 491
12.2 컬렉션 선택하기 ������������������������������������������������������������������������������������������������������ 493
12.3 컬렉션 라이브러리의 설계 방식 ���������������������������������������������������������������������������������� 494
12.3.1 Builder ��������������������������������������������������������������������������������������������������� 494
12.3.2 CanBuildFrom ���������������������������������������������������������������������������������������� 495
12.3.3 ... Like 트레이트 ��������������������������������������������������������������������������������������� 497
12.4 값 타입을 위한 특화 ������������������������������������������������������������������������������������������������� 499
12.4.1 미니박싱 �������������������������������������������������������������������������������������������������� 500
12.5 마치며 ������������������������������������������������������������������������������������������������������������������ 501
33
CONTENTS
CHAPTER
13 가시성 규칙
13.1 공개: 기본 가시성 ���������������������������������������������������������������������������������������������������� 503
13.2 가시성 지정 키워드 �������������������������������������������������������������������������������������������������� 505
13.3 공개 가시성 ������������������������������������������������������������������������������������������������������������ 506
13.4 보호 가시성 ������������������������������������������������������������������������������������������������������������ 507
13.5 비공개 가시성 ��������������������������������������������������������������������������������������������������������� 508
13.6 영역 지정 비공개와 영역 지정 보호 가시성 ������������������������������������������������������������������� 510
13.7 가시성에 대한 마지막 고찰 ���������������������������������������������������������������������������������������� 518
13.8 마치며 ������������������������������������������������������������������������������������������������������������������ 519
Part
3
CHAPTER
기초를 넘어서
14 스칼라 타입 시스템 I 14.1 매개변수화한 타입 ��������������������������������������������������������������������������������������������������� 524
14.1.1 변성 표기 ������������������������������������������������������������������������������������������������� 524
14.1.2 타입 생성자 ���������������������������������������������������������������������������������������������� 525
14.1.3 타입 매개변수의 이름 ��������������������������������������������������������������������������������� 525
14.2 타입 바운드 ������������������������������������������������������������������������������������������������������������ 525
14.2.1 상위 타입 바운드 ��������������������������������������������������������������������������������������� 526
14.2.2 하위 타입 바운드 ��������������������������������������������������������������������������������������� 527
14.3 맥락 바운드 ������������������������������������������������������������������������������������������������������������ 532
14.4 뷰 바운드 ��������������������������������������������������������������������������������������������������������������� 533
14.5 추상 타입 이해하기 �������������������������������������������������������������������������������������������������� 536
34
14.5.1 추상 타입과 매개변수화한 타입의 비교 ���������������������������������������������������������� 538
14.6 자기 타입 표기 �������������������������������������������������������������������������������������������������������� 541
14.7 구조적 타입 ������������������������������������������������������������������������������������������������������������ 547
14.8 복합 타입 ��������������������������������������������������������������������������������������������������������������� 552
14.8.1 타입 세분화 ���������������������������������������������������������������������������������������������� 552
14.9 존재 타입 ��������������������������������������������������������������������������������������������������������������� 554
14.10 마치며 ������������������������������������������������������������������������������������������������������������������ 556
CHAPTER
15 스칼라 타입 시스템 II 15.1 경로에 의존하는 타입 ����������������������������������������������������������������������������������������������� 557
15.1.1 C.this ����������������������������������������������������������������������������������������������������� 558
15.1.2 C.super �������������������������������������������������������������������������������������������������� 559
15.1.3 경로. x ���������������������������������������������������������������������������������������������������� 560
15.2 의존적 메서드 타입 �������������������������������������������������������������������������������������������������� 561
15.3 타입 투영 ��������������������������������������������������������������������������������������������������������������� 563
15.3.1 싱글턴 타입 ���������������������������������������������������������������������������������������������� 566
15.4 값에 대한 타입 �������������������������������������������������������������������������������������������������������� 567
15.4.1 튜플 타입 ������������������������������������������������������������������������������������������������� 567
15.4.2 함수 타입 ������������������������������������������������������������������������������������������������� 568
15.4.3 중위 타입 ������������������������������������������������������������������������������������������������� 568
15.5 고계 타입 ��������������������������������������������������������������������������������������������������������������� 569
15.6 타입 람다 ��������������������������������������������������������������������������������������������������������������� 575
15.7 자기 재귀 타입: F -바운드 다형성 ������������������������������������������������������������������������������ 577
15.8 마치며 ������������������������������������������������������������������������������������������������������������������ 580
35
CONTENTS
CHAPTER
16 고급 함수형 프로그래밍 16.1 대수적 데이터 타입 �������������������������������������������������������������������������������������������������� 581
16.1.1 합 타입과 곱 타입 �������������������������������������������������������������������������������������� 582
16.1.2 대수적 데이터 타입의 특성 �������������������������������������������������������������������������� 584
16.1.3 대수적 데이터 타입에 대한 마지막 고찰 ��������������������������������������������������������� 585
16.2 카테고리 이론 ��������������������������������������������������������������������������������������������������������� 586
16.2.1 카테고리란 무엇인가 ���������������������������������������������������������������������������������� 587
16.2.2 펑터 카테고리 ������������������������������������������������������������������������������������������� 588
16.2.3 모나드 카테고리 ���������������������������������������������������������������������������������������� 594
16.2.4 모나드의 중요성 ���������������������������������������������������������������������������������������� 596
16.3 마치며 ������������������������������������������������������������������������������������������������������������������ 599
Part
4
CHAPTER
고급 주제 및 실전 응용
17 동시성 프로그래밍 도구
17.1 scala.sys.process 패키지 ��������������������������������������������������������������������������������������� 604
17.2 퓨처 ��������������������������������������������������������������������������������������������������������������������� 605
17.2.1 Async ���������������������������������������������������������������������������������������������������� 609
17.3 액터를 활용해서 튼튼하고 확장성 있는 동시성 프로그래밍하기 ���������������������������������������� 612
17.4 아카: 스칼라를 위한 액터 ������������������������������������������������������������������������������������������ 612
17.4.1 액터: 마지막 고찰 �������������������������������������������������������������������������������������� 627
17.5 피클링과 스포어즈 ��������������������������������������������������������������������������������������������������� 628
17.6 반응형 프로그래밍 ��������������������������������������������������������������������������������������������������� 629
17.7 마치며 ������������������������������������������������������������������������������������������������������������������ 631
36
CHAPTER
18 스칼라를 활용한 빅데이터
18.1 빅데이터: 간략한 역사 ���������������������������������������������������������������������������������������������� 634
18.2 스칼라로 맵리듀스 개선하기 �������������������������������������������������������������������������������������� 635
18.3 맵리듀스를 넘어서 ��������������������������������������������������������������������������������������������������� 641
18.4 수학을 위한 카테고리 ����������������������������������������������������������������������������������������������� 644
18.5 스칼라 기반 데이터 도구 목록 ������������������������������������������������������������������������������������ 644
18.6 마치며 ������������������������������������������������������������������������������������������������������������������ 646
CHAPTER
19 스칼라 동적 호출
19.1 동기를 불어넣는 예제: 루비 온 레일즈의 ActiveRecord ������������������������������������������������ 647
19.2 Dynamic 트레이트를 사용해서 스칼라에서 동적 호출하기 ��������������������������������������������� 649
19.3 DSL에서 고려할 점 ������������������������������������������������������������������������������������������������� 655
19.4 마치며 ������������������������������������������������������������������������������������������������������������������ 655
CHAPTER
20 스칼라 도메인 특화 언어
20.1 예제: 스칼라를 위한 XML과 JSON DSL �������������������������������������������������������������������� 659
20.2 내부 DSL �������������������������������������������������������������������������������������������������������������� 661
20.3 파서 콤비네이터를 활용한 외부 DSL �������������������������������������������������������������������������� 667
20.3.1 파서 콤비네이터 ���������������������������������������������������������������������������������������� 668
20.3.2 급여 계산 외부 DSL ���������������������������������������������������������������������������������� 668
20.4 내부 DSL과 외부 DSL에 대한 마지막 고찰 ����������������������������������������������������������������� 671
20.5 마치며 ������������������������������������������������������������������������������������������������������������������ 673
37
CONTENTS
CHAPTER
21 스칼라 도구와 라이브러리 21.1 명령행 도구 ������������������������������������������������������������������������������������������������������������ 675
21.1.1 scalac 명령행 도구 ����������������������������������������������������������������������������������� 676
21.1.2 scala 명령행 도구 ������������������������������������������������������������������������������������� 680
21.1.3 scalap와 javap 명령행 도구 ����������������������������������������������������������������������� 684
21.1.4 scaladoc 명령행 도구 ������������������������������������������������������������������������������� 686
21.1.5 fsc 명령행 도구 ���������������������������������������������������������������������������������������� 686
21.2 빌드 도구 ��������������������������������������������������������������������������������������������������������������� 686
21.2.1 스칼라 표준 빌드 도구 SBT ������������������������������������������������������������������������ 687
21.2.2 다른 빌드 도구 ������������������������������������������������������������������������������������������ 690
21.3 IDE나 텍스트 편집기와 통합하기 ������������������������������������������������������������������������������� 690
21.3.1 텍스트 편집기 ������������������������������������������������������������������������������������������� 692
21.4 스칼라로 테스트 주도 개발하기 ���������������������������������������������������������������������������������� 692
21.5 서드파티 라이브러리 ������������������������������������������������������������������������������������������������ 693
21.6 마치며 ������������������������������������������������������������������������������������������������������������������ 696
CHAPTER
22 자바 상호 운용성
22.1 자바에서 정의한 이름을 스칼라 코드에서 사용하기 �������������������������������������������������������� 697
22.2 자바와 스칼라 제네릭스 �������������������������������������������������������������������������������������������� 698
22.3 자바빈즈 프로퍼티 ��������������������������������������������������������������������������������������������������� 700
22.4 AnyVal 타입과 자바 기본 타입 ���������������������������������������������������������������������������������� 702
22.5 자바 코드로 변환한 스칼라 이름 ��������������������������������������������������������������������������������� 702
22.6 마치며 ������������������������������������������������������������������������������������������������������������������ 702
38
CHAPTER
23 애플리케이션 설계
23.1 그 동안 배운 내용 복습 ��������������������������������������������������������������������������������������������� 703
23.2 애노테이션 ������������������������������������������������������������������������������������������������������������� 705
23.3 모듈로서의 트레이트 ������������������������������������������������������������������������������������������������ 710
23.4 디자인 패턴 ������������������������������������������������������������������������������������������������������������ 712
23.4.1 생성 패턴 ������������������������������������������������������������������������������������������������� 712
23.4.2 구조 패턴 ������������������������������������������������������������������������������������������������� 713
23.4.3 행동 패턴 ������������������������������������������������������������������������������������������������� 715
23.5 계약에 의한 설계를 활용해서 더 좋게 설계하기 ������������������������������������������������������������� 717
23.6 파르테논 구조 ��������������������������������������������������������������������������������������������������������� 721
23.7 마치며 ������������������������������������������������������������������������������������������������������������������ 727
CHAPTER
24 메타프로그래밍: 매크로와 리플렉션
24.1 타입을 이해하기 위한 도구 ���������������������������������������������������������������������������������������� 730
24.2 실행 시점 리플렉션 �������������������������������������������������������������������������������������������������� 731
24.2.1 타입에 대한 리플렉션 ��������������������������������������������������������������������������������� 731
24.2.2 클래스 태그, 타입 태그, 매니페스트 �������������������������������������������������������������� 733
24.3 스칼라의 고급 실행 시점 리플렉션 API ����������������������������������������������������������������������� 735
24.4 매크로 ������������������������������������������������������������������������������������������������������������������ 739
24.4.1 매크로 예제: 불변성 강제하기 ���������������������������������������������������������������������� 743
24.4.2 매크로에 대한 마지막 고찰 �������������������������������������������������������������������������� 747
24.5 마치며 ������������������������������������������������������������������������������������������������������������������ 747
부록 A 참고 문헌 �������������������������������������������������������������������������������������������������������������������������������������
749
찾아보기 ���������������������������������������������������������������������������������������������������������������������������������������������������
756
39
Part
I
스칼라와의 만남
스칼라를 공부하기로 결심한 여러분을 환영한다! 스칼라는 다양한 프로그래밍 패러다임을 잘 버무린 객체지향 기반의 함수형 언어다. 하지만 이런 설명만 들어서는 스칼라의 다양한 측면과 매력을 몸으 로 느끼기 어렵다. 1 부에서는 직접 스칼라로 코드를 작성해가면서 스칼라를 체험하고 기본적인 스칼 라의 특징을 간략하게 다룬다. 1 부를 마치고 나면 스칼라 프로그램의 기본 요소를 익히고 배운 것을 간단한 스크립트 작업 등에 활용할 수 있을 것이다.
Part I
스칼라와의 만남
1장
스칼라를 간략히 소개하고, 구체적으로 맛볼 수 있는 몇 가지 예제를 다룬다.
2장
스칼라 프로그래밍의 기본 단위라고 할 수 있는 함수와 메서드, 리터럴 값, 클래스 등에 대 해 다루면서 기본 문법 사항을 정리한다.
3장
2장에 이어서 기본 문법 사항을 정리한다. 연산자 오버로딩, if, for 내장, 루프 등에 대해
다룬다.
CHAPTER
1
빠른 출발: 스칼라 소개
스칼라를 진지하게 고려해봐야 하는 이유를 간단하게 보여주는 것부터 시작하자. 그 후 코드를 몇 개 작성해볼 것이다.
1.1 왜 스칼라인가? 스칼라는 최근 소프트웨어 개발자의 가려운 부분을 잘 긁어주는 언어다. 스칼라는 간결하고 우 아하며 유연한 문법을 사용하는 정적 타입의 다중 패러다임 JVM 언어로, 작은 인터프리터 방 식의 스크립트부터 대규모의 복잡한 애플리케이션에 이르기까지 폭넓은 규모 확장성을 제공하 는 여러 도구를 제공한다. 말이 너무 많았다. 지금까지 언급한 개념이 어떤 뜻인지 자세히 살펴 보자. JVM과 자바스크립트 언어
스칼라는 JVM의 성능과 최적화를 활용함은 물론이고 자바를 중심으로 구축된 풍부한 기존 도구와 라이브러리 생태계도 이어받는다. 하지만 스칼라가 JVM에만 한정된 것은 아니다.
Scala.js ( http://www.scala-js.org )는 스칼라를 자바스크립트로 포팅하는 실험 프로젝 트다.
43
정적 타입
스칼라는 튼튼한 애플리케이션을 만드는 도구로 정적 타입 지정static typing 을 채택했다. 스칼 라 타입 시스템은 자바 타입 시스템의 여러 단점을 고쳤고, 타입 추론type inference 을 사용해서 대부분의 귀찮고 불필요한 타입 표기를 생략할 수 있게 지원한다. 다중 패러다임 - 객체지향 프로그래밍
스칼라는 객체지향 프로그래밍object-oriented programming (OOP )을 완벽히 지원한다. 스칼라는 혼합 합성mixin composition 을 사용해서 타입을 깔끔하게 구현하는 트레이트trait 로 자바 객체 모델 을 보완한다. 스칼라에서는 실제로 모든 것이 객체다. 심지어 수를 표현하는 타입도 객체다. 다중 패러다임 - 함수형 프로그래밍
스칼라는 함수형 프로그래밍functional programming (FP )을 완전히 지원한다. FP는 동시성, 빅데 이터, 일반적인 코드의 정확성1을 사고하는 데 있어 최선의 도구다. 불변값immutable value, 1 급first class 함수, 부수 효과side effect 가 없는 함수, 고차higher-order 함수, 함수 컬렉션 등은 모두 간결하고 강력하며 정확한 코드를 작성하는 데 기여한다. 복잡한 타입 시스템
스칼라는 자바 타입 시스템을 더 유연한 제네릭스generics 로 확장하고 코드 정확성을 높이기 위 해 몇 가지 개선을 덧붙였다. 타입 추론을 사용하는 스칼라 코드는 종종 동적 타입 언어만큼이 나 간결하다. 간결하고 우아하며 유연한 문법
번거롭고 긴 자바 코드가 스칼라에서는 간결한 구문으로 바뀐다. 스칼라 기능을 활용하면, 도메인 특화 언어domain specific language (DSL )를 만들거나, 마치 ‘원래의’ 스칼라 구문인 것처럼 느낄 수 있는 API를 제공할 수 있다. 규모 확장성 - 아키텍처
스칼라를 사용하면 작은 인터프리터 방식의 스크립트부터 대규모의 복잡한 애플리케이션까 지 작성할 수 있다. 1 ) 트레이트를 사용한 혼합 합성, 2 ) 추상 타입 멤버abstract type member 와
1 역주_ 정확성(correctness)은 소프트웨어가 요구 사항을 만족하고 설계나 코딩상의 결함이 없음을 의미한다.
44
1부 스칼라와의 만남
제네릭스, 3 ) 내포 클래스, 4 ) 명시적인 자기 타입 지정의 네 가지 언어 메커니즘이 규모와 관계없이 시스템을 조합할 수 있도록 만들어준다. 스칼라라는 이름은 scalable language에서 비롯되었다. 스칼라 언어의 창시자인 마틴 오더스 키Martin Odersky 가 스칼라를 시작한 것은 2001년이다. 첫 번째 공개 날짜는 2004년 1월 20일이 었다(http://bit.ly/1toEmFE ). 마틴은 스위스 로잔에 위치한 이공계 연구중심 대학교 EPFL 의 컴퓨터 및 통신학부 교수였다. 그는 파스칼로 유명한 니클라우스 비르트Niklaus Wirth 가 이끄는 그룹에서 박사 과정을 보냈다. 마틴은 초기 JVM 기반의 함수형 언어인 피자Pizza 를 만들었다. 그 후 자바 제네릭스로 채택된 프로토타입인 GJ를 하스켈Haskell 설계자 중 한 명인 필립 와들러 Philip Wadler
와 함께 작성했다. 썬 마이크로시스템즈는 마틴을 javac 참조 구현을 만들기 위해 고
용했다. 그가 만든 컴파일러의 후신이 현재 자바 개발자 키트Java Developer Kit (JDK )에 들어 있는 자바 컴파일러다.
1.1.1 스칼라의 매력 이 책의 초판이 나온 이래 스칼라 사용자가 얼마나 늘었는지 보면 스칼라가 우리 시대를 위한 언 어라는 필자의 생각이 맞았음을 알 수 있다. 여러분은 JVM, 라이브러리, 생산성 도구를 활용하 면서도 여전히 빅데이터, 동시성을 활용한 규모 확장성, 고가용성high availability 과 강건성robustness 을 제공하는 간결하지만 표현력 높은 문법을 제공하는 최신 기술의 언어를 즐길 수 있다. 어느 분야에서건 프로 개발자는 정교하면서도 강력한 도구와 기법이 필요하다. 도구에 통달하 기까지 시간이 걸리겠지만, 성공을 위해서는 필수 요소인 만큼 노력을 기울여야 한다. 필자는 스칼라가 프로 개발자를 위한 언어라고 믿는다. 물론 모든 사용자가 프로는 아니다. 하지 만 스칼라는 우리 분야의 프로들에게 필요한 다양한 기능과 고성능, 다양한 범주의 문제에 적 합한 표현력을 갖춘 언어다. 스칼라에 통달하기까지는 시간이 걸리겠지만, 일단 통달하고 나면 프로그래밍 언어로 인해 여러분이 제약을 받는 일은 결코 없을 것이다.
1.1.2 자바 8이 나왔지만 여전히 유용한 스칼라 자바 8은 자바 5의 제네릭스 도입 이래 가장 커다란 변화를 가져왔다. 이제 람다lambda 라 부르는
1장 빠른 출발: 스칼라 소개
45
진정한 익명 함수를 사용할 수 있다. 이 책에서는 익명 함수가 유용한 이유를 살펴볼 것이다. 또 한 인터페이스는 메서드 선언에 기본default 구현을 추가할 수 있도록 확장되었다. 그로 인해 스칼 라의 트레이트trait 처럼 유용한 혼합 합성을 사용할 수 있다. 자바 8 이전에는 이 두 특징이 스칼라 가 JVM에 도입한 가장 귀중한 개선처럼 여겨졌다. 그렇다면 자바 8이 도입된 현재 시점에도 여 전히 스칼라로 전환할 필요가 있을까? 스칼라는 자바가 도입할 가능성이 결코 없을 것으로 보이는 여러 개선을 포함하고 있다. 그중 에는 자바가 하위 호환성을 유지하기 위해 (불가피하게) 도입이 불가능한 것도 있고, 설령 도입 할 수 있더라도 몇 년 안에는 도입이 이뤄지기 어려운 것도 있다. 예를 들어 스칼라는 자바가 제 공하는 것보다 더 강력한 타입 추론을 제공한다. 스칼라에 있는 강력한 패턴 매칭pattern matching 과
for 내장 comprehension 은 코드 크기와 타입 간의 결합을 크게 줄여준다. 이런 요소가 그토록 귀중 한 이유는 뒤에서 천천히 살펴볼 것이다. 한편 JVM 관련 하부 구조 업그레이드를 조심스럽게 해야 할 타당한 이유가 있는 조직도 많다. 그런 조직에서는 당분간 자바 8 JVM을 도입하기 어려울 것이다. 하지만 이들도 최소한 자바 6이 나 7 JVM에서 스칼라를 활용할 수는 있다. 팀이 자바 8을 도입했지만, 스칼라가 팀에 최선의 방향인지 아직 결정하지 못했을 수도 있다. 그렇다 하더라도 이 책의 여러 기법은 자바 8 애플리케이션에도 유용하다. 나아가 이 책을 읽다 보면 스칼라로 전환해야 할 이유가 될 만한 요소를 발견할 수 있으리라 생각한다. 좋다, 이제 시작하자.
1.2 스칼라 설치하기 여기서는 이 책의 예제를 다루는 데 필요한 명령행 도구를 설치하는 방법을 설명한다.2 이 책에 서 사용한 예제는 책을 쓰는 시점에 가장 최신 버전인 스칼라 2.11.2로 작성하고 컴파일했다.3
1판의 스칼라 2.10.4 버전 예제도 대부분 변경 없이 활용했다. 여러 팀에서 아직 옛 버전을 사 용하기 때문이다. 2 주_ 21장에서 이런 도구를 더 자세히 다룬다. 3 역주_ 2016년 5월 초 기준 최신 버전은 2.11.8이다. 버전 차이가 크지 않기 때문에 2.11.2를 기준으로 소스 코드 등을 확인했다.
46
1부 스칼라와의 만남
NOTE_ 스칼라 2.11 배포판은 2.10에 없는 새로운 기능 몇 가지를 포함하고 있긴 하지만, 대부분 일반적 인 성능 향상과 라이브러리 리팩토링에 초점을 맞추었다. 한편 스칼라 2.10은 2.9와 비교해서 새로운 기능을 상당수 도입했다. 이 세 가지 버전 중 어느 것을 여러분 조직이 채택할지 모르는 만큼, 차차 진행하면서 가장 중요한 차이점을 논의할 것이다(2.11의 개괄은 http://bit.ly/1DDlYtH에 있고, 2.10의 개괄은 http://bit.
ly/1toEt3R에 있다).
설치 절차는 다음과 같다. 자바 설치
스칼라 2.12가 나올 때까지는 자바 6, 7, 8을 모두 사용할 수 있으며, 컴퓨터에 자바를 반드 시 설치해야 한다(2016년 나올 예정인 스칼라 2.12는 자바 8만 지원할 것이다). 자바를 설 치해야 한다면 오라클 웹사이트(http://bit.ly/TEA7iC )에서 자바 개발 도구Java Development Kit
(JDK ) 설치 안내를 따르라.
SBT 설치
사실상 산업 표준 스칼라 빌드 도구인 SBT를 SBT 홈페이지(http://bit.ly/1toEO6H )의 절차를 따라 설치하라. 설치를 마친 다음에는 리눅스나 OS X 터미널, 윈도우 커맨드 창에서
SBT 명령을 사용할 수 있다. 다른 빌드 도구를 활용할 수도 있다. 21.2.2절 ‘다른 빌드 도구’ 를 살펴보라. 코드 예제 내려받기
20쪽 ‘코드 예제 내려받기’를 따라 예제를 내려받으라. 압축 파일을 컴퓨터의 적당한 위치에 풀면 된다. SBT 실행
셸이나 커맨드 창을 열고 코드 예제를 풀어둔 디렉터리로 이동한다. sbt test를 입력하면 스 칼라 컴파일러나 외부 라이브러리를 포함해서 해당 프로젝트가 의존하는 모든 라이브러리를 내려받는다. 따라서 인터넷 연결이 필요하며, 전체를 내려받는 데 약간 시간이 걸린다. sbt는 코드를 컴파일한 다음에 단위 테스트를 실행할 것이다. ‘success’로 끝나는 메시지를 수없 이 보게 될 것이다. 같은 명령을 한 번 더 입력하면 새로 실행할 것이 전혀 없기 때문에 매우 빠르게 명령 실행이 끝날 것이다.
1장 빠른 출발: 스칼라 소개
47
축하한다! 이제 시작할 준비가 되었다. 하지만 몇몇 유용한 요소를 더 설치하기 원할 수도 있다. TIP
이 책에서는 대부분 SBT를 사용해서 도구를 간접적으로 사용할 것이다. SBT는 우리가 원하는 버전의 스칼라 컴파일러, 표준 라이브러리, 그리고 프로젝트와 의존관계가 있는 서드파티 라이브러리를 모두 내려받는다.
SBT로 작업하지 않는 경우를 대비해서 스칼라 도구를 별도로 내려받아두면 편리하다. 몇 가지 예제를 SBT 밖에서 스칼라를 사용해서 실행해볼 것이다. 공식 스칼라 웹사이트( http://www.scala-lang.org )에서 링크를 따라가 스칼라를 내려 받고, 원한다면 스칼라를 위한 자바독javadoc 이라고 할 수 있는 스칼라독scaladoc 도 내려받으라 (2.11부터는 스칼라 라이브러리와 스칼라독이 여러 작은 라이브러리로 분할되었다). 스칼라 독을 온라인(http://bit.ly/1u1pv56 )으로 볼 수도 있다. 여러분의 편의를 위해 이 책에서 스칼 라 표준 라이브러리에 있는 타입을 언급할 때는 관련 스칼라독 페이지 링크도 제공할 것이다. 스칼라독 왼쪽의 타입 목록 맨 위에 있는 검색 필드를 사용하면 편리하다. 원하는 타입을 빠르게 검색하고 싶을 때 아주 유용하다. 또한 각 타입에 관한 문서에는 스칼라 깃허브 저장소(http://
bit.ly/1wOjJU0 )에 있는 소스 코드로 갈 수 있는 링크가 들어 있다. 소스 코드를 통해 라이브 러리 내부 구현을 잘 배울 수 있다. ‘Source’라는 표시가 있는 링크를 살펴보라. 일반적으로 스 칼라독 페이지에 있는 타입을 간략히 정리해놓은 소개글 다음에 소스 코드에 관한 링크가 위치 한다. 예제는 간단한 텍스트 편집기나 통합 개발 환경integrated development environment (IDE )만으로도 충분 히 시도해볼 수 있다. 모든 주요 편집기나 IDE의 스칼라 지원 플러그인을 찾을 수 있다. 자세한 내용은 21.3절 ‘IDE나 텍스트 편집기와 통합하기’를 보라. 일반적으로 여러분이 선호하는 편집 기의 커뮤니티야말로 해당 편집기의 스칼라 지원 관련 최신 정보를 찾기에 가장 좋은 곳이다.
1.2.1 SBT 사용하기 SBT가 어떻게 작동하는지는 21.2.1절 ‘스칼라 표준 빌드 도구 SBT ’에서 배울 것이다. 여기서 는 시작하는 데 필요한 기본적인 내용만 다룬다. sbt 명령을 시작할 때 어떤 작업을 수행할지 지정하지 않는다면, SBT는 대화형 REPL Read, Eval, Print, Loop
(명령을 읽어서 계산한 다음에 결과를 출력하는 루프)로 시작한다. REPL로 시작해서
사용 가능한 ‘작업’을 확인해보자. 48
1부 스칼라와의 만남
다음 목록에서 $는 셸 명령행 프롬프트(예를 들면 bash )며, 거기서 sbt 명령을 시작해야 한다. >는
SBT의 프롬프트고,4 #는 sbt 주석을 시작하는 기호다. 아래 명령을 원하는 순서로 마음대
로 실행할 수 있다.
$ sbt > help > tasks > tasks -V > compile > test > clean > ~test
> > > > >
console run show x eclipse exit
# # # # # # # # # # # # # #
명령을 설명한다. 현재 사용 가능한 작업 중 가장 일반적으로 사용하는 것들을 보여준다. 모든 가용 작업을 보여준다. 코드를 증분 컴파일한다. 코드를 증분 컴파일하고 테스트를 실행한다. 빌드로 산출된 결과물(중간 파일 등 포함)을 지운다. 저장된 파일이 바뀌면 증분 컴파일과 테스트를 진행한다. 작업 앞에 ‘~ ’를 붙이면 프로젝트 파일을 감시하다가, 일부 또는 전부가 변경되는 경우 작업을 실행한다는 뜻이다. 스칼라 REPL을 시작한다. 프로젝트의 ‘main’ 루틴 중 하나를 실행한다. 변수 ‘x’의 정의를 본다. 이클립스 프로젝트 파일을 생성한다.
REPL을 종료한다(Ctrl-D를 눌러도 마찬가지다).
나는 ~test를 사용해서 항상 변경 사항을 컴파일하고 관련 테스트를 실행한다. SBT는 증분 컴 파일러incremental compiler 와 테스트 실행기test runner 를 사용한다. 따라서 매번 전체를 새로 빌드할 때까지 기다릴 필요가 없다. ~test 상태에서 다른 명령을 실행하거나 sbt를 종료하고 싶다면 리턴키를 누르면 된다. eclipse 작업은 스칼라 플러그인을 설치해서 이클립스를 사용하는 경우에 유용하다. 이 명령은
프로젝트 코드를 이클립스에서 임포트할 수 있도록 적절한 프로젝트 파일을 생성한다. 예제 코드 를 이클립스에서 사용하고 싶은 독자는 eclipse를 실행하기 바란다. 인텔리J 아이디어IntelliJ IDEA 에서 스칼라를 사용한다면 직접 SBT 프로젝트를 임포트하면 된다. 스칼라는 자신만의 REPL이 있다. 이를 console 명령을 사용해서 실행할 수 있다. 이 책의 예제를
REPL에서 실행해야 하는 경우 대부분 console을 먼저 실행해야 할 것이다.
4 역주_ $는 유닉스 c 셸 계열의 기본 프롬프트다. 사용하는 셸이나 운영 체계, 사용자 설정에 따라 프롬프트는 달라질 수 있다.
1장 빠른 출발: 스칼라 소개
49
$ sbt > console [info] Updating {file:/.../prog-scala-2nd-ed/}prog-scala-2nd-ed... [info] ... [info] Done updating. [info] Compiling ... [info] Starting scala interpreter... [info] Welcome to Scala version 2.11.2 (Java HotSpot(TM) 64-Bit Server VM, Java ...). Type in expressions to have them evaluated. Type :help for more information. scala> 1 + 2 res0: Int = 3 scala> :quit
출력 중 일부를 생략했다. SBT REPL과 마찬가지로 Ctrl-D로 끝낸다. console을 실행하면 SBT는 먼저 프로젝트를 빌드하고 빌드한 프로젝트 결과를 CLASSPATH에
등록해서 스칼라가 사용할 수 있게 만든다. 따라서 여러분이 작성한 코드를 REPL에서 실험할 수 있다. TIP
스칼라 REPL을 사용하면 API를 배우고 스칼라 코드의 관용구를 익히는 데 매우 효율적이다. 심지어 자바
API도 익힐 수 있다. SBT에서 console로 스칼라 REPL을 시작하면 프로젝트 의존관계와 컴파일한 프로젝 트를 모두 CLASSPATH에 등록해주기 때문에 편리하다.
1.2.2 스칼라 명령행 도구 실행하기 스칼라 명령행 도구를 별도로 설치했다면, 자바 컴파일러 javac와 비슷하게 scalac라는 명령 으로 스칼라 컴파일러를 사용할 수 있다. 이 책에서는 직접 컴파일러를 사용하지는 않고 SBT를 활용해서 빌드할 것이다. javac를 실행해본 사람이라면 scalac 사용법도 쉽게 익힐 수 있다. 커맨드 창에서 다음 명령을 실행해서 컴파일러 버전과 명령행 인자에 관한 도움말을 보라. 앞에 서와 마찬가지로 $ 프롬프트 뒤의 명령을 입력해야 한다. 나머지는 모두 명령의 출력이다.
50
1부 스칼라와의 만남
$ scalac -version Scala compiler version 2.11.2 -- Copyright 2002-2013, LAMP/EPFL $ scalac -help Usage: scalac <options> <source files> where possible standard options include: Pass -Dproperty=value directly to the runtime system. -Dproperty=value Pass <flag> directly to the runtime system. -J<flag> Pass an option to a plugin -P:<plugin>:<opt> ...
마찬가지로 java와 비슷하게 scala 명령을 사용하면 프로그램을 실행할 수 있다.
$ scala -version Scala code runner version 2.11.2 -- Copyright 2002-2013, LAMP/EPFL $ scala -help Usage: scala <options> [<script|class|object|jar> <arguments>] or scala -help All options to scalac (see scalac -help) are also allowed. ...
때때로 scala를 사용해서 스칼라 ‘스크립트’ 파일을 실행할 것이다. java 명령에는 이런 기능이 없다. 코드 예제에서 다음 스크립트를 살펴보자.
// src/main/scala/progscala2/introscala/upper1.sc class Upper { def upper(strings: String*): Seq[String] = { strings.map((s:String) => s.toUpperCase()) } } val up = new Upper println(up.upper("Hello", "World!"))
1장 빠른 출발: 스칼라 소개
51
scala 명령으로 이 스크립트를 실행하라. 리눅스나 맥 OS X 사용자라면 첫 줄에 있는 경로를
사용해서 이 예제를 실행한다. 현재 디렉터리는 풀어둔 코드 예제의 최상위 디렉터리라고 가정 한다. 윈도우라면 슬래시(/) 대신 역슬래시(\)를 사용해야 한다.
$ scala src/main/scala/progscala2/introscala/upper1.sc ArrayBuffer(HELLO, WORLD!)
드디어 모든 프로그래밍 책이 꼭 지켜야 하는 조건, 즉 첫 프로그램이 ‘Hello World!’를 출력 해야 한다는 조건을 만족시켰다. 실행 대상인 컴파일된 main 루틴이나 스크립트 파일을 지정하지 않고 scala를 호출하면 REPL 모드로 들어간다. 이는 sbt에서 console 명령을 사용한 것과 비슷하다(하지만 sbt의 console 작업을 실행한 경우와 클래스 경로는 다르다). 다음은 몇 가지 유용한 명령을 보여주는 REPL 세 션이다(스칼라를 따로 설치하지 않은 독자는 sbt에서 console 작업을 사용해야 스칼라 REPL로 이런저런 실험이 가능하다). 이제 REPL 프롬프트인 scala>를 볼 수 있다. 출력 중 일부는 생략 했다.
$ scala Welcome to Scala version 2.11.2 (Java HotSpot(TM)...). Type in expressions to have them evaluated. Type :help for more information. scala> :help All commands can be abbreviated, e.g. :he instead of :help. add a jar or directory to the classpath :cp <path> edit history :edit <id>|<line> print this summary or command-specific help :help [command] show the history (optional num is commands to show) :history [num] ... 추가 목록 표시됨 scala> val s = "Hello, World!" s: String = Hello, World! scala> println("Hello, World!") Hello, World!
52
1부 스칼라와의 만남
scala> 1 + 2 res3: Int = 3 scala> s.con<tab> concat contains
contentEquals
scala> s.contains("el") res4: Boolean = true scala> :quit $ # 다시 셸 프롬프트로 돌아옴
s라는 변수에 문자열 "Hello, World!"를 대입했다. 이때 val 키워드를 사용해서 불변값으로
선언했다. println 함수(http://bit.ly/1aLWLEh )는 콘솔에 문자열을 출력한 다음 줄을 바꿔 준다. 이 println은 자바의 System.out.println (http://bit.ly/1s0NQ5n )과 결과적으로 같은 일 을 한다. 또한 스칼라는 문자열로 자바 문자열인 String (http://bit.ly/1wl7Bdg )을 사용한다. 다음으로 두 수를 더하되 결과를 변수에 대입하지는 않았다. REPL은 res3이라는 새 변수를 만 들어서 나중에 활용할 수 있게 해준다.
REPL은 탭 완성 기능을 제공한다. s.con<tab>과 같이 표시한 것은 s.con까지 입력하고 탭키를 눌렀다는 뜻이다. REPL은 String에 대해 con으로 시작하는 호출 가능한 메서드의 목록을 표시 해준다. 목록에서 contains 메서드를 선택해서 실행했다. 마지막으로 :quit 명령으로 REPL에서 나왔다. Ctrl-D로도 같은 일을 할 수 있다. 필요할 때마다 추가 REPL 명령을 설명할 것이다. 또한 21.1절 ‘명령행 도구’에서 REPL 명령을 더 자세히 살펴볼 것이다.
1.2.3 IDE에서 스칼라 REPL 실행하기 이클립스, 인텔리J 아이디어, 넷빈즈NetBeans 등의 IDE를 사용할 경우 편하게 쓸 수 있도록 IDE 에서 REPL을 사용하는 방법을 빠르게 살펴보자. 이클립스와 아이디어에는 컴파일을 위한 코 드나 스크립트를 편집할 때처럼 스칼라 코드를 편집하고 결과를 바로바로 볼 수 있는 워크시트
1장 빠른 출발: 스칼라 소개
53
worksheet
가 있다. 이 기능은 단순하지 않은 코드를 여러 번 수정하면서 실행해야 할 경우에 유용하
다. 넷빈즈에도 비슷한 대화식 콘솔Interactive Console 기능이 있다. 이들 IDE 중 하나를 사용하는 독자는 21.3절 ‘IDE나 텍스트 편집기와 통합하기’에서 스칼라 플러그인의 정보와 워크시트 또는 대화식 콘솔 사용법을 볼 수 있다.
1.3 스칼라 맛보기 1장의 나머지와 이어지는 2, 3장에 걸쳐 여러 스칼라 기능을 빠르게 살펴볼 것이다. 진행 과정 에서 어떤 일이 벌어지는지 여러분이 이해할 수 있는 정도로만 간략히 설명하겠다. 더 깊은 배경 지식을 배우려면 좀 더 기다려야 한다. 일단 1~3장은 스칼라 문법 소개와 매일매일 스칼라로 어떤 코드를 작성하는지 맛보는 과정으로 생각하라. TIP
스칼라 라이브러리에 있는 여러 타입을 이 책에서 언급할 때, 그와 관련해서 스칼라독을 자세히 살펴보는 작업 이 유용함을 알게 될 것이다. 현재 버전의 스칼라독은 http://bit.ly/1u1pv56에 있다. 왼쪽의 타입 목록 맨 위 에 있는 검색 필드를 이용하면 편리하다. 스칼라독은 타입 목록을 알파벳순으로 표시하는 자바독과 달리 타입 을 패키지별로 모아서 표시하기 때문에, 원하는 타입을 빠르게 검색할 수 있는 검색 창이 더 유용하다.
이 책에서는 대부분 스칼라 REPL을 사용할 것이다. 세 가지 방법, 즉 스크립트나 ‘main’ 인자 없이 직접 scala 명령을 실행하거나, SBT console 명령을 사용하거나, IDE가 제공하는 워크 시트를 사용해서 REPL을 시작할 수 있음을 다시 기억하라.
IDE를 사용하지 않는다면 가능한 SBT를 사용하길 권한다. 특히 여러분이 프로젝트를 수행 중 이라면 더 그렇다. 이 책에서는 대부분 SBT를 사용하겠지만, 직접 scala를 실행하거나 IDE에 서 워크시트를 실행하더라도 그다음 단계는 모두 동일하다. 여러분이 실제로는 IDE를 더 선호 한다고 해도, 커맨드 창에서 SBT를 한번 실행해서 모양을 살펴보라. 나 자신은 IDE를 거의 사 용하지 않지만, 그건 단지 개인 성향일 뿐이다. 셸 창에서 코드 예제의 최상위 디렉터리로 이동한 다음 sbt를 시작하자.
>
프롬프트 상에서
console을 입력하라. 이제부터는 sbt나 scala 출력 중 불필요한 부분은 생략하겠다.
54
1부 스칼라와의 만남
CHAPTER
2
입력은 조금만, 일은 더 많이
첫 장을 아카 액터 애플리케이션에 대한 ‘맛보기’ 예제로 끝냈다. 2장에서도 스칼라의 특징을 알아보는 여정을 이어간다. 특히 간결하고 유연한 코드를 작성할 수 있게 돕는 특징 위주로 진 행할 것이다. 파일과 패키지를 구성하는 방법, 다른 타입을 임포트하는 방법, 변수와 메서드를 선언하는 방법을 논의한 다음, 특히 유용한 타입을 몇 가지 설명하고, 기타 문법적 관례를 설명 할 것이다.
2.1 세미콜론 세미콜론은 예제를 구분하는 구분자delimiter 며, 스칼라는 이를 추론한다. 스칼라는 한 줄의 끝에 서 식을 다음 줄로 계속 이어가야 한다고 추론하지 않는 경우, 줄 끝을 식의 끝으로 취급한다.
// src/main/scala/progscala2/typelessdomore/semicolon-example.sc // 줄이 등호로 끝나면 다음 줄에 이어지는 코드가 더 있다는 뜻이다. def equalsign(s: String) = println("equalsign: " + s) // 줄이 중괄호를 열고 끝나면 다음 줄에 이어지는 코드가 더 있다는 뜻이다. def equalsign2(s: String) = { println("equalsign2: " + s) }
83
// 줄이 쉼표, 마침표, 연산자로 끝나면 다음 줄에 이어지는 코드가 더 있다는 뜻이다. def commas(s1: String, s2: String) = Console. println("comma: " + s1 + ", " + s2)
컴파일러와 비교했을 때 REPL은 좀 더 공격적으로 줄 끝을 식의 끝으로 판단한다. 따라서 여 러 줄에 걸친 식을 입력하는 경우에는 위 예제에서와 같이 (마지막 줄을 제외한) 각 줄을 식을 계속 이어가는 문자로 끝내는 것이 좋다. 반대로 한 줄에 여러 식을 넣을 때는 세미콜론으로 구분할 수 있다. TIP
REPL에서 세미콜론 추론이 너무 공격적이거나 또는 여러 식을 한꺼번에 붙여 넣어야 하는 경우 :paste 모드 를 활용하라. :paste를 입력하고 여러분이 원하는 코드를 입력한 다음 Ctrl-D를 눌러서 모드를 끝내라.
2.2 변수 정의 스칼라에서는 변수variable 가 불변(읽기 전용)인지 아닌지(읽기-쓰기) 선언 시 지정할 수 있다. 우린 이미 변경 불가능한 ‘변수’1를 val이라는 키워드로 선언할 수 있다는 것을 안다(이를 값 객 체value object 라고 생각하라).
val array: Array[String] = new Array(5)
대부분의 변수가 힙에 할당된 객체를 참조한다는 점에서 스칼라는 자바와 비슷하다. 방금 본 array라는 참조는 다른 Array를 가리키도록 재할당될 수 없다. 하지만 배열의 원소는 다음과
같이 변경 가능하다.
1 역주_ 변수(variable)라는 말 자체가 변할 수 있는 값이란 뜻이다. 하지만 프로그램 안에서 어떤 값에 이름을 붙이는 경우 변수라고 하는 경우도 많다. 그래서 val 변수, var 변수라고 말하기도 한다. 따라서 스칼라에서 변수라는 말이 항상 참조 대상을 대입을 통해 바꿀 수 있 는 이름만을 지칭하지는 않는다는 점에 유의하라. 사실 함수형 언어 계통에서는 val로 선언한 것을 변수라고 부르기보다는 그냥 값이나 이름이라고 부르는 것을 더 좋아한다.
84
1부 스칼라와의 만남
scala> val array: Array[String] = new Array(5) array: Array[String] = Array(null, null, null, null, null) scala> array = new Array(2) <console>:8: error: reassignment to val array = new Array(2) scala> array(0) = "Hello" scala> array res1: Array[String] = Array(Hello, null, null, null, null)
val은 선언 시 반드시 초기화해야 한다.
이와 비슷하게 변경 가능한 변수는 var라는 키워드로 선언할 수 있다. 이런 변수는 변경 가능하 므로 나중에 바꿀 수 있지만, 그럼에도 불구하고 반드시 선언할 때 초기화해야 한다.
scala> var stockPrice: Double = 100.0 stockPrice: Double = 100.0 scala> stockPrice = 200.0 stockPrice: Double = 200.0
분명히 하자면 우리는 stockPrice 자체의 값을 바꿨다. 하지만 stockPrice가 가리키는 ‘객체’ 를 변경할 수는 없다. Double이 불변이기 때문이다. 자바에서 기본 타입primitive type 이라고 부르는 char, byte, short, int, long, float, double, boolean은 근본적으로 객체(참조 타입)ㄱ와 다르다. 실제로 이런 기본 타입에는 객체도 참조
도 존재하지 않는다. 단지 ‘원래의’ 값만 존재한다. 스칼라는 좀 더 일관성 있게 객체지향적이기 위해 노력하고 있다. 그래서 스칼라에서는 이런 타입도 참조 타입과 마찬가지로 실제로 메서드 가 있는 객체다(8.2절 ‘참조 타입과 값 타입’을 보라). val이나 var 선언 시 반드시 초기화해야 한다는 규칙에는 몇 가지 예외가 있다. 예를 들어 타입
의 생성자 매개변수에 val이나 var를 사용할 수 있다. 그러면 매개변수가 그 타입의 필드가 된 다. 이때 val을 사용하면 변경 불가능, var를 사용하면 변경 가능 필드다.
2장 입력은 조금만, 일은 더 많이
85
다음 REPL 세션을 보자. 여기서는 Person 클래스를 정의하면서 변경 불가능한 이름(name )과 변경 가능한 나이(age ) 필드를 만든다(아마도 시간이 지남에 따라 사람들이 나이를 먹기 때문 이라고 생각한다).
// src/main/scala/progscala2/typelessdomore/person.sc scala> class Person(val name: String, var age: Int) defined class Person scala> val p = new Person("Dean Wampler", 29) p: Person = Person@165a128d scala> p.name res0: String = Dean Wampler
// name 값을 보여준다.
scala> p.age res2: Int = 29
// age 값을 보여준다.
scala> p.name = "Buck Trends" <console>:9: error: reassignment to val p.name = "Buck Trends" ^ scala> p.age = 30 p.age: Int = 30
// 변경할 수 없다!
// 변경할 수 있다!
NOTE_ var나 val 키워드는 어떤 참조가 다른 객체를 참조하도록 변경될 수 있는지(var), 또는 없는지(val) 여부만 지정한다. 참조가 가리키는 대상 객체의 내부 상태를 변경 가능한지 여부는 지정하지 않는다.
변경으로 인해 생길 수 있는 버그 유형을 방지하기 위해 가능한 한 변경 불가능한 값을 활용하라. 예를 들어 해시 기반의 맵에 변경 가능한 객체를 키로 사용하는 것은 위험하다. 해당 객체의 내 용이 바뀌면 hashCode 메서드의 반환값이 달라질 것이다. 그래서 바뀐 해시값으로는 원래 객 체와 대응시켰던 값의 원래 위치를 찾지 못할 수도 있다. 더 흔한 것은 여러분이 사용 중인 객체를 다른 누군가가 변경해서 생기는 예기치 못한 동작이다. 양자 물리에서 빌려온 용어를 사용해서 이런 버그를 유령 같은 원격 작용spooky action at a distance 이
86
1부 스칼라와의 만남
라고 부른다. 여러분이 지역적으로 수행한 일은 발생한 오류와 아무런 관련이 없고, 어딘가 다 른 곳에 원인이 있기 때문이다. 이런 버그는 공유된 변경 가능한 상태에의 접근을 동기화할 필요가 있는 다중 스레드 프로그램 에 가장 치명적이다. 이런 동기화는 제대로 하기 몹시 어렵다. 변경 불가능한 값을 사용한다면 이런 문제를 방지할 수 있다.
2.3 범위 메서드를 정의하는 법을 알아보자. 사용할 예제 중 일부에서 범위Range (http://bit.ly/1wNfl5V ) 라는 개념을 사용한다. 그러므로 범위를 먼저 이해하고 넘어가자. 때로 어떤 시작 값부터 마지막 값에 이르는 수열이 필요한 경우가 있다. Range 리터럴이 바로 그렇다. 다음 예는 Range 객체가 지원하는 Int, Long, Float, Double, Char, BigInt (크기와 관계없이 임의의 정수를 지원, http://bit.ly/13FUtUH ), 그리고 BigDecimal (크기와 관계없 이 임의의 소수를 지원, http://bit.ly/1b7tLWP ) 타입에 범위를 생성하는 방법을 보여준다. 범위를 만들 때는 끝 값을 포함시키거나 포함시키지 않고 만들 수도 있다. 또한 증분 값이 1이 아닌 경우, 그 값을 지정할 수 있다. 다음 출력의 일부는 페이지 크기에 맞춰 줄 바꿈 했다.
scala> 1 to 10 // Int 범위, 끝 값 포함, 증분 1, (1 to 10) res0: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) scala> 1 until 10 // Int 범위, 끝 값 제외, 증분 1, (1 to 9) res1: scala.collection.immutable.Range = Range(1, 2, 3, 4, 5, 6, 7, 8, 9) scala> 1 to 10 by 3 // Int 범위, 끝 값 포함, 증분 3 res2: scala.collection.immutable.Range = Range(1, 4, 7, 10) scala> 10 to 1 by -3 // Int 범위, 끝 값 포함, 증분 3, 큰 값부터 감소 res2: scala.collection.immutable.Range = Range(10, 7, 4, 1) scala> 1L to 10L by 3 // Long 범위, 끝 값 포함, 증분 3 res3: scala.collection.immutable.NumericRange[Long] = NumericRange(1, 4, 7, 10)
2장 입력은 조금만, 일은 더 많이
87
scala> 1.1f to 10.3f by 3.1f // Float 범위, 증분이 1이 아님 res4: scala.collection.immutable.NumericRange[Float] = NumericRange(1.1, 4.2, 7.2999997) scala> 1.1f to 10.3f by 0.5f // Float 범위, 증분이 1보다 작음 res5: scala.collection.immutable.NumericRange[Float] = NumericRange(1.1, 1.6, 2.1, 2.6, 3.1, 3.6, 4.1, 4.6, 5.1, 5.6, 6.1, 6.6, 7.1, 7.6, 8.1, 8.6, 9.1, 9.6, 10.1) scala> 1.1 to 10.3 by 3.1 // Double 범위 res6: scala.collection.immutable.NumericRange[Double] = NumericRange(1.1, 4.2, 7.300000000000001) scala> 'a' to 'g' by 3 // Char 범위 res7: scala.collection.immutable.NumericRange[Char] = NumericRange(a, d, g) scala> BigInt(1) to BigInt(10) by 3 res8: scala.collection.immutable.NumericRange[BigInt] = NumericRange(1, 4, 7, 10) scala> BigDecimal(1.1) to BigDecimal(10.3) by 3.1 res9: scala.collection.immutable.NumericRange.Inclusive[scala.math.BigDecimal] = NumericRange(1.1, 4.2, 7.3)
2.4 부분 함수 부분 함수PartialFunction (http://bit.ly/1yMpzEP )의 특성을 알아보자. 부분 함수에서 부분이란 말은 함수가 모든 가능한 입력에 대해 결과를 정의하지는 않는다는 뜻이다. 지정한 케이스 절에 서 어느 하나와 일치하는 입력에 대해서만 결과를 정의한다. 부분 함수 안에는 케이스 절만 들어갈 수 있고 반드시 전체를 중괄호로 묶어야 한다. 반면 ‘일 반적’ 함수 리터럴은 중괄호뿐 아니라 괄호로도 본문 식을 묶을 수 있다. 케이스 절의 하나와 일치하지 않는 값이 부분 함수의 인자로 들어오면 MatchError (http://bit.
ly/1DDkcsB ) 실행 시점 예외가 발생한다. 부분 함수, 즉 PartialFunction이 어떤 입력과 일치하는지는 isDefinedAt 메서드를 호출해 서 알 수 있다. 이 메서드를 사용해서 MatchError 예외를 발생시킬 위험을 없앨 수 있다.
88
1부 스칼라와의 만남
pf1 orElse pf2 orElse pf3 ... 등과 같이 PartialFunction을 여럿 ‘연쇄 호출’할 수 있
다. pf1과 일치하지 않으면 pf2를 시도하고, pf2가 실패하면 또 pf3를 시도하는 식이다. MatchError는 모든 부분 함수가 입력과 일치하지 않는 경우에 발생한다.
다음 예제는 이를 보여준다.
// src/main/scala/progscala2/typelessdomore/partial-functions.sc val pf1: PartialFunction[Any,String] = { case s:String => "YES" } val pf2: PartialFunction[Any,String] = { case d:Double => "YES" }
// ➊ // ➋
val pf = pf1 orElse pf2
// ➌
def tryPF(x: Any, f: PartialFunction[Any,String]): String = try { f(x).toString } catch { case _: MatchError => "ERROR!" }
// ➍
def d(x: Any, f: PartialFunction[Any,String]) = f.isDefinedAt(x).toString
// ➎
| println(" pf1 - String | pf2 - Double | pf - All") // ➏ | def? | pf1(x) | def? | pf2(x) | def? | pf(x)") println("x println("+++++++++++++++++++++++++++++++++++++++++++++++++++++++") List("str", 3.14, 10) foreach { x => printf("%-5s | %-5s | %-6s | %-5s | %-6s | %-5s | %-6s\n", x.toString, d(x,pf1), tryPF(x,pf1), d(x,pf2), tryPF(x,pf2), d(x,pf), tryPF(x,pf)) }
➊ 문자열과만 일치하는 부분 함수 ➋ 더블과만 일치하는 부분 함수 ➌ 두 함수를 묶어서 문자열과 더블 모두에 일치하는 새 부분 함수를 만듦 ➍ 부분 함수를 호출하고 발생하는 MatchError를 잡아내는 함수. 성공 여부와 관계없이 문자 열 반환 ➎ isDefinedAt을 호출해서 문자열 결과를 반환하는 함수 ➏ 여러 조합을 시도해서 결과를 표로 출력
2장 입력은 조금만, 일은 더 많이
89
이 코드의 나머지 부분에서는 세 가지 다른 부분 함수에 여러 다른 값을 시도해본다. 우선은 isDefinedAt을 호출해보고(출력란의 def? 열), 그 후 함수 자체를 호출한다.
pf1 - String | pf2 - Double | pf - All | | def? | pf1(x) | def? | pf2(x) | def? | pf(x) x +++++++++++++++++++++++++++++++++++++++++++++++++++++++ | true | YES | false | ERROR! | true | YES str | true | YES 3.14 | false | ERROR! | true | YES | false | ERROR! | false | ERROR! | false | ERROR! 10
pf1은 String이 주어지지 않으면 실패한다. 반면 pf2는 Double이 주어지지 않으면 실패한다.
두 함수 모두 Int가 주어지는 경우 실패한다. 조합한 함수는 String이나 Double이 주어진 경 우 성공하지만, Int에 관해서는 여전히 실패한다.
2.5 메서드 선언 메서드 정의 예제를 계속 진행하자. 앞에서 사용했던 Shape 예제를 약간 변형해서 사용하겠다 (단순화를 위해 Triangle은 제외한다).
2.5.1 메서드의 기본 인자와 이름 붙은 인자 다음은 Point 케이스 클래스를 고친 것이다.
// src/main/scala/progscala2/typelessdomore/shapes/Shapes.scala package progscala2.typelessdomore.shapes case class Point(x: Double = 0.0, y: Double = 0.0) { def shift(deltax: Double = 0.0, deltay: Double = 0.0) = copy (x + deltax, y + deltay) }
90
1부 스칼라와의 만남
// ➊ // ➋
➊ 기본 초깃값을 지정한 Point를 정의한다(1장과 같음). ➋ 새로 정의한 shift 메서드는 기존 Point로부터 x, y축의 위치를 더하거나 뺀 새 Point를 만든다. shift는 케이스 클래스에 자동으로 생기는 copy 메서드를 활용한다. copy 메서드를 사용하면 케이스 클래스의 기존 인스턴스를 복사하면서 일부 필드를 변경해서
새로운 객체를 만들 수 있다. 이 기능은 케이스 클래스가 큰 경우 매우 유용하다.
scala> val p1 = new Point(x = 3.3, y = 4.4) // 이름 붙은 인자를 명시적으로 사용한다. p1: Point = Point(3.3,4.4) scala> val p2 = p1.copy(y = 6.6) // 복사하되, y 값을 새로 지정한다. p2: Point = Point(3.3,6.6)
이름 붙은 인자named argument 를 사용하면 메서드를 사용하는 코드를 더 읽기 쉽게 만들 수 있다. 또한 인자 목록이 길고, 그 안에 같은 타입의 필드가 여럿 있는 경우에는 버그를 방지하는 데 도움이 된다. 물론 무엇보다 인자 목록이 너무 길어지지 않게 방지하는 것이 더 중요하다.
2.5.2 인자 목록이 여럿 있는 메서드 다음으로 Shape에 대한 변경을 생각해보자. 특히 draw 메서드의 변경 사항을 눈여겨보자.
abstract class Shape() { /** * 두 인자 목록을 받는다. * 한 인자 목록은 그림을 그릴 때 x, y축 방향으로 이동시킬 오프셋 값이고, * 나머지 인자 목록은 앞에서 봤던 함수 인자다. */ def draw(offset: Point = Point(0.0, 0.0))(f: String => Unit): Unit = f(s"draw(offset = $offset), ${this.toString}") } case class Circle(center: Point, radius: Double) extends Shape case class Rectangle(lowerLeft: Point, height: Double, width: Double) extends Shape
2장 입력은 조금만, 일은 더 많이
91
그렇다. draw는 이제 두 인자가 들어 있는 인자 목록을 하나만 가지는 대신, 인자를 하나만 받는 인자 목록을 두 개 가진다. 첫 인자 목록은 모양이 그려질 오프셋을 지정한다. 기본값으로 아무 이동이 없는 Point (0.0, 0.0 )을 지정했다. 두 번째 인자 목록의 인자는 원래의 draw와 같이 그림을 그리는 함수다. 원하는 만큼 인자 목록을 추가할 수 있다. 하지만 둘 이상을 사용하는 경우는 드물다. 그렇다면 인자 목록을 둘 이상 사용할 수 있도록 허용해야 하는 이유는 무엇일까? 여러 인자 목록을 사용하면, 마지막 인자 목록이 함수 인자를 하나만 받는 경우 다음과 같이 아주 멋진 블 록 구조의 구문을 사용할 수 있다. 새 draw 메서드를 다음과 같이 호출할 수 있을 것이다.
s.draw(Point(1.0, 2.0))(str => println(s"ShapesDrawingActor: $str"))
스칼라에서는 인자 목록을 둘러싼 괄호를 중괄호로 바꿔 쓸 수 있다. 따라서 이 코드는 다음과 같이 쓸 수 있다.
s.draw(Point(1.0, 2.0)){str => println(s"ShapesDrawingActor: $str")}
함수 리터럴이 한 줄에 쓰기에 너무 길면, 다음과 같이 쓸 수도 있다.
s.draw(Point(1.0, 2.0)) { str => println(s"ShapesDrawingActor: $str") }
또는 다음과 같이 쓸 수도 있다.
s.draw(Point(1.0, 2.0)) { str => println(s"ShapesDrawingActor: $str") }
92
1부 스칼라와의 만남
이 코드는 if나 for, 메서드 본문 등의 기본 언어 구성 요소에서 사용해온 전형적인 코드 블록 과 비슷해 보인다. 하지만 이 {...} 블록은 여전히 draw에 전달하는 함수다. 따라서 함수 리터럴이 더 길어질수록 (...) 대신 {...}를 사용하는 이런 ‘구문적 편의syntactic sugar ’ 가 더 보기 좋다. 우리에게 익숙한 블록 구조의 구문과 더 비슷해지기 때문이다. 아래와 같이 offset의 기본값을 사용하는 경우라도 첫 인자 목록의 괄호를 여전히 지정해주어 야 한다.
s.draw() { str => println(s"ShapesDrawingActor: $str") }
자바에서처럼 draw 메서드가 한 인자 목록 안에서 두 인자를 받게 만들 수도 있다. 그런 경우 메서드 호출 부분은 다음과 같다.
s.draw(Point(1.0, 2.0), str => println(s"ShapesDrawingActor: $str") )
이 코드는 그렇게 깔끔하지도 우아하지도 않다. 또한 offset에 기본값을 사용하더라도 그리 편 하지 않다. 이런 경우 함수 인자에 이름을 지정해야 한다.
s.draw(f = str => println(s"ShapesDrawingActor: $str"))
블록 구조 구문을 사용할 수 있다는 첫 번째 이점에 이어,두 번째 이점은 어떤 인자 목록의 다음 에 위치한 인자 목록에 관해 타입 추론이 가능해진다는 것이다. 다음 예를 보자.
scala> def m1[A](a: A, f: A => String) = f(a) m1: [A](a: A, f: A => String)String
2장 입력은 조금만, 일은 더 많이
93
scala> def m2[A](a: A)(f: A => String) = f(a) m2: [A](a: A)(f: A => String)String scala> m1(100, i => s"$i + $i") error: missing parameter type m1(100, i => s"$i + $i") ^
<console>:12:
scala> m2(100)(i => s"$i + $i") res0: String = 100 + 100
함수 m1과 m2는 거의 동일하다. 하지만 각각에 동일한 Int와 Int => String 함수 인자를 전달 하며 호출하는 경우 어떤 일이 벌어지는지 살펴보라. m1의 경우 스칼라는 함수 인자 i의 타입을 추론하지 못한다. 반면 m2에 관해서는 추론할 수 있다. 인자 목록을 여럿 만드는 방식의 세 번째 장점은 마지막 인자 목록을 암시적 인자implicit argument 를 위해 사용할 수 있다는 것이다. 암시적 인자는 implicit 키워드를 사용해서 선언한 인자다. 메서드를 호출할 때, 이런 인자에 명시적으로 인자를 지정하거나, 컴파일러가 영역 안에서 적절 한 값을 찾아서 채워 넣도록 할 수 있다. 암시적 인자는 기본값이 있는 인자보다 더 유연하다. 암 시적 인자를 활용하는 스칼라 라이브러리의 예로 Future를 살펴보자.
2.5.3 Future 맛보기 scala.concurrent.Future ( http://bit.ly/1xIkpsr ) API는 암시적 인자를 활용해서 코드에
필요한 준비 코드를 줄인다. Future API는 스칼라가 제공하는 또 다른 동시성 도구다. 아카가 Future를 사용하지만, 액터의 모든 기능이 필요하지 않은 경우에는 Future만 별도로 사용할 수
있다. 수행할 작업 중 일부를 Future로 감싸면 그 작업을 비동기적으로 수행하며, Future API는 결 과가 준비된 경우 콜백callback 을 호출해주는 등 결과를 처리할 수 있는 다양한 방법을 제공한다. 여기서는 콜백을 사용하고, API의 자세한 설명은 17장에서 진행할 것이다. 다음 예제는 다섯 가지 작업을 동시에 시작하고, 완료 시 결과를 처리한다.
94
1부 스칼라와의 만남
CHAPTER
3
기초를 튼튼히
스칼라 ‘기초’ 핵심 정리를 마저 끝내자.
3.1 연산자 오버로딩? 대부분의 ‘연산자’는 실제로는 메서드다. 가장 기본적인 다음 예를 보자.
1+2
두 수 사이의 덧셈 기호가 보이는가? 그것은 메서드다. 자바에서 특별한 ‘기본primitive ’ 타입은 실제로 스칼라에서는 일반적인 객체다. 이는 Float, Double, Int, Long, Short, Byte, Char, Boolean 타입에도 메서드가 있다는 의미다.
따라서 1 + 2는 1.+(2 )와 같다. 인자가 하나뿐인 메서드의 경우 중위infix 표기법에서 마침표와 괄호를 생략할 수 있다.1
1 주_ 실제로는 연산자 우선순위로 인해 이 둘이 항상 동일하게 작동하지는 않는다. 1 + 2 * 3 = 7이지만, 1.+(2)*3 = 9다. 마침표는 별표(*)보다 먼저 묶인다. 또한 스칼라 2 .11 이전 버전을 사용하는 경우 1 다음에 공백을 넣어야 한다. 그렇게 하지 않으면 1.이 항상 Double로 해석된다.
135
지금까지 본 것처럼, 스칼라 식별자에는 숫자나 영문이 아닌 문자도 들어갈 수 있다. 사용할 수 없는 문자에 대해서는 잠시 후에 살펴볼 것이다. 마찬가지로 인자가 없는 메서드는 항상 마침표 없이 호출할 수 있다. 이를 후위postfix 표기법이 라고 한다. 하지만 이런 후위 표기법을 사용하면 혼란을 일으킬 수 있으므로 스칼라 2.10은 이 를 선택적인 기능으로 만들었다. 그런 기능을 사용하고 싶다고 컴파일러에 명시하지 않고 이를 사용하면 빌드 시 경고를 표시하도록 SBT를 설정했다. 명시적으로 사용 의사를 표시하려면 임 포트 문을 사용한다. 다음의 scala 명령을 통한 REPL의 동작을 살펴보라(SBT console과 비 교해보라).
$ scala ... scala> 1 toString warning: there were 1 feature warning(s); re-run with -feature for details res0: String = 1
이는 그리 도움이 되지 않는다. REPL을 재시작하면서 -feature를 지정해서 경고 시 더 많은 정 보를 표시하도록 하자.
$ scala -feature ... scala> 1.toString // 일반적인 호출 res0: String = 1 scala> 1 toString // 후위 표기 호출 <console>:8: warning: postfix operator toString should be enabled by making the implicit value scala.language.postfixOps visible. This can ... adding the import clause 'import scala.language.postfixOps' or by setting the compiler option -language:postfixOps. See the Scala docs for value scala.language.postfixOps for a discussion why the feature should be explicitly enabled. 1 toString ^ res1: String = 1 scala> import scala.language.postfixOps import scala.language.postfixOps
136 1부 스칼라와의 만남
scala> 1 toString res2: String = 1
필자는 이런 긴 경고를 항상 켜놓는 편을 선호한다. 그래서 SBT 프로젝트도 -feature를 사용 하도록 설정했다. 따라서 SBT에서 console을 사용해서 REPL을 실행하면 이 컴파일러 설정이 항상 켜져 있는 상태가 된다. 이 경고를 해결하는 두 가지 방법이 있다. 하나는 이미 앞에서 보여준 대로 import scala. language.postfixOps를 사용하는 것이다. 또 하나는 -language:postfixOps를 지정해서 컴
파일러가 이 기능을 항상 활성화하도록 만드는 것이다. 필자는 경우에 따라 대응할 수 있는 import 문 쪽을 더 선호한다. 임포트를 사용하면 코드를 읽는 사람에게 내가 사용 중인 선택적
인 기능을 상기시키는 효과도 있다(다른 선택적 기능들은 21.1.1절 ‘scalac 명령행 도구’에서 나열할 것이다). 컴파일 시 모호성이 생기지 않고 사람들을 혼동시키지 않는 경우 이런 식으로 부호를 생략하면 코드를 더 깔끔하게 작성할 수 있고, 더 자연스럽게 읽을 수 있는 우아한 프로그램을 만들 수 있다. 그렇다면 식별자identifier 에서 사용할 수 있는 문자는 어떤 것이 있을까? 다음은 식별자 규칙을 요약한 것이다. 식별자는 메서드나 타입 이름, 변수 이름 등에 쓰인다. 문자
스칼라는 모든 출력 가능한 아스키 문자를 허용한다. 알파벳, 숫자, 밑줄(_ ), 달러 기호($ ) 를 사용할 수 있다. 괄호parenthetical 문자인 (, ), [, ], {, }와 구분자delimiter 문자인 `, ’, ', ", ., ;과 ,는 제외된다. 스칼라는 \\u0020과 \\u007F 사이에서 방금 설명한 문자들을 제외한
나머지 문자를 사용하도록 허용한다. 예를 들어 수학 기호를 비롯해서 /나 < 등의 연산자 문 자, 그 외의 기호 등을 사용할 수 있다.2
2 역주_ 스칼라에서 사용할 수 있는 글자는 공백(\u0020, \u0009, \u000D, \u000A ), 글자(소문자( lowercase letters, Ll ), 대문자 (upper case letters, Lu ), 제목케이스 문자( titlecase letters , Lt ), 기타 문자( other letters, Lo )), 숫자( 0~9 ), 괄호((, ), [, |, ], {, }), 구분자 문자( `, ’, ', ", ., ;, ,), 연산자 문자(\\u0020~\\u007f 사이의 모든 출력 가능한 문자, 유니코드 수학 기호(mathematical symbols, Sm), 유니코드 기타 기호(other symbols, So )로 나뉜다. 여기서 기타 문자(Lo )에는 한글이나 한자 등 동아시아 문자도 물 론 들어 있다. 스칼라 컴파일러는 실제로는 java.lang.Character.isUnicodeIdentifierStart 메서드가 참을 반환하는 문자를 식별자의 시작으로 삼는다(참고로 스칼라 소스 코드는 https://github.com/scala/scala에 있고, 2.11.x 브랜치 컴파일러 소스 코드는 그 저장 소의 https://goo.gl/wtPGPi에, 구문분석 시 사용하는 스캐너는 https://goo.gl/8Qj7xY에 있다).
3장 기초를 튼튼히 137
예약어는 사용 불가
다른 대부분의 언어와 마찬가지로 예약어는 식별자로 사용할 수 없다. 예약어에 대해서는 2.7 절 ‘예약어’에서 살펴봤다. 그중 일부는 연산자나 기호 문자의 조합이었다는 것을 기억하라. 예를 들면 하나의 밑줄( _ )도 예약어다! 일반 식별자 - 글자, 숫자, $, _ , 연산자의 조합
일반 식별자는 문자나 밑줄로 시작하고, 그 뒤에 여러 글자나 숫자, 밑줄, 달러 기호가 올 수 있다. 유니코드로 동등한 문자도 사용할 수 있다. 스칼라는 달러 기호를 내부적으로 사용하려 고 예약해두었다. 컴파일러가 강제로 쓰지 못하게 막지는 않지만, 여러분 코드에서는 달러 기호를 사용하지 말아야 한다. 밑줄 다음에는 글자나 숫자 또는 연산자 문자가 하나 이상 올 수 있다. 밑줄은 중요하다. 밑줄은 공백을 만날 때까지 모든 문자를 식별자의 일부분으로 사 용하라고 컴파일러에 알려준다. 예를 들어 val xyz_++= = 1은 xyz_++= 변수에 1이란 값을 대입하는 반면, val xyz++= = 1은 컴파일에 실패한다. 왜냐하면 ‘식별자’ 부분이 xyz 뒤에 무언가를 덧붙이는 xyz ++=로 해석될 수 있기 때문이다. 마찬가지로 밑줄 다음에 연산자 문 자를 넣어야 한다면 이를 글자나 숫자와 섞어서 사용하면 안 된다. 이런 제약은 abc_-123과 같은 식으로 인한 모호성을 피하기 위한 것이다. 이 식을 abc_-123이라는 식별자로 보아야 할까, 아니면 abc_에서 123을 빼는 것으로 보아야 할까? 일반식별자 - 연산자
어떤 식별자가 연산자 문자로 시작한다면, 그 식별자의 모든 나머지 문자도 연산자 문자여 야 한다. ‘역작은따옴표’ 리터럴
예를 들어 def `test that addition works` = assert (1 + 1 == 2 )와 같이 두 개의 역작 은따옴표back quote (백틱back tick 이라고도 함) 사이에 위치한 임의의 문자열을 식별자로 쓸 수 있다(어떤 경우에 쓸모가 있을지 의심스러운 이 기법은 테스트 이름을 길게 붙일 때 유용하 다). 또한 java.net.Proxy.`type`( )과 같이 자바 클래스의 메서드나 변수가 스칼라 예약 어와 같은 경우에도 이 기법으로 그 메서드나 변수를 사용할 수 있음을 이미 확인했다. 패턴 매칭 식별자
패턴 매칭 식(1.4절 ‘동시성 맛보기’의 액터 예제를 기억하자)에서 소문자로 시작하는 토큰
138 1부 스칼라와의 만남
은 변수 식별자로 취급하며, (클래스 이름 등) 대문자로 시작하는 토큰은 상수 식별자로 취 급한다. 이는 현재 패턴 매칭 case 식의 아주 간결한 문법(예를 들면 val 키워드를 패턴에 사 용하지 않는다)으로 인해 생기는 모호성을 해결하기 위한 규칙이다.
3.1.1 편의 구문 모든 연산자가 메서드라는 사실을 알고 나면, 낯설어 보이는 스칼라 코드를 이해하기가 쉬워진 다. 새 연산자를 볼 때마다 특별한 경우가 아닌지 고민할 필요가 없다. 1.4절 ‘동시성 맛보기’의 액터는 비동기 메시지를 느낌표(!)를 사용해서 서로에게 보낸다. 하지만 이것도 일반적인 메서 드일 뿐이다. 이렇게 메서드 이름을 유연하게 부여할 수 있으므로, 스칼라 자체를 확장한 것처럼 자연스럽게 느껴지는 라이브러리를 만들 수 있다. 여러분은 모든 일반적인 수학 연산자를 사용할 수 있는 새로운 수 타입numeric type 을 제공하는 수학 라이브러리를 만들 수 있다. 액터와 똑같이 작동하 는 새로운 동시성 메시지 계층을 작성할 수도 있다. 단지 몇 가지 메서드 이름에 대한 제약 사항 만 그 가능성을 제한할 뿐이다.
CAUTION_ 여러분이 연산자 기호를 만들 수 있다고 해서 꼭 그걸 만들어야 한다는 의미는 아니다. 이상한 기호로 만들어진 연산자는 사용자들이 읽고 배우고 기억하기 어렵다는 사실을 염두에 두어야 한다. 기호 연산 자를 과도하게 사용하면 코드에서 ‘행 내부 잡음line noise ’이 늘어나 가독성이 떨어진다. 따라서 연산자의 기존 관례를 지키되, 어떤 연산자를 사용할지 분명하지 않은 경우에는 가급적 더 읽기 쉬운 메서드 이름으로 하라.
3.2 빈 인자 목록이 있는 메서드 스칼라는 중위 및 후위 호출이 가능할 뿐 아니라, 인자가 없는 메서드에 대해 괄호를 사용하지 않아도 되도록 유연성을 제공한다. 메서드가 아무 매개변수도 취하지 않는다면 괄호 없이 정의할 수 있다. 호출하는 쪽에서는 그런 메서드를 호출할 때 괄호를 사용하지 말아야 한다. 반대로, 정의 시 메서드에 빈 괄호가 있었다 면, 호출 시 괄호를 넣거나 생략하는 방식 중 하나를 선택할 수 있다.
3장 기초를 튼튼히 139
예를 들어 List.size에는 괄호가 없다. 따라서 List (1, 2, 3 ).size라고 써야 한다. 만약 List (1, 2, 3 ).size ( )를 시도한다면 오류가 발생한다. java.lang.String의 length 메서드는 정의에 괄호가 들어 있다(자바에서는 항상 괄호를 넣
어야 한다). 하지만 스칼라에서는 "hello".length ( )나 "hello".length 모두 사용할 수 있 다. 스칼라에서 정의한 ‘빈 괄호가 들어 있는 메서드’에도 같은 규칙을 적용한다. 정의에 빈 괄호가 있는 경우와 없는 경우의 처리 규칙에 일관성이 없는 이유는 자바와의 상호 운용성 때문이다. 스칼라에서는 정의와 사용이 일치하는 쪽을 더 선호한다. 하지만 빈 괄호가 있는 경우 더 유연하므로, 자바의 인자 없는 메서드를 호출하는 경우와 스칼라의 인자 없는 메 서드를 호출하는 경우를 일관성 있게 처리할 수 있다. 스칼라 커뮤니티의 관례는 컬렉션의 크기 연산과 같이 부수 효과side effects 가 없는 인자 없는 메 서드의 괄호를 생략하는 것이다. 메서드에 부수 효과가 있는 경우, 상태 변경이 일어날 수 있 으니 좀 더 주의를 기울이라는 의미에서 보통 괄호를 더하여 작게나마 ‘경계 신호’를 보낸다. scala나 scalac를 실행하면서 -Xlint를 사용하면 I/O 등 부수 효과를 실행하는 메서드의 괄
호를 생략할 때마다 컴파일러가 경고를 표시한다. 이 책의 SBT 설정에는 이와 같은 플래그를 이미 반영했다. 무엇보다 괄호가 선택인지 여부에 대해 왜 그렇게 신경을 써야 할까? 괄호를 생략할 수 있으면 메서드 연쇄 호출 코드를 더 읽기 쉬운 ‘문장’처럼 보이게 할 수 있다.
// src/main/scala/progscala2/rounding/no-dot-better.sc def isEven(n: Int) = (n % 2) == 0 List(1, 2, 3, 4) filter isEven foreach println
이 코드는 다음과 같이 출력된다.
2 4
140 1부 스칼라와의 만남
아직 이 문법에 익숙하지 않다면, 문장 자체는 꽤 ‘분명해’ 보이는데도 대체 어떤 일이 벌어진 것인지 이해하기 어려울 수 있다. 다음은 마지막 줄을 점차 단순화한 과정을 보여준다. 가장 마 지막 줄은 원래 문장과 같다.
List(1, List(1, List(1, List(1,
2, 2, 2, 2,
3, 3, 3, 3,
4).filter((i: Int) => isEven(i)).foreach((i: Int) => println(i)) 4).filter(i => isEven(i)).foreach(i => println(i)) 4).filter(isEven).foreach(println) 4) filter isEven foreach println
첫 세 줄이 더 명시적인 만큼 초보자가 이해하기는 더 쉽다. 하지만 컬렉션의 filter가 인자를 하나만 받고, foreach는 컬렉션에 대해 암시적 루프 역할을 한다는 등의 내용을 기억한다면, 맨 마지막의 ‘간결한’ 구현을 훨씬 더 빠르게 읽고 이해할 수 있다. 다른 두 버전에는 여러분의 경 험이 축적됨에 따라 점점 더 방해물처럼 느껴지게 될 시각적 잡음이 들어 있다. 스칼라 코드를 읽는 법을 배워가면서 이를 염두에 두라. 이 식은 모든 메서드가 인자를 하나씩만 받으므로 잘 작동한다. 만약 이 연쇄 호출의 중간에 인 자가 없거나 인자를 하나 이상 취하는 메서드를 사용하면, 컴파일러를 교란시킬 수 있다. 그런 경우에는 기호 중 일부 또는 전부를 원래대로 돌려놔야 한다.
3.3 우선순위 규칙 2.0 * 4.0 / 3.0 * 5.0이 실제로는 여러 Double에 대한 메서드 호출의 연속이라면, 연산자 우
선순위는 어떻게 될까? 다음은 가장 낮은 우선순위부터 가장 높은 우선순위까지 이들을 나열한 것이다.
1. 모든 글자 2. | 3. ^ 4. & 5. <
>
3장 기초를 튼튼히 141
6. = ! 7. : 8. + 9. * / % 10. 다른 모든 특수 문자 같은 줄에 있는 문자는 같은 우선순위를 가진다. 예외는 대입에 사용하는 =이다. =을 대입에 사용하는 경우 가장 낮은 우선순위가 된다. 한편 *와 /의 우선순위는 같으므로 다음 두 줄은 똑같이 작동한다.
scala> 2.0 * 4.0 / 3.0 * 5.0 res0: Double = 13.333333333333332 scala> (((2.0 * 4.0) / 3.0) * 5.0) res1: Double = 13.333333333333332
왼쪽으로 결합하는left-associative 메서드를 연속적으로 호출하는 경우, 이들은 왼쪽에서 오른쪽 순으로 묶인다. 다만 모든 메서드가 ‘왼쪽 결합’인 것은 아니다. 스칼라에서는 모든 메서드가 왼 쪽으로 묶이지만, 이름이 콜론( : )으로 끝나는 메서드는 항상 오른쪽으로 묶인다. 예를 들어 List의 :: 메서드를 사용해서 원소를 맨 앞에 넣을 수 있다. 이를 ‘생성하다, 만들다’라는 뜻을
지닌 영어 ‘constructor’를 줄여 ‘콘즈cons ’라고 한다. 이 말은 리스프Lisp 에서 유래했다.
scala> val list = List('b', 'c', 'd') list: List[Char] = List(b, c, d) scala> 'a' :: list res4: List[Char] = List(a, b, c, d)
여기서 두 번째 식은 list.::('a')와 같다. TIP
메서드 이름이 :로 끝나면 오른쪽으로 묶이지 왼쪽으로 묶이지 않는다.
142 1부 스칼라와의 만남
3.4 도메인 특화 언어 도메인 특화 언어domain specific language, 즉 DSL은 특정 문제 영역을 위해 만들어진 언어로, 해 당 영역의 개념을 간결하고 직관적으로 표현할 수 있게 돕는 것이 목적이다. 예를 들어 SQL도
DSL로 간주할 수 있다. SQL은 관계형 모델Relational Model 의 해석을 표현하기 위한 프로그래밍 언어이기 때문이다. 하지만 DSL이라는 용어는 보통 호스트 언어에 내장embedded 하거나, 전용 구문분석기를 사용해 서 임의로 만든 언어에만 국한해서 사용하고는 한다. 여기서 내장이라는 용어는 DSL을 표현 하기 위해 호스트 언어에서 미리 정해진 관용구적 관습을 사용해서 코드를 작성한다는 뜻이다. 내장 DSL을 전용 구문분석기를 활용하는 외부 DSL과 구분해 내부 DSL이라고도 부른다. 내부 DSL을 사용하면 개발자가 호스트 언어의 기능을 전적으로 활용해서 DSL이 잘 처리하지 못하는 기능도 처리할 수 있다(이를 부정적인 관점에서 본다면, DSL에 ‘빠진 추상화’가 존재할 수 있다는 의미다). 또한 내부 DSL를 사용하면 새로운 언어를 위한 어휘분석기, 구문분석기, 그리고 여러 도구를 작성하는 수고를 덜 수 있다. 스칼라는 두 유형의 DSL을 모두 훌륭히 지원한다. 스칼라의 연산자 등에 대한 유연한 식별자 규칙, 메서드 호출 시 중위와 후위 호출을 지원하는 점 등은 내장 DSL을 일반 스칼라 구문을 사용해서 작성할 수 있는 건축 블록을 제공한다. 스칼라테스트ScalaTest ( http://www.scalatest.org/) 라이브러리를 사용해서 행동 주도 개발 Behavior-Driven Development
(BDD ) 방식의 테스트를 작성하는 예를 살펴보자. 스펙2 Specs2 (http://
bit.ly/1tpceR3 ) 라이브러리도 이와 유사하다.
// src/main/scala/progscala2/rounding/scalatest.scX // 스칼라테스트 예제. 단독 실행하지 마시오. import org.scalatest.{ FunSpec, ShouldMatchers } class NerdFinderSpec extends FunSpec with ShouldMatchers { describe ("nerd finder") { it ("identify nerds from a List") { val actors = List("Rick Moranis", "James Dean", "Woody Allen") val finder = new NerdFinder(actors)
3장 기초를 튼튼히 143
finder.findNerds shouldEqual List("Rick Moranis", "Woody Allen") } } }
이 코드는 스칼라를 사용한 DSL의 강력함을 보여주는 맛보기 사례일 뿐이다. 20장에서 더 많 은 예제를 살펴보고, 자신만의 DSL을 작성하는 방법에 대해 배울 것이다.
3.5 스칼라 if 문 표면적으로 스칼라의 if 문은 자바의 if 문과 비슷하다. if의 조건식을 평가해서 그 식이 참이 면 그다음 블록을 평가한다. 참이 아니면 다음번 가지를 검사하거나 실행하는 과정을 반복한다. 간단한 예를 살펴보자.
// src/main/scala/progscala2/rounding/if.sc if (2 + 2 == 5) { println("Hello from 1984.") } else if (2 + 2 == 3) { println("Hello from Remedial Math class?") } else { println("Hello from a non-Orwellian future.") }
스칼라가 자바와 다른 부분은, if 문을 비롯한 대부분의 스칼라 문장이 값을 결과로 돌려주는 식이라는 점이다. 따라서 if 식의 결과값을 다른 변수에 저장할 수 있다.
// src/main/scala/progscala2/rounding/assigned-if.sc val configFile = new java.io.File("somefile.txt") val configFilePath = if (configFile.exists()) { configFile.getAbsolutePath() } else {
144 1부 스칼라와의 만남
configFile.createNewFile() configFile.getAbsolutePath() }
돌려주는 값의 타입은 모든 가지의 최소 상위 바운드least upper bound 다. 이는 각 절(가지)의 모 든 잠재적인 값에 대응하는 가장 가까운 부모 타입이다. 위 예제에서 configFilePath 값은 설 정 파일이 존재하지 않는 경우를 내부에서 처리하고 절대 경로를 돌려주는 if 문의 값이다. 그 후 이 값을 애플리케이션 전체에서 활용할 수 있다. 이 값의 타입은 String일 것이다. if 문이 식이므로, C에서 파생된 언어에 있는 ‘조건 ? 참인_경우_처리 ( ) : 거짓인_경우_처리 ( )’
와 같은 3항 조건식ternary conditional expression 은 중복 기능이므로 지원하지 않는다.
3.6 스칼라 for 내장 또 다른 제어 구조 중 특히 스칼라에서 더 다양한 기능을 제공하는 것은 for 루프다. 스칼라에 서는 이를 for 내장comprehension 또는 for 식expression 이라고 부른다. 실제로 ‘내장comprehension ’이라는 용어는 함수형 프로그래밍에서 온 것이다. 이 용어는 우리가 하나 이상의 컬렉션을 순회하면서, 그 안에서 찾아낸 것을 ‘이해comprehending ’하고, 그로부터 새 로운 무언가를 계산한다는 생각을 표현한다. 이때 보통은 새로운 다른 컬렉션을 만들어낸다.3
3.6.1 for 루프 기본 for 식부터 살펴보자.
// src/main/scala/progscala2/rounding/basic-for.sc val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund", "Scottish Terrier", "Great Dane", "Portuguese Water Dog")
3 역주_ 집합의 조건 제시법을 코드로 표현한 것이라고 생각하면 된다. 컴프리핸션을 그냥 사용하거나, 내장이라는 단어를 주로 사용하지만, 역자에 따라서는 ‘리스트 조건 제시법’ 등의 용어를 사용하기도 한다.
3장 기초를 튼튼히 145
for (breed <- dogBreeds) println(breed)
짐작할 수 있겠지만, 이 코드는 ‘dogBreeds 리스트의 모든 원소에 대해 그 값을 저장할 breed라 는 임시 변수를 만들고, 그 변수를 출력하라’는 뜻이다. 출력은 다음과 같다.
Doberman Yorkshire Terrier Dachshund Scottish Terrier Great Dane Portuguese Water Dog
이 형태는 아무 값도 반환하지 않으며, 부수 효과만 수행할 뿐이다. 이런 유형의 for 내장을 자 바의 for 루프에 빗대어 for 루프라고 부르기도 한다.
3.6.2 제너레이터 식 앞의 예제에서 본 breed <- dogBreeds라는 식을 제너레이터 식generator expression 이라고 부른다. 컬렉션에서 개별 값을 생성해내므로 붙은 이름이다. 왼쪽 화살표 연산자(<- )는 List와 같은 컬렉션을 순회iterate 하기 위해 사용한다. 제너레이터 식에 Range를 사용하면 전통적인 루프와 더 비슷해 보이는 for 루프를 작성할 수 있다.
// src/main/scala/progscala2/rounding/generator.sc for (i
<-
1 to 10) println(i)
3.6.3 가드: 값 걸러내기 값을 좀 더 미세하게 조정하고 싶다면 어떻게 해야 할까? if 식을 추가해서 원소를 걸러내고 원
146 1부 스칼라와의 만남
하는 원소만 남길 수 있다. 이런 식을 가드guard 라고 부른다. 앞의 예제에서 등장한 개 품종 가운 데 테리어Terrier 만 남기고 싶다면 다음과 같이 바꾼다.
// src/main/scala/progscala2/rounding/guard-for.sc val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund", "Scottish Terrier", "Great Dane", "Portuguese Water Dog") for (breed <- dogBreeds if breed.contains("Terrier") ) println(breed)
이제 출력은 다음과 같이 바뀐다.
Yorkshire Terrier Scottish Terrier
가드를 둘 이상 추가할 수도 있다.
// src/main/scala/progscala2/rounding/double-guard-for.sc val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund", "Scottish Terrier", "Great Dane", "Portuguese Water Dog") for (breed <- dogBreeds if breed.contains("Terrier") if !breed.startsWith("Yorkshire") ) println(breed) for (breed <- dogBreeds if breed.contains("Terrier") && !breed.startsWith("Yorkshire") ) println(breed)
두 번째 for 내장은 두 if 문을 하나로 합쳤다. 두 for 문의 출력을 한꺼번에 보여주면 다음과 같다.
3장 기초를 튼튼히 147
Scottish Terrier Scottish Terrier
3.6.4 yield로 값 만들어내기 걸러낸 컬렉션을 출력하지 않고, 결과를 모아서 프로그램의 다른 부분에서 사용하도록 넘겨야 한 다면 어떻게 해야 할까? yield 키워드를 이용해서 for 식에서 새로운 컬렉션을 만들어내면 된다. 또한 괄호 대신 중괄호를 사용할 것이다. 이는 블록 구조가 더 시각적으로 분명해 보이도록 메서 드 인자 목록을 괄호 대신 중괄호로 둘러싸는 것과 비슷하다.
// src/main/scala/progscala2/rounding/yielding-for.sc val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund", "Scottish Terrier", "Great Dane", "Portuguese Water Dog") val filteredBreeds = for { breed <- dogBreeds if breed.contains("Terrier") && !breed.startsWith("Yorkshire") } yield breed
for 식을 매번 반복하면서 걸러낸 결과를 breed라는 이름의 값으로 만들어낸다. 이 결과는
매 루프마다 누적되며, 결과 컬렉션은 filteredBreeds라는 값에 저장된다. 이 for-yield 식의 결과 컬렉션의 타입은 반복을 수행한 대상 컬렉션의 타입으로부터 추론된다. 여기서 filteredBreeds의 타입은 List[String]이다. 원래의 dogBreeds 리스트가 List[String] 타
입이었기 때문이다. TIP
for 내장에 식이 하나만 들어가면 괄호를, 여러 식이 들어가면 중괄호를 사용하는 것이 비공식적인 관례다. 또 한 괄호를 사용할 경우 중간에 세미콜론(;)을 추가해야 한다는 사실도 기억하라.
for 내장이 yield를 사용하지 않는 대신, 화면 출력과 같은 부수 효과를 수행하는 경우 이를 for 루프라고 부른다. 이런 동작이 자바 등의 언어에서 익히 알고 있는 for 루프의 동작과 더 비
슷하기 때문이다.
148 1부 스칼라와의 만남
CHAPTER
4
패턴 매칭
패턴 매칭을 처음 보면 C 언어의 case 문과 비슷해 보인다. 전형적인 C 언어의 case 문에서는 서수 타입ordinal type 의 값에 대해서만 매치시킬 수 있고, 간단한 식에 대해서만 매치가 가능하다. 예를 들어 ‘i가 5인 경우 메시지를 출력하고, i가 6인 경우 프로그램을 종료하라’와 같은 명령 만 가능하다. 스칼라에서 패턴 매칭을 사용하면 타입, 와일드카드wildcard, 시퀀스, 정규 표현식에 대한 판별 은 물론 객체 상태를 깊이 관찰하는 것도 가능하다. 이런 관찰 기능에는 타입을 구현하는 사람 이 내부 상태를 노출하는 방식을 제어할 수 있는 절차가 들어 있다. 또한 그렇게 노출시킨 상태 를 쉽게 변수에 저장할 수 있다. 따라서 이런 기능에 대해 ‘추출extraction ’이나 ‘분해destructuring ’ 등 의 용어를 사용하기도 한다. 패턴 매칭을 코드의 여러 맥락에서 사용할 수 있다. 먼저 가장 일반적인 용례인 match 절을 살 펴볼 것이다. 그 후 다른 사용법에 대해 보여줄 것이다. 1.4절 ‘동시성 맛보기’의 액터 예제에서 좀 더 간단한 match 절 예제를 두 가지 봤다. 또한 2.4절 ‘부분 함수’에서 용례를 조금 더 설명 했다.
4.1 단순 매치 먼저 Boolean 값에 패턴 매치를 사용해서 동전을 뒤집어보자.
177
// src/main/scala/progscala2/patternmatching/match-boolean.sc val bools = Seq(true, false) for (bool <- bools) { bool match { case true => println("Got heads") case false => println("Got tails") } }
C의 case 문처럼 보인다. 실험을 위해 두 번째 case false 부분을 주석으로 가리고 스크립트 를 재실행하자. 다음과 같이 경고와 오류를 보게 된다.
<console>:12:
warning: match may not be exhaustive. It would fail on the following input: false bool match { ^ Got heads scala.MatchError: false (of class java.lang.Boolean) at .<init>(<console>:11) at .<clinit>(<console>) ...
컴파일러는 시퀀스의 타입으로부터 그 원소에 true와 false라는 두가지 경우가 있음을 안다. 따라서 매치가 완전하지 않다고 경고를 표시한다. 또한 일치하는 case 절이 없는 경우 어떤 일이 생기는지도 볼 수 있다. 이 경우 MatchError (http://bit.ly/1udfTVy )가 발생한다. 이러한 경우 if를 사용한 더 간단한 방법이 있다는 것도 언급해야겠다.
for (bool <- bools) { val which = if (bool) "head" else "tails" println("Got " + which) }
178 2부 기본기 다지기
4.2 매치 내의 값, 변수, 타입 몇 종류의 매치를 더 살펴보자. 다음 예제는 특정 값이나 특정 타입의 모든 값과 매치시키는 방 법을 보여주며, 모든 값에 일치하는 ‘기본’ 절을 쓰는 방법 중 하나를 보여준다.
// src/main/scala/progscala2/patternmatching/match-variable.sc for { x <- Seq(1, 2, 2.7, "one", "two", 'four) }{ val str = x match { => "int 1" case 1 => "other int: "+i case i: Int case d: Double => "a double: "+x => "string one" case "one" case s: String => "other string: "+s case unexpected => "unexpected value: " + unexpected } println(str) }
// ➊ // // // // // // //
➋ ➌ ➍ ➎ ➏ ➐ ➑
// ➒
➊ 여러 타입의 값이 섞여 있기 때문에 Seq[Any] 타입의 목록을 사용한다. ➋ x는 Any 타입이다. ➌ x가 1인 경우 일치한다. ➍ 다른 Int 값과 일치한다. 안전하게 x의 값을 Int로 변환하고, i에 대입한다. ➎ 모든 Double 값과 일치한다. 이 경우 x의 값을 Double 변수 d에 대입한다. ➏ 문자열 ‘one’과 일치한다. ➐ 다른 문자열과 일치한다. 이 경우 x의 값을 String 변수 s에 대입한다. ➑ 모든 다른 입력값과 일치한다. 이 경우 unexpected가 x의 값을 대입할 변수 이름이다. 타입 을 표기하지 않았기 때문에 Any 타입을 추론한다. 이 절이 ‘기본’ 절 역할을 한다. ➒ 돌려받은 문자열을 출력한다.
4장 패턴 매칭 179
알아보기 쉽게 => (화살표)를 중심으로 정렬했다. 다음은 출력이다.
int 1 other int: 2 a double 2.7 string one other string: two unexpected value: 'four
다른 모든 식과 마찬가지로 매치식도 값을 반환한다. 여기서는 모든 절이 문자열을 반환한다. 따라서 전체 match 문의 결과 타입도 String이다. 컴파일러는 모든 case 절이 반환하는 값의 가장 가까운 슈퍼타입(다른 말로 최소 상위 바운드least upper bound 라고 한다)을 추론한다. x가 Any 타입이기 때문에, 값이 취할 수 있는 모든 경우를 처리할 수 있는 case 절이 필요하다
(Boolean 값에 대해 매치시켰던 첫 번째 예제와 비교해보라). 그래서 ‘기본’ 절(unexpected가 있는 부분)이 있어야 한다. 하지만 PartialFunction을 만들 때는 가능한 값 중 일부분만 처리 하는 것이 의도이기 때문에, 모든 가능한 값에 대해 매치시킬 필요는 없다. 매치는 앞에서부터 차례대로 처리된다. 따라서 더 구체적인 절이 덜 구체적인 절 앞에 나와야 한다. 그렇지 않으면 더 구체적인 절은 결코 매치를 시도할 대상이 될 수 없다. 따라서 기본 절 은 가장 마지막에 와야 한다. 다행히 컴파일러가 이런 오류를 잡아준다. 부동소수점 리터럴과 매치시키는 것은 좋은 생각이 아니기 때문에, 부동소수점 리터럴을 처리하 는 절을 포함시키지 않았다. 소수에서 가장 작은 자릿수가 다른데도 반올림 오류rounding error 로 두 수가 같아질 수 있기 때문이다. 다음은 앞의 예제를 조금 바꾼 것이다.
// src/main/scala/progscala2/patternmatching/match-variable2.sc for { x <- Seq(1, 2, 2.7, "one", "two", 'four) }{ val str = x match { => "int 1" case 1
180 2부 기본기 다지기
case case case case case
_: Int _: Double "one" _: String _
=> => => => =>
"other int: "+x "a double: "+x "string one" "other string: "+x "unexpected value: " + x
} println(str) }
i, d, s와 unexpected 변수를 위치지정자( _ )로 바꿨다. 문자열을 만들 것이기 때문에, 실제로
는 각각의 타입에 따른 실제값을 만들 필요가 없다. 따라서 모든 경우에 x를 사용할 수 있다. TIP
PartialFunction이 아닌 경우, 매치는 완전해야 한다. 입력이 Any 타입이라면 맨 마지막에 ‘case _’나 ‘case 변수_이름’처럼 기본 절을 추가하라.
case 절을 작성할 때 염두에 두어야 할 몇 가지 규칙과 미묘한 부분이 있다. 컴파일러는 case 다
음에 대문자로 시작하는 이름이 오면 타입 이름으로 간주한다. 소문자로 시작하는 이름은 매치 또는 추출extract 한 값을 담을 변수로 취급한다. 이 규칙은 때로 놀라움을 선사한다. 아래 예를 보자.
// src/main/scala/progscala2/patternmatching/match-surprise.sc def checkY(y: Int) = { for { x <- Seq(99, 100, 101) }{ val str = x match { case y => "found y!" case i: Int => "int: "+i } println(str) } } checkY(100)
4장 패턴 매칭 181
첫 case 절에서 처리할 값을 하드코딩하기보다는 함수의 인자로 받고 싶었다. x가 100인 경우 y 값과 일치하기 때문에(y에는 인자로 받은 100이 들어 있다), 다음과 같은 결과가 나오리라 예상 했을 수도 있다.
int: 99 found y! int: 101
하지만 다음이 실제로 볼 수 있는 출력이다.
<console>:12:
warning: patterns after a variable pattern cannot match (SLS 8.1.1) If you intended to match against parameter y of method checkY, you must use backticks, like: case `y` => case y => "found y!" ^ <console>:13: warning: unreachable code due to variable pattern 'y' on line 12 case i: Int => "int: "+i ^ <console>:13: warning: unreachable code case i: Int > "int: "+i ^ checkY: (y: Int)Unit found y! found y! found y!
case y는 실제로는 ‘(타입 표기가 없기 때문에) 어떤 것이든 일치시켜서 그 값을 y라는 새 변
수에 대입하라’는 뜻이다. 여기서 y는 메서드 인자 y를 가리키는 것으로 해석되지 않는다. 따라 서 실제로 우리는 모든 것을 다 잡아내는 기본 절을 정의한 것이고, 그로 인해 이 ‘변수 패턴’이 모든 것을 잡아내어 두 번째 case 식에 도달할 수 없다는 경고가 발생한다. 그 후 코드에 도달할 수 없다는 오류를 두 번 더 볼 수 있다. ‘SLS 8.1.1’은 스칼라 언어 명세The Scala Language Specification 의 8.1.1절을 가리킨다(http://bit.ly/1wNBOR8 ). 첫 오류 메시지는 우리가 취해야 할 조치, 즉 y에 들어 있는 값에 대해 매치를 수행하고 싶다면 ‘역작은따옴표’를 사용해야 한다는 것을 알려준다.
182 2부 기본기 다지기
// src/main/scala/progscala2/patternmatching/match-surprise-fix.sc def checkY(y: Int) = { for { x <- Seq(99, 100, 101) } { val str = x match { case `y` => "found y!" case i: Int => "int: "+i } println(str) } } checkY(100)
// 바뀐 것은 ‘y’뿐이다.
이제 우리가 원하는 대로 출력이 나온다.
CAUTION_ case 절에서 소문자로 시작하는 이름은 뽑아낸 값을 저장할 새로운 변수의 이름으로 간주된 다. 앞에서 정의한 값을 참조하고 싶다면 역작은따옴표로 둘러싸야 한다. 대문자로 시작하는 이름은 타입 이 름으로 간주된다.
때로 여러 다른 매치를 한 곳에서 다루고 싶은 경우가 있다. 중복을 막고 싶다면 그 부분을 메서 드로 분리해낼 수 있을 것이다. 하지만 case 절에서도 |를 사용해서 ‘논리 합(or )’ 구성 요소를 사용할 수 있다.
// src/main/scala/progscala2/patternmatching/match-variable3.sc for { x <- Seq(1, 2, 2.7, "one", "two", 'four) }{ val str = x match { case _: Int | _: Double => "a number: "+x => "string one" case "one" => "other string: "+x case _: String => "unexpected value: " + x case _ }
4장 패턴 매칭 183
println(str) }
이제 첫 번째 case 절은 Int와 Double 값이 일치한다.
4.3 시퀀스에 일치시키기 Seq ( http://bit.ly/1wQxJyd )는 정해진 순서대로 원소를 순회할 수 있는 List ( http://bit.ly/
1toub3N )나 Vector (http://bit.ly/1tozAI0 ) 등의 모든 구체적인 컬렉션 타입의 부모 타입 이다(Seq는 시퀀스sequence (‘순서가 정해진 열’이라는 뜻)에서 온 말이다). 패턴 매칭과 재귀를 사용해서 Seq를 순회하는 전통적인 관용구를 살펴보면서, 시퀀스에 대해 몇 가지 유용한 기본 사항을 배우자.
// src/main/scala/progscala2/patternmatching/match-seq.sc val val val val val val val val
nonEmptySeq emptySeq nonEmptyList emptyList nonEmptyVector emptyVector nonEmptyMap emptyMap
= = = = = = = =
Seq(1, 2, 3, 4, 5) Seq.empty[Int] List(1, 2, 3, 4, 5) Nil Vector(1, 2, 3, 4, 5) Vector.empty[Int] Map("one" -> 1, "two" -> 2, "three" -> 3) Map.empty[String,Int]
def seqToString[T](seq: Seq[T]): String = seq match { case head +: tail => s"$head +: " + seqToString(tail) case Nil => "Nil" }
// ➊ // ➋ // ➌ // ➍
// ➎ // ➏ // ➐
for (seq <- Seq( // ➑ nonEmptySeq, emptySeq, nonEmptyList, emptyList, nonEmptyVector, emptyVector, nonEmptyMap.toSeq, emptyMap.toSeq)) { println(seqToString(seq)) }
184 2부 기본기 다지기
➊ 비어 있지 않은 Seq[Int] (http://bit.ly/1E8xLCt )를 만든다(실제로는 List (http://bit.
ly/15iqGNE )가 반환된다). 그다음 줄은 비어 있는 Seq[Int]를 만드는 전형적인 방법을 보여 준다. ➋ 비어 있지 않은 List[Int] (Seq의 서브타입)를 만든다. 그다음 줄은 라이브러리에서 모든 타입 매개변수에 대해 비어 있는 List를 표현하는 객체인 Nil을 선언한다. ➌ 비어 있지 않은 Vectors[Int] (http://bit.ly/1bgKyXi )를 만든다(이 타입도 Seq의 서브 타입이다). 그다음 줄은 비어 있는 Vector[Int]를 만든다. ➍ 비어 있지 않은 Map[String,Int] (http://bit.ly/13MzP5e )를 만든다. Map은 Seq의 서브 타입이 아니다. 이에 대해서는 잠시 후에 설명할 것이다. 키key 로 String을 사용하고, 값으로 Int를 사용한다. 그다음 줄은 비어 있는 Map[String,Int]를 만든다.
➎ T라는 타입에 대해 Seq[T]로부터 String을 생성하는 재귀적 메서드를 정의한다. 본문은 입 력으로 들어온 Seq[T]에 대해 매치하는 4줄짜리 식이다. ➏ 매치 절이 둘 있고, 이 둘이 모든 경우를 다 처리한다. 첫 번째 매치는 모든 비어 있지 않은 Seq와 일치하며, 첫 원소인 머리( head )와 꼬리( tail ) ( Seq에서 머리를 제외한 나머지 부분)
를 가져온다(Seq에도 head와 tail이라는 메서드가 있다. 하지만 여기서는 일반적인 case 절 에서 사용하는 변수 이름으로 그 두 단어를 사용했다). case 절의 본문에서는 머리 다음에 +:, 그리고 그다음에 seqToString을 꼬리에 대해 호출한 결과를 넣어서 String을 만들어낸다. ➐ 다른 유일한 경우는 빈 Seq다. 빈 List를 표현하는 특별 객체(Nil )를 사용해서 모든 빈 리스 트와 일치시킬 수 있다. 모든 Seq는 항상 같은 타입의 빈 인스턴스로 끝나는 것처럼 해석된다 는 사실을 기억하라. 하지만 실제로는 List와 같은 몇몇 타입만 실제로 빈 시퀀스의 인스턴스 를 구현한다. ➑ Seq를 다른 Seq 안에 넣는다(Map에 toSeq를 호출하면 키-값 쌍의 시퀀스로 맵을 바꿀 수 있 다). 그 후 그 Seq를 순회하면서 각각의 원소 Seq에 대해 seqToString을 호출한 결과를 출력 한다. 다음은 출력이다.
4장 패턴 매칭 185
1 +: 2 +: 3 +: 4 +: 5 Nil 1 +: 2 +: 3 +: 4 +: 5 Nil 1 +: 2 +: 3 +: 4 +: 5 Nil (one,1) +: (two,2) +: Nil
+: Nil +: Nil +: Nil (three,3) +: Nil
Map은 순회 시 특별한 순서를 보장하지 않기 때문에 Seq의 서브타입이 아니다. 따라서 Map. toSeq를 호출해서 키-값 튜플의 시퀀스를 만들었다. 이렇게 만든 Seq도 여전히 삽입 순서대로
키-값 쌍을 보여준다. 이는 작은 Map의 구현에 따라 생긴 부수적 효과지 늘 보장되는 성질은 아 니다. 여러 빈 컬렉션을 출력한 결과를 보면 seqToString이 잘 작동했음을 알 수 있다. 두 가지 새로운 유형의 case 절이 있다. 먼저 head +: tail은 어떤 Seq의 머리 원소와 꼬리 Seq (나머지 시퀀스)를 일치시킨다. 연산자 +:는 시퀀스의 ‘콘즈’ 연산자다. 이는 3.3절 ‘우선순
위 규칙’에서 본 List의 :: 연산자와 비슷하다. 메서드 이름이 콜론(: )으로 끝나면 오른쪽으로 결합되어 Seq의 꼬리에 대한 호출이 됨을 기억하라. 이들을 ‘연산자’나 ‘메서드’라고 부르고 있지만, 이 문맥에서 그런 용어는 전혀 맞지 않다. 이 식 과 여기서 정말로 어떤 일이 벌어지고 있는지에 대해서는 잠시 후에 다시 설명할 것이다. 지금은 몇 가지 핵심적인 부분을 짚어보자. 첫째, 이 case 절은 오직 비어 있지 않은 시퀀스와만 일치한다. 최소한 머리 원소는 있어야 하 며, 이 패턴은 머리 원소와 시퀀스의 나머지 부분을 각각 head와 tail이라는 이름의 변경 불가 능한 변수에 추출해준다. 둘째, 다시 일러두지만 head와 tail은 임의의 변수 이름이다. 하지만 Seq에는 시퀀스의 머리와 꼬리를 돌려주는 head와 tail 메서드도 있다. 보통은 메서드를 사용하는 중인지 여부를 문맥 에서 명확히 알 수 있다. 한편 빈 시퀀스에 대해 그 두 메서드를 호출하면 예외가 발생한다. Seq가 개념적으로는 연결 리스트처럼 동작한다. 연결 리스트에서 각각의 머리 노드는 원소를
저장하고, 꼬리(시퀀스의 나머지)를 가리킨다. 노드가 4개인 시퀀스를 표현하면 아래와 같은 계층구조를 가진다. 따라서 ‘끝’을 표현하기 위해 가장 자연스러운 표지marker 로 사용할 수 있는 것은 바로 빈 시퀀스다.
186 2부 기본기 다지기
CHAPTER
5
암시
논란의 여지는 있지만, 스칼라의 암시implicit 는 강력한 기능이다. 암시를 사용하면 준비 코드를 줄이고, 기존 타입에 새로운 메서드를 추가한 것과 같은 효과를 낼 수 있고, 도메인 특화 언어 domain specific language
(DSL )의 생성을 지원할 수 있다.
암시가 논란의 대상이 되는 이유는 암시를 찾을 때 소스 코드에서 ‘지역적이 아닌’ 부분까지 고 려하기 때문이다. 여러분은 Predef (http://bit.ly/1086O2z )에서 자동으로 임포트되는 경우 를 제외한 나머지 암시적 값이나 메서드를 지역 범위에 임포트한다. 컴파일러는 범위 안에 들 어온 암시를 호출해서 메서드에 인자를 제공하거나, 인자 중 일부를 원하는 타입으로 변환한 다. 하지만 소스 코드를 읽을 때는 어떤 암시적 값이나 메서드를 활용하고 있는지 분명히 알아 보는 것이 쉽지 않고, 그래서 혼란에 빠질 수 있다. 다행히 경험을 통해 어떤 경우에 암시가 관 련이 있는지 알 수 있고, 암시를 활용하는 API를 배우게 된다. 그럼에도 불구하고 초보자들은 함정에 빠지기 마련이다. 암시가 어떻게 작동하는지 이해하는 것은 상당히 단순하다. 이번 장은 꽤 길지만, 그중 대부분 은 암시가 해결할 수 있는 설계 문제를 다루고 있다.
5.1 암시적 인자 2.5.3절 ‘Future 맛보기’에서 implicit라는 키워드를 이미 살펴봤다. 메서드 인자 중 사용자가
217
명시적으로 제공할 필요가 없는 인자를 표시하기 위해 그 키워드를 사용했다. 암시적 인자를 생 략하는 경우, 그 문장을 둘러싸는 범위에서는 타입이 호환되는 값을 제공해야 한다. 그렇지 않은 경우 컴파일 오류가 발생한다. 세율이 암시적 인자인 거래세를 계산하는 메서드가 있다고 하자.
def calcTax(amount: Float)(implicit rate: Float): Float = amount * rate
이 메서드를 호출하는 코드에서는 지역 범위에 있는 암시적 값을 사용한다.
implicit val currentTaxRate = 0.08F ... val tax = calcTax(50000F) // 4000.0
간단한 경우에는 고정된 Float 값으로 충분할 것이다. 하지만 애플리케이션이 거래가 일어난 장 소를 알아야 할 필요도 있을 것이다. 예를 들면 지방세에 추가하는 경우를 들 수 있다. 일부 자 치 단체에서는 세밑의 쇼핑을 장려하기 위해 ‘세금 휴일’을 제공할 수도 있다. 다행히 암시적 메서드도 사용할 수 있다. 암시적 값과 마찬가지로 작동하기 위해서는 인자가 암 시적이 아니라면 인자 자체를 받아서는 안 된다. 인자가 필요하다면 인자를 암시적으로 만들어 야 한다. 다음은 거래세를 계산하는 완전한 코드다.
// src/main/scala/progscala2/implicits/implicit-args.sc // 실제 통화 계산에는 Float 등의 부동소수점수를 사용하지 말라. def calcTax(amount: Float)(implicit rate: Float): Float = amount * rate object SimpleStateSalesTax { implicit val rate: Float = 0.05F } case class ComplicatedSalesTaxData( baseRate: Float,
218 2부 기본기 다지기
isTaxHoliday: Boolean, storeId: Int) object ComplicatedSalesTax { private def extraTaxRateForStore(id: Int): Float = { // id로부터 위치를 정하고 추가 세금 등을 계산한다. 0.0F } implicit def rate(implicit cstd: ComplicatedSalesTaxData): Float = if (cstd.isTaxHoliday) 0.0F else cstd.baseRate + extraTaxRateForStore(cstd.storeId) } { import SimpleStateSalesTax.rate val amount = 100F println(s"Tax on $amount = ${calcTax(amount)}") } { import ComplicatedSalesTax.rate implicit val myStore = ComplicatedSalesTaxData(0.06F, false, 1010) val amount = 100F println(s"Tax on $amount = ${calcTax(amount)}") }
calcTax를 문자열 인터폴레이션 내에서 호출해도 아무 문제가 없다. 그래도 여전히 rate 인자
에 암시적 값을 사용한다. ‘복잡한’ 경우에는 암시적 메서드를 사용한다. 암시적 메서드는 암시적 인자를 받아서 필요한 데 이터를 처리할 수 있다. 위 스크립트를 실행하면 다음 출력을 얻을 수 있다.
Tax on 100.0 = 5.0 Tax on 100.0 = 6.0
5장 암시 219
5.1.1 implicitly 사용하기 Predef ( http://bit.ly/1086O2z )에는 implicitly라는 메서드 정의가 있다. 이 메서드와 타입
시그니처를 조합하면, 매개변수화한 타입인 암시적 인자를 단 하나만 사용하는 메서드의 시그 니처를 정의할 때 아주 유용하다. List ( http://bit.ly/15iqGNE )의 sortBy 예제를 감싸는 다음 예제를 살펴보자.
// src/main/scala/progscala2/implicits/implicitly-args.sc import math.Ordering case class MyList[A](list: List[A]) { def sortBy1[B](f: A => B)(implicit ord: Ordering[B]): List[A] = list.sortBy(f)(ord) def sortBy2[B : Ordering](f: A => B): List[A] = list.sortBy(f)(implicitly[Ordering[B]]) } val list = MyList(List(1,3,5,2,4)) list sortBy1 (i => -i) list sortBy2 (i => -i)
List.sortBy는 다양한 컬렉션에 존재하는 정렬 메서드 중 하나다. 이 메서드는 ‘인자를 math. Ordering을 만족하는 대상으로 변환하는 함수’를 인자로 받는다. math.Ordering은 자바의 Comparable ( http://bit.ly/1u1hDR8 ) 추상화와 비슷하다. 따라서 B 타입의 인스턴스 순서를
어떻게 지정할지에 대해 아는 암시적 인자가 필요하다. MyList는 sortBy와 비슷한 메서드를 작성하는 두 가지 다른 방법을 보여준다. 첫 번째 구현인 sortBy1에서는 익히 알고 있는 구문을 활용한다. 메서드는 Ordering[B] 타입의 암시적 값을
추가로 받는다. sortBy1을 사용하기 위해서는 범위 안에 원하는 B 타입의 인스턴스 사이의 ‘순 서’를 정하는 방법을 아는 객체가 있어야 한다. 이런 경우 B 타입이 ‘맥락’에 의해 바운드bound 된 다고 말한다. 여기서는 인스턴스의 순서를 정하는 능력이 타입을 바운드한다. 스칼라에서는 이런 관용구가 널리 쓰이기 때문에, 이를 짧게 쓸 수 있는 방법을 제공하는데, 그 게 바로 두 번째 구현인 sortBy2다. 타입 매개변수 B: Ordering을 맥락 바운드context bound 라고
220 2부 기본기 다지기
한다. 이는 Ordering[B]의 인스턴스를 취하는 암시적인 2번째 인자 목록이 있음을 의미한다. 하지만 이 메서드 안에서 Ordering 인스턴스에 접근할 필요가 있다. 그러나 소스 코드에서 명 시적으로 이를 선언하지는 않으므로, 더 이상 그 인스턴스에 이름을 붙일 필요는 없다. 바로 Predef.implicitly가 그런 일을 해준다. 메서드에 암시적으로 넘겨진 인스턴스는 implicitly
에 의해 암시적으로 처리된다. 다만 여기서는 타입 시그니처에 Ordering[B]가 필요함에 유의 하라.
NOTE_ 맥락 바운드와 implicitly 메서드를 결합한 것은 매개변수화한 타입의 암시적 인자가 필요한 특 별한 경우를 짧게 쓸 수 있는 방식이다. 여기서 타입 매개변수는 범위 안에 있는 다른 타입 중 하나여야 한다 (예를 들면 암시적 Ordering[B] 타입 매개변수에 대해 [B : Ordering]).
5.2 암시적 인자를 사용하는 시나리오 암시를 현명하게 사용하고, 자주 사용하지 않는 것이 중요하다. 암시를 과도하게 사용하면 코드 를 읽는 사람이 코드가 실제로 하는 일을 이해하기 어려울 수 있다. 이런 단점이 있음에도 불구하고 왜 암시적 인자를 사용해야 하는 걸까? 암시적 인자를 사용하는 몇 가지 관용구가 있는데, 이들이 주는 이익은 크게 두 가지 범주로 나눌 수 있다. 첫째 범주는 준비를 위한 코드를 없애는 것이다. 예를 들어 맥락 정보를 명시적으로 제공하는 대신 암시적 으로 제공할 수 있다. 둘째 범주는 매개변수화한 타입을 받는 메서드에 사용해서 버그를 줄이 거나 허용되는 타입을 제한하기 위한 제약 사항으로 사용하는 것이다.
5.2.1 실행 맥락 제공하기 2.5.3절 ‘Future 맛보기’의 퓨처 예제에서 Future.apply (http://bit.ly/1pbjSQR ) 메서드의 두 번째 암시적 인자 목록에 암시적 ExecutionContext (http://bit.ly/1s0FtqF )를 넘겼다.
apply[T](body: => T)(implicit executor: ExecutionContext): Future[T]
5장 암시 221
몇몇 다른 메서드도 이런 식으로 암시적 인자를 받는다. 이런 메서드를 호출할 때 우리는 ExecutionContext를 명시하지 않는 대신 컴파일러가 사용할 전역 기본 맥락을 임포트했다.
import scala.concurrent.ExecutionContext.Implicits.global
‘실행 맥락’을 넘길 때는 암시적 인자를 사용하는 것을 권장한다. 이런 맥락의 다른 예로는 실행 원자성을 보장하는 일련의 연산 묶음인 트랜젝션transaction, 데이터베이스 연결, 스레드 풀thread pool
, 사용자 세션user session 등이 있다. 메서드 인자를 사용하면 동작을 조합할 수 있다. 이때 그
인자를 암시적으로 만들면 사용자에게 더 깔끔한 API를 제공할 수 있다.
5.2.2 사용 가능한 기능 제어하기 맥락을 넘기는 것 외에, 암시적 인자를 통해서 사용 가능한 기능을 제어할 수도 있다. 예를 들어 권한 토큰이 들어 있는 암시적 사용자 세션 인자를 사용해서 특정 API 연산을 사용자 가 호출할 수 있는지 판단하거나, 데이터의 가시성을 제한할 수 있다. 사용자 인터페이스의 메뉴를 만드는데, 일부 메뉴는 사용자가 로그인한 경우에만 보여야 하며, 일부는 로그인하지 않은 경우에만 보여야 한다고 가정하자. 다음과 같이 암시적 세션을 활용해 서 이를 검사할 수 있다.
def createMenu(implicit session: Session): Menu = { val defaultItems = List(helpItem, searchItem) val accountItems = if (session.loggedin()) List(viewAccountItem, editAccountItem) else List(loginItem) Menu(defaultItems ++ accountItems) }
222 2부 기본기 다지기
5.2.3 사용 가능한 인스턴스 제한하기 매개변수화한 타입이 있는 메서드가 있고, 그 메서드의 타입 매개변수에 사용할 수 있는 타입 을 제한하고 싶다고 가정하자. 우리가 허용하고 싶은 타입이 특정 공통 슈퍼타입의 모든 서브타입이라면 객체지향적 기법을 사용해서 암시를 피할 수 있다. 그런 기법을 먼저 고려해보자.
3.10절 ‘이름에 의한 호출과 값에 의한 호출’에서 자원 관리자를 만들면서 그런 예를 살펴봤다.
object manage { def apply[R <: { def close():Unit }, T](resource: => R)(f: R => T) = {...} ... }
타입 매개변수 R은 close ( ):Unit 메서드가 있는 어떤 타입의 서브타입이어야 한다. 또는 대 상 자원이 모두 Closable 트레이트를 구현하고 있다고 가정할 수도 있다(트레이트가 자바 인터 페이스를 대신하고 확장한다는 사실을 기억하라. 3.14절 ‘트레이트: 스칼라 인터페이스와 혼합’ 을 보라).
trait Closable { def close(): Unit } ... object manage { def apply[R <: Closable, T](resource: => R)(f: R => T) = {...} ... }
이런 기법은 공통 슈퍼클래스가 없으면 사용할 수 없다. 그런 경우 암시적 인자를 사용해서 허 용되는 타입을 제한할 수 있다. 스칼라 컬렉션 API는 설계 문제를 해결하기 위해 그런 방법을 사용한다. 구체적 컬렉션 클래스가 지원하는 메서드 중 상당수는 부모 타입에서 구현된다. 예를 들어
List[A].map (f: A => B ): List[B]는 각 원소에 대해 f를 적용한 새로운 리스트를 만든다. 대
5장 암시 223
부분의 컬렉션이 map 메서드를 지원한다. 따라서 map을 더 일반적인 트레이트에서 한 번만 구 현하고, 이를 필요로 하는 컬렉션은 그 트레이트를 혼합하는 것이 타당하다. 3.14절 ‘트레이트: 스칼라 인터페이스와 혼합’에서 트레이트를 사용한 ‘혼합’에 대해 이미 살펴봤다). 하지만 맨 처 음 map을 시작할 때 사용했던 컬렉션과 동일한 타입의 컬렉션을 반환하고 싶다. 그렇다면 트레 이트에 단 한 번만 정의한 map 메서드를 사용해서 어떻게 그런 목적을 달성할 수 있을까? 스칼라 API는 map에 암시적 인자로 ‘빌더’를 전달하는 관례를 택했다. 빌더는 같은 타입의 새로 운 컬렉션을 만드는 방법을 알고 있다. 그러므로 ‘순회가능’ 컬렉션 타입에 혼합하는 트레이트인 TraversableLike ( http://bit.ly/1yMk4pK )에 있는 map의 시그니처는 실제로 다음과 같다.
trait TraversableLike[+A, +Repr] extends ... { ... def map[B, That](f: A => B)( implicit bf: CanBuildFrom[Repr, B, That]): That = {...} ... }
+A는 TraversableLike[A]가 A 타입에 대해 공변적임을 의미한다. 즉, B가 A의 서브타입이면 TraversableLike[B]도 TraversableLike[A]의 서브타입이다. CanBuildFrom ( http://bit.ly/1zRqZC9 )은 빌더다. 이름을 이렇게 붙인 이유는 암시적 빌더
객체가 존재하는 한 원하는 새로운 컬렉션을 이를 통해 생성할 수 있음을 강조하기 위한 것이다. Repr은 원소를 저장하기 위해 내부적으로 사용하는 실제 컬렉션 타입이다. B는 함수 f가 만들
어내는 원소 타입이다. That은 우리가 만들고자 하는 대상 컬렉션의 타입 매개변수다. 일반적으로, 비록 타입 매개변
수는 다를지라도, 입력과 같은 종류의 컬렉션을 만들고 싶을 것이다. 즉, B는 A와 같을 수도 있 고 다를 수도 있다. 스칼라 API에는 모든 기본 컬렉션 타입에 대한 CanBuildFrom 정의가 들어 있다. 따라서 map 연산의 결과 컬렉션으로 허용되는 것은, 현재 범위에 암시적(implicit )으로 선언 된 CanBuildFrom에 대응하는 인스턴스에 따라 결정된다. 여러분이 자신만의 컬렉션을 구현한 다면 TraversableLike.map과 같은 메서드 구현을 재사용하길 원할 것이다. 따라서 여러분 자
224 2부 기본기 다지기
신에게 필요한 CanBuildFrom 타입을 만들고, 그 타입의 암시적 인스턴스를 여러분의 컬렉션 을 사용하는 쪽에서 임포트하게 만들어야 한다. 다른 예제를 살펴보자. 여러분이 자바 데이터베이스 API를 위한 스칼라 래퍼wrapper 를 작성한 다고 하자. 다음은 카산드라Cassandra 의 API (http://bit.ly/10aucwc )에서 실마리를 얻어 만든 예제다.
// src/main/scala/progscala2/implicits/java-database-api.scala // 자바와 비슷한 데이터베이스 API. 편의상 스칼라로 만듦. package progscala2.implicits { package database_api { case class InvalidColumnName(name: String) extends RuntimeException(s"Invalid column name $name") trait def def def }
Row { getInt (colName: String): Int getDouble(colName: String): Double getText (colName: String): String
} package javadb { import database_api._ case class JRow(representation: Map[String,Any]) extends Row { private def get(colName: String): Any = representation.getOrElse(colName, throw InvalidColumnName(colName)) = get(colName).asInstanceOf[Int] def getInt (colName: String): Int def getDouble(colName: String): Double = get(colName).asInstanceOf[Double] def getText (colName: String): String = get(colName).asInstanceOf[String] } object JRow { def apply(pairs: (String,Any)*) = new JRow(Map(pairs :_*)) } } }
5장 암시 225
편의상 스칼라로 작성했다. Map을 사용해서 결과 집합result set 의 행row (데이터베이스 레코드)을 표현했다. 하지만 효율성을 위해 실제 구현에서는 바이트 배열을 사용해야 할 것이다. 핵심 기능은 getInt, getDouble, getText라는 메서드며, 필요하다면 다른 것도 구현할 수 있 다. 이런 메서드는 열column (레코드의 필드)의 ‘로우raw ’ 데이터를 적당한 타입의 값으로 변환 한다. 어떤 주어진 열에 대해 잘못된 타입의 메서드를 사용하면 ClassCastException (http://
bit.ly/1toAB2S )이 발생한다. 이를 get[T]라는 메서드 하나로 처리할 수 있다면 더 좋지 않을까? 여기서 T는 허용되는 타입 중 하나일 것이다. 이렇게 하면 좀 더 균일하게 메서드를 호출할 수 있고, 올바른 메서드를 호 출하기 위해 case 문을 쓸 필요도 없으며, 타입 추론을 통해 잘못된 타입으로 열을 사용하는 일 도 막을 수 있을 것이다. 자바에서 기본 타입과 참조 타입의 차별 중 하나는, 기본 타입을 get[T]와 같은 매개변수화한 메서드에 사용할 수 없다는 것이다. 그래서 int 대신 java.lang.Integer 같은 박싱 타입을 사용 해야 한다. 하지만 고성능 데이터 애플리케이션이라면 이런 박싱boxing 에 따른 부가 비용을 원치 않을 것이다! 하지만 스칼라로는 다음과 같이 할 수 있다.
// src/main/scala/progscala2/implicits/scala-database-api.scala // 자바와 비슷한 데이터베이스 API에 대한 스칼라 래퍼 package progscala2.implicits { package scaladb { object implicits { import javadb.JRow implicit class SRow(jrow: JRow) { def get[T](colName: String)(implicit toT: (JRow,String) => T): T = toT(jrow, colName) } implicit (jrow: implicit (jrow: implicit
val jrowToInt: (JRow,String) => Int = JRow, colName: String) => jrow.getInt(colName) val jrowToDouble: (JRow,String) => Double = JRow, colName: String) => jrow.getDouble(colName) val jrowToString: (JRow,String) => String =
226 2부 기본기 다지기
(jrow: JRow, colName: String) => jrow.getText(colName) } object DB { import implicits._ def main(args: Array[String]) = { val row = javadb.JRow("one" -> 1, "two" -> 2.2, "three" -> "THREE!") val oneValue1: Int val twoValue1: Double val threeValue1: String // val fourValue1: Byte
= = = =
row.get("one") row.get("two") row.get("three") row.get("four") // 컴파일 안 됨
println(s"one1 -> $oneValue1") println(s"two1 -> $twoValue1") println(s"three1 -> $threeValue1") = row.get[Int]("one") val oneValue2 = row.get[Double]("two") val twoValue2 val threeValue2 = row.get[String]("three") = row.get[Byte]("four") // 컴파일 안 됨 // val fourValue2 println(s"one2 -> $oneValue2") println(s"two2 -> $twoValue2") println(s"three2 -> $threeValue2") } } } }
implicits 객체 안에서, 자바 JRow를 우리가 원하는 get[T] 메서드가 존재하는 타입으로 감
싸주는 암시 클래스를 정의한다. 이런 클래스를 암시적 변환implicit conversion 이라고 부르는데, 그 에 대해서는 이번 장에서 설명할 것이다. 지금은 이런 암시적 변환을 사용하면 마치 미리 정의해 둔 것처럼 JRow 인스턴스에 대해 get[T]를 호출할 수 있다는 점만 알면 된다. get[T] 메서드는 두 인자 목록을 받는다. 하나는 행에서 가져올 열의 이름이며, 다른 하나는 암
시적 함수 인자다. 이 함수는 행에서 가져온 열의 데이터를 뽑아내고, 그 값을 적절한 타입으로 변환한다.
5장 암시 227
get[T]를 주의 깊게 살펴보면, 그 메서드가 SRow의 생성자에 넘겨진 jrow 인스턴스를 참조한
다는 사실을 알 수 있다. 하지만 그 값은 val로 선언되어 있지 않다. 따라서 그 값은 SRow 클래스 의 필드가 아니다. 그렇다면 어떻게 get[T]가 이를 참조할 수 있을까? 그렇게 할 수 있는 이유 는 단지 jrow가 클래스 본문의 범위 안에 들어 있기 때문이다.
NOTE_ 해당 타입이 고객에게 노출해야 하는 상태 정보를 생성자 인자가 포함하지 않는 경우에는 때로 그 인 자를 (val이나 var를 사용해서) 필드로 선언하지 않는 경우도 있다. 하지만 그 타입의 본문 안에서는 해당 인 자가 여전히 범위 안에 들어 있으므로 해당 타입의 다른 멤버 내부에서는 여전히 그 인자를 참조할 수 있다.
다음으로 JRow와 열 이름을 표현하는 String을 취해서 적당한 타입의 열 값을 반환하는 암시적 값을 세 가지 선언한다. 이런 함수는 get[T] 안에서 암시적으로 사용될 것이다. 마지막으로 시험을 위해 객체 DB를 정의한다. 그 DB는 JRow를 만들고, 세 열에 대해 get[T]를 호 출한다. 두 번 그런 호출을 시도한다. 첫 번째 시도에서는 oneValue1과 같은 변수의 타입으로부 터 T의 타입을 추론한다. 두 번째 시도에서는 변수에 대한 타입 표기를 생략하고, get[T]의 T 매 개변수를 명시적으로 지정한다. 실제로 필자는 두 번째 방식을 더 선호한다. 코드를 실행하기 위해서는 sbt를 시작하고, run-main progscala2.implicits.scaladb.DB를 입력한다. sbt는 필요하면 코드를 컴파일한 다음, 다음과 같은 결과를 보여줄 것이다.
run-main progscala2.implicits.scaladb.DB [info] Running scaladb.DB one1 -> 1 two1 -> 2.2 three1 -> THREE! one2 -> 1 two2 -> 2.2 three2 -> THREE! [success] Total time: 0 s, ... >
소스 코드에서 Byte 값을 뽑아오는 줄을 주석처리 했음에 유의하라. 그 줄의 // 문자를 없애면 컴파일 오류가 발생한다. 다음은 첫 번째 줄에 대한 오류다(책의 폭에 맞춰 일부 줄을 바꿨다).
228 2부 기본기 다지기
CHAPTER
6
스칼라 함수형 프로그래밍
한 종류의 데이터 구조에 쓸 수 있는 함수가 100개 있는 것보다, 10종류의 데이터 구조에 쓸 수 있 는 함수가 10개 있는 게 더 낫다. _앨런 J. 펠리스Alan J. Perlis
일이십 년마다 중요한 전산 관련 아이디어가 주류로 편입되고는 한다. 이런 아이디어는 어쩌면 수십 년간 전산학계나 업계의 한구석에서 천대받아왔던 것일 수도 있다. 주류에 편입되는 과정 은 그 아이디어와 딱 맞는 문제를 사람들이 인식하는 것으로부터 일어난다. 객체지향 프로그 래밍object-oriented programming (OOP )은 1960년대에 생겨났지만, 1980년대에 주류로 들어왔다. 논란의 여지는 있겠지만, 아마도 그래픽 사용자 인터페이스graphical user interface (GUI )의 출현이
OOP가 주류가 된 이유일 것이다. OOP는 GUI에 자연스럽게 들어맞는다. 함수형 프로그래밍functional programming (FP )도 비슷한 과정을 거치고 있다. FP에 대한 연구는 실 제로는 OOP보다 훨씬 더 오래된 것이지만, FP는 요즘 문제가 되는 세 가지 큰 도전을 효과적 으로 처리할 수 있는 기법을 제공한다.
1. 동시성에 대한 필요가 널리 퍼졌고, 그에 따라 애플리케이션을 수평적으로 쉽게 확장하 고, 서비스에 지장이 있는 경우에 대한 회복성을 향상시킬 수 있어야 한다. 따라서 동시 성 프로그래밍은 이제 모든 개발자가 통달해야 할 필수 기술이 되었다.
2. 데이터 중심(예를 들면 빅데이터)의 애플리케이션을 작성할 필요가 많아졌다. 물론 모든 프로그램이 어느 정도 데이터에 관심을 둬야 하지만, 최근 빅데이터의 유행으로 인해 거
269
대한 데이터 집합을 효율적으로 다룰 수 있는 기법의 중요성이 부각되고 있다.
3. 버그가 없는 애플리케이션이 필요하다. 물론 이는 프로그래밍의 탄생과 동시에 생겨난 도전이기는 하다. 하지만 FP는 수학에 기초한 새로운 도구를 제공하며, 우리를 버그가 없 는 프로그램으로 좀 더 가까이 이끌어줄 것이다. 공유된 변경 가능 데이터에 대한 접근을 중재하는 것은 동시성 프로그래밍에서 가장 어려운 문 제다. 불변성은 그런 문제를 없애준다. 따라서 불변성을 사용한 코드를 작성하는 것은 튼튼한 동 시성 프로그램을 작성하기 위한 필수적인 도구며, 불변성을 활용한 코드를 작성하는 가장 좋은 방법은 FP를 사용하는 것이다. 불변성, 그리고 더 일반적으로는 함수형 사고방식의 엄격함은 수학에 뿌리를 두고 있고, 프로그램 오류를 더 적게 만들어낸다. 데이터 중심 애플리케이션에서 FP가 얼마나 좋은지는 이번 장과 다음 장에서 함수형 연산에 통달하고 나면 분명해질 것이다. 빅데이터와 함수형 프로그램의 관계에 대해서는 18장에서 탐 구할 것이다. 이 책에서 다루는 여러 주제는 프로그램의 버그를 최소화해준다. 설명을 진행하면 서 특히 도움이 되는 부분에 대해 강조할 것이다. 이 책에서는 지금까지 여러분이 OOP의 기초 정도는 알고 있다고 가정했다. 하지만 FP는 덜 알려졌기 때문에 기본 개념을 설명하기 위해 약간의 시간을 할애할 것이다. 17장과 17.3절 ‘액 터를 활용해서 튼튼하고 확장성 있는 동시성 프로그래밍하기’를 보면 FP가 동시성 프로그래밍 에 접근하는 아주 유용한 방법이라는 사실을 확인하게 될 것이다. 그뿐 아니라 FP는 OO 코드도 더 낫게 향상시켜준다. 함수형 프로그래밍에 대한 논의를 시작하면서, 스칼라는 잠시 제쳐놓을 것이다. 스칼라는 혼합 패러다임 언어이기 때문에 함수형 프로그래밍의 규칙을 따르도록 요구하지는 않는다. 하지만 가 능하면 그런 규칙을 따를 것을 권고한다.
6.1 함수형 프로그래밍이란 무엇인가? 모든 언어에는 종류가 어떻든 함수가 있기 마련이다. 그것을 메서드라고 부르건, 혹은 프로시 저procedure 나 GOTO라고 부르건 간에, 프로그래머들은 항상 함수를 작성한다.
270 2부 기본기 다지기
함수형 프로그래밍은 함수와 값의 동작에 대한 수학적 규칙에 기반한다. 이런 시작점은 소프트 웨어에 지대한 영향을 끼치고 있다.
6.1.1 수학 함수 수학에서 함수는 아무런 부수 효과가 없다. 다음과 같은 전통적인 사인 함수를 생각해보자.
y = sin (x ) 얼마나 많은 일을 수행하건, sin (x )는 결과를 반환하고, 그 값은 y에 대입된다. sin (x )의 내부 에서는 다른 전역 상태를 일체 변경하지 않는다. 따라서 이런 함수를 부수 효과가 없다고 하거 나, 순수하다고 말한다. 순수성은 함수를 분석, 테스트, 디버깅하는 과정을 극적으로 단순화시켜준다. 함수를 호출하는 주위 환경을 고려하지 않고 그 모든 것을 수행할 수 있다. 주변 환경과 관계없이 명확하다는 특성은 참조 투명성referential transparency 을 낳는다. 참조 투명성 은 두 가지를 함의한다. 첫째, 여러분은 함수를 아무데서나 호출할 수 있고, 호출한 주변 환경 과 독립적으로 함수가 항상 같은 방식으로 동작하리라 자신할 수 있다. 전역 범위를 변경하는 일이 없기 때문에, 그런 함수를 동시 호출하는 것도 단순하며 항상 신뢰할 수 있다. 따로 복잡 하게 스레드 안전성을 위한 코드를 작성할 필요가 전혀 없다. 둘째, 어떤 식이 두 번 이상 반복될 때 맨 처음 계산해서 얻은 결과값을 가지고 두 번째 이후의 식을 안전하게 치환할 수 있다는 것이다. 예를 들어 sin (pi/2 ) = 1이라는 식을 생각해보자. 컴 파일러나 실행 시점의 가상 기계와 같은 코드 분석기는 sin 함수가 순수 함수인 한 프로그램의 정확성을 해치지 않고 sin (pi/2 )를 1로 바꿀 수 있다. TIP
Unit을 반환하는 함수는 부수 효과만 수행할 수 있다. 그런 함수의 내부에서는 변경 가능 상태를 바꿀 수만 있다. 간단한 예로 println이나 printf를 호출하기만 하는 함수를 들 수 있다. 그런 경우 I/O가 ‘외부 세계’ 를 변경시킨다.
값과 함수를 서로 치환할 수 있으므로 값과 함수 사이에는 자연스러운 일관성이 존재한다. 그렇 다면 값을 함수로 치환하는 대신, 함수를 값으로 취급하는 것은 어떨까?
6장 스칼라 함수형 프로그래밍 271
실제로 함수형 프로그래밍에서는 다른 일반적인 데이터 값과 마찬가지로 함수도 1급 계층값이 다. 함수를 다른 함수로부터 합성할 수 있다(예를 들면 tan (x ) = sin (x ) / cos (x ) ). 함수를 변수에 대입할 수도 있다. 함수를 다른 함수에 인자로 넘길 수도 있고, 어떤 함수가 다른 함수 를 값으로 반환할 수도 있다. 함수가 다른 함수를 인자로 받거나 함수를 반환할 때, 이를 고차 함수higher-order function 라고 부 른다. 수학에서 고차 함수의 예로 미분과 적분을 들 수 있다. 우리는 함수와 같은 식을 미분 ‘연 산’에 넘기고, 그 결과로 원래 함수나 식의 도함수를 돌려받는다. 우리는 이미 컬렉션 타입의 map 메서드와 같은 여러 고차 함수를 살펴봤다. map은 하나의 함수 를 인자로 받아서 컬렉션의 모든 원소에 적용한 새 컬렉션을 반환한다.
6.1.2 값이 바뀌지 않는 변수 ‘변수variable ’라는 말은 함수형 프로그래밍에서는 새로운 의미를 갖는다. 전통적인 OOP를 포함 하는, 순서 중심의 프로그래밍procedural-ordered programming 을 배운 독자라면 변수가 변할 수 있다 mutable
는 개념에 익숙할 것이다. 함수형 프로그래밍에서는 변수의 값이 변하지 않는다immutable.
이는 수학에서 출발해서 생긴 또 다른 결과다. y = sin (x )라는 식에서 x를 선택하면 y는 고정 된다. 이와 마찬가지로 값도 변하지 않는다. 정수 3을 1 증가시킨다고 해도, ‘3이라는 객체를 변경’하는 것은 아니다. 그냥 4를 표현하는 새로운 값을 만들어내는 것뿐이다. 우리는 이미 변 경 불가능한 인스턴스를 ‘값’이라는 다른 용어로 불러왔다. 익숙하지 않은 사람이 불변성을 가지고 작업하려 하면 처음에는 좀 어렵다. 변수의 값을 바꿀 수 없다면, 루프에서 횟수를 세는 변수를 만들 수도 없고, 메서드를 호출하면 상태를 바꾸는 객 체도 사용할 수 없다. 심지어 입력과 출력을 수행할 수도 없다. 입출력도 상태를 변경하기 때문이 다! 변경 불가능한 프로그래밍 요소를 가지고 생각하는 것을 배우려면 약간의 노력이 필요하다. 물론 항상 순수하기만 할 수는 없다. 입출력이 없다면 컴퓨터는 그냥 방을 데우는 기계일 뿐이 다. 실용적인 함수형 프로그래밍은 언제 코드가 상태 변경을 수행하고, 언제 순수성을 유지할 지에 대해 일관된 원칙을 세워서 결정한다.
272 2부 기본기 다지기
입력이나 출력이 순수하지 않은 이유는 무엇일까? sin(x)가 부수 효과가 없는 순수 함수라는 점은 이해하기 쉽다. 하지만 입출력을 부수 효과로
간주해서 순수하지 않은 것으로 여겨야 하는 이유는 뭘까? 그들은 파일의 내용이나 우리가 보 고 있는 화면 등 우리 주변 세계의 상태를 변경한다. 그들은 또한 참조 투명성도 없다. 나는 매 번 readline(Predef(http://bit.ly/11QI0fF)에 있는)을 호출할 때마다 다른 결과를 받는다. println(역시 Predef에 정의된)을 호출하면서 내가 넘기는 인수는 매번 다르더라도 반환값은 Unit으로 항상 같다.
이는 함수형 프로그래밍에 상태가 없다는 뜻은 아니다. 상태가 없다면 함수형 프로그래밍이 전 혀 쓸모가 없을 것이다. 다만 함수형 프로그래밍에서는 상태를 항상 새로운 인스턴스나 새로운 스택 프레임(즉, 함수 호출과 반환값)으로 표현할 뿐이다.
2장의 다음 예를 다시 기억해보자.
// src/main/scala/progscala2/typelessdomore/factorial.sc def factorial(i: Int): Long = { def fact(i: Int, accumulator: Int): Long = { if (i <= 1) accumulator else fact(i - 1, i * accumulator) } fact(i, 1) } (0 to 5) foreach ( i => println(factorial(i)) )
재귀를 사용해서 계승을 계산한다. accumulator에 대한 변경은 스택에 쌓인다. 계산하는 과정 에는 메모리의 한 위치에 있는 값을 변경하지 않는다. 예제의 마지막에서 결과를 출력함으로써 상태를 ‘변경’한다. 모든 함수형 언어는 I/O나 기타 필 요한 다른 변경을 수행하기 위해 순수성에서 벗어날 방법을 제공한다. 스칼라와 같이 더 많은 유연성을 제공하는 혼합 언어에서는 상태 변경을 신중하게 일정한 원리에 기초해서 수행하는 방 법을 배우는 것이 기술이다. 코드의 나머지 부분은 가능한 한 순수성을 유지해야 한다.
6장 스칼라 함수형 프로그래밍 273
불변성은 동시성을 사용해서 규모 확장이 가능한 코드를 작성할 때 크게 도움이 된다. 다중 스 레드 프로그래밍의 어려움 대부분은 공유된 변경 가능 상태에 대한 접근을 동기화하는 데 있 다. 변경 가능성을 제거하면 이런 문제가 사라진다. 함수형 프로그래밍이 동시성 소프트웨어를 작성할 때 더 좋은 방법론이 될 수 있도록 경쟁력 있게 만드는 요소는 참조 투명한 함수와 불변 값의 조합이다. 동시성을 통해 애플리케이션의 규모를 확장해야 할 필요가 늘어남에 따라 함수 형 프로그래밍에 대한 관심이 점차 늘어났다. 동시성은 함수형 프로그래밍에 대한 관심이 늘어 나는 주요 원인 중 하나다. 이런 특성은 다른 방식으로도 프로그램에 이익이 된다. 우리가 지난 60여 년간 컴퓨터 프로그 래밍에 대해 발명해온 것은 모두 복잡성을 다루기 위한 시도였다. 고차 순수 함수를 콤비네이 터combinator 라고 부른다. 이 이름은 그런 함수들이 서로 매우 잘 조합될 수 있기 때문에, 더 크 고 복잡한 프로그램을 구성하기 위한 유연하고 미세한 레고 블록처럼 사용할 수 있어서 붙은 이름이다. 이미 컬렉션 메서드를 연쇄적으로 조합해서 코드를 가능한 한 적게 사용하면서 간단 하지 않은 로직을 구현하는 예제를 본 적이 있다. 순수 함수와 불변성은 버그가 발생하는 빈도를 극적으로 감소시켜준다. 변경 가능한 상태는 대 부분의 치명적 버그의 근원이다. 이런 버그는 제품을 출시하기 전에 테스트를 통해 감지하기 가장 어렵고, 종종 수정하기도 가장 어렵다. 변경 가능 상태는 어떤 모듈에서 상태를 변경하더라도, 다른 모듈에서는 그런 변경이 예기치 못한 것임을 의미한다. 이는 종종 ‘유령 같은 원격 작용’ 현상을 일으킨다. 순수성을 사용하면 객체지향 코드에 필요한 여러 방어적 준비 코드를 제거할 수 있고, 설계를 단순화할 수 있다. 데이터 구조에 대한 접근은 객체 안에 캡슐화하는 것이 일반적이다. 변경 가 능한 데이터 구조를 고객과 무작정 공유할 수는 없다. 대신 접근을 제어하기 위한 특별한 접근 자accessor 메서드를 추가해서 고객들이 우리가 알지 못하는 상태에서 데이터를 변경하지 못하도 록 막는다. 접근자는 코드의 크기를 크게 만들고, 그에 따라 테스트나 유지보수 비용도 증가한 다. 또한 접근자는 API의 크기도 더 크게 만들며, 그에 따라 고객의 학습 부하도 더 늘어난다. 반대로, 변경 불가능한 데이터 구조를 사용하면 이런 문제 중 대부분은 그냥 사라진다. 데이터 소실이나 오염에 대한 걱정 없이 내부 데이터를 공개할 수 있다. 물론 상호 결합을 최소화하고 일관성 있는 추상화를 노출해야 한다는 일반적인 원칙을 여전히 적용해야 한다. 구현 세부 사 항을 감추는 것은 외부에서 보는 API의 크기를 최소화하는 데 있어 여전히 가장 중요하다.
274 2부 기본기 다지기
변경 가능한 상태를 사용할 때보다 불변성을 사용할 때 실행 속도가 실제로 빨라질 수도 있다는 데 불변성의 모순이 있다. 객체의 상태를 변경할 수 없다면, 상태를 변경하고 싶은 경우에는 복 사를 하는 수밖에 없다. 다행히 함수형 데이터 구조functional data structure 는 두 복사본 사이에 변화 가 없는 부분을 공유하는 방식을 사용해서 복사에 드는 부가 비용을 최소화한다. 반면 객체지향 언어에서 사용하는 데이터 구조에서는 효율적인 복사를 지원하지 않는 경우가 자주 있다. 방어적인 고객은 데이터 구조에 저장한 내부 상태를 공유할 필요가 있을 때, 원치 않 는 변경으로 인해 데이터가 오염되는 위험을 막기 위해 전체 변경 가능한 데이터 구조를 복사하 는 비싼 대가를 치러야 한다. 잠재적으로 성능 향상을 얻을 수 있는 다른 방법으로, 스칼라의 Stream (http://bit.ly/1toy0G5 ) 타입과 같이 지연 계산lazy evaluation 을 지원하는 데이터 구조를 사용할 수도 있다. 하스켈 같은 일 부 함수형 언어는 지연 계산이 기본이다. 이는 해답이 필요할 때까지 계산을 미뤄둔다는 의미다.1 스칼라는 기본적으로 미리 계산eager evaluation (엄격한 계산strict evaluation 이라고도 함)을 사용한다. 하지만 지연 계산을 활용하면 불필요한 작업을 피할 수 있다는 장점이 있다. 예를 들어 아주 큰 데이터 스트림을 처리하는 데 실제로는 그 데이터 맨 앞의 아주 작은 일부만 필요하다면 전체 데 이터를 처리하는 것은 낭비에 불과하다. 심지어 언젠가 전체 데이터가 필요한 경우라 할지라도, 지연 계산 전략을 사용하면 모든 데이터 처리가 끝날 때까지 고객이 기다리게 할 필요 없이, 기 초적인 답을 좀 더 빨리 제공할 수도 있다. 그렇다면 스칼라가 지연 계산을 기본적으로 사용하지 않는 이유는 무엇일까? 지연 계산이 덜 효 율적인 경우가 많이 있고, 지연 계산의 성능을 예측하는 것이 더 어렵기 때문이다. 따라서 대부 분의 함수형 언어는 미리 계산을 사용한다. 그 대신 데이터 스트림을 처리해야 하는 경우와 같 이 지연 계산이 더 나은 모델일 때를 대비해서 대부분의 함수형 언어는 지연 데이터 구조도 제공 한다. 이제 스칼라를 통해 함수형 프로그래밍의 실용성을 맛볼 때다. 진행하면서 다른 관점이나 이점 에 대해 설명할 것이다. 여러분이 함수형 프로그래밍의 이점을 정말로 이해할 수 있게 돕기 위 해, 스칼라의 함수형 프로그래밍 지원을 객체지향 지원보다 먼저 살펴볼 것이다. 자바 개발자에 게 있어 가장 저항이 적은 경로는 스칼라를 더 개선된 객체지향 언어처럼 사용해서 ‘더 나은 자
1 주_ 그래서 하스켈 커뮤니티에는 자신들이 최후의 순간까지 성공을 미뤄뒀다는 농담이 있다.
6장 스칼라 함수형 프로그래밍 275
바’로 채택하게 만드는 것이다. 이를 위해 약간 이상하고 악마적인 함수형 기능을 가능한 한 피 해야 한다. 하지만 나는 여러분이 그런 부분의 아름다움과 강력함을 인정하게 되길 바라며, 이 런 기능을 명백히 드러낼 것이다. 이번 장에서는 새로운 스칼라 개발자라면 반드시 알아두어야 할 필수적인 요소를 설명할 것이 다. 함수형 프로그래밍은 넓고 다양한 분야다. 따라서 새로운 개발자에게 덜 필수적인 좀 더 어 려운 주제는 16장에서 다룰 것이다.
6.2 스칼라 함수형 프로그래밍 스칼라는 복합적인 객체-함수형 언어이기 때문에, 함수가 순수하도록 요구하지 않으며, 변수가 불변일 것을 요구하지도 않는다. 하지만 가능하면 그런 방식으로 코드를 작성하도록 장려한다. 우리가 이미 살펴본 것을 빠르게 한 번 정리해보자. 정수의 리스트를 반복하면서 짝수만 골라서 2배한 다음, reduce를 사용해서 그들을 서로 곱하 고 축약하기 위해 몇 가지 고차 함수를 사용했다.
// src/main/scala/progscala2/fp/basics/hofs-example.sc (1 to 10) filter (_ % 2 == 0) map (_ * 2) reduce (_ * _)
결과는 122880이다. _ % 2 == 0, _ * 2, _ * _가 함수 리터럴이라는 사실을 기억하라. 앞의 두 함수는 위치지정자 _에 할당된 인자를 취한다. reduce에 전달된 마지막 함수는 인자를 두 개 취한다. reduce 함수는 여러 원소를 서로 곱하기 위해 사용되었다. 즉, 그 함수는 정수의 컬렉션을 단일
값으로 ‘축약reduce ’한다. reduce에 전달한 함수는 인자를 둘 취하고, 각 인자는 _ 위치지정자에 할당된다. 인자 중 하나는 입력 컬렉션의 현재 원소고, 다른 하나는 ‘누적값’이다. 이 누적값은 최초 호출 시에는 컬렉션의 원소지만, 그다음부터는 직전에 reduce를 호출한 결과로 얻은 값 이다(누적값이 인자의 첫 번째 값일지 두 번째 값일지는 구현에 따라 달라진다). reduce에 전
276 2부 기본기 다지기
달할 함수에는 수행할 축약 연산의 결합 법칙이 성립해야 한다는 요구 조건이 있다. 곱셈이나 덧 셈이 바로 그런 연산이다. 결합 법칙이 필요한 이유는 컬렉션 원소를 어떤 순서로 처리할지 아무 런 보장이 없기 때문이다! 따라서 반복 횟수를 추적하기 위한 var 변수나 축약 중인 값을 갱신할 var 변수를 사용하지 않 으면서도 성공적으로 모든 리스트의 원소에 대해 ‘루프’를 돌 수 있었다.
6.2.1 익명 함수, 람다, 클로저 앞의 예제를 다음과 같이 바꿔보자.
// src/main/scala/progscala2/fp/basics/hofs-closure-example.sc var factor = 2 val multiplier = (i: Int) => i * factor (1 to 10) filter (_ % 2 == 0) map multiplier reduce (_ * _) factor = 3 (1 to 10) filter (_ % 2 == 0) map multiplier reduce (_ * _)
먼저 factor라는 변수를 만들어서 승수로 활용한다. 또한 앞의 예제에 있는 익명 함수 _ * 2를 분리해서 factor를 사용하는 multiplier라는 값에 저장한다. multiplier가 함수라는 사실에 유의하라. 스칼라에서는 함수도 1급 계층값이기 때문에, 함수를 일반적인 값과 마찬가지로 정의 할 수 있다. 하지만 multiplier는 하드코딩된 2라는 값 대신 factor를 참조한다. factor의 값을 변경하면서 같은 컬렉션에 대해 같은 코드를 두 번 실행한다. 첫 실행은 예전과
마찬가지로 122880이라는 값을 얻지만, 두 번째 실행에서는 933120을 얻는다. 비록 multiplier가 변경 불가능한 함수값이었지만, factor가 바뀜에 따라 그 함수의 동작도 바 뀌었다. multiplier에는 두 가지 변수 i와 factor가 들어 있다. i는 함수의 형식 인자formal parameter 다.
즉, 그 값은 multiplier가 호출될 때마다 새로운 값에 결부bind 된다. 하지만 factor는 형식 인 자가 아니며, 자유 변수free variable, 즉 주위를 둘러싼 영역에 있는 변수에 대한 참조다. 따라서
6장 스칼라 함수형 프로그래밍 277
컴파일러는 multiplier 안의 문맥과 multiplier 안에서 지정되지 않은 변수들이 참조하는 외부 문맥을 함께 아우르는(또는 환경에 대해 ‘닫혀 있는’) 클로저closure 를 만든다. 따라서 클로 저는 multiplier 내의 모든 변수를 지정해준다. 이런 이유로 factor가 바뀌면 multiplier의 동작도 바뀐다. multiplier는 factor를 참조하 며, 계산을 수행할 때마다 factor의 현재 값을 가져온다. 어떤 함수에 아무런 외부 참조도 없 다면, 그냥 그 자신만으로도 이미 닫혀 있는 함수다. 이런 경우에는 외부 문맥이 필요 없다. 심지어 Factor가 메서드 등 어떤 영역에 속한 지역 변수였거나, 다른 영역으로 multiplier를 전달하는 경우에도 잘 작동할 것이다. Multiplier를 전달하면, 그 안의 자유 변수 factor도 함 께 전달된다.
// src/main/scala/progscala2/fp/basics/hofs-closure2-example.sc def m1 (multiplier: Int => Int) = { (1 to 10) filter (_ % 2 == 0) map multiplier reduce (_ * _) } def m2: Int => Int = { val factor = 2 val multiplier = (i: Int) => i * factor multiplier } m1(m2)
m2를 호출해서 Int => Int 타입의 함수값을 반환받는다. m2는 내부의 multiplier라는 값을
반환한다. 하지만 m2에는 factor라는 다른 값도 들어 있다. 이 값은 m2에서 반환된 다음에는 영역의 밖에 있다. 그 후 m1을 호출하면서 m2가 반환한 함수값을 넘긴다. 여기서 비록 factor가 m1 내부의 영역 에는 없지만, 출력은 예전과 마찬가지로 122880이다. m2가 반환한 함수는 실제로는 factor에 대한 참조까지 포함하는 클로저다. 개념이 약간 중복되는 자주 사용되는 용어가 몇 가지 있다.
278 2부 기본기 다지기
CHAPTER
7
for 내장
3.6절 ‘스칼라 for 내장’에서 for 내장에 대해 설명했다. 지금은 for 내장이 단지 흔히 볼 수 있 는 for 루프를 더 유연하고 좋게 만든 것으로 보일 것이다. 실제로는 겉보기와 다르게 복잡한 내 용이 숨겨져 있다. 하지만 이런 복잡성에는 여러 설계 문제에 대해 우아한 해법을 제시할 수 있는 간결한 코드를 작성할 수 있다는 이점이 있다. 이번 장에서는 for 내장을 정말로 이해하기 위해 표면 아래로 내려가 볼 것이다. 스칼라에서 for 내장을 어떻게 구현하는지와 여러분 자신의 컨테이너를 for 내장에서 활용할 수 있게 만드는 방법을 살펴볼 것이다. 여러 처리 단계로 이루어진 프로그램을 실행하는 과정에서 오류를 처리하는 방법과 같은 몇 가 지 일반적인 설계 문제를, 다양한 스칼라 컨테이너가 for 내장을 사용해서 어떻게 해결하는지 검토하는 것으로 본 장을 마칠 것이다. 또한 자주 발생하는 관용적인 표현을 함수형으로 처리할 수 있는 잘 알려진 기법을 뽑아낼 것이다.
7.1 돌아보기: for 내장의 기본 요소 for 내장에는 하나 이상의 제너레이터 식이 들어간다. 그리고 선택적으로 가드 식( for 걸러내
기)이나 값 정의가 있을 수 있다. 출력은 새로운 컬렉션을 만들거나, 매 단계마다 출력을 표시하 는 등의 부수 효과 블록을 만들기 위해 ‘산출yield ’될 수 있다. 다음은 이런 기능을 모두 보여주는 예제로, 텍스트 파일에서 빈 줄을 제거하는 프로그램이다. 337
// src/main/scala/progscala2/forcomps/RemoveBlanks.scala package progscala2.forcomps object RemoveBlanks { /** * 지정한 입력 파일에서 빈 줄을 제거한다. */ def apply(path: String, compressWhiteSpace: Boolean = false): Seq[String] = for { line <- scala.io.Source.fromFile(path).getLines.toSeq // ➊ if line.matches("""^\s*$""") == false // ➋ line2 = if (compressWhiteSpace) line replaceAll ("\\s+", " ") // ➌ else line } yield line2 // ➍ /** * 지정한 입력 파일에서 빈 줄을 제거하고 남은 줄들을 표준 출력에 하나씩 보낸다. * @param args 파일 경로 목록이다. 파일 이름의 앞에 ‘-’를 붙이면 파일에서 연속된 여러 공백을 하나로 압축해준다. * */ def main(args: Array[String]) = for { path2 <- args // ➎ (compress, path) = if (path2 startsWith "-") (true, path2.substring(1)) else (false, path2) // ➏ line <- apply(path, compress) } println(line) // ➐ }
➊ scala.io.Source ( http://bit.ly/1tIcX1h )를 사용해서 파일을 열고 각 줄을 읽는다. getLines는 scala.collection.Iterator ( http://bit.ly/1q92kQc )를 반환한다. 우리는 이
를 시퀀스로 바꿔야 한다. Iterator를 for 내장에서 반환할 수 없기 때문에, for 내장의 반환 타 입이 최초의 제너레이터에 의해 결정되었다. ➋ 정규 표현식을 사용해서 빈 줄을 걸러낸다. ➌ 지역변수를 정의한다. 공백 압축이 비활성화된 상태면 줄을 바꾸지 않고, 활성화된 상태면 줄에서 연속된 공백을 한 공백으로 압축한다. ➍ yield를 사용해서 줄을 반환한다. 따라서 for 내장은 Seq[String] (http://bit.ly/1E8xLCt ) 을 만들어내며, 이를 apply가 반환한다. apply가 반환하는 실제 컬렉션에 대해서는 잠시 후에
338 2부 기본기 다지기
다시 살펴볼 것이다. ➎ main 메서드는 for 내장을 사용해서 인자 목록을 처리한다. 각각의 인자는 처리할 파일의 경 로로 취급된다. ➏ 파일 경로가 ‘-’ 문자로 시작하는 경우 공백 압축을 활성화한다. 그렇지 않은 경우에는 빈 줄 만 제거한다. ➐ 처리한 모든 줄을 stdout에 쓴다. 이 파일을 sbt로 컴파일한다. sbt 프롬프트 상에서 소스 파일 자체에 대해 실행해보자. 먼저 ‘-’ 문자를 붙이지 않고 시도해보자. 다음은 출력 중 일부를 보여준다.
run-main progscala2.forcomps.RemoveBlanks \ src/main/scala/progscala2/forcomps/RemoveBlanks.scala [info] Running ...RemoveBlanks src/.../forcomps/RemoveBlanks.scala // src/main/scala/progscala2/forcomps/RemoveBlanks.scala package forcomps object RemoveBlanks { /** * 지정한 입력 파일에서 빈 줄을 제거한다. */ def apply(path: String, compressWhiteSpace: Boolean = false): Seq[String] = ...
>
원래 파일에서 빈 줄이 제거되었다. ‘-’를 붙이고 실행하면 다음과 같은 결과가 나온다.
run-main progscala2.forcomps.RemoveBlanks \ -src/main/scala/progscala2/forcomps/RemoveBlanks.scala [info] Running ...RemoveBlanks -src/.../forcomps/RemoveBlanks.scala // src/main/scala/progscala2/forcomps/RemoveBlanks.scala package forcomps object RemoveBlanks { /** * 지정한 입력 파일에서 빈 줄을 제거한다. */ def apply(path: String, compressWhiteSpace: Boolean = false): Seq[String] = ...
>
7장 for 내장 339
여기서는 여러 연속된 공백이 한 공백으로 압축되었다. 이 애플리케이션을 변경하면 번호를 추가하거나, 출력을 별도의 파일에 기록하거나, 각종 통계 를 계산하는 등 더 많은 선택 사항을 입력받을 수 있게 할 수 있다. 전형적인 유닉스 스타일의 명 령행 인자처럼 args 배열로부터 명령행 옵션의 개별 요소를 받을 수 있게 하려면 어떻게 해야 할 까? apply 메서드가 반환하는 실제 컬렉션으로 돌아가 보자. sbt 콘솔을 시작하면 이를 찾을 수 있다.
console Welcome to Scala version 2.11.2 (Java HotSpot(TM) ...). ... scala> val lines = forcomps.RemoveBlanks.apply( | "src/main/scala/progscala2/forcomps/RemoveBlanks.scala") lines: Seq[String] = Stream( // src/main/scala/progscala2/forcomps/RemoveBlanks.scala, ?) >
scala> lines.head res1: String = // src/main/scala/progscala2/forcomps/RemoveBlanks.scala scala> lines take 5 foreach println // src/main/scala/progscala2/forcomps/RemoveBlanks.scala package forcomps object RemoveBlanks { /** * 지정한 입력 파일에서 빈 줄을 제거한다.
지연 Stream (http://bit.ly/1toy0G5 )이 반환된다. 6.9.1절 ‘꼬리 재귀와 무한 컬렉션에 대한 순회’에서 이를 소개했다. REPL이 lines를 정의한 다음에 줄line 을 출력할 때, Stream.toString 메서드가 스트림의 머리(파일의 주석 부분)를 계산하고, 아직 계산하지 않은 꼬리에 대해서는 물음표를 표시한다. 머리를 요청한 다음 첫 다섯줄을 take로 가져왔다. 그에 따라 그 다섯줄이 강제로 평가된다. 걸 러내지 않은 전체 파일을 담기엔 요구하는 메모리가 너무 커질 수 있는 매우 큰 파일을 처리할 수도 있기 때문에 Stream을 사용하는 것이 적절하다. 불행히도 Stream은 평가한 모든 원소를 기억하기 때문에 커다란 데이터를 모두 읽고 나면 메모리에 전체를 저장하게 된다. (apply와
340 2부 기본기 다지기
main에 있는) 두 for 내장의 각 반복에는 상태가 없다. 따라서 메모리에 한 번에 한 줄 이상을
유지할 필요가 없다. 실제로 scala.collection.Iterator (http://bit.ly/1q92kQc )의 toSeq를 호출하면 scala. collection.TraversableOnce ( http ://bit .ly /1pbs2bK )에 있는 기본 구현은 Stream
(http://bit.ly/1toy0G5 )을 반환한다. 반면 Iterator의 서브클래스인 다른 타입들은 엄격한 컬렉션을 반환할 것이다.
7.2 for 내장: 내부 동작 for 내장 문법은 실제로는 컬렉션 메서드인 foreach, map, flatMap, withFilter를 호출하는
것에 대한 구문상의 편의일 뿐이다. 이런 메서드를 호출하는 다른 방법을 제공하는 이유가 뭘까? 단순하지 않은 연쇄 호출의 경우, 여러 API 함수를 호출하는 것보다 for 내장이 훨씬 읽기 쉽고 쓰기 쉽기 때문이다. withFilter는 우리가 이미 살펴본 filter 메서드처럼 원소를 걸러내는 용도로만 사용된다.
스칼라는 withFilter가 없는 경우 filter를 사용할 것이다(이때 경고를 출력한다). 하지만 filter와 달리 withFilter는 컬렉션을 만들어내지 않는다. 더 나은 효율성을 위해 다른 메서드
의 논리와 자신의 걸러내는 연산을 조합하기 위해 협력하여 컬렉션을 하나 덜 만들어낸다. 특히 withFilter는 그다음에 오는 map, flatMap, foreach, 또는 다른 withFilter 호출에 넘겨질
원소의 정의역domain 을 제한한다. for 내장이 어떻게 메서드 호출을 대신하는지 살펴보기 위해 몇 가지 엄밀하지 않은 비교를 시
도해보자. 그 후 정확한 변환 방법을 자세히 설명할 것이다. 간단한 for 내장을 살펴보자.
// src/main/scala/progscala2/forcomps/for-foreach.sc val states = List("Alabama", "Alaska", "Virginia", "Wyoming") for {
7장 for 내장 341
s <- states } println(s) // // // // //
결과:
Alabama Alaska Virginia Wyoming
states foreach println // 결과는 이전과 같음
출력은 주석에 표시해두었다. (지금부터는 REPL 세션을 덜 자주 보여주고, 코드를 표시할 것 이다. 중요한 결과가 있다면 주석에 그 결과를 표현할 것이다.) for 내장 뒤에 yield가 없는 단일 제너레이터 식은 컬렉션에 대해 foreach를 호출한 것과 같다. yield를 사용하면 어떤 일이 벌어질까?
// src/main/scala/progscala2/forcomps/for-map.sc val states = List("Alabama", "Alaska", "Virginia", "Wyoming") for { s <- states } yield s.toUpperCase // 결과: List(ALABAMA, ALASKA, VIRGINIA, WYOMING) states map (_.toUpperCase) // 결과: List(ALABAMA, ALASKA, VIRGINIA, WYOMING)
제너레이터가 하나만 있고 yield가 있는 식은 map을 호출하는 것과 같다. yield가 새로운 컨 테이너를 만들어내기 위해 사용될 때 첫 제너레이터 식의 타입이 최종 결과 컬렉션을 결정한 다는 것을 유의하라. 이는 대응하는 map 식을 보면 더 이해하기 쉽다. 입력 컬렉션인 List를 Vector로 바꾸면 새로운 Vector가 만들어지는 것을 볼 수 있다.
제너레이터가 둘 이상이면 어떨까?
342 2부 기본기 다지기
CHAPTER
12
스칼라 컬렉션 라이브러리
이번 장에서는 컬렉션 라이브러리의 설계에 대해 이야기함으로써 표준 라이브러리에 관한 주제 설명을 마칠 것이다. 표준 라이브러리 설계에 사용한 여러 기법은 함수와 객체지향적 기능을 조합해서 컬렉션을 만들 때 발생할 수 있는 구체적인 문제를 해결해주며, 다른 문제들도 해결해 준다. 컬렉션 라이브러리는 스칼라 2.8에서 큰 변화가 있었다. 이런 재설계에 대한 자세한 설명은 스 칼라독(http://bit.ly/1bCEnM3 )을 보라.
12.1 제네릭, 변경 가능, 변경 불가능, 동시성, 병렬 컬렉션, 아이고! 스칼라독을 열고 검색 창에 Map을 입력하면 5가지 타입을 볼 수 있다! 다행히 대부분은 실제로 관심 있는 구체적 Map의 일부를 선언하거나 구현하는 트레이트다. 이런 구체적인 타입의 차이 는 몇 가지 설계 문제로 귀결된다. 성능을 위해 변경 가능성이 필요한가(물론 그전에 프로파 일링을 통해 이를 측정해봐야 한다)? 동시 접근이 필요한가? 병렬로 처리해야 하는 연산이 있 나? 일반적인 키 기반의 검색 외에 키를 정렬된 순서로 방문하는 기능이 필요한가? [표 12-1]은 컬렉션과 관계있는 패키지와 그 목적을 보여준다. 이번 절에서는 scala라는 접두 사를 생략할 것이다. 실제로 임포트 시 그 접두사를 쓸 필요가 없기 때문이다.
483
표 12-1 컬렉션 관련 패키지 패키지
설명
collection
하위패키지에 있는 모든 정의를 포함해서 스칼라의 컬렉션 라이브러리를
( http://bit.ly/1rGCORI )
사용하거나 확장하기 위해 필요한 대부분의 기반 트레이트와 객체를 정의 한다. 여러분이 작업하게 될 추상화 중 대부분은 여기 있을 것이다.
collection.concurrent
원자적이고 락이 필요 없는 Map과 TrieMap 클래스를 정의한다.
( http://bit.ly/1pbpoTn )
collection.convert
스칼라 컬렉션을 자바 컬렉션 추상화로 감싸거나, 자바 컬렉션을 스칼라
( http://bit.ly/1wQupTN )
컬렉션 추상화로 감싸기 위한 타입을 정의한다.
collection.generic
특정 변경 가능하거나 변경 불가능한 컬렉션 등을 만들기 위해 재사용 가
( http://bit.ly/1tpnjSl )
능한 컴포넌트를 정의한다.
collection.immutable
변경 불가능한 컬렉션을 정의한다. 여러분은 그런 컬렉션을 가장 자주 사
( http://bit.ly/1u14Caq )
용하게 될 것이다.
collection.mutable
변경 가능한 컬렉션을 정의한다. 구체적 컬렉션 타입 대부분은 변경 가능
( http://bit.ly/1udcgPt )
한 형태와 변경 불가능한 형태를 모두 제공한다. 물론 일부 그렇지 않은 것 도 있다.
collection.parallel
병렬 스레드에서 분산 처리가 가능한 구체적인 변경 가능과 변경 불가능한
( http://bit.ly/1rGCUc6 )
컬렉션에 사용할 재사용 가능한 컴포넌트를 정의한다.
collection.parallel.immutable
변경 불가능한 병렬 컬렉션을 정의한다.
( http://bit.ly/1nWux0D )
collection.parallel.mutable
변경 가능한 병렬 컬렉션을 정의한다.
( http://bit.ly/1yMjg47 )
collection.script
컬렉션 연산을 관찰하기 위해 필요한 도구를 제공하는 패키지다. 사용 중
( http://bit.ly/13p3wKl )
단 안내 처리되었다.
이런 패키지에 정의된 타입 중 대부분은 설명하지 않을 것이다. 하지만 각 패키지의 가장 중요한 측면에 대해서는 논의할 것이다. 사용하지 않게 될 collection.script에 대해서는 더 이상 설 명하지 않을 것이다.
12.1.1 collection 패키지 collection에 정의된 타입은 변경 가능하거나 불가능한 순차 컬렉션 타입, 변경 가능하거나
불가능한 병렬 컬렉션 타입, 그리고 동시성 컬렉션 타입들이 공유할 추상화를 선언하고, 일부 정 의한다. 그 의미는 예를 들어 변경 가능한 타입에서만 찾을 수 있는 파괴적(변경 가능한) 연산 은 이 패키지에서는 찾아볼 수 없다는 뜻이다. 하지만 실행 시점의 실제 인스턴스는 변경 가능한 컬렉션일 수 있고, 그런 경우 스레드 안전성이 문제가 될 수 있음을 염두에 두어야 한다.
484 2부 기본기 다지기
구체적으로 말해 6 .7 .1 절 ‘시퀀스’에서 기본 Predef 로부터 얻을 수 있는 기본 Seq 가 collection.Seq ( http://bit.ly/1voJwOs )인 반면 List, Map, Set 등에 대해 Predef를 통해
얻을 수 있는 것은 collection.immutable에 있는 종류임을 기억하자. Predef가 collection. Seq를 사용하는 이유는 변경 가능한 자바의 배열도 균일하게 시퀀스로 다룰 수 있게 하기 위해
서다(Predef는 실제로 자바 배열을 시퀀스 연산을 구현하는 collection.mutable.ArrayOps (http://bit.ly/1E8xE9X )로 변환하는 암시적 변환을 정의한다). 미래의 스칼라 버전에서는 이를 변경 불가능한 Seq로 바꾸려는 계획이 있다. 불행히도 이는, 적어도 지금은, 메서드가 특별히 지정하지 않은 Seq를 반환하는 경우 변경 가능 한 인스턴스를 반환할 수도 있다는 뜻이다. 마찬가지로 메서드가 Seq를 인자로 취하는 경우에 도 호출하는 쪽에서 변경 가능한 인스턴스를 그 함수에 넘길 수 있다. 더 안전한 immutable.Seq를 기본 시퀀스로 사용하고 싶은 경우, 일반적인 방식은 여러분 패키 지 객체에 다음과 같이 Predef에 있는 Seq 정의를 가리는 새로운 Seq 타입 정의를 추가하는 것 이다.
// src/main/scala/progscala2/collections/safeseq/package.scala package progscala2.collections package object safeseq { type Seq[T] = collection.immutable.Seq[T] }
필요할 때 이 내용을 임포트하라. 다음 REPL 세션에서 Seq의 동작이 어떻게 바뀌는지 살펴보자.
// src/main/scala/progscala2/collections/safeseq/safeseq.sc scala> val mutableSeq1: Seq[Int] = List(1,2,3,4) mutableSeq1: Seq[Int] = List(1, 2, 3, 4) scala> val mutableSeq2: Seq[Int] = Array(1,2,3,4) mutableSeq2: Seq[Int] = WrappedArray(1, 2, 3, 4) scala> import progscala2.collections.safeseq._ import progscala2.collections.safeseq._
12장 스칼라 컬렉션 라이브러리 485
scala> val immutableSeq1: Seq[Int] = List(1,2,3,4) immutableSeq1: safeseq.Seq[Int] = List(1, 2, 3, 4) scala> val immutableSeq2: Seq[Int] = Array(1,2,3,4) error: type mismatch; found : Array[Int] required: safeseq.Seq[Int] (which expands to) scala.collection.immutable.Seq[Int] val immutableSeq2: Seq[Int] = Array(1,2,3,4) ^
<console>:10:
맨 처음의 두 Seq 인스턴스는 Predef가 노출하는 기본 collection.Seq다. 첫 번째는 변경 불 가능한 리스트를 가리키며, 두 번째는 변경 가능한 자바 배열(감싼 것)을 가리킨다. 그다음에 새 Seq의 정의를 임포트했다. 따라서 Predef의 정의가 가려졌다. 이제 리스트의 Seq 타입은 safeseq.Seq 별명이다. 하지만 그 별명을 사용해서 배열을 참조할 수 는 없다. 왜냐하면 immutable.Seq의 별명이 변경 가능한 컬렉션을 가리킬 수는 없기 때문이다. 어느 쪽이든 Seq는 맨 앞에서 몇 개의 원소만 원하거나, 맨 앞에서 맨 뒤까지 순차적으로 순회하 는 경우에 가장 알맞은 구체적 컬렉션에 대한 추상화다.
12.1.2 collection.concurrent 패키지 이 패키지에는 collection.concurrent.Map (http://bit.ly/1pbpB9b ) 트레이트와 그 트레이 트를 구현하는 collection.concurrent.TrieMap (http://bit.ly/1u14SWH ) 클래스, 이렇게 두 가지 타입만 들어 있다. Map은 collection.mutable.Map ( http://bit.ly/1tIa0hb )을 확장하지만, 연산이 원자적이 되
도록 보장한다. 따라서 스레드 안전한 동시 접근이 가능하다. collection.concurrent.Map의 구현 중 하나는 collection.concurrent.TrieMap이라는 해
시 트라이hash-trie 클래스다. 이는 트라이를 해시 배열로 매핑한, 동시성의 락이 없는 구현이다. 그 목적은 확장 가능한 동시적인 삽입과 제거 연산을 메모리 효율적으로 구현하는 것이다.
486 2부 기본기 다지기
12.1.3 collection.convert 패키지 이 패키지에 정의된 타입들은 스칼라 컬렉션을 자바 컬렉션으로 감싸거나, 반대 방향으로 감싸 기 위한 암시적 변환 메서드를 구현하기 위해 사용된다. 이에 대해서는 5.7절 ‘스칼라가 기본 제공하는 암시’에서 다뤘다.
12.1.4 collection.generic 패키지 collection이 모든 컬렉션에 대한 추상을 선언하는 반면, collection.generic은 구체적인
변경 가능, 변경 불가능, 동시성, 병렬, 컬렉션을 구현하기 위한 재사용 가능한 컴포넌트를 제 공한다. 이 패키지의 타입 중 대부분은 오직 컬렉션을 구현하는 사람들만 관심을 가질 만한 것 들이다.
12.1.5 collection.immutable 패키지 여러분이 작업하는 동안 대부분은 immutable 패키지에 정의되어 있는 컬렉션을 사용할 것이다. 여기 있는 컬렉션들은 단일 스레드(병렬과 반대되는 개념)의 연산이다. 이들은 변경할 수 없기 때문에 스레드 안전하다. [표 12-2]는 이 패키지에 있는 타입 중 가장 널리 사용되는 것을 알파 벳순으로 보여준다. 표 12-2 가장 널리 사용되는 불변 컬렉션 컬렉션
설명
BitSet
음수가 아닌 정수로 이루어진 메모리를 효율적으로 사용하는 집합. 각 원소는 가변 길이
( http://bit.ly/1DDgSOb )
의 꽉 찬 64비트 워드 배열의 비트로 표현된다. 가장 큰 원소가 이 집합의 메모리 사용 량을 결정한다.
HashMap
키에 대한 해시 트라이를 사용해서 구현한 맵.
( http://bit.ly/1sRf9yo )
HashSet
해시 트라이로 구현한 집합.
( http://bit.ly/13p3Ats )
List ( http://bit.ly/10FrqQC )
연결 리스트를 위한 트레이트. 머리에 접근하는 데는 O(1)의 복잡도, 내부 원소에 접근 하는 데는 O(n)의 복잡도를 보인다. 동반 객체에는 이 트레이트를 구현하는 서브클래스 의 인스턴스를 생성하는 apply 메서드와 다른 ‘팩토리’ 메서드가 들어 있다.
ListMap
리스트를 사용해서 만들어진 변경 불가능한 맵.
( http://bit.ly/1zmrbsb )
12장 스칼라 컬렉션 라이브러리 487
컬렉션
설명
ListSet
리스트를 사용해서 만들어진 변경 불가능한 집합.
( http://bit.ly/13p3ySx )
Map
모든 키-값 쌍이 변경 불가능한 맵. 임의 원소 접근에 O(1)이 걸린다. 동반 객체에는 이
( http://bit.ly/108duhc )
트레이트를 구현하는 서브클래스의 인스턴스를 생성하는 apply 메서드와 다른 ‘팩토 리’ 메서드가 들어 있다.
Nil
빈 리스트를 나타내는 객체.
( http://bit.ly/1wQuIOs )
NumericRange ( http://bit.ly/1G2y8A8 )
임의의 정수 타입에 대한 일반화된 범위. NumericRange는 Range 클래스를 더 일반 화해서 임의의 정수 타입에 대해 작동하게 만든 것이다. 반드시 범위를 나타낼 타입의
Integral 구현을 제공해야 한다. Queue
변경 불가능한 FIFO First In First Out (선입 선출) 큐.
( http://bit.ly/1nWuSAv )
Seq
변경 불가능한 시퀀스를 위한 트레이트. 동반 객체에는 이 트레이트를 구현하는 서브클
( http://bit.ly/1wO3Ih1 )
래스의 인스턴스를 생성하는 apply 메서드와 다른 ‘팩토리’ 메서드가 들어 있다.
Set
변경 불가능한 집합에 대한 연산을 정의한 트레이트. 동반 객체에는 이 트레이트를 구현
( http://bit.ly/108dA8A )
하는 서브클래스의 인스턴스를 생성하는 apply 메서드와 다른 ‘팩토리’ 메서드가 들어 있다.
SortedMap
원소를 정렬된 순서대로 순회할 수 있는 변경 불가능한 맵을 위한 트레이트. 동반 객체
( http://bit.ly/1DDh0wT )
에는 이 트레이트를 구현하는 서브클래스의 인스턴스를 생성하는 apply 메서드와 다
SortedSet
원소를 정렬한 순서대로 순회할 수 있는 변경 불가능한 집합을 위한 트레이트. 동반 객
( http://bit.ly/1nWuXEr )
체에는 이 트레이트를 구현하는 서브클래스의 인스턴스를 생성하는 apply 메서드와
Stack
변경 불가능한 LIFO Last In First Out (후입선출) 스택.
른 ‘팩토리’ 메서드가 들어 있다.
다른 ‘팩토리’ 메서드가 들어 있다. ( http://bit.ly/1wN9UUA )
Stream
값의 지연 리스트. 따라서 무한일 수도 있는 값의 시퀀스를 지원할 수 있다.
( http://bit.ly/1q8XiTI )
TreeMap
적-흑red-black 트리로 구현한 변경 불가능한 맵. O(log(n)) 연산 복잡도를 제공한다.
( http://bit.ly/1G2ygjh )
TreeSet
적-흑red-black 트리로 구현한 변경 불가능한 집합. O(log(n)) 연산 복잡도를 제공한다.
( http://bit.ly/1tIamED )
Vector
변경 불가능한, 인덱스가 있는 시퀀스의 기본 구현.
( http://bit.ly/1wO3PZV )
BitSet은 음수가 아닌 정수로 이루어진 집합이다. 각 원소는 가변 길이의 꽉 찬 64비트 워드 배
열의 비트로 표현된다. 가장 큰 원소가 이 집합의 메모리 사용량을 결정한다. Vector는 6.11절 ‘복사에 드는 비용은 어떤가?’에서 설명한 것처럼 트리 기반의 영속성 데이터
구조로 되어 있다. 벡터는 성능이 뛰어나다. 분할 상환 복잡도가 O (1 ) 이다. 488 2부 기본기 다지기
CHAPTER
15
스칼라 타입 시스템 II
이번 장에서는 앞 장에서 시작한 타입 시스템에 대한 정리를 계속한다. 여기서 논의할 타입 기능 들은 언젠가는 여러분이 마주치게 될 것이지만, 스칼라를 배운지 얼마 되지 않은 경우에는 급하 게 알 필요 없다. 스칼라 프로젝트를 진행하고, 서드파티 라이브러리를 사용함에 따라서 언젠 가는 여기서 다룬 내용을 마주치게 될 것이다(여기서 다룬 내용을 깊이 알고 싶은 독자는 “스 칼라 언어 명세”(http://bit.ly/1wNBOR8 )를 보라). 그렇더라도 여러분이 이번 장을 대충 훑 어볼 것을 권한다. 예를 들어 이 책의 뒷부분에서 더 고급 예제를 다루면서 경로에 의존하는 타 입path dependent type 을 사용한 것을 몇 가지 보게 될 것이다. 그래도 여기서 그에 대해 ‘깊이’ 이해할 필요는 없다.
15.1 경로에 의존하는 타입 스칼라는 그보다 앞선 자바와 마찬가지로 타입의 내포를 허용한다. 경로path 식을 사용해서 내포 시킨 타입에 접근할 수 있다. 다음 예제를 보자.
557
// src/main/scala/progscala2/typesystem/typepaths/type-path.scalaX package progscala2.typesystem.typepaths class Service { class Logger { def log(message: String): Unit = println(s"log: $message") } val logger: Logger = new Logger } val s1 = new Servic val s2 = new Service { override val logger = s1.logger }
// ➊ // ➋
// 오류! ➌
➊ 내부에 Logger 클래스를 포함하는 Service 클래스를 정의한다. ➋ 로그를 남기기 위해 여기서는 println을 사용한다. ➌ 컴파일 오류! 이 파일을 컴파일하면 마지막 줄에서 다음과 같은 오류가 발생한다.
error: overriding value logger in class Service of type this.Logger; value logger has incompatible type val s2 = new Service { override val logger = s1.logger } ^
두 Logger의 타입이 같아야 하지 않냐고? 아니다. 오류 메시지가 의미하는 바는 this.Logger 라는 타입의 logger가 필요하다는 것이다. 스칼라는 각 Service 인스턴스의 logger를 서로 다 른 타입으로 인식한다. 다른 말로 하면, 실제 타입은 경로에 의존path-dependent 한다. 타입 경로의 여러 유형을 살펴보자.
15.1.1 C.this C1이라는 클래스에 대해 this라는 이미 익숙한 식을 본문에 사용해서 현재 인스턴스를 참조할
수 있다. 하지만 this는 스칼라에서 실제로 C1.this다.
558 3부 기초를 넘어서
// src/main/scala/progscala2/typesystem/typepaths/path-expressions.scala class var def def }
C1 { x = "1" setX1(x:String): Unit = this.x = x setX2(x:String): Unit = C1.this.x = x
어떤 타입의 본문 내부에서, 그러나 메서드 정의 바깥 부분에서 this는 해당 타입 자체를 가리 킨다.
trait T1 { class C val c1: C = new C val c2: C = new this.C }
명확히 하자면 여기서 this.C의 this는 T1이라는 트레이트다.
15.1.2 C.super 어떤 타입의 부모를 super라는 식으로 가리킬 수 있다.
trait X { def setXX(x:String): Unit = {} // 아무 일도 하지 않음! } class C2 extends C1 class C3 extends C2 with X { def setX3(x:String): Unit = super.setX1(x) def setX4(x:String): Unit = C3.super.setX1(x) def setX5(x:String): Unit = C3.super[C2].setX1(x) def setX6(x:String): Unit = C3.super[X].setXX(x) // def setX7(x:String): Unit = C3.super[C1].setX1(x) // 오류 = def setX8 x String Unit C3 super super setX1 x // ( : ): . . . ( ) // 오류 }
15장 스칼라 타입 시스템 II 559
이 예제에서 C3.super는 super와 같다. 어떤 부모를 지칭하는지 [T]를 사용해서 명확히 할 수 있다. setX5에서는 C2를 선택했고, setX6에서는 X를 선택했다. 하지만 ‘조부모’ 타입을 참조할 방법은 없다(setX7 ). 또한 super를 연쇄적으로 사용할 수도 없다(setX8 ). 여러 조상이 있는 타입에 대해 특별히 타입을 명시하지 않고 super를 사용한다면, 어떤 타입이 super와 연결될까? 선형화 규칙이 super의 대상을 결정한다( 11.2절 ‘객체의 상속 계층을 선형
화하기’를 보라). this와 마찬가지로 메서드 밖 타입 본문에서는 super를 사용해서 부모 타입을 참조할 수 있다.
class C4 { class C5 } class C6 extends C4 { val c5a: C5 = new C5 val c5b: C5 = new super.C5 }
15.1.3 경로.x 내포된 타입에 접근하려면 마침표를 사용한 경로식을 사용하면 된다. 어떤 타입 경로의 맨 마지 막 부분을 제외한 나머지 부분은 안정적stable 이어야 한다. 대략적으로 이는 패키지, 싱글턴 객체, 또는 그 둘에 대한 타입 별명이어야 한다는 의미다. 경로의 마지막 부분은 안정적이지 않아도 된다. 그래서 클래스, 트레이트, 타입 멤버 등을 사용할 수 있다. 다음 예제를 보자.
package P1 { object O1 { object O2 { val name = "name" } class C1 { val name = "name" } } }
560 3부 기초를 넘어서
class C7 { val name1 = P1.O1.O2.name = P1.O1.C1 type C1 = new P1.O1.C1 val c1 // val name2 = P1.O1.C1.name }
// // // //
OK - 필드에 대한 참조 OK - ‘말단’ 클래스에 대한 참조 OK - 위와 같은 이유 오류 - P1.O1.C1은 안정적이지 않음
C7의 멤버인 name1, C1, c1은 경로의 마지막 부분을 제외하고는 모두 안정적인 요소를 사용하
지만, name2는 안정적이지 않은 요소(C1 )를 끝에서 두 번째 위치에 사용했다. 마지막의 name2 선언의 주석을 제거하면 다음과 같은 컴파일 오류가 나는 것을 보고 이를 알 수 있다.
[error] .../typepaths/path-expressions.scala:52: value C1 is not a member of object progscala2.typesystem.typepaths.P1.O1 [error] val name2 = P1.O1.C1.name [error] ^
물론 코드에서 복잡한 경로를 사용하지 않는 편이 더 좋은 생각이다.
15.2 의존적 메서드 타입 스칼라 2.10에 추가된 기능으로 의존적 메서드 타입dependent method type 이 있다. 이는 경로에 의 존하는 타입의 일종으로, 몇 가지 설계 문제를 해결할 때 유용하다. 한 가지 응용으로는 자석 패턴Magnet Pattern 을 들 수 있다. 그 패턴은 처리를 위한 메서드가 하나 있어서 자석이라 불리는 객체를 넘겨받고, 그 객체(자석)는 호환 가능한 반환 타입을 보장해 준다. 이런 기법의 자세한 예는 spray.io 블로그(http://bit.ly/1tpmIQw )를 참조하라. 예제 를 보면서 이를 알아보자.
15장 스칼라 타입 시스템 II 561
// src/main/scala/progscala2/typesystem/dependentmethodtypes/dep-method.sc import scala.concurrent.{Await, Future} import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global
// ➊
case class LocalResponse(statusCode: Int) case class RemoteResponse(message: String) ...
// ➋
➊ 비동기 계산에 필요한 scala.concurrent.Future (http://bit.ly/1xIkpsr )와 기타 클래스 를 임포트한다. ➋ 계산의 ‘응답’을 반환하기 위해 두 가지 케이스 클래스를 정의한다. 이 둘은 지역(프로세스 내) 호출이거나, 원격 서비스 호출이다. 이 두 클래스가 공유하는 슈퍼타입이 없다는 점에 유 의하라. 이 둘은 완전히 별개다.
17.2절 ‘퓨처’에서 Future (http://bit.ly/1xIkpsr )에 대해 자세히 살펴볼 것이다. 지금은 필요 한 부분만 개략적으로 살펴보자.
sealed trait Computation { type Response val work: Future[Response] } case class LocalComputation( work: Future[LocalResponse]) extends Computation { type Response = LocalResponse } case class RemoteComputation( work: Future[RemoteResponse]) extends Computation { type Response = RemoteResponse } ...
Computation에 대해 봉인한 계층이 서비스가 수행할 모든 ‘계산’ 유형인 지역 계산과 원격 계산
을 표현한다. 수행할 작업은 Future로 감싸여 있다. 따라서 각 작업은 비동기적으로 실행된다.
562 3부 기초를 넘어서
지역적인 처리는 상응하는 LocalResponse를 반환하며, 원격 처리는 그에 따른 RemoteResponse 를 반환한다.
object Service { def handle(computation: Computation): computation.Response = { val duration = Duration(2, SECONDS) Await.result(computation.work, duration) } } Service.handle(LocalComputation(Future(LocalResponse(0)))) // 결과: LocalResponse = LocalResponse(0) Service.handle(RemoteComputation(Future(RemoteResponse("remote call")))) // 결과: RemoteResponse = RemoteResponse(remote call)
서비스는 handle이라는 단일 진입 지점으로 정의된다. handle은 scala.concurrent.Await (http://bit.ly/1tI8RGg )를 사용해서 퓨처가 완료될 때까지 기다린다. Await.result는 입력 으로 들어온 Computation에 따라 LocalResponse나 RemoteResponse를 반환한다. handle이 공통의 슈퍼클래스의 인스턴스를 반환하지 않는다는 점에 유의하라. LocalResponse
나 RemoteResponse가 서로 관계 없기 때문에 그렇게 할 수 없다. 대신 handle은 인자에 의존하 는 타입을 반환한다. RemoteComputation이 LocalResponse를 반환하거나, 지역 계산이 원격 응답을 반환하는 일은 불가능하다. 타입 검사에서 그런 경우를 통과시키지 않기 때문이다.
15.3 타입 투영 15.1절 ‘경로에 의존하는 타입’에서 본 Service 설계를 다시 살펴보자. 먼저 Service를 재작성 해서 실제 애플리케이션에서 볼 수 있는 전형적인 추상화를 뽑아내자.
15장 스칼라 타입 시스템 II 563
// src/main/scala/progscala2/typesystem/valuetypes/type-projection.scala package progscala2.typesystem.valuetypes trait Logger { def log(message: String): Unit }
// ➊
class ConsoleLogger extends Logger { def log(message: String): Unit = println(s"log: $message") }
// ➋
trait Service { type Log <: Logger val logger: Log }
// ➌
class Service1 extends Service { type Log = ConsoleLogger val logger: ConsoleLogger = new ConsoleLogger }
// ➍
➊ Logger 트레이트 ➋ 단순화를 위해 콘솔에 로그를 출력하는 구체적인 Logger ➌ Logger에 대한 추상 타입 별명을 정의하고, 그에 대한 필드를 선언하는 Service 트레이트 ➍ ConsoleLogger를 사용하는 구체적인 서비스 Service1에 정의한 Log 타입을 ‘재활용’하고 싶다고 하자. REPL에서 몇 가지 가능성을 시험해
보자.
// src/main/scala/progscala2/typesystem/valuetypes/type-projection.sc scala> import progscala2.typesystem.valuetypes._ scala> val l1: Service.Log = new ConsoleLogger error: not found: value Service val l1: Service.Log = new ConsoleLogger ^
<console>:10:
564 3부 기초를 넘어서
scala> val l2: Service1.Log = new ConsoleLogger <console>:10: error: not found: value Service1 val l2: Service1.Log = new ConsoleLogger ^ scala> val l3: Service#Log = new ConsoleLogger error: type mismatch; found : progscala2.typesystem.valuetypes.ConsoleLogger required: progscala2.typesystem.valuetypes.Service#Log val l3: Service#Log = new ConsoleLogger ^
<console>:10:
scala> val l4: Service1#Log = new ConsoleLogger l4: progscala2.typesystem.valuetypes.ConsoleLogger = progscala2.typesystem.valuetypes.ConsoleLogger@6376f152
Service.log와 Service1.log를 사용한다는 것은 스칼라가 Service나 Service1이라는 객체
를 찾아야 한다는 것을 의미한다. 하지만 이런 동반 객체는 존재하지 않는다. 하지만 우리가 원하는 타입 이름을 #를 사용해서 투영project 할 수 있다. 첫 번째 시도는 타임 검사 를 통과하지 못한다. Service.Log와 ConsoleLogger가 모두 Logger의 서브타입이기는 하지만, Service.log는 추상 타입이기 때문에, 그 타입이 실제로 ConsoleLogger의 슈퍼타입인지 아직
은 알 수 없다. 즉, 추상 타입을 마지막으로 구체적으로 정의하면서 ConsoleLogger와 호환되지 않는 다른 Logger의 서브타입을 지정할 수도 있다. 제대로 작동하는 유일한 문장은 val l4 = Service1#Log = new ConsoleLogger다. 이 문장은 정적인 타임 검사를 통과한다. 우리가 매일 사용하는 간단한 타입 명세를 일컬어 타입 지정자type designator 라고 한다. 이들은 실 제로는 타입 투영을 짧게 쓴 것이다. 다음 코드는 몇 가지 타입 지정자와 그에 대한 더 긴 타입 투영을 보여준다. 이 코드는 “스칼라 언어 명세”의 3.2절에서 가져왔다.
Int // scala.type#Int scala.Int // scala.type#Int package pkg { class MyClass { type t // pkg.MyClass.type#t } }
15장 스칼라 타입 시스템 II 565
15.3.1 싱글턴 타입 object 키워드로 선언한 싱글턴 객체에 대해서는 이미 배웠다. 싱글턴 타입singleton type 이라는
개념도 있다. AnyRef의 서브타입인 인스턴스 v에는 모두 고유의 싱글턴 타입이 있다(null 포 함). v.type을 사용하면 그 타입을 사용할 수 있다. v.type은 타입 지정을 v가 지정하는 오직 한 인스턴스에만 한정시킨다. 앞에서 본 Logger와 Service 예제를 다시 사용해보자.
// src/main/scala/progscala2/typesystem/valuetypes/type-types.sc scala> val s11 = new Service1 scala> val s12 = new Service1 scala> val l1: Logger = s11.logger l1: ...valuetypes.Logger = ...valuetypes.ConsoleLogger@3662093 scala> val l2: Logger = s12.logger l2: ...valuetypes.Logger = ...valuetypes.ConsoleLogger@411c6639 scala> val l11: s11.logger.type = s11.logger l11: s11.logger.type = progscala2.typesystem.valuetypes.ConsoleLogger@3662093 scala> val l12: s11.logger.type = s12.logger error: type mismatch; found : s12.logger.type (with underlying type ...valuetypes.ConsoleLogger) required: s11.logger.type val l12: s11.logger.type = s12.logger ^
<console>:12:
l11과 l12에 대입할 수 있는 것은 s11.logger뿐이다. s12.logger는 호환되지 않는다.
싱글턴 객체는 인스턴스를 하나 정의하는 동시에 그와 대응하는 타입도 하나 정의한다.
// src/main/scala/progscala2/typesystem/valuetypes/object-types.sc case object Foo { override def toString = "Foo says Hello!" }
이런 타입을 인자로 취하는 메서드를 정의하고 싶다면 Foo.type을 사용하라.
566 3부 기초를 넘어서
scala> def printFoo(foo: Foo.type) = println(foo) printFoo: (foo: Foo.type)Unit scala> printFoo(Foo) Foo says Hello!
15.4 값에 대한 타입 모든 값에는 타입이 있다. 값 타입value type 이라는 말은 이런 타입이 취할 수 있는 모든 형태를 의미한다. 지금까지 살펴본 모든 타입이 이런 타입에 포함된다.
CAUTION_ 이번 절에서는 값 타입이라는 말을 ‘스칼라 언어 명세’의 용례에 따라 사용한다. 하지만 이 책 의 다른 곳에서는 더 일반적인 관례를 따라 AnyVal의 모든 서브타입을 일컫는 말로 값 타입이라는 말을 사 용한다.
모든 것을 알려주자면, 값 타입에는 매개변수화한 타입, 싱글턴 타입, 타입 투영, 타입 지정자, 복합 타입, 존재 타입, 튜플 타입, 함수 타입, 그리고 중위 타입infix type 이 있다. 타입을 사용할 때 편하게 구문을 작성할 수 있게 해주는 마지막 세 가지 타입을 살펴보자. 또한 지금까지 다루지 않은 몇 가지 세부 사항도 살펴볼 것이다.
15.4.1 튜플 타입 스칼라에서 Tuple3[A,B,C]를 (A,B,C )라고 쓸 수 있다는 사실은 이미 배웠다.
val t1: Tuple3[String, Int, Double] = ("one", 2, 3.14) = ("one", 2, 3.14) val t2: (String, Int, Double)
이러한 튜플 타입 표기를 사용하면 각괄호의 중복을 줄여서 더 복잡한 타입을 편하게 쓸 수 있 다. 게다가 TupleN이 들어가지 않기 때문에 길이도 훨씬 짧아진다. 사실 TupleN 형태를 타입 시
15장 스칼라 타입 시스템 II 567
그니처에서 사용하는 일은 드물다. List[Tuple2[Int,String]]과 List[(Int,String )]을 비 교해보면 이유는 분명하다.
15.4.2 함수 타입 함수의 타입은 화살표 표기를 사용해서 쓸 수 있다. 예를 들어 Function2를 보자.
val f1: Function2[Int,Double,String] = (i,d) => s"int $i, double $d" = (i,d) => s"int $i, double $d" val f2: (Int,Double) => String
TupleN 구문으로 튜플의 타입을 지정하는 일이 드문 것처럼, 함수 타입을 FunctionN 구문으로
지정하는 경우도 드물다.
15.4.3 중위 타입 타입 매개변수가 두 개인 매개변수화한 타입은 중위 표기법으로 쓸 수 있다. Either[A,B]를 사 용한 다음 예를 보자.
val val val val
left1: Either[String,Int] left2: String Either Int right1: Either[String,Int] right2: String Either Int
= = = =
Left("hello") Left("hello") Right(1) Right(2)
중위 타입을 내포시킬 수도 있다. 그런 경우 타입 이름이 콜론(: )으로 끝나지 않는 한 왼쪽을 우선으로 결합된다. :으로 끝나는 타입은 오른쪽으로 결합된다. 이는 항에서와 동일하다(항이 라는 용어를 강조하지는 않았지만, 타입이 아닌 모든 식을 항term 이라고 부른다). 괄호를 사용 해서 기본적인 결합 순서를 변경할 수 있다.
568 3부 기초를 넘어서
CHAPTER
24
메타프로그래밍: 매크로와 리플렉션
메타프로그래밍metaprogramming 은 프로그램을 데이터로 조작하는 프로그래밍이다. 일부 언어에서 는 프로그래밍과 메타프로그래밍의 차이가 그리 크지 않다. 예를 들어 리스프의 변종에서는 코 드와 데이터에 대해 동일한 S 표현식S-expression 을 사용하며, 이런 성질을 동형성homoiconicity 이라 고 부른다. 따라서 코드를 조작하는 것이 간단하며, 일반적이지 않다. 자바나 스칼라와 같은 정 적 타입 지정 언어에서는 메타프로그래밍이 덜 일반적이다. 하지만 여러 설계 문제를 해결하는 데 있어 메타프로그래밍은 여전히 유용한 도구다. 때로는 리플렉션reflection 이라는 말을 일반적인 메타프로그래밍의 의미로 사용하기도 한다. 스 칼라 리플렉션 라이브러리에서는 바로 그런 의미로 해당 용어를 사용한다. 하지만 때로는 같은 용어가, 실행 시점에 코드를 변경하지 않거나 제한적으로 변경하면서 코드에 대한 ‘인트로스펙 션introspection (자기 관찰)’을 수행하는 좀 더 좁은 범위를 의미하기도 한다. 코드를 컴파일한 다음에 실행하는 스칼라 같은 언어와 ‘그때그때’ 코드를 해석하는 여러 동적 타입 언어를 비교해보면, 컴파일 시점과 실행 시점 메타프로그래밍이라는 차이가 있다. 컴파일 시점 메타프로그래밍에서는 모든 호출이 컴파일 직전이나 도중에 일어난다. 고전적인 C 언어 의 전처리기preprocessor 는 컴파일하기 전에 소스 코드를 변환하는 처리의 한 예다. 스칼라의 메타프로그래밍 지원은 매크로macro 기능을 통해 컴파일 시점에 일어난다. 매크로는 구문분석이 끝난 소스 코드로부터 생성되는 추상 구문 트리abstract syntax tree (AST )를 조작하므로, 제한이 가해진 컴파일러 플러그인처럼 작동한다. 매크로는 바이트 코드 생성에 이르는 컴파일의 최종 단계 직전에 AST를 조작하기 위해 호출된다.
729
자바 리플렉션 라이브러리나 스칼라가 제공하는 확장 리플렉션 라이브러리는 실행 시점 리플렉 션을 제공한다. 스칼라의 리플렉션 API에는 매크로 지원도 들어 있으며, 스칼라에서 가장 빠르게 바뀌고 있는 부분 중 하나다. 이 분야의 변화가 빠르기 때문에, 이 책에서는 가장 안정적인 부분인 컴파일 시 점 및 실행 시점 리플렉션과 의사인용quasiquote 에 초점을 맞출 것이다. 하지만 현재 버전의 매크 로 API를 사용하는 완전한 매크로 예제도 마지막에 다룰 것이다. 차세대 매크로 기능은 현재 개발 중이다. 이 프로젝트를 스칼라 메타(http://scalameta.org ) 라고 부른다. 현재 스칼라 메타는 프리뷰 버전을 앞두고 있다. 향후 나올 스칼라 버전에 포함될 매크로 기능에 대한 최신 정보를 얻고 싶은 독자는 스칼라 메타 홈페이지를 살펴보라. 스칼라
2.10과 2.11의 매크로 구현에 대해서는 http://scalamacros.org와 현재의 매크로 시스템의 인큐베이터 프로젝트인 매크로 낙원Macro Paradise (http://bit.ly/1xIibcu )을 보라. 식의 타입을 이해하는 데 유용한 몇 가지 REPL 도구부터 시작할 것이다. 그 후 실행 시점 리플 렉션을 탐구하고, 의사인용과 매크로 예제를 다룰 것이다.
24.1 타입을 이해하기 위한 도구 REPL에는 타입 정보를 표시하는 :type 명령이 있다.
scala> if (true) false else 11.1 res0: AnyVal = false scala> :type if (true) false else 11.1 AnyVal scala> :type -v if (true) false else 11.1 // Type signature AnyVal // Internal Type structure TypeRef(TypeSymbol(abstract class AnyVal extends Any))
730 4부 고급 주제 및 실전 응용
:type 명령은 단지 타입을 보여줄 뿐이다. 어쨌든 REPL은 타입을 표시하기는 한다. 하지만 -v (자세히 설명) 옵션을 사용하면 ‘내부 타입 구조’도 보여준다. 이제 핵심 표준 라이브러리
와 분리된 리플렉션 API에는 scala.reflect.api.Types#TypeRef (http://bit.ly/1xIibJp )와 scala.reflect.api.Symbols#TypeSymbol ( http://bit.ly/1rGtvkG ) 타입이 들어 있다. 스칼
라독은 http://bit.ly/1wQkYDN에서 찾아볼 수 있다.
24.2 실행 시점 리플렉션 코드를 조작하기 위해 컴파일 시점의 리플렉션을 사용할 수 있지만, 실행 시점의 리플렉션은 주 로 언어의 의미를 ‘비틀고’, 컴파일 시점에는 알 수 없는 코드를 읽어 들이기 위한 것이다. 따라서 이를 극단적으로 늦게 바인딩하기extreme late binding 라고 부르기도 한다. 예를 들어 특정 기능을 위해 어떤 인스턴스를 사용할지 명령행 인자나 프로퍼티를 통해 동적으 로 지정할 수도 있다. 리플렉션 API를 사용하면 CLASSPATH에서 찾을 수 있는 바이트 코드 중 상응하는 타입을 찾아서, 그런 타입이 있는 경우 인스턴스를 만들어낼 수 있다. IDE 등의 도구 는 리플렉션을 활용해서 플러그인을 찾아 적재할 수 있다. IDE는 때로 리플렉션을 사용해서 프 로젝트나 라이브러리 안의 코드에 대한 정보를 얻어서 코드 자동 완성, 타입 검사 등에 활용한 다. 바이트 코드 도구는 리플렉션을 활용해서 보안상 취약점이나 다른 문제를 찾아낼 수도 있다.
24.2.1 타입에 대한 리플렉션 여러분은 java.lang.Class (http://bit.ly/1ucWSml )와 같은 자바의 리플렉션 API를 사용할 수 있다.
// src/main/scala/progscala2/metaprogramming/reflect.sc scala> import scala.language.existentials import scala.language.existentials scala> trait T[A] { | val vT: A
24장 메타프로그래밍: 매크로와 리플렉션 731
| def mT = vT | } defined trait T scala> class C(foo: Int) extends T[String] { | val vT = "T" | val vC = "C" | def mC = vC | | class C2 | } defined class C scala> val c = new C(3) c: C = $anon$1@5a58e6a4 scala> val clazz = classOf[C] clazz: Class[C] = class C
// 스칼라 메서드: classOf[C]
scala> val clazz2 = c.getClass clazz2: Class[_ <: C] = class $anon$1
// java.lang.Object의 메서드
scala> val name = clazz.getName name: String = C scala> val methods = clazz.getMethods methods: Array[java.lang.reflect.Method] = Array(public java.lang.String C.mC(), public java.lang.Object C.vT(), ...) scala> val ctors = clazz.getConstructors ctors: Array[java.lang.reflect.Constructor[_]] = Array(public C(int)) scala> val fields = clazz.getFields fields: Array[java.lang.reflect.Field] = Array() scala> val annos = clazz.getAnnotations annos: Array[java.lang.annotation.Annotation] = Array() scala> val parentInterfaces = clazz.getInterfaces parentInterfaces: Array[Class[_]] = Array(interface T) scala> val superClass = clazz.getSuperclass superClass: Class[_ >: C] = class java.lang.Object
732 4부 고급 주제 및 실전 응용
scala> val typeParams = clazz.getTypeParameters typeParams: Array[java.lang.reflect.TypeVariable[Class[C]]] = Array()
이런 메서드는 오직 AnyRef의 서브타입에서만 사용 가능하다. getFields는 스칼라 타입의 C에 있는 필드를 인식할 수 없었다는 사실에 유의하라! Predef에는 어떤 객체가 타입에 일치하는지 검사하고, 객체를 다른 타입으로 변환하기 위한 메
서드를 제공한다.
scala> c.isInstanceOf[String] <console>:13: warning: fruitless type test: a value of type C cannot also be a String (the underlying of String) c.isInstanceOf[String] ^ res0: Boolean = false scala> c.isInstanceOf[C] res1: Boolean = true scala> c.asInstanceOf[T[AnyRef]] res2: T[AnyRef] = C@499a497b
자바는 이런 작업에 언어에서 제공하는 키워드를 연산자로 사용한다. 스칼라에서 이런 메서드의 이름을 복잡하게 지은 이유는 될 수 있으면 사용하지 않게 하기 위해서다! 스칼라의 다른 기능, 특히 패턴 매칭이 훨씬 더 좋은 대안이다.
24.2.2 클래스 태그, 타입 태그, 매니페스트 스칼라 2.11 핵심 라이브러리에는 작은 리플렉션 API가 들어 있다. 반면 고급 리플렉션 기능 은 별도의 라이브러리에 들어 있다. 핵심 라이브러리의 ClassTag (http://bit.ly/1tHWxpq ) 를 살펴보자. 이는 JVM이 매개변수화한 타입을 인스턴스화하면서 타입 매개변수로 사용한 타 입을 보관하지 않는다는 특징인 타입 소거에 의해 제거될 수 있는 정보를 일부 보관해두기 위한 도구다.
24장 메타프로그래밍: 매크로와 리플렉션 733
ClassTag의 다른 중요한 용법은 제대로 된 AnyRef 서브타입으로 된 자바 배열을 만드는 것이
다. 다음은 ClassTag에 대한 스칼라독(http://bit.ly/1tHWxpq ) 페이지에서 가져온 예제다.
// src/main/scala/progscala2/metaprogramming/mkArray.sc scala> import scala.reflect.ClassTag import scala.reflect.ClassTag scala> def mkArray[T : ClassTag](elems: T*) = Array[T](elems: _*) mkArray: [T](elems: T*)(implicit evidence$1: scala.reflect.ClassTag[T])Array[T] scala> mkArray(1, 2, 3) res0: Array[Int] = Array(1, 2, 3) scala> mkArray("one", "two", "three") res1: Array[String] = Array(one, two, three) scala> mkArray(1, "two", 3.14) <console>:10: warning: a type was inferred to be `Any`;
this may indicate a programming error. mkArray(1, "two", 3.14) ^ res2: Array[Any] = Array(1, two, 3.14)
이는 AnyRef에 대한 Array.apply (http://bit.ly/1wN2lNE ) 메서드를 사용한다. 그 메서드의 두 번째 인자 목록에는 암시적인 ClassTag 인자가 하나만 들어 있다. 컴파일러는 자신이 아는 타입 정보를 사용해서 암시적인 ClassTag를 만든다. 하지만 이미 만 들어진 리스트를 제공할 경우에는 중요한 타입 정보를 이미 잃어버린 다음이다. 이는 여러분이 컬렉션을 호출 스택의 깊은 곳으로 전달하고, 전달받은 메서드 중 일부가 ClassTag를 사용해 서 인트로스펙션을 하려는 경우 문제가 된다. 컬렉션을 만드는 것과 그에 대응하는 ClassTag 를 만드는 일을 동일한 영역에서 진행하고, 어떤 방법으로든 두 가지를 한꺼번에 전달할 필요 가 있다. 아마도 컬렉션을 구축한 다음에 이루어지는 여러 메서드 호출에서 ClassTag를 암시적 인자로 사용하는 방법을 통해야 할 것이다. 이 문제에 대해서는 잠시 후에 다룰 것이다. 따라서 ClassTag는 바이트 코드에서 타입 정보를 ‘부활’시킬 수 없다. 그러나 어떤 타입 정보를 소거되기 전에 확보해서 사용하기 위해 ClassTag를 사용할 수 있다.
734 4부 고급 주제 및 실전 응용
ClassTag는 분리된 리플렉션 API에서 찾을 수 있는 scala.reflect.api.TypeTags#TypeTag
(http://bit.ly/1083R1Y )를 약하게 만든 버전이다. 후자는 전체 컴파일러 정보를 보존하지만 (이에 대해서는 잠시 후에 설명할 것이다), ClassTag는 오직 런타임 정보만 반환한다. 추상 타 입을 위한 scala.reflect.api.TypeTags#WeakTypeTag (http://bit.ly/1wNDoCw )도 있다. 이에 대한 자세한 설명은 스칼라 문서(http://bit.ly/1wNSvwT )를 참고하라. reflect 패키지에는 스칼라 2.10에서 TypeTag, ClassTag 등을 도입하기 전에 사용하던 더 오
래된 타입인 Manifest도 있다. 조만간 이런 타입은 사용 금지될 것이다. 오래된 소스 코드에서 그런 타입을 볼 수 있을 것이다. 하지만 여러분은 새로 도입한 기능을 활용해야 한다.
24.3 스칼라의 고급 실행 시점 리플렉션 API 스칼라 리플렉션 API의 나머지 부분에서는 실행 시점 리플렉션뿐 아니라 컴파일 시점 매크로도 지원한다. 그 안에는 추상 구문 트리를 표현하는 타입이나 다른 문맥을 표현하는 타입도 들어 있 다. 그런 API는 별도의 JAR 파일로 배포되며, 예제 sbt 빌드 파일에는 그에 대한 의존관계를 포 함시켜 뒀다. 이 API에 대한 자세한 설명은 스칼라 문서(http://bit.ly/1vorYlD )에 있다. 이
API는 규모가 매우 크다. 핵심 아이디어와 일반적인 작업, 실행 시점 타입 인트로스펙션에 대한 몇 가지 예제를 여기서 다룰 것이다.
// src/main/scala/progscala2/metaprogramming/match-type-tags.sc import scala.reflect.runtime.universe._
// ➊
def toType2[T](t: T)(implicit tag: TypeTag[T]): Type = tag.tpe def toType[T : TypeTag](t: T): Type = typeOf[T]
// ➋ // ➌
➊ 런타임 ‘유니버스’에 있는 정의를 임포트한다. 런타입 유니버스는 scala.reflect.api. JavaUniverse ( http://bit.ly/1tomZEK ) 타입이다. 이 ‘유니버스’는 대상 플랫폼의 언어 요소
를 반영하는 타입과 편의 메서드를 노출시킨다. ➋ TypeTag[T] (http://bit.ly/1G2nqd2 )에 대한 암시적 인자를 사용하고, 그 타입을 물어본다.
24장 메타프로그래밍: 매크로와 리플렉션 735
➌ 맥락 바운드 context bound 를 사용하는 더 편리한 방법을 보여준다. typeOf[T] 메서드는 implicitly[TypeTag[T]].tpe를 쓰기 쉽게 제공하는 짧은 표현이다. TypeTag가 전체 컴파일 시점의 타입 정보를 유지하는 반면, ClassTag는 실행 시점의 타입 정보
만 유지한다는 사실을 상기하라. 이런 메서드를 몇 가지 타입에 적용해보자.
scala> toType(1) res1: reflect.runtime.universe.Type = Int scala> toType(true) res2: reflect.runtime.universe.Type = Boolean scala> toType(Seq(1, true, 3.14)) warning: a type was inferred to be `AnyVal`; this may indicate a programming error. toType(Seq(1, true, 3.14)) ^ res3: reflect.runtime.universe.Type = Seq[AnyVal]
<console>:12:
scala> toType((i: Int) => i.toString) res4: reflect.runtime.universe.Type = Int => java.lang.String
매개변수화한 타입의 매개변수에 대한 타입을 제대로 결정할 수 있다. 따라서 useClassTag에 있었던 버그를 제대로 수정할 수 있다. 이제부터는 AnyVal에 대한 경고는 생략할 것이다. 동등성을 확인하기 위해 타입을 비교하거나 부모-자식 관계를 비교할 수 있다.
toType(1) =:= typeOf[AnyVal] toType(1) =:= toType(1) toType(1) =:= toType(true)
// false // true // false
toType(1) <:< typeOf[AnyVal] toType(1) <:< toType(1) toType(1) <:< toType(true)
// true // true // false
typeOf[Seq[Int]] =:= typeOf[Seq[Any]] typeOf[Seq[Int]] <:< typeOf[Seq[Any]]
// false // true
736 4부 고급 주제 및 실전 응용
w w w. h a n b i t . c o . k r
이것이 프로그래밍이다! 저자 직강 동영상 제공!
이것이 안드로이드다
이것이 C언어다
이것이 자바다
진정한 안드로이드 개발자로 이끌어줍니다.
세상에 없던 새로운 C언어 입문서 탄생!
가장 중요한 프로그래밍 언어를 하나 배워야 한다면, 결론은 자바다!
SDK 5.0 롤리팝 호환!
삼성, LG에서 펼쳐졌던 전설의 명강의를 풀타임 동영상 강좌로!
중급 개발자로 나아가기 위한 람다식, JavaFX, NIO 수록
이보다 더 확실한 방법은 없다, 칠판강의
책만 보고, 동영상 강좌로도 만족하지 못했다면 Daum 카페 '슈퍼드로이드'에서 만나요
전체 동영상 강좌 유투브 전격 공개!
자바의 모든 것을 알려주는 인터넷 강의 궁금한 것은 카페에서!
cafe.daum.net/superdroid
http://goo.gl/tJK3Tu
cafe.naver.com/thisisjava
박성근 저 | 1,164쪽 | 45,000원
서현우 저 | 708쪽 | 25,000원
신용권 저 | 1,224쪽 | 30,000원
w w w. h a n b i t . c o . k r
지금은 모던 웹 시대! 모던 웹 디자인을 위한
모던 웹을 위한
HTML5 + CSS3 입문 HTML5 분야 부동의 1위 도서
JavaScript + jQuery 입문
HTML5 표준안 확정에 맞춘 완전 개정판의 귀환!
자바스크립트에서 제이쿼리, 제이쿼리 모바일까지 한 권으로 끝낸다!
HTML5 권고안과 최신 웹 브라우저 환경 대응
시대의 흐름에 맞춰 다시 쓴 자바스크립트 교과서
윤인성 저 | 624쪽 | 30,000원
윤인성 저 | 980쪽 | 32,000원
모던 웹을 위한
HTML5 + CSS3 정복
Node.js
프로그래밍 페이스북, 월마트, 링크드인은 왜 Node.js를 선택했는가?
필요한 것만 배워 바로 현장에서 쓰는 HTML5
이 물음에 대한 답은 Node.js가 보여주는 빠른 처리 능력 때문이다.
순서대로 읽으며 실습할 수 있는 HTML5 자습서
윤인성 저 | 484쪽 | 25,000원
김상형 저 | 700쪽 | 32,000원
w w w. h a n b i t . c o . k r
Hanbit eBook
Realtime w w w. h a n b i t . c o . k r / e b o o k
DRM free! 어떤 디바이스에서도 자유롭게
eBook Oriented! 전자책에 꼭 맞는 최적의 내용과 디자인
Hanbit eBook
Hanbit eBook
Realtime 70
Realtime 89 49
MFC 프로그래밍 주식분석 프로그램 만들기 김세훈 지음
Hanbit eBook
Hanbit eBook
Realtime 90
Realtime 92 49
자바 개발자를 위한
Vert.x JavaScript Promise azu지음 /주우영옮김
애플리케이션 개발 모바일/웹 메시징 STOMP와 MQTT로 개발하는 IoT 모바일/웹 애플리케이션 Mobile and Web Messaging 제프 메스닐 지음 / 조건희 옮김
이연복 지음
w w w. h a n b i t . c o . k r
즐거운 상상이 가득! 2015년 화제의 신간
즐거운 상상이 가득! 2015년 화제의 신간
전자부품 백과사전 vol.1 찰스 플랫 지음 / 배지은 옮김 / 30,000원
취미공학에 필요한 핵심 전자부품을 사전식으로 정리한 안내서.
전자부품 백과사전 vol.1 찰스 플랫 지음 / 배지은 옮김 / 30,000원
처음 시작하는 센서
취미공학에 필요한 핵심 전자부품을 사전식으로 정리한 안내서.
전자부품 백과사전 vol.2
찰스 플랫 지음 / 가격미정
키모 카르비넨, 테로 카르비넨 지음 임지순 옮김 / 13,000원
세상을 수치로 읽어내는
<전자부품 백과사전> 시리즈의 두 번째 도서다.
부품인 센서를 알려주 는 책. 이 책을 통해 자신 만의 프로젝트에 다양한 처음 센서를 사용해보자.
Zero to Maker
: 누구나 메이커가 될 수 있다 데이비드 랭 지음 / 장재웅 옮김 / 14,000원
전자부품 백과사전 vol.2
찰스 플랫 지음 / 가격미정
일반인에서 메이커로. 날백수에서 무인 잠
<전자부품 백과사전> 시리즈의 수정 회사 CEO 가 된 사나이, 데이비드두 번째 도서다. 랭의 메이커 도전기.
Make: 센서
시작하는 센서
키모 카르비넨, 테로 카르비넨 지음 임지순 옮김 / 13,000원
세상을 수치로 읽어내는 부품인 센서를 알려주
키모 카르비넨, 테로 카르비 넨, 빌 발토카리 지음 는 책. 이 책을 통해 자신 / 가격미정
만의 프로젝트에 다양한
필수 전자부품인 센서를
센서를 사용해보자. 마이크로 컨트롤러 보드
Zero to Maker
: 누구나 메이커가 될 수 있다
에 응용하는 방법을 담
데이비드 랭 지음 / 장재웅 옮김 / 14 ,000원 Maker Pro
았다.
베이첼 지음 / 가격미정 일반인에서 메이커로.존날백수에서 무인 잠
메이커라면 반드시 읽어야 할 필수 계발
수정 회사 CEO가 된 사나이, 데이비드 서. 프로 메이커들과의 인터뷰 및 에세이 랭의 메이커 도전기. 수록.
프로젝트로 배우는 라즈베리 파이
도날드 노리스 지음 / 임지순 옮김
다양한 실전 프로젝트를 통해 라즈베리 파이를 쉽고 재미있게 배워본다.
Maker Pro 존 베이첼 지음 / 가격미정
메이커라면 반드시 읽어야 할 필수 계발
Make: 센서 키모 카르비넨, 테로 카르비 넨, 빌 발토카리 지음 / 가격미정
필수 전자부품인 센서를 마이크로 컨트롤러 보드 에 응용하는 방법을 담 았다.
입문자부터 고급 개발자까지, 모두가 기다린 실전 안내서 P r o g r a m m i n g
S c a l a
2 n d
실용적인 스칼라 활용법을 익히는 가장 확실한 실전 바이블
E d i t i o n
스칼라 2.11.x 버전 기준
Programming
이 책은 다양한 코드 예제가 포함된 실전 바이블이다. 초보자와 고급 사용자를 한데 아우를 뿐 아니라, 실 제 개발자들의 실용적 관심사에 초점을 맞추어 실전 활용법을 안내한다. 최신 스칼라 언어의 특징부터 패턴 매칭, for 내장, 고급 함수형 프로그래밍 등의 새로운 내용을 소개한다. 또한 스칼라 명령행 도구와 서드파티
Scala
도구, 라이브러리, IDE의 스칼라 지원에 대해 살펴보고, 그 과정에서 생산성을 발휘하는 방법을 제시한다.
•간결하고 유연한 문법으로 빠르게 프로그래밍하기 •함수형 프로그래밍의 기초와 고급 기법 익히기 •함수 컴비네이터로 빅데이터 애플리케이션 작성하기 •트레이트 혼합을 활용하고, 데이터 추출에 패턴 매칭 사용하기 •함수형과 객체 지향 프로그래밍 개념을 한데 묶는 정교한 타입 시스템 배우기 •액터 기반 동시성 모델 아카와, 비동기 코드 작성에 유용한 퓨처 다루기
2nd Edition
관련 도서
프로그래밍 스칼라
•도메인 특화 언어(DSL) 개발법 이해하기 •확장성이 좋으면서 튼튼한 애플리케이션 설계 기법 배우기
이 책은 매우 실용적이라는 점에서 나를 흥분시킨다. 저자들은 재미있는 예제와 설명으로 스칼라 언어를 소개할 뿐 아니라, 스칼라를 실제 어떻게 활용할지 보여준다는 측면에서도 멋진 작품을 만들어냈다. 실제 목표를 달성하 고자 하는 프로그래머를 위한 책이다.
관련 도서 브루스 테이트의 세븐 랭귀지
폴리글랏 프로그래밍
누구나 쉽게 배우는 스칼라 e-Book
딘 왐플러, 알렉스 페인 지음 오현석 옮김
요나스 보네르, Typesafe 공동 창립자
프로그래밍 언어 기타
예제 소스 http://www.hanbit.co.kr/exam/2275
정가 48,000원
딘 왐플러, 알렉스 페인 지음 오현석 옮김