이 글은 Java 환경에서 Dynamic Proxy 기반의 기능들을 사용할 때 주로 발생하는 문제인 Self-Invocation을 Lambda를 통해 어떻게 회피할 수 있는지에 대한 내용을 담고 있는 글입니다.

Dynamic Proxy, AOP에 대한 이론적인 부분(발생 원인, 호출 흐름 등)들은 다루지 않을 것이기 때문에 다른 글들을 참고하시길 바랍니다.


Lambda와 관련해서는 학습을 진행하며 작성한 글이기 때문에 잘못된 내용이 있을 수도 있습니다. 틀린 내용이 있다면 채널톡 혹은 댓글을 통해 피드백해주시길 바랍니다. ㅎㅎ


Self Invocation

Self Invocation은 Dynamic Proxy 기반의 기능들을 사용할 때 사소한 실수로 인하여 자주 발생하는 문제입니다. 쉽게 설명하자면, 객체 외부에서 보내는 메시지(요청)에 대해서만 반응하도록 설계되어 있기에 내부의 요청에 대해서는 반응하지 못하기 때문입니다.

JVM 생태계에서 많은 사랑을 받는 Spring Framework는 다양한 기능들을 Dynamic Proxy 방식으로 제공하고 있습니다.

  • @Transcational, @Async, @Cacheable, AOP(Before, Around) 등의 Aspect 기능들이 속합니다.


이러한 기능들에 대한 보편적인 해결 방법으로는

  • AspectJ으로 전환 혹은 부분 적용 (Weaving 방식 전환)
    • @EnableTransactionManagement(proxyTargetClass = true, mode = AdviceMode.ASPECTJ)
  • AopContext의 currentProxy() 메서드를 통해 해당 객체를 감싸고 있는 Proxy 객체를 반환
    • ((Type) AopContext.currentProxy()). someMethod();
  • 상태 변수를 통한 자기 참조 (@Autowired나 ApplicationContext.getBean() 활용)
  • 객체 외부에서 호출하는 메서드에 Dynamic Proxy가 반응하도록 설정하기

등이 있지만, 이것들을 적용하기에는 과한 상황이거나 Spring Container에 종속되는 좋지 않은 코드를 작성하게 될 수 있습니다.


Lambda를 이용하여 Self-Invocation 회피

Lambda로 어떻게 Self-Invocation을 회피할 수 있을까요?

결론만 이야기하자면 Lambda를 통해 실행되는 메서드를 접근하기 위해서 현재 호출 객체 외부로 메시지가 나가고, 최종적으로 호출해야 되는 메서드를 찾아 요청하였을 때, 외부에서 전달되기 때문에 감싸고 있는 Proxy가 해당 요청을 인지할 수 있기 때문입니다.

Java Lambda는 Reflection API, MethodHandle, LambdaMetaFactory 인터페이스를 이용하여 기능을 제공합니다.


Lambda Method를 호출하는 흐름

  1. Reflection API를 통해 실행 대상이 되는 메서드 정보를 가져옵니다.
  2. MethodHandle Lookup API에 정의된 Factory 메서드를 통해 Lookup 객체를 가져옵니다.
  3. 1번에서 가져온 정보를 Lookup.unreflect() 메서드에 전달함으로써 해당 메서드의 구현, 수행 정보를 알고 있는 MethodHandle 객체를 가져옵니다. (실제 메서드를 바라보고 있는 일종의 포인터)
  4. LambdaMetafactory.metafactory() 메서드에 필요한 인자를 넘겨 CallSite 객체를 반환받습니다. 해당 객체는 Functional Interface를 객체로 다룰 수 있으며, 매개 변수를 설정하고 응답을 반환합니다. 인자 목록은 밑에 나열하였습니다.
    1. 접근 권한을 가지고 있는 Lookup 객체
    2. 구현할 메서드 이름(Supplier Interface를 사용했을 경우 get이라는 문자열을 넘긴다.)
    3. 메서드의 매개 변수와 응답 값의 Class 정보. methodType(Supplier.class, {Type}. class)
    4. 함수 객체(Lambda)에 의해 반환될 응답 값의 유형. methodType(Object.class)
    5. 메서드의 구현 및 수행 방식을 알고 있는 MethodHandle 객체
    6. 호출 시 동적으로 적용되어야 할 응답 값의 세부 정보. methodType({Type}. class)
  5. callSite.getTarget()을 통해 호출할 메서드 정보를 가져오고 bindTo({Type}. class)를 통해 메서드에 넘길 인자 값을 지정한 뒤 Invoke를 통해 메서드를 호출합니다.

