티스토리 뷰
최근에 다시 함수형 인터페이스 (람다와는 다른) 추상메소드 인터페이스에 관심이 많아졌습니다. 생각보다 서비스 개발을 하게 되면서 잘 안쓰게 되고 되려 메소드 방식의 내용이 익숙하고 편한지라 그런 내용들을 자주 사용하곤 하였는데요, 이번 글에서는 이제부터라도 함수형 인터페이스를 좀 더 제대로 써보고 싶은 마음에 글을 작성하게 되었습니다.
목적 : 함수형 인터페이스가 무엇이 있는지 파악하고, 이를 실제 함수에 적용해본다.
1. 가장 먼저 함수형 인터페이스를 왜 사용하는지부터 알아보려 합니다.
기본적으로 가장 많이 사용하고 있는 자바의 함수형 인터페이스는 람다 표현식입니다. 화살표 함수라고 하는 이 녀석의 기본 꼴은 아래와 같습니다.
public interface FunctionUtil {
public abstract void print(String str);
}
public void printLog(String print) {
FunctionUtil function = str -> logger.info(str);
function.print(print);
}
말그대로 화살표 모양처럼 생겼다고 하여 화살표함수입니다. 이를 해체하면 복잡한 오버라이딩 함수로 풀립니다.
public void printLog(String text) {
FunctionUtil func = new FunctionUtil() {
@Override
public void print(String str) {
logger.info(str);
}
}
func.print(text);
func.print("Hello world"); // Hello world
}
이렇게 복잡하게 표현할 필요가 없기 때문에 인터페이스 함수라는 것으로 대체합니다.
그리고 함수형 인터페이스는 매번 람다표현식과 메서드별로 구현해주지 않아도 공통으로 사용 가능합니다.
2. 함수형 인터페이스 종류
많이 없습니다. 크게 5가지 입니다.
- Runnable : Thread 를 쓰시면 많이 보셨을 그 인터페이스
- Supplier : 파라미터 없이 생산만 하는 인터페이스 (getter)
- Consumer : 파라미터 넣으면 소비만 하는 인터페이스 (setter)
- Function : 파라미터가 2개이고, 앞에 타입을 넣으면 뒤의 타입을 반환해주는 형태의 인터페이스
- Predicate : 파라미터를 넣으면 true, false 로 리턴하는 인터페이스
3. 각 인터페이스의 형태
1. Runnable 을 알아봅시다. Runnable 은 철칙이 있습니다.
- 파라미터가 없다. 리턴도 없다.
- 모든 것은 run() 으로 끝난다.
쉽게 말해 실행만 시켜주는 함수형 인터페이스입니다. 즉, void 를 반환합니다.
public static <T> void run(Runnable runnable) {
runnable.run();
}
그래서 실행을 위해 예시를 들어보겠습니다.
public class CommandStep {
private Step step1;
private Step step2;
private StopExecution stop;
public void command() {
try {
if(step1.run() || step2.run()) { // step1, step2 를 같이 실행함, 반환값 없음
...
}
} catch(RuntimeException re) {
this.stop.execute(); // 중지 프로세스 실행
return;
}
}
}
public void execute(CommandStep step) {
Runnable runnable = () -> step.command(); // CommandStep.command 에 대해 실행
runnable.run();
}
2. Supplier 는 파라미터가 없이 Getter 만 존재합니다.
public TestDTO create() {
Supplier<TestDTO> supplier = () -> new TestDTO(UUID.randomUUID());
return supplier.get();
}
get() 이라는 메서드를 사용하여 파라미터가 없는 기본 생성자를 만들거나, 특정 변수값을 넣어서 사용하곤 합니다.
아래에서 Predicate 와 보다 자세히 다뤄보겠습니다.
3. Consumer 는 Runnable과는 다르게 파라미터가 있지만, 리턴이 없습니다.
Consumer 는 아래와 같은 성질이 있습니다.
- accept, andThen 함수 2가지로 이루어져 있다.
- andThen을 활용하여 두 개 이상의 Consumer 를 연속적으로 실행할 수 있다.
아래와 같은 형태로 함수를 만들어 보겠습니다.
public static <T> void ifPresent(T t, Consumer<T> func) {
if(t != null) {
func.accept(t);
}
}
위의 함수는 기본적으로 첫번째 파라미터에 타입 변수를 받고, 해당 변수가 NullSafe 할 때 두번째 파라미터로 입력받은 인터페이스 함수를 실행시켜주는 조건이 됩니다. 예를 들어 이렇게 실행시킬 수 있습니다.
@Setter
public class TestDTO {
private Long id;
private String testVal;
public void setValues(Long id, String testVal) {
TestDTO dto = new TestDTO();
Consumer<Long> setArgument = longValue -> dto.setId(longValue);
Consumer<String> setValue = str -> dto.setTestVal(str);
doConsume1(id, setArgument);
doConsume1(testVal, setValue);
}
public void doConsume1(T t, Consumer<T> consume) {
ifPresent(t, consume);
}
}
단, 여기 위에서 doConsume1(testVal, setArgument) 의 경우는 Syntax 에러가 발생합니다. 타입이 다르기 때문입니다.
앞에서 말씀드린 Runnable, Consumer를 사용하여 더 적극적으로 활용해보고자 합니다.
public static <T> void doFunctionOrElseRun(T t, Consumer<T> consume, Runnable runnable) {
if(t == null) { // 첫번째 파라미터가 null 인 경우
runnable.run(); // Runnable 을 실행한다.
} else {
consume.accept(t); // Consumer 를 실행한다.
}
}
그리고 2번에서 말씀드린대로 andThen도 실행 가능합니다. 단, 파라미터의 타입은 동일해야 합니다.
@Setter
public class TestDTO {
private long id;
private String testVal;
private String name;
}
public void setValues(String testVal) {
TestDTO dto = new TestDTO();
Consumer<String> setName = name -> dto.setName(name);
Consumer<String> setValue = str -> dto.setTestVal(str);
setName.andThen(setValue).accept(testVal);
setName.andThen(setValue).accept("My Name");
}
4. Function 은 다음과 같은 특성을 가집니다.
- 2개의 타입 변수를 받아 1번째 파라미터가 입력값, 2번째 파라미터가 출력값으로 쓰인다.
- 예를 들어, Function<AClass, BClass> 이면 apply로 리턴받는 값은 항상 BClass 이다.
- apply 로 기본 실행을 시키고, compose, andThen 이라는 함수로 전후를 정할 수 있다.
- identity 함수는 1번째 값을 반환시킨다.
Function 은 4가지 함수를 가지고 있기에 내용이 조금 깁니다.
Function 함수는 무궁무진하게 사용할 수 있습니다. 기본적으로 함수 자체에서 제공하는 추상 메소드가 다양하게 존재하다보니, 취향에 맞게 조절하여 사용하는 것이 가능합니다. 저는 대표적으로 형변환을 할 때 많이 사용합니다.
public String toComma(Long value) {
Function<Long, String> format = (val) -> new DecimalFormat("#,###").format(val);
return format.apply(value);
}
기본적으로 위의 함수를 통해 형변환이 가능합니다.
Function 함수는 아래와 같이 Optional과 같이 혼용해서 사용도 가능합니다.
public static <T, R> R getNullSafe(Optional<T> t, Function<T, R> func, R defaultValue) {
return t.isEmpty() ? defaultValue : func.apply(t.get());
}
5. Predicate 는 파라미터 타입을 받아 true, false 를 변환해주는 함수형 인터페이스 입니다. 다른 인터페이스와는 다르게 내용이 너무 길기에 그냥 코드를 가져와봤습니다.
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
/**
* Returns a composed predicate that represents a short-circuiting logical
* AND of this predicate and another. When evaluating the composed
* predicate, if this predicate is {@code false}, then the {@code other}
* predicate is not evaluated.
*
* <p>Any exceptions thrown during evaluation of either predicate are relayed
* to the caller; if evaluation of this predicate throws an exception, the
* {@code other} predicate will not be evaluated.
*
* @param other a predicate that will be logically-ANDed with this
* predicate
* @return a composed predicate that represents the short-circuiting logical
* AND of this predicate and the {@code other} predicate
* @throws NullPointerException if other is null
*/
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
/**
* Returns a predicate that represents the logical negation of this
* predicate.
*
* @return a predicate that represents the logical negation of this
* predicate
*/
default Predicate<T> negate() {
return (t) -> !test(t);
}
/**
* Returns a composed predicate that represents a short-circuiting logical
* OR of this predicate and another. When evaluating the composed
* predicate, if this predicate is {@code true}, then the {@code other}
* predicate is not evaluated.
*
* <p>Any exceptions thrown during evaluation of either predicate are relayed
* to the caller; if evaluation of this predicate throws an exception, the
* {@code other} predicate will not be evaluated.
*
* @param other a predicate that will be logically-ORed with this
* predicate
* @return a composed predicate that represents the short-circuiting logical
* OR of this predicate and the {@code other} predicate
* @throws NullPointerException if other is null
*/
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
/**
* Returns a predicate that tests if two arguments are equal according
* to {@link Objects#equals(Object, Object)}.
*
* @param <T> the type of arguments to the predicate
* @param targetRef the object reference with which to compare for equality,
* which may be {@code null}
* @return a predicate that tests if two arguments are equal according
* to {@link Objects#equals(Object, Object)}
*/
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
/**
* Returns a predicate that is the negation of the supplied predicate.
* This is accomplished by returning result of the calling
* {@code target.negate()}.
*
* @param <T> the type of arguments to the specified predicate
* @param target predicate to negate
*
* @return a predicate that negates the results of the supplied
* predicate
*
* @throws NullPointerException if target is null
*
* @since 11
*/
@SuppressWarnings("unchecked")
static <T> Predicate<T> not(Predicate<? super T> target) {
Objects.requireNonNull(target);
return (Predicate<T>)target.negate();
}
}
test 는 기본 함수이며 비교에 따라 true, false 를 반환해줍니다. 아래의 코드로 쉽게 이해가 가능합니다.
public boolean isNegative(Integer a) {
Predicate<Integer> negative = num -> num % 2 == 1;
return negative.test(a);
}
Predicate 는 연결해서 사용하기도 용이합니다. 2개의 Predicate 를 연결하는 함수는 and, or 입니다. and 는 연결된 내용의 두 명제가 모두 참일때 true를, or은 두 명제중 하나만 참이어도 true 를 리턴합니다.
isNegative.test(1); == true
isNegative.and(isPositive).test(1); == false
isNegative.and(isLessThanTen).test(7); == true
isNegative.or(isLessThanTen).test(11); == true
isEquals 함수는 static 메서드로 사용 가능합니다.
Predicate<Long> isFiveBillion = Predicate.isEqual(5_000_000_000);
isFiveBillion.test(1_000_000_000); // false
isFiveBillion.test(5_000_000_000); // true
Predicate 는 대표적으로 QueryDSL 의 BooleanBuilder와 같이 대표적으로 많이 사용하는 함수형 인터페이스입니다.
private Predicate today(RequestDTO search) {
if(search.isToday()) {
LocalDate currentDate = LocalDate.now();
LocalDate nextDay = currentDate.plusDays(1);
return entityClass1.updateTime.between(currentDate.atStartOfDay(), nextDay.atStartOfDay());
}
return null;
}
그리고 위에서 사용한 Supplier와 혼용하여 10번 시도하는 함수를 만들수도 있습니다. stopCondition 을 파라미터로 받아 해당 컨디션이 작동되었을 때까지 수행하는 메서드입니다.
public static <R> R exec(Supplier<R> supplier, Predicate<R> stopCondition) {
int MAX = 10;
int cnt = 0;
while (true) {
R r = supplier.get();
if(stopCondition.test(r)) {
return r;
}
++cnt;
if(MAX < cnt) {
throw new RuntimeException("재시도 %d 번 진행하였으나 실패하였습니다.".formatted(MAX));
}
}
}
4. 한 화면에 요약하기
오늘은 함수형 인터페이스에 대해 정의하고, 실제로 사용하기 위해 메서드화까지 진행해보는 시간을 가졌습니다. 찾아보니 이전에 작성한 글도 존재하여 그것도 링크로 남기게 되었어요.
Lambda와 Stream(2)
Author: 주니용 아래 소스는 Java8을 기준으로 작성되었습니다. 혹시 오타가 있거나 잘못 이해한 부분이 있으면 댓글로 적어주세요 :) 글(1) 에서 기본적으로 정의를 했다면 이번에는 어떻게 구체적
abbo.tistory.com
앞으로는 더 단련하여 함수형 인터페이스를 더 효율적으로 쓸 수 있는 그 날이 오길..!!
'Server' 카테고리의 다른 글
[Java] String template 사용하기 (0) | 2023.02.11 |
---|---|
[Jenkins] 배포 자동화 알림 Slack 으로 전송하기 (0) | 2023.02.06 |
[Java] Java 14 Record, Entity Class로 사용 가능할까? (0) | 2023.02.02 |
[Spring] Security 를 사용했을 때 세션의 만료 체크하기 (0) | 2023.01.31 |
[Java] Serialization 직렬화 분석 및 확장하기 (2023-01 수정) (1) | 2023.01.30 |
[Java] 보일러플레이트 코드와 실제 적용 후기 (0) | 2023.01.29 |