Optional

๋ฐฑ๊ธฐ์„  ๋‹˜์˜ <๋” ์ž๋ฐ”, Java 8>๋ฅผ ๋ณด๊ณ  ๊ณต๋ถ€ํ•œ ๋‚ด์šฉ์„ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.

1. ํ•„์š”์„ฑ

  • ๊ฐœ๋ฐœ์ž์ธ ์šฐ๋ฆฌ๋Š” ์ข…์ข… null ์ฒดํฌ๋ฅผ ๊นœ๋นกํ•˜๊ฒŒ ๋œ๋‹ค. ๊ทธ๋ž˜์„œ NPE ๋ฅผ ๋งŒ๋“ค์–ด๋‚ด๊ธฐ๋„ ํ•œ๋‹ค.

  • java8๋ถ€ํ„ฐ๋Š” ์ด๋Ÿฌํ•œ ๋ถ€๋ถ„์„ ๋ณด์™„ํ•˜๊ธฐ ์œ„ํ•ด Optional ์ด๋ผ๋Š” ํƒ€์ž…์ด ์ถ”๊ฐ€๋˜์—ˆ๋‹ค.

2. ์ ์ ˆํ•œ ๊ฐ’์ด ์—†์„ ๊ฒฝ์šฐ, ๋ฆฌํ„ด ๋ฐฉ๋ฒ•๋“ค

๋ฉ”์†Œ๋“œ ์ž‘์—… ์ค‘์— ๊ฐ’์„ ์ œ๋Œ€๋กœ ๋ฆฌํ„ดํ•˜๊ธฐ ํž˜๋“  ๊ฒฝ์šฐ, ๊ฐœ๋ฐœ์ž๊ฐ€ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์€ ๋Œ€๋žต ๋‹ค์Œ๊ณผ ๊ฐ™์•˜๋‹ค.

  1. null ์„ ๋ฆฌํ„ดํ•œ๋‹ค.

    1. ๋น„์šฉ๋ฌธ์ œ๋Š” ์—†์ง€๋งŒ, ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ null ์ฒ˜๋ฆฌ ๋ถ€๋‹ด์„ ์ „๊ฐ€ํ•œ๋‹ค.

  2. ์˜ˆ์™ธ๋ฅผ ๋˜์ง„๋‹ค.

    1. ์ŠคํƒํŠธ๋ ˆ์ด์Šค๋ฅผ ์ฐ๊ฒŒ ๋˜๋ฏ€๋กœ ๋น„์šฉ์ด ๋น„์‹ธ๋‹ค.

  3. java 8 ๋ถ€ํ„ฐ๋Š” Optional ์„ ๋ฆฌํ„ดํ•œ๋‹ค.

    1. ๊ฐ’์ด ์žˆ์„์ˆ˜๋„ ์žˆ๊ณ , ์—†์„ ์ˆ˜๋„ ์žˆ์Œ์„ ๋ช…์‹œ์ ์œผ๋กœ ์•Œ๋ ค์ค€๋‹ค.

    2. ๋นˆ ๊ฐ’์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐ•์ œํ•œ๋‹ค.

3. Optional

  • ์˜ค์ง ๊ฐ’ ํ•œ๊ฐœ๊ฐ€ ๋“ค์–ด์žˆ์„์ˆ˜๋„ ์—†์„์ˆ˜๋„ ์žˆ๋Š” "์ปจํ…Œ์ด๋„ˆ"

4. ์ฃผ์˜ํ•  ๊ฒƒ

