te___ho
NO RULES
te___ho
전체 방문자
오늘
어제
  • 분류 전체보기 (88)
    • My project (27)
      • High Traffic Lab (3)
      • Nanaland in Jeju (8)
      • Univey (3)
      • inflearn_clone? (13)
    • Spring (19)
    • Network & CS (9)
    • Java (1)
    • Front_End (8)
    • Algorithm (11)
    • ETC (6)
    • Scribble (7)

인기 글

최근 글

티스토리

hELLO · Designed By 정상우.
te___ho

NO RULES

Stream API 사용 중 .toList()에서 UnsupportedOperationException 발생한 썰
My project/Nanaland in Jeju

Stream API 사용 중 .toList()에서 UnsupportedOperationException 발생한 썰

2024. 7. 26. 01:00

 문제상황

 검색어 자동 완성을 구현하는 과정에서 keyword를 여러 값으로 나눈 후 (ex: 사용자의 입력이 "teho tistory" 일 경우 {"teho tistory", "tehotistory", "teho", "tistory"} 4개로 키워드를 만들어서 검색어에 맞는 게시물로 자동 완성을 해준다.) redis에서 추천 게시물을 갖고온 후 모든 값을 하나의 리스트에 합치는 과정에서 UnsupportedOperationException이 발생했다.

UnsupportedOperationException

 "사용할 수 없는 동작이야" 라는 오류인데 도대체 왜..? 어디서 발생하는 에러인지 한눈에 알 수 없어서 천천히 뜯어보았다.

 원인 파악

 전체적인 흐름은 메서드(searchByKeyword, searchBykeywordList)에서 keywrod를 포함하는 값들을 List<>로 반환받고 keywordSearch에 mergedKeywordSearch, splitKeywordSearch를 중복을 제거해 합친 후 response를 보내는 것이다.

에러 발생 지점 찾기..

 어디서 발생하는 에러인지 찾기위해 로그를 활용해 디버깅을 했다.

7 다음에 에러가 발생 ..!

 
 7 다음에 에러가 나왔으니 keywordSearch.add(dto)에서 발생했다. 다시 한번 설명하자면 keywrodSearch는 검색 결과를 담고 있는List<SearchPostForReviewDto> 형태이고, searchByKeyword(redisMap, keyword) 메서드는 keyword로 redis의 HashMap에 저장해 둔 게시글을 찾아 제목을 기준으로 사전 순으로 정렬해 준다.

searchByKeyword 메서드

 
 List<>에 .add()를 쓰는 게 왜 문제지..? 이런저런 고민을 해보다가 searchByKeyword가 반환하는 게 다른가? 하고 stream()의 .toList()의 반환형을 확인해 보기 위해 내부로 들어가 보았다.

unmodifiableList..

 
 이름에서부터 수정할 수 없다... stream에서 .toList()를 하면 Collections.unmodifiableList(불변 컬렉션)를 반환해 준다. 여기서 한번 더 cmd + left를 사용해 unmodifableList로 들어가 보면 

UnmodifiableList Class

 
 내부 메서드와 unmodifiableList의 필드를 확인할 수 있다. 일단 내 상황에서 에러가 발생한 원인을 바로 찾을 수 있다. stream().toList()에서 반환하는 객체는 unmodifiableList -> unmodifableList에서 set(), add(), remove()와 같이
객체의 값을 변경하는 함수를 실행했을 때 throw new UnsupprotedOperationException()이 적힌 것을 볼 수 있다.
 나는 이 list에 다른 값들도 추가해서 return 값을 만들고 싶은데 방법이 없는 것일까?? 라고 생각했고 구글링과 gpt를 사용해 두 가지 방법을 찾게 되었다.

 해결방법

1. 내가 사용한 해결 방법(new ArrayList<>()로 감싸기)

 나는 메서드에서 반환된 List<>가 수정이 가능하도록 new ArrayList<>()로 한번 감싸주었다. 

UnsupportedOperationException가 발생했던 기존 코드
개선된 코드

 
이게 가능한 이유를 알아보기 위해 new ArrayList<>()를 cmd + left를 하면,,,

