20 Oct 2017

함수형 프로그래밍의 예외처리

Java의 점검 예외나 미점검 예외(런타임 예외)는 매번 적절하게 다뤄줘야합니다. 점검 예외는 try…catch 구문을 사용하여 다룰 수 있으며, 런타임 예외는 컴파일 시점에 검증되지 않기 때문에 프로그램을 실패하게 할 수 있습니다. 런타임 예외는 코딩하는 단계에서 API 문서를 기반으로 API 올바르게 사용해야 해결할 수 있습니다. 링크에 점검 예외와 런타임 예외에 대한 설명이 있습니다.

함수형 프로그래밍에서 자바의 예외는 부수 효과를 발생 시키는 표현식일 뿐입니다. 참조 투명성을 해치게 될 경우 함수형 프로그래밍의 장점을 취할 수 없게됩니다.

1. 참조 투명성

검증하는 예제는 다음과 같습니다.

// 아래는 Supplier와 파라미터 문자열을 연결하여 반환하는 프로그램이다.

import java.util.function.Supplier;
Supplier<String> contentSupplier = () -> {
    int i=0;
    if(i==0) throw new RuntimeException(); 
    else return "content";
};

String concat(String suffix){
    String original = contentSupplier.get();
    try{
        return original + suffix;
    }catch(Exception e){
        return "";
    }
}
concat("terminated character");
//java.lang.RuntimeException thrown: 출력


// 아래는 참조 표현식이 참조 투명한지 판단하기위해 original을 평가 결과로 치환한 프로그램 입니다.

import java.util.function.Supplier;

Supplier<String> contentSupplier = () -> {
    int i=0;
    if(i==0) throw new RuntimeException(); 
    else return "content";
};

String concat(String suffix){
//    String original = contentSupplier.get();

    try{
        return contentSupplier.get() + suffix;
    }catch(Exception e){
        return "exception is caught";
    }
}
concat("terminated character");
//"exception is caught" 반환

위와 같이 일부 표현식만 치환해보아도 concat 함수는 참조 투명하지 않다는 걸 확인할 수 있습니다. 이는 예외가 문맥 의존적이기(context dependent) 때문입니다.

예외는 크게 2가지 문제를 갖고 있습니다.

  1. 위 예시처럼 참조투명성을 위반하고 문맥 의존성을 도입합니다.
  2. 형식에 안전하지 않습니다. concat의 메서드 시그니처만 봐서는 어떤 예외를 던질 지 알 수 없습니다.

2번이 점검 예외라서 해결되더라도 try…catch와 같은 판에 박힌 코드를 작성해야할 뿐만 아니라, 런타임 예외의 경우 잘 다룰 수 없습니다. 특히 일반적인 고차함수는 전부 람다식을 받는데 람다식은 점검예외를 발생시키지 않으므로 컴파일 타임에 점검되지 않습니다. (일반적인 자바의 function 패키지 클래스들은 점검 예외를 던지는 것은 허용하지 않습니다.)

자주 사용되는 해결방법은 경계값이나 null을 반환해서 해결하는 것 입니다. 예를들어 0으로 나눌 수 없을 경우 null이나 -1을 반환하는 것 입니다.

위의 방법은 몇가지 문제를 갖고 있습니다.

  • 먼저 함수에서 정상적으로 처리되었는지 확인할 수 없습니다.
  • 오류가 소리소문 없이 전파됩니다.
  • 경계값이나 null 둘다 적용 안되는 경우가 있습니다.
  • 호출자에게 위와 같은 특별한 규약을 전달해야합니다.

경계값을 사용할 수 없는 대표적인 예시는 다음과 같습니다.

// 리스트가 비어 있을때 반환할 수 있는 경계값이 없습니다.

import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
int mean(List<Integer> l){
    if(l.size() == 0){
        return 0;// ????

    }else if(l.size() == 1){
        return l.get(0);
    }else{
        int s = 0;
        Iterator<Integer> iter = l.iterator();
        while(iter.hasNext()){
            Integer i = iter.next();
            s = s + i;
        }
        return s / l.size();
    }
}
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(1000);
mean(list);

2. 함수형 해결방법

실제로 함수형 언어(스칼라)에서는 프로그램이 처리할 수 없을때나 예외가 발생했을때 사용할 수 있는 특별한 타입을 제공합니다.

  • Option[R]: 입력을 처리할 수 없을때 사용할 수 있는 타입(자바의 Optional)
  • Either[T,R]: 어떤 예외가 있는지 표현할 수 있는 타입

이러한 타입은 map(), flatMap(), filter(), getOrElse() 등의 고차함수를 통해 예외 처리를 통합할 수 있습니다.

장점은 다음과 같습니다.

  • 타입에 안전합니다. 따라서 개발자는 이를 인지하게되고 오류가 줄어듭니다.
  • 오류가 발생하지 않았다는 연속하여 가정하에 계산 로직을 합성할 수 있습니다. getOrElse()을 활용하여 필요한 경에만 오류에 대한 처리를 할 수 있습니다.

전통적인 예외처리 방식으로는 자연스럽게 처리할 수 없습니다.

Optional을 사용하는 예시입니다.

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
class Employee{
    public String name;
    public String dept;
    public Employee(String name, String dept){
        this.name = name;
        this.dept = dept;
    }
}

Employee e = new Employee("Changwon", "IT");
Employee fallback = new Employee("", "");
List<Employee> l = new ArrayList<>();
l.add(e);

Optional<Employee> findByName(String name){
    int size=l.size();
    for(int i=0; i<size; i++){
        if(l.get(i).name.equals(name))
            return Optional.of(l.get(i));
    }
    return Optional.empty();
}

findByName("Changwon").filter((e) -> e.dept.equals("HR")).orElse(fallback).name;

findByName("Changwon").filter((e) -> e.dept.equals("IT")).orElse(fallback).name;

참조

  • 함수형 프로그래밍 : 스칼라로 배우는 함수형 프로그래밍(서적)

Tags:
0 comments