개요

 

개발을 하다 보면, null인 데이터를 처리해야 할 때가 있다. 반환만 한다면, 문제없지만 데이터를 가공한다면, NullPointerException을 마주치게 될 것이다. 따라서 null일 수 있는 데이터 판별과 어떻게 처리해야 할 지에 대한 고민을 적어보려고 한다. 지극히 개인적인 생각이 많으므로 자유롭게 댓글 달아주셔도 됩니다.

 

예시 객체

@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
@Getter
public class Member {

    private String name;

    private LocalDateTime serviceStartTime;

    private Integer age;
}

 

고민의 시작

 

위의 객체 데이터를 반환할 때 아래와 같은 조건을 가지고 있다.

 

1. name -> 환영합니다. {name}고객님으로 반환해야 된다.

2. 글로벌 서비스이므로 LocalDateTime -> ZonedDateTime으로 변환해야 한다.
3. age는 null일 때 0으로 반환해야 한다. 

 

하나씩 해결해 보자.

String.format()를 이용해서 name으로 파싱을 진행하면 된다.

LocalDateTime은 atZone()을 이용하면 된다.

age는 null일 때만 0으로 반환하도록 작성하면 된다.

 

반환하는 코드를 작성해 보자. (예시이므로 빌더를 이용해서 반환한다고 가정)

 

아래처럼 작성했다.

 

@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
@Getter
public class Member {

    private String name;

    private ZonedDateTime serviceStartTime;

    private Integer age;

    public static Member getOfData(final String name, final LocalDateTime serviceStartTime, final Integer age){
        return Member.builder()
            .name(name)
            .serviceStartTime(serviceStartTime)
            .age(age)
            .build();
    }
}

 

반환하는 값 name, serviceStartTime, age 모두 조건을 만족한다. 하지만 파라미터로 들어오는 데이터가 null일 경우 예외가 발생한다. 이러한 null 처리를 어떻게 컨벤션을 정했는지 알아보자.

 

null 처리에 대한 여러 가지 방법

age의 경우 단순히 DEFAULT_AGE를 반환하면 된다.

 

나머지 2개의 경우를 보자.

 

name의 경우 파싱을 해야 하는 로직이 들어가고, serviceStartTime 또한 ZonedDateTime으로 변환하는 코드가 들어간다는 점을 생각하고 방법들을 살펴보자.

 

방법 1

 

"==" + 삼항 연산자 사용하기

 

public static Member getOfData(final String name, final LocalDateTime serviceStartTime, final Integer age){
    return Member.builder()
        .name(name == null ? null : String.format(MEMBER_NAME_FORMAT, name))
        .serviceStartTime(serviceStartTime == null ? null :serviceStartTime.atZone(DEFAULT_ZONE))
        .age(age == null ? DEFAULT_AGE : age)
        .build();
}

 

 

위에서 이야기했듯이 age는 "==" + 삼항 연산자로도 반환하고자 하는 데이터를 쉽게 파악할 수 있다. 

 

반면에 name, serviceStartTime의 경우 로직이 들어가 복잡해져서 단번에 파악하기 쉽지 않다.

 

방법 2

 

Objects + 삼항 연산자

 

public static Member getOfData(final String name, final LocalDateTime serviceStartTime, final Integer age){
    return Member.builder()
        .name(Objects.isNull(name) ? null : String.format(MEMBER_NAME_FORMAT, name))
        .serviceStartTime(Objects.isNull(serviceStartTime) ? null : serviceStartTime.atZone(DEFAULT_ZONE))
        .age(Objects.isNull(age) ? null : DEFAULT_AGE)
        .build();
}

 

위에서 사용한 age == null 보다 의미가 잘 전달되는 거 같다. 하지만 name, serviceStartTime에 대해서는 여전히 복잡해 보인다. 

 

방법 3

 

로직을 메서드로 추출해서 의미를 쉽게 파악할 수 있도록 하기

 

