Spring Boot 람다식 적용(Feat. 메서드 참조)

최민길(Gale)·2023년 6월 28일
1

Spring Boot 적용기

목록 보기
32/46

안녕하세요 오늘은 람다식과 메서드 참조 방식을 이용해서 코드를 가독성있게 줄여보는 작업을 진행하도록 하겠습니다.

람다식에 대해 알아보기 전 익명 함수에 대해 먼저 알아보아야 합니다. 익명 함수란 이름이 없는 함수로서, 함수의 정의와 동시에 사용되는 함수입니다. 익명 함수는 주로 함수 자체를 매개변수로 참조하고자 할 때 사용됩니다. 예를 들면 a+b를 수행하는 함수 자체를 매개변수로서 넣고 싶을 때 이 기능을 구현하는 별도의 클래스나 메소드를 만들지 않고 직접 매개변수에서 연산을 진행하는 방식으로 활용합니다.

람다식은 이런 익명 함수를 가독성있게 줄여주는 역할을 합니다. 아래의 예시를 보면서 설명을 해보겠습니다. 아래는 Mdc 키값 정보가 담긴 Enum 내부입니다. 보시는 것처럼 람다식 적용 이전에는 메소드를 인터페이스에서 오버라이드해서 직접 하나하나 구현하다보니 불필요한 반복되는 코드가 발생하며 이로 인해 가독성이 현저히 떨어지게 됩니다.

// 람다식 적용 이전
    REQUEST_URL{
        @Override public String get() { return MDC.get("request_url");}
        @Override public void add(HttpServletRequest request){ MDC.put("request_url", request.getRequestURI()); }
        @Override public void add(BindingResult bindingResult) { logger.error("wrong access"); }
        @Override public void remove(){ MDC.remove("request_url"); }
        @Override public void log(){ logger.info("request_url : " + MDC.get("request_url")); }
    }, ...

이를 해결하기 위해 람다식을 적용했습니다. 위에서 언급한 것처럼 함수 자체를 매개변수로서 넣고 싶을 때 사용하기 때문에 이전 방식처럼 메소드를 정의하는 대신 생성자를 통해 넣고자 하는 함수 자체를 람다식으로 정의해줍니다.

그럼 여기서 한 가지 의문이 생길겁니다. 오버라이딩 하지 않고 함수 자체를 넣는다고 한다면 함수는 어디서 정의할 것인가? 바로 자바에서 자체적으로 제공하는 함수형 인터페이스를 사용합니다. 함수형 인터페이스란 단 1개의 추상 메소드만을 갖는 인터페이스입니다. 람다식의 특성상 파라미터의 타입을 생략하기 때문에 여러 개의 메소드가 존재하면 람다식이 정의될 수 없습니다. 따라서 메소드가 1개인 함수형 인터페이스를 직접 생성하여 오버라이딩하거나(이 때 @FunctionalInterface를 인터페이스 위에 붙여주어야 합니다.), 또는 자바에서 기본적으로 제공하는 함수형 인터페이스를 사용합니다. 하지만 자바에서 제공하는 함수형 인터페이스 수가 충분히 많아 주로 이를 이용하는 편입니다.

아래 보시면 Supplier, Consumer, Runnable이라는 자바에서 제공하는 함수형 인터페이스를 정의한 뒤 해당 함수를 생성자로서 사용합니다. 그 후 원소들을 초기화 시 "(파라미터) -> 수행 메소드"의 형식으로 원하는 로직을 작성합니다.

각 인터페이스를 각각의 특징이 있습니다. 우선 Supplier<T>의 경우 매개변수를 받지 않고 T 타입을 리턴합니다. supplier.get()을 이용하여 초기화하며 매개변수가 없기 때문에 () -> 수행 메소드 의 형식으로 진행합니다. Consumer<T>의 경우 T를 매개변수로 void 메소드를 반환합니다. accept(T)를 이용하여 초기화하고 (T) -> 수행 메소드(리턴값 void) 의 형식으로 메소드를 작성합니다. Runnable의 경우 매개변수도, 리턴 타입도 없는 경우 사용합니다. .run()을 이용하여 초기화하고 () -> 수행 메소드(리턴값 void)로 진행합니다.