4-1. ๋ฆฌํ„ด ๊ฐ’์œผ๋กœ๋งŒ ์“ฐ๊ธฐ๋ฅผ ๊ถŒ์žฅํ•œ๋‹ค.

  • ๋งค์†Œ๋“œ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜ํƒ€์ž…, ๋งต์˜ ํ‚ค ํƒ€์ž…, ์ธ์Šคํ„ด์Šค ํ•„๋“œ ํƒ€์ž…์œผ๋กœ ์“ฐ์ง€ ์•Š๋Š”๋‹ค.

    • ๋งค๊ฐœ๋ณ€์ˆ˜ํƒ€์ž… : null ๊ฐ’์— ๋Œ€ํ•œ ์—ฐ์‚ฐ์ด ๋ฐœ์ƒํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์˜คํžˆ๋ ค ๋” ๋ฒˆ๊ฑฐ๋กญ๊ณ  ์œ„ํ—˜ํ•ด์ง„๋‹ค.

    • ๋งต์˜ ํ‚ค ํƒ€์ž… : ๋งต ์ธํ„ฐํŽ˜์ด์Šค์˜ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฒƒ์€ ํ‚ค๋Š” null ์ด ์•„๋‹ˆ๋ผ๋Š” ๊ฒƒ์ด๋‹ค. ์ด๊ฒƒ์— optional type ์ด๋ผ๋Š” ๊ฒƒ์€ ์•ˆ๋œ๋‹ค.

    • ์ธ์Šคํ„ด์Šค ํ•„๋“œ ํƒ€์ž… : ํด๋ž˜์Šค๋ฅผ ์ชผ๊ฐœ๋˜์ง€, ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์„ ์ด์šฉํ•œ๋‹ค.

public class Food {

    private int id;

    private String name;

    private boolean isLiked;

    private String bestRestaurantName;
    
    // return type ์œผ๋กœ๋งŒ ์‚ฌ์šฉํ•œ๋‹ค. 
    public Optional<String> getBestRestaurantName() {
        return Optional.ofNullable(bestRestaurantName);
    }
}

// ์•ˆํ‹ฐ ํŒจํ„ด
public class Food {

    private int id;

    private String name;

    private boolean isLiked;

    // 3. ์ธ์Šคํ„ด์Šค ํ•„๋“œ ํƒ€์ž…์œผ๋กœ ์“ฐ์ง€ ์•Š๋Š”๋‹ค. 
    private Optional<String> bestRestaurantName;
    
    public void setBestRestaurantName(Optional<String> bestRestaurantName) {
        // null ์— ๋Œ€ํ•ด์„œ ์ฒ˜๋ฆฌํ•  ๋•Œ NPE ๋ฐœ์ƒ ์šฐ๋ ค
        // ์˜คํžˆ๋ ค ๋” ๋ณต์žกํ•ด์ง„๋‹ค.  
        if (baseRestaurantName == null) {
            ...            
        }
    }
}

4-2. Optional ์„ ๋ฆฌํ„ดํ•˜๋Š” ๋ฉ”์†Œ๋“œ์—์„œ null ์„ ๋ฆฌํ„ดํ•˜์ง€ ๋ง์ž.

  • ์ฐจ๋ผ๋ฆฌ Optional.empty() ๋ฅผ ์ด์šฉํ•  ๊ฒƒ


public class Food {
    private int id;
    
    private String name;
    
    private boolean isLiked;
    
    private String bestRestaurantName;
    

    public Optional<String> getBestRestaurantName() {
        // return null; // ๊ธˆ์ง€!
        return Optional.empty();
    }
}

4-3. Primitive type ์„ ์œ„ํ•œ Optional ์ด ๋ณ„๋„๋กœ ์กด์žฌํ•œ๋‹ค.

  • OptionalInt, OptionalLong ...

  • ๋ฐ•์‹ฑ/์–ธ๋ฐ•์‹ฑ ๋น„์šฉ์„ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค.

@Test
void primitiveTypeOptionalTest() {
    OptionalInt optionalInt = OptionalInt.of(10);
    optionalInt.ifPresent(System.out::println);

    OptionalLong optionalLong = OptionalLong.of(10);
    if (optionalLong.isPresent()) {
        assertThat(optionalLong.getAsLong()).isEqualTo(10);
    }
}

