개발놀이터

코딩 스탠다드 2 : Optional 을 활용한 null 체크 본문

리팩토링/코딩 스탠다드

코딩 스탠다드 2 : Optional 을 활용한 null 체크

마늘냄새폴폴 2024. 4. 29. 22:18

이번엔 Optional을 활용해 null 체크를 하는 코딩 스탠다드에 대해서 포스팅해볼까합니다. 

 

Optional에 대한 더 자세한 내용은 망나니개발자님 포스팅을 참고해주세요! 아래의 포스팅이 Optional에 대해서 잘 설명이 되어있습니다. 

 

https://mangkyu.tistory.com/203

 

[Java] 언제 Optional을 사용해야 하는가? 올바른 Optional 사용법 가이드 - (2/2)

앞선 포스팅에서는 Optional의 개념과 문법을 살펴보았습니다. Optional은 Null이 될 수 있는 객체를 감싸는 Wrapper 클래스이기 때문에 비용이 발생합니다. 그래서 Optional은 필요한 경우에만 사용하는

mangkyu.tistory.com

 

우리는 상황을 하나 가정하고 Optional을 이용해 null 체크를 진행해보도록 하겠습니다. 

 

상황1

회원가입중 로그인 아이디의 중복체크를 해야하는 상황

 

우리는 다음과 같은 흐름으로 진행할겁니다. 

 

  1. 회원 데이터베이스에서 로그인 아이디로 회원을 조회한다.
  2. 존재한다 = 중복 => 예외를 던진다 / 존재하지 않는다 = 중복X 회원가입 가능 => 회원가입 로직

 

이제 흐름을 이해했으니 코드로 바로 들어가보겠습니다. 

 

        Member findMember = memberRepository.findByLoginId(loginId);

        if (member == null) {	// 중복 아님 = 회원가입 가능
            Member member = new Member();
            memberRepository.save(member);
        }
        else {
            throw new IllegalStateException("중복된 아이디입니다.");
        }

 

값이 존재하는 경우 예외를 터트려야 하기 때문에 Optional의 ifPresent() 메서드를 이용해 예외를 처리하면 됩니다. 

 

        Optional<Member> findMember = memberRepository.findByLoginId(loginId);

        findMember.ifPresent((member) -> {
        	throw new IllegalStateException("중복된 아이디입니다. 아이디 : " + member.getLoginId());
        });
        
        Member member = new Member();
        memberRepository.save(member);

 

코드도 굉장히 직관적이라 이해하기도 쉽습니다."회원을 찾고 만약 존재하면 예외를 터트려라" 이렇게 영어를 읽듯이 코드를 해석할 수 있습니다. 

 

하지만 이건 값이 존재하면 예외를 터트리는 것이죠. 만약 값이 존재하지 않으면 안되는 상황에선 어떻게 해야할까요? 

 

상황2

로그인 후 내 정보 보기를 하면 보여주는 정보들을 화면에 뿌려줘야한다. 

 

우리는 다음과 같은 흐름도를 가지고 진행할겁니다. 

 

  1. 현재 로그인한 아이디를 기반으로 회원을 검색한다. 
  2. 조회된 회원정보를 바탕으로 화면에 회원의 이름을 뿌려준다.

 

        Member findMember = memberRepository.findByLoginId(loginId);

        String memberName = findMember.getName();

        return memberName;

 

끝일까요? 

 

만약 findMember 가 null 인 경우엔 getName() 을 하는 순간 NullPointerException이 터지게 됩니다. 

 

이를 방지하기 위해선 어떻게 해야할까요? 

 

        Optional<Member> findMember = memberRepository.findByLoginId(loginId);

        Member optionalMember = findMember.orElseThrow(() -> new IllegalStateException("존재하지 않는 회원입니다."));
        
        String memberName = optionalMember.getName();
        
        return memberName;

 

이렇게 깔끔하게 코드를 변경할 수 있습니다. 

 

상황3

이렇게 객체를 하나만 반환하는 경우엔 처리하기 쉽지만 만약 List로 반환되는 경우엔 어떻게 처리해야할까요? 

 

망나니개발자님 포스팅에선 Optional<List<Member>> 와 같은 방식으로 Collection 을 Optional에 감싸지 말라고 나와있습니다. 그럼 어떻게 처리하죠? 

 

List<Bucket> myBucket = bucketRepository.findByLoginId(loginId);

 

이런 경우 저는 저희 팀 팀원들에게 두가지 방법을 제시합니다. 

 

List<Bucket> myBucket = bucketRepository.findByLoginId(loginId);

for (Bucket bucket : myBucket) {
	// 로직
}

 

