JAVA/Application

[Java][Backend] null 반환 리팩터링

히어로맛쿠키 2024. 3. 8. 15:51

계기: 간만에 이펙티브자바 복습했다. 아래 내용을 프로젝트에 적용해보기로 결정했다!

https://splendidlolli.tistory.com/672

 

[이펙티브 자바] null이 아닌, 빈 컬렉션이나 배열을 반환하라 ─ 8장:메서드:Item54

null이 아닌, 빈 컬렉션이나 배열을 반환하라 null이 아닌 빈 배열이나 컬렉션을 반환하라. null을 반환하는 API는 사용하기 어렵고, 오류 처리 코드도 늘어난다. 그렇다고 성능이 좋은 것도 아니다.

splendidlolli.tistory.com

 


🪄 리팩터링 케이스

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하다!

 

Collections.emptyList()는 EMPTY_LIST를 사용하지만 type-safe하도록 캐스팅해준다.

 

 

2. Collections.emptyList() vs List.of()

  Collections.emptyList() List.of()
불변 리스트를 반환하나? O O
인스턴스를 생성하나? X X

 

Collections.emptyList()는 static final EMPTY_LIST를 사용하는데, 리턴시 type-safe하도록 캐스팅해준다.
List.of() 또한 인스턴스를 매번 생성하지 않고 불변이다.

 

불변이라는 점과, 매번 생성하지 않는다는 점에서 동일하므로 고민 중이던 큰 부분에서의 차이는 없다.

찾아본 차이점은 각각의 방법으로 생성한 불변리스트에서 기능을 사용할 때의 처리방법이다.

 

예를 들어서 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

 

List.of() or Collections.emptyList()

As an special case of List.of(...) or Collections.unmodifiableList() - what is the preferred Java 9 way of pointing to an empty and immutable list? Keep writing Collections.emptyList(); or switc...

stackoverflow.com

 

이 차이는 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);
    }
    .
    .
}