public static Member getOfData(final String name, final LocalDateTime serviceStartTime, final Integer age){
    return Member.builder()
        .name(toMemberNameFormat(name))
        .serviceStartTime(toZonedDateTime(serviceStartTime))
        .age(Objects.isNull(age) ? null : DEFAULT_AGE)
        .build();
}

@Nullable
private static String toMemberNameFormat(final String name) {
    return Objects.isNull(name) ? null : String.format(MEMBER_NAME_FORMAT, name);
}

@Nullable
private static ZonedDateTime toZonedDateTime(final LocalDateTime serviceStartTime) {
    return Objects.isNull(serviceStartTime) ? null : serviceStartTime.atZone(DEFAULT_ZONE);
}

 

메서드 안의 로직을 정확히 파악하지 않아도, 메서드 명을 통해서 파악할 수 있다. 의미를 파악하기 한결 수월해졌다. 하지만 toZonedDateTIme과 toMemberNameFormat의 경우 다른 곳에서 사용할 수도 있다. 그렇다고 해서 public으로 열어두는 것은 반환을 위해서 Member 객체를 참조해야 하기 때문에 좋지 않다고 생각한다.

 

Utill 객체를 사용해 보자!

 

방법 4

 

공통으로 사용될 수 있는 로직을 Util 객체로 선언해서 사용하기

 

public static Member getOfData(final String name, final LocalDateTime serviceStartTime, final Integer age){
    return Member.builder()
        .name(StringParserUtil.toMemberNameForamt(name))
        .serviceStartTime(DateTimeUtil.toZonedDateTime(serviceStartTime))
        .age(Objects.isNull(age) ? null : DEFAULT_AGE)
        .build();
}

 

Util 객체를 사용함으로써 다른 객체에서 사용, 의미 파악 용이, 응집돼 있어서 유지보수 용이라는 장점을 가져간다. 물론 Util 객체를 만들어야 하는 번거로움이 있지만, 얻는 장점이 더 많은 거 같다.

 

방법 5

 

Optional로 처리하기

 

public static Member getOfData(final String name, final LocalDateTime serviceStartTime, final Integer age){
    return Member.builder()
        .name(Optional.ofNullable(name)
            .map(n -> String.format(MEMBER_NAME_FORMAT, n))
            .orElse(null))
        .serviceStartTime(Optional.ofNullable(serviceStartTime)
            .map(sst -> sst.atZone(DEFAULT_ZONE))
            .orElse(null))
        .age(Objects.isNull(age) ? null : DEFAULT_AGE)
        .build();
}

 

 

JPA에서 단건 쿼리에 대한 null을 Optional로 처리하는 것처럼 동일하게 처리할 수 있다. 해당 방법에 대한 문제는 불필요한 Optional객체를 선언해야 하므로 개인적으로 좋은 방법은 아닌 거 같다고 생각한다.

 

결론

 

약 5가지의 방법을 살펴봤고, null 처리에 대한 컨벤션을 정해봤다.

 

1. "==" 보다는 Objects.isNull() 사용하기.

2. 다른 곳에서 사용될 염려가 없을 경우 메서드로 추출하기

3. 다른 곳에서 사용될 수 있으면 Util 객체를 선언하고 사용하기.

4. Optional은 리소스 낭비가 될 수 있으므로 사용을 지양하기

 

이렇게 간단한 컨벤션을 정하는데도, 여러 가지 방법이 있고 상당한 시간이 소요된다. 하지만 프로젝트가 커짐에 따라 유지 보수 등의 이유로 공통의 코딩 스타일을 가져가는 것은 중요하다고 생각한다. 앞으로 컨벤션에 대한 글을 조금씩 올리려 한다. 댓글로 다양한 의견은 언제나 환영입니다. 

'JAVA' 카테고리의 다른 글

API 공통 응답 형식 정하기  (1) 2023.10.23
Java 8 -> 11 변경점  (0) 2022.12.07
Java Garbage Collection  (0) 2022.05.30
이펙티브 자바  (0) 2022.04.22
JAVA Overloading & Overriding  (0) 2022.02.26