함수형 프로그래밍
함수형 프로그래밍이란
데이터를 바꾸지 않고, 함수에 값을 넣어서 결과를 얻는 것에 집중하는 프로그래밍이다.
함수는 "입력"을 받아서 "출력"을 만들고 그 과정에서 프로그램의 다른 부분(변수 등등)에 영향을 주지 않는다.
이렇게 되면 어떤 값이 주어졌을 때 항상 같은 결과를 반환한다.
함수형 프로그래밍은
- 순수 함수 : 동일 입력에는 항상 동일 출력
- 불변성 : 데이터를 변경하지 않고 새 데이터 생성
- 고차 함수 : 함수를 인자로 전달, 함수 반환 가능
- 선언적 프로그래밍 : "어떻게"가 아니라 "무엇을"에 집중
이러한 특징이 있다.

Stream()
Stream()은 자바에서 제공하는 기능으로
리스트나 배열 같은 데이터 묶음을 한 줄씩 반복하지 않고 한 번에 여러 작업(map, filter 등)을 함수처럼 연결해서 처리할 수 있게 해 주며 데이터를 직접 바꾸지 않고 결과만 새로 만들어진다.
for문 → stream() + map()
for문 방식
여러 개의 데이터에서 원하는 조건으로 묶은 후, 묶은 정보들을 한 번에 모아서 리턴하려고 했었다.
기존에는 for문으로 데이터를 하나씩 꺼내서 각각의 데이터를 원하는 조건으로 다시 가공하고
가공한 데이터를 하나의 DTO로 만들어서 result에 쌓아서 return 하는 방식이였다.
이렇게 절차형으로 백엔드를 짰었다.
stream/map, filter
이걸 함수형 프로그래밍을 써서 stream/map 방식으로 바꾸는 게 더 좋을 것 같다는 리뷰를 받았다.
기존 for문으로는 데이터 반복을
for (Data data : dataList) {
// 각 데이터 처리
}
이렇게
stream에서는
dataList.stream()
이렇게 데이터를 stream으로 변환해서 처리하게 했다.
그런 후 특정 필드 기준으로 그룹화를 하기 위해
기존 for문을
Map<Long, List<Data>> groupMap = new HashMap<>();
for(Data data : dataList) {
Long infoId = data.getInfoId();
if(!groupMap.containsKey(infoId) {
groupMap.put(infoId, new ArrayList<>());
}
groupMap.get(infoId).add(data);
}
stream으로 바꿨다.
.collect(Collectors.groupingBy(Data::getInfoId))
그룹핑된 데이터를 반복 처리하기 위해
for문에서는 각 데이터마다 별도로 처리했지만
stream에서는
.entrySet().stream()
을 통해 그룹핑된 각 infoId, List<Data> 쌍을 stream으로 변환했다.
그 후 Info 정보 조회 및 ResponseDTO로 변환했다.
기존 for문에서는
Info info = infoRepository.findById(infoId).orElse(null);
if (info == null) continue;
ResponseDTO dto = new ResponseDTO();
dto.setInfoId(...);
// 필요한 정보 셋팅
result.add(dto);
이렇게 했지만
stream에서는 map을 사용해서 entry(info, List<Data> 쌍)을 ResponseDTO 객체로 변환했다.
.map(entry -> toResponseDTO(entry.getKey(), entry.getValue()))
그 후 잘못된(없는) info에 대한 필터링 조건은
기존 for문
if (info == null) continue;
stream의 filter를 사용해서
.filter(Objects:nonNull)
로 바꿨다.
이런 결과를 List로 수집하는 부분은
for문
result.add(dto);
//...
return result;
stream
.collect(Collectors.toList())
로 처리했다.
최종 stream
return dataList.stream()
.collect(Collectors.groupingBy(Data::getInfoId))
.entrySet().stream()
.map(entry -> toResponseDTO(entry.getKey(), entry.getValue()))
.filter(Objects:nonNull)
.collect(Collectors.toList());
이렇게 하니까 코드가 훨씬 짧고 명확해졌다.
stream의 .groupingBy(), .map(), .filter()등을 함수로 연결해서 사용하니까 데이터의 흐름이 한눈에 보여서 가독성도 좋아졌다.
또 for문은 result 리스트 등 상태를 직접 관리하니까 실수가 발생할 수 있는데
stream()은 원본 데이터 변경 없이 처리하기 때문에 더욱더 안정성이 생겼다.
또 stream에다가 filter, map, group등으로 확장과 변경이 쉬워졌다.
List<DTO> result = new ArrayList<>();
for (...) {
//1. 조건
//2. 변환
//3. 예외처리
//4. 직접 add
}
return DataList.stream()
.filter(...)
.map(...)
.collect(Collectors.toList());
이렇게 코드리뷰 받아서 새로운 방식도 찾아보면서 공부하고 상황마다 어떤 방식이 좋은지 고민하면서 배우는 게 재밌다!
코드리뷰 받을때 기본적인 걸 놓쳐서 받는 경우엔 조금 민망하고 그렇지만,
이렇게 다양한 방법으로 공부할 수 있다는 게 좋은 것 같다!