시작하며

Error 또는 중요한 배치 시 알람을 받을 방법으로 회사에서 사용하는 Slack을 접목시켰다.

 

링크 : https://github.com/seungsoos/slack-demo

 

Slack 의존성 추가 및 yml 설정

	implementation("com.slack.api:bolt:1.18.0")
	implementation("com.slack.api:bolt-servlet:1.18.0")
	implementation("com.slack.api:bolt-jetty:1.18.0")

 

구글링을 통해 방법을 찾고, 해당 토큰을 입력한다.

 

코드내용은 아래와 같다.

@Slf4j
@Component
@RequiredArgsConstructor
public class SlackComponentImpl implements SlackComponent {

    /**
     * Slack의 경우 하나의 채널당 tps 1 사용
     */
    @Value("${slack.send.tps}")
    private Integer tps;

    @Value(value = "${slack.token}")
    private String slackToken;
    private MethodsClient methods = null;

    private int tpsSendCount = 0;

    /**
     * 순서 보장 및 데이터 유실 방지를 위한 Queue 사용
     */
    BlockingQueue<SlackInnerResponse> blockingQueue = new LinkedBlockingQueue();

    @PostConstruct
    private void init() {
        methods = Slack.getInstance().methods(slackToken);
    }

    @Async
    public void addQueue(String message, SlackChannel slackChannel){
        SlackInnerResponse slackInnerResponse = new SlackInnerResponse(message, slackChannel);
        blockingQueue.add(slackInnerResponse);

        startSendMessage();
    }

    private void startSendMessage() {
        SlackInnerResponse poll = blockingQueue.poll();
        if(Objects.isNull(poll)){
            return;
        }
        sendMessage(poll.message, poll.slackChannel);
    }

    @AllArgsConstructor
    static class SlackInnerResponse{
        private String message;
        private SlackChannel slackChannel;
    }


    /**
     * Slack 메시지 전송 메서드
     * 비동기 처리
     */

    private synchronized void sendMessage(String message, SlackChannel slackChannel) {
        log.info("sendSlackMessage message = {}, channel ={}", message, slackChannel.desc());

        for (int i = 0; i < 3; i++) {
            if (getTpsSendCount() >= tps) {
                log.info("thread Sleep");
                /**
                 * SlackApiException: status: 429 발생으로 TPS 조절 필요
                 * https://api.slack.com/docs/rate-limits
                 */
                sleep(1000);
                clearTpsSendCount();
            }
            tpsSendCountPlus();
            try {
                ChatPostMessageResponse chatPostMessageResponse = send(message, slackChannel.desc());
                boolean result = chatPostMessageResponse.isOk();
                if (result) {
                    return;
                }
            } catch (SlackApiException | IOException e) {
                log.error("SlackException = ", e);
                sleep(30000);
            } catch (Exception e) {
                log.error("Exception =", e);
                sleep(30000);
            }
        }
    }

    private ChatPostMessageResponse send(String message, String channel) throws IOException, SlackApiException {
        
        ChatPostMessageRequest request = ChatPostMessageRequest.builder()
                .channel(channel)
                .text(message)
                .build();
        return methods.chatPostMessage(request);
    }

    private void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            log.error("sleep error", e);
        }
    }

    private synchronized void tpsSendCountPlus() {
        log.info("> Plus");
        ++tpsSendCount;
    }
    private synchronized int getTpsSendCount() {
        log.info("> GET");
        return tpsSendCount;
    }

    private synchronized void clearTpsSendCount() {
        log.info("> Clear");
        tpsSendCount = 0;
    }

}

Slack의 채널을 여러 분기처리하기위해 Enum을 사용하였고, token 및 tps는 config로 관리하였다.

 

해당 개발테스트 중 알게된 사항으로 Slack은 채널당 tps를 1로 엄격히 규정하고있다. 여러 테스트 결과 과도한 트래픽이 초과될시 429 에러가 발생하며 해당 내용은 코드에 정리하였다. 또한, 과도한 트래픽이 발생시 데이터의 유실을 확인하여 Queue를 활용하여 데이터를 안전하게 제공하였다.

 

시작하며

프로젝트 개발 테스트시 발생된 에러와 대처방법을 기록하였다.

인프라 영역을 누군가가 조작하고, 정보교환이 되지않아 발생하였다.

 

프로젝트에서 서버와 서버간의 통신을 할때 RestTemple을 사용하였다. 서버간의 통신구간에 try-catch문으로 예외처리를 한 부분에서 발생된 에러이다.

로그상 403 Forbidden이 발생하였고, 해당 403 에러의 경우 접근권한과 맞지 않는 접근 요청이 왔을 경우 발생하는 에러라고한다.

해당 부분은 대량의 트래픽이 서버로 전송되는 부분이였고, 관련되어 이전에 상용 서버에서 비슷한 경험이 있었다.

 

이전 경험을 토대로 수정하려했지만, AWS를 잘 몰라 이거 저거 만지고 찾아보며 삽질하며 해결한 방법이다.

 

AWS의 WAF & Shield 설정으로 이동

좌측 메뉴의 Web ACLs 선택 및 리전을 선택하면 ACL(Access Control List)가 나온다.

ACL의 Rules에서 문제가 발생하였던것으로 보인다.

 

1. 해결방법

운영설정과 비교해보니, 해당 설정에 특정IP의 대량 트래픽을 허용하는 룰이 삭제되어있어, 

IP sets에서 특정 IP에 대한 설정을 추가하고, 추가한 IP의 트래픽을 허용하는 룰을 만들었다.

 

해당 IP에 대한 트래픽을 허용하였지만, 아직 403 에러가 해결되지는 않았다.

 

이후 조치한 방법으로,

2. 해결방법

A라는 대량의 트래픽을 차단하는 룰 (Action 결과를 Block으로 차단)과,

B라는 특정 IP의 대량 트래픽을 허용하는 룰(Action 결과를 Allow으로 허용) 이 있을경우

 

순서가 적용이 되는것이였다.

 

이경우 대량의 특정 허용된 IP가 전송되었지만 A가 상단부분에 있었기 때문에 403 에러를 내렸던것이였다.

생각해보면 JAVA에서도

if(대량 트래픽 차단){
	return false;
}

if(특정아이피에 대해서는 허용){
	retrun true;
}

해당 설정이라 생각했을 경우 모두 첫번째 if문에서 걸렸을 것이다.

 

그래서 해당 설정 룰의 순서를 수정하니 해결되었다.👊🏻

시작하며

서버로그중 가끔씩 ClientAbortException가 보인다.

 

