[Java][Backend] null 반환 리팩터링
계기: 간만에 이펙티브자바 복습했다. 아래 내용을 프로젝트에 적용해보기로 결정했다!
https://splendidlolli.tistory.com/672
🪄 리팩터링 케이스
1. return값을 사용하지 않는데 null 반환을 금지해야 할까?
아래와 같이 리턴값을 사용하지 않는 메서드라도, null을 반환하지 않게 했다.
(코드의 일관성, 미래의 안정성을 근거로 함)
if (dishDtos.size() > 0) {
menuService.createMenuWithDishes(dishDtos);
}
2. API에 영향이 가는 조심스러운 상황
이런 API가 하나 있다. return부를 보면 deleteKeyword가 호출되고 있는데, 이 코드는 null을 반환할 여지가 있다.
@DeleteMapping
public KeywordResponseDto deleteKeyword(@RequestBody KeywordRequestDto keywordRequestDto) {
String email = tokenProvider.decodeToEmail(keywordRequestDto.getUser_id());
return keywordService.deleteKeyword(keywordRequestDto.getKeyword(), email);
}
프론트측 코드를 확인해보니까 null 처리 부분은 딱히 없고, DTO 필드인 List<Strings> keywords만 response가 넘어가는 상황이다. 다만 200코드가 아닐 때 삭제에 실패했다는 Toast를 띄워주고 있었으므로, 서버에서 500 상태코드를 던져주는 구조가 존재해야 의미가 있었다.
-- 시도한 것: 비어있는 DTO 객체를 던져주자.
그렇게 수정을 하려니까 지금처럼 DTO를 null로 던지나, 빈 DTO의 필드가 null이나 그게 그거 아닌가 싶었다. 오히려 DTO를 null로 던지는 현재 상태가 Controller가 처리하기 편할 것 같다는 생각도 들었다. 어떻게 해야 할까?
-- 결정: 예외를 던지자.
서비스 코드의 deleteKeyword에서부터 예외를 던지기로 했다. 가장 깔끔할 것 같다는 판단이 들었다.
그리고 Controller는 그대로 두기로 결정했다.
// Service 코드
// 변경 전: null을 반환
Keyword keyword = keywordRepository.findByName(keywordName);
if (keyword == null) {
return null;
}
// 변경 후: 예외 발생
Keyword keyword = keywordRepository.findByName(keywordName);
if (keyword == null) {
throw new KeywordNotFoundException("키워드 삭제 실패");
}
이젠 키워드 삭제 실패에 대해서 null을 리턴하는 것이 아니라,
대놓고 500에러를 일으킬 수 있도록 변경되었다.
3. null 대신 빈 스트링 리턴
아래와 같이 토큰 String을 검사하는 코드가 있다. resoleToken은 그동안 유효하지 않은 경우 null을 리턴했는데, 빈 스트링을 리턴하기로 결정했다.
유효성 검증의 책임은 아래 if문에 있기 때문이다.
String jwt = resolveToken(request); // 토큰 정보를 꺼내옴
// validateToken으로 토큰이 유효한지 검사
if(StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
🪄 배운 것
1. Collections.emptyList() vs Collections.EMPTY_LIST
=> 결론은 Collections.emptyList()를 사용해야 type-safe하다!
2. Collections.emptyList() vs List.of()
Collections.emptyList() | List.of() | |
불변 리스트를 반환하나? | O | O |
인스턴스를 생성하나? | X | X |
불변이라는 점과, 매번 생성하지 않는다는 점에서 동일하므로 고민 중이던 큰 부분에서의 차이는 없다.
찾아본 차이점은 각각의 방법으로 생성한 불변리스트에서 기능을 사용할 때의 처리방법이다.
예를 들어서 size() 메서드를 보자.
Collectoins.emptyList()는 size()에서 0값을 반환한다.
반면 List.of()는 empty가 목적이 아닌 코드로, size() 호출 시 구현체의 size 를 가져온다.
// 예시: ArrayList에서 size()
public int size() {
return size;
}
// EmptyList 클래스에서 size()
public int size() {
return 0;
}
판단해보자면, List.of와 Collectoins.emptyList 모두 static final이니까, 이후 처리를 고려하여 선호하는대로 사용하면 될 것 같다. 아래 링크도 참고할만하다.
https://stackoverflow.com/questions/39400238/list-of-or-collections-emptylist
이 차이는 Collections.emptyList()가 반환하는 것이 EmptyList 클래스타입인 것에서 기인한다.
다음의 Collectoins 클래스의 코드를 참고하자.
// Collections 클래스
public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}
public static final List EMPTY_LIST = new EmptyList<>();
// Collectoins 클래스 내부에 있는 EmptyList 클래스는 get()할 때 예외가 일어나도록 되어있다.
private static class EmptyList<E>
extends AbstractList<E>
implements RandomAccess, Serializable {
.
.
public int size() {return 0;}
public boolean isEmpty() {return true;}
public void clear() {}
.
.
public E get(int index) {
throw new IndexOutOfBoundsException("Index: "+index);
}
.
.
}