(사실상 그 호출하는 형태는 Dynamic Proxy와 유사한 것 같습니다)

꽤 복잡하지만, 결론적으로는 Lambda를 통해 호출되는 인터페이스를 인스턴스 화 하고, 메서드를 호출하기 때문에 객체 외부 요청으로 다시 돌아오는 것입니다.

  • 이 흐름은 Bytecode의 invokedynamic이 호출된 경우에 수행 흐름을 나타냅니다. Lambda에서 Function Interface를 사용하지 않고 단순한 로직을 사용하는 경우 static 메서드를 생성하여 활용하기도 합니다.
    • invokedynamic은 Bootstrap 메서드라는 특정한 메서드를 호출하고, 이를 통해 위의 호출 프로세스를 초기화 및 실행하여 CallSite 객체를 반환받습니다. (InnerClassLambdaMetafactory를 활용하여 내부 클래스 생성 후 반환) 
  • 한번 Bootstrap 메서드가 실행된다면 이후에는 기존의 CallSite와 MethodHandle를 재사용하여 요청을 처리합니다.

 

 

 

구현 예시

Self-Invocation을 회피하기 위해 구현한 TransactionalHandler입니다.

@Service
public class TransactionHandler {

    @Transactional(propagation = Propagation.REQUIRED)
    public <T> T runInTransaction(Supplier<T> supplier) {
        return supplier.get();
    }
}


Sample Service.

@Service
@RequiredArgsConstructor
public class SampleService {

    private final SampleRepository someRepository;
    private final TransactionHandler transactionHandler;

    // 특정 객체에서 호출하는 Method
    public void addNewSamples(List<Sample> samples) {
        return samples.forEach(sample ->
            transactionHandler.runInTransaction(() -> addSample(sample.toEntity()))
        )
    }

    // 외부에서 호출되는 Method
    @Transcational
    public SomeEntity addSample(SampleEntity newSample) {
        return someRepository.insertSample(newSample);
    }
}


단순한 예시여서 실제 효용성과 조금 동떨어진 감은 있지만, 실제 업무 중 활용할 수 있을만한 부분을 특정하실 수 있을 것이라고 생각합니다. ㅎㅎ 이 글은 여기까지입니다. 감사합니다.


참고 자료

 

 

함수형 프로그래밍이란?

순수 함수들을 조합하여 사이드 이펙트(부작용)를 제거하고, 모듈화를 높여 유지보수와 생산성을 올리는데 초점을 둔 패러다임이다. Non Blocking과 Asynchronous , Parallel Programming을 구현, 지원하는데 적합하다고 한다.

 

함수형 프로그래밍의 사고방식은 문제 해결에 대해 선언적인 행위(함수)들을 조합(구성)하여 해결하는 것이다.

자바도 스칼라, 자바스크립트와 같은 함수형 패러다임 언어 혹은 지원하는 언어, 기술들의 대두로 인하여 JDK 8부터 해당 기능을 도입하게 되었다.

 

함수형 인터페이스, 람다, 메서드 레퍼런스, 디폴트 메서드, Future, Fork-Join, 리액티브 등 추가

 

 

1급 객체

함수형 프로그래밍의 중요한 조건 중 하나를 의미한다. 이는 변수나 데이터 구조 안에 넣을 수 있고, 인자로 전달 가능하고, 동적으로 속성 값을 할당 가능하며, 리턴 값으로도 사용될 수 있는 메서드를 말한다.

 

 

순수 함수

같은 입력에 대해서 항상 같은 출력을 반환하는 형태의 메서드를 의미한다. 이는 인자로 주어지는 것들만 사용하고, 상태를 유지하지 않으며, 함수 외부 변수를 사용하지 않는 형태이다. 멀티스레드 환경에서 안전한 메서드.

public int minus(int a, int b) {
    return a - b;
}

 

 

고차 함수

