Optional<T>제네릭 클래스로 T 타입의 객체를 감싸는 Wrapper 클래스이다. 최종 연산의 결과를 Optional 객체에 담아서 반환하면 반환된 결과가 null인지 체크할 필요 없이(NullPointerException의 위험 없이) 간단하고 안전하게 사용할 수 있다. (자바의 정석 p835)

 

1. Optional 객체를 생성할 때는 of() 대신 ofNullable()을 사용

 

Optional을 사용하는 방법과 주의 사항을 간단하게 정리하면 먼저 Optional 객체를 생성할 때는 of() 또는 ofNullable()을 사용한다.

말 그대로 Nullable은 null을 허용하기 때문에 참조변수가 null의 가능성이 있으면 ofNullable()로 생성하는 것이 좋지만 Optional을 사용하는 것 자체가 null이 올 수 있기 때문에 사용하는 것이라 그냥 ofNullable()로 사용하는 것이 좋다.

 

2. Optional 객체의 값을 가져올 때는 get(), orElse() 대신 orElseGet()을 사용

 

Optional 객체의 값을 꺼낼 때는 get(), orElse(), orElseGet(), orElseThrow()를 사용하면 되는데 먼저 get()은 값이 null일 경우 NoSuchElementException이 발생하기 때문에 사용하지 않는 것이 좋다.

 

orElseThrow()는 예외를 던지는 것이니 그렇다 치고 orElse(), orElseGet()은 뭔가 비슷해서 무슨 차이가 있나 찾아보았다.

 

orElse() vs orElseGet()

  • orElse(): returns the value if present, otherwise returns other
  • orElseGet(): returns the value if present, otherwise invokes other and returns the result of its invocation

설명을 보면 둘다 만약 값이 존재하지 않을때 다른 other를 반환한다고 나와있지만 테스트를 해보면 orElse()의 경우에는 값이 존재할 때도 항상 호출이 된다. null이 아니더라도 계속 객체가 생성되고 있는 것이다. 그리고 메서드가 DB에 저장하는 로직이 있는 경우에는 더 큰 문제가 생길 수 있다. 그래서 성능상 orElseGet()을 사용하는 것이 더 나은 선택이다.

 

JMH로 간단하게 테스트 해봤는데 계속 이전에 테스트하던거랑 같이 실행이 되서 쩔쩔 메다가 OptionsBuilder()로 급하게 처리했는데 Logger도 뭐가 문제인지 제대로 안 되고 가볍게 해보려던 거에 시간을 많이 쓰니 멘탈이 흔들린다. (ㅠㅠ) 얕게 알면 이래서 위험하다. 다시 공부를 해봐야겠다.

 

 

 

JMH(Java Microbenchmark Harness) 사용법

개발을 진행하다가 보면, 성능문제를 해결해야 하는 경우는 매우 많다. 성능 문제를 해결하기 위해서는 우선 성능을 측정해야하며, 성능을 측정하는 방법와 도구는 셀수도 없이 많다. 대부분의

ysjee141.github.io

 

자바 Optional: 5. Optional 톺아보기

Optional 클래스를 의도에 맞게 잘 사용하려면 어떻게 해야할까?

madplay.github.io

 

Java Optional - orElse() vs orElseGet() | Baeldung

Explore the differences between Optional orElse() and OrElseGet() methods.

www.baeldung.com

 

Optional 과 null 에 대해 ⌥␀

런타임에서 발생하는 NullPointException 방어를 위해 만들어둔 로직체크는 코드의 가독성과 유지 보수성이 떨어진다. 어떻게 null…

tecoble.techcourse.co.kr

 

 

'Raw use of parameterized class 'List' 라는 경고를 보고 Raw use가 무엇인지 찾아봤는데 이펙티브 자바에 있는 내용이라 아직 초반부를 보고 있지만 해당 부분을 읽어보았다.

 

제네릭 타입은 일련의 매개변수화 타입(parameterized type)을 정의하는데 List<String>의 경우 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입인 것이다.

 

Raw Type이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말하는데 아래와 같이 원소의 타입을 정의하지 않고 List를 그대로 사용한 것이다.

 

List<String> listA = new ArrayList<>();

//Raw use of parameterized class 'List'
List listB = new ArrayList<>();

 

Raw Type은 제네릭이 도입되기 이전에 수 많은 코드들과 호환되도록 하기 위해 있는 것인데 가장 큰 문제는 타입 오류를 런타임에서야 발견할 수 있는 것이다.

 