Response body : {"code":"400","msg":"Insufficient parameters.","data":"I/O error while reading input message; nested exception is org.apache.catalina.connector.ClientAbortException: java.io.EOFException"}

 

발생원인 : 클라이언트가 연결을 끊었을때 발생하는것이 일반적이다.

서버가 클리이언트로 응답을 보내려하지만, 클라이언트의 연결이 이미끊어진경우 발생한다.

 

해당 에러는 클라이언트 단에서 발생하였다. 해당 에러는 Filter에서 Handling

설정을 하였다.

 

참고사이트 https://perfectacle.github.io/2022/03/20/client-abort-exception-deep-dive-part-01/

시작하며

Index 정리 및 예시를 통해 학습한 내용을 정리하였다.

인덱스란?

 - 인덱스는 데이터베이스 테이블에 대한 검색 성능 속도를 향상시켜주는 자료 구조이다. 인덱스는 특정 컬럼(여러컬럼의 조합)에 대한 정렬된 값의 집합으로, 데이터베이스 엔진이 데이터를 빠르게 찾을수 있도록 도와준다.

Ex) 책의 목차라고 생각하면 이해가 쉽다. 책의 목차에서는 내가 찾고자 하는 페이지가 어디 있는지 빠르게 찾을 수 있도록 해준다.

 

인덱스는 데이터베이스 테이블의 특정 컬럼(여러 컬럼)의 값을 사전 순서 또는 정렬 순서로 저장하여 데이터 접근과 검색을 최적화한다.

일반적으로 B-트리(B-tree)나 해시 테이블 등의 자료구조를 사용하여 인덱스를 관리한다.

 

실생활에서의 인덱스 사용예제)

 

인덱스의 자료구조

해시 테이블(Hash Table)

- 해시 테이블은 Key, Value를 한쌍으로 데이터를 저장하는 자료구조이다. key값을 이용해 대응된는 value값을 구하는 방식이다. 평균적으로 0(1)의 매우 빠른 시간만에 원하는 데이터를 찾을 수 있지만, 실제 인덱스에서 잘 사용하지 않는다. 해시 테이블은 등호(=) 연산에 최적화 되어있기 떄문이다. 

- 데이터베이스에선 부등호(<. >) 연산이 자주 사용되는데, 해시 테이블 내의 데이터들은 정렬되어 있지 않으므로 특정 기준보다 크거나, 작은 값을 빠른시간내에 찾을 수가 없다.

B-Tree

- B-Tree는 자식 2개만을 갖는 이진트리(Binary Tree)를 확장하여 N개의 자식을 가질 수 있도록 고완된것이다. 

- B-Tree는 생성당시는 균형 트리이지만, 테이블 갱신의 반복을 통해서 균형이 깨지고, 성능이 약화된다.

 

B+ Tree

- B+ Tree는 B-Tree의 확장으로서, 오직 leaf node에만 데이터를 저장하고 leaf node가 아닌 node에서는 자식 포인터만 저장한다.

- Leaf node에만 데이터를 저장하므로, B-Tree에 비해 같은 node에 더 많은 키를 저장 할 수 있다.

- 데이터를 찾기 위해 leaf node 까지 탐색을 해야 하는데, Lined list로 연결되어 있기 때문에 full scan시 leaf node들만 순차 탐색하면
  되기때문에 B-Tree보다 탐색에 유리하다.

 

mysql의 경우 사용하는 엔진에 따라 다른 구조를 가진다.

Clustered Index vs Non Clustered Index

Clusterd Index 특징

  • 테이블 당 1개만 존재
  • PK 제약조건으로 컬럼을 생성시 자동 적용
  • 데이터가 정렬된 상태

Ex) 해당 DB Index 페이지에서 DDD라는 컬럼이 추가가 되는 상황이다.

       현재의 리프페이지에는 더이상 데이터가 추가될수 없기에 페이지 분할이 일어나며 DDD라는 컬럼이 추가가 되었다.

현재에서 KKK라는 컬럼이 추가시 루트페이지 및 리프페이지가 부족하다 리프페이지의 페이지 분할 및 루트추가 작업이 발생한다.

 

Non Clusterd Index 특징

  • Secondary Index(보조 인덱스) 라고도 한다.
  • 테이블에 여러개 존재 할 수 있다.
  • Unique 제약조건으로 컬럼을 생성시 자동 적용
  • 정렬되지 않아도 된다.
  • 리프페이지에서 데이터가 있는 곳의 주소를 가진다.
  • Clusterd Index에 비해 조회 속도가 느리지만, Insert, Update, Delete 시 부하가 적다.

※ PK를 Clusterd Index가 아닌 Non Clusterd Index로 적용도 가능하다.

CREATE TABLE TB_STUDENT (
  ID INT PRIMARY KEY NONCLUSTERED
  ...
)

어떠한 컬럼에 Index를 설정해야하는가?

  1. 핵심적인 기준 4가지
    • 카디널리티가 높은(↑) 컬럼
      - 카디널리티가 높다란, 한 컬럼이 갖고 있는 값의 중복도가 낮다는 뜻이다.
      Ex) 사람이라는 테이블을 기준으로 Gender라는 성별은 남, 여 만 존재한다. 이보다, 주민등록번호와 같은 고유값을 설정하는게 좋다.
    • 선택도가 낮은(↓) 컬럼
      - 선택도 = 카디널리티 / 전체레코드수, 선택도가 1이면 모든 데이터가 unique하다는 뜻이다.
    • 조회 활용도가 높은(↑) 컬럼 
      - Where의 대상 컬럼으로 많이 활용되어야 Index설정의 의미가 있다.
    • 수정빈도가 낮은(↓) 컬럼
  2. 그 밖의 Index 명시 사항
    • WHERE에 자주 사용되는 컬럼
    • LIKE와 사용할 경우에는 %가 뒤에 사용되도록 하기(%가 앞에 사용될 경우 Full Table Scan을 함.)
      Ex) LIKE 'index%'
    • ORDER BY에 자주 사용되는 컬럼에 사용하기
    • JOIN에 자주 사용되는 컬럼에 사용하기
    • 데이터의 변경이 없는 컬럼에 사용하기
    • WHERE 절 컬럼에 연산을 사용할 경우 Index를 사용하지 않는다.
      Ex) SELECT * FROM TB_TABLE WHERE COLUMN * 10 < 100              (X)
             SELECT * FROM TB_TABLE WHERE COLUMN < 100 * 10              (O)

그렇다면 Index는 몇개 설정해야 좋을까?

