공부내용공유

java, kotlin에서 null 처리 본문

Server/Java

java, kotlin에서 null 처리

forfun 2024. 3. 31. 21:23

서론


 

프로젝트를 진행하다보면 null을 핸들링 해야하는 다양한 상황을 만난다. 오늘도 null 처리를 어디서, 어떻게, 어디까지 해줄까를 고민하고 팀원 분과 얘기를 하고 있었는데 kotlin의 문법을 null 처리 관련한 연산자를 알려주셨다.

 

 

코틀린을 사용해본 적이 없어서 이번 기회에  코틀린에서 지원해주는 null 핸들링 도구가 무엇이고 어떤 장점이 있는지와 내가 고민했던 java에서 null 처리 방법을  정리할 예정이다.

 

본론


적어도 null 처리 관점에서 가장 이상적인 객체의 설계는 nullable 한 필드가 없게하여 객체를 사용할 때NullPointerException을 피하는 설계라고 생각한다.

 

 

하지만 개발을 하다보면 Nullable한 값을 선언하고 (객체의 필드이든 메서드의 파라미터이든) 이를 적절히 핸들링하여 사용해야하는 경가 많이 생긴다. (레거시, 객체의 활용성 증대, 오버 엔지니어링(?)을 피하기 위한...)

 

 

이러한 상황에서 개발을 하다보면 null 처리에 대한 고민, null 처리를 위한 코드등 때문에 생산성이 많이 떨어지는구나를 오늘 따끈따근하게 느꼈다, null을 어떻게 핸들링 할 수 있는 각 방법들은 어떤 장,단점이 있을끼?

 

 

문제 상황

class Item {
    private String itemName;
    private Long itemPrice;
    private String itemNickName;
    private ItemType itemType;
}

 

대략 이런 클래스가 있고 레거시 데이터, 쉽지 않은 기획사항으로 itemNickName과 itemType이 nullalbe 하다고 가정하자.

 

 

2개의 nullable 한 필드를 저렇게 두는 것은 분명히 위험하다, itemNickName.lenght(), itemType.name() 등의 메서드가 호출되면 NullPointerException 이 발생하게 되고 단순 String, ItmepType을 보고는 이러한 상황을 예측하기 힘들다.

 

 

이러한 상황을 방지하기 위해서는 강제로 사용을 못하게 하거나 nullable 한 필드라는 것을 알려줘야 한다.

 

 

클래스 분리

 

위에서 얘기했던 것처럼 nullable한 필드를 없에는 것이다. 예시 클래스의 nullable한 필드를 없앤다고 하면

class LegacyItem extends Item {

}

class NewItem extends Item {
    private String itemNickName // NotNull,
    private ItemType itemType // NotNull
}

 

상속을 사용하든, 별도의 클래스를 만들어서 class 나 Enum Type으로 구분을 하든 애초에 nullable 한 필드를 없앨 수 있다.

 

 

하지만 이는 꽤나 많은 공수 작업을 요구할 것이다, 이를테면 DB에는 같은 테이블에 저장해야 하면 저장할 때 각 클래스를 구분하여 컨버팅을 해줘야 하고

 

 

해당 도메인에 대한 로직들에 전부 어떤 타입인지 구분하는 로직들이 추가 될 것이다.

 

 

Optional

class Item {
    private String itemName;
    private Long itemPrice;
    private Optional<String> itemNickName;
    private Optional<ItemType> itemType;
}

 

이렇게 Optional로 감싸면 다른 개발자에게 nullable한 필드임을 전달할 수 있고 사용할 때 NullPointerException 으로부터 안전해질 수 있다.

 

 

 

다만 해당 방법도 모든 nullable한 필드나 상황에 적용하기에는 해당 필드를 사용할 때 마다 orElseThrow(), get() 등을 사용해야 하는 추가 공수 작업이 발생하기에 살짝 아쉽다는 생각이 들었다.

 

 

Vo 사용

 

만약 해당 필드가 여러 곳에서 자주 사용된다 하면

class Item {
    private String itemName;
    private Long itemPrice;
    private ItemNickName itemNickName;
    private Optional<ItemType> itemType;
}

class ItemNickName {
    private String itemNickName;

    ItemNickName(String itemNickName) {
       this.itemNickName = itemNickName;
    }

    String getItemNickName() {
       if(itemNickName == null) {
            throw new NotSupportValueException();
       }
       return this.itemNickName;
    } 
}

 

 

이런식으로 사용이 가능할 것이다. 물론 둘 다 같은 RunTimeExcpetion이라 생각할 수 있지만 vo 에서 명시적인 예외를 던져주고 관리한다는 점에서  없는 것 보다는 낫다고 생각한다.

 

 

이렇게 한다면 Null 관련 로직은 vo안에만 있기 때문에 다른 코드에 퍼지지 않게 된다는 장점이 있다, 다만 과연 VO 가 null 검증을 책임지는게 맞을까? 라는 고려가 필요한 것 같다.

 

 

또한 사용하는 메서드마다 null 체크 로직들이 들어간다는 공수 작업이 기다리고 있다.

 

 

Kotlin ?

 

코를린 공식문서를 확인해보면 코틀린에서 NullPointerException이 발생하는 경우는

  1. 일부로 throw NullPointerException을 던지는 경우
  2. !! 을 사용한 경우
  3. 객체 생성시에 완벽히 생성되지 않는 this가 다른 곳에서 사용될 경우
  4. 제네릭과 같은 java의 문법을 잘못 사용할시

(자세한 내용과 예시는 도큐먼트에 잘 나와있다.)

 

 

kotlin은 null 값이 들어갈 수 있는 필드는 ?를 통해 nullalbe함을 표현할 수 있다. 또한 ?. 연산자를 통해 null 이면 null반환을 아니라면 함수실행이 가능하다.

val a = "Kotlin"
val b: String? = null
println(b?.length)
println(a?.length)

//result
null
6

 

 

만약 위에 예시처럼 b에 ? 연신자를 통해 nullable 함을 명시했는데 값 사용시에

  • ?.
  • if(b != null) b.length

이런식으로 사용하지 않는다면 컴파일시점에 오류가 발생한다.

 

 

이러한 연산자들로 좀 더 편하게 nullable한 필드임을 명시하고, null 값 검사를 통해 런타임중에 예외 발생을 막을 수 있다.

결론


java도 optional을 통해 nullable한 필드를 명시하고 안전하게 다루기 좋아졌지만 이번에 코틀린 문법을 간단하게 알아보면서
확실히 kotlin에서 제공하는 연산자를 사용했을 때 확실히 null 값 핸들링이 더 간단하게 할 수 있다고 생각했다.

 

 

지금 당장은 프로젝트를 하는데 필요한 지식도 공부하느라 바쁘지만 추후 코틀린도 제대로 공부를 해볼 예정이다!