제네릭을 활용하면 엉뚱한 타입의 인스턴스를 넣으려 할 때, 컴파일 오류가 발생해서 런타임 되기 전에 오류를 알아차릴 수 있다. Raw Type을 쓰면 제네릭이 주는 안전성과 표현력을 잃게 되는 것이다.

 

제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않으면 Raw Type보다 비한정적 와일드 카드를 사용하는 것이 좋다.

 

예외적으로 class 리터럴에는 매개변수화 타입을 사용할 수 없기 때문에 List.class 형태의 Raw Type으로 사용해야 하며, instanceOf 연산자도 런타임시에는 제네릭 정보가 지워지기 때문에 매개변수화 타입에는 적용할 수 없어 다음과 같이 검사 형변환(checked cast)으로 사용하는 것이 좋다.

 

if (o instanceof Set) {
    Set<?> s = (Set<?>) o;
    ...
}

 

아직 제네릭에 대한 공부가 더 필요하지만 Raw Type으로 선언 시 컴파일시에 문제점을 발견할 수 있는 제네릭의 장점을 살리지 못하고 런타임 오류가 발생할 수 있는 위험에 대해 알 수 있었다.

HashMap을 사용할 때, put(key, value)으로 데이터를 저장할 수 있는데 HashMap의 경우 key 값이 존재할 경우 value 값을 대체하고 기존의 value 값을 반환해준다. 하지만 key 값이 존재하지 않는 경우에는 새로 데이터를 저장하고 null을 반환한다. (기존 value 값이 없기 때문)

 

put()으로 데이터를 저장하고 반환된 value 객체를 참조할려고 하니 NullPointerException이 발생한다는 경고를 보고 어떻게 구현이 되어있나 확인해보았다.

 

HashMap에서 해싱이란 해시 함수를 이용해서 데이터를 해시테이블에 저장하고 검색하는 방식을 말하는데 해시함수가 데이터가 저장되어 있는 곳을 알려주기 때문에 다량의 데이터 중에서 원하는 데이터를 빠르게 찾을 수 있다.

해싱에서 사용하는 자료구조는 배열과 링크드 리스트의 조합으로 되어 있는데 key 값을 해시함수에 넣으면 배열의 한 요소를 얻게 되고, 다시 그 곳에 연결되어 있는 링크드 리스트에 저장이 된다.

해시함수의 계산 결과인 해시코드로 해당 값이 저장되어 있는 링크드 리스트를 찾고, 링크드 리스트에서 검색한 키와 일치하는 데이터를 찾는다. (링크드 리스트는 검색에 불리하기 때문에 해시코드가 서로 중복되지 않도록 하는 전략이 좋다.)

 

HashMap의 경우 Object 클래스에 정의된 hashCode()를 해시 함수로 사용하는데 이는 객체의 주소를 이용하는 알고리즘으로 모든 객체에 대해 hashCode() 결과가 서로 유일하게 된다. 

 

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

 

그래서 실제 HashMap의 put() 메서드를 보면 putVal() 메서드를 호출하면서 해시함수 hash(key)를 실행하고 해시 코드를 넘겨준다.

 

 

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
	    ...
        
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

 

putVal() 메서드의 일부를 보면 key가 존재하는지 확인을 하고 value를 교체한 뒤 oldValue를 반환하거나 그렇지 않을 경우 null을 반환한다.

 

 

Stack 자료구조의 특정 객체를 파라미터로 받아서 해당 요소가 있으면 위치를 반환해주는 search() 메서드를 구현해보던 중 equals()로 비교를 할 때, 전달 받은 파라미터가 null인 경우를 따로 체크하는 부분이 있었다.

 

public synchronized int lastIndexOf(Object o, int index) {
    if (index >= elementCount)
        throw new IndexOutOfBoundsException(index + " >= "+ elementCount);

    // Obect o 객체가 null 인 경우
    if (o == null) {
        for (int i = index; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = index; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

 

equals()를 사용할 때는 주의해야 될 부분이 있는데 값을 비교할 때 앞에 null인 객체가 들어올 경우 NullPointerException 예외가 발생하는 것이다. 

public int lastIndexOf(Object value) {
    int index = array.length - 1;
    for (int i = index; i >= 0; i--) {
        // value가 Null인 경우
        if (value.equals(array[i])) {
            return i;
        }
    }
    return -1;
}

 

객체 참조를 하기 때문에 당연한 부분이지만 전에도 이런 실수를 많이 했던 것 같다. 그래서 객체와 문자열의 값을 비교하는 경우에는 문자열을 왼쪽에 입력하고, 객체끼리 비교하는 경우에는 null 값을 고려해서 코드를 짜도록 해야겠다.

+ Recent posts