1급 객체의 서브셋으로 메서드의 인자로 전달할 수 있고, 리턴 값으로 사용할 수 있는 메서드를 의미한다.

 

자바에서의 고차 함수는 하나 이상의 인자로 Lambda 식을 가지고 있거나, Lambda 식을 반환하는 메서드를 말한다.

default Comparator<T> reversed() {
    return Collections.reverseOrder(this);
}

 

 

익명 함수

이름이 없는 함수를 의미하며, 이는 Lambda expression으로 구현되는 메서드를 의미한다.

 

 

합성 함수

원하는 값을 도출하기 위해, 둘 이상의 메서드를 조합하는 것을 말한다. Stream API 처럼 데이터가 흐르는 파이프라인을 구성하고, 필요한 메서드를 연속적으로 호출하여 구현한다.

 

 

 

람다식 사용법

Example

람다는 익명 클래스를 단순화하고 표현식을 메서드의 인수로 넘기거나, 객체를 생성하는 데 사용한다.

 

익명 객체 생성하기

Thread thread = new Thread(new Runnable() {

        @Override
        public void run() {
                System.out.println("Hello Lambda!");
        }

});

thread.start();

------------------------

// 익명 객체 내부에 한줄 코드만 존재하는 경우, { }와 return을 생략하고 작성할 수 있다.

Thread thread = new Thread( () -> System.out.println("Hello Lambda!") ); 

------------------------

// 2줄인 경우

Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello Lambda!");
            System.out.println("Line");
        }
});

------------------------

Thread thread = new Thread(() -> {
        System.out.println("Hello Lambda!");
        System.out.println("Line");
});

 

 

메서드의 인수로 넘기기

// JDK Dynamic Proxy code
TargetObject getRealObject = (TargetObject) Proxy.newProxyInstance(TargetObject.class.getClassLoader(), new Class[]{TargetObject.class}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            TargetObject targetObject = new TargetObjectImpl();

            System.out.println("Before");
            Object invoke = method.invoke(targetObject, args);
            System.out.println("After");

            return invoke;
        }});

------------------------

// InvocationHandler 인터페이스의 익명 객체를 Lambda Expression으로 대체하였다.

TargetObject realObject = (TargetObject) Proxy.newProxyInstance(TargetObject.class.getClassLoader(), new Class[]{TargetObject.class},
            (proxy, method, args) -> {
                TargetObject targetObject = new TargetObjectImpl();

                System.out.println("Before");
                Object invoke = method.invoke(targetObject, args);
                System.out.println("After");

                return invoke;
            });

 

 

인자 값 사용(소비) 하기

인자로 전달된 값을 사용하여 데이터를 처리하고 로직을 완료한다. 리턴 타입은 void이다.

String value = "val";
someMethod(value, (value) -> System.out.println(value));

 

 

불 값 리턴하기

인자로 전달된 값을 기반으로 불 값을 리턴한다. 주로 값의 유효성 검증 및 비교 작업을 담당한다.

String value = "val";
someMethod(value, (value) -> "val".equals(value));

 

 

두 객체 비교하기

각각의 타입으로 전달된 두 객체를 비교하여 결과 값을 리턴한다.

String value1 = "val";
String value2 = "val";

Boolean result = someMethod(value1, value2, (value1, value2) -> value1.equals(value2));

 

 

객체 생성하기

인자로 전달되는 것 없이 객체를 생성한다. 리턴 타입은 void이다.

Lob lob = someMethod(() -> new Lob());

 

 

객체를 변경하기

인자로 전달된 값을 변경해서 다른 객체로 리턴한다.

String value = "hello";
String subString = someMethod(value, (value) -> value.substring(0, 3))

 

 

값을 조합, 병합하기

인자로 전달된 값을 조합해서 새로운 값을 리턴한다.

String prefix = "hello ";
String suffix = "world";
String fullText = someMethod(prefix, suffix, (prefix, suffix) -> prefix + suffix);

 

 

 

함수형 인터페이스 ( java.util.function )

하나의 추상 메서드를 가지고 있는 인터페이스나 @FunctionaInterface Annotation이 작성된 인터페이스를 말한다.

 

@FunctionaInterface는 함수형 인터페이스 형식을 제약 사항으로 지정한다.

 

