티스토리 뷰

함수형 프로그래밍은 소프트웨어 개발에서 점차 중요한 패러다임으로 자리 잡고 있습니다. 이는 상태 변경을 최소화하고 함수를 일급 객체 취급하여 코드의 가독성과 유지보수성을 높이는 접근법입니다. Java는 8 버전부터 람다 표현식과 함수형 인터페이스를 도입함으로써 이러한 패러다임을 지원하기 시작했습니다. 이번 포스팅에서는 Java 의 함수형 인터페이스 중 SupplierConsumer 에 초점을 맞춰 설명하겠습니다. 이 인터페이스들은 데이터 제공과 소비를 추상화하여 코드의 모듈성과 재사용성을 강화합니다. 간단한 예시를 통해 알아보겠습니다.


1. Supplier 인터페이스 

supplier 인터페이스는 Java.util.function 패키지에 속하며, 매개변수를 받지 않고 결과를 반환하는 함수를 나타냅니다.

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

이 인터페이스는 지연 평가(Lazy evaluation)를 구현할 때 유용합니다. 예를 들어, 값이 필요할 때만 계산하거나 기존 객체의 속성을 동적으로 추출하는 데 활용됩니다. Supplier는 불변성을 유지하면서도 동적인 데이터 접근을 가능하게 하여, 함수형 프로그래밍의 핵심 원칙을 지킵니다. 실제로 Supplier 는 캐싱이나 초기화 지연과 같은 패턴에서 자주 사용되며, 코드의 성능을 최적화하는데 기여합니다.


2. Consumer 인터페이스

Consumer 인터페이스는 마찬가지로 Java.util.function 패키지에 속하며, 입력을 받아들이고 결과를 반환하지 않는 함수를 정의합니다.

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

이 인터페이스는 부작용(side effect)을 발생시키는 연산, 예를 들어 객체의 상태 변경이나 로깅을 담당합니다. 함수형 프로그래밍에서 Consumer 는 순수 함수와 결합되어 전체적인 로직을 구성할 때 필수적입니다. 이를 통해 코드의 책임을 분리하고, 테스트 용이성을 높일 수 있습니다. Consumer는 forEach 메서드나 이벤트 핸들링과 같은 시나리오에서 광범위하게 적용됩니다.


3. 예시

해당 예시는 "값 변경 감지 유틸리티" 를 구현한 것으로, 객체의 속성 업데이트 시 변경 여부를 감지하는 유틸리티를 구성해 보겠습니다.

import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Supplier;

public final class ValueUpdateHelper {

    private ValueUpdateHelper() {
        // 인스턴스 생성 방지
    }

    /**
     * 새로운 값이 기존 값과 다를 경우에만 setter를 호출합니다.
     * null 또는 빈 값은 무시하고 기존 값을 유지합니다.
     *
     * @param <T> 값의 타입
     * @param newValue 새로운 값
     * @param oldValueSupplier 기존 값을 공급하는 Supplier
     * @param setter 새로운 값을 설정하는 Consumer
     */
    public static <T> void updateIfDifferent(T newValue, Supplier<T> oldValueSupplier, Consumer<T> setter) {
        if (newValue == null) {
            return; // null 무시
        }

        Object normalizedValue = newValue;
        if (newValue instanceof String str && str.trim().isEmpty()) {
            normalizedValue = null; // 공백 문자열 정규화
        } else if (newValue instanceof List<?> lst && lst.isEmpty()) {
            normalizedValue = null; // 빈 리스트 정규화
        } // 추가 타입에 대한 정규화는 필요 시 확장 가능

        T oldValue = oldValueSupplier.get();
        if (!Objects.equals(normalizedValue, oldValue)) {
            setter.accept((T) normalizedValue);
        }
    }
}
  • 새로운 값이 null 이거나 빈 문자열/컬렉션인 경우, 기존 값을 유지합니다. 이는 데이터 무결성을 보호하며 불필요한 업데이트를 방지합니다.
  • null-safe 한 비교를 위해 표준 라이브러리인 Objects.equals 를 사용합니다.
  • 다양한 타입에 적용이 가능하도록 제네릭 타입으로 설계하였습니다.

사용예시

import static ~.ValueUpdateHelper.updateIfDifferent;
...

// ClientInfo 클래스 가정 (getter/setter 포함)
ClientInfo client = new ClientInfo();
ClientUpdateDto request = new ClientUpdateDto();

// 업데이트 로직
updateIfDifferent(request.getCategoryCode(), client::getCategoryCode, client::setCategoryCode); // 카테고리 코드
updateIfDifferent(request.getSectorId(), client::getSectorId, client::setSectorId); // 섹터 ID
updateIfDifferent(request.getDetailSectorId(), client::getDetailSectorId, client::setDetailSectorId); // 상세 섹터 ID
  • 이 접근법은 명령형 코드에 비해 더 선언적이며, Supplier 와 Consumer를 통해 속성 접근을 추상화합니다. 결과적으로 코드가 간결해지고, 오류 가능성이 줄어듭니다.

4. 장점과 고려사항

장점

  • 모듈성: 로직을 재사용 가능한 유틸리티로 캡슐화
  • 성능 최적화: 변경되지 않은 값에 대한 불필요한 setter 호출 방지
  • 함수형 원칙 준수: 상태 변경을 최소화하고, 함수를 통해 연산 분리

고려사항

  • 복잡한 타입(예: 커스텀 객체)에 대한 equals 메서드 구현이 필요하며, 다중 스레드 환경에서는 동시성 문제를 고려해야 합니다.

5. 결론

함수형 프로그래밍은 Java 개발자에게 강력한 도구를 제공하며, Supplier 와 Consumer 는 핵심 요소 중 하나입니다. 이번 포스팅에서 제시한 예시 이외에도 활용할 가치가 있으니 이를 바탕으로 코드베이스에 적용해 보시기 바랍니다.

감사합니다.

최근에 올라온 글
Total
Today
Yesterday