1. 뭐하다가 Stream의 map, flatMap?

gpt로 열심히 공부중이다. stream을 잘몰라서 일단 foreach로 던져주고 이거 어떻게 하면 스트림으로 바꾸지? 하면 얘가 잘바꿔준다. 그럼 그걸 계속보고 따라해보려고 하는식으로 학습중이다.

오늘의 예제는 이것이었다.

@Transactional(readOnly = true)
public List<GoalDto> getTodoList(Authentication authentication) {
	User loginUser = ((PrincipalDetails) authentication.getPrincipal()).getUser();
	List<TeamMember> joinedHistory = teamMemberRepository.findByUser(loginUser);
	List<GoalDto> results = new ArrayList<>();

	joinedHistory.forEach(j -> {
    	List<Goal> goalList = goalRepository.findByOwner(j);
    	List<GoalDto> goalDtoList = goalList.stream()
        	.map(GoalDto::fromGoal)
       	 .collect(Collectors.toList());
    	results.addAll(goalDtoList);
    });

	return results;
}

음 의도는 뭐였냐면 로그인한 유저의 가입한
1. 팀회원 entity를 가져와서
2. 그 팀회원 entity가 가지고 있는 모든 목표를 dto로 반환하는 것이었다.

쥐피티에게 이거 좀 다듬어줘라고 했다.
그러니까 아래처럼 해주었다.

@Transactional(readOnly = true)
public List<GoalDto> getTodoList(Authentication authentication) {
    User loginUser = ((PrincipalDetails) authentication.getPrincipal()).getUser();
    return getGoalsForUser(loginUser);
}

public List<GoalDto> getGoalsForUser(User user) {
    return teamMemberRepository.findByUser(user).stream()
        .flatMap(this::getGoalDtoList)
        .collect(Collectors.toList());
}

public Stream<GoalDto> getGoalDtoList(TeamMember teamMember) {
    return goalRepository.findByOwner(teamMember).stream()
        .map(GoalDto::fromGoal);
}

어우 훨씬 명확하다... 내꺼는 들여다봐야지만 gpt거는 함수명만 읽으면 아 뭐하는애겠구나 알거 같다. 나였으면 저걸 들여다보면서 리팩토링 리팩토링 해야 나오는 결과인데 ai는 남다르다. 어쨋든 저기에서 나는 많이 쓰기는 했지만 정확히 뭔지 모르겠는 map과 그냥 모르겠는 flatMap에 대해서 물어봤다.

1. map()

gpt가 말한걸 정리하면, 일단 map은 Java 8에서 도입된 Stream API의 메소드 중 하나이다. 이 메소드는 스트림의 각 요소에 함수를 적용하고, 그 결과를 새로운 스트림에 수집하는 역할을 한다.

간단한 예시를 들어보면 우리가 List<Integer>를 가지고 있고, 각 요소를 제곱하려고 한다고 가정해보자. 이를 위해 map을 사용할 수 있다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = numbers.stream()
    .map(n -> n * n)
    .collect(Collectors.toList());

위의 코드에서 map 메소드는 각 숫자에 제곱 함수 (n -> n * n)를 적용한다. 그 결과는 새로운 스트림에 수집되며, 이를 다시 리스트로 변환하여 squares에 저장한다.

public Stream<GoalDto> getGoalDtoList(TeamMember teamMember) {
    return goalRepository.findByOwner(teamMember).stream()
        .map(GoalDto::fromGoal);
}

마찬가지로, 코드에서 GoalDto::fromGoal는 map의 인자로 사용되었다. 이 함수는 Goal 객체를 GoalDto 객체로 변환한다. 따라서 map(GoalDto::fromGoal)은 Goal 객체의 스트림을 GoalDto 객체의 스트림으로 변환한다.

2. flatMap()

1) 그게뭔데?

flatMap은 스트림의 각 요소에 함수를 적용하고, 그 결과로 생성된 모든 스트림을 하나의 "평평한" 스트림으로 연결하는 메소드다.

예를 들어, 각 팀원(TeamMember)이 가진 목표(Goal) 리스트를 가져오는 경우를 생각해보자. map을 사용하면, 결과는 Stream<List<GoalDto>> 형태가 된다. 즉, 각 팀원이 가진 목표 리스트로 이루어진 스트림이 생성됩니다. 이는 우리가 원하는 List<GoalDto> 형태와 다르다.

이 문제를 해결하기 위해 flatMap을 사용한다. flatMap은 각 팀원의 목표 리스트를 하나의 "평평한" 스트림으로 만들어준다. 따라서, 최종적으로는 Stream<GoalDto> 형태의 스트림이 생성되며, 이는 우리가 원하는 List<GoalDto> 형태로 쉽게 변환할 수 있다.

2) 그럼 왜씀? 그냥 map 쓰면 되는거아님?

맞다. 나는 이렇게 들어선 잘 이해가 안갔다. 일단 읽어서 내가 안 내용은 flatMap은 일단 Stream<GoalDto>를 만들고, 얘에서 GoalDto를 추출해서 List<GoalDto>로 만든다는것이구나. 그럼 왜 굳이 map에서 나온 Stream<List<GoalDto>>에서 그냥 Stream만 벗겨주는게 아니라 flatMap으로 일일히 다 벗겨가면서 list로 모으는걸까?

이것은 Stream<List<GoalDto>>Stream<GoalDto>자료구조 측면에서 매우 다른 형태이기 때문이다. Stream<List<GoalDto>>의 경우 스트림의 각 요소가 List<GoalDto>인 구조이다. 따라서 이 스트림의 각 요소에 접근하려면 먼저 스트림을 순회하고, 그 후 각 리스트를 또 순회하는 구조인 것이다.

반면에 Stream<GoalDto>는 스트림의 구조가 GoalDto객체인 구조이다. 이 스트림의 각 요소에 접근하려면 스트림 순회 단 한번만하면 된다.

flatMap() 메서드는 Stream<List<GoalDto>>Stream<GoalDto>로 변환하는 역할을 한다. 이 메소드를 사용하면 중첩된 스트림을 단일 스트림으로 "평평하게" 만들 수 있으므로, 스트림의 요소에 접근하는 데 필요한 순회 과정을 한 단계로 줄일 수 있다. 이는 코드의 복잡성을 줄이고 가독성을 향상시키는 데 도움이 된다.

3. 마침

오늘도 gpt로 열공을 했다. 요즘엔 vue와 springboot를 이용해서 저런 목표관리 프로그램을 만들어보고 있다. vue에 대해서도 물어보고 공부하지만, 거의 모든걸 물어보면서 하기 때문에 일단 vue에 적응이 되고 나서 그때부터 알게되는 것들을 적을 생각이다.

0개의 댓글

Powered by GraphCDN, the GraphQL CDN