ArrayList 생성자

 
 매개변수로 들어온 값(위의 사진에서 c)을 바로 .toArray() 해버린다. toArray()를 마지막으로 보면!!!!

toArray() 설명

 
 이 collection에 있는 모든 요소를 갖고 있는 array를 반환한다!!
결론적으로 stream().toList()가 반환한 unmodifableList.toArray()를 하게 되면서 unmodifiableList -> array 즉 수정 불가한 컬렉션에서 수정 가능한 array로 바뀌게 된다. 문제 해결~ 이제 마음껏 .add()를 사용해도 된다.

2. 또 다른 방법

다른 방법은 .stream()에서 마지막에 .toList()를 하는 것이 아니라 애초에 우리에게 List<>를 선언할 때 익숙한 ArrayList로 반환하게 하는 것이다.

private List<SearchPostForReviewDto> searchByKeyword(Map<String, SearchPostForReviewDto> redisMap, String keyword) {
    // ArrayList를 사용하여 변경 가능한 리스트를 반환
    return redisMap.entrySet().stream()
        .filter(entry -> entry.getKey().contains(keyword))
        .map(Map.Entry::getValue)
        .sorted(Comparator.comparing(SearchPostForReviewDto::getTitle))
        .collect(Collectors.toCollection(ArrayList::new)); // ArrayList로 수집
}

 3. 또또 다른 방법

 마지막으로 어찌 보면 제일 깔끔한 방법을 개발바닥 부반장님께서 조언해주셨다. 전에 몇 번 사용까지 해본적이 있는데 아직 Stream에 익숙하지 않아서 생각하지도 못했다. (조언 정말 감사드린다는 말을 여기서 한번 더.. 🙏)
 
 2번 방법을 더 깔끔하고 직관적으로 할 수 있는 방법으로 .collect(Collectors.toList())를 사용하면 된다.

private List<SearchPostForReviewDto> searchByKeyword(Map<String, SearchPostForReviewDto> redisMap,
    String keyword) {
  return redisMap.entrySet().stream()
      .filter(entry -> entry.getKey().contains(keyword))
      .map(Map.Entry::getValue)
      .sorted(Comparator.comparing(SearchPostForReviewDto::getTitle))
      .collect(Collectors.toList());
}

 
Collectors.toList()를 확인해보면 안에서 2번 방법인 ArrayList로 반환을 똑같이 해주고 있다.

Collectors.toList()


 추가 내용

구글링을 하다가 알게 된 점인데 unmodifiableList의 생성자를 다시 보면 필드 멤버인 list를 단순히 this.list = list를 사용한다.
이 뜻은 얕은 복사(shallow copy, 값을 복사한 것이 아니라 객체의 주소를 공유)를 하고 있으며  A=B, 일 때 B를 변경하면 A도 함께 변경된다..

UnmodifiableList의 list 멤버와 생성자

 
 불변 객체이지만 그 불변 객체 안에 있는 list에 접근하여 값을 변경할 수 있다면 간접적으로 변경이 가능하다.
 
 또한 .toList()를 사용했을 때 불변 객체를 반환하는 것은 Java16 이상부터이다. 그전 버전에서는 아래와 같이 작성하던 것이 16이 되면서 .toList()로 대체 되었다.

List<teho> teho = a.stream()
// ....
// ....
	.collect(Collectors.toUnmodifiableList());

끝~

728x90
반응형
저작자표시 (새창열림)

'My project > Nanaland in Jeju' 카테고리의 다른 글

프로젝트 진행 시 엑셀에 작성한 데이터, DB에 저장하는 방법  (0) 2024.10.17
스프링 검색어 자동완성 비동기 처리 (Feat. 1만번의 부하테스트 결론은 Over Engineering 이었다고 한다..)  (5) 2024.08.29
검색어 자동완성 구현하기 with Redis (Feat. Elasticsearch)  (3) 2024.08.01
QueryDsl 부모에 자식 리스트 넣기 -> Transform (부제 : 쿼리의 수와 속도는 무조건 정비례가 아니다..)  (0) 2024.06.27
맥에서 MySql, MaraiDB 두개 다 사용하기 (Feat. Docker 이래서 쓰는구나, 맥 mariaDB 설치하기)  (0) 2024.03.27

    티스토리툴바