Function <T, R>

T라는 타입의 한 인자를 받아서 R 타입으로 반환하는 함수형 인터페이스이다.

 

받은 인자를 다른 값으로 변환해서 리턴할 때, 값을 변경하거나 매핑할 때 사용한다.

 

R apply (T value)

compute(), merge(), replaceAll() 등의 메서드를 구현하는 데 사용된다.

public static Long parseLong(String value, Function<String, Long> function) {
    return function.apply(value);
}

----------------------

public static void main(String[] args) {

        System.out.println(parseLong("100", Long::valueOf));
}

 

유사한 함수형 인터페이스

  • BiFunction <T, U, R>

    각각의 타입을 가지는 두 인자를 받아서 다른 타입으로 반환하는 함수형 인터페이스이다.

     

    R apply(T t, U u);

    compute(), merge(), replaceAll() 등의 메서드를 구현하는 데 사용된다.

     

    String, Integer  -> Long

      public static Long parseLong(String value1, Integer value2 , BiFunction<String, Integer, Long> function) {
          return function.apply(value1, value2);
      }
    
      ----------------------
    
      public static void main(String[] args) {
    
              System.out.println(parseLong("100", 100, (value1, value2) -> Long.parseLong(value1)+value2));
      }
  • DoubleFunction

    입력되는 인자가 double인 함수형 인터페이스이다.

    R apply(double value);

     

  • DoubleToIntFunction

    입력되는 인자가 double, 반환 타입은 int인 함수형 인터페이스이다.

    int applyAsInt(double value);

     

  • DoubleToLongFunction

    입력되는 인자가 double, 반환 타입은 long인 함수형 인터페이스이다.

    long applyAsLong(double value);

     

  • IntFunction

    입력되는 인자가 int인 함수형 인터페이스이다.

    R apply(int value);

     

  • IntToDoubleFunction

    입력되는 인자가 int, 반환 타입은 int인 함수형 인터페이스이다.

    double applyAsDouble(int value);

     

  • IntToLongFunction

    입력되는 인자가 int, 반환 타입은 long인 함수형 인터페이스이다.

    long applyAsLong(int value);

     

  • LongFunction

    입력되는 인자가 long인 함수형 인터페이스이다.

    R apply(long value);

     

  • LongToDoubleFunction

    입력되는 인자가 long, 반환 타입은 double인 함수형 인터페이스이다.

    double applyAsDouble(long value);

     

  • LongToIntFunction

    입력되는 인자가 long, 반환 타입은 int인 함수형 인터페이스이다.

    int applyAsInt(long value);

     

  • ToDoubleBiFunction <T, U>

    각각의 타입을 가지는 두 인자를 받아서 double를 반환하는 함수형 인터페이스이다.

    double applyAsDouble(T t, U u);

     

  • ToIntBiFunction <T, U>

    각각의 타입을 가지는 두 인자를 받아서 int를 반환하는 함수형 인터페이스이다.

    int applyAsInt(T t, U u);

     

  • ToIntFunction

    T라는 타입의 한 인자를 받아서 int를 반환하는 함수형 인터페이스이다.

    int applyAsInt(T value);

     

  • ToLongBiFunction <T, U>

    각각의 타입을 가지는 두 인자를 받아서 long을 반환하는 함수형 인터페이스이다.

    int applyAsInt(T t, U u);

     

  • ToLongFunction

    T라는 타입의 한 인자를 받아서 long를 반환하는 함수형 인터페이스이다.

    long applyAsLong(T value);

     

Consumer

T라는 타입의 한 인자를 받아서 소모하고 아무것도 반환하지 않는 함수형 인터페이스이다.

 

인자를 전달하여 처리한 뒤 결과를 리턴 받을 필요가 없을 때 사용한다.

 

void Aceept(T t)

forEach() 등의 메서드를 구현하는 데 사용된다.

public static void printList(List<String> list, Consumer<String> consumer) {
    for (String item : list) {
        consumer.accept(item);
        }
}

----------------------

public static void main(String[] args) {

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");

        printList(list, System.out::println);
}

 

