반응형
Recent Posts
Recent Comments
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

Real Vectorism. 훨씬 더 입체적으로...

Optional과 Stream...? 본문

Java (based by 1.8, Lambda)

Optional과 Stream...?

grast 2020. 3. 1. 23:24
반응형

자바 1.8 에서 추가된 새로운 개념인 Optional과 Stream의 개념... 어렵다. 그냥 하지말라고 권할 정도로 개념 자체가 생소하다. 하지만 웹 개발을 하느라 자바스크립트와 제이쿼리에 이미 굳은살이 생기기 시작한 개발자라면 오히려 적극 써보라고 권할 문법이다. 주어와 목적어의 개념을 바꿔서 생각해봐야 할 콜백의 개념을 프로그래밍 로직에 재밌게 녹여냈다는 정도의 문법이다.

예를 들기 위해 하나의 객체를 미리 만들어둔다.

List<String> list = new ArrayList<String> (); 
list.add("1st chicken"); 
list.add("2nd chicken"); 
list.add("3rd chicken"); 
list.add("4th chicken"); 
...... 중간생략합니다
... 주석기호도 귀찮아... 
list.add("100th chicken");

...... 물론... 비유적 설명도 같이 추가한다.

/**
 * 오늘도 치킨을 많이 팔아서 돈을 벌어보자 
 * 
 * 아침에 배송된 생닭 상자에 가공된 생닭이 100마리가 들어왔다. 
 * 그런데... 생닭 상태가 어찌 고르지 못하고 여러가지가 있다. 영계부터 불량계육까지...
 */

... 왜 또 치킨이냐... 보험

그렇다면 스트림이란 도데체 어떻게 이해해야하는건가?

스트림이란 구체적인 설명은 할 수 없지만 비유적으로 설명하자면 아이템 하나하나를 순회할 수 있도록 구조를 바꾼 새로운 형태의 (하지만 구성은 절대로 다르지 않은) 데이터 구조로 파악하면 된다.

list.stream().filter(item->!item.contains("0")).forEach(System.out::println);

... 비유적 설명은 또 어디다 팔아먹었냐고? 알았다 알았어......

/**
 * 아무래도 팔아먹을 닭은 선별하고 나머지는 폐기처리를 하든 음식물폐기물처리를 하든 해야겠지. 
 * 생닭을 하나하나 포장을 뜯어서 컨베이어벨트에 일자로 세워넣고 
 * 양호한 것만 선별하기 위해 컨베이어벨트를 작동시킨다.
 */

일자로 세워놓고가 바로 스트림의 기능이라고 생각하면 될 것 같다. 결국 stream()으로 호출된 자료는 자료를 하나하나 모두 호출하여 비교 또는 처리할 수 있도록 개인화시킨 것으로 생각하면 된다.

생닭이 개인화가 되었다는건 상품성이 있는지 없는지를 하나하나 확인해서 선별하기 위함이다. 그 좁은 컨베이어벨트에 한번에 여러개를 보고 선별해야 할 정도로 쌓여서 데이터를 검색한다면 어딘가가 인력으로 처리할 수 없는 상황이 도래할 것이니 애초에 잘 세워넣고 차근차근하자는 의미에서 도입된 기능이라고 생각하면 될 것 같다.

그렇게 해서... 선별이 끝났다. 그럼 아마 list 객체는 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, ... , 99만 남아있을 것이고, 치킨은 아무 문제 없이 바로 튀겨도 되는 신선한 계육만 남아서 장사를 바로 개시해도 될 것이다. 그런데 만약 처음부터 주어진 요소(또는 계육)이 개판이라서 선별하다보니 아예 남아있지 않는 경우라면...?

없으면 그냥 null 체크 또는 그냥 오늘 재료가 다 떨어져서 장사를 조기마감합니다 붙이면 될 것이다. 그런데 장사는 짬밥이 있다면 요령껏 넘어갈 수 있어도, 자바쪽에서는 이야기가 다르다. 여기서는 비유적 설명의 난이도가 확 올라가므로 이해가 안되면 해당 비유적 설명은 아예 그냥 못본 척 해주길 바란다......

