의도는 "메시징"이다. 훌륭하고 성장 가능한 시스템을 만들기 위한 핵심은 모듈 내부의 속성과 행동이 어떤가보다는 모듈이 어떻게 커뮤니케이션하는가에 달려 있다.
자율적인 책임
자율적인 객체란 스스로 정한 원칙에 따라 판단하고 스스로의 의지를 기반으로 행동하는 객체다. 타인이 정한 규칙이나 명령에 따라 판단하고 행동하는 객체는 자율적인 객체라고 부르기 어렵다.
객체가 어떤 행동을 하는 유일한 이유는 다른 객체로부터 요청을 수신했기 때문이다. 요청을 처리하기 위해 객체가 수행하는 행동을 책임이라고 한다. 따라서 자율적인 객체란 스스로의 의자와 판단에 따라 각자 맡은 책임을 수행하는 객체를 의미한다.
적절한 책임이 자율적인 객체를 낳고, 자율적인 객체들이 모여 유연하고 단순한 협력을 낳는다. 따라서 협력에 참여하는 객체가 얼마나 자율적인지가 전체 애플리케이션의 품질을 결정한다.
왕은 목격자인 모자장수에 "증언하라" 라는 요청을 전송한다. 모자장수가 재판이라는 협력에 참여하기위해서는 왕의 요청을 적절하게 처리한 후 응답해야한다. 요청은 수신자의 책임을 암시하므로 모자 장수는 재판이라는 협력에 참여하기 위해 '증언할' 책임을 진다. 왕은 모자 장수에게 '증언하라'라는 자신의 요청에 반응해 책임을 완수할 수만 있다면 어떤 방법으로 증언하는지에 관해서는 싱겨을 쓰지않으며, 스스로의 의자와 판단에 따라 자유롭게 선택할 수 있다.
반면 왕이 모자 장수가 증언하는데 필요한 행동을 좀더 상세히 요청한다고 가정해보자 '목격했던 장면을 떠올리고', '떠오르는 기억을 시간 순서대로 재구성'한 후 '말로 간결하게 표현' 해야 하는 책임을 떠안게 된다.
협력의 결과로 모자장수가 왕의 요청을 받아 자신이 목격한것을 증언하게 된다는점에서는 동일하다. 하지만 모자 장수에게 주어진 권한에는 큰 차이가 있다.
첫번째 모자장수에게 할당된 '증언하라' 라는 책임은 그 자체로 모자 장수의 자율성을 충분히 보장 할 수 있을 정도로 포괄적이고 추상적이면서도 모자 장수가 해야 할일을 명확하게 지시하고 있다. 반면 두번째 협력에서 모자 장수에게 할당된 좀 더 상세한 수준의 책임들은 모자 장수의 자율성을 제한한다.
자율적인 책임의 특징은 객체가 '어떻게' 해야하는가가 아니라 '무엇을' 해야하는가를 요청한다. 무엇은 수신한 객체의 책임이다.
책임이라는 말 속에는 어떤 행동을 수행한다는 의미가 포함되어 있다. 객체가 자신에게 할당된 책임을 수행하도록 만드는것은 외부에서 전달되는 요청이다. 객체가 다른 객체에게 접근할 수 있는 유일한 방법은 요청을 전송하는 것뿐이다. 그리고 이 요청을 메시지라고 한다. 메시지는 객체로 하여금 자신의 책임, 즉 행동을 수행하게 만드는 유일한 방법이다.
일단 컴퓨터를 조작하는 것이 추상화를 구축하고, 조작하고, 추론하는것에 관한 모든것이라는 것을 깨닫고 나면 (훌륭한) 컴퓨터 프로그램을 작성하기 위한 중요한 전제 조건은 추상화를 정확하게 다루는 능력이라는 것이 명확해진다.
책의 해리백 이야기를 읽고
추상화를 통한 복잡성 극복
현실에 존재하는 다양한 현상 및 사물과 상호작용하기 위해서는 우선 현실을 이해해야한다. 문제는 복잡성의 총체인 현실이라는 괴물을 그대로 수용하기에는 인간이 지니고 있는 인지 능력과 저장 공간이 너무나도 보잘것 없다. 따라서 사람들은 본능적으로 이해하기 쉽고 예측 가능한 수준으로 현실을 분해하고 단순화하는 전략을 따른다.
해리백의 지하철 노선도는 불필요한 지형 정보를 제거함으로써 단순함을 달성한 추상화의 훌륭햔 예이다. 진정한 의미에서 추상화란 현실에서 출발하되 불필요한 부분을 도려내가면서 사물의 놀라운 본질을 드러나게 하는 과정이라고 할 수 있다. 추상화의 목적은 불필요한 부분을 무시함으로써 현실에 존재하는 복잡성을 극복하는 것이다.
추상화
어떤 양상, 세부 사항, 구조를 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법이다. 복잡성을 다루기 위해 추상화는 두 차원에서 이뤄진다.
첫번째 차원은 구체적인 사물들 간의 공통점은 취하고 차이점은 버리는 일반화를 통해 단순하게 만드는것이다.
두번째 차원은 중요한 부분을 강조하기 위해 불필요한 세부사항을 제거함으로써 단순하게 만드는것이다.
모든 경우에 추상화의 목적은 복잡성을 이해하기 쉬운 수준으로 단순화하는 것이라는 점을 기억하라.
객체지향과 추상화
책의 모두 트럼프일뿐을 읽고
앨리스는 객체들중에서 하얀 토끼를 제외한 모든 객체를 '트럼프'라는 하나의 개념으로 단순화해서 바라보고 있다. 앨리스는 정원사들 , 병사들, 왕자와 공주, 하객으로 참석한 왕과 왕비들, 하트 왕과 하트여왕의 차이점은 과감하게무시한채 공통점만을 취해 단순화 해버렸다.
결과적으로 앨리스는 정원에 있는 인물을 두개의 그룹으로 나눴다. 하나는 트럼프 그룹이고 또 다른 하나는 토끼그룹이다.
앨리스가 인물들의 차이점을 무시하고 공통점만을 취해 트럼프라는 개념으로 단순화한것은 추상화의 일종이다. 이처럼 공통점을 기반으로
객체들을 묶기 위한 그릇을 개념이라고한다. 개념을 이용하면 객체를 여러 그룹으로 분류 할 수 있다. 앨리스가 정원에 존재하는 객체를 '트럼프'와 '토끼' 라는 두개의 개념으로 나누고는 두개념에 적합한 객체가 각그룹에 포함되도록 분류했다는 사실에 주목하라. 결과적으로 개념은 공통점을 기반으로 객체를 분류 할 수 있는 일종의 체라고 할 수 있다.
분류는 객체지향의 가장 중요한 개념 중 하나다. 어떤 객체를 어떤 개념으로 분류할지가 객체지향의 품질을 결정한다. 객체를 적절한 개념에 따라 분류하지 못한 애플리케이션은 유지보수가 어렵고 변화에 쉽게 대처하지 못한다.
객체지향 패러다임은 지식을 추상화하고 추상화한 지식을 객체 안에 캡슐화함으로써 실세계 문제에 내재된 복잡성을 관리하려고 한다.
객체를 발견하고 창조하는 것은 지식과 행동을 구조화하는 문제다.
객체지향 패러다임은 인간이 인지할 수 있는 다양한 객체들이 모여 현식 세계를 이루는 것처럼 소프트웨어의 세계 역시 인간이 인지할 수있는 다양한 소프트웨어 객체들이 모여 이뤄져 있다는 믿음에서 출발한다.
책의 이상한 나라의 앨리스 파트를 읽고
앨리스는 정원으로 가는길을 가로막고 있는 작은 문을 통과하기에 적당한 상태로 자신의 키를 계속해서 변화시킨 것이다.
앨리스의 키는 시간의 흐름에 다라 계속 변한다. 그렇다고 엘리스의 키가 아무런 이유없이 변하는것은 아니고, 앨리스의 키를 변화시키는것은 앨리스의 행동이다. 앨리스가 하는 행동에 따라 앨리스의 상태가 변하게된다.
행동에 의해 앨리스의 상태가 변경되더라도 앨리스가 앨리스라는 사실은 변하지 않는다. 앨리스는 상태 변경과 무관하게 유일한 존재로 식별가능하다.
앨리스는 상태를 가지며 상태는 변경가능하다.
앨리스의 상태를 변경 시키는것은 앨리스의 행동이다.
행동의 결과는 상태에 의존적이며상태를 이용해 서술할 수있다.
행동의 순서가 결과에 영향을 미친다.
앨리스는 어떤 상태에 있더라도 유일하게 식별 가능하다.
상태
어떤 행동의 결과는 과거에 어떤 행동들이 일어났느냐에 의존한다. 즉, 상태는 행동에의존한다.
필자는 책의 내용을 이해한 바로는 "자신의 메소드(행동)에 의해 필드(상태)가 변한다." 라고 이해된다.
때로는 단순한 값이 아니라 객체를 사용해 다른 객체의 상태를 표현해야 할 때가 있다.
앨리스가 현재 음료를 들고 있는 상태인지를 표현하고 싶다면 가장 간단하고 직관적인 방법으로 앨리스의 상태 일부를 음료라는 객체를 이용해 표현하는 것이다.
결론적으로 모든 객체의 상태는 단순한 값과 객체의 조합으로 표현할 수 있다. 이때 객체의 상태를 구성하는 모든 특징을 통틀어 객체의 프로퍼티라고 한다. 앨리스의 경우 키, 위치, 음료가 앨리스의 프로퍼티가 된다. 일반적으로 프로퍼티는 변경되지 않고 고정되기 때문에 '정적'이다. 반면 프로퍼티 값은 시간이 흐름에 따라 변경 되기 때문에 '동적'이다.
책의 객체의 상태 정의
상태는 특정 시점에 객체가 가지고 있는 정보의 집합으로 객체의 구조적 특징을 표현한다.
객체의 상태는 객체에 존재하는 정적인 프로퍼티와 동적인 프로퍼티 값으로 구성된다.
객체의 프로퍼티는 단순한 값과 다른 객체를 참조하는 링크로 구분할 수 있다.
객체는 자율적인 존재라는 점을 명심하라. 객쳊비향의 세계에서 객체는 다른 객체의 상태에 직접적으로 접근할 수도, 상태를 변경할 수도 없다. 자율적인 객체는 스스로 자신의 상태를 책임져야한다.
행동
객체의 상태는 저절로 변경되지 않는다. 객체의 상태를 변경하는것은 객체의 자발적인 행동뿐이다.
객체는 자신에게 주어진 책임을 완수하기 위해 다른 객체를 이용하고 다른 객체에게 서비스를 제공한다. 객체는 다른 객체와 적극적으로 상호작용하며, '협력하는 객체들의 공동체'에 참여하기 위해 노력한다.
객체가 다른 객체와 메시지를 통해서만 의사소통할 수 있다는것을 기억하라, 객체가 어떤 행동을 하도록 만드는 것은 객체가 외부로부터 수신한 메시지다. 객체는 수신된 메시지에 따라 적절히 행동하면서 협력에 참여하고 그 결과로 자신의 상태를 변경한다.
객체지향의 세계에서 모든객체는 자신의 상태를 스스로 관리하는 자율적인 존재이다. 앨리스 객체의 키를작게 만드는것은 앨리스 자신이여야하고, 음료의 양을 줄이는것은 음료 자신이여야한다.
위 파트를 처음 읽을때 무슨말인지 이해가 되지않았다. 곰곰이 생각해봤다.
앨리스는 자신의 상태를 변경하고, 이과정에서 앨리스는 자신이 먹은 양만큼 으료의 양을 줄여달라고 메시지를 전송한다.
이 그림에서 앨리스와 음료에게 전송되는 메시지 이름을 주목하면된다. 메시지를 앨리스에게 전송하는 객체이건 음료에게 메시지를 전송하는 앨리스 객체이건 메시지 송신자는 수신자의 상태 변경에 대해서는 전혀 알지 못한다. 이것이 캡슐화가 의미하는것이다.
식별자
객체가 식별 가능하다는것은 객체를 서ㅏ로 구별할 수 있는 특정한 프로퍼티가 객체 안에 존재한다는 것을 의미한다. 이 프로퍼티를 식별자라고 한다. 모든 객체는 식별자를 가지며 식별자를 이용해 객체를 구별 할 수있다.
값은 숫자, 문자, 날짜, 시간, 금액 등과 같이 변하지 않는 양을 모델링한다. 흔히 값의 상태는 변하지 않기 때문에 불변 상태를 가진다고 말한다. 값이 같은지 여부는 상태가 같은지를 이용해 판단한다. 값의 상태가 같으면 두 인스턴스는 동일한 것으로 판단하고 상태가 다르면 두인스턴스는 다른것으로 판단한다. 이처럼 상태를 이용해 구 값이 같은지 판단할 수 있는 성질을 동등성(equality)이라고 한다.
값은 오직 상태만을 이용해 동등성을 판단하기 때문에 인스턴스를 구별하기 위한 별도의 식별자를 필요로 하지 않는다.
객체는 시간에 따라 변경되는 상태를 포함하며, 행동을 통해 상태를 변경한다. 따라서 객체는 가변 상태를 가진다고 말한다. 타입이 같은 두 객체의 상태가 완전히 똑같더라도 두 객체는 독립적인 별개의 객체로 다뤄야한다. 이름이 앨리스이고 키가 동일한 두사람이 함께 있다고 하더라도 어떤 누구도 두 사람을 같은 사람이라고 생각하지 않는다.
어린 나는 현재의 나보다 키도 작고 나이도 적지만 두사람은 동일한 인물이다. 따라서 상태와 무관하게 동일한 사람으로 판단될 수 있는 일종의 식별자를 가지고 있는 개체라고 할 수 있다.
이처럼 식별자를 기반으로 객체가 같은지를 판단할 수 있는 성질을 동일성(identical)이라고 한다.
상태를 기반으로 객체의 동일성을 판단할 수 없는 이유는 시간의 흐름에 따라 객체의 상태가 변하기 때문이다. 어느 한 시점에 두 객체의 상태가 동일하더라도 한 객체의 상태가 변하는 순간 두 객체는 서로 다른 상태가 되어버린다.
행동이 상태를 결정한다.
객체를 설계할때 상태를 중심으로 객체를 바라본다. 객체에 필요한 상태가 무엇인지를 결정하고 그 상태에 필요한 행동을 추가한다.
상태를 먼저 결정하고 행동을 나중에 결정하는 방법은 설계에 나쁜 영향을 끼친다.
첫째, 상태를 먼저 결정할 경우 캡슐화가 저해된다. 상태에 초점을 맞출 경우 상태가 객체 내부로 깔끔하게 캡슐화되지 못하고 공용 인터페이스에 노출 될 확률이 높다.
둘째, 객체를 협력자가 아닌 고립된 섬으로 만든다. 객체가 필요한 이유는 애플리케이션의 문맥 내에서 다른 객체와 협력하기 위해서다.
셋째, 객체의 재사용성이 저하된다. 객체의 재사용성은 다양한 협력에 참여할 수 있는 능력에서 나온다. 상태에 초점을 맞춘 객체는 다양한 협력에 참여하기 어렵기 때문에 재사용성이 저하된다.
가장 중요한 덕목은 상태가 아니라 행동에 초점을 맞추는 것이다. 객체는 다른 객체와 협력하기 위해 존재한다. 객체의 행동은 객체가 협력에 참여하는 유일한 방법이다.
객체지향 설계는 애플리케이션에 필요한 행동을 생각 한 후 행동을 수행할 객체를 선택하는 방식으로 수행된다. 행동을 결정한 후에야 행동에 필요한 정보가 무엇인지를 고려하게되며 이과정에서 필요한 상태가 결정된다. 따라서 먼저 객체의 행동을 결정하고 그 후에 행동에 적절한 상태를 선택하게 된다.
24년 4월 실무경력 1년을 경험한, 연차로는 2년차 개발자가 되었다. 지난 1년을 생각해보면 실무 경험 및 나름의 공부를하며, 성장을 해왔지만 실무 경험에 맞춰 개발 및 장애대응을 하다보니 천천히 기초에 집중하기보다는 급하게 달려온 느낌이들었다.
또한 최근 자바 플레이그라운드 with TDD, 클린 코드 인강을 구매하여 숫자야구게임을 개발하면서 Java라는 언어를 사용하지만, 객체지향적으로 프로그래밍하지못하고 있는자신을 발견하여 객체지향에 대해 공부를 하기로 마음먹었다.
책을 읽으며 무조건 책의 내용을 정리하기보다는 책의 내용 정리 및 나의 생각을 녹여내고시간이 지나 책의 내용이 기억이 나지않을때 다시 읽어보기위해 기록하였다.
시너지를 생각하라. 전체는 부분의 합보다 크다.
객체지향이라고 불리는 새로운 세상의 문을 연 대부분의 사람들은 "객체지향이란 실세계를 직접적이고 직관적으로 모델링 할 수 있는 패러다임" 이라는 설명과 마주하게 된다.
책을 처음 접했을때는 "실세계", "현실 모방" 말은 많이 들었지만 객체지향이란 정확히 무엇일까? 어떤것을 말하고 싶은것일까?라는 생각이든다.
객체를 스스로 생각하고 스스로 결정하는 현실 세계의 생명체에 비유하는것은 상태와 행위를 '캡슐화'하는 소프트웨어 객체의 '자율성'을 설명하는데 효과적이다. 현실 세계의 사람들이 암묵적인 약속과 명시적인 계약을 기반으로 협력하며 목표를 달성해 나가는 과정은 '메시지'를 주고 받으며 공동의 목표를 달성하기 위해 '협력'하는 객체들의 관계를 설명하는데 적합하다.
책의 협력하는 사람들 파트를 읽고
따뜻한 커피와 함께 할 수 있는 소박한 아침 시간의 여유를 누릴 수 있는 이유는 커피 주문하는 손님, 주문을 받는 캐시어, 커피를 제조하는 바리스타라는 역할이 존재하기 떄문이다.
손님, 캐시어, 바리스타는 주문한 커피를 손님에게 제공하기 위해 협력하는 과정에서 자신이 맡은바 책임을 다한다. 손님은 카페인을 채우기 위해 커피를 주문할 책임을 수행한다.
커피 주문이라는 협력에 참여하는 모든 사람들은 커피가 정확하게 주문되고 주문된 커피가 손님에게 정확하게 전달 될 수 있도록 맡은바 역할과 책임을 다하고 있다.
협력하는 사람들 예제에서 각 객체들간의 협력을 보면 Application에서 객체들의 request, response를 잘나타낸 좋은 예제라고 생각이든다.
책에서 말하는 바는 객체지향에서 가장 중요한 개념 3가지 역할, 책임, 협력 이다.
객체 지향의 본질
객체지향이란 시스템을 상호 작용하는 자율적인 객체들의 공동체로 바라보고 객체를 이용해 시스템을 분할하는 방법이다.
자율적인 객체란 상태와 행위를 함께 지니며 스스로 자기 자신을 책임지는 객체를 의미한다.
객체는 시스템의 행위를 구현하기 위해 다른 객체와 협력한다. 각 객체는 협력내에서 정해진 역할을 수행하며 역할은 관련된 책임의 집합이다.
객체는 다른 객체와 협력하기 위해 메시지를 전송하고, 메시지를 수신한 객체는 메시지를 처리하는데 적합한 메서드를 자율적으로 선택한다.
자바 성능 튜닝 이야기 [Chap 3] <왜 자꾸 String을 쓰지 말라는거야>를 읽고 정리 및 추가 개념을 정리하였다.
String vs StringBuffer vs StringBuilder
// 책참고 코드
final String aValue = "abcde";
for(int outLoop=0;outLoop<10;outLoop++) {
String a = new String();
StringBuffer b = new StringBuffer();
StringBuilder c = new StringBuilder();
for(int loop=0;loop<10000;loop++) {
a+=aValue;
}
for(int loop=0;loop<10000;loop++) {
b.append(aValue);
}
String temp = b.toString();
for(int loop=0;loop<10000;loop++) {
c.append(aValue);
}
String temp2 = c.toString();
}
- 응답시간
주요 소스 부분
응답 시간(ms)
비고
a+=aValue;
95,801.41ms
95초
b.append(aValue); String temp=b.toString();
247.48ms 14.21ms
0.24초
c.append(aValue); String temp2=b.toString();
174.17ms 13.38ms
0.17초
- 메모리사용량
주요 소스 부분
메모리 사용량(bytes)
생성된 임시 객체 수
비고
a+=aValue;
100,102,000,000
4,000,000
약 95Gb
b.append(aValue); String temp=b.toString
29,493,600 10,004,000 1,200
200
약 28Mb 약 9.5Mb
c.append(aValue); String temp2=b.toString()
29,493,600 10,004,000
1,200 200
약 28Mb 약 9.5Mb
a += avalue; => a=a+aValue와 같다.
새로운 String 객체가 만들어지고, 이전에 있던객체는 GC의 대상이된다.
StringBuffer 클래스와 StringBuilder 클래스
StringBuffer, StringBuilder 클래스는 제공하는 메서드가 동일하다.
StringBuffer 클래스는 스레드에 안전하게(Thead Safe)설계 되어 있으므로, 여러개의 스레드에서 하나의 StringBuffer 객체를 처리해도 전혀 문제가 되지 않는다. 하지만, StringBuilder는 단일 스레드에서의 안정성만을 보장한다. 그렇기 때문에 여러개의 스레드에서 하나의 StringBuilder 객체를 처리하면 문제가 발생한다.
왼쪽이 StringBuffer의 append 메서드이며, synchronized가 선언되어있고, 오른쪽의 StringBuilder는 없다.
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 등 다양한 형식으로 이루어진 숫자 집합의 평균을 계산 할 수 있다.
하나의 요약 연산으로 메뉴에 있는 수, 칼로리합계, 평균, 최댓값, 최솟값등을 계산하는 코드이다.
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>를 반환한다.
스트림 인터페이스를 잘 사용하면 코드의 재사용성과 커스터 마이즈 가능성이 높아지고, 추상화와 일반화를 얻을 수 있다.