프로젝트를 진행하면서 람다는 시큐리티에서 사용해봤지만 Stream은 전혀 사용하지 않고 있었다.(잘 모르기도 했고 for문이 더 편했어서)
이번에 stream을 사용해보고자 코드를 리팩토링하고 시간을 측정했는데 생각 외의 결과가 나왔다.
아래는 스크럼을 조회하는 서비스이고 데이터는 2000개, 스레드 20개, 루프카운트 10으로 조회를 시작했다.
// 스크럼 팀 조회
@Override
public ScrumRoomListResponseDTO findScrums(Long teamId) {
User user = securityContext.getUser();
Team team = teamRepository.findById(teamId)
.orElseThrow(() -> new TeamNotFoundException("DB에서 " + teamId + "번 팀을 찾지 못했습니다."));
inviteTeamListRepository.findByUserAndTeamAndParticipantIsTrue(user, team)
.orElseThrow(() -> new NonParticipantUserException(teamId + "번 팀 초대 리스트에" + "번 유저가 존재 하지 않습니다."));
List<Scrum> scrumList = scrumRepository.findActiveScrumsByTeam(team);
List<ScrumRoomDTO> scrumRoomDTOList = new ArrayList<>();
for (Scrum s : scrumList) {
if (s.getEndTime() == null) {
ScrumRoomDTO scrumRoomDTO = ScrumRoomDTO.fromEntity(s);
scrumRoomDTOList.add(scrumRoomDTO);
}
}
return new ScrumRoomListResponseDTO(scrumRoomDTOList);
}
평균 요청 시간이 2288ms가 걸린을 볼 수 있다.
그럼 위 코드를 stream을 사용해서 리팩토링을 진행해보자.
// 스크럼 팀 조회
@Override
public ScrumRoomListResponseDTO findScrums(Long teamId) {
User user = securityContext.getUser();
Team team = teamRepository.findById(teamId)
.orElseThrow(() -> new TeamNotFoundException("DB에서 " + teamId + "번 팀을 찾지 못했습니다."));
inviteTeamListRepository.findByUserAndTeamAndParticipantIsTrue(user, team)
.orElseThrow(() -> new NonParticipantUserException(teamId + "번 팀 초대 리스트에" + "번 유저가 존재 하지 않습니다."));
List<Scrum> scrumList = scrumRepository.findActiveScrumsByTeam(team);
List<ScrumRoomDTO> scrumRoomDTOList = scrumList.stream()
.filter(scrum -> scrum.getEndTime() == null)
.map(ScrumRoomDTO::fromEntity)
.collect(Collectors.toList());
return new ScrumRoomListResponseDTO(scrumRoomDTOList);
}
filter를 통해 시간이 null인 것만 필터링을 하고, map을 통해 dto로 변환시켜서 List를 만들었다.
아니 왜요? 스트림이 for문보다 빠른 거 아니었나? 200ms가 늘어난 결과를 확인할 수 있었다.
그럼 내가 왜 stream을 도입하려 했는가?
1. 우선 가독성이 뛰어나다.
2. for문보다 시간이 빠른 줄 알았다.
1번은 납득이 된다. for문 돌리는 것보단 스트림이 가독성이 훨 뛰어나다.
근데 2번이 이해가 안된다. 그럼 가독성 말고는 stream을 쓸 이유가 없지 않나? 가독성도 중요하지만 서비스는 리소스가 생명이지 않은가
뭔가 이상하다 느꼈다. 그래서 따로 테스트를 진행해보자.
50만개의 int를 만들고 최댓값을 구하는 테스트 진행.
Long start = System.nanoTime();
for(int i=0; i<test.length; i++){
if(max < test[i]) max = test[i];
}
Long end = System.nanoTime();
System.out.println("걸린 시간 : " + String.valueOf(end - start));
---------------------------------------------------------------------
start = System.nanoTime();
max = Arrays.stream(test)
.reduce(Integer.MIN_VALUE, Math::max);
end = System.nanoTime();
System.out.println("걸린 시간 : " + String.valueOf(end - start));
위가 for문을 통해 최댓값을 찾고, 아래는 stream을 통해 최댓값을 탐색한다.
여러 번의 테스트를 했는데 최소 4배의 압도적인 차이가 났다.(for문 승리)
이번에는 Wrapper 타입인 Integer로 실험을 진행해보았다.
확실히 primitive보다 차이가 많이 줄어든 것을 확인할 수 있다.
왜 Wrapper 타입이 더 차이가 적을까?
Wrapper 타입의 데이터는 heap 메모리 영역에 저장되고, primitive 타입의 데이터는 stack 메모리 영역에 저장된다.
여기서 간접 참조와 직접 참조라는 개념이 사용된다.
간접 참조 : 스택에 있는 변수가 대상 heap의 주소를 얻은 후 실제 값을 참조하는 방법.
직접 참조 : 스택에 있는 변수가 대상 heap의 실제 주소를 가지고 있어 직접 참조하는 방법.
primitive는 직접 참조를, wrapper는 간접 참조를 이용하기에 시간 차이가 발생하는 것이다.
즉 순회비용이 계산비용보다 높다는 말이다.
stream을 제대로 사용하려면 계산비용이 높은 곳에서 stream을 사용하는 것이 적절하다는 생각이 들었다.
참고자료
'프로젝트 > 씈크럼 프로젝트' 카테고리의 다른 글
우당탕탕 동시성에 대해 파악하고 thread-safe하게 해보자 (0) | 2024.03.11 |
---|---|
내가 원하는 컬럼만 업데이트 쿼리가 나가고싶은데..(feat, DynamicUpdate) (0) | 2024.02.22 |
공통으로 가지는 생성시간, 수정시간을 상속으로 이용해보자. (0) | 2024.02.19 |
QueryDsl로 페이지네이션 도입(검색 API 구현) (1) | 2024.02.17 |