// 람다식 적용 이후
    REQUEST_URL(
            () -> MDC.get("request_url"),
            (request) -> MDC.put("request_url", request.getRequestURI()),
            (result) ->  LoggerFactory.getLogger(MdcKeys.class).error("wrong access"),
            () -> MDC.remove("request_url"),
            () -> LoggerFactory.getLogger(MdcKeys.class).info("request_url : " + MDC.get("request_url"))
    ), ...
    
    ...
    private final Supplier<String> get;
    private final Consumer<HttpServletRequest> add;
    private final Consumer<BindingResult> addBody;
    private final Runnable remove;
    private final Runnable log;
    
        public final String get(){
        return get.get();
    }

    public final void add(HttpServletRequest request){
        add.accept(request);
    }

    public final void addBody(BindingResult bindingResult){
        addBody.accept(bindingResult);
    }

    public final void remove(){
        remove.run();
    }

    public final void log(){
        log.run();
    }

하지만 람다식 역시 메소드를 내부에 정의하기 때문에 같은 메소드가 반복된다면 코드가 길어지게 됩니다. 이를 해결하기 위한 방식으로 메서드 참조 방식이 있습니다. 메서드 참조 방식은 람다식에서 부필요한 매개변수를 제거하여 사용할 수 있게 합니다. 사용 방식은 클래스 이름::메소드 이름이며 매개변수를 생략합니다. 또한 정의된 메소드가 static이어야 사용할 수 있습니다.

아래의 예시는 메서드 참조 방식이 적용된 경우입니다. 보시는 것처럼 람다식으로 메소드를 정의하는 대신 필요한 매개변수를 외부에서 직접 가져와 매칭시켜 함수형 인터페이스를 해당값으로 초기화합니다. 이 때 주의할 점은 함수형 인터페이스에서 정의한 매개변수 및 리턴값을 고려하여 작성해야 합니다. 아래의 경우 MDC 클래스의 get() 메소드의 경우 String을 매개변수로 받아 String을 리턴하는 함수입니다. 따라서 T를 입력받아 R을 출력하는 Function<T,R>을 사용하여 필요한 매개변수를 Enum을 사용하는 클래스에서 직접 가져와 Enum 내부에서 .apply()를 이용하여 함수형 인터페이스를 초기화합니다. 이로 인해 Enum 원소의 길이가 굉장히 줄어들고 가독성 역시 좋아지는 효과를 얻을 수 있습니다.

// 메서드 참조 방식 적용
REQUEST_URL("request_url", MDC::get, MDC::put, MDC::remove), ...

    private final String title;
    private final Function<String, String> get;
    private final BiConsumer<String,String> add;
    private final Consumer<String> remove;
    public final String get(){
        return get.apply(title);
    }
    public final void add(HttpServletRequest request, Logger logger){
        String val = switch (title) {
            case "request_id" -> UUID.randomUUID().toString();
            case "request_context_path" -> request.getContextPath();
            case "request_url" -> request.getRequestURI();
            case "request_method" -> request.getMethod();
            case "request_time" -> new Date().toString();
            case "request_ip" -> request.getRemoteAddr();
            case "request_header" -> request.getHeader(TokenProvider.HEADER_NAME);
            case "request_query_string" -> request.getQueryString();
            default -> "";
        };
        add.accept(title,val);
        logger.info(title + " : " + MDC.get(title));
    }
    public final void add(BindingResult bindingResult, Logger logger){
        add.accept(title, bindingResult.getModel().toString());
        logger.info(title + " : " + MDC.get(title));
    }
    public final void remove(Logger logger){
        remove.accept(title);
        logger.info(title + " : " + MDC.get(title));
    }

참고자료
http://www.tcpschool.com/java/java_lambda_reference
https://bcp0109.tistory.com/313

profile
저는 상황에 맞는 최적의 솔루션을 깊고 정확한 개념의 이해를 통한 다양한 방식으로 해결해오면서 지난 3년 동안 신규 서비스를 20만 회원 서비스로 성장시킨 Software Developer 최민길입니다.

0개의 댓글