Index 설정 시 데이터베이스에 할당된 메모리를 사용하여 테이블 형태로 저장하게된다. 그렇기 때문에 무분별한 Index 설정은 메모리를 많이 사용하게되며, Index로 지정된 컬럼의 값이 바뀌게 되면 Index 테이블이 갱신되어야 하므로 느려 질 수 있다.

그렇기에 Index는 한 테이블당 3~5개가 적당하다.

Index 설정 및 삭제

-- Clusterd Index 설정
ALTER TABLE '테이블명' ADD CONSTRAINT '인덱스명' PRIMARY KEY ('컬럼명');

-- Non Clusterd Index 설정
ALTER TABLE '테이블명' ADD CONSTRAINT '인덱스명' UNIQUE ('컬럼명');

-- Clusterd Index 설정(단 PK가 아닌 값을 지정하기 때문에 Unique여야만 한다.) 
CREATE FULLTEXT INDEX '인덱스명' ON '테이블명'('컬럼명');

-- Non Clusterd Index 설정(중복 허용) 
CREATE INDEX '인덱스명' ON '테이블명' ('컬럼명');

-- Non Clusterd Index 설정(중복 비허용)
CREATE UNIQUE INDEX '인덱스명' ON '테이블명' ('컬럼명');

-- Non Clusterd Index 설정(다중 컬럼 인덱스 생성)
CREATE UNIQUE INDEX '인덱스명' ON '테이블명' ('컬럼명', '컬렴명');

-- Index 삭제
DROP INDEX '인덱스명' ON '테이블명'
ALTER TABLE '테이블명' DROP INDEX '인덱스명'

Index 성능 비교

예제)

CREATE TABLE TB_STUDENT(
ID INT AUTO_INCREMENT PRIMARY KEY, -- ID : PK(Clusterd Index)
NAME VARCHAR(20),
AGE INT,
CTN VARCHAR(20) UNIQUE -- CTN : Unique(Non Clusterd Index)
);

테이블에 등록된 Index 확인

show index from '테이블명';

테이블의 index 크기 확인

show table status like '테이블명';

SELECT

-- Clusterd Index
SHOW GLOBAL STATUS LIKE 'Innodb_pages_read';
select * from TB_STUDENT ts WHERE ID = 52345;
SHOW GLOBAL STATUS LIKE 'Innodb_pages_read';

Clusterd Index의 경우 하나의 데이터를 찾기까지 20건 이하의 페이지를 찾았다.

-- Non Clusterd Index
SHOW GLOBAL STATUS LIKE 'Innodb_pages_read';
select * from TB_STUDENT ts WHERE CTN = 9965-8467-0135;
SHOW GLOBAL STATUS LIKE 'Innodb_pages_read';

반면, Non Clusterd Index의 경우 상당히 많은 페이지를 찾았다.

해당 Test를 통해 Clusterd Index의 조회성능을 알 수 있다.

 

단일 컬럼 인덱스와 다중 컬럼 인덱스 차이

-- 단일 컬럼 인덱스 테이블
CREATE TABLE TB_STUDENT10(
ID INT NOT NULL AUTO_INCREMENT,
NAME VARCHAR(20) NOT NULL,
AGE INT NOT NULL,
CTN VARCHAR(20) NOT NULL,
PRIMARY KEY(ID),
INDEX idx_name(NAME),
INDEX idx_CTN(CTN)
);

-- 다중 컬럼 인덱스 테이블
CREATE TABLE TB_STUDENT20(
ID INT NOT NULL AUTO_INCREMENT,
NAME VARCHAR(20) NOT NULL,
AGE INT NOT NULL,
CTN VARCHAR(20) NOT NULL,
PRIMARY KEY(ID),
INDEX idx_NAME_CTN(NAME, CTN)
);

각각의 테이블에 프로시저를 이용하여 100만건의 데이터를 삽입하였다.

이후 SELECT 실행에 따른 EXPLAIN 결과이다.

EXPLAIN SELECT * FROM TB_STUDENT10 ts 
WHERE NAME = 'NAME934540' AND CTN = 'CTN934540';

EXPLAIN SELECT * FROM TB_STUDENT20 ts 
WHERE NAME = 'NAME934540' AND CTN = 'CTN934540';

다중 컬럼 인덱스가 단일 컬럼인덱스에 비해 검색 소요시간이 더욱 짧다.

6.1 컬렉터란 무엇인가?

Collector 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할지 지정한다.

6.1.1 고급 리듀싱 기능을 수행하는 컬렉터

collect로 결과를 수집하는 과정을 간단하면서도 유연한 방식으로 정의 할 수 있다는 점이 컬렉터의 최대 강점이다. 구체적으로 설명해서 스트림에 collect를 호출하면 스트림의 요소에 리듀싱 연산이 수행된다.

Collectors 유틸리티 클래스는 자주사용하는 컬렉터 인스턴스를 손쉽게 생성 할 수 있는 정적 팩토리 메서드를 제공한다. 

Ex) 가장 많이 사용하는 직관적인 정적 메서드로 collect(Collectors.toList);

6.1.2 미리 정의된 컬렉터

미리 정의된 컬렉터는 groupingBy 같이 Collectors 클래스에서 제공하는 팩토리 기능을 설명한다. Collectors에서 제공하는 메서드의 기능은 크게 세가지로 구분 할 수 있다.

  • 스트림 요소를 하나의 값으로 리듀스하고 요약
  • 요소 그룹화
  • 요소 분할

6.2 리듀싱과 요약

        // Counting 메서드를 이용하여 메뉴에서 요리수를 계산
        long counting1 = menu.stream().collect(Collectors.counting());
        // 불팔요과정을 생략사용가능
        long counting2 = menu.stream().count();

6.2.1 스트림값에서 최댓값과 최솟값 검색

       /**
         * 6.2.1 스트림값에서 최댓값과 최솟값 검색
         */
        Comparator<Dish> dishComparator = Comparator.comparingInt(Dish::getCalories);

        Optional<Dish> max = menu.stream().collect(Collectors.maxBy(dishComparator));
        Optional<Dish> min = menu.stream().collect(Collectors.minBy(dishComparator));

6.2.2 요약 연산

Collectors 클래스는 Collectors.summingInt 라는 특별한 요약 팩토리 메서드를 제공한다.

        /**
         * 6.2.2 요약 연산
         */
	// 총 칼로리 계산
        int totalCalories = menu.stream().collect(Collectors.summingInt(Dish::getCalories));

