함수형 프로그래밍에서 에러 처리는 어떻게 하는 것이 좋을까?

2016-08-25 10:04

자바로 구현하다보면 Exception을 많이 사용한다. 즉, 다음과 같은 코드 구현이 자연스럽다.

public boolean canDelete(User loginUser) throws CannotOperateException {
    if (!writer.isSameUser(loginUser)) {
        throw new CannotOperateException("다른 사용자가 쓴 글을 삭제할 수 없습니다.");
    }
    
    if( answers.stream().filter(a -> !a.isSameUser(loginUser)).count() > 0 ) {
        throw new CannotOperateException("다른 사용자가 추가한 댓글이 존재해 삭제할 수 없습니다.");
    }
    
    return true;
}

위와 같은 코드를 함수형 프로그래밍 스타일로 구현하고 싶은데 막막하다. 함수형 프로그래밍에서는 Exception을 throw하는 것도 side effect의 하나로 생각하는 것으로 알고 있는데 그렇다면 어떻게 처리하는 것이 좋을까? Exception을 사용하지 않고 반환값에 error code나 메시지를 담아서 반환하는 형태로 구현해야 하나?

함수형 프로그래밍의 에러 처리 방식을 자바에 적용해보면 재미있겠다. 근데 가능하려나?

0개의 의견 from FB

12개의 의견 from SLiPP

2016-08-25 10:26

저는 읽다가 이게 Exception 처리가 되어야 하는지 의문이 듭니다. Validation 또는 권한 성격의 메서드인데 어떤 계층에서 사용하느냐에 따라서 다를수도 있겠네요.

Controller 계층에서 사용한다면 Try, Catch를 사용해야 하는 불편함, 지저분함도 있고 Exception을 던져야 하는 기준도 모호해 지는거 같습니다.

Service 계층에서 사용한다면 Controller 계층에서 validation 체크를 한것을 한번더 이중으로 체크를 해야 하는가에 대한 고민이 앞설거 같습니다.

저는 화면단 UI 또는 메시지 처리를 위해 validation 또는 권한 체크 메서드로 묶어서 코드로 처리 합니다.

2016-08-25 10:53

스칼라 같은 경우에는 정상적으로 처리된 값이 없는 경우가 있을 수 있다는 것을 암시하는 자료형을 반환하게 하면 되는데 함수형 언어가 아닌 프로그래밍 언어에서는 어떻게 하면 될지 판단이 안서네요. 스칼라에서 제공하는 그런 클래스를 만들어서 사용하면 될까요? Option이나 Either 혹은 Try 같은 클래스들...

2016-08-25 11:50

@eungju.park.1 나도 자바에 Either와 Validation이 있는지 물어볼려고 했는데 직접 만들어 써야 하는구나. 기존에 구현해 놓은 코드 공개해도 되면 나와 다른 개발자들을 위해 공유 좀 해주라.

2016-08-25 12:22

@자바지기 Either은 아래와 같이 구현했어요. 아마 Rust나 스칼라 구현을 참조했을거예요. Validation은 어디있는지 뒤져봐야겠네요.

import java.util.function.Function;

public interface Result<O, E> {
    static <O, E> Result<O, E> error(final E value) {
        return new Result<O, E>() {
            public <R> Result<R, E> mapOk(Function<O, R> f) {
                return error(value);
            }

            public O orElse(Function<E, O > f) {
                return f.apply(value);
            }

            public <R> R either(Function<O, R> f, Function<E, R> g) {
                return g.apply(value);
            }

            @Override
            public <R> Result<R, E> andThen(Function<O, Result<R, E>> f) {
                return error(value);
            }
        };
    }

    static <O, E> Result<O, E> ok(final O value) {
        return new Result<O, E>() {
            public <R> Result<R, E> mapOk(Function<O, R> f) {
                return ok(f.apply(value));
            }

            public O orElse(Function<E, O> f) {
                return value;
            }

            public <R> R either(Function<O, R> f, Function<E, R> g) {
                return f.apply(value);
            }

            @Override
            public <R> Result<R, E> andThen(Function<O, Result<R, E>> f) {
                return f.apply(value);
            }
        };
    }

    <R> Result<R, E> mapOk(Function<O, R> f);

    O orElse(Function<E, O> f);

    <R> R either(Function<O, R> f, Function<E, R> g);

    <R> Result<R, E> andThen(Function<O, Result<R, E>> f);
}
2016-08-25 14:49

@eungju.park.1 공유한 Result 활용해서 다음과 같이 구현해봤다.

class Question {
    public Result<Question, String> delete(User loginUser) {
        if (!writer.isSameUser(loginUser)) {
            return Result.error("다른 사용자가 쓴 글을 삭제할 수 없습니다.");
        }
        
        if( answers.stream().filter(a -> !a.isSameUser(loginUser)).count() > 0 ) {
            return Result.error("다른 사용자가 추가한 댓글이 존재해 삭제할 수 없습니다.");
        }
        
        return Result.ok(this);
    }
}

다음은 Question의 delete() 메소드를 호출하는 QnaService 코드이다.

@Service
@Transactional
public class QnaService {
    @Autowired
    private QuestionRepository questionRepository;

    public Result<Question, String> deleteQuestion(long questionId, User user) {
        Optional<Question> question = questionRepository.findOne(questionId);
        if (!question.isPresent()) {
            return Result.error("존재하지 않는 질문입니다.");
        }
        
        Result<Question, String> result = question.get().delete(user);
        return result.either(q -> {
            questionRepository.delete(q);
            return Result.ok(q);
        }, e -> Result.error(e));
    }
}

Exception을 사용하는 경우 항상 반환 값에 대해 명확히 하는 것이 쉽지 않았는데 이와 같이 구현하니 반환 값도 명확하니 나름 깔끔해졌다.

2016-08-25 22:36
Answer a = post.whynotdelete(user);
if (a == Answer.notme) ... ;
else if (a == Answer.hasotherscomments) ... ;
elde if (a == Answer.ucan) service.delete(post);
else ... ;

그냥 자바코드에서 boolean 리턴이 없어도 될거 같아서 다른 의견을 내봣어요. 예외를 던질지 코드를 리턴할지에 대한 결정은 낫프라이스의 의견에 저는 동의하며 비즈니스가 잘보이는 쪽으로 결정하는 것이 좋다고 생각해요~ 다행히 제가 만든 코드도 사이드 이펙트는 없겠죠?

2016-08-26 10:03

@benghun 좋은 의견 감사해요. 오랜만에 이야기 나누네요. 잘 지내시죠?

"비즈니스가 잘보이는 쪽으로 결정하는 것이 좋다."는 것에 적극 공감합니다. FP를 극단으로 추가하면 코드가 간결해지는 면은 있지만 비지니스가 잘 보이지 않는 경우도 많아서 좀 고민이 되기는 해요.

의견 추가하기