4-4. Collection, Map, Stream Array, Optional ๋“ฑ ์ด๋ฏธ ์ปจํ…Œ์ด๋„ˆ ํƒ€์ž…์œผ๋กœ ์กด์žฌํ•˜๋ฉฐ ๋น„์–ด์žˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ๋Š” ํƒ€์ž…๋“ค์€ Optinal ๋กœ ๊ตณ์ด ๊ฐ์‹ธ์ง€ ์•Š๋Š”๋‹ค.

  • ์ปจํ…Œ์ด๋„ˆ๋กœ ๋‘ ๋ฒˆ ๊ฐ์‹ธ๋Š” ๊ผด์ด๋‹ค.

@Test
void optional์ด_ํ•„์š”์—†์„๋•Œ_๋‘๋ฒˆ๊ฐ์‹ธ์ง€_๋ง์ž() {
    Optional<Optional<String>>  opt = Optional.ofNullable(null);    // not recommended
    Optional<String> s = opt.get(); //NPE ๋ฐœ์ƒ
    String s1 = s.get();

    Optional<List<Food>> optionalFoods = Optional.of(asianFoods);   // not recommended
    asianFoods.isEmpty();                                           // recommended
}

5. ์ฐธ๊ณ 

6. Optional API Practice

public class OptionalApiTest {

    private List<Food> asianFoods = new ArrayList<>(){{
        add(new Food(1, "๋น„๋น”๋ฐฅ", true));
        add(new Food(2, "์„ค๋ ํƒ•", false));
        add(new Food(3, "๋ถˆ๋‹ญ๋ณถ์Œ๋ฉด", true));
        add(new Food(4, "๋–ก๋ณถ์ด", true));
        add(new Food(5, "๊ถ์ค‘๋–ก๋ณถ์ด", false));
        add(new Food(6, "์ˆœ๋Œ€๊ตญ", false));
    }};


    @Test
    void optionalOf๋ฅผ์ด์šฉํ•˜์—ฌ_Optional์„_๋งŒ๋“ค์ˆ˜์žˆ๋‹ค() {
        Optional<Food> optionalOf = Optional.of(new Food(1, "๋ผ๋ฉด", true));
        Optional<Food> optionalNullable = Optional.ofNullable(null);
        Optional<Food> optionalEmpty = Optional.empty();
    }

    @Test
    void optionalIsPresent๋ฅผ_์ด์šฉํ•˜์—ฌ_๊ฐ’์˜_์กด์žฌ์—ฌ๋ถ€๋ฅผ_ํ™•์ธํ•œ๋‹ค() {
        Optional<Food> optionalOf = Optional.of(new Food(1, "๋ผ๋ฉด", true));
        Optional<Food> optionalNullable = Optional.ofNullable(null);
        Optional<Food> optionalEmpty = Optional.empty();

        assertThat(optionalOf.isPresent()).isTrue();
        assertThat(optionalNullable.isPresent()).isFalse();
        assertThat(optionalEmpty.isEmpty()).isTrue();
    }

    @Test
    void optionalGet์„_์ด์šฉํ•˜์—ฌ_๊ฐ’์„_๊บผ๋‚ธ๋‹ค() {
        Optional<Food> optionalRamen = Optional.of(new Food(1, "๋ผ๋ฉด", true));
        if (optionalRamen.isPresent()) {
            Food ramen = optionalRamen.get();
            assertThat(ramen).isEqualTo(new Food(1, "๋ผ๋ฉด", true));
        }
    }

    @Test
    void null๊ฐ’์„_Getํ•˜๋ ค๊ณ ํ•˜๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() {
        Optional<Food> optionalNullable = Optional.ofNullable(null);
        assertThatThrownBy(() -> optionalNullable.get())
                .isInstanceOf(NoSuchElementException.class);
    }

    @Test
    void empty๊ฐ’์„_Getํ•˜๋ ค๊ณ ํ•˜๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() {
        Optional<Food> optionalEmpty = Optional.empty();
        assertThatThrownBy(() -> optionalEmpty.get())
                .isInstanceOf(NoSuchElementException.class);
    }

    @Test
    void ifPresentConsumer๋กœ_๋ฐ˜ํ™˜๊ฐ’_์—†์ด_ํ–‰์œ„๋ฅผ_์ˆ˜ํ–‰ํ•œ๋‹ค() {
        Optional<Food> optionalRamen = Optional.of(new Food(1, "๋ผ๋ฉด", true));
        optionalRamen.ifPresent(f -> System.out.println(f.getName()));
        //๋ผ๋ฉด
    }