메뉴의 칼로리 총 칼로리를 계산하는 코드이다. summingInt 외에 summingDouble, summingLong 메서드도 있다.

이러한 단순 합계 외에  평균값 계산 등의 연산도 요약 기능으로 제공된다.

        // 칼로리의 평균값 계산
        Double avgCalories = menu.stream().collect(Collectors.averagingInt(Dish::getCalories));

averaginInt, averagingLong, averaginDouble 등 다양한 형식으로 이루어진 숫자 집합의 평균을 계산 할 수 있다.

하나의 요약 연산으로 메뉴에 있는 수, 칼로리합계, 평균, 최댓값, 최솟값등을 계산하는 코드이다.

	menu.add(new Dish("생선", false, 1200, Type.FISH));
        menu.add(new Dish("돼지고기", false, 2300, Type.MEAT));
        menu.add(new Dish("김치", true, 200, Type.OTHER));
        menu.add(new Dish("양파", true, 300, Type.OTHER));
        menu.add(new Dish("소고기", false, 1000, Type.MEAT));

        IntSummaryStatistics collect = menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));
        System.out.println(collect);

해당결과이다. 마찬가지로 int 뿐 아니라 long이나 double에 대응하는 summarizingLong, summarizingDouble메서드도 있다.

6.2.3 문자열 연결

Collector에 joining 팩토리 메서드를 이용하면 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다.

        /**
         * 6.2.3 문자열 연결
         */
        String joiningNames = menu.stream().map(Dish::getName).collect(Collectors.joining());
        System.out.println(joiningNames);

joining 메서드는 내부적으로 StringBuilder를 이용해서 문자열을 하나로 만든다.

        // 구분자 사용
        String joiningNamesSeparator = menu.stream().map(Dish::getName).collect(Collectors.joining(", "));
        System.out.println(joiningNamesSeparator);

6.2.4 범용 리듀싱 요약 연산

        /**
         * 6.2.4 범용 리듀싱 요약 연산
         */
        Integer reducing1 = menu.stream()
                .collect(Collectors.reducing(0, Dish::getCalories, (a, b) -> a + b));

reducing은 인수 세개를 받는다.

  • 첫 번째 인수는 리듀싱 연산의 시작 초기값이거나, 스트림에 인수가 없을때는 반환값이다.
  • 두 번째 인수는 요리를 칼로리 정수로 변환할 때 사용한 변환 함수이다.
  • 세 번째 인수는 같은 종류의 두항목을 하나의 값으로 더하는 BinaryOperator이다.
        // 가장 칼로리가 높은 요리를 찾는 방법
        Optional<Dish> reducing2 = menu.stream()
                .collect(Collectors.reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));

 

한개의 인수를 갖는 reduce

        int totalCalories2 = menu.stream()
        		.map(Dish::getCalories).reduce(Integer::sum).get();

한개의 인수를 갖는 reduce를 스트림에 적용한 다른 예제와 마찬가지로 빈스트림과 관련한 Null 문제를 피하도록 Optional<Integer>를 반환한다.

 

스트림 인터페이스를 잘 사용하면 코드의 재사용성과 커스터 마이즈 가능성이 높아지고, 추상화와 일반화를 얻을 수 있다.

6.3 그룹화

자바 8의 함수형을 이용하면 가독성 있는 한줄의 코드로 그룹화를 구현 할 수 있다.

        /**
         * 6.3 그룹화
         */
        Map<Type, List<Dish>> dishesByType = menu.stream().collect(Collectors.groupingBy(Dish::getType));
        System.out.println(dishesByType);
        
        menu.stream().collect(Collectors.groupingBy(dish ->{
            if(dish.getCalories() <= 400) return CaloricLevel.DIET;
            else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
            else return CaloricLevel.FAT
        }));

각 Key는 Dish의 Type으로 Value는 해당 값이 들어간다.

6.3.1 그룹화된 요소 조작

요소를 그룹화 한 다음에는 각 결과 그룹의 요소를 조작하는 연산

        /**
         * 6.3.1 그룹화된 요소 조작
         */
        // 500 칼로리 이상 필터 후 그룹핑
        Map<Integer, List<Dish>> caloricDishesByType = menu.stream().filter(dish -> dish.getCalories() >= 500)
                .collect(Collectors.groupingBy(Dish::getCalories));

위 코드의 단점으로 메뉴 요리는 맵 형태로 되어있는데, 해당 필터 타입에 맞지않는다면 키가 사라진다.

다음과 같이 수정 할 수 있다.

        Map<Integer, List<Dish>> caloricDishesByType2 = menu.stream().collect(Collectors.groupingBy(Dish::getCalories,
                Collectors.filtering(dish -> dish.getCalories() > 500, Collectors.toList())));
                
        System.out.println("caloricDishesByType1 = " + caloricDishesByType1);
        System.out.println("caloricDishesByType2 = " + caloricDishesByType2);

6.3.2 다수준 그룹화

        /**
         * 6.3.2 다수준 그룹화
         */
        Map<Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = menu.stream().collect(
                groupingBy(Dish::getType,       // 첫번째 수준의 분류함수
                        groupingBy(dish -> {    // 두번째 수준의 분류함수
                            if (dish.getCalories() <= 400)
                                return CaloricLevel.DIET;
                            else if (dish.getCalories() <= 700) {
                                return CaloricLevel.NORMAL;
                            } else return CaloricLevel.FAT;
                        })));
        System.out.println("dishesByTypeCaloricLevel = " + dishesByTypeCaloricLevel);

dishesByTypeCaloricLevel = {FISH={DIET=[Dish(name=prawns, vegetarian=false, calories=300, type=FISH)]}, OTHER={NORMAL=[Dish(name=french fries, vegetarian=true, calories=480, type=OTHER)], DIET=[Dish(name=seasonal fruit, vegetarian=true, calories=120, type=OTHER), Dish(name=rice, vegetarian=true, calories=350, type=OTHER)]}, MEAT={DIET=[Dish(name=chicken, vegetarian=false, calories=400, type=MEAT)]}}

 

외부 맵은 첫번째 수준의 분류함수에서 분류한 키값 FISH, MEAT, OTHER로 나누고, 두번째 수준의 분류함수의 NORMAL, DIET, FAT을 키값으로 갖는다.

6.3.3 서브그룹으로 데이터 수집

groupingBy 컬렉터를 이용한 count

        /**
         * 6.3.3 서브그룹으로 데이터 수집
         */
        Map<Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType, counting()));
        System.out.println("typesCount = " + typesCount);

typesCount = {FISH=1, OTHER=3, MEAT=1}