이렇게 처리하면 List가 null인 경우 for 문 자체가 돌지 않기 때문에 넘어갈 수 있습니다. 이는 용인가능한 정도이지 바람직한 것은 아니라고 생각합니다. 저는 이런 경우 Utility 클래스를 두어서 빈 리스트 객체를 반환하도록 Wrapper 클래스를 둡니다. 

 

public class ListUtils {
	
    public List<Bucket> getMyBucket() {
    	List<Bucket> myBucket = bucketRepository.findByLoginId(loginId);
        
        if (myBucket == null) {
        	return Collections.emptyList();
        }
        else {
        	return myBucket;
        }
    }
}

 

그리고 이 유틸리티 클래스를 가지고 빈리스트를 반환하게 하여 null과 관련된 예외들 (특히 NPE) 을 처리할 수 있도록 설계할 수 있습니다. 

 

 

상황4

만약 객체가 초기값을 가져야 하는 상황이면 어떻게 처리할 수 있을까요? 

 

지금 마땅히 예시가 생각나진 않지만 이런 경우가 종종 있었습니다. 데이터베이스에서 단일 객체를 조회하면서 없으면 초기값을 세팅해줘야 하는 경우엔 도메인에 초기값에 해당하는 값을 세팅해주고 Optional에 orElseGet을 써주면 됩니다. 

 

class Member {
	
    // 필드들
    
    public Member init() {
    	this.name = "홍길동";
        this.age = 27;
        // etc...
        return this;
    }
}

 

마땅한 예시가 떠오르지않아 회원객체로 하는 점 양해부탁드립니다. 

 

class MemberService {
	
    private final MemberRepositroy memberRepository;
    
    public Member method(String loginId) {
    	Member findMember = memberRepository.findByLoginId(loginId);
        
        if (findMember == null) {
        	return findMember.init();	// NPE 발생!
        }
        else {
        	return findMember;
        }
    }
}

 

저기서 NPE가 발생하므로 Member 객체를 하나 더 만들어줘야하죠. 

 

class MemberService {

    private final MemberRepositroy memberRepository;

    public Member method(String loginId) {
        Member findMember = memberRepository.findByLoginId(loginId);

        if (findMember == null) {
            Member member = new Member();
            return member.init();
        }
        else {
            return findMember;
        }
    }
}

 

이렇게 했을 경우 지금 당장 생각나는 이슈는 GC문제입니다. 지금 Member가 두개 만들어진 상태입니다. findByLoginId 를 호출할 때 하나, 만약 null 일 때 초기화를 위한 객체 하나.그럼 GC가 관리해야할 객체가 두개인 것이죠. 

 

이런게 한두개 쌓이다보면 무시못할 것 같다는 생각이 듭니다. 

 

그래서 우리는 Optional에 orElseGet 이라는 메서드를 사용하면 됩니다. 

 

class MemberService {

    private final MemberRepositroy memberRepository;

    public Member method(String loginId) {
        Member findMember = memberRepository.findByLoginId(loginId).orElseGet(() -> {
            Member member = new Member();
            return member.init();
        });

        return findmember;
    }
}

 

orElseGet의 특징은 만약 Optional의 객체가 있다면 해당 객체를 사용하고 없다면 orElseGet에 적혀있는 초기 객체를 세팅하도록 되어있습니다. 

 

Optional의 orElse와의 차이점은 orElse는 값이 있던 없던 초기 객체를 만들지만 orElseGet은 객체가 없을 때만 객체를 생성한다는 특징이 있습니다. 

 

때문에 저는 orElse 보단 orElseGet 을 더 많이 사용하는 것 같습니다. 

 

마치며

이렇게 Optional을 이용해 null 체크를 하면서 객체를 다루는 네가지 상황을 포스팅해봤습니다. 어쩌다보니 Optional에 대한 문법 포스팅이 된 것 같네요. 

 

이렇듯 Optional을 사용하면 존재하면 안되는경우, 존재해야하는 경우 모두 예외를 터트리면서 처리할 수 있습니다. 

 

이렇게 예외를 터트리게 되면 프론트에서 이 예외를 그대로 받아서 메시지를 띄워주면 됩니다. 

 

만약 프론트가 없고 jsp나 thymeleaf와 같은 템플릿 엔진으로 프론트를 구성하고 있다면 처리하기 까다로운 것은 사실입니다.

 

이런 예외를 던지고 처리하는 과정은 프론트와 백을 분리하는 것이 편해서 저는 jsp로 개발한다고 해도 비동기 통신으로 (AJAX) 프론트와 백을 분리하는 것을 선호합니다. 

 

이번 포스팅으로 NPE와 안녕하는 삶을 만끽하시길 바라겠습니다.