유사한 함수형 인터페이스

  • BiConsumer <T, U>

    입력되는 인자가 2개인 함수형 인터페이스이다.

    void accept(T t, U u);

     

  • DoubleConsumer

    기본형 타입인 double의 인자를 사용하는 함수형 인터페이스이다.

    void accept(double value);

     

  • IntConsumer

    기본형 타입인 int 타입의 인자를 사용하는 함수형 인터페이스이다.

    void accept(int value);

     

  • LongConsumer

    기본형 타입인 long 인자를 사용하는 함수형 인터페이스이다.

    void accept(long value);

     

  • ObjDoubleConsumer

    입력되는 인자가 2개이며 1번째 인자로는 Object, 2번째로는 double 타입의 인자를 사용하는 함수형 인터페이스이다.

    void accept(T t, double value);

     

  • ObjIntConsumer

    입력되는 인자가 2개이며 1번째 인자로는 Object, 2번째로는 int 타입의 인자를 사용하는 함수형 인터페이스이다.

    void accept(T t, int value);

     

  • ObjLongConsumer

    입력되는 인자가 2개이며 1번째 인자로는 Object, 2번째로는 long 타입의 인자를 사용하는 함수형 인터페이스이다.

    void accept(T t, long value);

     

Supplier

T라는 타입의 한 인자를 반환하는 함수형 인터페이스이다.

 

여기서 T는 받는 인자가 아닌 반환 타입을 지정한다.

 

T get();

orElseThrow(), orElseGet(), requireNonNull() 등의 메서드를 구현하는 데 사용된다.

public static String executeValue(Supplier<String> supplier) {
    return supplier.get();
}

----------------------

public static void main(String[] args) {

        String val = "value";
    System.out.println(executeValue(() -> val) );
}

 

유사한 함수형 인터페이스

  • BooleanSupplier

    boolean 값을 반환하는 함수형 인터페이스이다.

    boolean getAsBoolean();

     

  • DoubleSupplier

    double 값을 반환하는 함수형 인터페이스이다.

    double getAsDouble();

     

  • IntSupplier

    int 값을 반환하는 함수형 인터페이스이다.

    int getAsInt();

     

  • LongSupplier

    long 값을 반환하는 함수형 인터페이스이다.

    long getAsLong();

 

Predicate

T라는 타입의 한 인자를 받아서 그에 대한 Boolean 값을 제공하는 함수형 인터페이스이다.

 

주로 데이터를 필터링하거나, 조건에 맞는지 여부를 확인하는 데 사용한다.

 

boolean test(T t);

removeIf(), filter() 등의 메서드를 구현하는 데 사용된다.

public static boolean check(String value, Predicate<String> predicate) {
    return predicate.test(value);
}

----------------------

public static void main(String[] args) {

        System.out.println(check("hello", (value) -> value.length() > 4) );
}

 

유사한 함수형 인터페이스

  • BiPredicate <T, U>

    각각의 타입을 가지는 두 인자를 받고 boolean을 반환하는 함수형 인터페이스이다.

    boolean test(T t, U u);

     

  • DoublePredicate

    double 타입인 인자를 받아 boolean을 반환하는 함수형 인터페이스이다.

    boolean test(double value);

     

  • IntPredicate

    int 타입인 인자를 받아 boolean을 반환하는 함수형 인터페이스이다.

    boolean test(int value);

     

  • LongPredicate

    long 타입인 인자를 받아 boolean을 반환하는 함수형 인터페이스이다.

    boolean test(long value);

     

Operator

특정한 정수, 실수형 데이터를 처리하는 데 사용되는 함수형 인터페이스이다.

 