요리의 종류 별 칼로리가 가장 높은 요리 출력

        Map<Type, Optional<Dish>> mostCaloricByType = menu.stream().collect(
                groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));
        System.out.println("mostCaloricByType = " + mostCaloricByType);

컬렉터 결과를 다른 형식에 적용하기

마지막 그룹화 연산에서 맵의 모든 값을 Optional로 감쌀 필요가 없으므로 Optional을  삭제 할 수 있다.

        // Optional 삭제
        Map<Type, Dish> mostCaloricByTypeNotOptional = menu.stream().collect(groupingBy(Dish::getType,
                collectingAndThen(maxBy(comparingInt(Dish::getCalories)),
                        Optional::get)));
        System.out.println("mostCaloricByTypeNotOptional = " + mostCaloricByTypeNotOptional);

팩토리 메서드 collectionAndThen은 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환한다.

6.4 분할

5.1 필터링

5.1.1 프레디케이트로 필터링

스트림 인터페이스는 filter  메서드를 지원한다. filter 메서드는 프레디케이트를 인수로 받아서 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.

	/**
         * 5.1.1 프레디케이트로 필터링
         */
        List<Dish> menu = new ArrayList<>();
        List<Dish> vegetarianMenu = menu.stream()
                    .filter(Dish::isVegetarian) // 채식 요리인지 확인하는 메서드참조
                    .collect(Collectors.toList());

5.2 스트림 슬라이싱

5.2.1 프레디케이트를 이용한 슬라이싱

TAKEWHILE 활용

        List<Dish> specialMenu = Arrays.asList(
                new Dish("seasonal fruit", true, 120, Type.OTHER),
                new Dish("prawns", false, 300, Type.FISH),
                new Dish("rice", true, 350, Type.OTHER),
                new Dish("chicken", false, 400, Type.MEAT),
                new Dish("french fries", true, 530, Type.OTHER)
        );

        List<Dish> filteredMenu = specialMenu.stream()
                .filter(dish -> dish.getCalories() < 320)
                .collect(Collectors.toList());

위 리스트는 이미 칼로리 순으로 정렬되어 있다. 리스트가 이미 정렬되어 있다는 사실을 이용해서 320칼로리보다 크거나 같은 요리가 나왔을때 반복 작업을 중단 할 수 있다. 작은 리스트에서는 별거 아닌 것처럼 보일 수 있지만, 아주 많은 요소를 포함하는 큰 스트림에서는 상당한 차이가 될 수 있다.

 takeWhile을 이용하면 무한스트림을 포함한 모든 스트림에 프레디케이트를 적용해 스트림을 슬라이스 할 수 있다.

        // takeWhile 적용
	List<Dish> slicedMenu1 = specialMenu.stream()
                .takeWhile(dish -> dish.getCalories() < 320)
                .collect(Collectors.toList());

DROPWHILE 활용

320 칼로리보다 큰 요소를 찾는방법으로는  dropWhile을 활용

	// dropWhile 적용
        List<Dish> slicedMenu2 = specialMenu.stream()
                .dropWhile(dish -> dish.getCalories() < 320)
                .collect(Collectors.toList());

dropWhile은 takeWhile과 정반대의 작업을 수행한다. dropWhile은 프레디케이트가 처음으로 거짓이 되는 지점까지 발견된 요소를 버린다.프레디케이트가 거짓이 되면 그 지점에서 작업을 중단하고 남은 모든 요소를 반환한다. dropWhile은 무한한 남은 요소를 가진 무한 스트림에서도 동작한다.

5.2.2 스트림 축소

스틀미은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원한다. 스트림이 정렬되어 있으면 최대 요소 n개를 반환 할 수 있다. 

5.2.3 요소 건너뛰기

스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다. n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈스트림이 반환된다.

        /**
         * 5.2.3 요소 건너뛰기
         */
        List<Dish> menu = new ArrayList<>();
        List<Dish> dishes = menu.stream()
                .filter(d -> d.getCalories() > 300)
                .skip(2)
                .collect(Collectors.toList());

5.3 매핑

5.3.1 스트림의 가그 요소에 함수 적용하기

스트림은 함수를 인수로 받는 map 메서드를 지원한다. 인수로 제공된 함수는 각요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다.

        /**
         * 5.3.1 스트림의 각 요소에 함수 적용하기
         */
        List<String> dishNames = menu.stream()
                .map(Dish::getName)
                .collect(Collectors.toList());

getName은 문자열을 반환하므로 map 메서드의 출력 스트림은 Stream<String> 형식을 갖는다.

요리명의 문자길이를 알고 싶다면 map메서드를 연결 할 수 있다.

	// 요리명의 길이 추가
        List<Integer> dishNameLengths = menu.stream()
                .map(Dish::getName)
                .map(String::length)
                .collect(Collectors.toList());

5.3.2 스트림 평면화

리스트에서 고유문자로 이루어진 리스트를 반환한다. 중복을 제거하기위해 Distinct를 사용하여 중복을 제거할 수 있을거라 생각하지만, 

결과는 다음과같다.

        /**
         * 5.3.2 스트림 평면화
         */
        List<String> list = Arrays.asList("Hello", "World");

        List<String[]> result = list.stream()
                .map(word -> word.split(""))
                .distinct().collect(Collectors.toList());

        result.stream().forEach(x -> System.out.println(Arrays.toString(x)));

결과  : [H, e, l, l, o], [W, o, r, l, d]

map과 Arrays.stream 활용

우선 배열 스트림 대신 문자열 스트림이 필요하다. 

        /**
         * map과 Arrays.stream 활용
         */
        List<String> words = null;
        String[] arraysOfWord = {"Goodbye", "World"};
        Stream<String> streamOfWords = Arrays.stream(arraysOfWord);
        words.stream()
                .map(word -> word.split("")) // 각 단어를 개별 문자열로 반환 
                .map(Arrays::stream) // 각 배열을 별도의 스트림으로 생성
                .distinct()
                .collect(Collectors.toList());

결국 스트림 리스트가 만들어지면서  문제가 해결되지는 않는다. 문제를 해결하려면 먼저 각 단어를 개별 문자열로 이루어진 배열로 만든 다음에 각 배열을 별도의 스트림으로 만들어야한다.

flatMap 사용

	/**
         * flatMap 사용
         */
        List<String> uniqueCharacters = words.stream()
                .map(word -> word.split("")) // 각 단어를 개별 문자를 포함하는 배열로 변환
                .flatMap(Arrays::stream) // 생성된 스트림을 하나의 스트림으로 평면화
                .distinct()
                .collect(Collectors.toList());

