├── 2장 동작 파라미터화 코드 전달하기 ├── ch2 │ ├── settings.gradle │ ├── src │ │ ├── main │ │ │ ├── resources │ │ │ │ └── application.properties │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── demo │ │ │ │ ├── Ch2Application.java │ │ │ │ ├── service │ │ │ │ └── UserFilter.java │ │ │ │ ├── controller │ │ │ │ └── UserController.java │ │ │ │ └── entity │ │ │ │ └── User.java │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── demo │ │ │ └── Ch2ApplicationTests.java │ ├── .gitattributes │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── build.gradle │ ├── gradlew.bat │ └── gradlew └── ch2.md ├── 3장 람다 표현식 ├── img │ ├── 1.jpg │ ├── 2.jpg │ └── 3.jpg └── ch3.md ├── 4장 스트림 소개 ├── img │ ├── 1.jpg │ └── 2.jpg └── ch4.md ├── 13장 디폴트 메서드 ├── img │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ └── 6.jpg └── ch13.md ├── 14장 자바 모듈 시스템 ├── img │ └── 1.jpg └── ch14.md ├── README.md ├── 18장 함수형 관점으로 생각하기 └── ch18.md ├── 11장 null 대신 Optional 클래스 └── ch11.md ├── 6장 스트림으로 데이터 수집 └── ch6.md ├── 15장 CompletableFuture와 리액티브 프로그래밍 컨셉의 기초 └── ch15.md ├── 12장 새로운 날짜와 시간 API └── ch12.md ├── 8장 컬렉션 API 개선 └── ch8.md ├── 17장 리액티브 프로그래밍 └── ch17.md ├── 1장 자바 8, 9, 10, 11 무슨 일이 일어나고 있는가 └── ch1.md ├── 5장 스트림 활용 └── ch5.md ├── 7장 병렬 데이터 처리와 성능 └── ch7.md ├── 16장 CompletableFuture : 안정적 비동기 프로그래밍 └── ch16.md └── 9장 리팩토링, 테스팅, 디버깅 └── ch9.md /2장 동작 파라미터화 코드 전달하기/ch2/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'demo' 2 | -------------------------------------------------------------------------------- /2장 동작 파라미터화 코드 전달하기/ch2/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=ch2 2 | -------------------------------------------------------------------------------- /2장 동작 파라미터화 코드 전달하기/ch2/.gitattributes: -------------------------------------------------------------------------------- 1 | /gradlew text eol=lf 2 | *.bat text eol=crlf 3 | *.jar binary 4 | -------------------------------------------------------------------------------- /3장 람다 표현식/img/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryuseunghan/java-in-action-study/HEAD/3장 람다 표현식/img/1.jpg -------------------------------------------------------------------------------- /3장 람다 표현식/img/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryuseunghan/java-in-action-study/HEAD/3장 람다 표현식/img/2.jpg -------------------------------------------------------------------------------- /3장 람다 표현식/img/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryuseunghan/java-in-action-study/HEAD/3장 람다 표현식/img/3.jpg -------------------------------------------------------------------------------- /4장 스트림 소개/img/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryuseunghan/java-in-action-study/HEAD/4장 스트림 소개/img/1.jpg -------------------------------------------------------------------------------- /4장 스트림 소개/img/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryuseunghan/java-in-action-study/HEAD/4장 스트림 소개/img/2.jpg -------------------------------------------------------------------------------- /13장 디폴트 메서드/img/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryuseunghan/java-in-action-study/HEAD/13장 디폴트 메서드/img/1.jpg -------------------------------------------------------------------------------- /13장 디폴트 메서드/img/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryuseunghan/java-in-action-study/HEAD/13장 디폴트 메서드/img/2.jpg -------------------------------------------------------------------------------- /13장 디폴트 메서드/img/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryuseunghan/java-in-action-study/HEAD/13장 디폴트 메서드/img/3.jpg -------------------------------------------------------------------------------- /13장 디폴트 메서드/img/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryuseunghan/java-in-action-study/HEAD/13장 디폴트 메서드/img/4.jpg -------------------------------------------------------------------------------- /13장 디폴트 메서드/img/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryuseunghan/java-in-action-study/HEAD/13장 디폴트 메서드/img/5.jpg -------------------------------------------------------------------------------- /13장 디폴트 메서드/img/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryuseunghan/java-in-action-study/HEAD/13장 디폴트 메서드/img/6.jpg -------------------------------------------------------------------------------- /14장 자바 모듈 시스템/img/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryuseunghan/java-in-action-study/HEAD/14장 자바 모듈 시스템/img/1.jpg -------------------------------------------------------------------------------- /2장 동작 파라미터화 코드 전달하기/ch2/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryuseunghan/java-in-action-study/HEAD/2장 동작 파라미터화 코드 전달하기/ch2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /2장 동작 파라미터화 코드 전달하기/ch2/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /2장 동작 파라미터화 코드 전달하기/ch2/src/test/java/com/example/demo/Ch2ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class Ch2ApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /2장 동작 파라미터화 코드 전달하기/ch2/src/main/java/com/example/demo/Ch2Application.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Ch2Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Ch2Application.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /2장 동작 파라미터화 코드 전달하기/ch2/src/main/java/com/example/demo/service/UserFilter.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.service; 2 | 3 | import com.example.demo.entity.User; 4 | 5 | import java.util.List; 6 | import java.util.function.Predicate; 7 | import java.util.stream.Collectors; 8 | 9 | public class UserFilter { 10 | 11 | public static List filterUsers(List users, Predicate predicate) { 12 | return users.stream() 13 | .filter(predicate) // 조건에 따라 필터링 14 | .collect(Collectors.toList()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /2장 동작 파라미터화 코드 전달하기/ch2/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /2장 동작 파라미터화 코드 전달하기/ch2/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.4.1' 4 | id 'io.spring.dependency-management' version '1.1.7' 5 | } 6 | 7 | group = 'com.example' 8 | version = '0.0.1-SNAPSHOT' 9 | 10 | java { 11 | toolchain { 12 | languageVersion = JavaLanguageVersion.of(17) 13 | } 14 | } 15 | 16 | repositories { 17 | mavenCentral() 18 | } 19 | 20 | dependencies { 21 | implementation 'org.springframework.boot:spring-boot-starter' 22 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 23 | testRuntimeOnly 'org.junit.platform:junit-platform-launcher' 24 | implementation 'org.springframework.boot:spring-boot-starter-web' 25 | } 26 | 27 | tasks.named('test') { 28 | useJUnitPlatform() 29 | } 30 | -------------------------------------------------------------------------------- /2장 동작 파라미터화 코드 전달하기/ch2/src/main/java/com/example/demo/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.controller; 2 | 3 | import com.example.demo.entity.User; 4 | import com.example.demo.service.UserFilter; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | 9 | import java.util.Arrays; 10 | import java.util.List; 11 | 12 | @RestController 13 | public class UserController { 14 | // 필터링 조건을 동적으로 전달해주세요 15 | @GetMapping("/filtered-users") 16 | public List getFilteredUsers() { 17 | List users = Arrays.asList( 18 | new User("Alice", 25, "Admin"), 19 | new User("Bob", 30, "User"), 20 | new User("Charlie", 22, "Moderator") 21 | ); 22 | 23 | // 동적 필터링 조건: 나이가 25 이상인 사용자 24 | 25 | return UserFilter.filterUsers(users, user -> user.getAge() >= 25); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /2장 동작 파라미터화 코드 전달하기/ch2/src/main/java/com/example/demo/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.entity; 2 | 3 | public class User { 4 | private String name; 5 | private int age; 6 | private String role; 7 | 8 | // 생성자, Getter 및 Setter 9 | public User(String name, int age, String role) { 10 | this.name = name; 11 | this.age = age; 12 | this.role = role; 13 | } 14 | 15 | public String getName() { 16 | return name; 17 | } 18 | 19 | public void setName(String name) { 20 | this.name = name; 21 | } 22 | 23 | public int getAge() { 24 | return age; 25 | } 26 | 27 | public void setAge(int age) { 28 | this.age = age; 29 | } 30 | 31 | public String getRole() { 32 | return role; 33 | } 34 | 35 | public void setRole(String role) { 36 | this.role = role; 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | return "User{name='" + name + "', age=" + age + ", role='" + role + "'}"; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /4장 스트림 소개/ch4.md: -------------------------------------------------------------------------------- 1 | # 4.1 스트림이란 무엇인가? 2 | > **스트림** : 데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소 3 | - **연속된 요소** 4 | - 컬렉션과 마찬가지로 연속된 값 집합의 인터페이스 5 | - 컬렉션은 데이터, 자료구조, 시간/공간의 복잡성, 관련된 요소 저장, 접근 연산 6 | - 스트림은 표현계산식 7 | - **소스** 8 | - 컬렉션, 배열, I/O 자원 등의 데이터 제공 소스로부터 데이터를 소비한다. 9 | - 제공 소스와 같은 순서를 유지 10 | - **데이터 처리 연산** 11 | - 데이터베이스와 비슷한 연산 지원 12 | - 순차 실행, 병렬 실행 가능 13 | 14 | ## 스트림의 특징 15 | 1. **파이프라이닝** 16 | - 연산끼리 연결 17 | - 그 덕에 laziness, short-circuiting같은 최적화을 얻을 수 있음. 18 | 1. **내부 반복** 19 | - 데이터 표현과 하드웨어를 활용한 병렬성 구현을 자동으로 선택 20 | 1. 단 한 번만 소비 21 | - 한 번만 탐색할 수 있고, 탐색된 스트림의 요소는 소비된다. 22 | 23 | 24 | 25 | ## 스트림 API의 특징 26 | 1. **선언형** : 더 간결하고 가독성이 좋아진다. 27 | 1. **조립할 수 있음** : 유연성이 좋아진다. 28 | 1. **병렬화** : 성능이 좋아진다. 29 | 30 | 31 | ## 컬렉션과 스트림 32 | | 컬렉션 | 스트림 | 33 | | --- | --- | 34 | | DVD | 스트리밍 | 35 | | 모든 요소는 컬렉션에 추가하기 전에 계산 | 요청할 때만 요소를 계산 | 36 | | 생산자 중심 | 생산자와 소비자 관계 | 37 | | 적극적 생성 | 게으른 생성 | 38 | | 외부 반복 | 내부 반복 | 39 | 40 | ## 스트림 연산 41 | 1. 중간 연산 42 | - 연결할 수 있는 스트림 연산 43 | - 다른 스트림을 반환 44 | - 최종 연산을 실행하기 전까지는 **아무 연산도 수행하지 않는다**. (lazy) 45 | 46 | 1. 최종 연산 47 | - 스트림을 닫는 연산 48 | - 주로 List, Integer, void 등 스트림 이외의 결과가 반환됨 49 | -------------------------------------------------------------------------------- /2장 동작 파라미터화 코드 전달하기/ch2/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 모던 자바 인 액션 2 | 3 | 도서 [모던 자바 인 액션](https://m.yes24.com/Goods/Detail/78660184) 리딩을 바탕으로 양질의 cheat sheet를 제작합니다. 4 | 5 | ## 🏄🏻‍♂️ Collaborators 6 | 7 | > 본 스터디는 우리FISA 4기 멤버로 구성되어 있습니다. 8 | > 9 | 10 | ## 🏊🏻‍♀️ Rules 11 | 12 | - `매주 2챕터씩` 정독 & 정리합니다. 13 | - 매주 `토요일 23:59`까지 주차별 담당 cheat-sheet PR을 생성합니다. 14 | - 챕터 담당자 외의 나머지 멤버들은 매주 `월요일 23:59` 까지 코멘트를 작성합니다. 15 | 16 | ## 💻 Git Convention Rule 17 | 18 | - **Commit** : `add : 챕터명 doc upload` 19 | - **Commit Message Title** : `add`, `fix`, `delete` 20 | - ex) `add : chapter01 doc upload` 21 | - **Pull Request** : `add : 챕터명 doc upload` 22 | - Commit Convention과 동일 + PR template에 맞게 작성 23 | 24 | ## 📎 Cheat Sheet 25 | 26 | | **Chapter** | **Title** | 27 | | --- | --- | 28 | | **CH.01** | [📚 자바 8, 9, 10, 11 : 무슨 일이 일어나고 있는가?](https://github.com/ryuseunghan/java-in-action-study/blob/main/1%EC%9E%A5%20%EC%9E%90%EB%B0%94%208%2C%209%2C%2010%2C%2011%20%20%EB%AC%B4%EC%8A%A8%20%EC%9D%BC%EC%9D%B4%20%EC%9D%BC%EC%96%B4%EB%82%98%EA%B3%A0%20%EC%9E%88%EB%8A%94%EA%B0%80/ch1.md)| 29 | | **CH.02** | [📚 동작 파라미터화 코드 전달하기](https://github.com/ryuseunghan/java-in-action-study/blob/main/2%EC%9E%A5%20%EB%8F%99%EC%9E%91%20%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0%ED%99%94%20%EC%BD%94%EB%93%9C%20%EC%A0%84%EB%8B%AC%ED%95%98%EA%B8%B0/ch2.md)| 30 | | **CH.03** | 📚 람다 표현식 | 31 | | **CH.04** | 📚 스트림 소개 | 32 | | **CH.05** | [📚 스트림 활용](https://github.com/ryuseunghan/java-in-action-study/blob/main/5%EC%9E%A5%20%EC%8A%A4%ED%8A%B8%EB%A6%BC%20%ED%99%9C%EC%9A%A9/ch5.md) | 33 | | **CH.06** | [📚 스트림으로 데이터 수집](https://github.com/ryuseunghan/java-in-action-study/blob/main/6%EC%9E%A5%20%EC%8A%A4%ED%8A%B8%EB%A6%BC%EC%9C%BC%EB%A1%9C%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%EC%88%98%EC%A7%91/ch6.md) | 34 | | **CH.07** | [📚 병렬 데이터 처리와 성능](https://github.com/ryuseunghan/java-in-action-study/blob/main/7%EC%9E%A5%20%EB%B3%91%EB%A0%AC%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%EC%B2%98%EB%A6%AC%EC%99%80%20%EC%84%B1%EB%8A%A5/ch7.md) | 35 | | **CH.08** | [📚 컬렉션 API 개선](https://github.com/ryuseunghan/java-in-action-study/blob/main/8%EC%9E%A5%20%EC%BB%AC%EB%A0%89%EC%85%98%20API%20%EA%B0%9C%EC%84%A0/ch8.md) | 36 | | **CH.09** | [📚 리팩터링, 테스팅, 디버깅](https://github.com/ryuseunghan/java-in-action-study/blob/main/9%EC%9E%A5%20%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%2C%20%ED%85%8C%EC%8A%A4%ED%8C%85%2C%20%EB%94%94%EB%B2%84%EA%B9%85/ch9.md) | 37 | | **CH.10** | [📚 람다를 이용한 도메인 전용 언어](https://github.com/ryuseunghan/java-in-action-study/blob/main/10%EC%9E%A5%20%EB%9E%8C%EB%8B%A4%EB%A5%BC%20%EC%9D%B4%EC%9A%A9%ED%95%9C%20%EB%8F%84%EB%A9%94%EC%9D%B8%20%EC%A0%84%EC%9A%A9%20%EC%96%B8%EC%96%B4/ch10.md) | 38 | | **CH.11** | [📚 null 대신 Optional 클래스](https://github.com/ryuseunghan/java-in-action-study/blob/main/11%EC%9E%A5%20null%20%EB%8C%80%EC%8B%A0%20Optional%20%ED%81%B4%EB%9E%98%EC%8A%A4/ch11.md) | 39 | | **CH.12** | [📚 새로운 날짜와 시간 API](https://github.com/ryuseunghan/java-in-action-study/blob/main/12%EC%9E%A5%20%EC%83%88%EB%A1%9C%EC%9A%B4%20%EB%82%A0%EC%A7%9C%EC%99%80%20%EC%8B%9C%EA%B0%84%20API/ch12.md) | 40 | | **CH.13** | [📚 디폴트 메서드](https://github.com/ryuseunghan/java-in-action-study/blob/main/13%EC%9E%A5%20%EB%94%94%ED%8F%B4%ED%8A%B8%20%EB%A9%94%EC%84%9C%EB%93%9C/ch13.md) | 41 | | **CH.14** | [📚 자바 모듈 시스템](https://github.com/ryuseunghan/java-in-action-study/blob/main/14%EC%9E%A5%20%EC%9E%90%EB%B0%94%20%EB%AA%A8%EB%93%88%20%EC%8B%9C%EC%8A%A4%ED%85%9C/ch14.md) | 42 | | **CH.15** | [📚 CompletableFuture와 리액티브 프로그래밍 컨셉의 기초](https://github.com/ryuseunghan/java-in-action-study/blob/main/15%EC%9E%A5%20CompletableFuture%EC%99%80%20%EB%A6%AC%EC%95%A1%ED%8B%B0%EB%B8%8C%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%20%EC%BB%A8%EC%85%89%EC%9D%98%20%EA%B8%B0%EC%B4%88/ch15.md) | 43 | | **CH.16** | 📚 [CompletableFuture : 안정적 비동기 프로그래밍](https://github.com/ryuseunghan/java-in-action-study/blob/main/16%EC%9E%A5%20CompletableFuture%20%3A%20%EC%95%88%EC%A0%95%EC%A0%81%20%EB%B9%84%EB%8F%99%EA%B8%B0%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D/ch16.md) | 44 | | **CH.17** | 📚 [리액티브 프로그래밍](https://github.com/ryuseunghan/java-in-action-study/blob/main/17%EC%9E%A5%20%EB%A6%AC%EC%95%A1%ED%8B%B0%EB%B8%8C%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D/ch17.md) | 45 | | **CH.18** | 📚 [함수형 관점으로 생각하기](https://github.com/ryuseunghan/java-in-action-study/blob/main/18%EC%9E%A5%20%ED%95%A8%EC%88%98%ED%98%95%20%EA%B4%80%EC%A0%90%EC%9C%BC%EB%A1%9C%20%EC%83%9D%EA%B0%81%ED%95%98%EA%B8%B0/ch18.md) | 46 | | **CH.19** | 📚 [함수형 프로그래밍 기법](https://github.com/ryuseunghan/java-in-action-study/blob/main/19%EC%9E%A5%20%ED%95%A8%EC%88%98%ED%98%95%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%20%EA%B8%B0%EB%B2%95/ch19.md) | 47 | -------------------------------------------------------------------------------- /18장 함수형 관점으로 생각하기/ch18.md: -------------------------------------------------------------------------------- 1 | # 18.1 시스템 구현과 유지보수 2 | 3 | ## 18.1.1 공유된 가변 데이터 4 | 5 | 변수가 예상하지 못한 값을 갖는 이유는 공유된 가변 데이터 구조를 읽고 갱신하기 때문 6 | 7 | ![image](https://github.com/user-attachments/assets/cd0c1678-0ae4-4547-837e-c77503e87dd8) 8 | 9 | 이러한 문제는 **순수 메서드** 또는 **불변 객체**를 이용해 해결 가능 10 | 11 | **순수 메서드** 란? 12 | 13 | 다른 객체의 상태를 바꾸지 않으며 `return`문을 통해서만 자신의 결과를 반환하는 메서드, **부작용**이 없는 메서드 14 | 15 | **부작용**이란? 16 | 17 | - 자료구조를 고치거나 필드에 값을 할당(setter 메서드 같은 생성자 이외의 초기화 동작) 18 | - 예외 발생 19 | - 파일에 쓰기 등의 I/O 동작 수행 20 | 21 | --- 22 | 23 | ## 18.1.2 선언형 프로그래밍 24 | 25 | 프로그래밍 패러다임의 두 가지 방식: **명령형 프로그래밍**과 **선언형 프로그래밍** 26 | 27 | - **명령형 프로그래밍**은 *어떻게(how)* 문제를 해결할지에 초점, 개발자가 **절차와 흐름**을 일일이 명시해야 하며, 반복문, 조건문 등을 사용해 로직을 직접 제어 28 | - **선언형 프로그래밍**은 *무엇을(what)* 해야 하는지에 집중, **문제의 결과에만 집중**하고, 그 결과를 얻기 위한 구체적인 수행 방식은 추상화됩니다. 29 | 30 | 명령형 방식으로 가장 비싼 거래(`Transaction`)를 찾는 코드 31 | 32 | ```java 33 | Transaction mostExpensive = transactions.get(0); 34 | if (mostExpensive == null) 35 | throw new IllegalArgumentException("Empty list of transactions"); 36 | 37 | for (Transaction t : transactions.subList(1, transactions.size())) { 38 | if (t.getValue() > mostExpensive.getValue()) { 39 | mostExpensive = t; 40 | } 41 | } 42 | 43 | ``` 44 | 45 | 반복문을 사용하여 **어떻게 거래를 순회하고, 비교하고, 갱신할지를 직접 명시** 46 | 47 | --- 48 | 49 | ## 18.1.3 왜 함수형 프로그래밍인가? 50 | 51 | - 계산을 **수학적 함수의 조합**으로 표현하며, 52 | - 상태를 변경하지 않고, 53 | - **부작용(side effect)이 없는 연산**을 지향 54 | 55 | 함수형 프로그래밍에서는 데이터를 처리하는 방식이 다음처럼 간결해짐: 56 | 57 | ```java 58 | Transaction mostExpensive = 59 | transactions.stream() 60 | .max(Comparator.comparing(Transaction::getValue)) 61 | .orElseThrow(() -> new IllegalArgumentException("Empty list")); 62 | 63 | ``` 64 | 65 | 이 방식은 **무엇을 하고 싶은지(가장 값이 큰 거래를 찾는다)**에 집중할 수 있도록 도와주며, 코드가 더 **간결하고 읽기 쉬우며, 유지보수가 쉬운 장점** 66 | 67 | # 18.2 함수형 프로그래밍이 무엇인가? 68 | 69 | 함수형 프로그래밍에서 **함수**란 수학적인 함수와 같아, **부작용이 없음** 70 | 71 | ![image](https://github.com/user-attachments/assets/c6cac84a-0725-45b5-9a31-c1eb30384ed0) 72 | 73 | ![image](https://github.com/user-attachments/assets/9928260a-6542-43a5-8420-85ec4cfb052b) 74 | 75 | 함수형 프로그래밍은 **순수 함수형 프로그래밍**과 **함수형 프로그래밍**으로 나뉨 76 | 77 | **순수 함수형 프로그래밍** 78 | 79 | - if-then-else 등의 수학적 표현만 사용하는 방식 80 | 81 | **함수형 프로그래밍** 82 | 83 | - 시스템의 다른 부분에 영향을 미치지 않는다면 내부적으로는 함수형이 아닌 기능도 사용할 수 있는 방식 84 | 85 | ## 18.2.1 함수형 자바 86 | 87 | 자바는 완전한 **순수 함수형 언어**는 아니지만, 함수형 프로그래밍 스타일을 구현하는 것이 **충분히 가능하다** 88 | 89 | - 자바에서는 **부작용이 없는 함수**와 **불변 객체**를 활용하여 함수형 프로그래밍을 실현할 수 있음 90 | - 함수형이라 불릴 수 있는 함수 또는 메서드는: 91 | - **지역 변수만 변경**해야 하며 92 | - 참조하는 **객체는 불변 객체**여야 하고 93 | - **예외를 발생시키지 않아야 하며** 94 | - **같은 입력에 대해 항상 같은 출력을 반환** 95 | - 예외 대신 `Optional`을 사용하면 **안전하게 결과를 표현 가능** 96 | - 라이브러리 함수를 사용할 때는 **부작용이 발생하지 않는 경우에만 사용**해야 하며, 97 | - 부득이한 경우, **주석이나 마커 어노테이션**으로 해당 메서드가 부작용을 가질 수 있음을 명시하는 것이 바람직함 98 | - 참고: 디버깅 출력을 포함한 I/O는 함수형 규칙에서 벗어나지만, **로깅을 제외하고는 부작용 없이 구현 가능**한 함수형 프로그래밍의 장점을 유지 가능 99 | 100 | --- 101 | 102 | ## 18.2.2 참조 투명성 (Referential Transparency) 103 | 104 | **참조 투명성**은 같은 인수로 함수를 호출했을 때, 항상 **동일한 결과를 반환**하는 특성을 의미 105 | 106 | - 참조 투명한 함수는 내부 상태나 외부 환경에 의존하지 않고, **순수하게 입력만으로 결과를 도출** 107 | - 이 개념은 **부작용을 제거해야 한다는 원칙으로 이어짐** 108 | 109 | 예: 110 | 111 | ```java 112 | final int a = 5; 113 | final int b = a * 2; // 항상 같은 결과 10 114 | 115 | ``` 116 | 117 | - 참조 투명한 함수의 예시지만, 아래와 같은 경우에는 문제가 될 수 있습니다: 118 | 119 | ```java 120 | List list1 = createList(); 121 | List list2 = createList(); 122 | ``` 123 | 124 | - `list1.equals(list2)`는 true일 수 있지만, **메모리 상의 참조는 다르므로** 참조적으로 완전히 투명하다고 보긴 어려움 125 | - 그러나 일반적으로 **동일한 결과를 반환하는 메서드는 참조 투명한 것으로 간주** 126 | 127 | --- 128 | 129 | ## 18.2.3 객체지향 프로그래밍과 함수형 프로그래밍 130 | 131 | 자바 8 이후부터는 객체지향 프로그래밍(OOP)과 함수형 프로그래밍(FP)이 **혼합되어 사용되는 경향**이 강해짐 132 | 133 | - 해당 절에서는 두 프로그래밍 패러다임을 비교하며, 자바 개발자들이 어떻게 이 두 접근법을 혼합해서 사용하고 있는지를 설명 134 | - 특히 자바에서는 하드웨어 병렬 처리 성능, 데이터 조작 성능에 대한 요구가 증가함에 따라 **함수형 프로그래밍 스타일이 점차 확대 중** 135 | 136 | 따라서 앞으로 137 | - 자바 개발자가 OOP와 FP의 **장점을 조합해 모듈성과 성능을 모두 확보**할 수 있도록 하는 방법을 제시하며 138 | - 19장에서는 FP 스타일의 특징과 **모듈성**, **병렬 처리**, **불변성**, **순수성**을 이용해 더욱 적합한 코드를 작성하는 방법을 다룸 139 | 140 | --- 141 | 142 | # 18.3 재귀와 반복 143 | 144 | **함수형 프로그래밍에서는 반복문 대신 재귀(recursion)를 선호** 145 | 146 | 반복문이 보통 내부 상태의 **변화**를 필요로 하기 때문 147 | 148 | ### 전통적인 반복문 예시 (명령형 방식): 149 | 150 | ```java 151 | Iterator it = apples.iterator(); 152 | while (it.hasNext()) { 153 | Apple apple = it.next(); 154 | // ... 155 | } 156 | 157 | ``` 158 | 159 | - 반복문 내부에서 상태가 변경될 수 있으며, 특히 다음과 같은 코드는 **함수형 원칙에 위배**될 수 있음 160 | 161 | ```java 162 | public void searchForGold(List l, Stats stats) { 163 | for (String s : l) { 164 | if ("gold".equals(s)) { 165 | stats.incrementFor("gold"); // 부작용 발생 166 | } 167 | } 168 | } 169 | ``` 170 | 171 | - 위처럼 `stats`를 변경하는 부작용은 **하스켈과 같은 순수 함수형 언어**에서는 금지되며, 이를 해결하려면 **재귀**를 활용하는 방식으로 구현 172 | 173 | --- 174 | 175 | ### 팩토리얼 예제: 반복 vs 재귀 vs 스트림 176 | 177 | ```java 178 | // 반복 방식 179 | static int factorialIterative(int n) { 180 | int r = 1; 181 | for (int i = 1; i <= n; i++) { 182 | r *= i; 183 | } 184 | return r; 185 | } 186 | 187 | // 재귀 방식 188 | static long factorialRecursive(long n) { 189 | return n == 1 ? 1 : n * factorialRecursive(n - 1); 190 | } 191 | 192 | // 스트림 기반 함수형 방식 193 | static long factorialStreams(long n) { 194 | return LongStream.rangeClosed(1, n) 195 | .reduce(1, (a, b) -> a * b); 196 | } 197 | 198 | ``` 199 | 200 | - **스트림 기반** 방식은 함수형 스타일로 작성된 것으로, **변수 변경 없이** 계산이 수행 201 | 202 | ![image](https://github.com/user-attachments/assets/99b1628a-a2e8-4cbe-bcd6-f5da30519387) 203 | 204 | ![image](https://github.com/user-attachments/assets/159ece23-05e3-44cf-8674-452435985fb2) 205 | 206 | --- 207 | 208 | ### 꼬리 재귀 (Tail Recursion) 209 | 210 | ```java 211 | static long factorialTailRecursive(long n) { 212 | return factorialHelper(1, n); 213 | } 214 | 215 | static long factorialHelper(long acc, long n) { 216 | return n == 1 ? acc : factorialHelper(acc * n, n - 1); 217 | } 218 | 219 | ``` 220 | 221 | - 꼬리 재귀는 **함수 호출이 끝날 때 다시 자기 자신만 호출하는 패턴**으로, 222 | 223 | 일부 함수형 언어에서는 **꼬리 호출 최적화(TCO)**를 통해 **스택 오버플로우 없이** 반복처럼 수행 가능 224 | 225 | 226 | 자바는 꼬리 재귀 최적화를 지원하지 않지만, 이러한 구조를 통해 함수형의 장점을 살릴 수 있음 227 | 228 | --- 229 | 230 | ## ✅ 요약 231 | 232 | | 항목 | 내용 | 233 | | --- | --- | 234 | | 함수형 자바 | 순수 함수, 불변성, 예외 제거, Optional 활용 | 235 | | 참조 투명성 | 같은 입력 → 항상 같은 결과, 부작용 없음 | 236 | | OOP vs FP | 자바 8 이후 두 패러다임을 혼합하는 추세 | 237 | | 재귀 vs 반복 | 함수형에서는 반복 대신 재귀 선호, 상태 변경을 줄이기 위해 | 238 | | 팩토리얼 구현 | 반복 → 재귀 → 스트림 → 꼬리 재귀까지 다양한 방식 비교 | 239 | | 꼬리 재귀 | 최적화를 통해 효율성 확보 (자바는 직접 지원 X) | 240 | -------------------------------------------------------------------------------- /2장 동작 파라미터화 코드 전달하기/ch2.md: -------------------------------------------------------------------------------- 1 | **변화하는 요구사항에 대응** 2 | 3 | **동작 파라미터화** 4 | 5 | **익명 클라스** 6 | 7 | **람다 표현식 미리보기** 8 | 9 | **실전 예제 : Comparator, Runnable, GUI** 10 | 11 | ### 2.0 OVERVIEW 12 | 13 | 변화하는 요구사항에 맞추어 유지보수가 쉽도록 하기 위해 **동작 파라미터화**가 등장하였다. 14 | 15 | 동작 파라미터화를 통해 코드의 동작을 **고정된 구현**이 아니라, **파라미터로 전달된 값이나 로직에 따라 동적으로 변화**하도록 설계할 수 있다. 16 | 17 | EX) 18 | 19 | - 리스트의 모든 요소에 대해 어떤 동작 수행 20 | - 리스트 관련 작업을 끝낸 다음 어떤 다른 동작 수행 21 | - 에러가 발생 시 정해진 어떤 다른 동작을 수행 22 | 23 | ### 2.1 변화하는 요구사항에 대응하기 24 | 25 | 1. **녹색사과 필터링** 26 | 27 | ```java 28 | public static List filterGreenApples(List inventory) { 29 | List result = new ArrayList<>(); 30 | for (Apple apple : inventory) { 31 | if (apple.getColor() == Color.GREEN) { 32 | result.add(apple); 33 | } 34 | } 35 | return result; 36 | } 37 | ``` 38 | 39 | → 다른 색상 필터링이 필요할 시 비슷한 코드가 복제됨 40 | 41 | → 해당 **코드를 추상화**하자 42 | 43 | 2. **색을 파라미터화** 44 | 45 | ```java 46 | public static List filterApplesByColor(List inventory, Color color) { 47 | List result = new ArrayList<>(); 48 | for (Apple apple : inventory) { 49 | if (apple.getColor() == color) { 50 | result.add(apple); 51 | } 52 | } 53 | return result; 54 | } 55 | ``` 56 | 57 | 변화하는 요구사항에 좀 더 유연하게 대응하는 코드를 만들 수 있음 58 | 59 | `List greenApples = filterApplesByColor(inventory, GREEN);` 60 | 61 | **무게**가 기준일 시 62 | 63 | ```java 64 | public static List filterApplesByWeight(List inventory, int weight) { 65 | List result = new ArrayList<>(); 66 | for (Apple apple : inventory) { 67 | if (apple.getWeight() > weight) { 68 | result.add(apple); 69 | } 70 | } 71 | return result; 72 | } 73 | ``` 74 | 75 | 이는 소프트웨어 공학의 **`DRY(don’t repeat yourself)`** 원칙을 지키기 위함. 하지만, 필터링 코드의 구현 코드에 중복이 많기에 **DRY 원칙을 위배**하고 있다. 76 | 77 | 3. **가능한 모든 속성으로 필터링**(좋지 않은 예시) 78 | 79 | ```java 80 | public static List filterApples(List inventory, Color color, int weight, boolean flag) { 81 | List result = new ArrayList<>(); 82 | for (Apple apple : inventory) { 83 | if ((flag && apple.getColor() == color)|| 84 | (!flag && apple.getWeight() > weight)) { 85 | result.add(apple); 86 | } 87 | } 88 | return result; 89 | } 90 | ``` 91 | 92 | 요구사항 변경에 유연하게 대응할 수 없으며 flag 파라미터의 의미가 모호함. 93 | 94 | 95 | ### 2.2 동작 파라미터화 96 | 97 | 선택 조건을 결정하는 인터페이스 **Predicate**을 정의하자 ( **전략 디자인 패턴** ) 98 | 99 | ```java 100 | interface ApplePredicate { 101 | 102 | boolean test(Apple a); 103 | 104 | } 105 | ``` 106 | 107 | ![image](https://github.com/user-attachments/assets/5c1f70a7-eeb6-49fa-a882-83a3a083edaf) 108 | 109 | 114 | 115 | *전략 디자인 패턴 관련 예시 추가하기* 116 | 117 | 4. **추상적 조건으로 필터링** 118 | 119 | **`ApplePredicate`**을 통해 필요에 따라 **`filterApples`**메서드로 전달 가능 🎉 120 | 121 | ```java 122 | public static List filterApplesByWeight(List inventory, ApplePredicate p) { 123 | List result = new ArrayList<>(); 124 | for (Apple apple : inventory) { 125 | if (p.test(apple)) { 126 | result.add(apple); 127 | } 128 | } 129 | return result; 130 | } 131 | ``` 132 | 133 | 메서드는 객체만 인수로 받으므로 **`test`**메서드를 **`ApplePredicate`**객체로 감싸서 전달해야한다. 134 | 135 | ![image](https://github.com/user-attachments/assets/ad6eae47-e62f-45f8-a132-7ec2a7bbeb26) 136 | 137 | ```java 138 | static class AppleWeightPredicate implements ApplePredicate { 139 | 140 | @Override 141 | public boolean test(Apple apple) { 142 | return apple.getWeight() > 150; 143 | } 144 | 145 | } 146 | 147 | static class AppleColorPredicate implements ApplePredicate { 148 | 149 | @Override 150 | public boolean test(Apple apple) { 151 | return apple.getColor() == Color.GREEN; 152 | } 153 | 154 | } 155 | 156 | static class AppleRedAndHeavyPredicate implements ApplePredicate { 157 | 158 | @Override 159 | public boolean test(Apple apple) { 160 | return apple.getColor() == Color.RED && apple.getWeight() > 150; 161 | } 162 | 163 | } 164 | ``` 165 | 166 | ### 2.3 복잡한 과정 간소화 - 익명 클래스 167 | 168 | **`ApplePredicate`** 인터페이스를 다양하게 사용하기 위해서는 여러 클래스를 정의한 다음에 인스턴스화해야한다. 이는 매우 복잡하기에 **익명 클래스**를 통해 간소화 해보자 169 | 170 | 176 | 177 | ```java 178 | interface MyInterfae { 179 | void doSomething(); 180 | } 181 | 182 | public class Example { 183 | 184 | interface MyInterface { 185 | void doSomething(); 186 | } 187 | 188 | public static void main(String[] args) { 189 | MyInterface myClass = new MyInterface() { 190 | @Override 191 | public void doSomething() { 192 | System.out.println("doSomething"); 193 | } 194 | }; 195 | 196 | myClass.doSomething(); 197 | } 198 | } 199 | 200 | ``` 201 | 202 | 여기서 인터페이스 자체는 실체화를 할 수 없으나 익명 함수에서는 클래스 이름이 없기 때문에 new를 통해 임시로 실체화하는 것이므로 헷갈리지 말자. 203 | 204 | 5. **익명 클래스 사용** 205 | 206 | ```java 207 | List redApples = filterApples(inventory, new ApplePredicate() { 208 | public boolean test(Apple apple) { 209 | return RED.equals(apple.getColor()); 210 | } 211 | }); 212 | ``` 213 | 214 | 익명 클래스는 매번 new를 통해 객체를 생성하며 코드가 길어져 불편함이 크다. 215 | 216 | 6. **람다 표현식** 217 | 218 | ```java 219 | List redApples = filterApples(inventory, 220 | (Apple apple) -> RED.equals(apple.getColor())); 221 | ``` 222 | 223 | ![image](https://github.com/user-attachments/assets/c28bb039-db84-447b-86e4-33a03357747e) 224 | 225 | 226 | 227 | 7. **리스트 형식으로 추상화** 228 | 229 | ```java 230 | public interface Predicate { 231 | boolean test(T t); 232 | } 233 | 234 | public static List filter(List list, Predicate p){ 235 | List result = new ArrayList<>(); 236 | for(T e: list){ 237 | if(p.test(e)) { 238 | result.add(e); 239 | } 240 | } 241 | return result; 242 | } 243 | 244 | List redApples = filter(inventory, (Apple apple) -> RED.equals(apple.getColor()); 245 | ``` 246 | 247 | 248 | ### 2.4 실전 예제 249 | 250 | 1. **Comparator** 로 정렬하기 251 | 252 | ```java 253 | inventory.sort(new Comparator() { 254 | public int compare(Apple o1, Apple o2) { 255 | return o1.getWeight().compareTo(o2.getWeight()); 256 | } 257 | }); 258 | inventory.sort((Apple o1, Apple o2) -> o1.getWeight().compareTo(o2.getWeight())); 259 | ``` 260 | 261 | 2. **Runnable**로 코드 블록 실행하기 262 | 263 | 273 | 274 | ```java 275 | Thread t = new Thread(new Runnable() { 276 | @Override 277 | public void run() { 278 | System.out.println("Hello world"); 279 | } 280 | }); 281 | Thread t = new Thread(()-> System.out.println("Hello world")); 282 | ``` 283 | 284 | 3. **Callable**을 결과로 반환하기 285 | 286 | ```java 287 | ExecutorService executorServce = Executors.newCachedThreadPool(); 288 | Future threadName = executorServce.submit(new Callable() { 289 | @Override 290 | public String call() throws Exception { 291 | return Thread.currentThread().getName(); 292 | } 293 | }); 294 | ``` 295 | 296 | 297 | ### 문제 - 확장: 필터링 조건을 동적으로 전달하기 298 | 299 | **요청 예시:** 300 | 301 | 1. `GET /filtered-users?minAge=25` 302 | - 결과: 25세 이상 사용자 303 | 2. `GET /filtered-users?role=Admin` 304 | - 결과: 역할이 Admin인 사용자 305 | 3. `GET /filtered-users?minAge=25&role=Admin` 306 | - 결과: 25세 이상이면서 역할이 Admin인 사용자 307 | -------------------------------------------------------------------------------- /11장 null 대신 Optional 클래스/ch11.md: -------------------------------------------------------------------------------- 1 | ## 11.1 값이 없는 상황을 어떻게 처리할까? 2 | 3 | 객체가 객체를 가지고 있는 중첩 구조에서는 nullPointException 이 발생할 수 있다. 4 | 5 | ### 11.1.1 보수적인 자세로 NullPointerException 줄이기 6 | 7 | **예제 11-2** 8 | 9 | ```java 10 | public String getCarInsuranceName (Person person) { 11 | if (person != null) { 12 | Car car = person.getCar(); 13 | if (car != null) { 14 | Insurance insurance = car.getInsurance(); 15 | if (insurance != null) { 16 | return insurance.getName(); 17 | } 18 | } 19 | } 20 | return "Unknown"; 21 | } 22 | ``` 23 | 24 | 모든 변수가 null 인지 의심하며 변수에 접근할 떄마다 중첩된 if문이 추가되며 들여쓰기 수준이 증가한다. 25 | 26 | 이와 같은 반복 패턴 코드를 `깊은 의심` 이라 한다. 27 | 28 | 이는 코드의 구조가 엉망이 되고 가독성이 떨어진다는 단점이 있다. 29 | 30 | ```java 31 | public String getCarInsuranceName (Person person) { 32 | if(Person == null) { 33 | return "Unknown"; 34 | } 35 | Car car = person.getCar(); 36 | if(car == null) { 37 | return "Unknown"; 38 | } 39 | Insurance insurance = car.getInsurance(); 40 | if(insurance == null) { 41 | return "Unknown"; 42 | } 43 | return insurance.getName(); 44 | } 45 | ``` 46 | 47 | null 변수가 있다면 즉시 ‘UnKnown’ 을 반환하며 중첩 if 블록을 없애버린다면 어떻게 될까? 48 | 49 | 네 개의 출구에 대한 유지보수 문제도 존재하며 ‘UnKnown’ 문자열 반복이라는 문제 또한 존재한다. 50 | 51 | (다만 문자열 반복에 대해서는 문자열 상수를 통해 해결할 수 있다.) 52 | 53 | ### 11.1.2 null 떄문에 발생하는 문제 54 | 55 | 자바에서 null 참조를 사용하면서 발생할 수 있는 문제 56 | 57 | - 에러의 근원 58 | - 코드를 어지럽힌다. 59 | - 아무 의미가 없다. 60 | - 자바 철학에 위배된다. 61 | - 형식 시스템에 구멍을 만든다. 62 | 63 | ### 11.1.3 다른 언어는 null 대신 무얼 사용하나? 64 | 65 | - 그루비 : 안전 내비게이션 연산자 66 | - 하스켈 : 선택형 값을 저장할 수 있는 Maybe라는 형식을 제공 67 | - 스칼라 : Optional[ T ] 68 | 69 | ## 11.2 Optional 클래스 소개 70 | 71 | 자바 8은 하스켈과 스칼라의 영향을 받아 java.util.Optional 라는 새로운 클래스를 제공한다. 72 | 73 | Optional 클래스를 통해 ~되어 있을 수도 아닐 수도 있음을 명확하게 설명 가능하다. 74 | 75 | Optional을 사용하면 값이 없을 수 있는 상황에 적절하게 대응하도록 강제할 수 있지만, 모든 null 참조를 Optional로 대치하는 것은 바람직하지 않다. 76 | 77 | ## 11.3 Optional 적용 패턴 78 | 79 | ### 11.3.1 Optional 객체 만들기 80 | 81 | - 빈 Optional 82 | - Optional.empty() 83 | - null이 아닌 값으로 Optional 만들기 84 | - 정적 팩토리 메서드 Optional.of를 활용하여 생성 가능 85 | - null 값으로 Optional 만들기 86 | - 정적 팩토리 메서드 Optional.ofNullable를 활용하여 생성 가능 87 | 88 | ### 11.3.2 맵으로 Optional의 값을 추출하고 변환하기 89 | 90 | ```java 91 | //이름 정보에 접근하기전에 insurance가 null인지 확인 92 | String name = null; 93 | if(insurance != null) { 94 | name = insurance.getName(); 95 | } 96 | // Optional이 비어있다면 아무 일도 일어나지 않는다. 97 | // 이를 통해 안전하게 메소드 호출이 가능하다. 98 | Optional optInsurance = Optional.ofNullable(insurance); 99 | Optional name = optInsurance.map(Insurance::getName); 100 | ``` 101 | 102 | ![Image](https://github.com/user-attachments/assets/199f0f8b-0087-4bbd-aeca-4c92b8a24d64) 103 | 104 | ### 11.3.3 flatMap으로 Optional 객체 연결 105 | 106 | Optional 객체를 만들고 map메소드를 호출하게 된다면 반환되는 타입은 Optional> 이다. 이는 이전에 배웠던 flatMap을 통해 평준화가 가능하며 flatMap을 통하여 Optional 의 형식으로 반환 타입을 지정해줄 수 있다. 107 | 108 | **예제 11-5** Optional로 자동차의 보험회사 이름 찾기 109 | 110 | ```java 111 | public String getCarInsuranceName(Optional person) { 112 | return person.flatMap(Person::getCar) 113 | .flatMap(Car::getInsurance) 114 | .map(Insurance::getName) 115 | .orElse("Unknown"); 116 | // 결과 Optional이 비어있으면 기본값 사용 117 | } 118 | ``` 119 | 120 | 이전 예제 11-2와 비교해본다면 Optional과 Stream API의 사용으로 인한 코드 간결화와 가독성 향상을 엿볼 수 있다. 121 | flatMap을 두 번 사용하는 이유는 Optional와 Optional가 중첩되는 것을 방지하여, 최종적으로 Optional을 얻기 위함이다. 122 | 123 | ### 도메인 모델에 Optional을 사용했을 때 데이터를 직렬화할 수 없는 이유 124 | 125 | Optional 클래스는 필드 형식으로 사용할 것을 가정하지 않았으므로 Serializable 인터페이스를 구현하지 않는다. 126 | 127 | 따라서 직렬화 모델이 필요하다면 해당 클래스 내부에 Optional로 값을 반환받을 수 있는 메서드를 추가하는 방식을 권장한다. 128 | 129 | - 이는 직렬화 과정에서 `Optional`을 사용하면 객체를 변환하거나 저장할 때, 값이 없을 수도 있는 필드가 있을 수 있기 때문이다. 이때 해당 클래스가 `Optional` 타입을 잘 처리할 수 있도록 `Optional`을 반환하는 메서드를 추가하자는 의미이다. 130 | 131 | ### 11.3.4 Optional 스트림 조작 132 | 133 | ```java 134 | public Set getCarInsuranceNames(List persons) { 135 | return persons.stream() 136 | // 사람 목록을 각 사람이 보유한 자동차에 대한 Optional 스트림으로 변환 137 | .map(Person::getCar) 138 | // 각 Optional를 해당하는 Optional로 변환 (Car가 있으면 그에 해당하는 보험 정보로 매핑) 139 | .map(optCar -> optCar.flatMap(Car::getInsurance)) 140 | // Optional를 Optional으로 변환 (보험이 있으면 그 보험 이름을 가져옴) 141 | .map(optIns -> optIns.map(Insurance::getName)) 142 | // Stream>을 Stream으로 변환 (Optional이 비어 있지 않으면 그 값을 스트림에 포함) 143 | .flatMap(Optional::stream) 144 | // 중복되지 않는 이름들을 Set에 모은 후 반환 145 | .collect(toSet()); 146 | } 147 | ``` 148 | 149 | Optional의 Stream 메서드를 이용하여 한 번의 연산으로 같은 결과를 얻을 수 있다. 150 | 151 | ```java 152 | Stream> stream = ... // 어떤 스트림이든 153 | Set result = stream.filter(Optional::isPresent) // Optional이 비어 있지 않은 경우에만 필터링 154 | .map(Optional::get) // Optional에서 실제 값 꺼내기 155 | .collect(toSet()); // Set으로 수집 156 | ``` 157 | 158 | ### 11.3.5 디폴트 액션과 Optional 언랩 159 | 160 | 1. get( ) 161 | 1. 값을 읽는 가장 간단한 메서드면서 동시에 가장 안전하지 않은 메서드 162 | 2. 래핑된 값이 있다면 해당 값을 반환, 값이 존재하지 않을 경우 NoSuchElementException을 발생시키킨다. 따라서 Optional에 값이 반드시 있다고 가정할 수 있는 상황이 아니라면 get메소드를 사용하지 않는 편이 좋다. 163 | 2. orElse(T other) 164 | 1. Optional이 값을 포함하지 않을 때 기본값을 제공한다. 165 | 3. orElseGet(Supplier other) 166 | 1. orElse의 Lazy 버전 167 | 2. Optional에 값이 없을 떄만 Supplier 메소드가 실행 168 | 3. 디폴트 메서드를 만드는데 시간이 걸리거나 Optional이 비어있을 떄만 기본값을 생성하고 싶다면 사용해야 한다. 169 | 4. orElseThrow(Supplier exceptionSupplier) 170 | 1. Optional이 비어있을 때 예외를 발생시킨다. 171 | 2. 단 get 메서드와 다르게 예외의 종류를 선택할 수 있다. 172 | 5. ifPresent(Consumer consumer) 173 | 1. 값이 존재할 때 인수로 넘겨준 동작을 실행할 수 있다. 단 값이 없다면 아무 일도 일어나지 않는다. 174 | 6. ifPresentOrElse(Consumer action, Runnable emptyAction) - 자바9에서 추가 175 | 1. Optional이 비었을 때 실행할 수 있는 Runnable을 인수로 받는다. 176 | 177 | ### 11.3.6 두 Optional 합치기 178 | 179 | ```java 180 | public Insurance findCheapestInsurance(Person person, Car car) { 181 | // 보험회사가 제공하는 서비스 조회 182 | // 모든 결과 데이터 비교 183 | return cheapestCompany; 184 | } 185 | ``` 186 | 187 | 두 Optional을 인수로 받아 Optional 를 반환하는 null-safe 메서드를 구현하며 인수로 전달한 값 중 하나라도 비어있으면 빈 Optional 를 반환해야한다. 188 | 189 | ```java 190 | // 첫 번째 구현은 isPresent()를 사용하여 명시적으로 체크하기 때문에 기존 null 체크 코드와 유사하다. 191 | public Optional nullSafeFindCheapestInsurance( 192 | Optional person, Optional car) { 193 | if(person.isPresent() && car.isPresent()){ 194 | return Optional.of(findCheapestInsurance(person.get(), car.get())); 195 | } else { 196 | return Optional.empty(); 197 | } 198 | } 199 | 200 | // 반면, map과 flatMap을 활용하면 더 함수형 스타일로 변환할 수 있으며, 중첩된 Optional을 방지하여 코드가 더 간결해진다. 201 | 202 | public Optional nullSafeFindCheapestInsurance( 203 | Optional person, Optional car) { 204 | return person.flatMap(p -> car.map(c -> findCheapestInsurance(p,c))); 205 | } 206 | ``` 207 | 208 | ### 11.3.7 필터로 특정값 거르기 209 | 210 | ```java 211 | // 조건문을 통한 null 체크 212 | Insurance insurance = ...; 213 | if(insurance != null && "CambridgeInsurance".equal(insurance.getName())) { 214 | System.out.println("ok"); 215 | } 216 | 217 | // Optional 객체에 filter 메서드 사용 218 | Optional optInsurance = ...; 219 | optInsurance.filter(ins -> "CambridgeInsurance".equals(insurance.getName())) 220 | .ifPresent(x -> System.out.println("ok");); 221 | ``` 222 | 223 | ## 11.4 Optional을 사용한 실용 예제 224 | 225 | ### 11.4.1 잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기 226 | 227 | Map의 get 메소드 사용 시 null 값에 대한 처리 228 | 229 | - 아래 코드로 null일 수도 있는 값을 Optional을 통해 안전하게 변환 가능하다. 230 | 231 | ```java 232 | Optional value = Optional.ofNullable(map.get("key")); 233 | ``` 234 | 235 | ### 11.4.2 예외와 Optional 클래스 236 | 237 | ```java 238 | public static Optional stringToInt(String s) { 239 | try{ 240 | return Optional.of(Integer.parseInt(s)); 241 | } catch (NumberFormatException e) { 242 | return Optional.empty(); // 예외 발생시 빈 Optional 반환 243 | } 244 | } 245 | ``` 246 | 247 | 위 코드와 같이 try-catch 문에서 Optional을 활용하여 더 안전하며 함수형 스타일로 처리가 가능하다. 248 | 249 | ### 11.4.3 기본형 Optional을 사용하지 말아야 하는 이유 250 | 251 | 스트림처럼 Optional도 기본형 Optional(ex. OptionalInt)이 있다. 252 | 253 | 하지만 Optional의 최대 요소 수는 한 개이므로 스트림이 많은 요소를 가질 떄 기본형 특화 스트림을 통해 성능 개선을 한 방식을 기본형 특화 Optional에서 수행하여도 성능 개선을 기대하기 힘들다. 254 | 255 | 또한 기본형 특화 Optional에는 map과 flatMap, filter등의 유틸리티 메서드가 존재하지 않는 등 장점보단 단점이 많다. 256 | -------------------------------------------------------------------------------- /6장 스트림으로 데이터 수집/ch6.md: -------------------------------------------------------------------------------- 1 | ## 6.1 컬렉터란 무엇인가? 2 | 3 | ```java 4 | collect(Collectors.toList()); 이때 Collectors 인터페이스를 컬렉터라 한다. 5 | ``` 6 | 7 | 컬렉터를 사용하여 스트림의 데이터를 리스트나 셋, 맵 등의 자료구조로 변환이 가능하다. 8 | 9 | Collector 인터페이스의 구현을 통해 종단연산인 `.collect()` 에서 어떻게 리듀싱을 할 것인가에 대하여 결정해준다. 10 | 11 | ### 6.1.1 고급 리듀싱 기능을 수행하는 컬렉터 12 | 13 | 스트림에서 collect를 호출하면 스트림의 요소(컬렉터로 파라미터화된)에 리듀싱 연산이 수행된다. 14 | 15 | toList는 스트림의 모든 요소를 List형태로 수집한다. 16 | 17 | ### 6.1.2 미리 정의된 컬렉터 18 | 19 | Collectors에서 제공하는 메서드의 기능 3가지 20 | 21 | - 스트림의 요소를 하나의 값으로 리듀스하고 요약 22 | - 요소 그룹화 23 | - 요소 분할 24 | - 분할은 프레디케이트를 그룹화 함수로 사용한다. 25 | 26 | ## 6.2 리듀싱과 요약 27 | 28 | `Collectors.counting()`으로 스트림의 개수를 체크할 수 있다. 29 | 30 | ### 6.2.1 스트림값에서 최댓값과 최솟값 검색 31 | 32 | `Collectors.maxBy(Comparator comparator)`는 인수로 비교 및 정렬에 대한 기준을 제시하는 Comparator를 받으며, 인수로 받은 Comparator를 기준으로 하여 최대값을 가지는 객체를 리턴한다. 33 | 34 | 이때 최대값이 없을 수 있기 때문에 Optional타입으로 값을 반환한다. 35 | 36 | ```java 37 | // 칼로리를 비교할 Comparator 구현 38 | Comparator dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories); 39 | 40 | // Collectors.maxBy를 통하여 구현한 Comparator 전달 41 | Optional maxCalorieDish = Dish.menu.stream().collect(maxBy(dishCaloriesComparator)); 42 | ``` 43 | 44 | 스트림에 있는 객체의 숫자 필드의 합계, 평균 등을 구하는 연산에도 리듀싱 기능이 사용되는데, 이러한 연산을 **요약 연산**이라 한다. 45 | 46 | ### 6.2.2 요약 연산 47 | 48 | Collectors 클래스는 Collectors.summingInt라는 특별한 요약 팩토리 메서드를 제공한다. 49 | 50 | 아래의 함수들은 Int뿐 아니라 Double과 Long 또한 가지고 있다. 51 | 52 | - 합 계산 53 | 54 | `Collectors.summingInt(ToIntFunction)` 는 스트림에 있는 객체를 int 타입으로 매핑하는 함수를 인수로 받습니다. 55 | 56 | ```java 57 | int total = menu.stream().collect(summingInt(Dish::getCalories)); 58 | // summingInt는 int를 리턴하는 getCalories 함수를 인수로 가집니다 59 | ``` 60 | 61 | - 평균 계산 62 | 63 | `Collectors.averagingInt(ToIntFunction)` 는 스트림에 있는 객체를 int 타입으로 매핑하는 함수를 인수로 받습니다. 64 | 65 | ```java 66 | int total = menu.stream().collect(averagingInt(Dish::getCalories)); 67 | // averagingInt는 int를 리턴하는 getCalories 함수를 인수로 가집니다 68 | ``` 69 | 70 | - 하나의 요약 연산으로 모든 정보를 보기 71 | 72 | count, max, min, average, sum의 모든 정보를 한번에 보기 위해서는 73 | 74 | `Collectors.summarizingInt(ToIntFunction)` 를 활용하여 모든 정보가 담긴 객체를 IntSummaryStatistics타입의 객체로 받을 수 있습니다. 75 | 76 | ### 6.2.3 문자열 연결 77 | 78 | 컬렉터에 joining 팩토리 메서드 패턴을 이용하면 스트림의 각 객체에 toString함수를 적용해 모든 객체의 toString을 하나의 문자열로 연결하여 반환한다. 이때 내부적으로 StringBuilder를 사용한다. 79 | 80 | ```java 81 | String showMenu = menu.stream().map(Dish::getName).collect(joining()); 82 | ``` 83 | 84 | 만약 Dish 클래스에 toString메소드를 포함하고 있을 경우에는 상단의 map과정을 생략해줄 수 있다. 85 | 86 | 구분자를 적용해주고 싶다면 아래 코드와 같이 구분자를 넣어줄 수 있다. 87 | 88 | ```java 89 | String shorMenu = menu.stream().map(Dish::getName).collect(joining(", ")); 90 | ``` 91 | 92 | ### 6.2.4 범용 리듀싱 요약 연산 93 | 94 | 앞에서 살펴본 모든 컬렉터는 사실 reducing 팩토리 메서드로 정의할 수 있다. 즉 범용 컬렉터인 `Collectors.reducing`으로도 구현이 가능하다는 소리이다. 95 | 96 | ## 🔹 **세 가지 `reducing()` 오버로드 방식** 97 | 98 | | 메서드 | 설명 | 반환 타입 | 99 | | --- | --- | --- | 100 | | `reducing(BinaryOperator)` | 스트림 요소를 하나로 줄이는 `reduce`와 동일 | `Optional` | 101 | | `reducing(T identity, BinaryOperator)` | `identity` 값을 기준으로 reduce 수행 | `T` | 102 | | `reducing(T identity, Function mapper, BinaryOperator)` | `mapper`로 변환 후 reduce 수행 | `U` | 103 | 104 | ### 컬렉션 프레임워크 유연성 : 같은 연산도 다양한 방식으로 수행 가능 105 | 106 | counting 컬렉터도 생각해본다면 reducing을 팩토리 메서드로 구현이 가능하다. 107 | 108 | ```java 109 | collect(reducing(0L, t -> 1L, Long::sum); 110 | 시작 0, 모든 요소들을 1이라는 카운팅 숫자로 변환 -> 이후 SUM함수를 통해 모두 더해준다면 111 | 동일한 역할을 수행한다. 112 | ``` 113 | 114 | ### 자신의 상황에 맞는 것을 사용하자! 115 | 116 | 스트림 인터페이스에서 제공하는 함수와 컬렉터를 통하여 계산을 구현하는 것은 난이도에서 분명 차이가 존재한다. 하지만 코드가 복잡하다는 것은 높은 수준의 추상화와 일반화를 통하여 재사용성을 크게 높여줄 수 있다는 것을 의미하기 떄문에 무엇이 더 좋다고 말할 순 없다. 117 | 118 | ## 6.3 그룹화 119 | 120 | 팩토리 메서드 Collectors.groupingBy 를 이용해 데이터베이스의 group By와 같은 효과를 얻을 수 있다. 121 | 122 | groupingBy 연산은 특성상 Map의 형태를 가지므로 반환타입은 Map형식이 된다. 123 | 124 | groupingBy 가 인수로 가지는 함수에 의하여 그룹핑이 결정되므로 이 함수를 **분류함수**라 한다. 125 | 126 | ```java 127 | // if-else를 통해서 그룹핑을 보다 상세하게 해주려면 아래와 같이 해줄 수 있다. 128 | menu.stream().collect(groupingBy(dish -> { 129 | if(dish.getCalories > 100) return MyEnum.Level1; 130 | else return MyEnum.Level2; 131 | })); 132 | ``` 133 | 134 | ### 6.3.1 그룹화된 요소 조작 135 | 136 | **각각의 음식 종류**에 대해서 특정 칼로리 이상인 애들을 **그룹핑**하려면 어떻게 할까? 이때 filtering메소드를 사용한다. 137 | 138 | filtering메소드는 또 다른 정적 팩토리 메소드로써 프레디케이트를 인수로 가진다. 139 | 140 | ```java 141 | Map c = menu.stream().collect(groupingBy(Dish::getType, 142 | filtering(dish -> dish.getCalories > 500), toList()))); 143 | ``` 144 | 145 | mapping메소드를 이용하는 것도 가능하다. 이는 스트림 요소를 그룹에 맞추어 변환해준다. 146 | 147 | ```java 148 | Map> dishNamesByType = menu.stream() 149 | .collect(groupingBy(Dish::getType, mapping(Dish::getName, toList()))); 150 | ``` 151 | 152 | 물론 이때에도 flatMap을 활용하여 평면화를 진행할 수 있다. 153 | 154 | ### 6.3.2 다수준 그룹화 155 | 156 | 두 인수를 받는 groupingBy 메소드를 이용하여 항목을 다수준으로 그룹화 할 수 있다. 157 | 158 | `groupingBy(키_매핑_함수, 값_매핑_함수)` 159 | 160 | 마치 Map> 와 같이 중첩이 가능하다는 것과 동일하다. 161 | 162 | 왜 이렇게 되는지는 groupingBy의 내부를 찾아보면 알 수 있다. 163 | 164 | ```java 165 | public static 166 | Collector> groupingBy(Function classifier, 167 | Collector downstream) { 168 | return groupingBy(classifier, HashMap::new, downstream); 169 | } 170 | ``` 171 | 172 | 첫 번째 인수로는 classifier(분류함수)를 받으며, 두 번째 인수로는 Collector를 받기 때문에 두 번째 인수에 groupingBy를 전달할 수 있다. 173 | 174 | 다수준 그룹화가 아닌 그룹화의 groupingBy 메소드는 이렇다. 175 | 176 | ```java 177 | public static Collector>> 178 | groupingBy(Function classifier) { 179 | return groupingBy(classifier, toList()); 180 | } 181 | ``` 182 | 183 | ### 6.3.3 서브 그룹으로 데이터 수집 184 | 185 | groupingBy의 내부를 보면 알 수 있듯이 Collector 형태의 인수가 모두 가능하기 떄문에 `counting()`이나 `maxBy()`나 `minBy()`도 가져와 사용할 수 있다. 186 | 187 | ### 컬렉터 결과를 다른 형식에 적용하기 188 | 189 | **collectingAndThen** 190 | 191 | ```java 192 | public static Collector collectingAndThen( 193 | Collector downstream, 194 | Function finisher 195 | ); 196 | ``` 197 | 198 | 스트림을 downstream에 오는 Collector로 처리하고, 이 결과에 대하여 finisher에 해당하는 함수를 적용한다. 199 | 200 | ## 6.4 분할 201 | 202 | 분할은 **분할 함수**라 불리는 프레디케이트를 분류 함수로 사용한다. 203 | 204 | 분할 함수의 리턴 타입은 불리언이기에 맵의 키 형식은 boolean이 된다. 키의 최대치가 True or False 2가지이므로 그룹화 맵은 최대 2개의 그룹으로 만들어진다. 205 | 206 | ### 6.4.1 분할의 장점 207 | 208 | 분할 함수에 들어가는 partitioningBy 메소드의 내부 구조는 다음과 같다. 209 | 210 | ```java 211 | public static Collector>> partitioningBy( 212 | Predicate predicate) 213 | ``` 214 | 215 | ```java 216 | public static 217 | Collector> partitioningBy(Predicate predicate, 218 | Collector downstream) 219 | ``` 220 | 221 | 두 가지의 인수를 가지는 오버로드된 함수를 보면 두 번째 인수로 Collector를 가지는 것을 볼 수 있고, 첫 번째 인수로 오는 함수가 리턴 타입으로 boolean 형태를 가지는 **분할 함수**이다. 222 | 223 | ## 6.5 Collector 인터페이스 224 | 225 | ```java 226 | public interface Collector { 227 | Supplier supplier(); 228 | BiConsumer accumulator(); 229 | BinaryOperator combiner(); 230 | Function finisher(); 231 | Set characteristics(); 232 | } 233 | ``` 234 | 235 | - **T**는 수집될 스트림 항목의 제네릭 형식이다. 236 | - **A**는 누적자. 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식이다. 237 | - **R**은 수집 연산 결과 객체의 형식(항상 그런 것은 아니지만 대개 컬렉션 형식)이다. 238 | 239 | 이를 활용하면 다양한 Collector를 만들 수 있다. 240 | 241 | ### 6.5.1 Collector 인터페이스의 메서드 살펴보기 242 | 243 | ### supplier 메서드 : 새로운 결과 컨테이너 만들기 244 | 245 | supplier는 수집 과정에서 빈 누적자 인스턴스를 만드는 **파라미터**가 없는 함수다. 246 | 247 | 이는 reducing연산을 수행하면서 값을 누적할 저장소(A)에 해당한다. 248 | 249 | ```java 250 | public Supplier> supplier() { 251 | return ArrayList::new; 252 | } 253 | ``` 254 | 255 | ### accumulator : 결과 컨테이너에 요소 추가하기 256 | 257 | accumulator는 리듀싱 연산을 수행하는 함수를 반환한다. 258 | 259 | 요소를 탐색하면서 데이터(T)를 저장소(A)에 추가하는 역할을 하는 함수라 생각하면 이해가 더 편하다. 260 | 261 | 저장소의 상태를 변경하기에 리턴 타입은 void이다. 262 | 263 | ```java 264 | public BiConsumer, T> accumulator() { 265 | return List::add; 266 | } 267 | ``` 268 | 269 | ### finisher 메서드 : 최종 변환값을 결과 컨테이너로 적용하기 270 | 271 | finisher는 스트림의 모든 요소를 누적한 후, 최종 결과로 변환하는 역할을 한다. 272 | 273 | 따라서 **누적자(A)를 최종 결과(R)로 변환하는 함수**를 반환해야한다. 274 | 275 | 물론 누적자 객체가 그 자체만으로 최종 결과일 수 있기에 finisher 메소드는 항등 함수를 반환한다. 276 | 277 | ```java 278 | public Function, List> finisher() { 279 | return Function.identity(); // 변환 없이 그대로 반환하는 항등 함수 280 | } 281 | ``` 282 | 283 | 이 3개의 메서드로 순차적 스트림 리듀싱 기능을 구현할 수 있지만, 고려해야할 사항이 많기에 스트림 리듀싱 기능 구현은 생각보다 복잡하다. 284 | 285 | ### combiner 메서드 : 두 결과 컨테이너 병합 286 | 287 | combiner는 스트림의 서로 다른 서브 파트를 병렬로 처리할 때, 누적자가 이 결과를 어떻게 처리할지 정의한다. 단, 이때 병렬스트림에서만 동작한다. 288 | 289 | 스트림을 분할해야 하는지 정의하는 조건이 거짓으로 바뀌기 전까지 원래 스트림을 재귀적으로 분할한다. 290 | 291 | 보통 분산된 작업의 크기가 너무 작아지면 병렬 수행의 속도는 순차 수행의 속도보다 느려진다. 즉, 병렬 수행의 효과가 상쇄된다.  292 | 293 | ### Characteristics 메서드 294 | 295 | 컬렉터의 연산을 정의하는 Characteristics 형식의 **불변 집합**을 반환한다. 296 | 297 | - **UNORDERED** 298 | - 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않는다. 299 | - **CONCURRENT** 300 | - 다중 스레드에서 accumulator 함수를 동시에 호출할 수 있으며 이 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있다. 301 | - 컬렉터 플러그에 UNORDERED를 함께 설정하지 않았다면 데이터 소스가 정렬되어 있지 않은 상황에서만 병렬 리듀싱을 수행할 수 있다. 302 | - **IDENTITY_FINISH** 303 | - finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 이를 생략할 수 있다.즉, 리듀싱 과정에서 최종 결과 형태로 변환하지 않고 누적자 객체를 바로 사용할 수 있다. 304 | 305 | 예를 들어 스트림 요소를 누적하는데 사용한 리스트가 최종 결과 형식이며 추가 변환이 필요없다면 **IDENTITY_FINISH** 이다. 306 | 307 | 리스트의 순서가 상관이 없다면 **UNORDERED** 이며 병렬로 실행이 가능하다면 **CONCURRENT** 이다. 308 | -------------------------------------------------------------------------------- /2장 동작 파라미터화 코드 전달하기/ch2/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /3장 람다 표현식/ch3.md: -------------------------------------------------------------------------------- 1 | # 3.1 람다란 무엇인가? 2 | > **람다 표현식** : 메서드로 전달할 수 있는 익명 함수를 단순화한 것 3 | 4 | ## 특징 5 | 1. **익명** 6 | - 이름이 없는 메서드 7 | 1. **함수** 8 | - 특정 클래스에 종속되지 않음 9 | - 하지만, 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함 10 | 1. **전달** 11 | - 람다 표현식을 메서드 인수로 전달하거나 변수로 저장 12 | 1. **간결성** 13 | - 익명 클래스와 달리 자질구레한 코드 구현할 필요 없음 14 | ## 장점 15 | 1. 동작 파라미터 형식의 코드를 더 쉽게 구현 16 | 1. 코드가 간결하고 유연해짐 17 | 18 | 19 | ## 예시 20 | ```java 21 | Comparator byWeight = new Comparator() { 22 | public int compare(Apple a1, Apple a2) { 23 | return a1.getWeight().compareTo(a2.getWeight()); 24 | } 25 | } 26 | ``` 27 | 28 | ⬇️ 29 | 30 | ```java 31 | //람다 표현식 32 | Comparator byWeight = 33 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); 34 | ``` 35 | 36 | ## 구성 37 | ```java 38 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); 39 | ``` 40 | 1. 람다 파라미터 41 | - `(Apple a1, Apple a2)` 42 | - Comparator의 compare 메서드 파라미터 43 | 1. 화살표 44 | - `->` 45 | - 파라미터 리스트와 바디를 구분 46 | 1. 람다 바디 47 | - `a1.getWeight().compareTo(a2.getWeight()); ` 48 | - 반환값에 해당하는 표현식 49 | 50 | ## 스타일 51 | 1. **표현식** 스타일 (기본 문법) 52 | ```java 53 | (parameters) -> expression 54 | ``` 55 | 1. **블록** 스타일 56 | ```java 57 | (parameters) -> { statements; } 58 | ``` 59 | 60 | ## 유효한 람다 표현식 예제 61 | ```java 62 | (String s) = s.length() 63 | //String 형식의 파라미터, int 반환 64 | //람다 표현식에는 return이 함축되어 있음 65 | 66 | (Apple a) -> a.getWeight() > 150 67 | //Apple 형식의 파라미터, boolean 반환 68 | 69 | (int x, int y) -> { 70 | System.out.println("Result:"); 71 | System.out.println(x + y); 72 | } 73 | //int 형식의 파라미터 두 개, 리턴값 없음(void 리턴) 74 | //여러 개의 문장 포함 가능 75 | 76 | () -> 42 77 | //파라미터 없음, int 42 반환 78 | 79 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) 80 | //Apple 형식의 파라미터 두 개, int 반환 81 | ``` 82 | 83 |

84 | 85 | # 3.2 어디에, 어떻게 람다를 사용할까? 86 | 87 | 람다는 함수형 인터페이스라는 문맥에서 사용할 수 있다. 88 | 89 | ## 3.2.1 함수형 인터페이스 90 | > **함수형 인터페이스** : 정확히 **하나의 추상 메서드**를 지정하는 인터페이스 91 | > 92 | > ⚠️디폴트 메서드가 많더라도 추상 메서드가 오직 하나면 함수형 인터페이스다. 93 | 94 | ### 예시 95 | ```java 96 | public interface Comparator { 97 | int compare(T o1, T o2); 98 | } 99 | 100 | pubic interface Runnable { 101 | void run(); 102 | } 103 | ``` 104 | 105 | ### 람다 활용법 106 | 전체 표현식을 **함수형 인터페이스의 인스턴스**로 취급할 수 있다. 107 | 108 |
109 | 110 | ## 3.2.2 함수 디스크립터 111 | > **함수 디스트립터** : 람다 표현식의 시그니처를 서술하는 메서드 112 | 113 |
114 | 115 | ### 예시 116 | - `(Apple, Apple) -> int` : Apple 형식의 파라미터 두 개, int 반환 117 | - `() -> void` : 파라미터 없음, void 반환 118 | - 등등 119 | 120 | | 함수형 인터페이스 | 함수 디스크립터 | 기본형 특화 | 121 | | --- | --- | --- | 122 | | Predicate\ | T → boolean | IntPredicate, LongPredicate, DoublePredicate | 123 | | Consumer\ | T → void | IntConsumer, LongConsumer, DoubleConsumer | 124 | | Function | T → R | IntFunction\, IntToDoubleFunction, IntToLongFunction, LongFunction\, LongToDoubleFunction, LongToIntFunction, DoubleFunction\, DoubleToIntFunction, DoubleToLongFunction, ToIntFunction\, ToDoubleFunction\, ToLongFunction\ | 125 | | Supplier\ | () → T | BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier | 126 | | UnaryOperator\ | T → T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator | 127 | | BinaryOperator\ | (T, T) → T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator | 128 | | BiPredicate | (T, U) → boolean | | 129 | | BiConsumer | (T, U) → void | ObjIntConsumer\, ObjLongConsumer\, ObjDoubleConsumer\ | 130 | | BiFunction | (T, U) → R | ToIntBiFunction, ToLongBiFunction, ToDoubleBiFunction | 131 | 132 | > **기본형 특화** : 기본형을 입출력으로 사용하는 상황에서 오토박싱을 피할 수 있도록 한 함수형 인터페이스 133 | > 134 | > - 제네릭 파라미터는 참조형만 사용할 수 있다. 135 | > - 기본형 -> 참조형으로 변환하는 "박싱"을 자동으로 해주는 "**오토박싱**"을 하게 되면, 메모리를 더 소비하고, 기본형을 가져올 때도 메모리를 탐색하는 과정이 필요하다. 136 | > - 그걸 피하기 위해 기본형 특화 인터페이스가 있음. 137 | 138 | 139 |
140 | 141 | 142 | > 💡 **`@FunctionalInterface`** 143 | > - 함수형 인터페이스임을 가리키는 어노테이션. 144 | > - 실제로 함수형 인터페이스가 아니면 컴파일 에러를 발생시킨다. 145 | > - 예를 들어 추상 메서드가 한 개 이상이라면 에러 발생. 146 | 147 |

148 | 149 | # 3.3 람다 활용 : 실행 어라운드 패턴 150 | > **실행 어라운드 패턴** 151 | > 1. 초기화/준비 코드 (ex.자원 열기) 152 | > 1. **작업** 153 | > 1. 정리/마무리 코드 (ex.자원 닫기) 154 | 155 | 156 | 157 | # 3.5 형식 검사, 형식 추론, 제약 158 | 159 | ## 3.5.1 형식 검사 160 | 람다가 사용되는 컨텍스트를 이용해 람다의 형식을 추론할 수 있다. 161 | > **대상 형식** : 어떤 컨텍스트에서 기대되는 람다 표현의 형식 162 | 163 | ### 형식 확인 과정 예시 164 | ```java 165 | List heavierThan150g = filter(inventory, 166 | (Apple apple) -> apple.getWeight() > 150); 167 | ``` 168 | 1. filter 메서드의 선언을 확인 169 | 1. filter 메서드는 두 번째 파라미터로 **Predicate 형식**(대상 형식)을 기대 170 | 1. Predicate은 **test**라는 **한 개의 추상 메서드**를 정의하는 **함수형 인터페이스**다. 171 | 1. test 메서드는 **Apple을 받아 boolean을 반환**하는 함수 디스크립터를 묘사 172 | 1. filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 함 173 | 174 | 175 | 176 | 177 | ## 3.5.3 형식 추론 178 | - **형식 추론**은 **할당문 컨텍스트**, **메서드 호출 콘텍스트**(파라미터, 반환값), **형변환 컨텍스트** 등으로 할 수 있다. 179 | - 자바 컴파일러는 람다 표현식이 사용된 컨텍스트(대상 형식)를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다. 180 | - 즉, 대상 형식 -> 함수 디스크립터 알아냄 -> 람다의 시그니처 추론 181 | 182 | ### 예시 183 | ```java 184 | Comparator c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); 185 | //형식을 추론하지 않음 186 | 187 | Comparator c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); 188 | //형식을 추론함 189 | ``` 190 | - 상황에 따라 명시적으로 형식을 포함하는 것이 좋을 때도 있고, 형식을 배제하는 것이 가독성을 향상시킬 때도 있다. 191 | 192 | 193 | ## 3.5.4 지역 변수 사용 194 | > **자유 변수** : 파라미터로 넘겨진 변수가 아닌 **외부**에서 정의된 변수 195 | 196 | > **람다 캡처링** : 익명함수가 하는 것처럼 자유 변수를 활용하는 것 197 | 198 | ### 주의사항 199 | - 람다는 인스턴스 변수와 정적 변수를 자유롭게 캡처(자신의 바디에서 참조)할 수 있다. 200 | - 그러려면 **지역 변수**는 **final** 선언이 되어있어야 하거나, 실질적으로 값이 변하지 않는 final 변수처럼 사용되어야 한다. 201 | - why? 202 | - 인스턴스 변수는 힙에 저장, 지역변수는 스택에 위치 203 | - 람다에서 지역 변수에 바로 접근할 수 있는 가정 하에, 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있다. 204 | - 이를 방지하기 위해 원래 변수에 접근X, 자유 지역 변수의 **복사본**을 제공. 205 | - 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다. == `final` 206 | 207 | ### 예시 208 | ```java 209 | // 가능 코드 210 | int portNumber = 1337; 211 | Runnable r = () -> System.out.println(portNumber); 212 | ``` 213 | ```java 214 | // 불가능 코드 215 | int portNumber = 1337; 216 | Runnable r = () -> System.out.println(portNumber); 217 | portNumber = 31337; 218 | ``` 219 | 220 | 221 |

222 | 223 | # 3.6 메서드 참조 224 | ```java 225 | inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())); 226 | ``` 227 | ⬇️ 228 | 229 | ```java 230 | // 메서드 참조 231 | inventory.sort(comparing(Apple::getWeight)); 232 | ``` 233 | 234 | ## 3.6.1 람다 표현식 -> 메서드 참조 235 | 1. 정적 메서드 참조 236 | ```java 237 | //람다 238 | (args) -> Classname.staticMethod(args) 239 | 240 | //메서드 참조 241 | ClassName::staticMethod 242 | ``` 243 | ```java 244 | //람다 245 | ToIntFunction stringToInt = (String s) -> Integer.parseInt(s); 246 | 247 | //메서드 참조 248 | Function stringToInteger = Integer::parseInt; 249 | ``` 250 | 1. 다양한 형식의 인스턴스 메서드 참조 251 | ```java 252 | //람다 253 | (arg0, rest) -> arg0.instanceMethod(rest) 254 | 255 | //메서드 참조 256 | ClassName::instanceMethod 257 | ``` 258 | ```java 259 | //람다 260 | BiPredicate, String> contains = (list, element) -> list.contains(element) 261 | 262 | //메서드 참조 263 | BiPredicate, String> contains = List::contains; 264 | ``` 265 | 266 | 1. 기존 객체의 인스턴스 메서드 참조 267 | ```java 268 | //람다 269 | (args) -> expr.instanceMethod(args) 270 | 271 | 272 | //메서드 참조 273 | expr::instanceMethod 274 | ``` 275 | ```java 276 | //람다 277 | Predicate startsWithNumber = (String string) -> this.startsWithNumber(string); 278 | 279 | //메서드 참조 280 | Predicate startsWithNumber = this::startsWithNumber; 281 | ``` 282 | 283 | ## 3.6.2 생성자 참조 284 | - 정적 메서드의 참조를 만드는 방법과 비슷 285 | 286 | ### 예시 287 | ```java 288 | Supplier c1 = Apple::new; 289 | Apple a1 = c1.get(); //Supplier의 get 메서드를 호출해 새로운 Apple 객체를 만들 수 있다. 290 | ``` 291 | ```java 292 | //파라미터가 1개 있는 생성자 293 | Function c2 = Apple::new; 294 | Apple a2 = c2.apply(110); //Function의 apply 메서드에 무게를 인수로 호출해 새로운 Apple 객체를 만들 수 있다. 295 | ``` 296 | ```java 297 | //파라미터가 2개 있는 생성자 298 | BiFunction c3 = Apple::new; 299 | Apple a3 = c3.apply(GREEN, 110); //BiFunction의 apply 메서드에 색과 무게를 인수로 제공공해 새로운 Apple 객체를 만들 수 있다. 300 | ``` 301 | 302 |

303 | 304 | # 3.8 람다 표현식을 조합할 수 있는 유용한 메서드 305 | - Comparator, Function, Predicate 같은 함수형 인터페이스는 람다 표현식을 **조합**할 수 있도록 **유틸리티 메서드**를 제공 306 | - 즉, 여러 개의 람다 표현식을 조합 -> 복잡한 람다 표현식 307 | - **디폴트 메서드**가 있기 때문에 함수형 인터페이스에서도 정의에 어긋나지 않게 추가 메서드를 제공할 수 있다. 308 | 309 | ## 3.8.1 Comparator 조합 310 | 1. `.reversed()` 311 | - 역정렬 312 | 1. `.thenComparing(함수)` 313 | - 함수를 인수로 받아 첫 번쨰 비교자를 이용해서 두 객체가 같다고 판단되면 두 번째 비교자에 객체를 전달 314 | 315 | ```java 316 | inventory.sort(comparing(Apple::getWeight)) 317 | .reversed() //무게를 내림차순으로 정렬 318 | .thenComparing(Apple::getCountry) //두 사과의 무게가 같으면 국가별로 정렬 319 | ``` 320 | 321 | ## 3.8.2 Predicate 조합 322 | 1. `.negate()` 323 | - '빨간색이 **아닌** 사과'처럼 특정 Predicate를 반전시킬 때 324 | ```java 325 | Predicate notRedApple = redApple.negate(); 326 | //기존 Predicate 객체 redApple의 결과를 반전시킨 객체를 만든다. 327 | ``` 328 | 1. `.and()` 329 | - '빨간색이**면서** 무거운 사과' 330 | 1. `.or()` 331 | - '빨간색이면서 무거운 사과 **또는** 그냥 녹색 사과' 332 | - **주의사항** 333 | - `a.or(b).and(c)` 는 `(a || b) && c` 334 | 335 | ## 3.8.3 Function 조합 336 | 1. `andThen` 337 | - 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달 338 | ```java 339 | Function f = x -> x + 1; 340 | Function g = x -> x * 2; 341 | Function h = f.andThen(g); //g(f(x)) 342 | 343 | int result = h.apply(1); //4 344 | ``` 345 | 346 | 1. `compose` 347 | - 인수로 주어진 함수를 먼저 실행-> 그 결과를 외부 함수의 인수로 제공 348 | ```java 349 | Function f = x -> x + 1; 350 | Function g = x -> x * 2; 351 | Function h = f.compose(g); //f(g(x)) 352 | 353 | int result = h.apply(1); //3 354 | ``` 355 | 356 | -------------------------------------------------------------------------------- /15장 CompletableFuture와 리액티브 프로그래밍 컨셉의 기초/ch15.md: -------------------------------------------------------------------------------- 1 | # 15.1 동시성을 구현하는 자바 지원의 진화 2 | 3 | - 초기 자바 : Runnable과 Thread 4 | - 자바 5 : 스레드 실행과 태스트 제출을 분리하기 위한 ExecutorService, Runnable과 Thread의 변형을 반환하는 Callable과 Future, 제네릭 5 | - 자바 7 : 분할 정복 알고리즘의 포크/조인 구현을 지원하는 java.util.concurrent.RecursiveTask 6 | - 자바 8 : 스트림과 새로 추가된 람다 지원에 기반한 병렬 프로세싱. Future를 조합하는 기능을 추가하며 동시성을 강화한 CompletableFuture 7 | - 자바 9 : 분산 비동기 프로그래밍을 지원. 리액티브 프로그래밍을 위한 Flow 인터페이스 추가 8 | 9 | ## 15.1.1 스레드와 높은 수준의 추상화 10 | 11 | 직접 Thread를 사용하지 않고 스트림을 이용해 스레드 사용 패턴을 추상화할 수 있다. 스트림으로 추상화하는것은 디자인 패턴을 적용하는 것과 비슷하지만 대신 쓸모 없는 코드가 라이브러리 내부로 구현되면서 복잡성도 줄어든다는 장점이 더해진다. 12 | 13 | ```java 14 | sum = Arrays.stream(stats).parallel().sum(); 15 | ``` 16 | 17 | ## 15.1.2 Executor와 스레드 풀 18 | 19 | 자바5는 Executor 프레임워크와 스레드 풀을 통해 스레드의 힘을 높은 수준으로 끌어올리는 즉 자바 프로그래머가 태스크 제출과 실행을 분리할 수 있는 기능 제공 20 | 21 | ### 스레드의 문제 22 | 23 | 자바 스레드는 직접 운영체제 스레드에 접근한다. 운영체제가 지원하는 스레드 수를 초과해 사용하면 자바 애플리케이션이 예상치 못한 방식으로 크래시될 수 있으므르 기존 스레드가 실행되는 상태에서 계속 새로운 스레드를 만드는 상황이 일어나지 않도록 주의해야 한다. 24 | 25 | ### 스레드 풀 그리고 스레드 풀이 더 좋은 이유 26 | 27 | 자바 ExecutorService 는 태스크를 제출하고 나중에 결과를 수집할 수 있는 인터페이스를 제공한다. 28 | 29 | 워커 스레드라 불리는 nThreads를 포함하는 ExecutorService 를 만들고 이들을 스레드 풀에 저장한다. 스레드 풀에서 사용하지 않은 스레드로 제출된 태스크를 먼저 온 순서대로 실행한다. 태스크 실행이 종료되면 이들 스레드를 풀로 반환한다. 30 | 31 | 장점은 하드웨어에 맞는 수의 태스크를 유지함과 동시에 수 천개의 태스크를 스레드 풀에 아무 오버헤드 없이 제출할 수 있다. 32 | 33 | 태스크(Runnable 이나 Callable) 를 제공하면 스레드가 이를 실행 34 | 35 | ### 스레드 풀 그리고 스레드 풀이 나쁜 이유 36 | 37 | 거의 모든 관점에서 스레드를 직접 사용하는 것보다 스레드 풀을 이용하는 것이 바람직 하지만 두 가지 사항을 주의해야 한다. 38 | 39 | - k 스레드를 가진 스레드 풀은 오직 k만큼의 스레드를 동시에 실행할 수 있다. 이때 잠을 자거나 I/O를 기다리거나 네트워크 연결을 기다리는 태스크가 있다면 주의해야 한다. 이런 상황에서 스레드는 블록되며, 블록 상황에서 태스크가 워커 스레드에 할당된 상태를 유지하지만 아무 작업도 하지 않게 된다. 핵심은 **블록할 수 있는 태스크는 스레드 풀에 제출하지 말아야 한다는 것이지만 항상 이를 지킬 수 있는 것은 아니다** 40 | - 프로그램을 종료하기 전에 모든 스레드 풀을 종료하자. 자바는 이런 상황을 위해 Thread.setDaemon 메서드를 제공한다. 41 | 42 | ## 15.1.3 스레드와 다른 추상화 : 중첩되지 않은 메서드 호출 43 | 44 | 엄격한 포크/조인 방식이 아닌 비동기 메서드로 여유로운 포크/조인을 사용할 수 있다. 45 | 46 | - 엄격한 포크/조인 : 스레드 생성과 join()이 한 쌍처럼 중첩된 메서드 호출 방식 47 | ![image](https://github.com/user-attachments/assets/5a71ad13-f0ef-4ee9-9801-a9a61fea9fb8) 48 | - 여유로운 포크/조인 : 시작된 태스크를 내부 호출이 아니라 외부 호출에서 종료하도록 기다리는 방식 49 | ![image](https://github.com/user-attachments/assets/cfec3827-ee49-4ab9-9caa-997b70109449) 50 | - 스레드 실행은 메서드를 호출한 다음의 코드와 동시에 실행되므로 데이터 경쟁 문제를 일으키지 않도록 주의해야함 51 | - 기존 실행 중이던 스레드가 종료되지 않은 상황에서 자바의 main() 메서드가 반환할 때 스레드의 행동 52 | 53 | # 15.2 동기 API와 비동기 API 54 | 55 | ## 15.2.1 Future 형식 API 56 | 57 | 자바 5에서 소개된 Future를 이용한다. 일회성값을 처리하는데 적합하다. 58 | 59 | ```java 60 | Future f(int x); 61 | Future g(int x); 62 | 63 | Future y = f(x); 64 | Future z = g(x); 65 | y.get() + z.get(); 66 | ``` 67 | 68 | - 메서드 f는 호출 즉시 자신의 원래 바디를 평가하는 테스크를 포함하는 Futrure를 반환 69 | - get 메서드를 이용하여 두 Future가 완료되어 결과가 합쳐지기를 기다린다. 70 | 71 | ## 15.2.2 **리액티브 형식 API** 72 | 73 | 콜백 형식으로 일련의 값을 처리하는데 적합하다. 74 | 75 | ```java 76 | void f(int x, IntConsumer dealWithResult); 77 | 78 | f(x, (int y) -> { 79 | left = y; 80 | print(left + right); 81 | }) 82 | 83 | g(x, (int z) -> { 84 | rignt = z; 85 | print(letf + right); 86 | }) 87 | ``` 88 | 89 | - 위처럼 람다를 통한 계산은 합계를 정확하게 알 수 없다. 어떤 함수가 먼저 계산될 지 알 수 없기 때문이다. 90 | - if 조건문을 이용해 적절하게 락을 걸어 두 콜백이 모두 호출되었는지 판단한다. 91 | - 리액티브 형식의 API는 보통 한 결과가 아니라 일련의 이벤트에 반응하도록 설계되었으므로 Future를 사용하는것이 적절하다. 92 | - 두 대안 모두 코드를 복잡하게 만들며 어떤 API를 사용할지 결정이 필요하다. 93 | - API는 명시적으로 스레드를 처리하는 코드에 비해 사용코드를 더 단순하게 만들고, 높은 수준의 구조를 유지할 수 있게 도와준다. 94 | 95 | ## **15.2.3 잠자기(그리고 기타 블로킹 동작)는 해로운 것으로 간주** 96 | 97 | 스레드는 잠들어도 여전히 시스템 자원을 점유한다. 스레드 풀에서 잠을 자는 태스크는 다른 태스크가 시작되지 못하게 막으므로 자원을 소비한다. 블록 동작도 이와 마찬가지다. 98 | 99 | 이런 상황을 방지하는 방법은 **이상적으로 절대 태스크에서 기다리는 일을 만들지 않는 것**과 **코드에서 예외를 일으키는 방식**이 존재한다. 100 | 101 | ```java 102 | //10초 동안 워커 스레드를 점유한 상태에서 아무것도 안하는 코드 103 | work1(); 104 | Thread.sleep(10000); 105 | work2(); 106 | ``` 107 | 108 | ```java 109 | // 다른 작업이 실행될 수 있도록 허용하는 코드(스레드를 사용할 필요가 없이 메모리만 조금 더 사용) 110 | public class ScheduledExecutorServiceExample { 111 | 112 | public static void main(String[] args) { 113 | ScheduledExecutorService scheduledExecutorService = Executor.newScheduledThreadPool(1); 114 | 115 | work1(); 116 | scheduledExecutorService.schedule(ScheduledExecutorServiceExample::work2, 10, 117 | timeUnit.SECONDS); 118 | 119 | scheduledExecutorService.shutdown(); 120 | 121 | } 122 | 123 | public static void work1() { 124 | //... 125 | } 126 | 127 | public static void work2() { 128 | //... 129 | } 130 | } 131 | ``` 132 | 133 | ## **15.2.5 비동기 API에서 예외는 어떻게 처리할까?** 134 | 135 | • Future를 구현한 CompletableFuture에서는 런타임 get 메서드에 예외를 처리할 수 있는 기능을 제공, exceptionally와 같은 메서드도 제공된다. 136 | 137 | • Future나 리액티브 형식의 비동기 API에서 호출된 메서드의 실제 바디는 별도의 스레드에서 호출되며 이때 발생하는 어떤 에러는 이미 호출자의 실행 범위와는 관계가 없는 상황이 된다. 138 | 139 | • 자바 9 플로 API에서는 Subscriber클래스를 이용하며, 그렇지 않는 경우 예외가 발생했을 때 실행될 추가 콜백을 만들어 인터페이스를 구현해야 한다. 140 | 141 | # **15.3 박스와 채널 모델** 142 | 143 | 박스와 채널 모델(box-and-channel model)은 동시성 모델을 설계하고 개념화하기 위한 모델이다. 144 | 145 | - 박스와 채널 모델을 이용하면 생각과 코드를 구조화할 수 있으며, 시스템 구현의 추상화 수준을 높일 수 있다. 146 | - 박스로 원하는 연산을 표현하면 계산을 손으로 코딩한 결과보다 더 효율적일 것이다. 147 | - 또한 병렬성을 직접 프로그래밍하는 관점을 콤비네이터를 이용해 내부적으로 작업을 처리하는 관점으로 바꿔준다. 148 | 149 | ![image](https://github.com/user-attachments/assets/318bab13-1840-4d08-8b45-fe982688dac1) 150 | 151 | ```java 152 | int t = p(x) 153 | System.out.println( r(q1(t), q2(t)) ); 154 | // 위 방식은 q1, q2를 차례로 호출하여 하드웨어 병렬성 활용과는 거리가 멀다. 155 | ``` 156 | 157 | ```java 158 | int t = p(x); 159 | Future a1 = executorService.submit(() -> q1(t)); 160 | Future a2 = executorService.submit(() -> q2(t)); 161 | System.out.println(r(a1.get(), a2.get()); 162 | ``` 163 | 164 | 박스와 채널 다이어그램의 모양상 p와 r을 Future로 감싸지 않았지만, 병렬성을 극대화하려면 모든 함수를 Future로 감싸야 한다. 165 | 166 | 많은 태스크가 get() 메서드를 호출해서 Future가 끝나기를 기다리게 되면 하드웨어의 병렬성을 제대로 활용하지 못하거나 데드락에 걸릴 수도 있다. 167 | 168 | # **15.4 CompletableFuture와 콤비네이터를 이용한 동시성** 169 | 170 | - Java 8에서는 Future 인터페이스의 구현인 CompletableFuture를 이용하여 Future를 조합할 수 있는 기능을 추가했다. 171 | - 일반적으로 Future는 실행하여 get()으로 결과를 얻을 수 있는 Callable로 만들어진다. 하지만 CompletableFuture는 실행할 코드 없이 Future를 만들수 있고 complete() 메서드를 활용하여 나중에 어떤 값을 이용해 다른 스레드가 이를 완료할 수 있고 get()으로 결과를 얻을수 있도록 허용한다. 172 | 173 | ```java 174 | main() { 175 | ExecutorService executorService = Executors.newFixedThreadPool(10); 176 | int x = 1337; 177 | 178 | CompletableFuture a = new CompletableFuture<>(); 179 | executorService.submit(() -> a.complete(f(x))); 180 | int b = g(x); 181 | print(a.get() + b); 182 | } 183 | ``` 184 | 185 | - 위 코드는 f(X)의 실행이 끝나지 않으면 get()을 기다려야 하기 때문에 프로세싱 자원을 낭비할 수 있다. 이러한 문제점을 CompletableFuture를 사용하여 해결할 수 있다. 186 | - CompletableFuture의 thenCombine 메서드를 활용하여 두 연산결과를 효과적으로 더할 수 있다. 187 | 188 | ```java 189 | CompletableFuture thenCombine(CompletableFuture other, BiFunction fn); 190 | ``` 191 | 192 | 이 메서드는 두 개의 CompletableFuture 값 (T, U 결과 형식)을 받아 한 개의 새 값을 만든다. 처음 두 작업이 끝나면 두 결과 모두에 fn을 적용하고 블록하지 않은 상태로 결과 Future를 반환한다. 193 | 194 | ```java 195 | main() { 196 | ExecutorService executorService = Executors.newFixedThreadPool(10); 197 | int x = 1337; 198 | 199 | CompletableFuture a = new CompletableFuture<>(); 200 | CompletableFuture b = new CompletableFuture<>(); 201 | CompletableFuture c = a.thenCombine(b, (y, z) + y + z); 202 | executorService.submit(() -> a.complete(f(x))); 203 | executorService.submit(() -> b.complete(g(x))); 204 | 205 | print(c.get()); 206 | } 207 | ``` 208 | 209 | - Future a와 Future b의 결과를 알지 못한 상태에서 thenCombine은 두 연산이 끝났을 때 스레드 풀에서 실행된 연산을 만든다. 210 | - 결과를 추가하는 세번째 연산 c는 다른 두작업이 끝나기 전까지 스레드에서 실행되지 않는다. 211 | 212 | # 15.5 **발행-구독 그리고 리액티브 프로그래밍** 213 | 214 | Future는 독립적 실행과 병렬성에 기반하므로, 한 번만 실행해 결과를 제공한다.반면 리액티브 프로그래밍은 시간이 흐르면서 여러 Future 같은 객체를 통해 여러 결과를 제공한다. 또한 가장 최근의 결과에 대해 반응(react)하는 부분이 존재한다. 215 | 216 | 자바 9에서는 java.util.concurrent.Flow의 인터페이스에 발행-구독 모델을 적용해 리액티브 프로그래밍을 제공한다. 217 | 218 | 자바 9 플로 API는 다음과 같이 세 가지로 정리할 수 있다. 219 | 220 | - `구독자(Subscriber)`가 구독할 수 있는 `발행자(Publisher)` 221 | - 연결을 `구독(subscription)`이라한다. 222 | - 이 연결을 이용해 `메시지(또는 이벤트)`를 전송한다. 223 | 224 | ## 15.5.1 두 플로를 합치는 예제 225 | 226 | - 두 정보 소스로 부터 발생하는 이벤트를 합쳐 다른 구독자가 볼 수 있도록 발행하는 예를 통해 pub - sub의 특징을 간단히 확인할 수 있다. 227 | - C1 + C2라는 공식을 포함하는 C3를 만들자. C1이나 C2의 값이 변경되면 C3에도 새로운 값이 반영되어야 한다. 228 | 229 | ```java 230 | private class SimpleCell { 231 | private int value = 0; 232 | private String name; 233 | 234 | public SimpleCell(String name) { 235 | this.name = name; 236 | } 237 | } 238 | 239 | SimpleCell c2 = new SimpleCell("c2"); 240 | SimpleCell c1 = new SimpleCell("c1"); 241 | ``` 242 | 243 | c1 이나 c2의 값이 변경되는 경우 어떻게 c3가 두 값을 더하도록 지정할 수 있을까 ? c1과 c2에 이벤트가 발행 했을때 c3를 구독하도록 만들어야 한다. 244 | 245 | 다음과 같이 Publisher 인터페이스를 활용하여 구현할 수 있다. 246 | 247 | ### **Publisher** 248 | 249 | Publisher는 발행자이며 subscribe를 통해 구독자를 등록한다. 250 | 251 | ```java 252 | interface Publisher { 253 | void subscribe(Subscriber subscriber); 254 | } 255 | ``` 256 | 257 | ### **Subscriber** 258 | 259 | Subscriber 인터페이스는 onNext라는 정보를 전달할 단순 메서드를 포함하며 구현자가 필요한 대로 이 메서드를 구현할 수 있다. 260 | 261 | ```java 262 | interface Subscriber { 263 | public void onNext(T t); 264 | public void onError(Throwable t); 265 | public void onComplete(); 266 | public void onSubscribe(Subscription s); 267 | } 268 | ``` 269 | 270 | ### **업스트림과 다운스트림** 271 | 272 | 데이터가 Publisher에서 Subscriber로 흐름에 착안하여 이를 업스트림(upstream) 또는 다운스트림(downstream)이라 부른다. 273 | 274 | ## 15.5.2 역압력 275 | 276 | 매 초마다 수천개의 메시지가 onNext로 전달된다면 빠르게 전달되는 이벤트를 아무 문제 없이 처리할 수 있을까? 277 | 278 | 이러한 상황을 `압력(pressure)` ;lojkm이라 부른다. 이럴 때는 정보의 흐름 속도를 제어하는 역압력 기법이 필요하다. 279 | 280 | 자바 9 플로 API에서는 발행자가 무한의 속도고 아이템을 방출하는 대신 요청했을 때만 다음 아이템을 보내도록 하는 `request()` 메서드를 제공한다. 281 | 282 | Publisher와 Subscriber 사이에 채널이 연결되면 첫 이벤트로 `Subscriber.onSubscribe(Subscription subscription)`메서드가 호출된다. Subscription 객체는 다음처럼 Subscriber와 Publisher와 통신할 수 있는 메서드를 포함한다. 283 | 284 | ```java 285 | void onSubscribe(Subscription subscription); 286 | 287 | public interface Subscription { 288 | void request(long n); 289 | public void cancel(); 290 | } 291 | ``` 292 | 293 | 콜백을 이용한 역방향 소통효과에 주목, Publisher는 Subscription 객체를 만들어 Subscriber로 전달시 Subscriber는 이를 이용하여 Publisher로 정보를 보낼 수 있다. 294 | 295 | ## 15.5.3 실제 역압력의 간단한 형태 296 | 297 | - 한 번에 한개의 이벤트만 처리하도록 구성시 다음과 같은 작업이 필요하다. 298 | - Subscriber가 OnSubscribe로 전달된 Subscription 객체를 필드로 저장. 299 | - Subscriber가 수많은 이벤트를 받지 않도록 onSubscribe, onNext, onError의 마지막 동작에 channel.request(1)을 추가해 오직 한 이벤트만 요청한다. 300 | - 요청을 보낸 채널에만 onNext, onError 이벤트를 보내도록 Publisher의 notifyAllSubscribers 코드를 바꾼다. 301 | - 보통 여러 Subscriber가 자신만의 속도를 유지할 수 있도록 Publisher는 새 Subscription을 만들어 각 Subscriber와 연결한다. 302 | - 구현이 간단해 보일수 있지만 실제로 역압력을 구현시 장단점을 생각해야 한다. 303 | - 여러 Subscriber가 있을 때 이벤트를 가장 느린 속도로 보낼 것인가 ? 아니면 각 Subscriber에게 보내지 않은 데이터를 저장할 별도의 큐를 가질 것인가? 304 | - 큐가 너무 커지면 어떻게 해야 할까? 305 | - Subscriber가 준비되지 않으면 큐의 데이터를 폐기 할 것인가? 306 | 307 | # 15.6 리액티브 시스템 vs 리액티브 프로그래밍 308 | 309 | **리액티브 시스템** 310 | 311 | - 런타입 환경이 변화에 대응하도록 전체 아키텍처가 설계된 프로그램. 312 | - 반응성(responsive), 회복성(resilient), 탄력성(elastic)으로 세 가지 속성을 가진다. 313 | - **반응성**은 리액티브 시스템이 큰 작업을 처리하느라 간단한 질의의 응답을 지연하지 않고 실시간으로 입력에 반응하는 것이다. 314 | - **회복성**은 한 컴포넌트의 실패로 전체 시스템이 실패하지 않음을 의미한다. 315 | - **탄력성**은 시스템이 잣긴의 작업 부하에 맞게 적응하며 작업을 효율적으로 처리함을 의미 316 | 317 | **리액티브 프로그래밍** 318 | 319 | - 리액티브 시스템이 가지는 속성을 구현하기 위한 프로그래밍 형식을 의미한다. 320 | - java.util.concurrent.Flow 관련된 자바 인터페이스에서 제공하는 리액티브 프로그래밍 형식. 321 | - 이들 인터페이스 설계는 메시지 주도(message-driven) 속성을 반영한다. 322 | -------------------------------------------------------------------------------- /13장 디폴트 메서드/ch13.md: -------------------------------------------------------------------------------- 1 | # 13.0 개요 2 | ## 문제 상황 3 | - 라이브러리 설계자 입장에서 인터페이스를 바꾸고 싶을 때 4 | - 바꾸면 이전에 구현했던 모든 클래스들의 구현도 고쳐야 한다. 5 | 6 | ## 해결 방법 7 | 1. 인터페이스 내부의 **정적 메서드** 8 | 1. 인터페이스 내부의 **디폴트 메서드** 9 | 10 | - 예시 11 | ```java 12 | List numbers = Arrays.asList(3, 5, 1, 2, 6); 13 | numbers.sort(Comparator.naturalOrder()); 14 | ``` 15 | - `sort(...)` : default 메서드 16 | - `Comparator.naturalOrder()` : 정적 메서드 17 | 18 |
19 | 20 | 21 | > [!NOTE] 22 | > **정적 메서드와 인터페이스** 23 | > 24 | > - 자바에서는 인터페이스, 인터페이스의 인스턴스를 활용할 수 있는 정적메서드들의 모음집인 "**유틸리티 클래스**"를 활용한다. 25 | > - 자바 8에서는 인터페이스에 직접 정적 메서드를 구현할 수 있다. 26 | > - 유틸리티 클래스가 없어져도 되지만, 이전 버전들과의 호환성을 위해 남아있다. 27 | 28 |
29 | 30 | 31 | ➡️ 기존의 코드 구현을 바꾸도록 강요하지 않으면서 인터페이스 바꿀 수 있음. 32 | 33 | 34 | 35 |

36 | 37 | 38 | # 13.1 변화하는 API 39 | ## 13.1.0 API를 바꾸는 것이 왜 어려운가? 40 | 자바 그리기 라이브러리 설계자가 되었다고 가정 41 | - Interface `Resizable` : `setHight`, `setWidth`, `getHeight`, `getWidth`, `setAbsoluteSize` 등을 정의하는 인터페이스 제공 42 | - class `Rectangle`, `Square` : `Resizable`을 구현하는 클래스 제공 43 | - class `Ellipse` : 사용자가 직접 `Resizable`을 구현하는 클래스 44 | 45 | 이때, `Resizable`에 `setRelativeSize`라는 메서드를 추가하고 싶다. 46 | 47 | 48 |
49 | 50 | ## 13.1.1 API 버전 1 51 | - 초기 버전 52 | ```java 53 | public interface Resizable extends Drawable { 54 | int getWidth(); 55 | int getHeight(); 56 | void setWidth(int width); 57 | void setHeight(int height); 58 | void setAbsoluteSize(int width, int height); 59 | } 60 | ``` 61 | - 사용자 구현 62 | ```java 63 | public class Ellipse implements Resizable { 64 | ... 65 | } 66 | ``` 67 | ```java 68 | public class Game { 69 | public static void main(String... args) { 70 | List resizableShapes = Arrays.asList(new Square(), new Rectangle(), new Ellipse()); //크기를 조절할 수 있는 모양 리스트 71 | Utils.paint(resizableShapes); 72 | } 73 | } 74 | 75 | public class Utils { 76 | public static void paint(List l) { 77 | l.forEach(r -> { 78 | r.setAbsoluteSize(42, 42); 79 | r.draw(); 80 | }) 81 | } 82 | } 83 | ``` 84 | 85 | 86 |
87 | 88 | ## 13.1.2 API 버전 2 89 | - 시간이 지나고 `Resizable`에 `setRelativeSize` 메서드 추가 90 | 91 | ```java 92 | public interface Resizable extends Drawable { 93 | int getWidth(); 94 | int getHeight(); 95 | void setWidth(int width); 96 | void setHeight(int height); 97 | void setAbsoluteSize(int width, int height); 98 | void setRelativeSize(int wFactor, int hFactor); //추가된 메서드 99 | } 100 | ``` 101 |
102 | 103 | ## 13.1.3 사용자가 겪는 문제 104 | 1. `Resizable`을 구현하는 모든 클래스는 `setRelativeSize` 메서드를 구현해야 한다. 하지만 `Ellipse`는 `setRelativeSize` 를 구현하지 않는다. 105 | - 인터페이스에 새로운 메서드를 추가하면 **바이너리 호환성**은 유지된다. 106 | 107 | > **바이너리 호환성** : 새로 추가된 메서드를 **호출하지만 않으면**, 새로운 **메서드 구현이 없어도** 기존 클래스 파일 구현이 잘 **동작**한다. 108 | - 하지만, 언젠가는 setRelativeSize를 사용하는 코드로 바꿀 수 있다. 109 | 1. 공개된 API를 고치면 **기존 버전과의 호환성 문제** 발생 110 | - 자신만의 API를 별도로 만든 다음 예전 버전과 새로운 버전을 직접 관리하면 되긴 함. 그러나 111 | 1. 라이브러리 관리가 복잡함 112 | 1. 사용자는 같은 코드에 두 버전 모두 사용해야 하는 상황 발생 113 | 1. 결국 메모리 사용⬆️, 로딩 시간⬆️ 114 | 115 |
116 | 117 | > [!NOTE] 118 | > **바이너리 호환성, 소스 호환성, 동작 호환성** 119 | > 120 | > - **바이너리 호환성** : 뭔가를 바꾼 이후에도 **에러 없이** 기존 **바이너리가 실행**될 수 있는 상황. 121 | > - 인터페이스에 메서드를 추가했을 떄, 추가된 메서드를 호출하지 않는 한 문제 X 122 | > - **소스 호환성** : 코드를 고쳐도 **기존 프로그램**을 **성공**적으로 **재컴파일** 123 | > - 인터페이스에 메서드 추가 ➡️ 소스 호환성 X. 추가한 메서드를 구현하도록 클래스를 고쳐야 하기 때문. 124 | > - **동작 호환성** : 코드를 바꾼 다음에도 **같은 입력값**이 주어지면 프로그램이 **같은 동작**을 실행 125 | > - 인터페이스에 메서드를 추가해도 프로그램에서 추가된 메서드를 호출할 일은 없으므로 동작 호환성 O. 126 | 127 |

128 | 129 | # 13.2 디폴트 메서드란 무엇인가? 130 | > **디폴트 메서드** : 인터페이스 자체에서 기본으로 제공하는 구현된 메서드 131 | 132 |
133 | 134 | ## 구조 135 | - 특징 136 | 1. `default` 키워드 137 | 1. 메서드 **바디** 포함 138 | 139 | - 예시 140 | ```java 141 | public interface Sized { 142 | //추상메서드 143 | int size(); 144 | 145 | //디폴트 메서드 146 | default boolean isEmpty() { 147 | return size() == 0; 148 | } 149 | } 150 | ``` 151 | 152 | > [!NOTE] 153 | > **추상 클래스와 자바 8의 인터페이스** 154 | > - 공통점 : **추상 메서드**와 **바디**를 포함하는 메서드 정의 가능 155 | > - 차이점 156 | > | 추상 클래스 | 인터페이스 | 157 | > | --- | --- | 158 | > | 하나의 추상 클래스만 상속(**단일 상속**) | 여러 개의 인터페이스 구현(**다중 구현**) | 159 | > | **인스턴스 변수**(필드)로 공통 상태 가질 수 **O** | 인스턴스 변수 가질 수 **X** | 160 | 161 |
162 |
163 | 164 | # 13.3 디폴트 메서드 활용 패턴 165 | ## 13.3.1 선택형 메서드 166 | 1. 잘 사용하지 않는 메서드 167 | - ex) `Iterator`의 `remove()` 168 | 169 | ⬇️ 170 | 1. 기존의 **빈 구현** 171 | ```java 172 | interface Iterator { 173 | boolean hasNext(); 174 | T next(); 175 | void remove(); 176 | } 177 | ``` 178 | ```java 179 | class IteratorImpl<...> implements Iterator<...> { 180 | ... 181 | ... 182 | void remove() {} //빈 구현 183 | } 184 | ``` 185 | 186 | ⬇️ 187 | 1. 디폴트 메서드의 **기본 구현** 188 | ```java 189 | interface Iterator { 190 | boolean hasNext(); 191 | T next(); 192 | 193 | //잘 사용하지 않던 remove 함수의 기본 구현 제공 194 | default void remove() { 195 | throw new UnsupportedOperationException(); 196 | } 197 | } 198 | ``` 199 | 200 | ⬇️ 201 | 1. 빈 구현 할 필요 X, **불필요한 코드 줄임** 202 | 203 |
204 | 205 | ## 13.3.2 동작 다중 상속 206 | 207 | 208 | ### 예시 209 | ```java 210 | public class ArrayList extends AbstractList //한 개의 클래스 상속 211 | implements List, RandomAccess, Cloneable, Serializable { //4개의 인터페이스 구현 212 | } 213 | ``` 214 | - ArrayList는 AbstractList, List, RandomAccess, Cloneable, Serializable의 **`서브형식(subtype)`** 이다. 215 | 216 | 217 | ### 장점 218 | 1. **중복되지 않는 최소한의 인터페이스**를 유지 ➡️ 동작을 **쉽게 재사용**&**조합** 219 | 220 | - **중복되지 않는 최소한의 인터페이스**의 예시 221 | ```java 222 | public interface Rotatable { 223 | void setRotationAngle(int angleInDegrees); //구현 필요 224 | int getRotationAngle(); //구현 필요 225 | 226 | //기본 구현이 제공되는 default method. 따로 구현 X. 227 | default void rotateBy(int angleInDegrees) { 228 | setRotationAngle((getRotationAngle() + angleInDegrees) % 360); 229 | } 230 | } 231 | ``` 232 | ```java 233 | public interface Moveable { 234 | int getX(); 235 | int getY(); 236 | void setX(int x); 237 | void setY(int y); 238 | 239 | default void moveHorizontally(int distance) { 240 | setX(getX() + distance); 241 | } 242 | 243 | default void moveVertically(int distance) { 244 | setY(getY() + distance); 245 | } 246 | } 247 | ``` 248 | ```java 249 | public interface Resizable { 250 | int getWidth(); 251 | int getHeight(); 252 | void setWidth(int width); 253 | void setHeight(int height); 254 | void setAbsoluteSize(int width, int height); 255 | 256 | default void setRelativeSize(int wFactor, int hFactor) { 257 | setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor); 258 | } 259 | } 260 | ``` 261 | - **인터페이스 조합** 예시 262 | ```java 263 | public class Monster implements Rotatable, Moveable, Resizable { 264 | //모든 추상 메서드 구현 O. 265 | //디폴트 메서드(rotateBy, moveHorizontally, moveVertixally, setRelativeSize) 구현 X. 266 | } 267 | ``` 268 | ```java 269 | public class Sun implements Moveable, Rotatable { 270 | //모든 추상 메서드 구현 O. 271 | //디폴트 메서드(rotateBy, moveHorizontally, moveVertixally) 구현 X. 272 | } 273 | ``` 274 | 275 | 1. 상위 인터페이스의 디폴트 메서드의 구현을 고쳐야 할 때, 구현하는 모든 클래스들은 **자동**으로 **변경된 코드**를 상속받는다. 276 | 277 |
278 | 279 | > [!WARNING] 280 | > 281 | > **옳지 못한 상속** 282 | > 283 | > 한 개의 메서드를 재사용하려고 100개의 메서드, 필드가 정의되어 있는 클래스를 상속받는 것은 좋지 X. 284 | > 285 | > 이럴 때는 **멤버 변수**를 이용해 클래스에서 필요한 메서드를 **직접 호출**하는 **`"델리게이션"`** 을 사용하는 것이 좋다. 286 | 287 |

288 | 289 | # 13.4 해석 규칙 290 | 클래스는 여러 인터페이스의 다중 구현이 가능하다.
291 | 그런데 이때, **같은 시그니처**를 갖는 **디폴트 메서드**를 **여러 인터페이스, 클래스에서 상속**받는 상황이 생길 수 있다. 292 | 293 | ```java 294 | public interface A { 295 | default void hello() { 296 | System.out.println("Hello from A"); 297 | } 298 | } 299 | 300 | public interface B extends A { 301 | default void hello() { 302 | System.out.println("Hello from B"); 303 | } 304 | } 305 | 306 | public class C implements B, A { 307 | public static void main(String... args) { 308 | new C().hello(); //무엇이 출력될까? 309 | } 310 | } 311 | ``` 312 | 313 | 그럼 어떤 인터페이스의 디폴트 메서드를 사용하게 될까? 314 | 315 | 316 | ## 규칙 317 | 1. **클래스**가 항상 이긴다. 318 | - 우선권 : 클래스/슈퍼클래스에서 정의한 메서드 > 디폴트 메서드 319 | - 예시 320 | 1. 클래스 D 추가 321 | ```java 322 | public class D implements A { 323 | void hello() { 324 | System.out.println("Hello from D"); 325 | } 326 | } 327 | 328 | public class C extends D implements B, A { 329 | public static main(String... args) { 330 | new C().hello(); //무엇이 출력될까? 331 | } 332 | } 333 | ``` 334 | - D는 디폴트 메서드인 `hello()`를 오버라이드 O.
335 | ⬇️
336 | _"클래스의 메서드 구현"_ 규칙 O. 337 | - 우선권 : D > B 338 | - `"Hello from D"` 출력 339 | 1. 1번 규칙 이외의 상황에서는 **서브인터페이스**가 이긴다. 340 | - 예시 341 | 1. 위 예제 342 | 343 | - B가 A를 상속받는 서브인터페이스 ➡️ 우선권 : B > A 344 | - `"Hello from B"` 출력 345 | 1. 클래스 D 추가 346 | 347 | ```java 348 | public class D implements A { } 349 | 350 | public class C extends D implements B, A { 351 | public static main(String... args) { 352 | new C().hello(); //무엇이 출력될까? 353 | } 354 | } 355 | ``` 356 | - D는 디폴트 메서드인 `hello()`를 오버라이드 X.
357 | ⬇️
358 | A의 디폴트 메서드 구현을 상속받는다.
359 | ⬇️
360 | _"1. 클래스의 메서드 구현"_ 이 아니므로 1번 규칙 X. 361 | - B가 A를 상속받는 서브인터페이스 ➡️ 우선권 : B > A 362 | - `"Hello from B"` 출력 363 | 1. 1, 2로도 우선순위가 결정되지 않았다면, **명시적**으로 디폴트 메서드를 **오버라이드**하고 **호출**해야 한다. 364 | - 예시 365 | 1. B가 A를 상속받지 않을 때 366 | 367 | ```java 368 | public interface A { 369 | void hello() { 370 | System.out.println("Hello from A"); 371 | } 372 | } 373 | 374 | public interface B { 375 | void hello() { 376 | System.out.println("Hello from B"); 377 | } 378 | } 379 | 380 | public class C implements B, A { } 381 | ``` 382 | 383 | - 인터페이스 간에 상속관계 X ➡️ _"2. 서브인터페이스"_ 규칙 X. 384 | - `"Error: class C inherits unrelated defaults for hello() from types B and A"` 출력 385 | 386 | - 해결 387 | - 사용하려는 메서드를 **명시적**으로 **선택** 388 | ```java 389 | public class C implements B, A { 390 | void hello() { 391 | B.super.hello(); 392 | } 393 | } 394 | ``` 395 | > `X.super.m(...)` 396 | > - `X` : 호출하려는 메서드 `m`의 슈퍼인터페이스 397 | > - `m(...)` : 호출하려는 메서드 398 | 399 | -------------------------------------------------------------------------------- /12장 새로운 날짜와 시간 API/ch12.md: -------------------------------------------------------------------------------- 1 | ## 자바 8 이전까지의 날짜와 시간 API의 문제점 2 | 3 | - Date는 JVM 기본시간대인 중앙 유럽 시간대를 사용했다. 4 | - DateFormat은 스레드에 안전하지 않다. 5 | - Date와 Calendar는 모두 가변 클래스이기에 유지보수가 어렵다. 6 | 7 | ## 12.1 LocalDate, LocalTime, Instant, Duration 8 | 9 | java.time 패키지는 LocalDate, LocalTime, LocalDateTime, Instant, Duration, Period 등 새로운 날짜와 시간에 관련된 클래스를 제공한다. 10 | 11 | ### 12.1.1 **LocalDate와 LocalTime** 12 | 13 | 1. LocalDate 인스턴스는 시간을 제외한 날짜를 표현하는 불변 객체다.(어떤 시간대 정보도 포함하지 않는다.) 14 | 2. 인스턴스 생성 시에는 정적 팩토리 메소드인 of를 이용하여 생성한다. 15 | 3. 팩토리 메소드 now는 시스템 시계의 정보를 이용해서 현재 날짜 정보를 얻는다. 16 | 17 | ```java 18 | LocalDate date = LocalDate.of(2017, 9, 21); // 2017-09-21 19 | int year = date.getYear(); // 2017 20 | Month month = date.getMonth(); // SEPTEMBER 21 | int day = date.getDayOfMonth(); // 21 22 | DayOfWeek dow = date.getDayOfWeek(); // THURSDAY 23 | int len = date.lengthOfMonth(); // 31(3월의 일 수) 24 | boolean leap = date.isLeapYear(); // false(윤년이 아님) 25 | ``` 26 | 27 | ```java 28 | // 팩토리 메소드 now 29 | LocalDate today = LocalDate.now(); 30 | ``` 31 | 32 | ChronoField는 TemporalField 인터페이스를 제공하기에 열거자를 이용하여 쉽게 날짜 데이터를 얻을 수 있다. 33 | 34 | ```java 35 | int year = date.get(ChronoField.YEAR); 36 | int month = date.get(ChronoField.MONTH_OF_YEAR); 37 | int day = date.get(ChronoField.DAY_OF_MONTH); 38 | ``` 39 | 40 | java.time 패키지의 내장 메서드를 이용하여 가독성을 높일 수 있습니다. 41 | 42 | ```java 43 | int year = date.getYear(); 44 | int month = date.getMonth(); 45 | int day = date.getDayOfMonth(); 46 | ``` 47 | 48 | LocalTime 클래스를 사용하여 시간을 표현할 수 있다. 49 | 50 | of 메소드(시간, 분 혹은 시간, 분, 초)를 이용하여 LocalTime 인스턴스를 만들 수 있습니다. LocalDate와 마찬가지로 다양한 get메서드를 제공하기에 이를 사용하여 원하는 정보를 얻을 수 있습니다. 51 | 52 | ```java 53 | LocalTime time = LocalTime.of(13, 45, 20); // 13:45:20 54 | int hour = time.getHour(); // 13 55 | int minute = time.getMinute(); // 45 56 | int second = time.getSecond(); // 20 57 | ``` 58 | 59 | 날짜와 시간 문자열로 LocalDate와 LocalTime의 인스턴스를 만드는 방법도 있다. 이때 parse 정적 메서드를 사용할 수 있다. 60 | 61 | ```java 62 | LocalDate date = LocalDate.parse("2017-09-21"); 63 | LocalDate time = LocalDate.parse("13:45:20"); 64 | ``` 65 | 66 | parse 메서드 사용 시에 DateTimeFormatter 인스턴스를 전달하여 날짜, 시간 객체의 형식을 지정할 수 있다. 67 | 68 | DateTimeFormatter는 java.util.DateFormat 클래스를 대체한다. 69 | 70 | 문자열을 LocalDate나 LocalTime으로 파싱이 불가능할 떄 parse메서드는 DateTimeParseException(RuntimeException을 상속받은 예외)을 일으킨다. 71 | 72 | ### 12.1.2 날짜와 시간 조합 73 | 74 | LocalDateTime은 LocalDate와 LocalTime을 쌍으로 가지는 복합 클래스이다. 75 | 76 | ```java 77 | // 2017-09-21T13:45:20 78 | LocalDateTime dt1 = LocalDateTime.of(2017, Month.SEPTEMBER, 21,13,45,20); 79 | LocalDateTime dt2 = LocalDateTime.of(date, time); 80 | LocalDateTime dt3 = date.atTime(13,45,20); 81 | LocalDateTime dt4 = date.atTime(time); 82 | LocalDateTime dt5 = time.atDate(date); 83 | 84 | LocalDateTime date1 = dt1.toLocalDate(); // 2017-09-21 85 | LocalDateTime time1 = dt1.toLocalTime(); // 13:45:20 86 | ``` 87 | 88 | ### 12.1.3 Instant 클래스 : 기계의 날짜와 시간 89 | 90 | Instant 클래스는 유닉스 에포크 시간을 기준으로 특정 지점까지의 시간을 초로 표현한다. 91 | 92 | Instant 클래스는 기계적인 관점에서 시간을 표현하며 ofEpochSecond 팩토리 메서드를 사용하여 초를 인수로 전달하여 Instant 클래스의 인스턴스를 만들 수 있다. 93 | 94 | Instant 클래스는 나노초(10억분의 1초) 단위의 정밀도를 제공하며 오버로드된 ofEpochSecond 메서드에서는 두 번째 인수를 사용하여 나노초 단위로 시간을 보정할 수 있다. 95 | 96 | 이때 두 번째 인수는 0에서 999,999,999 사이의 값을 가질 수 있다. 97 | 98 | Instant 클래스는 초와 나노초 단위의 시간 표현법이기에 사람이 확인할 수 있도록 시간을 표현해주는 정적 팩토리 메소드인 now를 지원하지만 사람이 읽을 수 있는 시간 정보를 제공하지는 않는다.(이는 Instant가 기계 전용 유틸리티이기 떄문이다.) 99 | 100 | 따라서 통상적으로 날짜를 받기위해 사용하는 int day를 통해 정보를 받아오려 하면 예외를 발생시킨다. 101 | 102 | ```java 103 | int day = Instant.now().get(ChronoField.DAY_OF_MONTH); 104 | // 예외 발생 105 | java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth 106 | ``` 107 | 108 | Instant에서는 Duration과 Period 클래스를 함께 활용할 수 있다. 109 | 110 | ### 12.1.4 Duration과 Period 정의 111 | 112 | 현재까지 살펴본 모든 클래스는 Temporal 인터페이스를 구현하는데 Temporal 인터페이스는 특정 시간을 모델링하는 객체의 값을 어떻게 읽고 조작할지 정의한다. 113 | 114 | Duration 클래스의 정적 팩토리 메서드 between으로 두 시간 객체 사이의 지속시간을 만들 수 있다. 115 | 116 | ```java 117 | // LocalDateTime객체와 Instant 객체는 혼합 불가 118 | Duration d1 = Duration.between(time1, time2); 119 | Duration d1 = Duration.between(dateTime1, dateTime2); 120 | Duration d2 = Duration.between(instant1, instant2); 121 | 122 | // Period 클래스의 팩토리 메서드 between을 이용한 두 LocalDate의 차이 123 | Period tenDays = Period.between(LocalDate.of(2017,9,11), LocalDate(2017,9,21)); 124 | 125 | // Duration과 Period 클래스가 제공하는 다양한 팩토리 메서드. 126 | Duration threeMinutes = Duration.ofMinutes(3); 127 | Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES); 128 | 129 | Period tenDays = Period.ofDays(10); 130 | Period threeWeeks = Period.ofWeeks(3); 131 | Period twoYearsSixMonthsOneDay = Period.of(2,6,1); 132 | ``` 133 | 134 | 여기까지 살펴본 모든 클래스는 불변으로 함수형 프로그래밍, 스레드 안정성과 도메인 모델의 일관성을 유지하는데 좋은 특성이다. 135 | 136 | 허나 새로운 날짜와 시간 API에서는 변경된 객체 버전을 만들 수 있는 메서드를 제공해준다. 137 | 138 | ## 12.2 날짜 조정, 파싱, 포메팅 139 | 140 | withAttribute 메서드로 기존의 LocalDate를 바꾼 버전을 직접 간단하게 만들 수 있다. 141 | 142 | 아래 코드는 기존 객체를 바꾸지 않는다. 143 | 144 | ```java 145 | LocalDate date1 = LocalDate.of(2017, 9, 21); // 2017-09-21 146 | LocalDate date2 = date1.withYear(2011); // 2011-09-21 147 | LocalDate date3 = date2.withDayOfMonth(25); // 2011-09-25 148 | LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 2); // 2011-02-25 149 | ``` 150 | 151 | TemporalField를 인자로 받는 with 메서드를 사용하면 불변 객체의 필드값을 변경할 수 있다. 152 | 153 | Temporal 인터페이스는 특정 시간을 정의하며 get과 with 메서드를 통해 Temporal 객체의 필드값을 읽거나 고칠 수 있다. 154 | 155 | Temporal 객체가 지정된 필드를 지원하지 않을 경우 `UnsupportedTemporalTypeException`이 발생한다. 156 | 157 | 아래와 같이 LocalDate를 선언형으로 사용해 지정된 시간을 추가하거나 뺄 수 있다. 158 | 159 | ```java 160 | LocalDate date1 = LocalDate.of(2017, 9, 21); // 2017-09-21 161 | LocalDate date2 = date1.plusWeek(1); // 2017-09-28 162 | LocalDate date3 = date2.minusYears(6); // 2011-09-28 163 | LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS); // 2012-03-28 164 | ``` 165 | 166 | Temporal 인터페이스에 정의된 plus와 minus 메서드를 이용하여 Temporal 객체를 특정 시간만큼 앞뒤로 이동시킬 수 있다. 167 | 168 | 메서드 인수에 숫자와 TemporalUnit을 활용할 수 있으며, ChronoUnit 열거형을 통해 TemporalUnit 인터페이스를 쉽게 활용할 수 있는 구현을 제공한다. 169 | 170 | LocalDate, LocalTime, LocalDateTime, Instant 등 날짜와 시간을 표현하는 모든 클래스는 서로 비슷한 메서드를 제공한다. 171 | 172 | ### 12.2.1 TemporalAdjusters 사용하기 173 | 174 | 복잡한 날짜 조정 기능에는 TemporalAdjuster를 통해 오버로드된 버전의 with 메서드를 호출하여 해당 기능을 수행한다. 175 | 176 | ```java 177 | import static java.time.temporal.TemporalAdjusters.*; 178 | LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18 179 | LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY)); // 2014-03-23 180 | LocalDate date3 = date2.with(lastDayOfMonth()); // 2014-03-31 181 | ``` 182 | 183 | TemporalAdjuster를 활용한다면 좀 더 복잡한 날짜 조정 기능을 직관적으로 해결할 수 있다. 184 | 185 | 또한 필요에 따라 쉽게 커스텀 TemporalAdjuster 구현을 만들 수 있다. 186 | 187 | TemporalAdjuster 인터페이스는 188 | 189 | ```java 190 | @FunctionalInterface 191 | public interface TemporalAdjuster { 192 | Temporal adjustInto(Temporal temporal); 193 | } 194 | ``` 195 | 196 | 위 코드와 같이 하나의 메서드만 정의하므로 함수형 인터페이스에 속한다. 197 | 198 | 결국 TemporalAdjuster 인터페이스의 구현은 Temporal 객체를 어떻게 다른 Temporal 객체로 변환할 지 정의하므로 TemporalAdjuster 인터페이스를 UnaryOperator과 유사한 형태로 간주할 수 있다. 199 | 200 | `UnaryOperator` 는 입력과 출력 타입이 같은 함수형 인터페이스로 `T → T`의 형식을 지니는데 이는 201 | 202 | `TemporalAdjuster`가 `Temporal → Temporal` 형식이기 때문이다. 203 | 204 | ### 12.2.2 날짜와 시간 객체 출력과 파싱 205 | 206 | 포매팅과 파싱 전용 패키지인 java.time.format에서 가장 중요한 클래스는 바로 `DateTimeFormatter`이다. 정적 팩토리 메서드와 상수를 이용해 손쉽게 포매터를 만들 수 있다. 207 | 208 | BASIC_ISO_DATE와 ISO_LOCAL_DATE 등의 상수를 미리 정의하고 있으며 이러한 상수를 이용해 날짜와 시간을 특정 형식의 문자열로 변환할 수 있다. 209 | 210 | ```java 211 | LocalDate date = LocalDate.of(2014, 3, 18); 212 | String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); // 20140318 213 | String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2014-03-18 214 | ``` 215 | 216 | 반대로 날짜나 시간을 표현하는 문자열을 파싱하여 날짜 객체를 만들 수 있다. 217 | 218 | 이때 parse 메서드를 활용한다. 219 | 220 | ```java 221 | LocalDate date1 = LocalDate.parse("20140318", DateTimeFormatter.BASIC_ISO_DATE); 222 | LocalDate date2 = LocalDate.parse("2014-03-18", DateTimeFormatter.ISO_LOCAL_DATE); 223 | ``` 224 | 225 | 기존 java.util.DateFormat과의 가장 큰 차이는 바로 `DateTimeFormatter` 는 Thread-Safe 하다는 점이다. 또한 `DateTimeFormatter` 는 특정 패턴으로 포매터를 만들 수 있는 정적 팩토리 메서드도 지원한다. 226 | 227 | ```java 228 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy"); 229 | LocalDate date1 = LocalDate.of(2014, 3, 18); 230 | String formattedDate = date1.format(formatter); 231 | LocalDate date2 = LocalDate.parse(formattedDate, formatter); 232 | ``` 233 | 234 | LocalDate의 format 메서드는 요청 형식의 패턴에 해당하는 문자열을 생성하고, parse 메서드는 해당 문자열을 파싱하여 날짜를 생성한다. 235 | 236 | ofPattern메서드도 Locale을 지정하여 포매터를 만들 수 있는 오버로드된 메서드를 제공한다. 237 | 238 | ```java 239 | DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN); 240 | LocalDate date1 = LocalDate.of(2014, 3, 18); 241 | String formattedDate = date.format(italianFormatter); // 18. marzo 2014 242 | LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter); 243 | ``` 244 | 245 | `DateTimeFormatterBuilder` 클래스를 통해 복합적인 포매터로 더욱 세부적으로 제어할 수 있다. 246 | 247 | ```java 248 | DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder() 249 | .appendText(ChronoField.DAY_OF_MONTH) 250 | .appendLiteral(". ") 251 | .appendText(ChronoField.MONTH_OF_YEAR) 252 | .appendLiteral(" ") 253 | .appendText(ChronoField.YEAR) 254 | .parseCaseInsensitive() 255 | .toFormatter(Locale.ITALIAN); 256 | ``` 257 | 258 | ## 12.3 다양한 시간대와 캘린더 사용법 259 | 260 | 그렇다면 시간대에 대한 정보는 어떻게 사용하며 활용할까? 261 | 262 | java.time.ZoneId 클래스는 기존의 java.util.TimeZone을 대체하며 서머타임(DST)와 같은 복잡한 요구 사항을 자동으로 처리한다. 이때 ZoneId 클래스는 불변 클래스이다. 263 | 264 | ### 12.3.1 시간대 사용하기 265 | 266 | ZoneRules 클래스는 표준 시간이 같은 지역을 묶어서 시간대 규칙 집합을 정의하는 클래스이다. 267 | 268 | ZoneRules 클래스에는 약 40개의 시간대가 존재하며 getRules( )를 이용해 해당 시간대의 규정을 획득할 수 있다. 269 | 270 | 다음처럼 지역 ID로 특정 ZoneId를 구분한다. 271 | 272 | ```java 273 | ZoneId romeZone = ZoneId.Of("Europe/Rome"); 274 | ``` 275 | 276 | 지역ID는 `‘{지역}/{도시}’` 형식으로 이루어지며 IANA Time Zone Database에서 제공하는 지역 집합 정보를 사용한다. 277 | 278 | ZoneId의 toZoneId를 통해 기존의 TimeZone 객체를 ZoneId 객체로 변환할 수 있다. 279 | 280 | ```java 281 | ZoneId zoneId = TimeZone.getDefault().toZoneId(); 282 | ``` 283 | 284 | ZoneId 객체를 통해 LocalDate, LocalDateTime, Instant 객체를 ZonedDateTime으로 변환할 수 있다. 285 | 286 | ```java 287 | LocalDate date = LocalDate.of(2014, Month.MARCH, 18); 288 | ZonedDateTIme zdt1 = date.atStartOfDay(romeZone); 289 | LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45); 290 | ZonedDateTIme zdt2 = dateTime.atZone(romeZone); 291 | Instant instant = Instant.now(); 292 | ZonedDateTime zdt3 = instant.atZone(romeZone); 293 | ``` 294 | 295 | ![Image](https://github.com/user-attachments/assets/30cc6a45-b40d-4961-be8f-4b7b0fa6f1fd) 296 | 297 | ZoneId를 이용해서 LocalDateTime을 Instant로 변환할 수도 있다. 298 | 299 | ```java 300 | Instant instant = Instant.now(); 301 | LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone); 302 | ``` 303 | 304 | Instant를 사용한 이유는 기존의 Date 클래스를 처리하는 코드를 사용해야 할 수 있기 때문이다. 305 | 306 | 옛날 API와 새로운 API간의 동작에 도움이 되는 toInstant(), fromInstant() 메서드가 존재한다. 307 | 308 | ### 12.3.2 UTC/Greenwich 기준의 고정 오프셋 309 | 310 | ZoneOffset 클래스를 이용해서 UTC/GMT 기준으로 시간대를 표현할 수 있고 이를 통해 특정 지역의 표준 시간과 **UTC(세계 표준 시간)와의 차이**를 표현할 수 있다. 311 | 312 | ```java 313 | ZoneOffset newYorkOffset = ZoneOffset.of("-05:00"); 314 | ``` 315 | 316 | ZoneOffset 클래스는 서머타임을 처리하지 않으므로 권장하지 않는다. 따라서 ZoneId를 사용해서 오프셋을 처리하는 것이 좋다. 317 | 318 | ISO-8601 캘린더 시스템에서 정의하는 UTC/GMT와 오프셋으로 날짜와 시간을 표현하는 OffsetDateTime을 사용하는 방법도 있다. 319 | 320 | ```java 321 | LocalDateTime dateTIme = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45); 322 | OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(date, newYorkOffset); 323 | ``` 324 | 325 | 새로운 날짜와 시간 API는 ISO 캘린더 시스템에 기반하지 않은 정보도 처리할 수 있는 기능을 제공한다. 326 | -------------------------------------------------------------------------------- /8장 컬렉션 API 개선/ch8.md: -------------------------------------------------------------------------------- 1 | # 8.1 컬렉션 팩토리 2 | 3 | > 자바 9에서는 **작은** 컬렉션 객체를 쉽게 만들 수 있는 몇 가지 방법을 제공한다. 4 | 5 | ### 팩토리 메서드 6 | 7 | > 기존 객체를 바탕으로 새로운 객체를 생성하는 메서드 8 | 9 | 리스트에 소수의 요소를 추가하는데 많은 코드 작성이 필요하다. 10 | 11 | ```java 12 | List friends = new ArrayList<>(); 13 | friends.add("Raphael"); 14 | friends.add("Olivia"); 15 | friends.add("Thibaut"); 16 | ``` 17 | 18 | ### Arrays.asList 팩토리 메서드 19 | 20 | 코드를 줄이기 위해 asList 메서드를 활용했다. 21 | 22 | asList()는 고정 크기의 리스트를 반환하고 요소를 갱신할 순 있지만 새 요소를 추가하거나 삭제할 시, **UnsupportedOperationException** 예외가 발생한다. 23 | 24 | ```java 25 | List friends = Arrays.asList("Raphael", "Olivia"); 26 | friends.set(0, "Richard"); //Raphael -> Richard 27 | friends.add("Thibaut"); 28 | ``` 29 | 30 | 그렇다면 집합은 어떨까? 31 | 32 | Arrays.asSet()은 존재하지 않으므로 다음과 같은 두 가지로 집합을 생성한다. 33 | 34 | ### HashSet 생성자 사용 35 | 36 | HashSet 생성자에 asList()로 반환한 List를 전달하면 새로운 Set이 생성되어 수정이 가능해진다. 37 | 38 | ```java 39 | Set friends = new HashSet<>(Arrays.asList("Raphael", "Olivia", "Thibaut")); 40 | ``` 41 | 42 | ### Stream API 사용 43 | 44 | Stream.of()로 문자열 스트림을 생성 후, Collector.toSet()을 통해 Set으로 반환한다. 45 | 46 | ```java 47 | Set friends = Stream.of("Raphael", "Olivia", "Thibaut") 48 | .collect(Collectors.toSet()); 49 | ``` 50 | 51 | 🚨 하지만 위 두 가지 방법에도 **내부적으로 불필요한 객체 할당이 발생**한다는 단점이 있다. 52 | 53 | **🪄 자바 9에서는 작은 리스트, 집합, 맵을 쉽게 만들 수 있도록 팩토리 메서드를 제공한다.** 54 | 55 | ## 8.1.1 리스트 팩토리 56 | 57 | ### List.of() 팩토리 메서드 58 | 59 | ```java 60 | List friends = List.of("Raphael", "Olivia", "Thibaut"); 61 | System.out.println(friends); //[Raphael, Olivia, Thibaut] 62 | ``` 63 | 64 | 여기서 새로운 요소를 추가한다면? 65 | 66 | ```java 67 | List friends = List.of("Raphael", "Olivia", "Thibaut"); 68 | friends.add("Chih-Chun"); //UnsupportedOperationException 69 | friends.set(0, "fisa"); // UnsupportedOperationException 70 | ``` 71 | 72 | **UnsupportedOperationException**이 발생한다. 73 | 74 | 불변의 리스트에 요소를 추가하려고 했기 때문!!! set()도 마찬가지다. 75 | 76 | 불변성이 있어 컬렉션이 의도치 않게 변하는 것을 막을 수 있다. 77 | 78 | ### 오버로딩 vs 가변 인수 79 | 80 | List 인터페이스를 보면 List.of의 다양한 오버로드 버전이 있다는 사실을 알 수 있다. 81 | 82 | ```java 83 | static List of(E e1, E e2, E e3, E e4) 84 | static List of(E e1, E e2, E e3, E e4, E e5) 85 | 86 | // 최대 10개까지 요소 받을 수 있음 87 | ``` 88 | 89 | 왜 아래처럼 다중 요소를 받을 수 있도록 자바 API를 만들지 않았을까? 90 | 91 | 이유는 성능 최적화를 위해서!! 92 | 93 | ```java 94 | static List of(E...elements) 95 | ``` 96 | 97 | - 내부적으로 가변 인수 버전은 추가 배열을 할당해서 리스트로 감싼다. 98 | - 배열을 할당하고 초기화하며 **나중에 Garbage Collection을 하는 비용을 지불**해야 한다. 99 | - 고정된 숫자 요소(**최대 10개**)를 API로 정의해서 **이런 비용을 제거**할 수 있다. 100 | - List.of로 10개 이상의 요소를 가진 리스트를 만들 수도 있지만, 이때는 가변 인수를 이용하는 메서드가 활용된다. 101 | 102 | ```java 103 | // 10개까지는 오버로딩된 버전 사용 (성능 최적화) 104 | public static List of(E e1, E e2, ..., E10) { 105 | return new ImmutableCollections.ListN<>(e1, e2, ..., e10); 106 | } 107 | 108 | // 11개 이상이면 가변 인수 버전 호출 109 | @SafeVarargs 110 | public static List of(E... elements) { 111 | return new ImmutableCollections.ListN<>(elements); 112 | } 113 | 114 | ``` 115 | 116 | 즉, **불변적이고 간단한 구조를 가진 리스트를 생성할 때 팩토리 메서드를 사용**하면 된다! 117 | 118 | ## 8.1.2 집합 팩토리 119 | 120 | List.of와 비슷한 방법으로 **바꿀 수 없는 집합**을 만들 수 있다. 121 | 122 | ```java 123 | Set friends = Set.of("Raphael", "Olivia", "Thibaut"); 124 | System.out.println(friends); //[Raphael, Olivia, Thibaut] 125 | ``` 126 | 127 | 중복된 요소가 있을 때, IllegalArgumentException 발생🚨 128 | 129 | ```java 130 | Set friends = Set.of("Raphael", "Olivia", "Olivia"); //IllegalArgumentException 131 | ``` 132 | 133 | 고유의 요소만 집합에 넣도록 하자. 134 | 135 | ## 8.1.3 맵 팩토리 136 | 137 | 바꿀 수 없는 집합을 생성하는 방법에는 2가지가 있다. 138 | 139 | ### **Map.of 팩토리 메서드 사용** 140 | 141 | 키와 값을 번갈아 가면서 사용한다. 142 | 143 | 10개 이하의 키와 값 쌍을 가진 맵을 만들 때 유용하다. 144 | 145 | ```java 146 | //key, value를 번갈아 제공하며 맵 만들기 147 | Map ageOfFriends = Map.of("Raphael", 30, "Olivia", 25, "Thibaut", 26); 148 | System.out.println(ageOfFriends); //{Olivia=25, Raphael=30, Thibaut=26} 149 | ``` 150 | 151 | ### **Map.Entry 객체를 인수로 받아** 가변 인수인 **Map.ofEntries 팩토리 메서드 사용** 152 | 153 | Map.ofEntries 메서드는 **키와 값을 감쌀 추가 객체 할당을 필요로 한다.** 154 | 155 | Map.entry는 Map.Entry 객체를 만드는 새로운 팩토리 메서드 156 | 157 | 11개 이상일 때 유용하다. 158 | 159 | ```java 160 | import static java.util.Map.entry; 161 | 162 | Map ageOfFriends = Map.ofEntries(entry("Raphael", 30), 163 | entry("Olivia", 25), 164 | entry("Thibaut", 26)); 165 | System.out.println(ageOfFriends); //{Olivia=25, Raphael=30, Thibaut=26} 166 | ``` 167 | 168 | ### ❓퀴즈 169 | 170 | 다음 코드 실행 결과는? 171 | 172 | ```java 173 | Set friends = Set.of("Alice", "Bob", "Alice"); 174 | System.out.println(friends); 175 | ``` 176 | 177 | # 8.2 리스트와 집합 처리 178 | 179 | 자바 8에서는 List, Set 인터페이스에 다음과 같은 메서드를 추가했다. 180 | 181 | 새로운 결과를 만드는 스트림 및 앞선 팩토리 메서드와 달리 이 메서드들은 호출한 컬렉션 자체를 바꾼다. 182 | 183 | ## 8.2.1 removeIf 메서드 184 | 185 | 프레디케이트를 만족하는 요소를 제거한다. 186 | 187 | List나 Set을 구현하거나 그 구현을 상속받은 모든 클래스에서 이용할 수 있다. 188 | 189 | ```java 190 | // 숫자로 시작되는 참조 코드를 가진 트랜잭션을 삭제하는 코드 191 | for (Transaction transaction : transactions) { 192 | if (Character.isDigit(transaction.getReferenceCode().charAt(0))) { 193 | transactions.remove(transaction); // ❌ ConcurrentModificationException 발생! 194 | } 195 | } 196 | ``` 197 | 198 | for-each 루프에서 remove()를 직접 호출하고 있어 **ConcurrentModificationException**이 발생한다.🚨 199 | 200 | ```java 201 | Iterator iterator = transactions.iterator(); 202 | while (iterator.hasNext()) { 203 | Transaction transaction = iterator.next(); 204 | if (Character.isDigit(transaction.getReferenceCode().charAt(0))) { 205 | iterator.remove(); // ✅ 안전하게 요소 제거 206 | } 207 | } 208 | ``` 209 | 210 | Iterator의 remove() 메서드를 사용하면 반복 중에 안전하게 요소를 삭제할 수 있다. 211 | 212 | 근데 코드를 더 깔끔하고 간단히 짜고 싶다면? → **removeIf 메서드**를 활용하면 된다. 213 | 214 | ```java 215 | transactions.removeIf(transaction -> 216 | Character.isDigit(transaction.getReferenceCode().charAt(0)) 217 | ); 218 | ``` 219 | 220 | ## 8.2.2 replaceAll 메서드 221 | 222 | 리스트에서 이용할 수 있는 기능으로 UnaryOperator 함수를 이용해 **요소를 바꾼다.** 223 | 224 | ### sort 메서드 225 | 226 | List 인터페이스에서 제공하는 기능으로 리스트를 정렬한다. 227 | 228 | 다음 코드는 새 문자열 컬렉션을 생성하는 코드다. 이는 불필요한 추가 리스트 할당이 발생할 수 있다. 229 | 230 | ```java 231 | // 새 문자열 컬렉션 생성 232 | List referenceCodes = List.of("a12", "C14", "b13"); 233 | 234 | List updatedCodes = referenceCodes.stream() 235 | .map(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1)) 236 | .collect(Collectors.toList()); 237 | 238 | updatedCodes.forEach(System.out::println); 239 | ``` 240 | 241 | 이를 해결하기 위해 요소를 바꾸는 set() 메서드를 지원하는 ListIterator 객체로 만든 코드를 보자. 242 | 243 | ```java 244 | List referenceCodes = new ArrayList<>(List.of("a12", "C14", "b13")); 245 | 246 | ListIterator iterator = referenceCodes.listIterator(); 247 | while (iterator.hasNext()) { 248 | String code = iterator.next(); 249 | iterator.set(Character.toUpperCase(code.charAt(0)) + code.substring(1)); 250 | } 251 | 252 | System.out.println(referenceCodes); 253 | ``` 254 | 255 | 좀 코드가 복잡해진 것을 확인할 수 있다. 256 | 257 | 이를 **replaceAll()**로 간단하게 구현할 수 있다. 258 | 259 | ```java 260 | referenceCodes.replaceAll(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1)); 261 | //UnaryOpertaor 구현하여 요소를 바꿈 262 | ``` 263 | 264 | # 8.3 맵 처리 265 | 266 | 자바 8에서 Map 인터페이스에 몇 가지 디폴트 메서드를 **추가했다.** 267 | 268 | ## 8.3.1 forEach 메서드 269 | 270 | Map.Entry를 사용하여 getKey(), getValue()를 호출할 수 있다. 271 | 272 | 하지만 코드가 다소 길고 가독성이 떨어진다. 273 | 274 | ```java 275 | for (Map.Entry entry : ageOfFriends.entrySet()) { 276 | String friend = entry.getKey(); 277 | Integer age = entry.getValue(); 278 | System.out.println(friend + " is " + age + " years old"); 279 | } 280 | ``` 281 | 282 | Map 인터페이스는 Map.Entry를 사용할 필요 없이 BiConsumer(키와 값을 직접 인수로 받음)를 인수로 받는 forEach 메서드를 지원한다. 283 | 284 | ```java 285 | ageOfFriends.forEach((friend, age) -> System.out.println(friend + "is" + age)); 286 | //key와 value 이용하여 출력 287 | ``` 288 | 289 | ## 8.3.2 정렬 메서드 290 | 291 | 다음 2개의 새로운 유틸리티를 통해 맵의 항목을 **키 또는 값을 기준으로 정렬한다.** 292 | 293 | - Entry.comparingByValue 294 | - Entry.comparingByKey 295 | 296 | ```java 297 | Map favouriteMovies 298 | = Map.ofEntries(entry("Raphael", "Star Wwars"), 299 | entry("Cristina", "Matrix"), 300 | entry("Olivia", "James Bond")); 301 | 302 | favouriteMovies.entrySet() //Map에 포함된 모든 키-값 쌍을 Set 컬렉션으로 변경 -> {A=Apple, B= Banana etc...} 303 | .stream() 304 | .sorted(Entry.comparingByKey()) //키 값을 기준으로 정렬 305 | .forEachOrdered(System.out::println); //사람의 이름을 알파벳 순으로 스트림 요소 처리 306 | 307 | //결과 308 | Cristina=Matrix 309 | Olivia=James Bond 310 | Raphael=Star wars 311 | ``` 312 | 313 | ## 8.3.3 getOrDefault 메서드 314 | 315 | 첫 번째 인수로 키를, 두 번째 인수로 기본값을 받으며 **맵에 키가 존재하지 않으면 두 번째 디폴트 값을 반환**한다. 316 | 317 | 단, 키가 존재하더라도 값이 null이면 null 반환이 가능하다. 318 | 319 | ```java 320 | Map favouriteMovies = Map.ofEntries(entry("Raphael", "Star wars"), 321 | entry("Olivia", "James Bond")); 322 | 323 | System.out.println(favouriteMovies.getOrDefault("Olivia", "Matrix")); 324 | //키가 존재하므로 James Bond 출력 325 | System.out.println(favouriteMovies.getOrDefault("Thibaut", "Matrix")); 326 | //키가 존재하지 않으므로 Matrix 출력 327 | ``` 328 | 329 | ## 8.3.4 계산 패턴 330 | 331 | 키의 존재 여부에 따라 어떤 동작을 실행하고 결과를 저장할 지를 고려해야 할 때가 있다. 332 | 333 | - computeIfAbsent 334 | - 키에 해당하는 값이 없으면(또는 null) 키를 이용해 새로운 값을 계산하고 맵에 추가 335 | - computeIfPresent 336 | - 키가 존재하면 새 값을 계산하고 맵에 추가 337 | - compute 338 | - 키로 새 값을 계산하고 맵에 저장 339 | 340 | ```java 341 | friendsToMovies.computeIfAbsent("Raphael", name -> new ArrayList<>()).add("Star Wars"); 342 | //{Raphael:[Star Wars]} 343 | ``` 344 | 345 | ## 8.3.5 삭제 패턴 346 | 347 | **기존 remove 메서드에 추가로 특정한 값과 연관되었을 때만 항목을 제거하는 오버로드 버전을 제공한다.** 348 | 349 | ```java 350 | // 기존 remove(K) 메서드 → key에 해당하는 값 제거 351 | favouriteMovies.remove("Raphael"); 352 | 353 | // remove(K, V) 354 | favouriteMovies.remove("Raphael", "Jack Reacher 2"); 355 | ``` 356 | 357 | ## 8.3.6 교체 패턴 358 | 359 | 맵 항목을 바꾸는 데 다음 2개의 메서드를 사용할 수 있다. 360 | 361 | - **replaceAll**: BiFunction을 적용한 결과로 각 항목의 값 교체, List의 replaceAll과 비슷 362 | - **Replace**: 키가 존재하면 맵의 값을 바꾼다. 키가 특정 값으로 매핑되었을 때만 값을 교체하는 오버로드 버전 존재 363 | 364 | ```java 365 | Map favouriteMovies = new HashMap<>(); 366 | favouriteMovies.put("Raphael", "Star Wars"); 367 | favouriteMovies.put("Olivia", "James Bond"); 368 | 369 | favouriteMovies.replaceAll((friend, movie) -> movie.toUpperCase()); //값을 대문자로 변경 370 | 371 | System.out.println(favouriteMovies); 372 | //결과 : {Olivia=JAMES BOND, Raphael=STAR WARS} 373 | 374 | // Olivia의 영화를 "Mission Impossible"로 변경 375 | favouriteMovies.replace("Olivia", "Mission Impossible"); 376 | 377 | System.out.println(favouriteMovies); 378 | //결과 : {Olivia=Mission Impossible, Raphael=STAR WARS} 379 | ``` 380 | 381 | ## 8.3.7 합침 382 | 383 | - putAll: 두 개의 맵에서 값을 합치거나 바꿔야 할때 사용 384 | - merge: 중복된 키가 있는 경우에 사용한다. 중복된 키를 어떻게 합칠지 결정하는 **BiFunction을 인수로 받는다.** 385 | 386 | ```java 387 | Map family = Map.ofEntries(entry("Teo", "Star Wars"), entry("Cristina", "James Bond")); 388 | Map friends = Map.ofEntries(entry("Raphael", "Star Wars"), entry("Cristina", "Matrix")); 389 | 390 | Map everyone = new HashMap<>(family); 391 | friends.forEach((k, v) -> everyone.merge(k,v, (movie1, movie2) -> movie1 + "&" + movie2)); 392 | //중복된 키가 있으면 두 값을 연결(BiFunction을 인수로 받았음) 393 | System.out.println(everyone); 394 | //{Raphael=Star Wars, Cristina=James Bond & Matrix, Teo=Star Wars} 395 | ``` 396 | 397 | # 8.4 개선된 ConcurrentHashMap 398 | 399 | ## 8.4.1 리듀스와 검색 400 | 401 | ConcurrentHashMap은 스트림에서 봤던 것과 비슷한 종류의 세 가지 새로운 연산을 지원한다. 402 | 403 | - **forEach**: 각 (키, 값) 쌍에 주어진 액션을 실행 404 | - **reduce**: 모든 (키, 값) 쌍을 제공된 리듀스 함수를 이용해 결과로 합침 405 | - **search**: 널이 아닌 값을 반환할 때까지 각 (키, 값) 쌍에 함수를 적용 406 | 407 | ### 연산 형태 408 | 409 | - **키, 값으로 연산** - forEach, reduce, search 410 | - **키로 연산** - forEachkey, reduceKeys, searchKeys 411 | - **값으로 연산** - orEachValue, reduceValues, searchValues 412 | - **Map.Entry 객체로 연산** - forEachEntry, reduceEntries, searchEntries 413 | 414 | 위 연산들은 ConcurrentHashMap의 상태를 변경하지 않기 때문에, 락을 사용하지 않고 동작한다. 415 | 416 | 또한 연산에 병렬성 기준값(threshold)을 정해야 한다. 417 | 418 | 맵의 크기가 기준값보다 작으면 순차적으로 연산을 진행하고 기준값보다 크면 병렬 연산 처리를 한다. 419 | 420 | ## 8.4.2 계수 421 | 422 | 맵의 매핑 개수를 반환하는 mappingCount 메서드를 제공한다. 423 | 424 | 기존에 제공되던 size 메서드 대신 int형으로 반환하지만 long형으로 반환하는 mappingCount 메서드를 사용할 때 매핑의 개수가 int의 범위를 넘어서는 상황에 대하여 대처할 수 있다. 425 | 426 | ## 8.4.3 집합뷰 427 | 428 | ConcurrentHashMap을 집합 뷰로 반환하는 keySet 메서드를 제공한다. 429 | 430 | 맵을 바꾸면 집합도 바뀌고 반대로 집합을 바꾸면 맵도 영향을 받는다. 431 | 432 | newKeySet이라는 메서드를 통해 ConcurrentHashMap으로 유지되는 집합을 만들 수도 있다. 433 | -------------------------------------------------------------------------------- /17장 리액티브 프로그래밍/ch17.md: -------------------------------------------------------------------------------- 1 | ### 리액티브가 요구되는 시대적 변화 2 | 3 | 다음과 같은 세 가지 변화로 인해 리액티브 시스템의 필요성이 대두되었다. 4 | 5 | - **빅데이터** 6 | - 데이터가 **페타바이트(PB)** 단위로 구성되며 **매일 지속적으로 증가**함. 7 | - **다양한 환경** 8 | - 모바일 디바이스부터 **수천 개 멀티 코어 프로세서 기반의 클라우드 클러스터**까지 다양한 환경에서 애플리케이션이 실행됨. 9 | - **사용 패턴** 10 | - 사용자들은 **밀리초(ms)** 단위의 빠른 응답 속도를 기대함. 11 | 12 | --- 13 | 14 | # 17.1 리액티브 매니페스토 15 | 16 | ### **핵심 원칙 (Core Principles of Reactive Systems)** 17 | 18 | 1. **반응성 (Responsiveness)** 19 | - 항상 **일정하고 예측 가능한 반응 시간**을 제공해야 한다. 20 | 2. **회복성 (Resilience)** 21 | - 장애 발생 시에도 **시스템은 계속 반응**할 수 있어야 한다. 22 | 3. **탄력성 (Elasticity)** 23 | - 다양한 작업 부하에도 **애플리케이션의 반응성**을 유지할 수 있어야 한다. 24 | 4. **메시지 드리븐 (Message-Driven)** 25 | - 시스템 컴포넌트 간의 **경계를 명확히 정의**하고, **비동기 메시지**를 통해 통신해야 한다. 26 | 27 | ![image](https://github.com/user-attachments/assets/8460daff-c6f4-4e88-a43d-5773b6853518) 28 | 29 | --- 30 | 31 | ## 17.1.1 애플리케이션 수준의 리액티브 32 | 33 | ### **리액티브 프로그래밍이란?** 34 | 35 | - **비동기 작업 처리**를 통해 스레드 관련 문제를 직접 다루지 않아도 됨. 36 | - 따라서 **비즈니스 로직에 더 집중**할 수 있음. 37 | 38 | ### **주의사항** 39 | 40 | - 스레드를 분리하더라도 **메인 이벤트 루프에서는 절대 블로킹 동작을 수행하지 말 것**. 41 | - 모든 **I/O 작업은 블로킹 동작**에 해당하므로 주의 필요. 42 | 43 | ### **리액티브 프레임워크의 역할** 44 | 45 | - **RxJava, Akka** 등의 프레임워크는 **별도의 스레드 풀**에서 블로킹 작업을 실행하여 메인 풀의 방해를 막음. 46 | - 그 결과: 47 | - 모든 CPU 코어가 **최적의 상태로 작업 수행** 가능 48 | - **CPU 작업과 I/O 작업 분리**로 풀 크기 조정 및 성능 관찰이 용이함 49 | 50 | ## 17.1.2 시스템 수준의 리액티브 51 | 52 | ### **리액티브 시스템이란?** 53 | 54 | - 회복성 있는 소프트웨어 아키텍처를 기반으로 함 55 | - 짧은 생명 주기의 데이터 스트림을 처리하며, 이벤트 드리븐(Event-Driven)으로 동작 56 | - 단순히 하나의 애플리케이션이 아닌, 여러 컴포넌트를 조립하고 이들 간 통신을 조율하는 구조 57 | 58 | --- 59 | 60 | ### **메시지 vs 이벤트** 61 | 62 | | 구분 | 메시지 (Message) | 이벤트 (Event) | 63 | | --- | --- | --- | 64 | | 전달 대상 | 하나의 명확한 목적지 | 등록된 모든 수신자 | 65 | | 방식 | 점대점 통신 | 브로드캐스트 기반 | 66 | | 사용 사례 | 컴포넌트 간 명확한 요청/응답 | 상태 변화 알림 및 반응 | 67 | 68 | *메시지 드리븐은 리액티브 시스템의 핵심 속성 중 하나* 69 | 70 | --- 71 | 72 | ### **컴포넌트 간 결합 제거의 중요성** 73 | 74 | - 각 컴포넌트는 완전히 고립되어야 함 75 | - 결합도를 낮추면 다음과 같은 이점을 가짐: 76 | - 회복성 유지: 하나의 컴포넌트 오류가 전체 시스템에 영향을 주지 않음 77 | - 탄력성 향상: 컴포넌트 독립 확장이 가능해짐 78 | - 반응성 유지: 장애 상황에서도 사용자 응답 가능 79 | 80 | --- 81 | 82 | ### 탄력성의 핵심: 위치 투명성 (Location Transparency) 83 | 84 | - 컴포넌트는 상대방의 위치(IP 등)를 알 필요 없이 통신 가능 85 | - 결과적으로: 86 | - 동적으로 부하 분산 가능 87 | - 서비스 확장과 축소가 유연해짐 88 | - 클라우드, 마이크로서비스 환경에 최적화됨 89 | 90 | *예: 서비스 A는 서비스 B가 어느 서버, 어느 리전에 있든 상관 없이 메시지를 전달할 수 있어야 함* 91 | 92 | # 17.2 리액티브 스트림과 플로 API 93 | 94 | 리액티브 프로그래밍은 리액티브 스트림을 사용하는 프로그래밍 95 | 96 | 리액티브 스트림은 잠재적으로 무한의 비동기 데이터를 순서대로 그리고 블록하지 않는 역압력을 전제해 처리하는 표준 기술 97 | 98 | 역압력은 발행-구독 프로토콜에서 이벤트 스트림의 구독자가 발행자가 이벤트를 제공하는 속도보다 느린 속도로 이벤트를 소비하면서 문제가 발생하지 않도록 보장하는 장치 99 | 100 | ## 17.2.1 Flow 클래스 소개 101 | 102 | 자바 9에서는 리액티브 프로그래밍을 제공하는 콜래스 java.util.concurrent.Flow가 추가 됨 103 | 104 | 정적 컴포넌트 하나를 포함하고 있으며 인스턴스화 가능 105 | 106 | 리액티브 스트림 프로젝트의 표준에 따라 pub-sub 모델을 지원할 수 있도록 flow 클래스는 중첩된 인터페이스 4개를 포함 107 | 108 | - Publisher 109 | - Subscriber 110 | - Subscription 111 | - Processor 112 | 113 | 자바의 `java.util.concurrent.Flow` API는 Java 9에서 도입된 **Reactive Streams** 사양의 일부로, 비동기 스트림 처리와 backpressure(요청 기반 데이터 흐름 제어)를 지원합니다. 이 API는 `Publisher`, `Subscriber`, `Subscription`, `Processor`라는 네 가지 핵심 인터페이스로 구성되어 있습니다. 114 | 115 | 각 구성 요소를 간단한 설명과 함께 예시로 정리해드릴게요. 116 | 117 | --- 118 | 119 | ### 1. `Publisher` 120 | 121 | - **역할**: 데이터를 발행(publish)하는 주체. 122 | - **기능**: `Subscriber`가 구독하면(`subscribe`) 데이터를 전달. 123 | - **메서드**: 124 | 125 | ```java 126 | void subscribe(Subscriber subscriber); 127 | ``` 128 | 129 | 예시: 130 | 131 | ```java 132 | Flow.Publisher publisher = new MyPublisher(); 133 | publisher.subscribe(new MySubscriber()); 134 | ``` 135 | 136 | 137 | --- 138 | 139 | ### 2. `Subscriber` 140 | 141 | - **역할**: 데이터를 수신(receive)하는 주체. 142 | - **메서드**: 143 | 144 | ```java 145 | void onSubscribe(Flow.Subscription subscription); // 구독 시작 146 | void onNext(T item); // 새 데이터 수신 147 | void onError(Throwable throwable); // 에러 발생 시 호출 148 | void onComplete(); // 데이터 전송 완료 시 호출 149 | ``` 150 | 151 | 핵심: `onSubscribe()`에서 받은 `Subscription`을 통해 얼마나 데이터를 받을지 요청해야 함 (`subscription.request(n)`) 152 | 153 | 154 | --- 155 | 156 | ### 3. `Subscription` 157 | 158 | - **역할**: `Publisher`와 `Subscriber` 간 연결을 표현하며, 데이터 요청과 구독 취소 관리. 159 | - **메서드**: 160 | 161 | ```java 162 | void request(long n); // n개의 아이템 요청 163 | void cancel(); // 구독 취소 164 | ``` 165 | 166 | 예시: 167 | 168 | ```java 169 | @Override 170 | public void onSubscribe(Flow.Subscription subscription) { 171 | this.subscription = subscription; 172 | subscription.request(1); // 처음에는 하나만 요청 173 | } 174 | ``` 175 | 176 | 177 | --- 178 | 179 | ### 4. `Processor` 180 | 181 | - **역할**: `Publisher`이자 `Subscriber`인 중간 처리자. 데이터를 받아서 가공 후 다른 Subscriber에게 전달. 182 | - **예시**: 데이터 필터링, 변환 등 중간 단계 처리에 유용. 183 | 184 | 구조 예시: 185 | 186 | ```java 187 | class MyProcessor implements Flow.Processor { 188 | // String을 받아서 Integer로 변환해 다음 Subscriber에게 전달 189 | } 190 | ``` 191 | 192 | 193 | --- 194 | 195 | 196 | 197 | ![image](https://github.com/user-attachments/assets/38847ad0-ab95-483a-8b4e-09ca91f819bf) 198 | 199 | 흐름: 200 | 201 | **1. `main` → `Publisher`** 202 | 203 | `publisher.subscribe(subscriber);` 204 | 205 | - 메인 프로그램에서 `Publisher`에게 `Subscriber`를 등록함. 206 | 207 | --- 208 | 209 | **2. `Publisher` → `Subscriber`** 210 | 211 | `onSubscribe(Subscription subscription)` 212 | 213 | - `Publisher`는 `Subscriber`에게 `Subscription` 객체를 전달하며 구독을 시작함. 214 | 215 | --- 216 | 217 | **3. `Subscriber` → `Subscription`** 218 | 219 | `subscription.request(n);` 220 | 221 | - `Subscriber`는 원하는 데이터 개수 `n`를 요청함. 222 | 223 | 이게 바로 **역압력 (Backpressure)** 개념 (`Subscriber`가 감당할 수 있는 만큼만 요청) 224 | 225 | 226 | --- 227 | 228 | **4. `Subscription` → `Subscriber`** 229 | 230 | `onNext(data);` 231 | 232 | - 요청한 수만큼 데이터 항목을 `onNext()`로 전송. 233 | 234 | --- 235 | 236 | **5. `Subscriber` → `Subscription`** 237 | 238 | `subscription.request(n);` 239 | 240 | - 또 다른 요청. 이 흐름은 계속 반복될 수 있음. 241 | 242 | --- 243 | 244 | **6. 완료 또는 에러** 245 | 246 | `onComplete();` `onError(Throwable)` 247 | 248 | - 데이터 스트림이 정상적으로 끝났다면 `onComplete()` 호출. 249 | - 에러 발생 시에는 `onError()`로 알림. 250 | 251 | --- 252 | 253 | ## 17.2.4 자바는 왜 FLOW API 구현을 제공하지 않는가? 254 | 255 | API 만들 당시 Akka, RxJava 등 다양한 리액티브 스트림의 자바 코드 라이브러리가 존재했기 때문 256 | 257 | # 17.3 리액티브 라이브러리 RxJava 사용하기 258 | 259 | **RxJava (Reactive Extensions for Java)** 260 | 261 | → **이벤트 기반 비동기 프로그래밍**을 지원하는 라이브러리 262 | 263 | → 데이터 스트림을 관찰하고, 변환하고, 결합하고, 제어할 수 있음 264 | 265 | RxJava는 **Observable 패턴** + **Iterator 패턴** + **함수형 프로그래밍** + **Backpressure**를 조합한 모델 266 | 267 | ### **RxJava가** 권장되지 않는 상황 268 | 269 | - 천 개 이하의 요소를 가진 스트림이나 마우스 움직임, 터치 이벤트 등 역압력을 적용하기 힘든 gui 이벤트 270 | - 자주 발생하지 않는 종류의 이벤트 271 | 272 | → 역압력 적용 X (모든 구독자는 구독 객체의 **`request(Long.MAX_VALUE)`**를 통해 역압력을 끌 수 있음) 273 | 274 | ## 17.3.1 Observable 만들고 사용하기 275 | 276 | ### Observable이란? 277 | 278 | **데이터를 발행(emit)**하고, 그것을 **구독자(Observer)가 받아서 처리**하게 해주는 **비동기 데이터 스트림의 출발점 → `RxJava의 Publisher 역할 수행`** 279 | 280 | | 메서드 | 설명 | 281 | | --- | --- | 282 | | `just(T...)` | 고정된 값들을 발행 | 283 | | `fromArray()` / `fromIterable()` | 배열이나 컬렉션을 발행 | 284 | | `create()` | 커스텀 발행 (onNext/onError/onComplete 직접 호출) | 285 | | `interval()` | 일정 시간마다 발행 | 286 | | `range()` | 숫자 범위 발행 | 287 | | `empty()` / `never()` / `error()` | 특별한 상황 제어용 | 288 | 289 | **메서드** 290 | 291 | ```java 292 | void onSubscribe(Disposable d); 293 | void onNext(T item); 294 | void onError(Throwable throwable); 295 | void onComplete(); 296 | ``` 297 | 298 | **RxJava의 api는 Flow api보다 훨씬 유연함** 299 | 300 | ex) 이벤트 수신하는 **consumer**의 **onNext** 메서드만 구현하여, **`Observable onPerSec`**에 가입하고 뉴욕에서 매 초마다 발생하는 온도를 출력하는 기능을 코드 한 줄로 구현 가능 301 | 302 | ```java 303 | onePerSec.subscribe(i -> System.out.println(TempInfo.fetch("New York"))); 304 | ``` 305 | 306 | 하지만 위 코드를 메인 메서드에 추가해서 실행할 시 출력 X 307 | 308 | → 매 초마다 정보를 발행하는 Observable이 RxJava의 연산 스레드 풀 즉 데몬 스레드에서 실행되기 때문 309 | 310 | → 따라서 blockingSubscrive 311 | 312 | →**`blockingSubscribe`** 메서드를 사용 시 문제 해결 가능 313 | 314 | ```java 315 | onePerSec.blockingSubscribe(i -> System.out.println(TempInfo.fetch("New York"))); 316 | ``` 317 | 318 | 하지만 설계 상 온도를 가져오는 기능이 임의로 실패하기 때문에 에러 발생 319 | 320 | 예외처리를 구현하지 않았기에 처리되지 않은 예외가 사용자에게 직접 보여짐 321 | 322 | 예제의 난도를 높여 에러 처리만 추가하는 것이 아닌 온도를 직접 출력하지 않고 사용자에게 팩토리 메서드를 제공해 매 초마다 온도를 방출하는 Observable을 반환 323 | 324 | ```java 325 | // 1초마다 한개의 온도를 방출하는 Observable 326 | public static Observable getTemperature(String town) { 327 | return Observable.create(emitter -> Observable.interval(1, TimeUnit.SECONDS).subscribe(i -> { 328 | if (!emitter.isDisposed()) { 329 | if (i >= 5) { 330 | emitter.onComplete(); 331 | } 332 | else { 333 | try { 334 | emitter.onNext(TempInfo.fetch(town)); 335 | } 336 | catch (Exception e) { 337 | emitter.onError(e); 338 | } 339 | } 340 | } 341 | })); 342 | ``` 343 | 344 | | 기능 | 설명 | 345 | | --- | --- | 346 | | `Observable.interval` | 1초 간격으로 tick 발행 | 347 | | `TempInfo.fetch(town)` | 현재 온도 조회 (예: 랜덤 생성, API 호출 등) | 348 | | `onNext()` | 온도 데이터 전송 | 349 | | `onError()` | 예외 발생 시 처리 | 350 | | `onComplete()` | 5회 이후 스트림 종료 | 351 | | `emitter.isDisposed()` | 구독이 취소되었는지 확인해서 자원 낭비 방지 | 352 | 353 | **`emitter`란?** 354 | 355 | `emitter`는 `Observable.create(emitter -> {...})` 구문 안에서 주어지며, 356 | 357 | **데이터를 구독자에게 전달하는 통로** 358 | 359 | | 메서드 | 설명 | 360 | | --- | --- | 361 | | `onNext(T item)` | 새 데이터를 발행 | 362 | | `onError(Throwable e)` | 오류 발생을 알림 (스트림 종료됨) | 363 | | `onComplete()` | 더 이상 발행할 데이터가 없음을 알림 | 364 | | `isDisposed()` | 구독자가 구독을 취소했는지 확인 (자원 낭비 방지용) | 365 | 366 | 수신한 온도를 출력하는 **`Observer`** 367 | 368 | ```java 369 | public class TempObserver implements Observer { 370 | 371 | @Override 372 | public void onComplete() { 373 | System.out.println("Done!"); 374 | } 375 | 376 | @Override 377 | public void onError(Throwable throwable) { 378 | System.out.println("Got problem: " + throwable.getMessage()); 379 | } 380 | 381 | @Override 382 | public void onSubscribe(Disposable disposable) {} 383 | 384 | @Override 385 | public void onNext(TempInfo tempInfo) { 386 | System.out.println(tempInfo); 387 | } 388 | 389 | } 390 | ``` 391 | 392 | ```java 393 | public class Main { 394 | 395 | public static void main(String[] args) { 396 | Observable observable = getTemperature("New York"); 397 | observable.blockingSubscribe(new TempObserver()); 398 | } 399 | } 400 | ``` 401 | 402 | - `getTemperature("New York")`: 403 | 404 | → `Observable`를 리턴하는 **팩토리 메서드**로, 1초마다 뉴욕의 온도를 발행 (앞서 만든 `getTemperature()` 메서드) 405 | 406 | - `.blockingSubscribe(new TempObserver())`: 407 | 408 | → `TempObserver`를 통해 온도를 구독하면서 처리함 409 | 410 | → **blocking**이기 때문에 메인 스레드가 **스트림이 끝날 때까지 대기** (보통 콘솔 앱에서 테스트용으로 사용) 411 | 412 | 413 | ## 17.3.2 Observable을 변환하고 합치기 414 | 415 | ![image](https://github.com/user-attachments/assets/c21c223d-ff40-4d7d-8ddd-b4e938fdf11e) 416 | 417 | - Observable이 시간에 따라 데이터를 발행하고 (`onNext`) 418 | - 연산자 (`flip`, `map`, `merge` 등)를 통해 **데이터를 변형하거나**, **다른 스트림과 조합하거나**, **종료 또는 에러 처리**를 함 419 | - 아래로 향하는 화살표는 Observable이 데이터를 **구독자에게 전달**하는 흐름을 나타냄 420 | 421 | ![image](https://github.com/user-attachments/assets/ae67b9b9-d0c1-4905-85a5-69635426444e) 422 | 423 | ### `merge` 424 | 425 | - **두 개 이상의 Observable**을 합쳐서 **단일 스트림**으로 만듦 426 | - 발행되는 순서에 상관없이 **동시적으로 처리** 427 | - 그림에서는 위쪽 스트림과 아래쪽 스트림의 이벤트들을 **하나의 타임라인에 결합**함 428 | 429 | ```java 430 | Observable o1 = Observable.just(1, 2, 3); 431 | Observable o2 = Observable.just(4, 5); 432 | Observable.merge(o1, o2) 433 | // 결과: 1, 2, 3, 4, 5 (순서는 발행 타이밍에 따라 달라질 수 있음) 434 | ``` 435 | 436 | **화씨 → 섭씨 변환 (map 사용)** 437 | 438 | ```java 439 | public static Observable getCelsiusTemperature(String town) { 440 | return getTemperature(town) 441 | .map(temp -> new TempInfo(temp.getTown(), 442 | (temp.getTemp() - 32) * 5 / 9)); 443 | } 444 | ``` 445 | 446 | - `getTemperature(town)` → 화씨 단위의 온도를 발행 447 | - `.map(...)` → 각 온도를 섭씨로 변환 448 | - 결국 사용자에겐 **섭씨로 변환된 온도 스트림**이 제공됨 449 | 450 | ```java 451 | public static Observable getCelsiusTemperatures(String... towns) { 452 | return Observable.merge(Arrays.stream(towns) 453 | .map(TempObservable::getCelsiusTemperature) 454 | .collect(toList())); 455 | } 456 | ``` 457 | 458 | - `Arrays.stream(towns)` → 여러 도시 이름(문자열 배열)을 스트림으로 변환 459 | - `.map(TempObservable::getCelsiusTemperature)` → 각 도시에 대해 섭씨 온도를 발행하는 `Observable` 생성 460 | - `.collect(toList())` → 각 도시별 Observable을 리스트로 수집 461 | - `Observable.merge(...)` → 여러 도시의 온도 Observable들을 **하나의 스트림으로 병합** 462 | - 결국 사용자에겐 **여러 도시의 섭씨 온도를 동시에 발행하는 단일 Observable 스트림**이 제공됨 463 | -------------------------------------------------------------------------------- /1장 자바 8, 9, 10, 11 무슨 일이 일어나고 있는가/ch1.md: -------------------------------------------------------------------------------- 1 | **자바가 거듭 변화하는 이유** 2 | 3 | **컴퓨팅 환경의 변화** 4 | 5 | **자바에 부여되는 시대적 변화 요구** 6 | 7 | **자바 8과 자바9의 새로운 핵심 기능 소개** 8 | 9 | 시대 변화에 따른 빅데이터를 효과적으로 처리할 필요성 증가 → 자바 8의 병렬 프로세싱을 위한 **멀티코어 병렬성 강화** 10 | 11 | ### 1.2 자바 8 설계의 밑바탕을 이루는 3가지 프로그래밍 개념 12 | 13 | 1. **스트림처리** 14 | 15 | 한 번에 한 개 식 만들어지는 연속적인 데이터 항목들의 모임, 입력/출력 스트림은 데이터를 한개씩 처리 16 | 17 | [java.util.stream](http://java.util.stream) , Stream → 스트림 API는 조립라인과 같이 어떤 항목을 연속으로 제공하는 기능 18 | 19 | 자바 8에서 고수준으로 스트림을 추상화함으로써 처리 가능해짐(?) 20 | 21 | 스레드를 사용하지 않고 공짜 병렬성 획득 가능 → 4장부터 7장에 걸쳐 설명 22 | 23 | 2. **동적 파리미터화로 메서드에 코드 전달** 24 | 25 | 자바 8에서는 메서드(작성한 코드)를 다른 메서드의 인수로 넘겨주는 기능을 제공→ 2,3,18,19 장 26 | 27 | ![image](https://github.com/user-attachments/assets/00efbca2-1c6f-41d7-a4ca-0d264213fd13) 28 | 29 | 30 | 3. **병렬성과 공유 가변 데이터** 31 | 32 | 공짜 병렬성을 위해 스트림을 이용하되, 안전하게 코드들을 동시 실행하기 위해 가변 데이터에 접근하지 않아야 함 → pure func / side-effect-free func / stateless func → 18, 19장 33 | 34 | 공유된 변수나 객체가 있을 시 발생하는 병렬성 문제 ex) 두 프로세스가 공유된 변수를 동시에 바꾸려할 시? 기존과 같이 synchronized를 이용 시에는 시스템 성능에 악영향(다중 처리 코어에서는 코드가 순차적으로 실행되어야 하므로) 따라서 자바 8 스트림 이용 35 | 36 | - chatgpt 설명 37 | 38 | 자바 8에서 도입된 **스트림 API**는 병렬성과 공유 가변 데이터 문제를 개선하면서도 동기화(`synchronized`)로 인한 비효율성을 최소화할 수 있도록 설계되었습니다. 그 이유는 다음과 같은 스트림의 특성과 원칙 때문입니다. 39 | 40 | --- 41 | 42 | ### 1. **스트림의 선언형 프로그래밍** 43 | 44 | 스트림 API는 데이터를 처리하는 방식을 선언형으로 정의합니다. 즉, **"무엇을 해야 하는지"**를 정의하며, **"어떻게 수행할지"**는 스트림 라이브러리와 실행 환경이 처리합니다. 이 선언형 접근 방식은 다음과 같은 이점을 제공합니다: 45 | 46 | - 데이터 처리 작업이 병렬적으로 수행될 수 있도록 내부적으로 최적화됩니다. 47 | - 사용자는 동기화를 고려하지 않고도 데이터 처리를 정의할 수 있습니다. 48 | 49 | ### 예시: 50 | 51 | ```java 52 | List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 53 | 54 | // 병렬 스트림 사용 55 | int sum = numbers.parallelStream() 56 | .filter(n -> n % 2 == 0) 57 | .mapToInt(n -> n * n) 58 | .sum(); 59 | 60 | ``` 61 | 62 | - `parallelStream()`은 데이터 처리를 병렬로 수행합니다. 63 | - 병렬화의 복잡성은 스트림 API가 내부적으로 처리하므로, 사용자는 병렬 처리의 세부 사항을 관리하지 않아도 됩니다. 64 | 65 | --- 66 | 67 | ### 2. **스트림은 **불변성(Immutability)**을 기반으로 설계** 68 | 69 | 스트림의 요소는 본질적으로 **불변(immutable)**으로 처리됩니다. 공유 가변 데이터를 다루는 대신, 스트림 연산(예: `filter`, `map`, `reduce`)은 데이터의 복사본이나 파이프라인 단위로 새 데이터를 생성합니다. 70 | 71 | - **불변성** 덕분에 병렬 처리 시 데이터 경합(data race)이 발생하지 않습니다. 72 | - 공유 상태를 수정할 필요가 없으므로, `synchronized`와 같은 비용이 큰 동기화 메커니즘이 불필요합니다. 73 | 74 | ### 예시: 75 | 76 | ```java 77 | List names = Arrays.asList("Alice", "Bob", "Charlie"); 78 | 79 | // 기존 데이터를 수정하지 않고 새로운 스트림 생성 80 | List upperCaseNames = names.stream() 81 | .map(String::toUpperCase) 82 | .collect(Collectors.toList()); 83 | 84 | // 기존 리스트는 변하지 않음 85 | System.out.println(names); // [Alice, Bob, Charlie] 86 | System.out.println(upperCaseNames); // [ALICE, BOB, CHARLIE] 87 | 88 | ``` 89 | 90 | --- 91 | 92 | ### 3. **병렬 스트림(parallelStream)의 병렬 처리** 93 | 94 | 병렬 스트림은 **Fork/Join 프레임워크**를 기반으로 동작하며, 데이터를 여러 청크로 나누고 각 청크를 병렬로 처리합니다. 이 방식은 다음과 같은 특징을 제공합니다: 95 | 96 | ### (1) **데이터 분할 및 작업 병렬화** 97 | 98 | - 스트림 API는 데이터 소스를 분할하여 병렬로 처리합니다. 99 | - 각 분할된 데이터 청크는 별도의 스레드에서 독립적으로 처리됩니다. 100 | 101 | ### (2) **결과 병합** 102 | 103 | - 병렬 처리된 결과는 작업이 완료된 후에 자동으로 병합됩니다. 104 | 105 | ### (3) **효율성** 106 | 107 | - 병렬 처리는 공유 가변 데이터에 대한 접근을 최소화하므로 동기화 오버헤드가 거의 없습니다. 108 | - 작업이 독립적이기 때문에 데이터 충돌이 발생하지 않아 추가적인 `synchronized` 처리가 불필요합니다. 109 | 110 | ### 예시: 111 | 112 | ```java 113 | List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 114 | 115 | int sum = numbers.parallelStream() 116 | .filter(n -> n % 2 == 0) 117 | .mapToInt(n -> n) 118 | .sum(); 119 | 120 | System.out.println(sum); // 병렬로 처리된 결과 121 | 122 | ``` 123 | 124 | --- 125 | 126 | ### 4. **Fork/Join 프레임워크의 역할** 127 | 128 | 스트림의 병렬 처리 방식은 자바 7에서 도입된 **Fork/Join 프레임워크**를 기반으로 동작합니다. Fork/Join 프레임워크는 작업을 분할(fork)하고, 병렬로 처리한 후 다시 병합(join)하는 방식을 사용합니다. 스트림 API는 이 프레임워크를 활용하여 병렬성을 자동으로 관리합니다. 129 | 130 | ### 동기화와의 비교: 131 | 132 | - 동기화는 공유 자원에 대한 접근을 순차적으로 처리하므로 병렬성을 제한하고 성능 저하를 초래합니다. 133 | - Fork/Join 프레임워크는 작업을 독립적으로 병렬 처리하므로 동기화의 필요성을 줄입니다. 134 | 135 | ### Fork/Join의 동작 방식: 136 | 137 | 1. 데이터를 청크로 나눔. 138 | 2. 각 청크를 별도의 워커 스레드에서 병렬로 처리. 139 | 3. 최종적으로 결과를 병합. 140 | 141 | --- 142 | 143 | ### 5. **최적화된 연산 파이프라인** 144 | 145 | 스트림은 데이터 처리 파이프라인을 형성하며, **중간 연산(lazy evaluation)**과 **최종 연산(terminal operation)**을 분리합니다. 146 | 147 | - 중간 연산은 **게으르게(lazy)** 평가되어 최적의 병렬 처리 구조를 제공합니다. 148 | - 병렬 스트림은 작업을 분산하고, 가능한 효율적으로 실행할 수 있는 최적의 경로를 자동으로 결정합니다. 149 | 150 | ### 예시: 151 | 152 | ```java 153 | List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 154 | 155 | int result = numbers.parallelStream() 156 | .filter(n -> n % 2 == 0) // 게으른 연산 157 | .mapToInt(n -> n * n) // 게으른 연산 158 | .sum(); // 최종 연산 159 | 160 | ``` 161 | 162 | --- 163 | 164 | ### 6. **공유 상태를 배제하도록 설계** 165 | 166 | 스트림 API는 기본적으로 공유 상태를 배제하도록 설계되어 있습니다. 스트림 연산은 **순수 함수**(pure function)를 사용해 데이터의 변형을 수행하며, 상태를 외부에서 공유하거나 변경하지 않습니다. 167 | 168 | - 병렬 스트림 사용 시, 스트림 연산은 상태를 공유하지 않기 때문에 동기화의 필요성이 사라집니다. 169 | - 하지만, 사용자 정의 상태 공유가 포함된 경우(예: 컬렉션 수정), 동기화 문제가 다시 발생할 수 있습니다. 이런 경우는 반드시 피해야 합니다. 170 | 171 | ### 비권장 예시(상태 공유): 172 | 173 | ```java 174 | List numbers = Arrays.asList(1, 2, 3, 4, 5); 175 | List results = new ArrayList<>(); 176 | 177 | numbers.parallelStream() 178 | .map(n -> { 179 | results.add(n * n); // 공유 상태 수정 180 | return n; 181 | }) 182 | .forEach(System.out::println); 183 | 184 | // 결과: ConcurrentModificationException 가능 185 | 186 | ``` 187 | 188 | ### 권장 예시(상태 공유 없음): 189 | 190 | ```java 191 | List results = numbers.parallelStream() 192 | .map(n -> n * n) 193 | .collect(Collectors.toList()); 194 | 195 | ``` 196 | 197 | --- 198 | 199 | ### 요약 200 | 201 | 자바 8의 스트림 API는 다음 이유로 `synchronized` 없이도 병렬성과 공유 가변 데이터 문제를 해결할 수 있었습니다: 202 | 203 | 1. **불변성을 기반으로 한 데이터 처리**: 공유 가변 상태를 배제함. 204 | 2. **Fork/Join 프레임워크 사용**: 병렬 작업을 효율적으로 처리. 205 | 3. **최적화된 병렬 처리 구조**: 데이터 청크 분할과 병합을 자동 관리. 206 | 4. **선언형 프로그래밍**: 동기화와 병렬 처리의 복잡성을 라이브러리 내부에서 해결. 207 | 208 | 결과적으로, 스트림 API는 병렬성을 간단하고 효율적으로 활용하면서도 공유 상태로 인한 동기화 오버헤드를 제거했습니다. 209 | 210 | 211 | ### 1.3 자바함수 212 | 213 | 자바 8의 특징 214 | 215 | 1. 함수를 “값”과 같이 처리한다 → 런타임에 메서드가 전달 가능해졌다 216 | 217 | 기존에는 함수와 클래스를 다른 값(”abc”(string 형식), new Integer(11), new HashMap(100), 배열 등)과 달리 자유롭게 전달이 되지 않았기에 값이 될 수 없었다. 218 | 219 | 1. **메서드 & 람다** 220 | 221 | **메서드 참조 method reference :: → 3장** 222 | 223 | ```jsx 224 | File[] hiddenFiles = new File(".").listFiles(new FileFilter(){ 225 | public boolean accept(File file){ 226 | return file.isHidden(); 227 | } 228 | })' 229 | ``` 230 | 231 | 자바 8 → 232 | 233 | ```jsx 234 | File[] hiddenFiles = new File(".").listFiles(File::isHidden); 235 | ``` 236 | 237 | 기존에는 File class에 이미 isHidden이라는 메서드가 있음에도 FileFilter로 감싼 다음에 FileFilter를 인스턴스화해야 했음 238 | 239 | ![image](https://github.com/user-attachments/assets/271fe4ca-451d-46d9-8da7-a72c4fe7d244) 240 | 241 | 242 | **람다 : 익명(anonymous) 함수** 243 | 244 | named 함수 뿐만 아니라 람다 함수를 포함하여 함수도 값으로 취급할 수 있게 됨 245 | 246 | ex) (int x) → x + 1 247 | 248 | 2. **코드 넘겨주기** 249 | 250 | ```java 251 | public static List filterGreenApples(List inventory) { 252 | List result = new ArrayList<>(); 253 | for (Apple apple : inventory) { 254 | if ("green".equals(apple.getColor())) { 255 | result.add(apple); 256 | } 257 | } 258 | return result; 259 | } 260 | 261 | ``` 262 | 263 | ```java 264 | public static boolean isGreenApple(Apple apple) { 265 | return "green".equals(apple.getColor()); 266 | } 267 | 268 | public static boolean isHeavyApple(Apple apple) { 269 | return apple.getWeight() > 150; 270 | } 271 | 272 | 273 | ############################################# 274 | 275 | public static List filterApples(List inventory, Predicate p) { 276 | List result = new ArrayList<>(); 277 | for (Apple apple : inventory) { 278 | if (p.test(apple)) { 279 | result.add(apple); 280 | } 281 | } 282 | return result; 283 | } 284 | 285 | ########################################### 286 | 287 | filterApples(inventory, Apple::isGreenApple); 288 | ``` 289 | 290 | 코드의 재사용성을 높여줄 수 있음 291 | 292 | **메서드 전달에서 람다로** 293 | 294 | ```java 295 | filterApples(inventory, (Apple a) -> GREEN.equals(a.getColor())); 296 | ``` 297 | 298 | 병렬성을 만약 고려하지 않았더라면 이러한 filter에 특화된 함수를 만들었을 것 299 | 300 | **스트림** 301 | 302 | ```java 303 | Map> transactionsByCurrencies = new HashMap<>(); 304 | for(Transaction transaction: transactions){ 305 | if(transaction.getPrice()>1000){ 306 | Currency currency = transaction.getCurrency(); 307 | List transactionsForCurrency = transactionsByCurrencies.get(currency); 308 | ,,,, 309 | transactionsForCurrency.add(transaction); 310 | } 311 | } 312 | ``` 313 | 314 | ```java 315 | public static void main(String[] args) throws IOException { 316 | Map> transactionsByCurrencies = 317 | transactions.stream() 318 | .filter((transaction t) -> t.getPrice() > 1000) 319 | .collect(groupingBy(Transaction::getCurrency)); 320 | } 321 | ``` 322 | 323 | 컬렉션으로 필터링 코드를 수행 시 반복 과정을 for-each 루프를 통해 직접 처리해야했다.(외부 반복) 반면 스트림 API를 이용하면 루프를 신경 쓸 필요 없이 라이브러리 내부에서 모든 데이터가 처리된다.(내부 반복) 324 | 325 | ![image](https://github.com/user-attachments/assets/bcc39356-ecc3-4c93-bdba-5d1aed59a5ed) 326 | 327 | 328 | - 내부 반복자를 사용하면 컬렉션 내부에서의 요소를 어떻게 반복 시킬지는 컬렉션에 맡겨 둔다. 개발자는 요소 처리에만 집중할 수 있다. 329 | - 멀티코어 CPU를 최대한 활용하기 위해 요소들을 분배시켜 병렬작업을 도와주기 때문에 하나씩 처리하는 순차적인 외부 반복자보다는 내부 반복자를 사용하는 것이 좀 더 효율적이다. 330 | 331 | ### 1.4 멀티스레딩은 어렵다 332 | 333 | 자바 버전에서 제공하는 스레드 API로 멀티스레딩 코드를 구현해서 병렬성을 이용하는 것은 쉽지 않다 334 | 335 | ![image](https://github.com/user-attachments/assets/7ac5661a-ecf9-4918-9c5e-b263587f080a) 336 | 337 | 그렇기에 스트림을 이용 338 | 339 | 자바 8의 스트림 API 340 | 341 | 1. 컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제 해결 342 | 1. 데이터 필터링 / 데이터 추출 / 데이터 그룹화 등 자주 사용되는 기능 제공 343 | 2. 멀티코어 활용 어려움 해결 344 | 345 | ### 1.5 디폴트 메서드와 자바 모듈 346 | 347 | 디폴트 메서드를 이용하여 기존의 클래스를 수정하지 않고 원래의 인터페이 설계를 자유롭게 확장 가능해짐 default라는 새로운 키워드를 지원함 348 | 349 | ```java 350 | interface InterfaceA { 351 | default void greet() { 352 | System.out.println("Hello from InterfaceA"); 353 | } 354 | } 355 | 356 | interface InterfaceB { 357 | default void greet() { 358 | System.out.println("Hello from InterfaceB"); 359 | } 360 | } 361 | 362 | public class MyClass implements InterfaceA, InterfaceB { 363 | @Override 364 | public void greet() { 365 | InterfaceA.super.greet(); // 명시적으로 호출 366 | InterfaceB.super.greet(); 367 | System.out.println("Hello from MyClass"); 368 | } 369 | } 370 | 371 | ``` 372 | 373 | 자바 8에서 List에서 직접 sort 메서드를 호출할 수 있는 이유도 default를 이용해 모든 구현 374 | -------------------------------------------------------------------------------- /5장 스트림 활용/ch5.md: -------------------------------------------------------------------------------- 1 | ## 5.1 필터링 2 | 3 | ### 5.1.1 프레디케이트로 필터링 4 | 5 | Filter 메서드는 프레디케이트를 인수로 받아 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다. 6 | 7 | - 이떄 프레디케이트는 특정 조건에 대한 불리언 타입의 반환 함수이다. 8 | 9 | ### 5.1.2 고유 요소 필터링 10 | 11 | 고유 여부에 대해서 객체의 hashCode, equals를 이용하여 체크하는 distinct메서드를 지원한다. 12 | 13 | ![Image](https://github.com/user-attachments/assets/4bf9462a-d648-43ed-8fa4-01d2468e085b) 14 | ## 5.2 스트림 슬라이싱 15 | 16 | ### 5.2.1 프레디케이트를 이용한 슬라이싱 17 | 18 | - **`takeWhile(Predicate predicate)`** 19 | - **조건이 참(`true`)인 동안 요소를 유지** 20 | - 조건이 **거짓(`false`)이 되는 순간 멈추고, 그 이후의 요소는 포함하지 않음** 21 | - **`dropWhile(Predicate predicate)`** 22 | - **조건이 참(`true`)인 동안 요소를 버림** 23 | - 조건이 **거짓(`false`)이 되는 순간부터 남은 모든 요소를 유지** 24 | 25 | ### 5.2.2 스트림 축소 26 | 27 | 스트림은 주어진 값 이하의 크기를 가지는 **새로운 스트림**을 반환하는 limit(n) 메서드를 지원한다. 최대 요소 n개를 반환할 수 있다. 28 | 29 | ![Image](https://github.com/user-attachments/assets/b95c4334-3a5c-40fa-816d-d9f9eea63d6a) 30 | ### 5.2.3 요소 건너뛰기 31 | 32 | 처음 n개 요소를 제외한 **스트림**을 반환하는 skip(n) 메서드를 지원한다. 33 | 34 | 만약 n개보다 적은 요소를 포함하는 스트림에서 skip(n)을 호출 시 빈 스트림이 반환된다. 35 | 36 | ![Image](https://github.com/user-attachments/assets/1c74acad-e10d-4adf-8c6e-210f2de5dbeb) 37 | ## 5.3 매핑 38 | 39 | ### 5.3.1 스트림의 각 요소에 함수 적용하기 40 | 41 | 함수를 인수로 받는 map메서드를 지원한다. 인수로 제공된 함수는 각 요소에 적용되고 함수를 적용한 결과가 새로운 요소로 매핑된다. 42 | 43 | 이때 기존 요소를 고치는(modify)게 아닌 ‘새로운 버전을 만든다’에 가까운 개념이므로 변환에 가까운 매핑이란 개념을 사용한다. 44 | 45 | 이 과정에서 메소드 참조를 사용할 수 있으며, 여러 개의 map을 연속으로 사용할 수 있다. 46 | 47 | ### 5.3.2 스트림 평면화 48 | 49 | 스트림에 map을 사용한 결과가 중첩된 스트림일 수 있다. 가령 각각의 단어에 대해서 고유한 문자로 이루어진 result를 만들어내고 싶다고 가정해보자 50 | 51 | ```java 52 | import java.util.List; 53 | import java.util.stream.Collectors; 54 | 55 | public class Main { 56 | public static void main(String[] args) { 57 | List words = List.of("Hello", "World"); 58 | 59 | List result= words.stream() 60 | .map(word -> word.split("")) // 각 단어를 문자 배열로 변환 (Stream) 61 | .distinct() 62 | .collect(toList()); 63 | 64 | } 65 | } 66 | ``` 67 | 68 | map 메소드에 전달된 람다는 각 단어들의 String[ ](문자열 배열) 이므로 계산 결과의 반환 형태가 Stream이다. 우리가 원하는 것은 Stream의 반환 형태를 원하기 때문에 이와는 다른 방법을 사용해야한다. 69 | 70 | ![Image](https://github.com/user-attachments/assets/92697d71-527d-4a2d-8561-cd07af421bb6) 71 | - 해결책 1 - map과 Arrays.stream 활용 72 | 73 | ```java 74 | import java.util.List; 75 | import java.util.Arrays; 76 | import java.util.stream.Collectors; 77 | 78 | public class Main { 79 | public static void main(String[] args) { 80 | List words = List.of("Hello", "World"); 81 | 82 | List result = words.stream() 83 | .map(word -> word.split("")) // 각 단어를 문자 배열로 변환 (Stream) 84 | .map(Arrays::stream) // String[]을 Stream으로 변환 (Stream>) 85 | .distinct() // ⚠️ 이 시점에서 중첩된 Stream이므로 distinct가 정상 동작하지 않음 86 | .collect(toList()); 87 | } 88 | } 89 | 90 | ``` 91 | 92 | 접근법은 좋았지만 결국 스트림 리스트인 List>가 만들어지기 떄문에 해결법이 될 수 없다. 93 | 94 | - 해결책 2 - flatMap활용 95 | 96 | flatMap은 생성된 스트림을 하나의 스트림으로 평면화 할 수 있다. flatMap자체가 각 배열을 스트림이 아닌 스트림의 콘텐츠로 매핑하기 떄문이다. 97 | ```java 98 | List number1 = Arrays.asList(1,2,3); 99 | List number2 = Arrays.asList(3,4); 100 | List pairs = number1.stream() 101 | .flatMap(i -> number2.stream() 102 | .filter(j -> (i + j) % 3 == 0) 103 | .map(j -> new int[]{i, j}) 104 | ) 105 | .collect(toList()); 106 | ``` 107 | 108 | ![Image](https://github.com/user-attachments/assets/013b4fb3-d989-4829-a417-ac2a832bd12e) 109 | ## 5.4 검색과 매칭 110 | 111 | ### 5.4.1 프레디케이트가 적어도 한 요소와 일치하는지 확인 112 | 113 | 프레디케이트가 주어진 스트림에서 **적어도 한 요소**와 일치하는지 확인할 때에는 anyMatch 메서드를 활용한다. 114 | 115 | ```java 116 | boolean isVegetarian = menu.stream().anyMatch(Dish::isVegetarian); 117 | ``` 118 | 119 | anyMatch는 불리언을 반환하므로 **최종연산**이다. 120 | 121 | ### 5.4.2 프레디케이트가 모든 요소와 일치하는지 검사 122 | 123 | allMatch 메서드는 스트림의 모든 요소가 주어진 프레디케이트와 일치하는지 검사한다. 124 | 125 | ```java 126 | boolean isHealthy = menu.stream().allMatch(dish -> dish.getCalories < 1000); 127 | ``` 128 | 129 | - NoneMatch 130 | - NoneMatch는 allMatch와 반대의 연산을 수행한다. 주어진 프레디케이트와 일치하는 요소가 없는지를 확인한다. 131 | 132 | ```java 133 | boolean isMatch = menu.stream().noneMatch(Dish -> dish.getCalories() < 1000); 134 | ``` 135 | 136 | 137 | anyMatch, allMatch 그리고 noneMatch는 모두 스트림 쇼트서킷 기법(자바의 &&나 ||와 같은)을 사용하여 무한한 요소에 대한 연산에서 이점을 가져간다.(혹은 무한하지 않더라도 다량의 데이터에 대한) 138 | 139 | ### 5.4.3 요소 검색 140 | 141 | findAny 메서드는 현재 스트림에서 임의의 요소를 반환한다. 142 | 143 | ```java 144 | Optional dish = menu.stream() 145 | .filter(Dish::isVegetarian) 146 | .findAny(); //findAny는 Optional 객체를 반환 147 | ``` 148 | 149 | 스트림 파이프라인은 내부적으로 단일 과정으로 실행될 수 있도록 최적화된다. 즉, 쇼트서킷을 활용하여 결과를 찾는 즉시 실행을 종료한다. 다만 현재 코드에 쓰인 Optional이라는 표현에 대하여서 알아볼 필요가 있다. 150 | 151 | ### Optional 152 | 153 | `Optional`클래스는 값의 존재나 부재 여부를 표현하는 **컨테이너 클래스**이다. findAny메서드가 만약 아무것도 반환하지않는 null을 return한다면 어떻게 될까? 154 | 155 | 이를 방지하기 위하여 `Optional` 클래스는 **값이 존재할 수도 있고 존재하지 않을 수도 있는 경우를 안전하게 처리**하기 위해 사용된다. 156 | 157 | - isPresent() 158 | - Optional이 값을 포함하면 True 아니면 False를 반환한다. 159 | - ifPresent(Consumerblock) 은 값이 있으면 주어진 블록을 실행 160 | - consumer함수형 인터페이스는 T형식의 인수를 받으며 void를 반환하는 람다를 전달 가능 161 | - T get()은 값이 존재한다면 값을 반환하고, 값이 없다면 NoSuchElementException을 발생시킨다. 162 | - T orElse() ( T other)는 값이 있다면 값을 반환하고, 없다면 기본 값을 반환한다. 163 | 164 | 즉, 위의 findAny연산을 이와 같이 수정한다면 null값에 대한 대비 또한 가능하다. 165 | 166 | ```java 167 | Optional dish = menu.stream() 168 | .filter(Dish::isVegetarian) 169 | .findAny() //findAny는 Optional 객체를 반환 170 | .ifPresent(t -> System.out.println(t.getName)); 171 | ``` 172 | 173 | ### 5.4.4 첫 번째 요소 찾기 - findFirst() 174 | 175 | 리스트 혹은 정렬된 연속 데이터로부터 생성된 스트림에는 논리적 아이템 순서가 정해져 있을 수 있다. 이런 스트림에서 첫 번쨰 요소를 찾을 때 findFirst()메소드를 사용한다. 176 | 177 | 병렬성 떄문에 findAny를 더 선호한다. 178 | 179 | ## 5.5 리듀싱 180 | 181 | 모든 스트림 요소를 처리해서 값으로 도출하는 것을 **리듀싱 연산**이라하며, 이를 함수형 프로그래밍에서는 **폴드(fold)**라 한다. 182 | 183 | ### 5.5.1 요소의 합 184 | 185 | reduce는 기본적으로 2개의 인수를 가진다. 186 | 187 | 먼저 첫 번째 인수는 초기값에 해당하며, 두 번째 인수는 두 요소를 조합하여 새로운 값을 만드는 BinaryOperator이다. 188 | 189 | ```java 190 | T reduce(T identity, BinaryOperator accumulator); 191 | ``` 192 | 193 | ![Image](https://github.com/user-attachments/assets/29de3c56-1207-4400-ae02-73e94e03ca8d) 194 | BinaryOperator에 메소드 참조가 들어갈 수 있다. 195 | 196 | ```java 197 | int sum = numbers.stream().reduce(0, Integer::sum); 198 | ``` 199 | 200 | - 초기값이 없게 오버로드된 reduce 메소드 201 | - 해당 reduce는 Optional 객체를 반환한다. 이는 초기값이 존재하지 않기 떄문에 반드시 초기값은 스트림의 요소를 소비해야하는데 소비할 스트림에 데이터가 없을때를 대비하기 위함이다. 202 | 203 | ### 5.5.2 최댓값과 최솟값 204 | 205 | reduce의 두번째 인자는 스트림의 두 요소를 합쳐서 하나의 값을 만드는데 사용된다. 206 | 207 | 이를 응용한다면 최댓값과 최솟값을 만들 수 있다. 208 | 209 | ```java 210 | Optional max = numbers.stream().reduce(Integer::max); 211 | Optional max = numbers.stream().reduce(Integer::min); 212 | ``` 213 | 214 | ![Image](https://github.com/user-attachments/assets/fc65206c-89ba-4b98-9b83-2968d1a3289b) 215 | ### Map-Reduce 패턴 216 | 217 | https://hadoop.apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html 218 | 219 | 쉽게 병렬화하는 특징 덕분에 구글이 웹 검색에 적용하면서 유명해졌다. 220 | 221 | ### 스트림 연산 : 상태 없음과 상태 있음 222 | 223 | ### 🔹 **1. 상태 없는 연산 (Stateless Operations)** 224 | 225 | 상태 없는 연산은 **입력 스트림의 각 요소에 대해 독립적인 처리를 하며** 각 요소가 이전의 처리 결과와 관계없이 바로 처리됩니다. 226 | 227 | 따라서 **내부 상태를 가지지 않는** 연산입니다. 228 | 229 | - **예시**: `map`, `filter`, `flatMap`, `forEach`, `peek` 등 230 | 231 | 이 연산들은 입력 스트림에서 결과를 바로 출력 스트림으로 보냅니다. 232 | 233 | 즉, 각 요소가 **독립적**이며 **누적되는 상태를 가지지 않기 때문에** 상태 없는 연산이라 부릅니다. 234 | 235 | ### 🔹 **2. 상태 있는 연산 (Stateful Operations)** 236 | 237 | 상태 있는 연산은 **결과를 누적하거나 과거의 처리 결과를 기억**할 필요가 있기 때문에 **내부 상태를 가지게 됩니다.** 238 | 239 | 이러한 연산들은 **이전에 처리한 결과를 바탕으로 새로운 결과를 계산**하기 때문에, 전체 데이터를 다루기 전에 **상태를 기억하고 관리해야 할 필요**가 있습니다. 240 | 241 | 따라서 **상태 있는 연산**은 **병렬 처리 시 성능 저하**를 일으킬 수 있습니다. 242 | 243 | - **예시**: `reduce`, `collect`, `sum`, `max`, `min`, `count` 등 244 | 245 | 이 연산들은 스트림의 각 요소를 처리할 때 **결과를 누적하는 상태를 관리**하거나 **최종적인 결과를 계산하기 위해 여러 번 상태를 갱신**합니다. 246 | 스트림의 요소 개수와 상관없이 최종 결과 하나만 유지하면 되므로 내부 상태 크기가 **한정(bounded)** 됩니다. 247 | 248 | ### 🔹 **3. 상태 있는 연산: `sorted`와 `distinct`** 249 | 250 | `sorted`와 `distinct`와 같은 연산은 **과거의 이력을 알아야 할 때** 상태를 유지해야 하므로, 상태 있는 연산으로 분류됩니다. 251 | 252 | - **`sorted`**: 데이터를 **정렬하려면** 현재 요소뿐만 아니라 **전체 요소를 비교해야** 하므로 내부적으로 상태를 유지해야 합니다. 또한 정렬을 하려면 전체 요소를 저장하고 비교해야 하므로, 스트림의 크기와 관계없이 **내부 상태가 커질 수 있습니다(unbounded).** 253 | - **`distinct`**: 중복을 제거하려면 **과거의 요소들을 기억**해야 하므로, 각 요소가 이전에 처리된 값과 비교되어야 합니다. **따라서 상태를 유지해야 합니다.** 또한 중복을 제거하려면 과거의 요소들을 기억해야 하므로, 이전에 처리한 값들과 비교하는 과정에서 **내부 상태가 커질 수 있습니다(unbounded).** 254 | 255 | 이 연산들은 **상태를 관리하며**, 병렬 처리 시 성능에 영향을 줄 수 있습니다. 256 | 257 | ## 5.7 숫자형 스트림 258 | 259 | ```java 260 | menu.stream().map(Dish::getCalories).reduce(0, Integer::sum); 261 | ``` 262 | 263 | 위 코드를 유심히 보면 불필요한 비용이 발생하는 걸 알 수 있다. 264 | 265 | 바로 Integer 사용으로 인한 박싱 비용인데 이를 없애려면 어떻게 해야할까? 266 | 267 | ```java 268 | menu.stream().map(Dish::getCalories).sum(); 269 | ``` 270 | 271 | 위 코드처럼 한다면 박싱비용을 해결할 수 있겠지만 아쉽게도 불가능하다. 272 | 273 | map이 생성하는 스트림은 Stream 를 생성하기 떄문이다. 하지만 Stream의 요소 형식은 Stream로 Integer를 가질텐데 어째서 sum메소드가 인터페이스에 존재하지 않을까? 274 | 275 | Stream에서 볼 수 있듯이 만약 Stream의 요소에 Dish라는 객체가 와버린다면 sum메소드를 사용할 수 없기 때문이다. 276 | 277 | 이를 해결해주기 위하여 **기본형 특화 스트림**을 사용할 필요가 있다 278 | 279 | ### 5.7.1 기본형 특화 스트림 280 | 281 | 자바 8에서는 각각 int, double 그리고 long에 특화된 IntStream, DoubleStream, LongStream을 제공하며 각각의 인터페이스에는 자주 사용하는 숫자 관련 리듀싱 연산 수행 메소드를 제공한다. 282 | 283 | 또한 필요할때 특화형이 아니라 다시 객체 스트림인 Stream으로 복원할 수도 있다. 284 | 285 | 특화된 스트림들은 **박싱**(primitive → object) 없이 기본형 값을 다루기 때문에 성능 상 유리하다. 286 | 287 | ### 숫자 스트림으로 매핑 288 | 289 | 스트림을 특화 스트림으로 변환할 때에는 mapToInt, mapToDouble, mapToLong을 사용한다. 290 | 291 | 이들은 map과 정확히 같은 기능을 수행하지만, Stream 대신 특화 스트림을 반환한다. 292 | 293 | IntStream은 sum메서드 뿐 아니라 max, min, average와 같은 유틸리티 메서드도 지원한다. 294 | 295 | ### 객체 스트림으로 복원하기 296 | 297 | 특화 스트림에서 특화되지 않은 원상태의 스트림으로 복원하기 위해서는 boxed 메소드를 사용한다. 298 | 299 | ```java 300 | Stream stream = intStream.boxed(); 301 | ``` 302 | 303 | ### 기본값 : OptionalInt 304 | 305 | 기본값이 존재하지 않는 Stream연산에서 최대,최솟값을 구하기 위하여서 기본형 특화 스트림에 Optional을 붙여 OptionalInt처럼 사용한다. 306 | 307 | orElse와 함께 사용하여 값이 없을때 기본 최대,최솟값을 명시적으로 지정이 가능해진다. 308 | 309 | ```java 310 | int max = maxCalories.orElse(1); // 값이 없을떄 기본 최대값을 명시적으로 설정한다. 311 | ``` 312 | 313 | ### 5.7.2 숫자 범위 314 | 315 | 자바 8의 IntStream과 LongStream에서 range와 rangeClosed라는 두가지 정적 메서드를 제공한다. 316 | 317 | 두 정적 메소드는 첫 번째 인수로 시작값, 두 번째 인수로 종료값을 가진다. 318 | 319 | range(1,100) 은 1 < … < 100 에 대한 수를, rangeClosed(1,100) 은 1 ≤ … ≤ 100 에 대한 수를 가진 스트림을 생성해준다. 320 | 321 | ## 5.8 스트림 만들기 322 | 323 | ### 5.8.1 값으로 스트림 만들기 324 | 325 | Stream.of를 통하여 스트림을 생성할 수 있다. 또한 empty메서드를 통하여 빈 스트림을 만들 수 있다. 326 | 327 | ```java 328 | Stream stream = Stream.of("hi","hello","bye"); 329 | Stream stream = Stream.empty(); 330 | ``` 331 | 332 | ### 5.8.2 null이 될 수 있는 객체로 스트림 만들기 333 | 334 | 자바 9 이상에서 추가된 `Stream.ofNullable` 은 인수의 값이 null이 아닐 경우 스트림을 정상적으로 생성하며, 만약 null일 경우에는 `Stream.empty()` 를 반환하여 빈 스트림을 생성한다. 335 | 336 | ```java 337 | Stream homeValueStream = Stream.ofNullable(매개변수); 338 | ``` 339 | 340 | ### 5.8.3 배열로 스트림 만들기 341 | 342 | 배열을 인수로 받는 정적 메서드 Arrays.stream을 이용하여 스트림을 만들 수 있다. 343 | 344 | ```java 345 | int [] numbers = {1,2,3,4}; 346 | 347 | int getData = Arrays.stream(numbers).최종연산; 348 | // (ex.. sum,count 등) 을 통해서 파이프라인을 349 | // 명시적으로 닫아주어야한다. 350 | Arrays.stream(numbers) -> 이 시점에서 만들어지는 스트림은 IntStream 이다. 351 | ``` 352 | 353 | ### 5.8.4 파일로 스트림 만들기 354 | 355 | 파일처리 등의 I/O 연산을 할 떄 사용하는 자바의 NIO API 또한 스트림 API를 활용할 수 있다. 356 | 357 | 기존 finally 블록에서 자원을 반환하던 것을 Stream 을 사용한다면 Stream 인터페이스 자체가 AutoCloseable 인터페이스의 구현체이므로 자원을 자동으로 관리해준다. 358 | 359 | ### 5.8.5 함수로 무한 스트림 만들기 360 | 361 | 스트림API는 함수에서 스트림을 생성할 수 있는 두 정적 메소드인 `Stream.iterate` 와 `Stream.generate`를 제공한다. 두 가지의 연산을 이용하여 크기가 고정되어있지 않은 무한 스트림을 만들 수 있다. 362 | 363 | ### iterate 메소드 364 | 365 | iterate와 generate에서 만든 스트림은 요청할 때마다 주어진 함수를 이용하여서 값을 만들지만 보통 limit와 같이 제한을 둔다. iterate의 순차적인 특성으로 인하여 연속된 일련의 값을 만들때에 사용한다. 366 | 367 | `iterate(초기값, 람다)` 368 | 369 | ```java 370 | import java.util.stream.Stream; 371 | 372 | public class Main { 373 | public static void main(String[] args) { 374 | // Stream.iterate()로 무한 스트림 생성 375 | Stream infiniteStream = Stream.iterate(1, n -> n + 1) 376 | .limit(10) 377 | .forEach(System.out::println);; // 1부터 시작해서 1씩 증가 378 | // iterate는 이전 요소에 +1을 한 데이터를 지속적으로 만든다 379 | } 380 | } 381 | 382 | ``` 383 | 384 | 이러한 **무한 스트림**을 다른 용어로 **언바운드 스트림**이라 부른다. 385 | 386 | 자바 9 이상의 iterate메소드는 프레디케이트를 지원하는데 프레디케이트 사용시에는 387 | 388 | `iterate(초기값, 프레디케이트, 연산을 위한 람다식)` 의 형태로 만들어진다. 389 | 390 | - iterate 뒤에 filter와 takeWhile의 차이 391 | - iterate가 계속 생성하는데 filter는 생성하는 데이터에 대해서 필터링을 진행하기에 계속 생성하는 것을 막는 것은 불가능하다. 392 | - takeWhile의 경우 조건이 true인 동안만 **스트림을 소비**하기에 조건이 false가 된다면 무한하게 생성하는 iterate로부터 빠져나올 수 있다. 393 | 394 | ### generate 메소드 395 | 396 | generate는 iterate와 다르게 생성된 값을 연속적으로 계산하지 않는다. 397 | 398 | generate는 Supplier(발행자) 를 인수로 받아 새로운 값을 생산한다. 399 | 400 | 단 Supplier 는 별도의 인수를 필요로 하지 않기에 generate메소드는 스트림 생성시 별도의 인수가 필요하지 않을때 사용한다. 401 | 402 | ```java 403 | IntStream.generate(()-> 2); 와 같이 인수가 필요없는( () -> ) 형태에 사용한다. 404 | ``` 405 | 406 | 위 예시의 경우 limit와 같이 제한을 두지 않는다면 2를 계속 생성하는 무한 스트림이 만들어진다. 407 | -------------------------------------------------------------------------------- /7장 병렬 데이터 처리와 성능/ch7.md: -------------------------------------------------------------------------------- 1 | # 7.1 병렬 스트림 2 | 3 | > 각각의 스레드에서 처리할 수 있도록 스트림 요소를 여러 청크로 분할한 스트림 4 | 5 | ## 7.1.1 순차 스트림을 병렬 스트림으로 변환하기 6 | 7 | ## 예제 8 | 9 | ### 반복문 10 | 11 | ```java 12 | public long iterativeSum(long n) { 13 | long result = 0; 14 | for (long i = 1L; i <= n; i++) { 15 | result += i; 16 | } 17 | return result; 18 | } 19 | ``` 20 | 21 | ### 순차 스트림 22 | 23 | ```java 24 | public long sequntialSum(long n) { 25 | return Stream.iterate(1L, i -> i + 1) // 무한 자연수 스트림 생성 26 | .limit(n) // n개 이하로 제한 27 | .reduce(0L, Long::sum); // 모든 숫자를 더하는 스트림 리듀싱 연산 28 | } 29 | ``` 30 | 31 | ### 병렬 스트림 32 | 33 | 다음 코드와 같이 순차 스트림에서 `parallel 메서드`를 호출하면 기존의 함수형 리듀싱 연산이 병렬로 처리된다. 리듀싱 연산은 여러 청크에 병렬로 수행할 수 있다. 34 | 35 | ```java 36 | public long parallelSum(long n) { 37 | return Stream.iterate(1L, i -> i + 1) 38 | .limit(n) 39 | .parallel() // 스트림을 병렬 스트림으로 변환 40 | .reduce(0L, Long::sum); 41 | } 42 | ``` 43 | 44 | **parallel()을 호출해도 원본 스트림이 변하는 것이 아니다!** 45 | 46 | 원본 스트림 자체가 병렬화 되는 것이 아니라, 병렬 실행 여부를 설정하는 플래그(flag)를 변경하는 것. 즉, 스트림 연산이 실제로 실행될 때 병렬로 수행되도록 설정한다. 47 | 48 | ### parallel()과 sequential()을 이용해서 연산 제어 49 | 50 | parallel 호출 이후, 연산이 **병렬로 수행** 51 | 52 | sequential 호출 이후, 연산이 **순차로 수행** 53 | 54 | ```java 55 | stream.parallel() 56 | .filter(...) // 병렬 수행 57 | .sequential() 58 | .map(...) // 순차 수행 59 | .parallel() 60 | .reduce(); // 병렬 수행 61 | ``` 62 | 63 | ### 병렬 스트림에서 사용하는 스레드 풀 설정 64 | 65 | parallel() 메서드는 내부적으로 `ForkJoinPool`이라는 스레드 풀을 사용하여 병렬 연산을 수행한다. 66 | 67 | 기본적으로 병렬 스트림은 CPU 코어 수만큼의 스레드 사용하지만, 필요에 따라 스레드 개수 변경 가능. 다음 코드는 최대 12개의 스레드를 사용하도록 설정되었다. 68 | 69 | But, **전역으로밖에 변경을 못 한다.** 되도록 기본값 사용하기! 70 | 71 | ```java 72 | System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "12"); 73 | ``` 74 | 75 | ## 7.1.2 스트림 성능 측정 76 | 77 | JMH(자바 마이크로 벤치마크 하니스)라는 라이브러리를 이용해 벤치마크 작업을 실행한다. 78 | 79 | ### 예제 80 | 81 | ```java 82 | @BenchmarkMode(Mode.AverageTime) // 벤치마크 대상 메서드를 실행하는 데 걸린 평균 시간 측정 83 | @OutputTimeUnit(TimeUnit.MILLISECONDS) // 벤치마크 결과를 밀리초 단위로 출력 84 | @Fork(2, jvmArgs={"-Xms4G", "-Xms4G"}) 85 | public class ParallelStreamBenchmark { 86 | private static final long N = 10_000_000L; 87 | 88 | // 순차 스트림 89 | @Benchmark 90 | public long sequentialSum() { 91 | return Stream.iterate(1L. i -> i + 1).limit(N) 92 | .reduce(0L, Long::sum); 93 | } 94 | 95 | // 일반 반복문 96 | @Benchmark 97 | public long iterativeSum() { 98 | long result = 0; 99 | for(long i = 1L; i <= N; i++) { 100 | result += i; 101 | } 102 | return result; 103 | } 104 | 105 | // 병렬 스트림 106 | @Benchmark 107 | public long parallelSum() { 108 | return Stream.iterate(1L. i -> i + 1).limit(N) 109 | .parallel() 110 | .reduce(0L, Long::sum); 111 | } 112 | 113 | // 벤치마크 실행 후 가비지 컬렉터 동작 114 | @TearDown(Level.Invocation) 115 | public void tearDown() { 116 | System.gc(); 117 | } 118 | } 119 | ``` 120 | 121 | ### 실행 시간 비교 122 | 123 | **일반 반복문 < 순차 스트림 < 병렬 스트림** 124 | 125 | ### 왜 병렬 스트림은 코어 CPU를 활용 못하고 느리게 실행되는 걸까?🤔 126 | 127 | - 반복 결과로 박싱된 객체가 만들어지기에 연산하기 위해선 숫자로 언박싱해야 함 128 | - 반복 작업은 병렬로 수행할 수 있는 독립 단위로 나누기 어려움 129 | 130 | **▶️ iterate 연산은 이전 연산의 결과에 따라 다음 함수의 입력이 달라지기에 청크로 분할하기 어렵다‼️** 131 | 132 | ### 그렇다면? 133 | 134 | `LongStream.rangeClosed(start, end)`를 통해 최적화시키자! 135 | 136 | 👉🏻 기본형 long을 사용하기에 박싱/언박싱 작업이 필요없음(오버헤드 감소) 137 | 138 | 👉🏻 이전 연산의 영향을 받는 iterate와 달리 범위를 직접 정해주기에 청크로 쉽게 분할할 수 있음 139 | 140 | 👉🏻 사용 후, 전체적으로 실행 시간 감소, 병렬 스트림 실행 시간 < 순차 스트림 실행 시간 141 | 142 | 즉, 항상 parallel 메서드의 병렬화 작업을 올바르게 사용하고 있는지 주의하자. 143 | 144 | ## 7.1.3 병렬 스트림의 올바른 사용법 145 | 146 | 병렬 스트림의 많은 문제는 **공유된 가변 상태에서 비롯**된다. 147 | 148 | ```java 149 | // 병렬 스트림 150 | public long sideEffectParallelSum(long n) { 151 | Accumulator accumulator = new Accumulator(); 152 | LongStream.rangeClosed(1, n).parallel().forEach(accumulator::add); 153 | return accumulator.total; 154 | } 155 | 156 | // -> 위 결과로 올바른 값이 나오지 않는다. 157 | ``` 158 | 159 | 여러 스레드에서 공유된 add 메서드를 호출하면서 올바른 값이 나오지 않는 현상 발생🚨 160 | 161 | ⇒ **즉, 올바른 병렬 스트림을 사용하고 싶다면 공유된 가변 상태를 피해야 한다.** 162 | 163 | ## 7.1.4 병렬 스트림 효과적으로 사용하기 164 | 165 | 1. 확신이 서지 않으면 직접 측정하라. 166 | 2. 박싱을 주의하라. 기본형 특화 스트림을 사용하라. 167 | 3. limit나 findFirst처럼 요소의 순서에 의존하는 연산에서는 순차 스트림보다 성능이 떨어진다. 168 | 4. 스트림에서 수행하는 전체 파이프라인 연산 비용을 고려하라. 169 | 5. 소량의 데이터에서는 병렬 스트림이 도움이 안된다. 170 | 6. 스트림을 구성하는 자료구조가 적절한지 확인하라. (하단 표 참고) 171 | 7. 스트림의 특성과 파이프라인의 중간 연산이 스트림의 특성을 어떻게 바꾸는지에 따라 분해 과정 성능이 달라질 수 있다. 172 | 8. 최종 연산의 병합 과정 비용을 살펴보라. 173 | 9. 병렬 스트림이 수행되는 내부 인프라 구조를 살펴라. 174 | 175 | ### 스트림 소스와 분해성 176 | 177 | | 소스 | 분해성 | 178 | | --------------- | ------ | 179 | | ArrayList | 훌륭함 | 180 | | LinkedList | 나쁨 | 181 | | IntStream.range | 훌륭함 | 182 | | Stream.iterate | 나쁨 | 183 | | Hashset | 좋음 | 184 | | TreeSet | 좋음 | 185 | 186 | # 7.2 포크/조인 프레임워크 187 | 188 | > 서브태스크를 스레드 풀(ForkJoinPool)의 작업자 스레드에 분산 할당하는 Executor 인터페이스를 구현 189 | 190 | ## 7.2.1 RecursiveTask 활용 191 | 192 | 스레드 풀을 이용하기 위해 RecursiveTask의 서브클래스를 생성한다. 193 | 194 | 여기서 **R은 병렬화된 태스크가 생성한 결과 형식이고 만약 결과가 없다면 RecursiveAction 형식**이다. 195 | 이 때, RecursiveAction은 외부 데이터(전역 변수)를 직접 수정하는 역할을 수행한다. 196 | 197 | RecursiveTask를 정의하기 위해 추상 메서드 compute를 구현해야 한다. 198 | 199 | `compute()` : 태스크를 서브태스크로 분할하는 로직과 더 이상 분할할 수 없을 때 개별 서브태스크의 결과를 생산할 알고리즘을 정의 200 | 201 | ```java 202 | protected abstract R compute(); 203 | ``` 204 | 205 | ### compute 메서드의 의사코드 206 | 207 | ```java 208 | if (태스크가 충분히 작거나 더 이상 분할할 수 없으면) { 209 | 순차적으로 태스크 계산 210 | } else { 211 | 태스크를 두 서브태스크로 분할 212 | 태스크가 다시 서브태스크로 분할되도록 이 메서드를 재귀적으로 호출함 213 | 모든 서브태스크의 연산이 완료될 때까지 기다림 214 | 각 서브태스크의 결과를 합침 215 | } 216 | ``` 217 | 218 | ### RecursiveTask의 compute 추상 메서드 오버라이드 219 | 220 | ```java 221 | @Override 222 | protected Long compute() { 223 | int length = end - start; 224 | if (length <= THRESHOLD) { 225 | return computeSequentially(); 226 | } 227 | ForkJoinSumCalculator leftTask = 228 | new ForkjoinSumCalculator(numbers, start, start + length / 2); 229 | 230 | // 중요!!! 231 | leftTask.fork(); // 병렬 실행 232 | 233 | ForkJoinSumCalculator rightTask = 234 | new ForkJoinSumCalculator(numbers, start + length / 2, end); 235 | 236 | Long rightResult = rightTask.compute(); 237 | Long leftResult = leftTask.join(); 238 | return leftResult + rightResult; 239 | } 240 | ``` 241 | 242 | compute()를 실행하고 있는 A스레드가 leftTask.fork()를 호출하며 B스레드에서 leftTask를 비동기적으로 실행한다. 243 | 244 | **그럼, rightTask도 똑같이 rightTask.fork()를 호출하면 되지 않을까?** 245 | 246 | ➡️ 그러면 C스레드를 새롭게 할당해줘야 한다. 247 | 그 오버헤드를 줄이기 위해, compute()를 실행하고 있는 A스레드에서 쭉 진행하면 된다! 그러기 위해선 fork()가 아닌 rightTask.compute(...)를 하면 비용을 절약할 수 있다. 248 | 249 | ### ForkJoinSumCalculator를 활용한 예제 코드 250 | 251 | 다음 코드는 숫자 n까지의 병렬 합산을 수행한 코드다. 252 | 253 | ```java 254 | public static long forkJoinSum(long n){ 255 | long[] numbers = LongStream.rangeClosed(1, n).toArray(); 256 | ForkJoinTask task = new ForkJoinSumCalculator(numbers); // 위 코드인 병렬 합산 알고리즘 수행 257 | return new ForkJoinPool().invoke(task); // 생성한 태스크를 invoke 메서드로 전달 258 | } 259 | ``` 260 | 261 | 위 코드에서 invoke(task)를 호출하는 이유는 ForkJoinPool이 병렬 작업을 시작하고, 결과를 반환받기 위해서다. 262 | 구체적으로 invoke()는 다음과 같은 역할을 한다. 263 | 264 | ### invoke()의 역할 265 | 266 | 1. **ForkJoinPool에서 작업(task) 실행** : 267 | invoke(ForkJoinTask task)는 주어진 ForkJoinTask를 현재 스레드에서 실행하고 결과를 반환한다. 268 | 내부적으로 스레드 풀을 활용하여 병렬로 작업을 수행한다. 269 | 270 | 2. **결과 반환** : 271 | invoke()는 작업이 끝날 때까지 기다렸다가(join()과 유사) 최종 결과를 반환한다. 272 | 즉, 병렬 연산이 완료될 때까지 메인 스레드는 대기하며, 완료된 결과를 리턴한다. 273 | 274 | 3. **ForkJoinTask의 실행을 중앙 집중화** : invoke()는 직접 ForkJoinTask를 실행하며, fork() 및 join()을 수동으로 호출하는 것보다 간결하게 병렬 작업을 실행할 수 있다. 275 | 276 | **invoke()가 없으면?** 277 | 278 | 만약 invoke(task)를 호출하지 않고, 단순히 task.fork();만 호출하면 메인 스레드가 즉시 반환되어, 연산이 완료되지 않은 상태에서 값이 리턴될 수 있습니다. 279 | 따라서 invoke()를 호출해야 ForkJoinPool이 연산을 마칠 때까지 기다렸다가 최종 결과를 반환할 수 있습니다. 280 | 281 | **일반적으로 ForkJoinPool은 애플리케이션에서 단 한 번만 인스턴스화 해서 정적 필드에 싱글턴으로 저장**한다. 282 | 283 | ForkJoinSumCalculator의 compute 메서드는 병렬로 실행할 수 있을만큼 태스크의 크기가 작아졌는지 확인하며, 아직 태스크의 크기가 크다고 판단되면 숫자 배열을 반으로 분할해서 두 개의 새로운 ForkJoinSumCalculator로 할당한다. 284 | 285 | ⇒ **재귀적인 태스크 분할 반복** 286 | 287 | ## 7.2.2 포크/조인 프레임워크를 제대로 사용하는 방법 288 | 289 | - join 메서드를 태스크에 호출하면 태스크가 생산하는 결과가 준비될 때까지 호출자를 블록시킨다. join 메서드는 두 서브태스크가 모두 시작된 다음에 호출하자. 290 | - RecursiveTask 내에서는 ForkJoinPool의 invoke 메서드를 사용하지 말아야 한다. 대신 compute나 fork 메서드를 호출하자. 291 | - 왼쪽 작업과 오른쪽 모두에 fork 메서드를 사용하는 것 대신, 한쪽 작업에 compute를 호출하자. 두 서브태스크의 한 태스크에는 같은 스레드를 재사용할 수 있으므로 풀에서 불필요한 태스크를 할당하는 오버헤드를 줄일 수 있다. 292 | - 디버깅이 어렵다는 점을 고려하자. 293 | - 각 서브태스크의 실행시간은 새로운 태스크를 포킹하는 데 드는 시간보다 길어야 한다. 294 | 295 | ## 7.2.3 작업 훔치기 296 | 297 | 코어 개수만큼 병렬화된 태스크로 작업 분할을 하면 모든 CPU 코어에서 태스크를 실행할 것이고 크기가 같은 태스크들은 같은 시간에 종료될 것이라 생각하겠지만, 실제 현실에서는 작업 완료 시간이 크게 달라질 수 있다. 분할 기법이 효율적이지 않았거나 예기치 않은 디스크 접근 속도 저하 및 외부 서비스와의 지연 과정 때문이다. 298 | 299 | 포크/조인 프레임워크에서는 이를 해결하기 위해 **“작업 훔치기 기법”**을 사용한다. 300 | 301 | ### 작업 훔치기 과정 302 | 303 | ⇒ 각각의 스레드는 자신에게 할당된 태스크를 포함하는 이중 연결 리스트(doubley linked list)를 참조하면서 작업이 끝날 때마다 큐의 헤드에서 다른 태스크를 가져와서 작업을 처리 304 | 305 | ⇒ 이 때, 한 스레드는 다른 스레드보다 자신에게 할당된 태스크를 더 빨리 처리할 수 있는데, 할 일이 없어진 스레드는 유휴 상태로 바뀌는 것이 아니라 **다른 스레드의 큐의 꼬리에서 작업을 훔쳐온다.** 306 | 307 | ⇒ 모든 태스크가 작업을 끝낼 때 까지 이 과정을 반복한다. 308 | 309 | 따라서, 태스크의 크기를 작게 나누어야 작업자 스레드 간의 작업부하를 비슷한 수준으로 유지할 수 있다. 310 | 311 | # 7.3 Spliterator 인터페이스 312 | 313 | > 자동으로 스트림을 분할하는 기법 314 | 315 | ### Spliterator 인터페이스 메서드 316 | 317 | ```java 318 | public interface Spliterator { 319 | boolean tryAdvance(Consumer action); 320 | Spliterator trySplit(); 321 | long estimateSize(); 322 | int characteristics(); 323 | } 324 | ``` 325 | 326 | - **T** : Spliterator에서 탐색하는 요소의 형식 327 | - **tryAdvance** : Spliterator의 요소를 하나씩 순차적으로 소비하면서 탐색해야 할 요소가 남아있으면 참을 반환 328 | - **trySplit** : Spliterator의 일부 요소를 분할해서 두 번째 Spliterator를 생성하는 메서드 329 | - **estimateSize** : 탐색해야 할 요소 수 정보를 제공 330 | - **characteristics** : Spliter의 특성 331 | 332 | ### Spliterator 특성 333 | 334 | - **ORDERED**: 정해진 순서에 따라 요소를 탐색하고 분할할 때 순서에 유의해야 한다. 335 | - **DISTINCT**: x, y 두 요소를 방문했을 때 x.equals(y)는 항상 false를 반환한다. 336 | - **SORTED**: 탐색된 요소는 미리 정의된 정렬 순서를 따른다. 337 | - **SIZED**: 크기가 알려진 소스로 Spliterator를 생성했으므로 estimatedSize()는 정확한 값을 반환 338 | - **NON-NULL**: 탐색하는 모든 요소는 null이 아니다. 339 | - **IMMUTABLE**: Spliterator는 변할 수 없다. 즉, 요소를 탐색하는 동안 요소를 변화시킬 수 없다. 340 | - **CONCURRENT**: 동기화 없이 Spliterator의 소스를 여러 스레드에서 동시에 고칠 수 있다. 341 | - **SUBSIZED**: Spliterator와 그로 인해 분할된 Spliterator는 모두 SIZED 특성을 갖는다. 342 | 343 | ## 7.3.2 커스텀 Spliterator 구현하기 344 | 345 | ### 함수형으로 단어 수를 세는 메서드 재구현하기 346 | 347 | ```java 348 | class WordCounter { 349 | private final int counter; 350 | private final boolean lastSpace; 351 | public WordCounter(int counter, boolean lastSpace) { 352 | this.counter = counter; 353 | this.lastSpace = lastSpace; 354 | } 355 | public WordCounter accumulate(Character c) { // 문자열의 문자를 하나씩 탐색 356 | if(Character.isWhitespace()) { 357 | return lastSpace ? 358 | this : 359 | new WordCounter(counter, true); 360 | } else { 361 | return lastSpace ? 362 | new WordCounter(counter+1, false) : // 탐색하다 공백을 만나면 지금까지 탐색한 문자를 하나로 간주하여 단어 수 증가 363 | this; 364 | } 365 | } 366 | } 367 | public WordCounter combine(WordCounter wordCounter) { 368 | return new WordCounter(counter + wordCounter.counter, wordCounter.lastSpace); 369 | } 370 | public int getCounter() { 371 | return counter; 372 | } 373 | } 374 | ``` 375 | 376 | 1. accumulate 메서드를 통해 새로운 문자를 탐색했을 때 WordCounter 상태 변이 377 | 2. combine 메서드를 통해 문자열 서브 스트림을 처리한 WordCounter의 결과를 병합(WordCounter 내부 counter 값 병합) 378 | 379 | ### 문자 스트림의 리듀싱 연산(순차적) 380 | 381 | ```java 382 | private int countWords(Stream stream) { 383 | WordCounter wordCounter = stream.reduce(new WordCounter(0, true), 384 | WordCounter::accumulate, 385 | WordCounter::combine; 386 | return wordCounter.getCounter(); 387 | } 388 | 389 | Stream stream = IntStream.range(0, SENTENCE.length()) 390 | .mapToObj(SENTENCE::charAt()); 391 | System.out.println("Found " + countWords(stream)+ " words"); 392 | ``` 393 | 394 | ### 문자 스트림의 리듀싱 연산(병렬적) 395 | 396 | 다음 병렬 실행 코드는 위 순차 리듀싱 연산보다 단어가 많이 나타나는 상황 발생 397 | 398 | ```java 399 | System.out.println("Found " + countWords(stream.parallel()) + " words"); 400 | ``` 401 | 402 | 왜 그럴까?🤔 403 | 404 | > 잘못된 스트림 분할 위치로 인해 공백 기준으로 단어를 나누는 것이 아닌 하나의 단어를 둘로 나누어버려서 나타난 상황 405 | 406 | ### Spliterator를 활용한 해결방법 407 | 408 | Spliterator를 이용해서 단어가 끝나는 위치에서 분할하도록 한다. 409 | 410 | ```java 411 | class WordCounterSpliterator implements Spliterator { 412 | private final String string; 413 | private int currentChar = 0; 414 | public WordCounterSpliterator(String string) { 415 | this.string = string; 416 | } 417 | @Override 418 | public boolean tryAdvance(Consumer action) { 419 | action.accept(string.charAt(currentChar++)); // 현재 문자를 소비한다. 420 | return currentChar < string.length(); // 소비할 문자가 남아있으면 true를 반환한다. 421 | } 422 | @Override 423 | public Spliterator trySplit() { 424 | int currentSize = string.length() - currentChar; 425 | if (currentSize < 10) { 426 | return null; // 파싱할 문자열을 순차 처리할 수 있을 만큼 충분히 작아졌음을 알리는 null을 반환한다. 427 | } 428 | for (int splitPos = currentSize / 2 + currentChar; 429 | splitPos < string.length(); splitPost++) { // 파싱할 문자열의 중간을 분할 위치로 설정한다. 430 | if (Character.isWhitespace(string.charAt(splitPos))) { // 다음 공백이 나올 때까지 분할 위치를 뒤로 이동 시킨다. 431 | Spliterator spliterator = // 처음부터 분할 위치까지 문자열을 파싱할 새로운 WordCounterSpliterator를 생성한다. 432 | new WordCounterSpliterator(string.substring(currentChar, splitPos)); 433 | currentChar = splitPos; // 이 WordCounterSpliterator의 시작 위치를 분할 위치로 설정한다. 434 | return spliterator; // 공백을 찾았고 문자열을 분리했으므로 루프를 종료한다. 435 | } 436 | } 437 | return null; 438 | } 439 | @Override 440 | public long estimateSize() { 441 | return string.length() - currentChar; 442 | } 443 | @Override 444 | public int characteristics() { 445 | return ORDERED + SIZED + SUBSIZED + NON-NULL + IMMUTABLE; 446 | } 447 | } 448 | ``` 449 | 450 | WordCounterSpliterator를 활용하면 병렬 스트림으로 사용하면 앞선 순차 리듀싱 연산보다 단어가 많이 나타나는 병렬 리듀싱 문제를 해결할 수 있다. 451 | 452 | ```java 453 | Spliterator spliterator = new SimpleSpliterator(SENTENCE); 454 | Stream stream = StreamSupport.stream(spliterator, true); 455 | 456 | System.out.println("Found " + countWords(stream) + " words"); 457 | ``` 458 | -------------------------------------------------------------------------------- /16장 CompletableFuture : 안정적 비동기 프로그래밍/ch16.md: -------------------------------------------------------------------------------- 1 | # 16.1 Future의 단순 활용 2 | 3 | Java 5 부터 미래의 어느 시점에 결과를 얻는 모델에 활용할수 있도록 Future 인터페이스를 제공하고 있다. 4 | 5 | 시간이 걸리는 작업들을 Future 내부에 설정하여 호출자 스레드가 결과를 기다리는 동안 다른 유용한 작업들을 할 수 있다. 6 | 7 | ```java 8 | ExecutorService es =ex.newCachedThreadPool(); 9 | Future future = ex.submit(new Callable() { 10 | public Double call() { 11 | return doSomeLongComputation(); 12 | } 13 | }) 14 | doSomeThingElse(); 15 | future.get(1, TimeUnit.SECONDS); // 비동기 결과가 준비되어 있지 않으면 1초간 기다린다. 16 | ``` 17 | 18 | - 위 예재와 같이 시간이 오래 걸리는 작업을 다른 스레드에서 처리하고 메인 스레드에서는 다른 작업들을 미리 수행할 수 있다. 19 | - 만약 결과가 준비되지 않았다면 작업이 완료될때까지 스레드를 블록할 수 있다. 이때 영원히 스레드가 종료되지 않는 경우를 대비하여 최대 타임아웃을 설정가능하다. 20 | 21 | ## 16.1.1 Future 제한 22 | 23 | Future 인터페이스에는 비동기 계산에 대한 대기와 결과 처리 메서드들이 있다. 하지만 여러 Future 간 의존성은 표현하기 어렵다. 24 | 25 | 그래서 자바8에서는 CompletableFuture 클래스로 다음과 같은 기능을 제공한다. 26 | 27 | - 두 개의 비동기 계산 결과를 합친다. 두 결과는 서로 독립적 또는 한쪽에 의존적일 수 있다. 28 | - Future 집합이 실행하는 모든 태스크의 완료를 기다린다. 29 | - Future 집합에서 가장 빨리 완료되는 태스크를 기다렸다가 결과를 얻는다. 30 | - 프로그램적으로 Future를 완료시킨다.(비동기 동작에서 수동으로 결과 제공) 31 | - Future 완료 동작에 반응한다.(결과를 기다리면서 블록되지 않음) 32 | 33 | # 16.2 **비동기 API 구현** 34 | 35 | - **동기 API** : 메서드를 호출한 다음에 메서드가 계산을 완료할 때까지 기다렸다가 메서드가 반환되면 호출자는 반환된 값으로 계속 다른 동작을 수행. 블록 호출(blocking call)이라 한다. 36 | - **비동기 API** : 메서드가 즉시 반환되며 끝내지 못한 나머지 작업을 호출자 스레드와 동기적으로 실행될 수 있도록 다른 스레드에 할당한다. 비블록 호출(non-blocking call)이라 한다 37 | 38 | ```java 39 | public class Shop { 40 | public double getPrice(String product) { 41 | return calculatePrice(product); 42 | } 43 | 44 | private double calculatePrice(String product) { 45 | delay(); 46 | return new Random().nextDouble() * product.charAt(0) + product.charAt(1); 47 | } 48 | 49 | public static void delay() { 50 | try { 51 | Thread.sleep(1000L); 52 | } catch (InterruptedException e) { 53 | e.printStackTrace(); 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | - getPrice는 외부 요청과 같이 시간이 오래걸리는 작업을 수행 할 수 있으므로 임의로 1초 동안 sleep 메소드를 호출하였다. 60 | - 위 API를 사용자가 호출하는 경우 비동기 동작이 완료될때까지 1초 동안 블록된다. 61 | - 동기 메소드를 비동기 메소드로 소비하는 방법을 알아보자. 62 | 63 | ## **16.2.1 동기 메서드를 비동기 메서드로 변환** 64 | 65 | 동기 메서드를 CompletableFuture를 통해 비동기 메서드로 변환할 수 있다. 66 | 67 | 비동기 계산과 완료 결과를 포함하는 CompletableFuture 인스턴스를 만들고 완료 결과를 complete 메서드로 전달하여 CompletableFuture를 종료할 수 있다. 68 | 69 | ```java 70 | public Future getPriceAsync(String product) { 71 | CompletableFuture futurePrice = new CompletableFuture<>(); 72 | new Thread(() -> { 73 | int price = calculatePrice(product); 74 | futurePrice.complete(price); 75 | }).start(); 76 | return futurePrice; 77 | } 78 | ``` 79 | 80 | 클라이언트 사용 예시 코드 81 | 82 | ```java 83 | Shop shop = new Shop("BestShop"); 84 | long start = System.nanoTime(); 85 | Future futurePrice = shop.getPriceAsync("my favorite product"); // 제품 가격 요청 86 | long invocationTime = ((System.nanoTime() - start) / 1_000_000); 87 | 88 | // 제품의 가격을 계산하는 동안 89 | doSomethingElse(); 90 | //다른 상점 검색 등 작업 수행 91 | try { 92 | double price = futurePrice.get(); // 가격 정보를 받을 때까지 블록 93 | } catch (Exception e) { 94 | throw new RuntimeException(e); 95 | } 96 | long retrievalTime = ((System.nanoTime() - start) / 1_000_000); 97 | ``` 98 | 99 | ## **16.6.2 에러 처리 방법** 100 | 101 | 위 로직에서 가격을 계산하는 동안 에러가 발생한다면 어떻게 될까? 102 | 103 | 예외가 발생하면 해당 스레드에만 영향을 미치기 때문에 클라이언트는 get 메서드가 반환될 때까지 영원히 기다릴 수도 있다. 104 | 105 | 따라서 타임아웃을 활용해 예외처리를 하고, completeExceptionally 메서드를 이용해 CompletableFuture 내부에서 발생한 에외를 클라이언트로 전달해야 한다. 106 | 107 | 클라이언트는 타임아웃값을 받는 get메서드와 try/catch문을 통해 이 문제를 해결할 수 있다. 그래야 문제가 발생했을 때 클라이언트가 영원히 블록되지 않을 수 있고 타임아웃 시간이 지나면 **TimeoutException**을 받을 수 있다. 108 | 109 | ```java 110 | // Future.get시 반환할 value를 전달한다. 111 | public boolean complete(T value) 112 | // Future.get시 반환할 value와 Timeout 시간을 설정한다. 113 | public T get(long timeout, TimeUnit unit) 114 | ``` 115 | 116 | catch한 TimeoutException에 대하여 catch 블록 내 `completExecptionally` 메서드를 이용해 CompletableFuture 내부에서 발생한 예외를 클라이언트로 전달할 수 있다. 117 | 118 | ```java 119 | public boolean completeExceptionally(Throwable ex) 120 | ``` 121 | 122 | 예시 코드 123 | 124 | ```java 125 | public Future getPriceAsync(String product) { 126 | CompletableFuture futurePrice = new CompletableFuture<>(); 127 | new Thread(() -> { 128 | try { 129 | double price = calculatePrice(product); 130 | futurePrice.complete(price); 131 | } catch { 132 | futurePrice.completeExceptionally(ex); // 에러를 포함시켜 Future를 종료 133 | } 134 | }).start(); 135 | return futurePrice; 136 | } 137 | ``` 138 | 139 | ### **팩토리 메서드 supplyAsync로 CompletableFuture 만들기** 140 | 141 | supplyAsync 메서드를 Supplier를 인수로 받아서 CompletableFuture를 반환한다. CompletableFuture는 Supplier를 실행해서 비동기적으로 결과를 생성한다. ForkJoinPool의 Executor 중 하나가 Supplier를 실행하며, 오버로드된 메서드를 이용하면 다른 Executor를 지정할 수 있다. 142 | 143 | ```java 144 | public static CompletableFuture supplyAsync(Supplier supplier) { 145 | return asyncSupplyStage(asyncPool, supplier); 146 | } 147 | 148 | public static CompletableFuture supplyAsync(Supplier supplier, Executor executor) { 149 | return asyncSupplyStage(screenExecutor(executor), supplier); 150 | } 151 | ``` 152 | 153 | # 16.3 비블록 코드 만들기 154 | 155 | ```java 156 | private List shops = Arrays.asList(new Shop("shop1"), 157 | new Shop("shop2"), 158 | new Shop("shop3"), 159 | new Shop("shop4")); 160 | 161 | public List findPrices(String product) { 162 | return shops.stream() 163 | .map(shop -> String.format("%s is %.2f", shop.getName(), shop.getPrice(product))) 164 | .collect(Collectors.toList()); 165 | } 166 | ``` 167 | 168 | 위와 같은 예제를 실행한다면 shops의 수 만큼 price를 순차적으로 계산하게 되어 많은 시간이 소요될 것이다. 169 | 170 | ## 16.3.1 병렬 스트림으로 요청 병렬화하기 171 | 172 | ```java 173 | public List findPrices(String product) { 174 | return shops.parallelStream() 175 | .map(shop -> String.format("%s is %.2f", shop.getName(), shop.getPrice(product))) 176 | .collect(Collectors.toList()); 177 | } 178 | ``` 179 | 180 | 병렬 스트림을 이용하여 동시에 가격을 계산할 수 있도록 개선하면 시간이 단축된다. 181 | 182 | 다음 CompletableFuture를 활용하여 findPrices 메서드의 동기 호출을 비동기 호출로 변경해보자. 183 | 184 | ## 16.3.2 CompletableFuture로 비동기 호출 구현하기 185 | 186 | ```java 187 | // 팩토리 메서드를 활용 188 | public List findPrices(String product) { 189 | List> priceFutures = shops.stream() 190 | .map(shop -> CompletableFuture.supplyAsync(() -> String.format("%s is %.2f", shop.getName(), shop.getPrice(product)))) 191 | .collect(Collectors.toList()); 192 | 193 | return priceFutures.stream() 194 | .map(CompletableFuture::join) // 모든 비동기 동작이 끝나길 대기 195 | .collect(Collectors.toList()); 196 | } 197 | ``` 198 | 199 | 두 map 연산을 하나의 스트림 처리 파이프라인이 아닌, 두 개의 파이프라인으로 처리했다는 사실에 주목하자. 200 | 201 | 스트림 연산은 게으른 특성이 있으므로 하나의 파이프라인으로 처리했다면 모든 가격 정보 요청 동작이 동기적, 순차적으로 이루어지게 된다 202 | 203 | ## 16.3.3 더 확장성이 좋은 해결 방법 204 | 205 | - 병렬 스트림 버전의 코드는 지정한 숫자의 상점에 하나의 스레드를 할당하여 네 개의 작업을 병렬로 처리하였다. 상점이 추가되는 경우는 어떻게 될까 ? (스레드 갯수는 4개가 최대라고 제한) 206 | - 순차 실행인 경우는 5초 이상, 병렬 스트림, CompletableFuture를 사용한 경우 2초 이상이 소모 될것이다. 207 | - 병렬 스트림과 CompletableFuture는 비슷한 결과를 보이지만 CompletableFuture는 병렬 스트림에 비해 작업에 사용할 다양한 Executor를 지정할 수 있다. 208 | 209 | ## 16.3.4 커스텀 Executor 사용하기 210 | 211 | 애플리케이션에서 사용하는 자원을 고려하여 풀에서 관리하는 최적의 스레드 수에 맞게 Executor를 만드는 것이 효율적일것이다. 212 | 213 | 스레드가 너무 많을수록 사용하지 않는 스레드가 많아지고 서버가 크래시 날 수 있으므로 적정한 갯수를 지정하는것이 중요하다. 214 | 215 | ```java 216 | private final Executor executor = Executors.newFixedThreadPool(Math.min(shops.size(), 100), new ThreadFactory() { 217 | @Override 218 | public Thread newThread(Runnable r) { 219 | Thread t = new Thread(r); 220 | t.setDaemon(true); // 프로그램 종료를 방해하지 않는 데몬 스레드를 사용. 221 | return t; 222 | } 223 | }); 224 | 225 | // 데몬 스레드를 사용하면 자바 프로그램이 종료될 때 강제로 스레드 실행이 종료될 수 있다. 226 | ``` 227 | 228 | # 16.4 비동기 작업 파이프라인 만들기 229 | 230 | Stream API의 map 메서드와 CompletableFuture의 메서드들을 이용하여 비동기 작업 파이프라인을 만들 수 있다. 231 | 232 | - supplyAsync : 전달받은 람다 표현식을 비동기적으로 수행한다. 233 | - thenApply : CompletableFuture가 동작을 완전히 완료한 다음에 thenApply로 전달된 람다 표현식을 적용한다. 234 | - thenCompose : 첫 번째 연산의 결과를 두 번째 연산으로 전달한다. 235 | - thenCombine : 독립적으로 실행된 두 개의 CompletableFuture 결과를 이용하여 연산한다. 두 결과를 어떻게 합칠지 정의된 BiFunction을 두 번째 인수로 받는다. 236 | - thenCombineAsync : 두 개의 CompletableFuture 결과를 반환하는 새로운 Future를 반환한다. 237 | 238 | 상점이 하나의 할인 서비스를 사용한다고 가정, 다음과 같이 구현할 수 있다. 239 | 240 | ```java 241 | public class Discount { 242 | public enum Code { 243 | NONE(0), SILVER(5), GOLD(10), PLATINUM(15), DIAMOND(20); 244 | 245 | private final int percentage; 246 | 247 | Code(int percentage) { 248 | this.percentage = percentage; 249 | } 250 | } 251 | ... 252 | } 253 | 254 | public String getPrice(String product) { 255 | double price = calculatePrice(product); 256 | Discount.Code value = Discount.Code.values()[new Random().nextInt(Discount.Code.values().length)]; 257 | return String.format("$s %.2f $s", name, price, value); 258 | } 259 | ``` 260 | 261 | ## 16.4.1 할인 서비스 구현 262 | 263 | 할인 정보는 언제든 변경될 수 있으므로 매번 서버에서 받아오는걸로 가정 264 | 265 | ```java 266 | public class Discount { 267 | public enum Code { 268 | } 269 | 270 | public static String applyDiscount(Quote quote) { 271 | return quote.getShopName() + "price is " + Discount.apply(quote,getPrice, quote.getDiscountCoe()); 272 | } 273 | 274 | private static double apply(double price, Code code) { 275 | delay(); 276 | return format(price * (100 - code.percentage) / 100); 277 | } 278 | } 279 | ``` 280 | 281 | ## 16.4.2 할인 서비스 사용 282 | 283 | Discount 서비스는 원격 서비스이므로 1초의 지연을 추가 284 | 285 | ```java 286 | public List findPrices(String product) { 287 | return shops.stream() 288 | .map(shop -> shop.getPrice(product)) 289 | .map(Quote::parse) 290 | .map(Discount::applyDiscount) 291 | .collect(toList()); 292 | } 293 | ``` 294 | 295 | - 세 개의 map 연산을 사용하여 상점 스트림에 파이프라인으로 원하는 결과를 얻었지만 성능 최적화와는 거리가 먼 코드이다. 296 | - 병렬 스트림으로 개선한다면 성능이 좋아지겠지만 스레드 풀의 크기가 고정되어 있다는 단점으로 인해 CompletableFuture에서 수행하는 커스텀 Executor를 정의해보자. 297 | 298 | ## 16.4.3 동기 작업과 비동기 작업 조합하기 299 | 300 | ```java 301 | public List findPrices(String product) { 302 | List> priceFutures = 303 | shops.stream() 304 | .map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice(product), executor)) 305 | .map(future -> future.thenApply(Quote::parse)) 306 | .map(future -> future.thenCompose(quote -> CompletableFuture.supplyAsync(() -> Discoount.applyDiscount(qoute), executor))) 307 | .collect(toList()); 308 | 309 | return priceFutures.stream() 310 | .map(CompletableFuture::join) 311 | .collect(toList()); 312 | } 313 | ``` 314 | 315 | - **가격 정보 얻기** 316 | - 첫번째 연산에서 supplyAsync에 닮다 표현식을 전달하여 비동기적으로 상점에서 정보를 조회했다. 첫번째 반환 결과는 Stream futurePriceInUSD = CompletableFuture.supplyAsync(() -> shop.getPrice(product)) // 1번째 태스크 - 가격정보 요청 335 | .thenCombine(CompletableFuture.suuplyAsync( 336 | () -> exchangeService.getRate(Money.EUR, Money.USD)), // 2번째 태스크 - 환율정보 요청 337 | (price, rate) -> price * rate)); // 두 결과 합침 338 | ``` 339 | 340 | 독립적인 두 개의 비동기 태스크는 각각 수행되고, 마지막에 합쳐진다. 341 | 342 | ## 16.4.6 **타임아웃 효과적으로 사용하기** 343 | 344 | Future가 작업을 끝내지 못할 경우 TimeoutException을 발생시켜 문제를 해결할 수 있다. 345 | 346 | ```java 347 | Funtion futurePriceInUSD = CompletableFuture.supplyAsync(() -> shop.getPrice(product)) 348 | .thenCombine(CompletableFuture.suuplyAsync( 349 | () -> exchangeService.getRate(Money.EUR, Money.USD)), 350 | (price, rate) -> price * rate)) 351 | .orTimeout(3, TimeUnit.SECONDS); 352 | ``` 353 | 354 | compleOnTimeout 메서드를 통해 예외를 발생시키는 대신 미리 지정된 값을 사용하도록 할 수도 있다. 355 | 356 | ```java 357 | Funtion futurePriceInUSD = CompletableFuture.supplyAsync(() -> shop.getPrice(product)) 358 | .thenCombine(CompletableFuture.suuplyAsync( 359 | () -> exchangeService.getRate(Money.EUR, Money.USD)), 360 | .completOnTimeout(DEFAULT_RATE, 1, TimeUnit.SECONDS), 361 | (price, rate) -> price * rate)) 362 | .orTimeout(3, TimeUnit.SECONDS); 363 | ``` 364 | 365 | # 16.5 CompletableFuture의 종료에 대응하는 방법 366 | 367 | 각 상점에서 물건 가격 정보를 얻어오는 findPrices 메서드가 모두 1초씩 지연되는 대신, 0.5~2.5초씩 임의로 지연된다고 하자.그리고 각 상점에서 가격 정보를 제공할때마다 즉시 보여줄 수 있는 최저가격 검색 어플리케이션을 만들어보자. 368 | 369 | - thenAccept : CompletableFuture가 생성한 결과를 어떻게 소비할지 미리 지정한다. 370 | - allOf : 전달받은 CompletableFuture 배열이 모두 완료될 때 CompletableFuture를 반환한다. 371 | - anyOf : 전달받은 CompletableFuture 배열 중 하나라도 작업이 끝났을 때 완료한 CompletableFuture를 반환한다. 372 | 373 | ## 16.5.1 최저가격 검색 어플리케이션 리팩터링 374 | 375 | ```java 376 | public Stream> findPriceStream(String product) { 377 | return shop.stream() 378 | .map(shop -> CompletableFuture.suppltAsync( 379 | () -> shop.getPrice(product), executor)) 380 | .map(future -> future.thenApply(Quote::parse)) 381 | .map(future -> future.thenCompose(quote -> 382 | CompletableFuture.supplyAsync( 383 | () -> Discount.applyDiscount(quote), executor))); 384 | } 385 | ``` 386 | 387 | 이제 findPriceStream 메서드 내부에서 세 가지 map 연산을 적용하고 반환하는 스트림에 네 번째 map 연산을 적용하자. 388 | 389 | ```java 390 | findPriceStream("myPhone").map(f -> f.thenAccept(System.out::println)); 391 | ``` 392 | 393 | 팩토리 메서드 allOf는 전달된 모든 CompletableFuture가 완료된 후에 CompletableFuture를 반환한다.이를 통해 모든 결과가 반환되었음을 확인할 수 있다. 394 | 395 | ```java 396 | CompletableFuture[] futures = findPriceStream("myPhone") 397 | .map(f -> f.thenAccept(System.out::println)) 398 | .toArray(size -> new CompletableFuture[size]); 399 | CompletableFuture.allOf(futues).join(); 400 | ``` 401 | 402 | 만약 CompletableFuture 중 하나만 완료되기를 기다리는 상황이라면 팩토리메서드 anyOf를 사용할 수 있다. 403 | -------------------------------------------------------------------------------- /14장 자바 모듈 시스템/ch14.md: -------------------------------------------------------------------------------- 1 | 2 | # 14.1 모듈화: 추론하기 쉬운 소프트웨어 3 | 지금까지는 이해하고, 유지보수하기 쉬운 코드를 구현하는 데 사용할 수 있는 새로운 언어기능을 공부했음 👉 `"저수준"` 4 | 5 | 궁극적으로 소프트웨어 아키텍처에서 기반 코드를 바꿔야 할 때, 유추하기 쉽도록 생산성을 높일 수 있는 프로젝트가 필요함. 👉 `"고수준"` 6 | 7 | 추론하기 쉬운 소프트웨어를 만드는 데에는 **"관심사 분리"**, **"정보 은닉"** 이 필요하다. 8 | 9 |
10 | 11 | ## 14.1.1 관심사분리 12 | > **관심사분리** (SoC, Separation of Concerns) : 컴퓨터 프로그램을 고유의 기능으로 나누는 동작을 권장하는 원칙. 13 | 14 | - 특징 15 | - 회계 애플리케이션 개발을 한다고 가정하면,
16 | 파싱, 분석, 레포트 기능을 **모듈**, 즉 **서로 거의 겹치지 않는 코드 그룹**으로 분리할 수 있다. 17 | 18 | - 자바 패키지가 클래스를 그룹으로 만든다고 생각할 수 있지만,
19 | 패키지는 모듈성을 지원하지 않는다. 20 | 21 | - M/V/C 같은 아키텍처 관점에서의 상황, 복구 기법(ex. 장애 감지, 재시도)과 비즈니스 로직을 분리하는 하위 수준 접근 등의 상황에 유용 22 | 23 | - 장점 24 | 1. 개별 기능을 **따로 작업**할 수 있음 ➡️ 쉬운 협업 25 | 1. 개별 부분을 **재사용**하기 쉬움 26 | 1. 전체 시스템을 **쉽게 유지보수** 가능함. 27 | 28 | 29 |

30 | 31 | ## 14.1.2 정보 은닉 32 | > **정보 은닉** : 세부 구현을 숨기도록 장려하는 원칙 33 | 34 | - 특징 35 | - 요구사항이 자주 바뀌는 상황에서 세부 구현을 숨김으로써 어떤 부분을 수정했을 때 다른 부분까지 영향을 미칠 가능성 줄어듦 36 | - 즉, **코드**를 **관리**하고 **보호**하는 데 유용한 원칙 37 | 38 | - **컴파일러**로 은닉성 확인하기 39 | - `클래스 내의 컴포넌트` : **`private`** 키워드를 사용했는지를 기준으로 컴파일러을 이용해 캡슐화를 확인할 수 있었다. 40 | > **캡슐화** : 특정 코드 조각이 애플리케이션의 다른 부분과 고립되어 있음 41 | - `클래스`, `패키지` : 자바 9 이전에는 의도된 대로 공개되었는지 컴파일러로 확인할 수 **없었다**. 42 | 43 | 44 |

45 | 46 | # 14.2 자바 모듈 시스템을 설계한 이유 47 | ## 14.2.1 모듈화의 한계 48 | - 자바는 `클래스`, `패키지`, `JAR` 세 가지 수준의 **코드 그룹화** 제공 49 | - `클래스` : 접근 제한자, 캡슐화 50 | - `패키지`, `JAR` : 캡슐화 거의 지원 X 51 | 52 | ### 1) 제한된 가시성 제어 53 | 1. 패키지 간의 가시성 제어 기능 거의 X
54 | > **가시성 제어** : public, protected, default(패키지 수준), private 55 | 56 | ⬇️
57 | 1. 한 패키지의 클래스&인터페이스를 다른 패키지로 공개하려면 public으로 선언해야 함
58 | ⬇️
59 | 1. 모두에게 공개하는 꼴
60 | ⬇️
61 | 1. `public`이므로 사용자가 내부 구현을 마음대로 사용
62 | ⬇️
63 | 1. 내부적으로 사용할 목적이었는데 다른 프로그래머가 써버리면, 라이브러리 코드 바꾸기가 어려워짐. 보안 측면에서도 위험함.
64 | 65 | 66 | ### 2) 클래스 경로 67 | 애플리케이션을 번들&실행할 때, 68 | 69 | 1. 클래스를 모두 컴파일 70 | 1. 한 개의 평범한 JAR 파일에 넣음 71 | 1. 클래스 경로에 이 JAR 파일을 추가해 사용 72 | 73 | 그러나, 위와 같은 `클래스 경로 + JAR` 조합은 약점이 있음 74 | 75 | 1. 클래스 경로에는 같은 **클래스를 구분**하는 **버전** 개념이 **없다**. 76 | - 파싱 라이브러리의 `JSONParser` 클래스를 버전 1.0인지 2.0인지 지정 못함 77 | - 클래스 경로에 두 가지 버전의 같은 라이브러리 존재 78 | - 이때 어떤 일이 일어날지 예측 X. 79 | 1. 클래스 경로에는 **명시적인 의존성**을 **지원하지 않는다**. 80 | - 한 JAR 안의 모든 클래스는 `classes`라는 한 주머니로 합쳐짐 81 | - 한 JAR가 다른 JAR에 포함된 클래스 집합을 사용하라고 **명시적으로 의존성을 정의할 수 없음** 👉 `"JAR 지옥"` or "`클래스 경로 지옥"` 82 | - 어떤 일이 일어날지 예측 X. 83 | - "빠진 게 있는가?", "충돌이 있는가?" 같은 의문 발생 84 | - 결국 에러를 발생시키지 않고 **정상적으로 실행할 때까지** 클래스 경로에 클래스 파일을 더하거나, 제거해보는 수밖에 없음. 85 |

86 | 87 | ## 14.2.2 거대한 JDK 88 | 1. 환경에 따라 JDK 전부를 필요로 하지 않는 문제 발생 89 | 1. 컴팩트 프로파일 등장 90 | > **컴팩트 프로파일** : 관련 분야에 따라 JDK 라이브러리가 세 가지 프로파일로 나뉘어 각각 다른 메모리 풋프린트를 제공 91 | 92 | 1. 그러나, 땜질식 처리였고, 내부 API가 외부에 공개됨. 93 | 1. 호환성을 깨지 않고는 관련 API를 바꾸기가 어려워짐 94 | 1. JDK 자체를 모듈화할 수 있는 자바 모듈 시스템의 필요성 제기됨. 95 | 96 |
97 | 98 | ## 14.2.3 OSGI와 비교 99 | > [!NOTE] 100 | > 스킵 가능! 101 | 102 | - **OSGi란** 103 | - 자바 9에서 모듈화 기능이 추가되기 전에도 OSGi라는 모듈 시스템이 존재했음. (공식 기능 X) 104 | - OSGi는 광범위한 영역, 직소에서 제공하지 않는 여러 기능 제공 105 | - 번들이라 불리는 OSGi 모듈을 특정 OSGi 프레임워크 내에서만 실행 106 | - 프레임워크 예시) 아파치 펠릭스, 에퀴녹스 107 | - 번들의 동작에 필요한 **외부 패키지**가 **무엇**이며, 어떤 **내부 패키지**가 외부로 노출되어 다른 번들로 **제공**되는지를 서술하는 **텍스트 파일**로 각 번들을 정의. 108 | 109 | 110 | - **장점** 111 | 1. **시스템을 재시작하지 않고도** 애플리케이션의 다른 하위 부분을 **핫스왑**할 수 있다. 112 | 113 | > **핫스왑** : 시스템을 재시작하지 않고 코드나 모듈을 변경하는 기술 114 | 1. 프레임워크 내에 **같은 번들의 다른 버전**을 설치할 수 있다 115 | 116 | - **자바 9 모듈 시스템과의 차이점** 117 | - `OSGi` : 각 번들이 자체적인 클래스 로더 가짐. 118 | - `자바 9 모듈 시스템의 직소` : 애플리케이션 당 한 개의 클래스를 사용하므로 버전 제어 X 119 | 120 |

121 | 122 | # 14.3 자바 모듈 : 큰 그림 123 | ### 1) 모듈의 구조 124 | ```java 125 | module 모듈명 { 126 | 바디 127 | } 128 | ``` 129 | > [!NOTE] 130 | > `module`, `requires`, `export` 같은 모듈 관련 용어는 `제한된 키워드`다. 131 | > 132 | > 프로그램 내에서는 자유롭게 사용할 수 있지만, 모듈이 허용된 곳에서는 키워드로 해석됨 133 | 134 | ### 2) 모듈 디스크립터 135 | - **`module-info.java`** 라는 파일에 저장됨 136 | - 엄밀하게 따지면 137 | - **`모듈 선언(module declareation)`** : **텍스트 형식** 138 | - **`모듈 디스크립터`** : `module-info.class`에 저장된 **바이너리 형식** 139 | - 보통 **패키지와 같은 폴더**에 위치 140 | - 한 개 이상의 패키지를 서술&캡슐화하고, 단순한 상황에서는 이들 패키지 중 한 개만 외부로 노출시킴 141 | ```java 142 | //module-info.java 143 | module 모듈명 { 144 | exports 패키지명 //한 페이지를 노출시키는 간단한 형식 145 | requires 모듈명 //0개 이상의 모듈 146 | } 147 | ``` 148 | 149 | 150 | - `exports` : 돌출부 151 | - `requires` : 패인 부분 152 | 153 |

154 | 155 | # 14.4 자바 모듈 시스템으로 애플리케이션 개발하기 156 | ## 14.4.1 애플리케이션 셋업 157 | ``` 158 | - 파일이나 URL에서 비용 목록을 읽는다. 159 | - 비용의 문자열 표현을 파싱한다. 160 | - 통계를 계산한다. 161 | - 유용한 요약 정보를 표시한다. 162 | - 각 태스크의 시작, 마무리 지점을 제공한다. 163 | ``` 164 | ➡️ 이 요구사항들에서 분리할 수 있는 여러 기능 (관심사) 165 | 166 | 1. 다양한 소스에서 데이터를 읽음(Reader, HttpReader, FileReader) 167 | 1. 다양한 포맷으로 구성된 데이터를 파싱(Parser, JSONParserk ExpenseJSON-Parser) 168 | 1. 도메인 객체를 구체화(Expense) 169 | 1. 통계를 계산하고 반환(SummaryCalculator, SummaryStatistics) 170 | 1. 다양한 기능을 분리 조정(ExpensesApplication) 171 | 172 | ➡️ 이 기능들을 그룹화 173 | - expenses.readers 174 | - expenses.readershttp 175 | - expenses.readersfile 176 | - expenses.parsers 177 | - expenses.parsersjson 178 | - expenses.model 179 | - expenses.statistics 180 | - expenses.application 181 | 182 |
183 | 184 | ## 14.4.2 세부적인 모듈화, 거친 모듈화 185 | | | 세부적인 모듈화 | 거친 모듈화 | 186 | | --- | --- | --- | 187 | | **특징** | 모든 패키지가 자신의 모듈을 갖는다. | 한 모듈이 시스템의 모든 패키지를 포함한다. | 188 | | **단점** | 이득에 비해 설계 비용이 증가 | 모듈화의 장점을 잃음 | 189 | 190 |
191 | 192 | ## 14.4.3 자바 모듈 시스템 기초 193 | ### 1) 디렉터리 구조 194 | ``` 195 | |-- expenses.application 196 | |-- module-info.java ===========> 모듈 디스크립터 197 | |-- com 198 | |-- example 199 | |-- expenses 200 | |-- application 201 | |-- ExpensesApplication.java 202 | ``` 203 | 204 | ### 2) 모듈 디스크립터 205 | - `module-info.java` 206 | - 모듈의 소스 코드 파일 **루트**에 위치 207 | - 모듈의 **의존성**, **어떤 기능**을 외부로 **노출할지** 정의 208 | - 현재는 이름만 정의되어 있고 내용은 비어있다. 209 | ```java 210 | module expenses.application { 211 | 212 | } 213 | ``` 214 | 215 | ### 3) 모듈화 애플리케이션 실행 216 | 217 | 218 | 219 |

220 | 221 | 222 | # 14.5 여러 모듈 활용하기 223 | ## 14.5.1 `exports` 구문 224 | > `exports` : 다른 모듈에서 사용할 수 있도록 특정 패키지를 공개 형식으로 만든다. 225 | 226 | ### 1) 특징 227 | - 모듈 내의 모든 것은 캡슐화되는데, **화이트 리스트** 기법을 이용하기 때문에 **다른 모듈에서 사용할 수 있는 기능이 무엇**인지 **명시적**으로 결정해야 한다. 228 | 229 | ### 2) 예시 230 | ```java 231 | module expenses.readers { 232 | exports com.example.expenses.readers; 233 | exports com.example.expenses.readers.file; 234 | exports com.example.expenses.readers.http; 235 | } 236 | ``` 237 | - 세 개 모두 모듈명이 아닌 **패키지명**이다. 238 | 239 | ### 3) 디렉터리 구조 240 | ``` 241 | |-- expenses.application 242 | |-- module-info.java ===========> 모듈 디스크립터 243 | |-- com 244 | |-- example 245 | |-- expenses 246 | |-- application 247 | |-- ExpensesApplication.java 248 | 249 | 250 | |-- expenses.readers ===========> 모듈 251 | |-- module-info.java ===========> 모듈 디스크립터 252 | |-- com ===========> 패키지 253 | |-- example 254 | |-- expenses 255 | |-- readers 256 | |-- Reader.java 257 | |-- file 258 | |-- FileReader.java 259 | |-- http 260 | |-- HttpReader.java 261 | ``` 262 | 263 |
264 | 265 | ## 14.5.2 `requires` 구문 266 | > `requires` : 의존하고 있는 모듈을 지정한다. 267 | 268 | ### 1) 예시 269 | ```java 270 | module expenses.readers { 271 | requires java.base; 272 | 273 | exports com.example.expenses.readers; 274 | exports com.example.expenses.readers.file; 275 | exports com.example.expenses.readers.http; 276 | } 277 | ``` 278 | - `requires` 다음 : 패키지 명이 아니라 **모듈명**이다. 279 | - `exports` 다음 : 세 개 모두 모듈명이 아닌 **패키지명**이다. 280 | 281 |
282 | 283 | > [!NOTE] 284 | > 모든 모듈은 `java.base`라는 플랫폼 모듈에 의존하는데 항상 필요한 기본 모듈이므로 생략 285 | 286 |

287 | 288 | ## 14.5.3 이름 규칙 289 | 1. **인터넷 도메인명의 역순** (com.iteratrlearning.training) 290 | 1. **노출된 주요 API 패키지**와 **이름이 같아**야 한다. 291 | 292 |

293 | 294 | # 14.6 컴파일과 패키징 295 | 1. 각 모듈에 `pom.xml`을 추가한다. 296 | ``` 297 | |-- pom.xml ** 298 | |-- expenses.application 299 | |-- pom.xml ** 300 | |-- src 301 | |-- main 302 | |-- java 303 | |-- module-info.java ** 304 | |-- com 305 | |-- example 306 | |-- expenses 307 | |-- application 308 | |-- ExpensesApplication.java 309 | 310 | |-- expenses.readers 311 | |-- pom.xml ** 312 | |-- src 313 | |-- main 314 | |-- java 315 | |-- module-info.java ** 316 | |-- com 317 | |-- example 318 | |-- expenses 319 | |-- readers 320 | |-- Reader.java 321 | |-- file 322 | |-- FileReader.java 323 | |-- http 324 | |-- HttpReader.java 325 | ``` 326 | - 전체 프로젝트의 빌드를 조정하기 위해 **모든 모듈의 부모 모듈**에도 `pom.xml` 추가 327 | - 모듈 디스크립터(module-info.java)는 `src/main/java` 디렉터리에 위치해야 함 328 | - 각 모듈은 독립적으로 컴파일됨 ➡️ 각각이 한 개의 프로젝트다. 329 | 330 | 1. `expenses.readers` 프로젝트의 `pom.xml` 331 | ```xml 332 | 333 | 335 | 4.0.0 336 | 337 | com.example 338 | expenses.readers 339 | 1.0 340 | jar 341 | 342 | 343 | com.example 344 | expenses 345 | 1.0 346 | 347 | 348 | ``` 349 | - 명시적으로 부모 모듈을 지정함 350 | 351 | 1. `expenses.application` 모듈의 `pom.xml` 352 | ```xml 353 | 354 | 355 | 4.0.0 356 | 357 | com.example 358 | expenses.application 359 | 1.0 360 | jar 361 | 362 | 363 | com.example 364 | expenses 365 | 1.0 366 | 367 | 368 | 369 | 370 | com.example 371 | expenses.readers 372 | 1.0 373 | 374 | 375 | 376 | ``` 377 | 378 | - `ExpenseApplication`이 필요로 하는 클래스와 인터페이스가 있으므로 `expenses.readers`를 의존성으로 추가 379 | 380 | 1. 전역 `pom.xml` 381 | ```xml 382 | 383 | 387 | 4.0.0 388 | 389 | com.examples 390 | expenses 391 | pom 392 | 1.0 393 | 394 | 395 | expenses.application 396 | expenses.readers 397 | 398 | 399 | 400 | 401 | 402 | 403 | org.apache.maven.plugins 404 | maven-compiler-plugin 405 | 3.7.0 406 | 407 | 9 408 | 9 409 | 410 | 411 | 412 | 413 | 414 | 415 | ``` 416 | 417 | 1. `mvn clean package`로 프로젝트 모듈➡️JAR 로 변환 418 | 1. 부산물(artifact) 생성 419 | - `./expenses.application/target/expenses.application-1.0.jar` 420 | - `./expenses.readers/target/expenses.readers-1.0.jar` 421 | 422 | 1. 애플리케이션 실행 423 | ```bash 424 | java --module-path \ 425 | ./expenses.application/target/expenses.application-1.0.jar:\ 426 | ./expenses.readers/target/expenses.readers-1.0.jar \ 427 | --module \ 428 | expenses.application/com.example.expenses.application.ExpensesApplication 429 | ``` 430 | 431 |

432 | 433 | # 14.7 자동 모듈 434 | - 자바는 JAR를 자동 모듈이라는 형태로 적절하게 변환한다. 435 | - **모듈 경로상**에 있으나 **`module-info` 파일**을 **가지지 않은** 모든 **JAR**는 **자동 모듈**이 된다. 436 | - 자동 모듈은 자신의 **모든 패키지를 노출**시킨다. 437 | - 자동 모듈의 이름은 **JAR 이름**을 이용해 정해진다. 438 | - 바꾸려면 439 | ```bash 440 | jar --file=./expenses. readers/target/dependency/httpclient-4.5.3.jar \ 441 | --describe-module httpclient@4.5.3 automatic 442 | ``` 443 | - httpclient라는 이름으로 바뀜. 444 | - httpclient JAR를 모듈 경로에 추가한 다음 애플리케이션 실행 445 | ```bash 446 | java --module-path \ 447 | ./expenses. application/target/expenses .application-1.0.jar:\ 448 | ./expenses . readers/ target/expenses.readers-1.0.jar \ 449 | ./expenses. readers/ target/dependency/httpclient-4.5.3.jar \ 450 | -module \ 451 | expenses.application/com.example.expenses.application.ExpensesApplication 452 | ``` 453 | 454 |

455 | 456 | # 14.8 모듈 정의와 구문들 457 | ## 14.8.1 `requires` 458 | 생략 459 | ## 14.8.2 `exports` 460 | 생략 461 | ## 14.8.3 `requires transitive` 462 | > `requires transitive` : 다른 모듈이 제공하는 public type을 한 모듈에서 사용할 수 있다고 지정할 수 있다. (**전이성**) 463 | 464 | ### 예시 465 | ```java 466 | module com.iteratrlearning.ui { 467 | requires transitive com.iteratrlearning.core; 468 | 469 | exports com.iteratrlearning.ui.panels; 470 | exports com.iteratrlearning.ui.widgets; 471 | } 472 | ``` 473 | ```java 474 | module com.iteratrlearning.application { 475 | requires com.iteratrlearning.ui; 476 | } 477 | ``` 478 | - `com.iteratrlearning.application` 모듈은 `com.iteratrlearning.core`에서 노출한 public type에 접근할 수 O. 479 | - 필요로 하는 모듈 (`com.iteratrlearning.ui`)이 다른 모듈(`com.iteratrlearning.core`)의 형식을 반환하는 상황에서 전이성 선언이 유용해짐. 480 | - 즉, `com.iteratrlearning.core` ➡️ `com.iteratrlearning.ui` ➡️ `com.iteratrlearning.application` 481 | 482 |
483 | 484 | ## 14.8.4 `exports to` 485 | > `exports to` : 사용자에게 공개할 기능을 제한함으로 가시성을 정교하게 제어 486 | 487 | ### 예시 488 | ```java 489 | module com.iteratrlearning.ui { 490 | requires com.iteratrlearning.core; 491 | 492 | exports com.iteratrlearning.ui.panels; 493 | exports com.iteratrlearning.ui.widgets to com.iteratrlearning.ui.widgetuser; 494 | } 495 | ``` 496 | 497 | - `com.iteratrlearning.ui.widgets`의 접근 권한을 가진 사용자의 권한을 `com.iteratrlearning.ui.widgetuser`로 제한할 수 있다. 498 | 499 |
500 | 501 | ## 14.8.5 `open`과 `opens` 502 | > `open` : 모듈 선언에 open 한정자를 이용하면 다른 모듈에 모든 패키지를 Reflection access 할 수 있도록 허용할 수 있다. 503 | 504 | > `Reflection(리플렉션)` : 실행 중인 클래스, 메서드, 필드 등에 접근하는 기능 505 | 506 | ### 1) 예시 507 | 1. `open` 없이 Reflection 접근 시 오류 발생 508 | 1. 모듈 선언 (module-info.java) 509 | ```java 510 | module com.example { 511 | exports com.example; 512 | } 513 | ``` 514 | 1. Reflection을 사용해 필드 접근 515 | ```java 516 | package com.example; 517 | import java.lang.reflect.Field; 518 | 519 | public class ReflectionExample { 520 | private String secret = "Hidden Data"; 521 | 522 | public static void main(String[] args) throws Exception { 523 | ReflectionExample obj = new ReflectionExample(); 524 | Field field = ReflectionExample.class.getDeclaredField("secret"); 525 | field.setAccessible(true); // private 필드 접근 시도 526 | System.out.println("Secret Value: " + field.get(obj)); 527 | } 528 | } 529 | ``` 530 | 1. 실행하면 에러 발생 (Java 9 이상) 531 | ```java 532 | java.lang.reflect.InaccessibleObjectException: 533 | Unable to make field private java.lang.String com.example.ReflectionExample.secret accessible 534 | ``` 535 | 1. `open` 키워드 사용하여 Reflection 허용 536 | 1. open 사용한 모듈 선언 537 | ```java 538 | open module com.example { 539 | exports com.example; 540 | } 541 | ``` 542 | 1. 실행 결과 543 | ```java 544 | Secret Value: Hidden Data 545 | ``` 546 | 547 |
548 | 549 | ### 2) 특징 550 | - 자바 9 이전에는 리플렉션으로 객체의 private 변수에 접근할 수 있었다. 551 | - 즉, 진정한 캡슐화는 없었다. 552 | - 자바 9 에서는 private에는 접근할 수 없으며, open 구문을 명시적으로 사용해야 Reflection을 사용할 수 있다. 553 | - `opens` : 특정 패키지만 Reflection 허용 554 | - `open` : 전체 패키지 허용 555 | - `open to` : 특정 모듈에만 허용 556 | -------------------------------------------------------------------------------- /9장 리팩토링, 테스팅, 디버깅/ch9.md: -------------------------------------------------------------------------------- 1 | # 9.1 가독성과 유연성을 개선하는 리팩토링 2 | 3 | ## 리팩토링 예제 3가지 4 | 5 | - **익명 클래스를 람다 표현식으로 리팩토링하기** 6 | - **람다 표현식을 메서드 참조로 리팩토링하기** 7 | - **명령형 데이터 처리를 스트림으로 리팩토링하기** 8 | 9 | ### 1. 익명 클래스를 람다 표현식으로 10 | 11 | ```java 12 | // 익명클래스 13 | Runnable r1 = new Runnable(){ 14 | public void run(){ 15 | System.out.println("hello!!"); 16 | } 17 | }; 18 | // 람다 표현식으로 리팩토링 19 | Runnable r2 =()-> System.out.println("hello!!"); 20 | ``` 21 | 22 | 익명 클래스를 람다 표현식으로 변환 불가능한 경우 23 | 24 | 1. 익명 클래스에서의 this는 람다를 감싸는 클래스를 가르키며, this는 익명클래스는 자신을 가르키므로 this와 super 처리를 잘해주어야 함 25 | 2. 익명 클래스를 감싸고 있는 클래스의 변수를 가릴 수 있지만(shadow variable) 람다표현식으로는 불가능 26 | 3. 익명 클래스를 람다 표현식을 바꿀 시 콘텍스트 오버로딩에 따른 모호함이 초래 됨 27 | 28 | 익명 클래스는 인스턴스화할 때 명시적으로 형식이 정해지는 반면 람다의 형식은 콘텍스트에 따라 달라지기 때문 29 | 30 | ```java 31 | public interface Task { 32 | public void execute(); 33 | } 34 | public interface Task { 35 | public void execute(); 36 | } 37 | public static void doSomething(Runnable r){ r.run();} 38 | public static void doSomething(Task a){ r.execute();} 39 | 40 | doSomething(new Task(){ 41 | public void execute(){ 42 | System.out.println("Danger danger!!"); 43 | } 44 | }) 45 | // 람다 표현식과 같은 경우에는 모호함 46 | doSomething(()->System.out.println("Danger danger!!")); 47 | ``` 48 | 49 | 50 | ### 2. 람다 표현식을 메서드 참조로 리팩토링하기 51 | 52 | 람다 표현식을 별도의 메서드로 추출한 다음에 groupingBy 인수로 전달할 시 코드가 간결해지고 의도가 명확해짐 53 | 54 | ```java 55 | // 이전 56 | private static Map> groupDishesByCaloricLevel() { 57 | return menu.stream().collect( 58 | groupingBy(dish -> { 59 | if (dish.getCalories() <= 400) { 60 | return CaloricLevel.DIET; 61 | } 62 | else if (dish.getCalories() <= 700) { 63 | return CaloricLevel.NORMAL; 64 | } 65 | else { 66 | return CaloricLevel.FAT; 67 | } 68 | }) 69 | ); 70 | } 71 | 72 | // 이후 73 | Map> dishesByCaloricLev = menu.stream().collect(groupingBy(Dish::getCaloricLevel)); 74 | 75 | // dish class 해당 메서드 추가 76 | public Grouping.CaloricLevel getCaloricLevel(){ 77 | if(this.getCalories() <= 400) return Grouping.CaloricLevel.DIET; 78 | else if (this.getCalories() <=700) return Grouping.CaloricLevel.NORMAL; 79 | else return Grouping.CaloricLevel.FAT; 80 | } 81 | 82 | ``` 83 | 84 | `comparing`과 `maxBy` 와 같은 정적 헬퍼 메서드 활용 85 | 86 | ```java 87 | // 1 88 | inventory.sort(new Comparator() { 89 | 90 | @Override 91 | public int compare(Apple a1, Apple a2) { 92 | return a1.getWeight() - a2.getWeight(); 93 | } 94 | }); 95 | // 2 96 | inventory.sort((Apple a1, Apple a2)-> a1.getWeight().compareTo(a2.getWeight())); 97 | 98 | 99 | // 3 100 | inventory.sort(comparing(Apple::getWeight)); 101 | 102 | ``` 103 | 104 | 내장 컬렉터 이용하기 105 | 106 | ```java 107 | // 이전 108 | menu.stream().collect(reducing(0, Dish::getCalories, (Integer i, Integer j) -> i + j) 109 | // 이후 110 | menu.stream().collect(summingInt(Dish::getCalories)); 111 | ``` 112 | 113 | ### 3. 명령형 데이터 처리를 스트림으로 리팩토링하기 114 | 115 | 반복자를 이용한 기존의 컬렉션 처리 코드를 Stream API로 바꿔주기 116 | 117 | → 쇼트서킷과 LAZY 최적화 + 멀티코어 아키텍쳐(비동기) 이점 118 | 119 | 필터링 + 추출 코드 120 | 121 | ```java 122 | menu.stream().parallel() 123 | .filter(d -> d.getCalories() > 300) 124 | .map(Dish::getName) 125 | .collect(toList()); 126 | 127 | ``` 128 | 129 | ### 4. 코드 유연성 개선 130 | 131 | 람다 표현식 이용 시 **동작 파라미터화**를 쉽게 구현 가능 132 | 133 | **함수형 인터페이스 적용** 134 | 135 | - 조건부 연기 실행 136 | - 실행 어라운드 137 | 138 | **조건부 연기 실행** 139 | 140 | ```java 141 | // logger의 상태가 isLoggable이라는 메서드에 의해 클라이언트 코드로 노출 142 | // 메시지를 로깅할 때마다 logger 객체의 상태를 확인할 필요가 없음 143 | if(logger.isLoggalbe(Log.FINER)){ 144 | logger.finer("PROBLEM" + generateDiagnostic()); 145 | } 146 | // 메시지를 로깅하기 전에 logger 객체가 적절한 수준으로 설정되었는지 내부적으로 확인 147 | // 이를 통해 불필요한 if 문 제거 및 logger 상태 노출할 필요 없음 148 | logger.log(Level.FINER, "PROBLEM" + generateDiagnostic()); 149 | 150 | // 람다를 통해 메시지 생성을 지연 151 | logger.log(Level.Finer, ()-> "PROBLEM" + generateDiagnostic()) 152 | 153 | // log 메서드 내부 구현 154 | public void log(Level level, Supplier msgSupplier){ 155 | if(logger.isLoggable(level)){ 156 | log(level, msgSupplier.get()); 157 | } 158 | } 159 | ``` 160 | 161 | **실행 어라운드** 162 | 163 | 람다를 이용해 다양한 방식으로 파일을 처리할 수 있도록 파라미터화 164 | 165 | ```java 166 | String oneLine = processFile((BufferedReader b) -> b.readLine()); 167 | System.out.println(oneLine); 168 | 169 | String twoLines = processFile((BufferedReader b) -> b.readLine() + b.readLine()); 170 | System.out.println(twoLines); 171 | 172 | public static String processFileLimited() throws IOException { 173 | try (BufferedReader br = new BufferedReader(new FileReader(FILE))) { 174 | return br.readLine(); 175 | } 176 | } 177 | 178 | public static String processFile(BufferedReaderProcessor p) throws IOException { 179 | try (BufferedReader br = new BufferedReader(new FileReader(FILE))) { 180 | return p.process(br); 181 | } 182 | } 183 | 184 | public interface BufferedReaderProcessor { 185 | 186 | String process(BufferedReader b) throws IOException; 187 | 188 | } 189 | 190 | ``` 191 | 192 | # 9.2 람다로 객체지향 디자인 패턴 리팩토링하기 193 | 194 | ## 전략 패턴 195 | 196 | ![image](https://github.com/user-attachments/assets/7a86b0c5-17b8-41ab-9d1f-10e83bc695ad) 197 | 198 | 알고리즘을 나타내는 인터페이스 → Strategy 인터페이스 199 | 200 | 다양한 알고리즘을 나타내는 한 개 이상의 인터페이스 구현 → ConcreteStrategyA, ConcreteStrategyB 201 | 202 | 전략 객체를 사용하는 한 개 이상의 클라이언트 203 | 204 | ```java 205 | interface ValidationStrategy { 206 | boolean execute(String s); 207 | } 208 | 209 | static private class IsAllLowerCase implements ValidationStrategy { 210 | 211 | @Override 212 | public boolean execute(String s) { 213 | return s.matches("[a-z]+"); 214 | } 215 | 216 | } 217 | 218 | static private class IsNumeric implements ValidationStrategy { 219 | 220 | @Override 221 | public boolean execute(String s) { 222 | return s.matches("\\d+"); 223 | } 224 | 225 | } 226 | ``` 227 | 228 | 구현한 클래스를 이용한 검증 전략 229 | 230 | ```java 231 | static private class Validator { 232 | 233 | private final ValidationStrategy strategy; 234 | 235 | public Validator(ValidationStrategy v) { 236 | strategy = v; 237 | } 238 | 239 | public boolean validate(String s) { 240 | return strategy.execute(s); 241 | } 242 | 243 | } 244 | ``` 245 | 246 | 람다 표현식 사용 예시 247 | 248 | ```java 249 | // old school 250 | Validator v1 = new Validator(new IsNumeric()); 251 | System.out.println(v1.validate("aaaa")); 252 | Validator v2 = new Validator(new IsAllLowerCase()); 253 | System.out.println(v2.validate("bbbb")); 254 | 255 | // with lambdas 256 | Validator v3 = new Validator((String s) -> s.matches("\\d+")); 257 | System.out.println(v3.validate("aaaa")); 258 | Validator v4 = new Validator((String s) -> s.matches("[a-z]+")); 259 | System.out.println(v4.validate("bbbb")); 260 | 261 | ``` 262 | 263 | ## 템플릿 메서드 264 | 265 | 사용자가 고객 ID를 애플리케이션에 입력할 시 은행 데이터베이스에서 고객 정보를 가져오고 고객이 원하는 서비스 제공할 시에 제공 서비스 동작을 정의하는 추상 클래스→ **makeCustomerHappy** 266 | 267 | ```java 268 | abstract class OnlineBanking { 269 | 270 | public void processCustomer(int id) { 271 | Customer c = Database.getCustomerWithId(id); 272 | makeCustomerHappy(c); 273 | } 274 | 275 | abstract void makeCustomerHappy(Customer c); 276 | 277 | // 더미 Customer 클래스 278 | static private class Customer {} 279 | 280 | // 더미 Database 클래스 281 | static private class Database { 282 | 283 | static Customer getCustomerWithId(int id) { 284 | return new Customer(); 285 | } 286 | 287 | } 288 | 289 | } 290 | ``` 291 | 292 | **makeCustomerHappy** 시그니처와 동일한 인수를 processCustomer에 추가 293 | 294 | 이를 통해 **(Customer c) -> System.out.println("Hello!"))** 와 같이 람다로 다양한 컴포넌트 구현 가능 295 | 296 | ```java 297 | public class OnlineBankingLambda { 298 | 299 | public static void main(String[] args) { 300 | new OnlineBankingLambda().processCustomer(1337, (Customer c) -> System.out.println("Hello!")); 301 | } 302 | 303 | public void processCustomer(int id, Consumer makeCustomerHappy) { 304 | Customer c = Database.getCustomerWithId(id); 305 | makeCustomerHappy.accept(c); 306 | } 307 | 308 | // 더미 Customer 클래스 309 | static private class Customer {} 310 | 311 | // 더미 Database 클래스 312 | static private class Database { 313 | 314 | static Customer getCustomerWithId(int id) { 315 | return new Customer(); 316 | } 317 | 318 | } 319 | 320 | } 321 | 322 | ``` 323 | 324 | ## 옵저버 패턴 325 | 326 | ![image](https://github.com/user-attachments/assets/12da09f2-b156-4796-8031-3743cd18e485) 327 | 328 | 한 객체가 다른 다수의 객체 리스트에 자동으로 알림을 보내야하는 상황에서 옵저버 디자인 패턴을 사용 329 | 330 | Observer 인터페이스는 새로운 트윗이 있을 때 주제(Feed)가 호출될 수 있도록 inform이라고 하는 메서드 제공 331 | 332 | ```java 333 | interface Observer { 334 | void inform(String tweet); 335 | } 336 | ``` 337 | 338 | 다양한 키워드에 다른 동작을 수행하는 여러 옵저버 339 | 340 | ```java 341 | 342 | static private class NYTimes implements Observer { 343 | 344 | @Override 345 | public void inform(String tweet) { 346 | if (tweet != null && tweet.contains("money")) { 347 | System.out.println("Breaking news in NY!" + tweet); 348 | } 349 | } 350 | 351 | } 352 | 353 | static private class Guardian implements Observer { 354 | 355 | @Override 356 | public void inform(String tweet) { 357 | if (tweet != null && tweet.contains("queen")) { 358 | System.out.println("Yet another news in London... " + tweet); 359 | } 360 | } 361 | 362 | } 363 | 364 | static private class LeMonde implements Observer { 365 | 366 | @Override 367 | public void inform(String tweet) { 368 | if (tweet != null && tweet.contains("wine")) { 369 | System.out.println("Today cheese, wine and news! " + tweet); 370 | } 371 | } 372 | 373 | } 374 | ``` 375 | 376 | 주제 377 | 378 | ```java 379 | interface Subject { 380 | void registerObserver(Observer o); 381 | void notifyObservers(String tweet); 382 | } 383 | ``` 384 | 385 | 주제는 `registerObserver`메서드로 새로운 옵저버를 등록한 뒤 `notifyObservers` 메서드로 트윗의 옵저버에 이를 알린다 386 | 387 | ```java 388 | static private class Feed implements Subject { 389 | 390 | private final List observers = new ArrayList<>(); 391 | 392 | @Override 393 | public void registerObserver(Observer o) { 394 | observers.add(o); 395 | } 396 | 397 | @Override 398 | public void notifyObservers(String tweet) { 399 | observers.forEach(o -> o.inform(tweet)); 400 | } 401 | } 402 | ``` 403 | 404 | 데모 애플리케이션 405 | 406 | ```java 407 | public static void main(String[] args) { 408 | Feed f = new Feed(); 409 | f.registerObserver(new NYTimes()); 410 | f.registerObserver(new Guardian()); 411 | f.registerObserver(new LeMonde()); 412 | f.notifyObservers("The queen said her favourite book is Java 8 & 9 in Action!"); 413 | 414 | Feed feedLambda = new Feed(); 415 | 416 | // 람다 표현식 이용 417 | feedLambda.registerObserver((String tweet) -> { 418 | if (tweet != null && tweet.contains("money")) { 419 | System.out.println("Breaking news in NY! " + tweet); 420 | } 421 | }); 422 | feedLambda.registerObserver((String tweet) -> { 423 | if (tweet != null && tweet.contains("queen")) { 424 | System.out.println("Yet another news in London... " + tweet); 425 | } 426 | }); 427 | 428 | feedLambda.notifyObservers("Money money money, give me money!"); 429 | } 430 | 431 | ``` 432 | 433 | ## 책임 연쇄 패턴 434 | 435 | 객체들이 연결된 체인 형태로 존재하며, 각 객체가 요청을 처리하지 못할 경우 다음 객체에게 책임을 전달하고, 다음 객체 또한 처리하지 못하면 다시 다음 객체로 전달하는 패턴 436 | 437 | ![image](https://github.com/user-attachments/assets/45213aa5-5b35-439c-9573-a0a5106cabf4) 438 | 439 | 다이어그램을 보면 템플릿 메서드 디자인 패턴이 사용되었음 440 | 441 | ```java 442 | private static abstract class ProcessingObject { 443 | 444 | protected ProcessingObject successor; 445 | 446 | public void setSuccessor(ProcessingObject successor) { 447 | this.successor = successor; 448 | } 449 | 450 | public T handle(T input) { 451 | T r = handleWork(input); 452 | if (successor != null) { 453 | return successor.handle(r); 454 | } 455 | return r; 456 | } 457 | 458 | abstract protected T handleWork(T input); 459 | 460 | } 461 | ``` 462 | 463 | ```java 464 | private static class HeaderTextProcessing extends ProcessingObject { 465 | 466 | @Override 467 | public String handleWork(String text) { 468 | return "From Raoul, Mario and Alan: " + text; 469 | } 470 | 471 | } 472 | 473 | private static class SpellCheckerProcessing extends ProcessingObject { 474 | 475 | @Override 476 | public String handleWork(String text) { 477 | return text.replaceAll("labda", "lambda"); 478 | } 479 | 480 | } 481 | 482 | ``` 483 | 484 | 두 작업처리 객체를 연결해 작업 체인을 만든다 485 | 486 | ```java 487 | public static void main(String[] args) { 488 | ProcessingObject p1 = new HeaderTextProcessing(); 489 | ProcessingObject p2 = new SpellCheckerProcessing(); 490 | p1.setSuccessor(p2); 491 | String result1 = p1.handle("Aren't labdas really sexy?!!"); 492 | System.out.println(result1); 493 | 494 | // 람다 표현식 이용 495 | UnaryOperator headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text; 496 | UnaryOperator spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda"); 497 | Function pipeline = headerProcessing.andThen(spellCheckerProcessing); 498 | String result2 = pipeline.apply("Aren't labdas really sexy?!!"); 499 | System.out.println(result2); 500 | } 501 | 502 | ``` 503 | 504 | ## 팩토리 505 | 506 | 인스턴스화 로직을 클라이언트에 노출하지 않고 객체를 만들 때 팩토리 디자인 패턴 사용 507 | 508 | ```java 509 | static private class ProductFactory { 510 | 511 | public static Product createProduct(String name) { 512 | switch (name) { 513 | case "loan": 514 | return new Loan(); 515 | case "stock": 516 | return new Stock(); 517 | case "bond": 518 | return new Bond(); 519 | default: 520 | throw new RuntimeException("No such product " + name); 521 | } 522 | } 523 | 524 | // 생성자와 설정을 외부로 노출하지 않음으로써 클라이언트가 단순히 상품을 생산 가능 525 | Product p1 = ProductFactory.createProduct("loan"); 526 | ``` 527 | 528 | 생성자와 설정을 외부로 노출하지 않음 529 | 530 | ```java 531 | // 람다 표현식 이용 532 | Supplier loanSupplier = Loan::new; 533 | 534 | final static private Map> map = new HashMap<>(); 535 | static { 536 | map.put("loan", Loan::new); 537 | map.put("stock", Stock::new); 538 | map.put("bond", Bond::new); 539 | } 540 | 541 | ``` 542 | 543 | # 9.3 람다 테스팅 544 | 545 | 예제 코드 546 | 547 | ```java 548 | private static class Point { 549 | 550 | private int x; 551 | private int y; 552 | 553 | private Point(int x, int y) { 554 | this.x = x; 555 | this.y = y; 556 | } 557 | 558 | public int getX() { 559 | return x; 560 | } 561 | 562 | public void setX(int x) { 563 | this.x = x; 564 | } 565 | public Point moveRightBy(int x){ 566 | return new Point(this.x + x, this.y); 567 | } 568 | 569 | } 570 | ``` 571 | 572 | 예제 테스트 코드 573 | 574 | ```java 575 | @Test 576 | public void testMoveRightBy() throws Exception { 577 | Point p1 = new Point(5,5); 578 | Point p2 = p1.moveRightBy(10); 579 | assertEquals(15, p2.getX()); 580 | assertEquals(5, p2.getY()); 581 | } 582 | ``` 583 | 584 | ### 보이는 람다 표현식의 동작 테스팅 585 | 586 | ```java 587 | public final static Comparator compareByXAndThenY = 588 | Comparator.comparing(Point::getX).thenComparing(Point::getY); 589 | ... 590 | 591 | @Test 592 | public void testMoveRightBy() throws Exception { 593 | Point p1 = new Point(10,15); 594 | Point p2 = new Point(10, 20); 595 | int result = Point.compareByXAndThenY.compare(p1, p2); 596 | assertEquals(result < 0); 597 | } 598 | ``` 599 | 600 | ### 복잡한 람다를 개별 메서드로 분할하기 601 | 602 | 람다 표현식을 메서드 참조로 바꾸기(새로운 일반 메서드 선언) 603 | 604 | ### 고차원 함수 테스팅 605 | 606 | 함수를 인수로 받거나 다른 함수를 반환하는 메서드(고차원 함수)인 경우 다른 람다로 메서드의 동작을 테스트할 수 있음 607 | 608 | # 디버깅 609 | 610 | 기존 디버깅의 경우 보편적으로 스택 트레이스와 로깅을 확인하지만 람다는 이러한 확인이 어려운 경우가 많음 611 | 612 | ### 스택트레이스 613 | 614 | ```java 615 | import java.util.function.Consumer; 616 | 617 | public class LambdaExceptionTest { 618 | public static void main(String[] args) { 619 | Consumer faultyLambda = s -> { 620 | System.out.println("Processing: " + s); 621 | if (s.equals("error")) { 622 | throw new RuntimeException("Intentional Exception!"); 623 | } 624 | }; 625 | 626 | try { 627 | faultyLambda.accept("error"); 628 | } catch (Exception e) { 629 | e.printStackTrace(); 630 | } 631 | } 632 | } 633 | 634 | ``` 635 | 636 | **에러 발생** 637 | 638 | ```java 639 | Processing: error 640 | Exception in thread "main" java.lang.RuntimeException: Intentional Exception! 641 | at LambdaExceptionTest.lambda$main$0(LambdaExceptionTest.java:8) 642 | at LambdaExceptionTest.main(LambdaExceptionTest.java:14) 643 | 644 | ``` 645 | 646 | - `LambdaExceptionTest.lambda$main$0` 와 같은 메서드명이 등장함. 647 | - 람다는 컴파일 시 **익명 메서드 (lambda$메서드명$번호)** 형태로 변환됨. 648 | - 따라서 스택 트레이스를 보면 `lambda$main$0`이 **람다에서 발생한 예외임을 알 수 있음.** 649 | 650 | 반면 함수 참조에서 발생한 에러는 스택 트레이스에 제대로 표시 됨 651 | 652 | ### 로깅 653 | 654 | `peek` 스트림의 각 요소를 소비한 것처럼 동작을 실행하지만 forEach처럼 실제로 스트림의 요소를 소비하지 않기에 각 단계별 상태를 보여줄 수 있음 655 | 656 | peek는 파이프라인 각 동작 전후의 값을 출력해줌 657 | 658 | ```java 659 | numbers.stream() 660 | .map(x -> x + 17) 661 | .filter(x -> x % 2 == 0) 662 | .limit(3) 663 | .forEach(System.out::println); 664 | 665 | ``` 666 | 667 | ![image](https://github.com/user-attachments/assets/8bddb648-5ff4-4daf-bc3b-ef35f0cbfa18) 668 | 669 | ```java 670 | public class Peek { 671 | 672 | public static void main(String[] args) { 673 | List result = Stream.of(2, 3, 4, 5) 674 | .peek(x -> System.out.println("taking from stream: " + x)) 675 | .map(x -> x + 17) 676 | .peek(x -> System.out.println("after map: " + x)) 677 | .filter(x -> x % 2 == 0) 678 | .peek(x -> System.out.println("after filter: " + x)) 679 | .limit(3) 680 | .peek(x -> System.out.println("after limit: " + x)) 681 | .collect(toList()); 682 | } 683 | 684 | } 685 | 686 | ``` 687 | 688 | 출력값 : 689 | 690 | from stream: 2 691 | 692 | after map: 19 693 | 694 | from stream: 3 695 | 696 | after map: 20 697 | 698 | after filter: 20 699 | 700 | after limit: 20 701 | 702 | from stream: 4 703 | 704 | after map: 21 705 | 706 | from stream: 5 707 | 708 | after map: 22 709 | 710 | after filter: 22 711 | 712 | after limit: 22 713 | --------------------------------------------------------------------------------