자바 8 람다에서 checked exception을 어떻게 구현하면 좋을까?

2017-11-13 15:54

오늘 리뷰할 코드는 자바 8에서 람다를 사용하면서 checked exception 처리를 어떻게 구현하면 좋을지에 대한 내용이다. 람다를 쓰면 코드가 깔끔해 지는 것은 좋은데 checked exception 코드가 있는 경우 여간 짜증나지 않는다.

예제 코드는 다음과 같다.

public class ControllerScanner {
    private Map<Class<?>, Object> controllerMap = new HashMap<Class<?>, Object>();

    public Set<Class<?>> getControllerKeySet(){
        return controllerMap.keySet();		
    }

    public ControllerScanner(Object[] obj) {
        Reflections reflections = new Reflections(obj);			
        instantiateControllers(reflections.getTypesAnnotatedWith(Controller.class));
    }

    private void instantiateControllers(Set<Class<?>> annotated) {
        controllerMap.putAll(annotated.stream().collect(Collectors.toMap(a -> a, a -> {
          try { 
              return a.newInstance(); 
          } catch (Exception e) { 
              throw new ControllerInstantiationException(); 
          }
          }))
        );	
    }

    public Object getControllerInstance(Class<?> clazz) {
      return controllerMap.get(clazz);
    }
}

위 예제의 instantiateControllers 메소드에서 람다를 사용하고 있는데 checked exception 하나 때문에 코드가 상당히 지저분해지는 것을 볼 수 있다. 이 코드를 어떻게 개선하면 좋을까?

위 샘플 예제는 코드스쿼드 에서 새롭게 진행 중인 마스터즈 코스에서 발췌한 코드입니다. 코드스쿼드의 마스터즈 코스는 코드 리뷰 방식의 개인별 맞춤 학습 방법입니다.

0개의 의견 from FB

BEST 의견 원본위치로↓
2017-11-13 18:11

Handling checked exceptions in Java streams 문서에서 다루고 있는 다음 방법도 좋아 보이네요.

@FunctionalInterface
public interface FunctionWithException<T, R, E extends Exception> {
    R apply(T t) throws E;
}

위와 같이 FunctionalInterface 하나 추가한 후에 FunctionWithException<T, R, E extends Exception>을 Function<T, R>로 변환해 주는 메소드를 다음과 같이 하나 추가한다.

    private <T, R, E extends Exception> Function<T, R> wrapper(FunctionWithException<T, R, E> fe) {
        return arg -> {
            try {
                return fe.apply(arg);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        };
    }

위와 같이 구현하면 람다에서 다음과 같이 wrapper를 사용해 구현할 수 있겠네요.

    public String encodedAddressUsingExtractedMethod(String... address) {
        return Arrays.stream(address)
                .map(wrapper(s -> URLEncoder.encode(s, "UTF-8")))
                .collect(Collectors.joining(","));
    }

checked exception이 발생했을 때 별도의 로직 처리를 하지 않는 경우라면 위와 같이 RuntimeException으로 변경하는 방식으로 접근해도 좋겠다는 생각이 드네요.

물론 checked exception이 발생했을 때 별도의 예외 처리를 해야 한다면 메소드를 추출하는 것이 괜찮은 접근 방법인 것 같네요.

    private String encodeString(String s) {
        try {
            return URLEncoder.encode(s, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            // Exception에 따른 별도의 예외 처리한다.
            e.printStackTrace();
        }
        return s;
    }

    public String encodedAddressUsingExtractedMethod(String... address) {
        return Arrays.stream(address)
                .map(this::encodeString)
                .collect(Collectors.joining(","));
    }

3개의 의견 from SLiPP

2017-11-13 17:46

저는 FunctionalInterface 만들고, 구현체를 만들어서 exception을 처리해줬습니다.

@FunctionalInterface
public interface HandlerRuntimeException<T, R> {
	T handling(R param);
}
public class HandlerControllerInstantiationException implements HandlerRuntimeException<Object, Class<?>> {
	@Override
	public Object handling(Class<?> param) {
		try {
			return param.newInstance();
		} catch (Exception e) {
			throw new ControllerInstantiationException();
		}
	}
}
private void instantiateControllers(Set<Class<?>> annotated) {
		controllerMap.putAll(annotated.stream().collect(Collectors.toMap(a -> a, a -> new HandlerControllerInstantiationException())));
	}

인터페이스를 따로 생성하는 번거로움이 있긴 한데 코드 자체는 깔끔해지네요..

2017-11-13 18:11

Handling checked exceptions in Java streams 문서에서 다루고 있는 다음 방법도 좋아 보이네요.

@FunctionalInterface
public interface FunctionWithException<T, R, E extends Exception> {
    R apply(T t) throws E;
}

위와 같이 FunctionalInterface 하나 추가한 후에 FunctionWithException<T, R, E extends Exception>을 Function<T, R>로 변환해 주는 메소드를 다음과 같이 하나 추가한다.

    private <T, R, E extends Exception> Function<T, R> wrapper(FunctionWithException<T, R, E> fe) {
        return arg -> {
            try {
                return fe.apply(arg);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        };
    }

위와 같이 구현하면 람다에서 다음과 같이 wrapper를 사용해 구현할 수 있겠네요.

    public String encodedAddressUsingExtractedMethod(String... address) {
        return Arrays.stream(address)
                .map(wrapper(s -> URLEncoder.encode(s, "UTF-8")))
                .collect(Collectors.joining(","));
    }

checked exception이 발생했을 때 별도의 로직 처리를 하지 않는 경우라면 위와 같이 RuntimeException으로 변경하는 방식으로 접근해도 좋겠다는 생각이 드네요.

물론 checked exception이 발생했을 때 별도의 예외 처리를 해야 한다면 메소드를 추출하는 것이 괜찮은 접근 방법인 것 같네요.

    private String encodeString(String s) {
        try {
            return URLEncoder.encode(s, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            // Exception에 따른 별도의 예외 처리한다.
            e.printStackTrace();
        }
        return s;
    }

    public String encodedAddressUsingExtractedMethod(String... address) {
        return Arrays.stream(address)
                .map(this::encodeString)
                .collect(Collectors.joining(","));
    }
의견 추가하기