Operator 인터페이스

  • UnaryOperator extends Function <T, T>

    T라는 타입의 한 인자를 받아서 해당 타입으로 반환하는 함수형 인터페이스이다.

     

    내부적으로 Function을 상속받았다.

      static <T> UnaryOperator<T> identity() {
          return t -> t;
      }

 

  • BinaryOperator extends BiFunction <T, T, T>

      public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
          Objects.requireNonNull(comparator);
          return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
      }
    
      public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
          Objects.requireNonNull(comparator);
          return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
      }

 

  • DoubleBinaryOperator

    double 타입의 두 인자를 받고 해당 타입으로 값을 반환하는 함수형 인터페이스이다.

    double applyAsDouble(double left, double right);

     

  • DoubleUnaryOperator

    인자와 반환 타입이 double인 메서드를 제공하는 함수형 인터페이스이다.

    double applyAsDouble(double operand);

     

  • IntBinaryOperator

    int 타입의 두 인자를 받고 해당 타입으로 값을 반환하는 함수형 인터페이스이다.

    int applyAsInt(int left, int right);

     

  • IntUnaryOperator

    인자와 반환 타입이 int인 메서드를 제공하는 함수형 인터페이스이다.

    int applyAsInt(int operand);

     

  • LongBinaryOperator

    long 타입의 두 인자를 받고 해당 타입으로 값을 반환하는 함수형 인터페이스이다.

    long applyAsLong(long left, long right);

     

  • LongUnaryOperator

    인자와 반환 타입이 long인 메서드를 제공하는 함수형 인터페이스이다.

    long applyAsLong(long operand);

     

 

번외 : Runnable

해당 인터페이스는 JDK 1.0 부터 존재해왔던 오래된 인터페이스이지만, 자바의 함수형 인터페이스 제약을 지키고 있기에 추가하였다.

 

인자 타입과 반환 타입이 존재하지 않는 메서드를 제공하는 (논리적인) 함수형 인터페이스이다.

 

public abstract void run();

Runnable runnable = () -> System.out.println("class.run");
runnable.run();

 

 

번외 : Comparator<T>

해당 인터페이스는 JDK 1.2 부터 존재해왔던 인터페이스로 역시 자바의 함수형 인터페이스 제약을 지키고 있다. 

@FunctionalInterface도 타입 래벨에 정의되어 있음을 알 수 있다.

 

 

T라는 타입을 가지는 두 인자를 받아 int 값을 반환하는 메서드를 제공하는 함수형 인터페이스이다. 

객체, 값 객체 간의 우선 순위 즉 정렬을 위한 값을 얻어내는데 주로 사용된다.

 

int compare(T o1, T o2);

 Arrays.sort(split, (o1, o2) -> o2.compareTo(o1));

 

 

 

Variable Capture (Lambda Capturing)

Lambda의 body에서 인자로 넘어온 것 이외의 변수를 접근하는 것을 Variable Capture라고 한다.

 

 

Lambda는 인스턴스, 정적 변수final로 선언된 혹은 final처럼 사용되고 있는 지역 변수를 참조할 수 있다.

 

지역변수를 사용할 때에는 해당 변수에게 값의 재할당이 일어나서는 안된다.

// final int value = 100;
// 값을 통한 초기화 이후에 value의 값은 변경되어서는 안된다.
int value = 100;

// 순수 함수 형태가 아닌 사용 방식.
Lob lob = () -> new Lob(value);

--------------------
예제

@FunctionalInterface
interface Lob {
    public abstract void print();
}

//private final int a = 10; 도 동일하게 재정의가 가능하다.
private int a = 10;

public void hello() {

    final int b = 20;
    int c = 30;
    int d = 40;

    final Lob lobA = () -> System.out.println(a);
    lobA.print();

    // a 재정의
    a = 20;
    final Lob lobB = () -> System.out.println(a);
    lobB.print();

    final Lob lobC = () -> System.out.println(b);
    lobC.print();

    final Lob lobD = () -> System.out.println(c);
    lobD.print();

    final Lob lobE = () -> System.out.println(d);
    lobE.print();
}

public static void main(String[] args) {
        Lambda lambda = new Lambda();
        lambda.hello();
}

 

왜 지역 변수를 재정의해서 사용할 수 없는가?

이는 지역 변수가 스택 영역에 존재하기에 발생하는 문제점인데, 해당 변수를 초기화하는 스레드가 사라져 변수 할당이 해제된 경우에, Lambda를 실행하고 있는 별도의 스레드가 해당 변수 정보에 접근하려는 경우가 발생할 수 있다.

 

지역변수는 스레드 간에 공유가 되지 않는다.

 

자바에서는 람다를 실행하는 스레드의 스택에 지역 변수 할당 시 생성한 변수의 복사본을 저장하여 동작시키게 되는데, 이 값이 변경되었을 경우를 예측할 수 없기에 final 혹은 재할당 방지 제약조건을 걸어둔 것이다.

 

 