list.stream().filter(item->item.indexOf("z") > 0).forEach(System.out::println);
/**
 * 그냥 치킨 말고 로또를 할래... 
 * 이왕 닭 선별해야하는거 혹시 모르니 황금알을 품은 닭을 찾아볼까? 
 * 
 * (MBN 황금알은 그냥 볼 가치도 없이 폐기처분이다.)
 */

1st부터 100th까지 z가 들어가는 서수표현식도 없다, chicken에도 z의 스펠링이 포함되는 경우는 없다. 어느 동물이든 몸 속에 황금을 품고있다면 그건 생명에 치명적일테니 장사에도 써먹을 계육이 아니다. 그럼 결국 남는 요소는 아무것도 없다. 위의 문법이라면 forEach가 실행되지 않겠지만 만약 reduce를 통해 하나의 요소로 돌출을 해야하는 구문이라면? list타입도 아니고 string 타입도 아닌 stream으로 변환된 데이터를 null 체크할 방법이 있을까?

여기서 잠깐, mapReduce에 대한 간단한 개념 하나를 훑어보고 가자. 훑어보지 않고 핥아도 된다.

class Point { 
	int x; 
    int y; 
    
    Point(int x, int y) { 
    	this.x = x; 
        this.y = y; 
    } 
    
    쓸데없이 너무 길어지니 getter와 setter는 생략합니다. 
    어차피 코틀린에서도 getter랑 setter 쓰지 말고 public으로 수정하라고 했었다는데... 
    꺼라위키라서 나도 못믿겠다...... 
} 

List<Point> pointList = new ArrayList<Point> (); 
pointList.add(new Point(0, 0)); 
pointList.add(new Point(0, 1)); 
pointList.add(new Point(2, 0)); 
pointList.add(new Point(2, 1)); 

pointList.stream().map(item->item.getX() + item.getY()).reduce((i1, i2)->i1+i2)

함수형 스트림에서 주어지는 요소 중 map과 reduce라는 것이 있는데 구글에서 그냥 맵리듀스라고 치면 자바 스트림하고는 조금... 거리가 있는 구글의 맵리듀스가 나온다. 하둡 관련해서 자료가 많이 나오는데 빅데이터 처리를 위한 일종의 병렬처리연산이라고 생각하면 될 것 같다. 어차피 여기서 대강 감으로 설명하는거지 정확한 개념은 아니니까 여기선 그냥 그러려니하고 나중에 제대로 배우자. 틀린걸 바로잡는건 창피한게 아니다.

맵핑은 리스트에 들어있는 객체의 타입을 바꾸는 특징을 가지고 있다. 조건을 지정해주면 해당 조건에서 지정된 리턴타입에 맞게 변환된다. 위의 소스코드에서는 map(item->item.getX() + item.getY()) 라고 했는데 x값과 y값을 더하면 애초에 x와 y가 int로 선언된 만큼 x+y 또한 int 타입이 된다. 그런데 저 map이라는 메소드는 정말 특이한 기능을 수행하는거라서 List<Point> 타입인 pointList가 단숨에 List<Integer>로 바뀌어버리는 결과가 나온다. (그러나 구체적으로는 List<Integer>가 아닌 Stream<Integer>가 되어 리스트처럼 바로 사용할 방법이 없다.)

리스트에 다수 들어있는 아이템을 한번에 바꿀 수가 없다면 아예 이런식으로 바꿔버리는 방법도 있을 뿐 더러 코딩하는데 List타입의 객체를 여러개 만들기가 싫다면 이렇게 해버리는 경우도 있다. 잘못 다루면 회사에서 짤릴 수도 있다...