map(Arrays::stream)과 달리 flatMap(Arrays::stream)은 하나의 평면화된 스트림을 반환한다.

요약하면 flatMap 메서드는 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행한다.

5.4 검색과 매칭

5.4.1 프레디케이트가 적어도 한 요소와 일치하는지 확인

	/**
         * 5.4.1 프레디케이트가 적어도 한 요소와 일치하는지 확인
         */
        if (menu.stream().anyMatch(Dish::isVegetarian)) {
            System.out.println("vegetarian food");
        }

anyMatch는 Boolean을 반환하므로 최종 연산이다.

5.4.2 프레디케이트가 모든욧고와 일치하는지 검사

allMatch 메서드는 anyMatch와 달리 스트림의 모든 요소가 주어진 프레디케이트와 일치하는지 검사한다.

        /**
         * 5.4.2 프레디케이트가 모든욧고와 일치하는지 검사
         */
        boolean isHealthy1 = menu.stream()
                                .allMatch(dish -> dish.getCalories() < 1000);

noneMatch

noneMatch는 allMatch와 반대연산을 수행한다. 즉, 주어진 프레디케이트와 일치하는 요소가 없는지 확인한다.

        /**
         * noneMatch
         */
        boolean isHealthy2 = menu.stream()
                                .noneMatch(dish -> dish.getCalories() >= 1000);

5.4.3 요소 검색

findAny 메서드는 현재 스트림에서 임의의 요소를 반환한다. findAny 메서드를 다른 스트림연산과 연결해서 사용 할 수 있다.

        /**
         * 5.4.3 요소 검색
         */
        Optional<Dish> dish = menu.stream()
                                .filter(Dish::isVegetarian)
                                .findAny();

스트림 파이프라인은 내부적으로 단일 과정으로 실행 할 수 있도록 최적화된다. 결과를 찾는 즉시 실행을 종료한다.

5.4.4 첫번째 요소 찾기

        /**
         * 5.4.4 첫번째 요소 찾기
         */
        List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
        Optional<Integer> first = someNumbers.stream()
                                            .map(x -> x * x)
                                            .filter(x -> x % 3 == 0)
                                            .findFirst();

※ 병렬 실행에서는 첫번째 요소를 찾기 어렵다. 따라서 요소의 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다. 

5.5 리듀싱

리듀싱연산은 모든 스트림 요소를 처리해서 값으로 도출한다. 

5.5.1 요소의 합

reduce는 두개의 인수를 갖는다.

        /**
         * 5.5.1 요소의 합
         */
        List<Integer> numbers = Arrays.asList(4, 5, 3, 9);

        Integer reduce = numbers.stream().reduce(0, (a, b) -> a + b);
        System.out.println(reduce);

초깃값 0, 두요소를 조합해서 새로운 값을 만드는 BinaryOperator를 사용

reduce의 내부 동작 : 초깃값 0 + 4 -> 4 + 5 -> 9 + 3 ->  12 + 9 -> 21  

메서드 참조를 이용한 코드의 간결화 적용

        /**
         * 메서드 참조 사용
         */
        Integer reduce1 = numbers.stream().reduce(0, Integer::sum);

초깃값 없음

스트림에 아무 요소도 없는 상황이라면  초깃값이 없으므로 reduce는 합계를 반환 할 수 없다. 따라서 합계가 없음을 가리킬 수 있도록

Optional로 반환한다.

	/**
         * 초깃값 없음
         */
        Optional<Integer> reduce2 = numbers.stream().reduce((a, b) -> (a + b));

5.5.2 최댓값과 최솟값

        /**
         * 5.5.2 최댓값과 최솟값
         */
        Optional<Integer> reduceMax = numbers.stream().reduce(Integer::max);
        Optional<Integer> reduceMin = numbers.stream().reduce(Integer::min);

5.7 숫자형 스트림

메뉴의 칼로리 합계 계산

	Integer reduce = menu.stream()
                            .map(Dish::getCalories)
                            .reduce(0, Integer::sum);

위코드에는 박싱 비용이 숨어있다. 내부적으로 합계를 계산하기 전에 Integer를 기본형으로 언박싱해야한다.

5.7.1 기본형 특화 스트림

Java 8에서는 세가지 기본형 특화 스트림을 제공한다. 스트림 API는 박싱 비용을 피할 수 있도록 IntStream, DoubleStream, LongStream을 제공한다. 특화 스트림은 오직 박싱 과정에서 일어나는 효율성과 관련이 있으며, 스트림에 추가 기능을 제공하지는 않는다.

숫자 스트림으로 매핑

스트림을 특화 스트림으로 변환 할 때는 mapToInt, mapToDouble, mapToLong 세가지 메서드를 가장 많이 사용한다.

map과 정확히 같은 기능을 수행하지만, Stream<T> 대신 특화된 스트림을 반환한다.

	// 숫자 스트림으로 매핑
        int sum = menu.stream()
                .mapToInt(Dish::getCalories)
                .sum();

mapToInt 메서드는 각 요리에서 모든 칼로리를 추출한 다음에 IntStream을 반환한다. IntStream 인터페이스에서 제공하는 sum 메서드를 이용해서 칼로리 합계를 계산하였다.

객체 스트림으로 복원하기

 

        // 객체 스트림으로 복원하기
        IntStream intStream = menu.stream()
                .mapToInt(Dish::getCalories); // Stream -> IntStream 변환
        Stream<Integer> boxed = intStream.boxed(); // IntStream -> Stream 변환

기본값 : OptionalInt

Stream에서 요소가 없는 상황과, 실제 최댓값이 0인 상황을 어떻게 구별할까? Optional을 Integer, String 등의 참조형식으로 파라미터화 할 수 있다. 또한 OptionalInt, OptionalDouble, OptionalLong 세가지 기본형 특화 스트림도 제공한다.

        // 기본값 : OptionalInt
        OptionalInt max = menu.stream().mapToInt(Dish::getCalories).max();

5.7.2 숫자 범위

Java 8의 IntStream과 LongStream에서는 range와 rangeClosed 두 정적 메서드를 제공한다.

range 메서드는 시작값과 종료값이 결과에 포함되지 않는 반면, rangeClosed는 시작값과 종료값이 결과에 포함된다.

        IntStream intStream1 = IntStream.rangeClosed(1, 100) // 1부터 100까지
                .filter(x -> x % 2 == 0); // 짝수

5.8 스트림 만들기