인스턴스 변수와 정적 변수는 왜 이런 제약조건을 걸지 않았는가?

이는 앞서 말했던 스레드 간의 가시성 문제의 연장선인데, 인스턴스 변수와 정적 변수는 모든 스레드에서 접근 가능한 값이기 때문에, 값의 변경이 이루어져도 직접적으로 접근할 수 있다.

 

 

 

메서드, 생성자 레퍼런스

메소드 참조?

JDK 8에 추가된 기능으로 함수를 메서드의 인자로 전달하는 것을 메서드 참조라고 한다.

 

해당 방식을 사용함으로써 해당 메서드 시그니처를 여러 곳에서 재사용할 수 있고, 기본적인 제공 메서드와 커스텀한 메서드 모두를 사용할 수 있다는 장점이 있다.

 

추가적으로 메서드 참조는 람다 표현식을 한번 더 축약적으로 표현할 수 있으며, 그를 통해 가독성을 향상할 수 있다.

 

람다 표현식을 대체하기보다는 상호 보완적인 관계를 형성한다.

// example

public static void printList(List<String> list, Consumer<String> consumer) {
        for (String item : list) {
            consumer.accept(item);
        }
}

public static void main(String[] args) {

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");

        // 람다 표현식
        printList(list, (item) -> System.out.println(item));

        // 메서드 참조
        // 한 단계 더 축약된 것을 알 수 있다.
        printList(list, System.out::println);
}

 

메서드 참조를 사용하는 방법?

  • 정적 메서드의 참조 : ( Class::staticMethod )

    static으로 정의한 메서드를 참조할 때 사용하는 방식이다.

     

    Long.parseLong(value); → Long::parseLong

      // 람다 표현식
      someMethod(value, (value) -> Long.parseLong(value));
    
      // 메서드 참조
      someMethod(value, Long::parseLong);

 

  • 비한정적 메서드의 참조 (인스턴스) : ( Class::instanceMethod )

    public 혹은 protected로 정의된 메서드를 참조할 때 사용되는 방식이다.

     

    비한정적이라는 표현은 구문 자체가 특정한 객체를 참조하기 위한 변수를 지정하지 않는다는 것을 의미한다.

     

    string.length() → String::length

      // 람다 표현식
      someMethod(value, (value) -> value.length());
    
      // 메서드 참조
      someMethod(value, String::length);

 

  • 한정적 메서드 참조 (외부 인스턴스 변수) : ( Instance::instanceMethod )

    Lambda body 외부에서 선언된 객체의 메서드를 호출하거나, 객체를 생성해서 메서드 참조할 때 사용되는 방식이다.

     

    한정적이라는 표현은 참조하는 메서드가 특정 객체의 변수로 제한되는 것을 의미한다.

     

    lob.isLob() → lob::isLob

      Lob lob = new Lob(true);
    
      // 람다 표현식
      someMethod(() -> lob.isLob());
    
      // 메서드 참조
      someMethod(lob::isLob);

 

 

생성자 참조?

자바 언어에서는 메서드와 생성자를 구분하고 있다.

 

문법적인 구조상의 차이로는 리턴 타입이 없다는 것이 있지만, 메서드는 접근 권한만 있다면 호출 가능하나, 생성자는 객체가 생성될 때에만 호출할 수 있다.

 

생성자 참조는 ClassName::new 형식으로 작성된다.

 

생성자 참조는 새로운 객체를 생성하고 리턴하는 경우 등에서 사용된다.

// 람다 표현식
someMethod(String name -> new Lob(name)).forEach((Lob lob) -> System.out.println(lob.name))

// 생성자 참조
someMethod(Lob::new).forEach((Lob lob) -> System.out.println(lob.name))

// 생성자, 메서드 참조
someMethod(Lob::new).forEach(System.out::println);

 

 

 

참고 자료

  • Practical 모던 자바

 

 

 

'Live Study' 카테고리의 다른 글

Live Study_Week 10. Multithreading programming  (3) 2021.03.02
Live Study_Week 13. I/O  (0) 2021.02.20
Live Study_Week 12. Annotation  (0) 2021.02.02
Live Study_Week 11. Enum  (0) 2021.01.28
Live Study_Week 09. 예외 처리  (0) 2021.01.11

+ Recent posts