    @Test
    void orElse๋Š”_๋ฌด์กฐ๊ฑด_์‹คํ–‰๋˜๋ฏ€๋กœ_์ด๋ฏธ_๋ฐ˜ํ™˜ํ• _๊ฐ’์ด_์žˆ์„๋–„_์‚ฌ์šฉํ•˜๋ฉด_์ข‹๋‹ค() {
        Optional<Food> optionalEmpty = Optional.empty();
        Food food = optionalEmpty.orElse(createNewFood());
        assertThat(food).isEqualTo(new Food(1, "๋ผ๋ฉด", true));

        System.out.println("=====");

        Optional<Food> optionalOf = Optional.of(new Food(1, "๋ผ๋ฉด", true));
        Food food2 = optionalOf.orElse(createNewFood());
        assertThat(food2).isEqualTo(new Food(1, "๋ผ๋ฉด", true));

        //new food created
        //=====
        //new food created
    }

    @Test
    void orElseGet์€_์—†์„๋•Œ๋งŒ_์‹คํ–‰๋˜๋ฏ€๋กœ_์ƒˆ๋กญ๊ฒŒ_๋งŒ๋“ค์–ด์ค„๋–„_์‚ฌ์šฉํ•˜๋ฉด_์ข‹๋‹ค() {
        Optional<Food> optionalEmpty = Optional.empty();
        Food food = optionalEmpty.orElseGet(() -> createNewFood()); //supplier
        assertThat(food).isEqualTo(new Food(1, "๋ผ๋ฉด", true));

        System.out.println("=====");

        Optional<Food> optionalOf = Optional.of(new Food(1, "๋ผ๋ฉด", true));
        Food food2 = optionalOf.orElseGet(OptionalApiTest::createNewFood);  //supplier
        assertThat(food2).isEqualTo(new Food(1, "๋ผ๋ฉด", true));

        //new food created
        //=====
    }

    @Test
    void orElseThrow๋Š”_์—†๋Š”๊ฒฝ์šฐ_์—๋Ÿฌ๋ฅผ_๋˜์ง„๋‹ค() {
        Optional<Food> optionalEmpty = Optional.empty();
        assertThatThrownBy(() -> optionalEmpty.orElseThrow(() -> new NullPointerException()))
                .isInstanceOf(NullPointerException.class);

        Optional<Food> optionalEmpty2 = Optional.empty();
        assertThatThrownBy(() -> optionalEmpty2.orElseThrow(NullPointerException::new))
                .isInstanceOf(NullPointerException.class);
    }

    @Test
    void filter๋ฅผ_์ด์šฉํ•˜์—ฌ_optional_๊ฐ’์„_๊ฑธ๋Ÿฌ๋‚ผ์ˆ˜์žˆ๋‹ค() {
        Optional<Food> first = asianFoods.stream()
                .filter(f -> f.getId() > 2)
                .findFirst();

        Food food = first.filter(f -> f.getId() == 3).orElseThrow();
        assertThat(food.getName()).isEqualTo("๋ถˆ๋‹ญ๋ณถ์Œ๋ฉด");
    }

    @Test
    void flatMap์œผ๋กœ_Optional์—_๋“ค์–ด์žˆ๋Š”๊ฐ’์„_๋ณ€ํ™˜ํ• ์ˆ˜์žˆ๋‹ค() {
        Optional<Food> first = asianFoods.stream()
                .filter(f -> f.getId() > 2)
                .findFirst();

        Optional<String> filtered = first.map(f -> f.getBestRestaurantName()).orElse(Optional.empty());
        assertThat(filtered).isEmpty();

        Optional<String> filtered2 = first.flatMap(f -> f.getBestRestaurantName());
        assertThat(filtered2).isEmpty();
    }


    private static Food createNewFood() {
        System.out.println("new food created");
        return new Food(1, "๋ผ๋ฉด", true);
    }
}

Last updated