문제상황
검색어 자동 완성을 구현하는 과정에서 keyword를 여러 값으로 나눈 후 (ex: 사용자의 입력이 "teho tistory" 일 경우 {"teho tistory", "tehotistory", "teho", "tistory"} 4개로 키워드를 만들어서 검색어에 맞는 게시물로 자동 완성을 해준다.) redis에서 추천 게시물을 갖고온 후 모든 값을 하나의 리스트에 합치는 과정에서 UnsupportedOperationException이 발생했다.
"사용할 수 없는 동작이야" 라는 오류인데 도대체 왜..? 어디서 발생하는 에러인지 한눈에 알 수 없어서 천천히 뜯어보았다.
원인 파악
전체적인 흐름은 메서드(searchByKeyword, searchBykeywordList)에서 keywrod를 포함하는 값들을 List<>로 반환받고 keywordSearch에 mergedKeywordSearch, splitKeywordSearch를 중복을 제거해 합친 후 response를 보내는 것이다.
어디서 발생하는 에러인지 찾기위해 로그를 활용해 디버깅을 했다.
7 다음에 에러가 나왔으니 keywordSearch.add(dto)에서 발생했다. 다시 한번 설명하자면 keywrodSearch는 검색 결과를 담고 있는List<SearchPostForReviewDto> 형태이고, searchByKeyword(redisMap, keyword) 메서드는 keyword로 redis의 HashMap에 저장해 둔 게시글을 찾아 제목을 기준으로 사전 순으로 정렬해 준다.
List<>에 .add()를 쓰는 게 왜 문제지..? 이런저런 고민을 해보다가 searchByKeyword가 반환하는 게 다른가? 하고 stream()의 .toList()의 반환형을 확인해 보기 위해 내부로 들어가 보았다.
이름에서부터 수정할 수 없다... stream에서 .toList()를 하면 Collections.unmodifiableList(불변 컬렉션)를 반환해 준다. 여기서 한번 더 cmd + left를 사용해 unmodifableList로 들어가 보면
내부 메서드와 unmodifiableList의 필드를 확인할 수 있다. 일단 내 상황에서 에러가 발생한 원인을 바로 찾을 수 있다. stream().toList()에서 반환하는 객체는 unmodifiableList -> unmodifableList에서 set(), add(), remove()와 같이
객체의 값을 변경하는 함수를 실행했을 때 throw new UnsupprotedOperationException()이 적힌 것을 볼 수 있다.
나는 이 list에 다른 값들도 추가해서 return 값을 만들고 싶은데 방법이 없는 것일까?? 라고 생각했고 구글링과 gpt를 사용해 두 가지 방법을 찾게 되었다.
해결방법
1. 내가 사용한 해결 방법(new ArrayList<>()로 감싸기)
나는 메서드에서 반환된 List<>가 수정이 가능하도록 new ArrayList<>()로 한번 감싸주었다.
이게 가능한 이유를 알아보기 위해 new ArrayList<>()를 cmd + left를 하면,,,
매개변수로 들어온 값(위의 사진에서 c)을 바로 .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로 반환을 똑같이 해주고 있다.
추가 내용
구글링을 하다가 알게 된 점인데 unmodifiableList의 생성자를 다시 보면 필드 멤버인 list를 단순히 this.list = list를 사용한다.
이 뜻은 얕은 복사(shallow copy, 값을 복사한 것이 아니라 객체의 주소를 공유)를 하고 있으며 A=B, 일 때 B를 변경하면 A도 함께 변경된다..
불변 객체이지만 그 불변 객체 안에 있는 list에 접근하여 값을 변경할 수 있다면 간접적으로 변경이 가능하다.
또한 .toList()를 사용했을 때 불변 객체를 반환하는 것은 Java16 이상부터이다. 그전 버전에서는 아래와 같이 작성하던 것이 16이 되면서 .toList()로 대체 되었다.
List<teho> teho = a.stream()
// ....
// ....
.collect(Collectors.toUnmodifiableList());
끝~
'My project > Nanaland in Jeju' 카테고리의 다른 글
프로젝트 진행 시 엑셀에 작성한 데이터, DB에 저장하는 방법 (0) | 2024.10.17 |
---|---|
스프링 검색어 자동완성 비동기 처리 (Feat. 1만번의 부하테스트 결론은 Over Engineering 이었다고 한다..) (5) | 2024.08.29 |
검색어 자동완성 구현하기 with Redis (Feat. Elasticsearch) (2) | 2024.08.01 |
QueryDsl 부모에 자식 리스트 넣기 -> Transform (부제 : 쿼리의 수와 속도는 무조건 정비례가 아니다..) (0) | 2024.06.27 |
맥에서 MySql, MaraiDB 두개 다 사용하기 (Feat. Docker 이래서 쓰는구나, 맥 mariaDB 설치하기) (0) | 2024.03.27 |