5.8.1 값으로 스트림 만들기

임의의 수를 인수로 받은 정적 메서드 Stream.of를 이용해서 스트림을 만들 수 있다.

        /**
         * 5.8.1 값으로 스트림 만들기
         */
        Stream<String> modern = Stream.of("Modern", "Java", "In", "Action");
        modern.map(String::toUpperCase).forEach(System.out::println);
        Stream<Object> empty = Stream.empty(); // empty메서드를 이용 Stream을 비울수 있다.

5.8.2 null이 될 수 있는 객체로 스트림 만들기

Java 9에서는 null이 될 수 있는 개체를 스트림으로 만들수 잇는 메서드가 추가되었다. 예를들어 System.getProperty는 제공된 키에 대응하는 속성이 없으면 null을 반환한다.

        /**
         * 5.8.2 null이 될 수 있는 객체로 스트림 만들기
         */
        String homeValue = System.getProperty("home");
        Stream<String> homeValueStream1 = homeValue == null ? Stream.empty() : Stream.of("value");

        // ofNullable 사용
        Stream<String> homeValueStream2 = Stream.ofNullable(System.getProperty("home"));

ofNullable의 내부구조는 다음과 같다.

5.8.3 배열로 스트림 만들기

배열을 인수로 받는 정적 메서드 Arrays.stream을 이용해서 스트림을 만들수 있다.

        /**
         * 5.8.3 배열로 스트림 만들기
         */
        int[] numbers = {2, 3, 4, 5, 6, 7, 8};
        int sum = Arrays.stream(numbers).sum();

5.8.4 파일로 스트림 만들기

        /**
         * 5.8.4 파일로 스트림 만들기
         */
        long uniqueWords = 0;
        try (Stream<String> lines =
                     Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){
            uniqueWords = lines.flatMap(line -> Arrays.stream(line.split("")))
                            .distinct()
                            .count();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

Stream은 AutoiCloseable 인터페이스를 구현한다. 따라서 try 블록 내의 자원은 자동으로 관리된다.

5.8.5 함수로 무한 스트림 만들기

iterate 메서드

        // iterate 메서드
        Stream.iterate(0, n -> n+2)    // 초기값 0에서 +2 증가
                .limit(10)          // 10개
                .forEach(System.out::println);

limit을 주석처리시 무한으로 값이 증가한다.

        // 초깃값 0에서, 0보다 작을때가지, +4 증가
        IntStream.iterate(0, n -> n < 100, n -> n + 4)
                .forEach(System.out::println);
        // filter 메서드가 적용되지않음, 무한 증가
        IntStream.iterate(0, n -> n + 4)
                .filter(n -> n < 100)
                .forEach(System.out::println);
        // 해결방법 : takeWhiles
        IntStream.iterate(0, n -> n + 4)
                .takeWhile(n -> n < 100)
                .forEach(System.out::println);

generate 메서드

iterate와 비슷하게 generate도 무한 스틀미을 만들 수 있다.하지만 iterate와 달리 generate는 생산된 값을 연속적으로 계산하지 않고, 새로운 값을 생산한다.

        //generate 메서드
        Stream.generate(Math::random)
                .limit(5)
                .forEach(System.out::println);

마찬가지로 limit  주석시 무한 스트림이 된다.

 

4.1 스트림이란 무엇인가?

스트림은 자바 8 API에 새로 추가된 기능이다. 스트림을 이용하면 선언형으로 컬렉션 데이터를 처리 할 수 있다.

스트림을 이용하면 멀티스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬로 처리 할 수 있다. 

Ex) 기존 자바 7 코드

        /**
         * 기존 자바 7코드
         */
	List<Dish> lowCaloricDishes = new ArrayList<>();
        List<Dish> menu = new ArrayList<>();

        for (Dish dish : menu) {
            if (dish.getCalories() < 400) {
                lowCaloricDishes.add(dish);
            }
        }

        Collections.sort(lowCaloricDishes, new Comparator<Dish>() {   // 익명 클래스 사용
            @Override
            public int compare(Dish dish1, Dish dish2) {
                return Integer.compare(dish1.getCalories(), dish2.getCalories());
            }
        });

        List<String> lowCaloricDishesName = new ArrayList<>();
        for (Dish dish : lowCaloricDishes) {
            lowCaloricDishesName.add(dish.getName());
        }

Ex) 기존 자바 8 코드

 	/**
         * 자바 8 코드
         */
        List<String> lowCaloricDishesName2 =
                        menu.stream()
                        .filter(d -> d.getCalories() < 400)
                        .sorted(Comparator.comparing(Dish::getCalories))
                        .map(Dish::getName)
                        .collect(Collectors.toList());

stream()을 parallelStream()으로 바꾸면 이 코드를 멀티코어 아키텍처에서 병렬로 실행 할 수 있다.

        /**
         * parallelStream 사용
         */
        List<String> lowCaloricDishesName3 =
                        menu.parallelStream()
                        .filter(d -> d.getCalories() < 400)
                        .sorted(Comparator.comparing(Dish::getCalories))
                        .map(Dish::getName)
                        .collect(Collectors.toList());
  • 선언형으로 코드를 구현 할 수 있다. 즉, 루프와 if 조건문 등의 제어 블록을 사용해서 어떻게 동작을 구현 할지 지정할 필요없이 
    '저칼로리의 요리만 선택하라' 같은 동작의 수행을 지정 할 수 있다. 선언형 코드와 동작 파라미터화를 활용하면 변하는 요구사항에 쉽게 대응 할 수 있다. 즉, 기존 코드를 복사하여 붙여 넣는 방식을 사용하지 않고 람다 표현식을 이용해서 저칼로리 대신 고칼로리의 요리만 필터링하는 코드도 쉽게 구현 할 수있다.
  • 맨처음 Java를 배울때 스트림은 파이프 라인이라고 배웠다. 위코드에서는 여러연산을 파이프라인으로 연결하여 filter 메서드의 결과는 sorted 메서드로, 다시 sorted 결과는 map 메서드로, map 메서드의 결과는 collect로 연결된다.

자바 8의 스트림 API의 특징을 다음처럼 요약 할 수 있다.

  • 선언형 : 더 간결하고 가독성이 좋아진다.
  • 조립할 수 있음 : 유연성이 좋아진다.
  • 병렬화 : 성능이 좋아진다.

4.2 스트림 시작하기