리듀스는 다른 방향으로 조금 특이한데 매개변수가 2개다. 어떻게 2개의 매개변수를 하나의 아이템이 되도록 병합하여 합칠 것인지 기준을 정한다. 위에서는 i1, i2를 매개변수로 두고, i1 + i2 를 가지는 값을 반환하도록 했다. map구절에서 x+y 값의 Integer 스트림으로 바꾸었으니 그 값을 모두 더해주면 된다. 소스코드를 읽는 방법은 map에서 x+y를 했으니 (0+0), (0+1), (2+0), (2+1) 의 스트림이 되어 0, 1, 2, 3의 스트림이 되었고, 리듀스에서는 i1 + i2 로 정의를 했으니 (((0 + 1) + 2) + 3) 를 수행하여 최종적으로 6을 받게 된다. 그러나 실제로는 반환데이터가 6이 아닌 Optional<new Integer(6)>인 상태가 된다. reduce의 반환형 타입은 Optional<T>이다.

이제 다 왔다. Optional은 선택적인 귀차니즘 객체이다. 한글로 풀어서 말하자면 이 객체는 아니면 말구 객체다. 아주 간단하다. Optional에는 어떠한 값이든 다 넣을 수 있고(단, 원시형인 int, float, double 등은 안된다) 해당 값이 있으면 뭘 하고 없으면 안한다 또는 없으면 대신 뭘 한다 이런식으로 행위를 정하는 것이 가능하다.

Optional객체의 메소드

Optional.get: 아니면 말구 객체 안에 유효한 값이 있으면 바로 그것을 가지고 온다. 없을 경우 문제가 생긴다.

Optional.orElse: 아니면 말구 객체 안에 값이 없을 경우 디폴트로 받을 값을 지정한다. 없을 경우 이 값을 받는다.

Optional.ifPresent: 해당 값이 있을 경우 어떻게 해당 값을 소모시킬 것인지 행위를 지정한다.

Optional.orElseGet: 해당 값이 없을 경우 어떤 후속조치를 취할 것인지 행위를 지정하는... 것 같다......

즉, Optional<"test">.orElse("zzzz").ifPresent(item->{ 곧 치킨튀길 개발자입니다 }) 이런식으로 하면 ifPresent에서 지정한 행위가 실행이 되는 것이고, 나머지는 나도 모른다...

Optional은 그냥 간단하게 말해, 치킨튀길 계육이 없으면 배째라 나도 장사 안해 라고 설명할 수 있을 것 같다. 있으면 당연히 한마리라도 튀겨서 돈벌어야지 장사 시작 뭐 이런거...? (그런데 한마리가지고 장사할 수가 있을까... 60계는 60마리라도 튀기지......)

이러다보니 함수형 패러다임에 대해 다시 한번 생각하게 되는 계기가 된 것 같다.

사실 진행중인 프로젝트가 막바지에 갑작스럽게 신규인원이 투입되었는데 유지보수를 하기 어려운 들여쓰기와 alias 지정이 더러운 쿼리문을 실행하도록 디자인한 바람에 order구문 추가위치와 where 검색이 완전 malbolge 급이 되어버린 소스코드의 수정을 포기하고 함수형으로 순서정렬을 강제로 구해버린 경우가 엊그제였나 어제였나 암튼 있었다. 결국 걸리면 모가지이지만 아무것도 못할 바에 이걸로라도 임기응변으로 대응해야지 몇번 해보면 정말 재밌는 기능이 아닐 수 없다. 물론 함수형 프로그래밍의 한계로 Optional과 Stream의 첫 입문은 코딩재미가 아닌 디버깅 지옥을 거쳐야 한다는것이 어마어마한 문제점이다... 어차피 개발자들도 뜨거운 기름 앞에서 닭 튀기는게 처음엔 코딩하는것보다 훨씬 어렵겠지만... 작정한 프로그래머들은 짬밥만 쌓이면 닭 뜯듯 튀길꺼면서......

반응형
Comments