스트림이란 정확히 뭘까? 스트림이란 데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소로 정의 할 수 있다.

  • 연속된 요소 : 컬렉션과 마찬가지로 스트림은 특정 요소 형식으로 이루어진 연속된 값 집합의 인터페이스를 제공한다. 컬렉션은 자료구조이므로 컬력션에서는시간과 공간의 복잡성과 관련된 요소 저장 및 접근 연산이 주를 이룬다. 반면 스트림은 filter, sorted, map 처럼 표현 계산식이 주를 이룬다. 즉, 컬렉션의 주제는 데이터고 스트림의 주제는 계산이다.
  • 소스 : 스트림은 컬렉션, 배열, I/o 자원 등의 데이터 제공 소스로부터 데이터를 소비한다. 정렬된 컬렉션으로 스트림을 생성하면 정렬이 그대로 유지된다. 즉, 리스트로 스트림을 만들면 스트림의 요소는 리스트의 요소와 같은 순서를 유지한다.
  • 데이터 처리 연산 : 스트림은 함수형 프로그래밍 언어에서 일반적으로 지원하는 연산과 데이터베이스와 비슷한 연산을 지원한다. 

또한, 스트림의 두가지 중요 특성으로

  • 파이프라이닝 : 대부분의 스트림연산은 스트림 연산끼리 연결해서 커다란 파이프라인을 구성 할 수 있도록 스트림 자신을 반환한다.
  • 내부 반복 : 반복자를 이용해서 명시적으로 반복하는 컬렉션과 달리 스트림은 내부 반복을 지원한다.
	 List<Dish> menu = new ArrayList<>();

        /**
         * 파이프라인 연산 만들기
         */
        List<String> threeHighCaloricDishNames = menu.stream()
                .filter(dish -> dish.getCalories() > 300) // 첫번쨰로 고칼로리 요리를 필터링
                .map(Dish::getName)                       // 요리명을 추출
                .limit(3)                         	  // 3개로 제한
                .collect(Collectors.toList());            //결과를 리스트로 저장

4.3 스트림과 컬렉션

데이터를 언제 계산하느냐가 컬렉션과 스트림의 가장 큰 차이이다.

컬렉션은 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 자료구조이다. 즉, 컬렉션의 모든 요소는 컬렉션에 추가하기 전에 계산되어야 한다.(컬렉션에 요소를 추가하거나 컬렉션의 요소를 삭제 할 수있다. 이런 연산을 수행 할 때마다 컬렉션의 모든 요소를 메모리에 저장해야 하며 컬렉션에 추가하려는 요소는 미리 계산되어야 한다.)

 

반면 스트림은 이론적으로 요청할 때만 요소를 계산하는 고정된 자료 구조이다.(스트림에 요소를 추가하거나 제거할 수 없다.)

4.3.1 딱 한 번만 탐색 할 수 있다.

반복자와 마찬가지로 스트림도 한번만 탐색 할 수 있다. 한번 탐색한 요소를 다시 탐색하려면 초기 데이터 소스에서 새로운 스트림을 만들어야 한다.

        /**
         * 4.3.1 딱 한 번만 탐색 할 수 있다.
         */
        List<String> title = Arrays.asList("Java", "In", "Action");

        Stream<String> s = title.stream();
        s.forEach(System.out::println);
        // IllegalStateException: stream has already been operated upon or closed
        s.forEach(System.out::println);
    }

4.3.2 외부 반복과 내부 반복

컬렉션 인터페이스를 사용하려면 사용자가 직접 요소를 반복해야 한다.(Ex for-each) 이를 외부 반복이라고 한다. 반면, 스트림 라이브러리는(반복을 알아서 처리하고 결과 스트림 값을 어딘가에 저장해주는) 내부 반복을 사용한다. 함수에 어떤 작업을 수행 할지만 지정하면 모든것이 알아서 처리된다.

        /**
         * 4.3.2 외부 반복과 내부 반복
         */
        // 컬렉션 for-each루프를 이용하는 외부 반복
        List<Dish> menu1 = new ArrayList<>();
        List<String> names1 = new ArrayList<>();
        for (Dish dish : menu1) {
            names1.add(dish.getName());
        }
        // 컬렉션 내부적으로 숨겨졌던 반복자를 사용한 외부 반복
        List<String> menu2 = new ArrayList<>();
        List<String> names2 = new ArrayList<>();
        Iterator<String> iterator = menu2.iterator();

        while (iterator.hasNext()) {
            String dish = iterator.next();
            names2.add(dish);
        }
        // 스트림 내부반복
        List<Dish> menu3 = new ArrayList<>();
        List<String> names3 = menu3.stream()
                    .map(Dish::getName).collect(Collectors.toList());

내부반복 사용시 작업을 투명하게 병렬로 처리하거나 더 최적화된 다양한 순서로 처리할 수있는 장점이 있다.

또한, 스트림 라이브러리의 내부 반복은 데이터 표현과 하드웨어를 활용한 병렬성 구현을 자동으로 선택한다. 반면 for-each를 이용하는 외부 반복에서는 병렬성을 스스로 관리해야 한다.

4.4 스트림 연산

	 List<Dish> menu = new ArrayList<>();

        /**
         * 파이프라인 연산 만들기
         */
        List<String> threeHighCaloricDishNames = menu.stream()
                .filter(dish -> dish.getCalories() > 300) // 첫번쨰로 고칼로리 요리를 필터링
                .map(Dish::getName)                       // 요리명을 추출
                .limit(3)                         	  // 3개로 제한
                .collect(Collectors.toList());            //결과를 리스트로 저장

filter, map, limit 과 같은 연결할 수 있는 스트림연산을 중간연산 이라고 하며, 스트림을 닫는 연산 collect를 최종연산 이라고 한다.

4.4.1 중간 연산

중간연산의 중요한 특징은 단말 연산을 스트림 파이프라인에 실행하기 전까지는 아무 연산도  수행하지 않는다는것이다.

중간연산을 합친 다음에 합쳐진 중간 연산을 최종연산으로 한번에 처리하기 때문이다.

	/**
         * 4.4.1 중간 연산
         */
        List<Dish> menu = new ArrayList<>();
        List<String> names = menu.stream()
                .filter(dish -> {
                    System.out.println("filtering : " + dish.getName());
                    return dish.getCalories() > 300;
                })
                .map(dish -> {
                    System.out.println("mapping : " + dish.getName());
                    return dish.getName();
                })
                .limit(3)
                .collect(Collectors.toList());

4.4.2 최종연산

최종연산은 스트림 파이프라인에서 결과를 도출한다. 보통 최종연산에 의해 List, Integer, void 등 스트림 이외의 결과가 반환된다.

+ Recent posts