블로그

양성빈

[인프런 워밍업 스터디 클럽] 0기 백엔드 미션 - 어노테이션(Day1)

어노테이션 서론드디어 '인프런 워밍업 스터디 클럽 0기' 첫 날이 밝아왔다. 강의를 듣고 미션을 보니 어노테이션에 관련한 내용이었다.나는 이 미션을 보고 오히려 기쁜 마음이 들었다. 😆 내가 강의를 들으면서 어노테이션 부분이 많이 궁금하였는데 이렇게 공부하게 될 계기가 생긴 것 같아서 미션도 완성시키고 나 스스로 깊게 공부도 할 겸 미션을 시작할려고 한다. 미션 내용은 아래와 같다.진도표 1일차와 연결됩니다우리는 최초로 API를 만들어 보았습니다. GET API를 만들기 위해 사용했던 어노테이션에 익숙하지 않다면 자바 어노테이션에 대해서 몇 가지 블로그 글을 찾아보세요! 다음 질문을 생각하며 공부해보면 좋습니다! 😊 [질문]어노테이션을 사용하는 이유 (효과) 는 무엇일까?나만의 어노테이션은 어떻게 만들 수 있을까?내가 알아본 어노테이션의 정의나는 강의의 실습을 통하여 스프링 부트 프로젝트를 생성하고, GET API를 만들어보고 포스트맨을 통하여 테스트 작업도 해보았다. 나는 여기서 다양한 어노테이션들을 볼 수 있었다. @SpringBootApplication, @RestController, @GetMapping 등 여러 어노테이션들을 볼 수 있었다. 여기서 나는 어노테이션이 무엇일까 고민을 해보았다. 단순히 어노테이션은 @ 붙인거라고만 알고 있었기에 이번 기회에 미션도 수행할 겸 깊게 알아보는 것도 좋다 생각하여 공부해보기로 하겠다.먼저 어노테이션이 대체 어떤 정의가 있는지 구글링을 해보기로 하였다. 구글링을 해보니, 다양한 블로그들이 나왔지만 정의가 수록된 위키백과를 먼저 참조해보기로 하였다. 위키백과는 다음과 같이 정의를 내렸다. 자바 어노테이션은 자바 소스 코드에 추가하여 사용할 수 있는 메타데이터의 일종이다. 보통 @ 기호를 앞에 붙여서 사용한다. JDK 1.5 버전 이상에서 사용 가능하다. 자바 어노테이션은 클래스 파일에 임베디드되어 컴파일러에 의해 생성된 후 자바 가상머신에 포함되어 작동한다. 그리고 강의 중에 코치님께서도 어노테이션에 대해 아래와 같이 언급해주셨다. 어노테이션은 어노테이션마다 너무 다양한 역할을 한다. 또한 마법같은 일을 자동으로 해준다는 것이다.예를 들어서, @SpringBootApplication 어노테이션은 스프링을 실행시킬 때 다양한 설정이 필요한데 이 설정을 모두 자동으로 해준다. 또한 이런것이 가장 핵심적인 마법같은 일이다.위키사전, 코치님의 설명을 통해 어노테이션의 정의를 알 수 있었다. 좀 더 내가 설명한 식으로 풀어보자면 다음과 같다.자바의 어노테이션은 코드에 추가 정보를 제공하는 데 사용되며, 컴파일 시간, 배포 시간, 또는 실행 시간에 해당 정보를 활용할 수 있습니다. 이를 통해 개발자는 코드에 메타데이터를 추가하여 코드의 가독성, 유지 보수성을 향상시키고, 다양한 도구와 프레임워크에서 활용될 수 있는 정보를 제공할 수 있습니다.좀 더 자세히 풀어보자.어노테이션은 자바 5부터 도입된 기능으로, 코드에 대한 메타데이터를 제공하는 방법입니다. 어노테이션은 주석과 비슷하지만, 실제로 코드에 영향을 줄 수 있으며, 컴파일러에게 정보를 제공하거나 실행 시간에 특정 동작을 하도록 할 수 있습니다. 어노테이션은 선언적 형태로 코드 안에 포함되어, 클래스, 메소드, 변수 등 다양한 요소에 적용될 수 있습니다.이제 위의 내용을 좀 더 정리해보겠다. 어노테이션이란?자바를 개발한 사람들은 소스코드에 대한 문서를 따로 만들기보다 소스코드와 문서를 하나의 파일로 관리하는 것이 낫다고 생각했다. 그래서 소스코드의 주석에 소스코드에 대한 정보를 저장하고, 소스코드의 주석으로부터 HTML 문서를 생성해내는 프로그램(javadoc.exe)를 만들어 사용했다. 그런데 여기서 의문점이 하나 든다. 🙋🏻 왜 어노테이션이라는 것을 살펴보려 하는데 주석이라는 내용이 먼저 나올까? 프로그램의 소스코드 안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것이 바로 어노테이션이다.어노테이션은 주석(comment)처럼 프로그래밍 언어에 영향을 미치지 않으면서도 다른 프로그램에게 유용한 정보를 제공할 수 있다는 장점이 있다. 📚 어노테이션(annotation)의 뜻은 주석, 주해, 메모이다.package org.example; public @interface SampleAnnotation { }위의 코드는 인텔리제이로 나의 어노테이션을 만든 코드이다.그럼 인텔리제이로 어노테이션을 만드는 것도 끝났으니 이제 끝인가? 나는 여기서 더 나아가서 이 어노테이션 코드가 .class파일로 컴파일 되었을 때 어떻게 나오는지 보고 싶어서 터미널로 컴파일을 해보았다. 컴파일 결과는 다음과 같다.public interface org.example.SampleAnnotation extends java.lang.annotation.Annotation { }컴파일 시점에 extends 한적 없는 java.lang.annotation.Annotation 이 extends 되어 있다. 이제 좀 더 자세한 어노테이션의 내용과 활용법을 알아가보자. 어노테이션은 JDK에서 기본적으로 제공하는 것과 다른 프로그램에서 제공하는 것들이 있는데, 어느 것이든 그저 약속된 형식의 정보를 제공하기만 하면 될 뿐이다.JDK에서 제공하는 표준 어노테이션은 주로 컴파일러를 위한 것으로 컴파일러에게 유용한 정보를 제공한다. 📚 JDK에서 제공하는 어노테이션은 'java.lang.annotation' 패키지에 포함되어 있다.어노테이션은 코드에 넣는 주석이다. 완전히 주석같지는 않지만 그 비슷한 부류이다.주석이기 때문에, 실행되는 코드라고 생각하면 안된다. 어노테이션은 기능을 가지고 있는 것이라 착각을 할 수 있지만 어노테이션은 마크, 표시 해놓는 주석이다. 어노테이션은 다이나믹하게 실행되는 코드는 들어가지 않는다.즉, 런타임에 알아내야 하는 것들은 못 들어간다.위의 내용을 좀 더 풀어쓰면 컴파일러 수준에서 해석이 되야 하거나, 완전히 정적이어야 한다는 말이다.이유를 아래 코드로 보여주겠다. package me.sungbin.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { private static final String hello = "/hello"; @GetMapping(hello) public String hello() { return "hello"; } }위와 같이 hello 변수는 정적 변수이므로 @GetMapping 어노테이션에 사용할 수 있다.하지만. hello 변수가 동적인 변수라면 컴파일 에러가 발생한다.아래의 코드를 보자. 컴파일 에러가 발생하는 것을 볼 수 있을 것이다. 간략한 어노테이션 정의 방법새로운 어노테이션을 정의하는 방법은 아래와 같다.'@'기호를 붙이는 것을 제외하면 인터페이스 정의와 동일하다. package me.sungbin; public @interface SampleAnnotation { 타입요소이름(); } 📚 타입요소등, 어노테이션 정의에 대한 자세한 정의방법과 내용들은 구체적인 내용들을 확인 후, 살펴보자.자바의 표준 어노테이션자바에서 기본적으로 제공하는 어노테이션들은 몇 개 없다.그나마 이들의 일부는 '메타 어노테이션(meta annotation)' 으로 어노테이션을 정의하는데 사용되는 어노테이션의 어노테이션이다. 표준 어노테이션과 메타 어노테이션@Override: 컴파일러에게 오바리이딩하는 메서드라는 것을 알린다.@Deprecated: 앞으로 사용하지 않을 것을 권장하는 대상에 붙인다.@SuppressWarnings: 컴파일러의 특정 경고메시지가 나타나지 않게 해준다.@SafeVarags: 제네릭스 타입의 가변인자에 사용한다. (JDK 1.7)@FunctionalInterface: 함수형 인터페이스라는 것을 알린다. (JDK 1.8)@Native: native 메서드에서 참조되는 상수 앞에 붙인다. (JDK 1.8)@Target*: 어노테이션이 적용가능한 대상을 지정하는데 사용한다.@Documented*: 어노테이션 정보가 javadoc으로 작성된 문서에 포함되게 한다.@Inherited*: 어노테이션이 자손 클래스에 상속되도록 한다.@Retention*: 어노테이션이 유지되는 범위를 지정하는데 사용한다.@Repeatable*: 어노테이션을 반복해서 사용할 수 있게 한다. (JDK 1.8)*이 붙은 것이 메타 어노네이션이다.📚 메타 어노테이션: 어노테이션을 정의하는데 사용하는 어노테이션의 어노테이션 @Override현재 메서드가 슈퍼 클래스의 메서드를 오버라이드한 것임을 컴파일러에게 명시해준다.메서드가 슈퍼클래스에 없다면 에러를 발생시기 때문에 오타와 같은 실수도 잡을 수 있다. @Deprecated마커 어노테이션으로 다음 버전에 지원되지 않을 수도 있기 때문에 앞으로 사용하지 말라고 경고를 알린다.@Deprecated를 붙인 메서드는 인텔리제이에서 아래의 사진과 같이 표시해준다. @SuppressWarning경고를 제거하는 어노테이션으로 개발자가 의도를 가지고 설계를 했는데 컴파일은 이를 알지 못하고 컴파일 경고를 띄울 수 있기 때문에 이를 제거하는 목적이다. @SafeVarargsJava 7이상에서 사용가능하고 제네릭같은 가변인자 매개변수 사용시 경고를 무시한다제네릭사용할 클래스,메서드 내부에서의 데이터타입을 외부에서 지정하는 기법 @FunctionalInterfaceJava 8이상에서 사용가능하고 컴파일러에게 함수형 인터페이스라는 것을 알리는 어노테이션이다.메타 어노테이션'어노테이션을 위한 어노테이션' 쯕, 어노테이션에 붙이는 어노테이션으로 어노테이션을 정의할 때 어노테이션의 적용대상(target) 이나 유지기간(retention)등을 지정하는데 사용된다. 📚 메타 어노테이션은 java.lang.annotation 패키지에 포함되어 있다. @Target어노테이션이 적용가능한 대상을 지정하는데 사용한다.아래 예제는 '@SuppressWarnings' 를 정의한 것인데, 이 어노테이션에 적용할 수 있는 대상을 '@Target' 으로 지정한다.여러 개의 값을 지정할 때는 배열처럼 괄호{} 를 이용하여 지정할 수 있다.package me.sungbin; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) @Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { String[] value(); } @Target으로 지정할 수 있는 어노테이션 적용대상의 종류ANNOTATION_TYPE: 어노테이션CONSTRUCTOR: 생성자FIELD: 필드(멤버 변수, ENUM 상수)LOCAL_VARIABLE: 지역변수METHOD: 메서드PACKAGE: 패키지PARAMETER: 매개변수TYPE: 타입(클래스, 인터페이스, ENUM)TYPE_PARAMETER: 타입 매개변수(JDK1.8)TYPE_USE: 타입이 사용되는 모든 곳(JDK1.8)📚 java.lang.annotation.ElementType 이라는 열거형에 정의되어 있다. static import문을 사용하면 ElementType.TYPE 이 아니라 TYPE 과 같이 간략히 사용할 수 있다. TYPE은 타입을 선언할 때 어노테이션을 붙일 수 있다는 뜻TYPE_USE는 해당 타입의 변수를 선언할 때 붙일 수 있다는 뜻이다.FIELD 는 기본형에 사용할 수 있고, TYPE_USE는 참조형에 사용된다는 점을 주의한다.타입 선언부제네릭 타입, 변수 타입, 매개변수 타입, 예외 타입...타입에 사용할 수 있으려면TYPE_PARAMETER : 타입 변수에만 사용할 수 있다.TYPE_USE : 타입 변수를 포함해서 모든 타입 선언부에 사용할 수 있다.package me.sungbin; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; @Target({FIELD, TYPE, TYPE_USE}) public @interface MyAnnotation { } package me.sungbin; import me.sungbin.controller.HelloController; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MyAnnotation public class AnnotationTestApplication { @MyAnnotation int i; @MyAnnotation HelloController helloController; public static void main(String[] args) { SpringApplication.run(AnnotationTestApplication.class, args); } } @Retention어노테이션 유지되는 기간을 지정하는데 사용한다. 어노테이션 유지정책의 종류SOURCE: 소스 파일에만 존재. 클래스파일에는 존재하지 않는다.CLASS: 클래스 파일에 존재. 실행 시에 사용 불가능하다. (기본값)RUNTIME: 클래스 파일에 존재하며 실행시에 사용 가능하다.SOURCE -> CLASS -> RUNTIMESOURCE는 소스코드만 유지하겠다.컴파일 시에만 사용하겠다는 것!컴파일하고 나면 어노테이션은 없어진다. -> 바이트코드에도 남아있지 않다.CLASS애노테이션에 대한 정보를 클래스 파일까지, 즉 바이트 코드에도 남겨 두겠다.클래스 정보를 읽어들이는 방법(바이트 코드를 읽어들이는)을 바탕으로 애노테이션 정보를 읽어와서 처리할 수 있다.예) BYTE BUDDY, ASM 활용바이트 코드엔 남아 있지만, 이 클래스파일을 JVM이 실행할 때 클래스에 대한 정보를 클래스로더가 읽어서 메모리에 적재하게되고, 이후 사용 시점에 메모리에서 읽어올 때 애노테이션 정보를 제외하고 읽어옴RUNTIME위 CLASS와 동일하지만, 메모리에 적재된 클래스 정보를 읽어올 때 애노테이션 정보를 그대로 포함하는 것이다.바이트코드에서 읽어오는게 빠를까?RetentionPolicy를 CLASS로 한 이후, 바이트코드를 읽어 처리하는 라이브러리를 활용?리플렉션으로 읽어오는게 빠를까?RetentionPolicy를 CLASS로 한 이후, 바이트코드를 읽어 처리하는 라이브러리를 활용? -> 리플렉션 자체가 부하가 존재한다.-> 바이트 코드의 양에 영향을 끼친다.-> 리플렉션은 메모리에 이미 올라와 있는 정보를 읽는다. 클래스 로더가 읽고 메모리에 적재시킨 후 읽어온다. 📚 커스텀하게 만든 애노테이션이 정말로 RUNTIME 까지 필요한 정보인가? RUNTIME 까지 사용할 필요가 없다면, CLASS 레벨로 내려가거나 SOURCE 레벨로 내려갈 수도 있을 것이다. 그냥, 의례적으로 RUNTIME으로 작성하는 경우가 있었다면? 그 역할을 다시 살펴보고 명확한 Retention Policy 를 정의하자. 표준 어노테이션 중 '@Override' 나 '@SuppressWarnings' 처럼 컴파일러가 사용하는 어노테이션은 유지 정책이 'SOURCE' 이다. -> 컴파일러를 직접 작성할 것이 아니면, SOURCE 이상의 유지정책을 가질 필요가 없다. 유지 정책을 RUNTIME 으로 한다면,실행 시에 리플렉션(Reflection) 을 통해 클래스 파일에 저장된 어노테이션의 정보를 읽어서 처리 할 수 있다.Retention 정책은 RUNTIME 으로 정의하고Target은 TYPE과 FIELD로 정의한다.package me.sungbin; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; @Target({TYPE, FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { } Target이 TYPE과 FIELD 임으로 클래스에도 애노테이션을 선언할 수 있고클래스 내부의 필드에도 애노테이션을 선언할 수 있다.package me.sungbin; @MyAnnotation public class TestClass { @MyAnnotation private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } TestClass 클래스에 선언된 Annotation을 리플렉션을 이용해 확인할 수 있다.package me.sungbin; import java.lang.reflect.Field; import java.util.Arrays; public class App { public static void main(String[] args) { Arrays.stream(TestClass.class.getAnnotations()).forEach(System.out::println); Field[] declaredFields = TestClass.class.getDeclaredFields(); for (Field declaredField : declaredFields) { Arrays.stream(declaredField.getAnnotations()).forEach(System.out::println); } } }  표준 어노테이션 중 '@FunctionalInterface' 는 '@Override' 처럼 컴파일러가 체크해주는 어노테이션이지만, 실행 시에도 사용되므로 유지 정책이 "RUNTIME"으로 되어 있다. @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface FunctionalInterface {} 유지 정책을 "CLASS" 으로 한다면컴파일러가 어노테이션의 정보를 클래스 파일에 저장할 수 있게 하지만,클래스 파일이 JVM에 로딩 될 때는 어노테이션의 정보가 무시되어 실행 시에 어노테이션에 대한 정보를 얻을 수 없다.→ CLASS 가 유지정책의 기본값임에도 불구하고 잘 사용되지 않는 이유 지역 변수에 붙은 어노테이션은 컴파일러만 인식할 수 있으므로, 유지 정책이 RUNTIME인 어노테이션을 지역변수에 붙여도 실행 시에는 인식되지 않는다. @Documented어노테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록 한다.표준 어노테이션 중 Override와 SuppressWarnings를 제외하고 모두 Documented 메타 어노테이션이 붙어 있다. @Documented애노테이션 정보가 javadoc으로 작성된 문서에 포함된다고 한다. 이것이 무슨말일까? 내 코드가 자바docs에 올라간다는 말일까?정확히 말하면 자바docs에 올라간다는 말이 아니라,직접 javadoc을 만들 수 있다는 뜻이다.이런식으로 만들 수 있는데, Local 지역입력 ko_KRother command line arguments : 한글깨짐 방지-encoding UTF-8 -charset UTF-8 -docencoding UTF-8적절하게 내용을 채운뒤 output directory에 경로를 입력해주면 끝이다.그러면 @Documented를 붙인거와 안 붙인것을 비교해보자. 코드public class Korea implements Great{ @Override @Make public String country() { return "한국"; } } 없는거 있는거JavaDoc애노테이션을 알기 전에 JavaDoc에 대해 알아보자.JavaDoc은 Java코드에서 API문서를 HTML 형식으로 생성해주는 도구이다.HTML 형식이기 때문에 다른 API를 하이퍼 링크를 통해 접근이 가능하다. JavaDoc TagsJavaDoc은 여러 Tag를 작성하여 문서를 완성한다.Java 코드에서 애노테이션으로 추가한다.IDE에서 /** 입력 후 엔터를 치면 자동으로 형식이 생성된다.Javadoc Tags의 종류들@author@deprecated@exception@param@return@see@serial@serialData@serialField@since@throws@since@throws@version@Inherited어노테이션이 자손 클래스에 상속되도록 한다.'@Inherited' 가 붙은 어노테이션을 조상 클래스에 붙이면, 자손 클래스도 이 어노테이션이 붙은 것과 같이 인식된다.MyAnnotation은 Inherited 애노테이션을 통해 자손 클래스에도 인식되도록 정의한다.package me.sungbin; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; @Retention(RetentionPolicy.RUNTIME) @Target({TYPE, FIELD}) @Inherited public @interface MyAnnotation { } 부모클래스인 Sungbin클래스에 MyAnnotation을 정의package me.sungbin; @MyAnnotation("hi") public class Sungbin { @MyAnnotation("yang sung bin") private String name; }  Sungbin 클래스의 자식 클래스인 Child 클래스에는 별도의 어노테이션 정의가 없다.package me.sungbin; public class Child extends Sungbin { } 리플렉션을 이용해 Child 클래스의 어노테이션을 확인해보자.→ ChildSson 클래스에는 정의한 애노테이션이 없지만,→ 부모 클래스인 Sson 클래스에 정의한 애노테이션이 확인됨을 볼 수 있다.→ Inherited 애노테이션을 통해 자식 클래스까지 전파될 수 있음을 확인할 수 있다. Inherited 애노테이션을 바탕으로 리플렉션을 활용해 자식클래스에서부모클래스에 정의되어 있는 Inherited 애노테이션을 확인할 수 있다. 📚 리플랙션의 getDeclaredFields(); 를 하면 클래스에 정의된(선언된) 것들을 가져와서 조작할 수 있다. public이던, private 이던,, Getter와 Setter에 대해 논의를 하며 큰 비용을 소모하는 것이 크게 가치가 없다.. 객체지향을 얘기하며 Getter, Setter의 정의 관련한 내용으로 얘기할 수 있겠지만, Getter와 Setter가 없더라도 리플랙션을 이용하면 충분히 가져오고 수정할 수 있기 때문이다. 중요한건 Getter, Setter 가 아닌것 같다. @Repeatable보통은 하나의 대상에 한 종류의 어노테이션을 붙이게 되는데,'@Repeatable'이 붙은 어노테이션은 여러 번 붙일 수 있다. 일반적인 어노테이션과 달리 같은 이름의 어노테이션이 어러 개가 하나의 대상에 적용될 수 있기 때문에, 이 어노테이션들을 하나로 묶어서 다룰 수 있는 어노테이션도 추가로 정의해야 한다. @Native네티이브 메서드(native method)에 의해 참조되는 '상수 필드(constant field)'에 붙이는 어노테이션이다.여기서, 네이티브 메서드는 JVM이 설치된 OS의 메서드를 말한다.네이티브 메서드는 보통 C언어로 작성되어 있는데, 자바에서는 메서드의 선언부만 정의하고 구현하지 않는다.그래서 추상 메서드처럼 선언부만 있고 구현부가 없다. 어노테이션 타입 정의어노테이션의 요소어노테이션 내에 선언된 메서드를 어노테이션의 요소라고 한다. 📚 어노테이션에도 인터페이스처럼 상수를 정의할 수 있지만, 디폴트 메서드는 정의할 수 있다. 어노테이션의 요소는 반환 값이 있고 매개변수는 없는 추상 메서드의 형태를 가진다.다만, 어노테이션을 적용할 때 이 요소들의 값을 빠짐없이 지정해주어야 한다.각 요소들은 기본값을 가질 수 있으며, 기본값이 있는 요소들은 어노테이션을 적용할 때 값을 지정하지 않으면 기본값이 사용된다.어노테이션의 요소가 오직 하나 뿐이고 이름이 value 인 경우, 어노테이션을 적용할 때 요소의 이름을 생략하고 값만 적어도 된다.요소 타입이 배열인 경우, 괄호{} 를 사용해 여러 개의 값을 지정할 수 있다.하나인 경우는 괄호{} 를 생략할 수 있다.java.lang.annotation.Annotation모든 어노테이션의 조상은 Annotation이다.그러나 어노테이션은 상속이 허용되지 않으므로 아래와 같이 명시적으로 Annotation을 조상으로 지정할 수 없다. @interface TestInfo extends Annotation{ // 에러. 허용되지 않는 표현이다. int count(); String testedBy(); ... } Annotation 을 살펴보면Annotation은 어노테이션이 아니라 일반적인 인터페이스로 정의되어 있다. 모든 어노테이션의 조상인 Annotation 인터페이스가 위와 같이 정의되어 있기 때문에모든 어노테이션 객체에 대해 equals(), hashCode(), toString() 과 같은 메서드를 호출하는 것이 가능하다.리플랙션(Reflection)을 이용해 특정 클래스에 선언된 애노테이션들을 조회하여 equals, hashCode, toString 메서드를 호출해본다.어노테이션 요소의 규칙어노테이션의 요소를 선언할 때 반드시 지켜야 하는 규칙요소 타입은 기본형, String, Enum, 어노테이션, Class 만 허용() 안에 매개변수를 선언할 수 없다.예외를 선언할 수 없다.요소를 타입 매개변수로 정의할 수 없다.마커 어노테이션 Marker Annotation값을 지정할 필요가 없는 경우,어노테이션의 요소를 하나도 정의하지 않을 수 있다.Serializable 이나 Cloneable 인터페이스처럼, 요소가 하나도 정의되지 않은 어노테이션을 마커 어노테이션이라 한다. 🙋🏻 이런 마커 어노테이션은 왜 사용될까? 글을 찾아보니 아래의 내용이 있었다.마커 어노테이션을 통해 코드 작성 시점, 컴파일 시점, 러타임 시점에 부가적인 작업을 추가할 수 있을 것이다.코드 작성 시점에 어노테이션 정보를 통해 부가적인 정보를 check 하여 컴파일에러를 발생시킬 수 있을 것이며컴파일하는 과정에서 어노테이션 정보를 바탕으로 부가적인 정보를 포함하여 컴파일된 결과를 내보낼 수도 있을 것이다.또한, 런타임 시점에 리플랙션을 이용하여 애노테이션 정보를 바탕으로 부가적인 작업을 할 수 있을 것이다.Java8 어노테이션 변화 애노테이션 관련 큰 변화 두가지자바 8 부터 애노테이션을 타입 선언부에도 사용할 수 있게 되었다.자바 8 부터 애노테이션을 중복해서 사용할 수 있게 되었다.타입 선언부제네릭 타입변수 타입매개변수 타입예외 타입...타입에 사용할 수 있으려면TYPE_PARAMETER : 타입 변수에만 사용할 수 있다.TYPE_USE : 타입 변수를 포함해서 모든 타입 선언부에 사용할 수 있다.중복 사용할 수 있는 애노테이션을 만들기@Repeatable애노테이션들을 감싸고 있을 컨테이너 애노테이션을 선언해야 한다.중복 사용할 애노테이션 만들기컨테이너 애노테이션은 중복 애노테이션과 @Retention 및 @Target 이 같거나 더 넓어야 한다.컨테이너이기 떄문에 , 이것은 접근 지시자의 범위와 유사한 개념이라고 볼 수 있다.@Retention : 애노테이션을 언제까지 유지할 것이냐?@Target : 애노테이션을 어디에 사용할 것이냐?애노테이션 프로세서애노테이션 프로세서는 소스코드 레벨에서 소스코드에 붙어있는애노테이션을 읽어서 컴파일러가 컴파일 하는 중에 새로은 소스코드를 생성하거나 기존 소스코드를 바꿀 수 있다.또는, 클래스(바이트코드) 도 생성할 수 있고 별개의 리소스파일을 생성할 수 있는 강력한 기능이다. 애노테이션 프로세서 사용 예롬복 (기존코드를 변경한다)AutoService (리소스 파일을 생성해준다.)java.util.ServiceLoader 용 파일 생성 유틸리티@Override애노테이션 프로세서 장점바이트코드에 대한 조작은 런타임에 발생되는 조작임으로 런타임에 대한 비용이 발생한다.but. 애노테이션 프로세서는 애플리케이션을 구동하는 런타임 시점이 아니라,컴파일 시점에 조작하여 사용함으로 런타임에 대한 비용이 제로가 된다.단점은 기존의 코드를 고치는 방법은 현재로써는 public 한 API 가 없다.롬복 같은 경우.. 기존 코드를 변경하는 방법이지만 public 한 api를 이용한 것이 아님으로 해킹이라고 할 수 도 있다.롬복(Lombok)의 동작원리Lombok@Getter @Setter, @Builder 등의 애노테이션과애노테이션 프로세서를 제공하여 표준적으로 작성해야 할 코드를 개발자 대신 생성해주는 라이브러리이다.사용하기의존성 추가IntelliJ Lombok 플로그인 설치Intellij Annotation Processing 옵션 활성화동작원리컴파일 시점에 "애노테이션 프로세서"를 사용하여 (자바가 제공하는 애노테이션 프로세서)소스코드의 AST(Abstract Syntax Tree) 를 조작한다.AST에 대한 참고 사이트 (아래 참조 참고)javax.annotation.processing || Interfaec Processor⇒ 소스코드의 AST를 원래는 참조만 할 수 있다. // 수정하지 못한다. 그리고 하면 안된다!⇒ 그러나 수정이 됬음을 알 수 있다.(컴파일 이후 바이트코드 확인)⇒ 참조만 해야 하는 것을 내부 클래스를 사용하여 기존 코드를 조작하는 것임으로 "해킹" 이라고 얘기하기도 한다. 논란 거리공개된 API가 아닌 컴파일러 내부 클래스를 사용하여 기존 소스 코드를 조작한다.특히 이클립스의 경우에는 Java Agent를 사용하여 컴파일러 클래스까지 조작하여 사용한다.해당 클래스들 역시 공개된 API가 아니다보니 버전 호환성에 문제가 생길 수도 있고 언제라도 그런 문제가 발생해도 이상하지 않다.그럼에도 불구하고 엄청난 편리함 때문에 널리 쓰이고 있으며, 대안이 몇가지 있지만 롬복의 모든 기능과 편의성을 대체하지 못하는 상황이다.AutoValueImmutables기존 Getter, Setter, equals, hasCode 등의 메소드를 생성하는 순간?해당 클래스는 이미 방대해진 모습을 볼 수 있다.해당 클래스를 위한 메소드들이 선언이 되어 있더라도 위 메소드들 사이에 파묻혀 있다면?개발자 입장에서 놓칠 수도 있다. (그래서 boilerplat 코드라는 개념도 나온다.)⇒ 롬복을 이용하여 쉽게, 그리고 가독성 높게 클래스를 구현할 수 있다. package me.sungbin; import lombok.Getter; import lombok.Setter; @Getter @Setter public class Member { private String name; private int age; }  위의 롬복이 적용된 코드를 컴파일하면 아래와 같이 나온다. package me.sungbin; public class Member { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } 결론위에서 미션에 대해 다 애기한 듯 하다. 결론을 내보겠다.어노테이션을 사용하는 이유는 단순하다.코드의 가독성과 유지보수성 향상: 어노테이션을 사용하면 개발자가 코드의 의도를 더 명확하게 표현할 수 있습니다. 예를 들어, @Override 어노테이션은 메소드가 상위 클래스의 메소드를 오버라이드한다는 것을 명시합니다.컴파일 시간 검사: 어노테이션을 통해 코드에 대한 추가적인 검사를 수행할 수 있어, 잠재적인 오류를 컴파일 시간에 발견하고 수정할 수 있습니다.런타임 처리: 특정 어노테이션이 적용된 요소를 런타임에 검사하고 처리할 수 있어, 리플렉션을 사용한 동적 처리가 가능해집니다. 이는 프레임워크와 라이브러리에서 많이 활용됩니다.이런 이유로 사용이 되며 이로인하여 코드문서화, 컴파일러에 특정처리를 지시, 코드분석 도구 지원, 런타임처리등이 가능해지게 된다. 우리가 스프링의 의존성 주입을 할 때 @Autowired도 이런 기능처리를 해준다. 컴파일러에서의 처리:코드 검증: 컴파일러는 어노테이션을 사용하여 코드에 대한 추가적인 검증을 수행합니다. 예를 들어, @Override 어노테이션은 메서드가 실제로 상위 클래스나 인터페이스의 메서드를 오버라이드하는지 확인하는 데 사용됩니다. 만약 오버라이드하는 메서드가 없다면, 컴파일러는 에러를 발생시킵니다.정책 적용: 일부 어노테이션은 컴파일러에 특정 정책을 적용하도록 지시합니다. 예를 들어, @Deprecated 어노테이션이 적용된 요소를 사용하는 코드는 컴파일러 경고를 발생시키며, 이는 개발자에게 해당 요소가 더 이상 사용되지 않아야 함을 알립니다.소스 코드 변환: 어노테이션 프로세서를 사용하여 컴파일 시점에 소스 코드를 자동으로 생성하거나 수정할 수 있습니다. 이는 코드 생성 라이브러리나 프레임워크에서 흔히 사용되는 기법입니다.런타임에서의 처리:리플렉션을 통한 접근: 런타임에는 리플렉션 API를 사용하여 어노테이션 정보에 접근할 수 있습니다. 이를 통해 개발자는 실행 중인 프로그램에서 클래스, 메서드, 필드 등에 적용된 어노테이션을 검사하고, 해당 어노테이션에 지정된 정보를 바탕으로 동적인 처리를 수행할 수 있습니다.동적 처리: 런타임에 어노테이션을 기반으로 동적 처리를 하는 예로, Java EE와 Spring 프레임워크에서 의존성 주입을 구현하는 방법을 들 수 있습니다. 이러한 프레임워크는 특정 어노테이션(@EJB, @Autowired)이 붙은 필드나 메서드를 찾아, 런타임에 자동으로 의존성을 주입합니다.구성 관리: 어플리케이션의 구성 정보를 어노테이션을 통해 관리할 수 있습니다. 예를 들어, 웹 어플리케이션에서 서블릿이나 REST 엔드포인트를 정의할 때 사용되는 어노테이션들은 런타임에 웹 서버가 해당 구성 정보를 읽어들여 서비스를 구동하는 데 사용됩니다.이러한 방식으로 어노테이션은 컴파일 시점과 런타임에 다양한 목적으로 활용됩니다. 컴파일 시점에는 코드의 정확성을 보장하고, 런타임에는 코드의 동적인 행위를 제어하는 데 중요한 역할을 합니다.커스텀 어노테이션이것 또한 위에서 예제로 많이 보여드렸으므로 어노테이션 예제를 보여줌으로 이 글을 마치려고 한다. 정말 단순히 어노테이션부터 시작해서 리플렉션까지 갔는데 정말 험난한 여정이였지만 보람찬 공부가 되었다. package me.sungbin; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; @Retention(RetentionPolicy.RUNTIME) @Target({TYPE, FIELD}) @Inherited public @interface MyAnnotation { String value(); }  📚 참조https://b-programmer.tistory.com/264http://javaparser.org/inspecting-an-ast/

백엔드인프런워킹업스터디클럽백엔드미션어노테이션

양성빈

[인프런 워밍업 스터디 클럽] 0기 백엔드 미션 - 람다식 (Day3)

과제진도표 3일차와 연결됩니다우리는 JdbcTemplate을 사용하는 과정에서 익명 클래스와 람다식이라는 자바 문법을 사용했습니다. 익명 클래스는 자바의 초창기부터 있던 기능이고, 람다식은 자바 8에서 등장한 기능입니다. 다음 키워드를 사용해 몇 가지 블로그 글을 찾아보세요! 아래 질문을 생각하며 공부해보면 좋습니다! 😊 [키워드]익명 클래스 / 람다 / 함수형 프로그래밍 / @FunctionalInterface / 스트림 API / 메소드 레퍼런스 [질문]자바의 람다식은 왜 등장했을까?람다식과 익명 클래스는 어떤 관계가 있을까? - 람다식의 문법은 어떻게 될까?람다벌써 스터디 클럽 3일차가 되었다. 이번 강의에서는 DB 쿼리들에 대해서 배우고 DB를 Spring Boot 프로젝트와 연동하여 JdbcTemplate을 이용하여 실습을 해보았다. 이 과정에서 람다식이 나왔고, 오늘은 람다식에 대해 다뤄보도록 하겠다. 자바 개발자를 위한 코틀린 입문 - 17강(람다)람다를 본격적으로 다루기 전에 강의 중에 코치님이 '자바 개발자를 위한 코틀린 입문편'에 람다를 보는 것을 추천드린다고 하셔서 학습을 해보았다. Java에서 람다를 다루기 위한 노력먼저 예시 코드를 살펴보자.package me.sungbin.lecture; public class Fruit { private final String name; private final int price; public Fruit(String name, int price) { this.name = name; this.price = price; } public String getName() { return name; } public int getPrice() { return price; } }  그리고 main 함수에 아래와 같이 작성한다. List<Fruit> fruits = Arrays.asList( new Fruit("사과", 1_000), new Fruit("사과", 1_200), new Fruit("사과", 1_200), new Fruit("사과", 1_500), new Fruit("바나나", 3_000), new Fruit("바나나", 3_200), new Fruit("바나나", 2_500), new Fruit("수박", 10_000) ); 여기서 어느 손님이 와서 "사과만 보여주세요~"라고 말한다. 그러면 우리는 개발자로서 이에 해당하는 메서드를 만들어 작성할 것이다. 그런데 갑자기 조건이 붙기 시작한다. 사과뿐만 아니라, 바나나도 보여주고 각각 가격은 5000원 이상인 것들만 보여달라고 주문한다. 🤔 그래서 우리는 고민을 하다가 일일이 메서드를 만드는 것은 불필요하기에 인터페이스를 이용하기로 한다. package me.sungbin.lecture; public interface FruitFilter { boolean isSelected(Fruit fruit); }  위와 같이 인터페이스를 만들고 아래와 같이 메서드 안에 인터페이스를 적용하고 이 메서드를 호출하는 쪽에서 익명클래스로 구현해주면 된다. package me.sungbin; import me.sungbin.lecture.Fruit; import me.sungbin.lecture.FruitFilter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * @author : rovert * @packageName : org.example * @fileName : ${NAME} * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class Main { public static void main(String[] args) { List<Fruit> fruits = Arrays.asList( new Fruit("사과", 1_000), new Fruit("사과", 1_200), new Fruit("사과", 1_200), new Fruit("사과", 1_500), new Fruit("바나나", 3_000), new Fruit("바나나", 3_200), new Fruit("바나나", 2_500), new Fruit("수박", 10_000) ); filterFruits(fruits, new FruitFilter() { @Override public boolean isSelected(Fruit fruit) { return Arrays.asList("사과", "바나나").contains(fruit.getName()) && fruit.getPrice() >= 5_000; } }); } private static List<Fruit> filterFruits(List<Fruit> fruits, FruitFilter fruitFilter) { List<Fruit> results = new ArrayList<>(); for (Fruit fruit : fruits) { if (fruitFilter.isSelected(fruit)) { results.add(fruit); } } return results; } } 🥲 익명 클래스 아쉬운 점1. 익명클래스를 사용하는 것은 딱 봐도 복잡해 보인다.2. 다양한 Filter가 필요할 수도 있다. ex) 과일 간의 무게 비교, n개의 과일을 한번에 비교등등... 이러한 이유로 JDK8부터 람다(이름 없는 함수) 등장하였다. 또한 FruitFilter와 같은 인터페이스와 같은 Predicate, Consumer등을 많이 만들어 두었다. 그래서 위의 코드는 아래와 같이 변경되었다. package me.sungbin; import me.sungbin.lecture.Fruit; import me.sungbin.lecture.FruitFilter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Predicate; /** * @author : rovert * @packageName : org.example * @fileName : ${NAME} * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class Main { public static void main(String[] args) { List<Fruit> fruits = Arrays.asList( new Fruit("사과", 1_000), new Fruit("사과", 1_200), new Fruit("사과", 1_200), new Fruit("사과", 1_500), new Fruit("바나나", 3_000), new Fruit("바나나", 3_200), new Fruit("바나나", 2_500), new Fruit("수박", 10_000) ); filterFruits(fruits, fruit -> fruit.getName().equals("사과")); } private static List<Fruit> filterFruits(List<Fruit> fruits, Predicate<Fruit> fruitFilter) { List<Fruit> results = new ArrayList<>(); for (Fruit fruit : fruits) { if (fruitFilter.test(fruit)) { results.add(fruit); } } return results; } } 💡 람다로 변경되면서 바뀐 점1. 호출하는 부분이 filterFruits(fruits, fruit -> fruit.getName().equals("사과")); 처럼 바뀌었다.2. 그 다음에 함수는 Predicate로 통하여 리팩토링을 할 수 있다. 변수 -> 변수를 이용하는 함수 혹은 (변수1, 변수2) -> 변수1과 변수2를 이용한 함수 이런 형태가 람다다. 여기서 또 JDK8에 위의 for문과 if문을 간결하게 처리하기 위해 간결한 스트림이 등장했다. (병렬처리에도 강점)그래서 코드가 아래와 같이 변경되었다. package me.sungbin; import me.sungbin.lecture.Fruit; import java.util.Arrays; import java.util.List; import java.util.function.Predicate; import java.util.stream.Collectors; public class Main { public static void main(String[] args) { List<Fruit> fruits = Arrays.asList( new Fruit("사과", 1_000), new Fruit("사과", 1_200), new Fruit("사과", 1_200), new Fruit("사과", 1_500), new Fruit("바나나", 3_000), new Fruit("바나나", 3_200), new Fruit("바나나", 2_500), new Fruit("수박", 10_000) ); filterFruits(fruits, fruit -> fruit.getName().equals("사과")); } private static List<Fruit> filterFruits(List<Fruit> fruits, Predicate<Fruit> fruitFilter) { return fruits.stream().filter(fruitFilter).collect(Collectors.toList()); } } 또한 람다는 아래와 같이 메서드 레퍼런스를 활용이 가능하다. package me.sungbin; import me.sungbin.lecture.Fruit; import java.util.Arrays; import java.util.List; import java.util.function.Predicate; import java.util.stream.Collectors; /** * @author : rovert * @packageName : org.example * @fileName : ${NAME} * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class Main { public static void main(String[] args) { List<Fruit> fruits = Arrays.asList( new Fruit("사과", 1_000), new Fruit("사과", 1_200), new Fruit("사과", 1_200), new Fruit("사과", 1_500), new Fruit("바나나", 3_000), new Fruit("바나나", 3_200), new Fruit("바나나", 2_500), new Fruit("수박", 10_000) ); filterFruits(fruits, Fruit::isApple); } private static List<Fruit> filterFruits(List<Fruit> fruits, Predicate<Fruit> fruitFilter) { return fruits.stream().filter(fruitFilter).collect(Collectors.toList()); } }비즈니스 로직을 도메인 메서드로 빼주고 이런 형식으로도 관리가 가능하다. 이렇게 자바에서 메서드 자체를 직접 넘겨주는 것처럼 보이지만 실제로는 인터페이스를 받기 때문이다.이 말은 메서드는 변수에 할당하거나 파라미터로 전달할 수 없고 2급시민으로 간주한다.위의 설명으로 잔도표에 3일차에 미션은 금방 끝나게 될 것이다. 하지만 단순 강의로 과제를 할 수는 없기에 내가 한번 더 찾아보고 학습한 내용을 공유드린다. 익명 클래스익명 클래스가 등장한 이유는 위의 강의처럼 사용자의 요구 조건이 메서드로 처리하기엔 너무 많아지고 조건이 복잡해지면서 인터페이스 혹은 추상클래스를 이용할 때 조금 간편하게 하기 위해 등장한 것이다. 조금 더 간결히 이야기를 하면 익명 클래스는 인터페이스나 추상 클래스의 인스턴스를 간편하게 생성하기 위해 등장했습니다.익명 클래스는 이름 없이 선언과 동시에 객체를 생성할 수 있는 클래스로, 주로 단일 사용 인스턴스에 대한 정의에 사용됩니다. 이러한 클래스는 GUI 이벤트 처리나 작은 콜백 객체 같은 곳에 유용하게 쓰입니다. 익명 클래스의 주된 목적은 코드의 간결성을 높이고, 즉석에서 필요한 구현을 제공하여 별도의 클래스 파일을 만들지 않아도 되게 하는 것입니다.그럼 예시 코드를 보자. package me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : AnonymousClass * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class AnonymousClass { public static void main(String[] args) { Thread myThread = new Thread(new Runnable() { @Override public void run() { System.out.println("익명 클래스를 사용한 스레드 실행"); } }); myThread.start(); // Thread start } } 람다식(Lambda Expression)자바가 1996년 등장안 이후 두번의 큰 변화가 있었다. 첫번째 JDK 1.5 부터 추가된 제네릭의 등장이고, 두번째 JDK 1.8 부터 추가된 람다식의 등장이다. 람다의 도입으로 인해 자바는 객체지향언어인 동시에 함수형 언어가 되었다. 람다식이란?람다식은 간단히 말해 메서드를 하나의 식(expression) 으로 표현한 것이다. 람다식은 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해준다.메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 '익명 함수(anonymous function)' 이라고도 한다. package me.sungbin.blog; import java.util.Arrays; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : LambdaBlogEx1 * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class LambdaBlogEx1 { public static void main(String[] args) { int[] arr = new int[5]; Arrays.setAll(arr, (i) -> (int)(Math.random() * 5) + 1); } }  위의 코드에서 () → (int)(Math.random() * 5) + 1 구문이 람다식이다. 이 람다식이 하는 일을 메서드로 굳이 표현하면 아래와 같다. int method(){ return (int)(Math.random() * 5) + 1; } 메서드 형태보다 람다식이 간결하고 이해가 쉽다.게다가 모든 메서드는 클래스에 포함되어야 하므로 클래스도 새로 만들어야 하고, 객체도 생성해야 이 메소드를 호출할 수 있지만, 람다식은 이 과정 없이 오직 람다식 자체만으로 이 메서드의 역할을 수행할 수 있는 것이 큰 장점이다. 🙋🏻 메서드와 함수 차이함수는 수학에서 따온 것이다. 수학의 함수와 개념이 유사하다. 그러나 OOP에서는 함수대신 객체의 행위나 동작을 의미하는 메서드라는 용어를 사용한다.메서드는 함수와 같은 의미지만, 특정 클래스에 반드시 속해야 한다는 제약이 있기 때문에 기존의 함수와 같은 의미를 다른 용어를 선택해서 사용한 것이다. 그러나 이제 다시 람다식을 통해 메서드가 하나의 독립적인 기능을 하기 때문에 함수라는 용어를 사용하게 되었다. 람다식 작성하기람다식은 '이름 없는 함수'답게 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통 {} 사이에 '->'를 추가한다. AS-IS 반환타입 메서드이름(매개변수 선언){ 문장들 } TO-BE (매개변수 선언) -> { 문장들 } AS-IS int max(int a, int b){ return a> b ? a: b; } TO-BE (int a, int b) -> { return a > b ? a: b; } TO-BE에서 반환 값이 있는 메서드의 경우 return문 대신 '식(expression)'으로 대신할 수 있다.식의 연산결과가 자동적으로 반환된다.문장이 아닌 식으로 끝에 ';'를 붙이지 않는다.(int a, int b) -> a > b ? a: b // TO-BE 선언된 매개변수의 타입은 추론이 가능한 경우 생략할 수 있다.람다식에 반환타입이 없는 이유도 항상 추론이 가능하기 때문이다.(a, b) -> a > b ? a : b // TO-BE 선언된 매개변수가 하나인 경우 괄호() 를 생략할 수 있다.단, 매개변수의 타입이 있으면 괄호()를 생략할 수 없다.TO-BE a -> a * a // 올바른 예 int a -> a * a // 잘못된 예괄호{} 안의 문장이 하나일 때는 괄호{}를 생략할 수 있다.문장의 끝에 ';'을 붙이지 않아야 한다는 것에 주의한다.AS-IS (String name, int i) -> { System.out.println(name+"="+i); } TO-BE (String name, int i) -> System.out.println(name+"="+i) 함수형 인터페이스자바에서 모든 메서드는 클래스 내에 포함되어야 하는데, 람다식은 어떤 클래스에 포함되는 것일까?람다식은 익명 클래스의 객체와 동등하다. (int a, int b) -> a > b ? a : b // 위(람다식)와 아래(익명 클래스의 객체 내부 메소드)와 같다 new Object(){ int max(int a, int b){ return a > b ? a : b; } } 그렇다면, 람다식으로 정의된 익명 객체의 메서드를 어떻게 호출할 수 있을 것인가?참주변수가 있어야 객체의 메서드를 호출할 수 있으니 이 익명 객체의 주소를 f 라는 참조변수에 저장해본다.타입 f = (int a, int b) -> a > b ? a : b; // 여기서 참조변수 f의 타입은??? 참조변수 f의 타입은 어떤 것이어야 할까?참조형이니깐 클래스 또는 인터페이스가 가능하다.그리고 람다식과 동등한 메서드가 정의되어 있는 것이어야 한다.그래야만 참조변수로 익명 객체(람다식)의 메서드를 호출할 수 있기 때문이다.위 내용을 바탕으로 예를 들어 max()라는 메서드가 정의된 MyFunction 인터페이스가 정의되어 있다고 가정해본다. public interface MyFunction{ public abstract int max(int a, int b); } 위 인터페이스를 구현한 익명 클래스의 객체는 아래와 같이 생성가능하다. MyFunction f = new MyFunction() { @Override public int max(int a, int b) { return a > b ? a: b; } }; int big = f.max(5, 3); System.out.println(big); 여기서 MyFunction인터페이스에 정의된 메서드 max() 는람다식 '(int a, int b) → a > b ? a: b' 와 일치한다.→ 익명 객체를 담다식으로 아래와 같이 대체할 수 있다.MyFunction f = (a, b) -> a > b ? a: b; int big = f.max(5, 3); System.out.println(big); 위와 같이 MyFunction 인터페이스를 구현한 익명 객체를 람다식으로 대체할 수 있는 이유는람다식도 실제로는 익명 객체이고, MyFunction 인터페이스를 구현한 익명 객체의 메서드 max()와 람다식의 개맥변수 타입과 개수 그리고 반환값이 일치하기 때문이다. 하나의 메서드가 선언된 인터페이스를 정의해서 람다식을 다루는 것은 기존의 자바의 규칙들을 어기지 않으면서도 자연스럽다.그렇기 때문에 인터페이스를 통해 람다식을 다루기로 결정되었으며,람다식을 다루기 위한 인터페이스를 함수형 인터페이스(functional interface)라 부르기로 했다.@FunctionalInterface public interface MyFunction{ public abstract int max(int a, int b); }단, 함수형 인터페이스에서는 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있다.그래야 람다식과 인터페이스의 메서드가 1:1로 연결될 수 있기 때문이다.다만, static 메서드와 default 메서드의 개수에는 제약이 없다.함수형 인터페이스로 구현한 인터페이스라면 반드시 '@FunctionalInterface' 애노테이션을 정의하도록 하자.컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인해주니깐📚 바이트코드로 확인 (참고. https://dreamchaser3.tistory.com/5)람다는 익명 내부 클래스와 다르다.예제로 살펴보자.FCOnline, Readypackage me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : FCOnline * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ @FunctionalInterface interface Ready { void setup(); } public class FCOnline { public void playing(Ready ready) { ready.setup(); System.out.println("FC Online is playing"); } }FCOnlineExamplepackage me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : FCOnlineExample * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class FCOnlineExample { public static void main(String[] args) { FCOnline fcOnline = new FCOnline(); fcOnline.playing(new Ready() { @Override public void setup() { System.out.println("FC Online is ready??"); } }); } }위의 기본 예제와 람다 예제로 작성하였을 시 결과는 동일한데 어떻게 실행되고바코드가 동일한지 궁금한지 확인해보았다. 예제에서 익명클래스인 FCOnlineExample$1 새로운 클래스를 생성하여 초기화를 해주고 Ready 인터페이스를 실행하는 것과 같이 확인이 된다.컴파일 시, 익명내부클래스는 $과 같은 클래스 파일이 생긴다.익명클래스는 INVOKESPECIAL 이란 OPCODE로 생성자를 호출하고, INVOKEVIRTUAL로 Setting을 호출한다. 익명클래스 & Function Type자바에서는 왜 람다를 내부적으로 익명클래스로 컴파일하지 않을까?Java8 이전 버전에서 람다를 쓰기 위한 retrolambda 같은 라이브러리나, kotlin 같은 언어에서는 컴파일 시점에 람다를 단순히 익명클래스로 치환이 된다.다만, 익명 클래스로 사용할 경우 아래와 같은 문제가 발생할 수 있다.항상 새로운 인스턴스로 할당한다.람다식마다 클래스가 하나씩 생기게 된다.람다 예제 바이트 코드 람다예제의 바이트코드에서는 기본예제의 바이트코드와 다른점이 있었다.새로운 메서드를 static으로 생성하고 있는 부분을 볼 수 있다.중간즈음 INVOKEDYNAMIC setup() ... 이라는 구문을 볼 수 있는데 이 부분이 바로INVOKEDYNAMIC CALL → INDY 이다.INDY가 호출되게되면 bootstrap 영역의 lambdafactory.metafactory()를 수행하게 된다.lambdafactory.metafactory() : java runtime library의 표준화 method어떤 방법으로 객체를 생성할지 dynamically 를 결정한다.클래스를 새로 생성, 재사용, 프록시, 래퍼클래스 등등 성능향상을 위한 최적화된 방법을 사용하게 된다.java.lang.invoke.CallSite 객체를 return 한다.LambdaMetaFactory ~ 이렇게 되어 있는 곳의 끝에 CallSite 객체를 리턴하게 된다.해당 lambda의 lambda factory, MethodHandle을 멤버변수로 가지게 된다.람다가 변환되는 함수 인터페이스의 인터페이스를 반환한다.한번만 생성되고 재호출시 재사용이 가능하다. 📚 더 알아보기INVOKEDYNAMIC으로 구현되어 있는 이유는 여러가지가 있지만 자바의 버전이 올라갈 때 인보크 다이나믹으로 구현한 부분은 하위호환성을 유지하면서 개선할 여지를 가지고 있다.중간 정리 함수형 인터페이스 (Functional Interface)추상 메소드를 딱 하나만 가지고 있는 인터페이스SAM(Single Abstract Method) 인터페이스@FunctionalInterface 에노테이션을 가지고 있는 인터페이스 람다 표현식 (Lambda Expressions)함수형 인터페이스의 인스턴스를 만드는 방법으로 쓰일 수 있다.코드를 줄일 수 있다.메소드 매개변수, 리턴파입, 변수로 만들어 사용할 수 있다.자바에서 함수형 프로그래밍함수를 First Class Object로 사용할 수 있다.순수 함수 (Pure Function)사이드 이팩트 만들 수 없다. (함수 밖에 있는 값을 변경하지 못한다.)상태가 없다. (함수 밖에 정의되어 있는)고차 함수 (High-Order Function)함수가 함수를 매개변수로 받을 수 있고 함수를 리턴할 수 있다.추상 메서드 하나만 있으면 함수형 인터페이스다.MyFunction.java Interface는 추상메서드 하나만 가지고 있기 때문에 함수형 인터페이스@FunctionalInterface public interface MyFunction { int max(int a, int b); }-> @FunctionalInterface 애노테이션을 정의하고 나서 추가적인 추상 메서드를 입력하면, 컴파일 시 에러가 발생된다. (why? 함수형 인터페이스가 아니게 됨으로) 인터페이스에 static, default 메소드를 선언할 수 있다.아래와 같이 다른 형태(static, default) 메소드가 있더라도, 추상 메소드 하나만 있다면 FunctionalInterface 이다.@FunctionalInterface public interface MyFunction { int max(int a, int b); static void printNumber() { System.out.println(1); } default void printDefaultNumber() { System.out.println(0); } } 위에서 정의한 함수형 인터페이스를 이용해보자. public class App { public static void main(String[] args) { MyFunction myFunction = new MyFunction() { @Override public void printAnyThing() { System.out.println("anything"); } }; } }위의 코드는 익명내부클래스를 정의하는 방식이다.-> 람다로 변경 public class App { public static void main(String[] args) { MyFunction myFunction = () -> System.out.println("anything"); } } public class App { public static void main(String[] args) { MyFunction myFunction = () -> { System.out.println("anything"); System.out.println("Lambda"); }; myFunction.printAnyThing(); } }함수형 인터페이스를 인라인으로 구현한 오브젝트로 볼 수 있다.위 예시와 같이 구현한 자체를 Return 하거나, 메소드의 파라미터로 전달할 수도 있다.@FunctionalInterface public interface MyFunction { void printAnyThing(String name); static void printNumber() { System.out.println(1); } default void printDefaultNumber() { System.out.println(0); } }package me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { MyFunction myFunction = name -> System.out.println(name); myFunction.printAnyThing("양성빈"); myFunction.printAnyThing("인프런"); } } 같은 값을 넣었을 때 같은 값이 나오는 것. pure한 함수그렇지 않으면, 함수형 프로그래밍이 X그렇지 않은 경우가 어떻게? → 함수 밖에 있는 값을 참조해서 사용하는 경우 (상태값에 의존한다는 의미) → 외부에 있는 값을 변경하려는 경우package me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { MyFunction myFunction = new MyFunction() { int baseNumber = 100; @Override public void printAnyThing(String name) { baseNumber++; System.out.println(baseNumber + name); } }; } }아래와 같은 경우 참조는 할 수 있지만, 변경할 수 없다.final 이라 가정하고 사용될 수 있는 경우이다.package me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { int baseNumber = 100; MyFunction myFunction = name -> System.out.println(baseNumber + name); } }  이러한 경우들은 함수형 프로그래밍과 거리가 멀다.Java에서 기본으로 제공하는 함수형 인터페이스ava.lang.function 패키지자바에서 미리 정의해둔 자주 사용할만한 함수 인터페이스Function<T, R>BiFunction<T, U, R>Consumer<T>Supplier<T>Predicate<T>UnaryOperator<T>BinaryOperator<T>....Function<T, R>값을 하나 받아서 리턴하는 일반적인 함수R apply<T>Plus.java → implements Function<Integer, Integer>Integer값을 받아서 Integer 값으로 반환하고자 함.package me.sungbin.blog; import java.util.function.Function; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : Plus * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class Plus implements Function<Integer, Integer> { @Override public Integer apply(Integer integer) { return integer + 100; } } package me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { int baseNumber = 100; MyFunction myFunction = name -> System.out.println(baseNumber + name); Plus plus = new Plus(); System.out.println(plus.apply(1)); } } 위와 같은 동작을 하는 함수를 Plus라는 별도 클래스 없이도 사용할 수 있다.Function<Integer, Integer> 함수형 인터페이스를 바로 구현package me.sungbin.blog; import java.util.function.Function; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { int baseNumber = 100; MyFunction myFunction = name -> System.out.println(baseNumber + name); Function<Integer, Integer> plus10 = (number) -> number + 100; System.out.println(plus10.apply(1)); } }  함수의 조합도 가능하다.compose입력값을 가지고 먼저 뒤에 오는 함수를 적용한다.그 결과값을 다시 입력값으로 사용하는 것이다.package me.sungbin.blog; import java.util.function.Function; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { Function<Integer, Integer> plus10 = (number) -> number + 10; Function<Integer, Integer> multiply2 = (number) -> number * 2; System.out.println(plus10.apply(1)); System.out.println(multiply2.apply(1)); Function<Integer, Integer> multiply2AndPlus10 = plus10.compose(multiply2); System.out.println(multiply2AndPlus10.apply(2)); } }andThencompose와 반대로 먼저 적용하고 뒤에 오는 함수를 적용한다.package me.sungbin.blog; import java.util.function.Function; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { Function<Integer, Integer> plus10 = (number) -> number + 10; Function<Integer, Integer> multiply2 = (number) -> number * 2; System.out.println(plus10.apply(1)); System.out.println(multiply2.apply(1)); Function<Integer, Integer> plus10AndMultiply2 = plus10.andThen(multiply2); System.out.println(plus10AndMultiply2.apply(2)); } } BiFunction<T, U, R>Function<T, R> 과 유사하지만, 입력값을 2개를 받는 것이다.(T, U) → RR apply(T t, U u) Consumer<T>리턴이 없다. | 함수 조합용 메소드 : andThenvoid Accept(T t)Consumer<T> 함수형 인터페이스 사용 예시package me.sungbin.blog; import java.util.function.Consumer; import java.util.function.Function; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { Consumer<Integer> printT = System.out::println; printT.accept(100); } } Supplier<T>T 타입의 값을 제공해주는 함수형 인터페이스T get() Supplier<T> 사용 예시package me.sungbin.blog; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { Supplier<Integer> get100 = () -> 100; System.out.println(get100.get()); } } Predicate<T>T 타입의 값을 받아서 boolean 을 반환하는 함수 인터페이스boolean test(T t) 함수 조합용 메소드And, Or, NegatePredicate<T> 사용 예시package me.sungbin.blog; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { Predicate<String> startsWithYang = (str) -> str.startsWith("yang"); Predicate<Integer> isEven = (i) -> i % 2 == 0; } } UnaryOperator<T>Function<T, R>의 특수한 형태입력값 하나를 받아서 동일한 타입을 리턴하는 함수 인터페이스입력/리턴 값이 같으므로 이전의 Function<T, R> 을 아래와 같이 변경할 수 있다.package me.sungbin.blog; import java.util.function.*; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { UnaryOperator<Integer> plus10 = (number) -> number + 10; UnaryOperator<Integer> multiply2 = (number) -> number * 2; } } BinaryOperator<T>BiFunction<T, U, R> 의 특수한 형태동일한 타입의 입력값 2개를 받아서 리턴하는 함수 인터페이스3개의 타입이 다 같을 것이라는 가정으로 작성됨. 📚 자바 api에서 제공해주는 함수형 인터페이스의 별명Function별명 : 트랜스포머(변신로봇)이유 : 값을 변환하기 때문에!Consumer별명 : Spartan (스파르탄!)이유 : 모든 것을 빼앗고 아무것도 내주지 마라 !Predicate별명 : 판사이유 : 참 거짓으로 판단하기 때문Suppliers별명 : 게으른 공급자.이유 : 입력값이 존재하지 않는데, 내가 원하는 것을 미리 준비하기 때문람다(인자 리스트) → {바디}인자 리스트인자가 없을 때 : ()인자가 한개 일 때 : (one) 또는 one인자가 여러개 일 때 : (one, two)인자 타입은 생략 가능→ 컴파일러가 추론(infer)하지만 명시할 수도 있다. → (Integer one, Integer two)바디화살표 오른쪽에 함수 본문을 정의여러 줄인 경우 { } 을 사용하여 묶는다.한 줄인 경우 생략 가능, return 또한 생략 가능하다.변수 캡쳐 (Variable Capture)로컬 변수 캡쳐final이거나 effective final인 경우에만 참조할 수 있다.변수를 변경되도록 수정해보면 이것은 effective final 인지 아닌지 확인가능하다.변경이 되면 effective final 이 아니게 되며, 람다에서 사용할 수 없게 된다.그렇지 않을 경우 concurrency 문제가 생길 수 있어서 컴파일이 불가능하다.effective final이것 역시 자바 8 부터 지원하는 기능으로 "사실상" final인 변수.final 키워드 사용하지 않은 변수를 익명 클래스 구현체 또는 람다에서 참조할 수 있다.익명 클래스 구현체와 달리 "쉐도윙" 하지 않는다.익명 클래스는 새로 스콥을 만들지만, 람다는 람다를 감싸고 있는 스콥과 같다.변수 캡쳐 케이스 예.→ run() 메소드의 int baseNumber 는 IntConsumer 람다에서 참조되고 있다.package me.sungbin.blog; import java.util.function.*; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : BlogExample * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class BlogExample { public static void main(String[] args) { /** * 순수함수프로그래밍을 할려면 외부에 있는 값을 변경하거나 참조하면 안된다. 함수 내부 및 파라미터만 가지고 써야한다. */ // int baseNumber = 10; // RunSomething runSomething = number -> number + baseNumber; // System.out.println(runSomething.doIt(2)); // // UnaryOperator<Integer> plus10 = (i) -> i + 10; // UnaryOperator<Integer> multiply2 = (i) -> i * 2; // Function<Integer, Integer> multiply2AndPlus10 = plus10.compose(multiply2); // // Consumer<Integer> printT = System.out::println; // Supplier<Integer> get10 = () -> 10; // // Predicate<String> startsWithSungbin = (str) -> str.startsWith("sungbin"); // // System.out.println(plus10.apply(1)); // System.out.println(multiply2.apply(1)); // System.out.println(multiply2AndPlus10.apply(2)); // System.out.println(plus10.andThen(multiply2).apply(2)); // printT.accept(10); // System.out.println(get10.get()); // System.out.println(startsWithSungbin.test("sungbin")); UnaryOperator<Integer> plus10 = (i) -> i + 10; UnaryOperator<Integer> multiply2 = (i) -> i * 2; System.out.println(plus10.andThen(multiply2).apply(2)); Supplier<Integer> get10 = () -> 10; BinaryOperator<Integer> sum = Integer::sum; BlogExample example = new BlogExample(); example.run(); } private void run() { int baseNumber = 10; // 내부 클래스 :: 쉐도잉 가능 class LocalClass { void printBaseNumber() { int baseNumber = 11; System.out.println(baseNumber); } } // 익명 클래스 :: 쉐도잉 가능 Consumer<Integer> integerConsumer = new Consumer<Integer>() { @Override public void accept(Integer baseNumber) { System.out.println(baseNumber); } }; // 람다 IntConsumer printInt = (i) -> { System.out.println(i + baseNumber); }; printInt.accept(10); LocalClass localClass = new LocalClass(); localClass.printBaseNumber(); integerConsumer.accept(11); } }  로컬 클래스와 익명 클래스 <> 람다와 다른 점→ 쉐도윙 (가려지는 것, 덮어지는 것)로컬 클래스와 익명 클래스는 메소드 내에서 새로운 Scope 이다.→ 쉐도잉 발생-> 쉐도잉 발생 int baseNumber = 10; // 1. 로컬 클래스 class LocalClass{ void PrintBaseNumber(){ int baseNumber = 11; // baseNumber 값은 11이 찍힐 것이다. (scope) // run 메소드에서 선언한 baseNumber에 대해 쉐도잉이 발생 System.out.println(baseNumber); } } // 2. 익명 클래스 Consumer<Integer> integerConsumer = new Consumer<Integer>() { @Override public void accept(Integer baseNumber) { // 파라미터로 전달받은 baseNumber 가 찍힐 것이다. // run 메소드에서 선언한 baseNumber에 대해 쉐도잉이 발생 System.out.println(baseNumber); } }; 람다는 람다를 감싸고 있는 메소드와 같은 Scope이다. → 같은 이름의 변수를 선언할 수 없다.→ 람다에 들어있는 변수와 람다가 사용되고 있는 클래스의 변수들은 같은 Scope이다.int baseNumber = 10; // 3. 람다 IntConsumer printInt = (baseNumber) -> { System.out.println(baseNumber); };위와 같이 선언하게 되는경우 에러가 발생하게 된다.→ Variable 'baseNumber' is already defined in the scope Variable Capture의 자세한 설명 추가람다식의 실행 코드 블록 내에서 클래스의 멤버 필드와 멤버 메소드, 그리고 지역 변수를 사용할 수 있다.클래스의 멤버 필드와 멤버 메소드는 특별한 제약없이 사용 가능하지만, 지역변수를 사용함에 있어 제약이 존재한다.이 내용을 이해하기 위해서는 jvm 메모리에 대해 알아야 한다. 잠시 람다식이 아닌 다른 얘기를 해보자.멤버 메소드 내부에서 클래스의 객체를 생성해서 사용할 경우 다음과 같은 문제가 있다.익명 구현 객체를 푸함해서 객체를 생성할 경우 new 라는 키워드를 사용한다.new라는 키워드를 사용한다는 것은 동적 메모리 할당 영역(이하 heap)에 객체를 생성한다는 의미이다.이렇게 생성된 객체는 자신을 감싸고 있는 멤버 메소드의 실행이 끝난 이후에도 heap영역에 존재하므로 사용할 수 있지만, 이 멤버 메소드에 정의된 매개변수나 지역 변수는 런타임 스택 영역(이하 stack)에 할당되어 메소드 실행이 끝나면 해당 영역에서 사라져 더 이상 사용할 수 없게 된다.그렇기 때문에 멤버 메소드 내부에서 생성된 객체가 자신을 감싸고 있는 메소드의 매개변수나 지역변수를 사용하려 할 때 문제가 생길 수 있다.클래스의 멤버 메소드의 매개변수와 이 메소드 실행 블록 내부의 지역변수는 JVM의 STACK에 생성되고 실행이 끝나면 STACK에서 사라진다.new 연산자를 사용해서 생성한 객체는 JVM의 HEAP영역에 객체가 생성되고 GC(Garbage Collector)에 의해 관리되며, 더 이상 사용하지 않는 객체에 대해 필요한 경우 메모리에서 제거한다.heap에 생성된 객체가 stack의 변수를 사용하려고 하는데, 사용하는 시점에 stack에 더 이상 해당 변수가 존재하지 않을 수 있다는 것이다.왜냐하면 stack은 메소드 실행이 끝나면 매개변수나 지역변수에 대해 제거하기 때문이다.그래서 더 이상 존재하지 않는 변수를 사용하려 할 수 있기 떄문에 오류가 발생한다.→ 자바는 이 문제를 Variable Capture 라고 하는 값 복사를 사용해서 해결한다.즉, 컴파일 시점에 멤버 메소드의 매개변수나 지역변수를 멤버 메소드 내부에서 생성한 객체가 사용할 경우 객체 내부로 값을 복사해서 사용한다.하지 모든 값을 복사해서 사용할 수 있는 것은 아니다.여기에도 제약이 존재하는데 final 키워드로 작성되었거나 final 성격을 가져야 한다.final 키워드는 알겠는데 final 성격을 가져야한다는 것은 왜그럴까?final 성격을 가진다는 것은 final 키워드로 선언된 것은 아니지만 값이 한번만 할당되어 final 처럼 쓰이는 것을 뜻한다.복잡한 내용과 예제가 존재하지만, 쉽게 생각한다면 익명 구현 객체를 사용할 때와 람다식을 사용했을 때 다음과 같은 차이점이 있다는 것만이라도 기억해보자.람다식은 익명 구현 객체 처럼 별도의 객체를 생성하거나 컴파일 결과 별도의 클래스를 생성하지 않는 다는 것이다.람다식 내부에서 사용하는 변수는 Variable Capture가 발생하며, 이 값은 final이거나 final처럼 사용해야 한다는 것이다.익명 구현 객체에 대해서는 new를 사용해서 객체도 생성된 것으로 보이고 별도 클래스 파일이 생긴 것을 확인할 수 있을 것이다.람다식이 쓰인 부분에서는 INVOKEDYNAMIC 이라는 OPCODE를 사용했는데, JAVA8 부터 생긴 것으로 interface의 default method와 lambda 식에서 사용된다고 한다.메소드 레퍼런스람다가 하는 일이 기준 메소드 또는 생성자를 호출하는 것이라면, 메소드 레퍼런스를 사용해서 매우 간결하게 표현할 수 있다. 메소드 참조하는 방법스태틱 메소드 참조 → 타입::스태틱 메소드특정 객체의 인스턴스 메소드 참조 → 객체 래퍼런스::인스턴스 메소드임의 객체의 인스턴스 메소드 참조 → 타입::인스턴스 메소드생성자 참조 → 타입::new메소드 또는 생성자의 매개변수로 람다의 입력값을 받는다.리턴값 또는 생성한 객체는 람다의 리턴 값이다. package me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : Eating * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class Eating { private String food; public Eating() { } public Eating(String food) { this.food = food; } public String eat(String food) { return "eating " + food; } public static String keepEat(String food) { return "keep eating " + food; } } Function<T, R> 을 이용해 구현 가능하지만, 동일한 작업을 하는 Greeting 객체의 메소드를 활용하여 아래와 같이 작업해볼 수 있다.메소드 레퍼런스 -> Eating::keepEatpackage me.sungbin.blog; import java.util.function.UnaryOperator; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { UnaryOperator<String> keepEatFunction = (s) -> "hi " + s; UnaryOperator<String> keepEatingObject = Eating::keepEat; System.out.println(keepEatingObject.apply("햄버거")); } }인스턴스 메서드 사용package me.sungbin.blog; import java.util.function.UnaryOperator; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { Eating eating = new Eating(); UnaryOperator<String> printEat = eating::eat; System.out.println(printEat.apply("햄버거")); } }  생성자 사용Supplier를 이용한 것과 Function을 이용한 생성자 호출은 엄연히 다르다. Supplier는 인자가 없고 Function은 인자가 있다.사용하는 부분인 메소드 레퍼런스만 보면 "Eating::new" 와 동일하지만 다르다는 점!package me.sungbin.blog; import java.util.function.Supplier; import java.util.function.UnaryOperator; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { // 입력값은 없는데 반환값은 있는 함수형 인터페이스 > Supplier Supplier<Eating> newEating = Eating::new; Eating eating = newEating.get(); } }package me.sungbin.blog; import java.util.function.Function; import java.util.function.Supplier; import java.util.function.UnaryOperator; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { // 입력값 T 를 받아 R 반환 함수형 인터페이스 > Function Function<String, Eating> hamburgerEating = Eating::new; Eating hamburger = hamburgerEating.apply("햄버거"); } } 임의의 객체를 참조하는 메소드 레퍼런스package me.sungbin.blog; import java.util.Arrays; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { String[] names = {"A", "B", "C", "D"}; Arrays.sort(names, String::compareToIgnoreCase); System.out.println(Arrays.toString(names)); } }람다식의 타입과 형변환 정리!함수형 인터페이스로 람다식을 참조할 수 있는 것일 뿐이지 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아니다.람다식은 익명 객체이고 익명 객체는 타입이 없다.정확히는 타입은 있지만 컴파일러가 임의로 이름을 정하기 때문에 알 수 없는 것이다.그래대입 연산자의 양변의 타입을 일치시키기 위해 형변환이 필요하다. MyFunction f = (MyFunction)(() -> {...});람다식은 MyFunction 인터페이스를 직접 구현하지 않았지만, 이 인터페이스를 구현한 클래스의 객체와 완전히 동일하기 때문에 위와 같은 형변환을 허용한다. 그리고 이 형변환은 생략가능하다.람다식은 이름이 없을 뿐 분명히 객체인데도, Object 타입으로 형변환할 수 없다.람다식은 오직 함수형 인터페이스로만 형변환이 가능하다. Object obj = (Object)( () -> { ... }); // ERROR. 함수형 인터페이스로만 가능하다. 굳이 변경하고자 한다면, 함수형 인터페이스로 변환하고 난 후 가능하다.다음 예제를 통해 컴파일러가 람다식의 타입을 어떤 형식으로 만들어내는지 알아보자. package me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : MyFunction02 * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ @FunctionalInterface public interface MyFunction02 { void myMethod(); }package me.sungbin.blog; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { MyFunction02 f = () -> {}; Object obj = (MyFunction02) (() -> {}); String str = ((Object) (MyFunction02) (() -> {})).toString(); System.out.println(f); System.out.println(obj); System.out.println(str); System.out.println((MyFunction02) (() -> {})); System.out.println(((Object) (MyFunction02) (() -> {})).toString()); } } 일반적인 익명 객체라면, 객체의 타입이 외부클래스이름$번호 와 같은 형식으로 타입이 결정되었을텐데, 람다식의 타입은 외부클래스이름$Lambda$번호 와 같은 형식으로 되어 있는 것을 확인할 수 있다. INDY 상세한 정리!INVOKEDYNAMIC 의 내부 동작 분석Java SE7 부터 등장한 새로운 바이트코드 셋이다.기존에는 invoke 시리즈는 4가지만 존재하였다. (invoke + virtual/static/interface/special)invokevirtual : instance 메소드를 디스패치 하기 위한 명령어invokestatic : static 메소드를 디스패치 하기 위한 명령어invokeinterface : 인터페이스를 통해서 method를 디스패치 하기 위한 명령어invokespecial : 생성자, 수퍼클래스, private method등 invoke-virtual이 아닌 메소드들을 디스패치 하기 위한 명령어저 명령어들을 보면 어느 한 메소드를 부르기 위해서는 아래와 같은 4가지 메소드 정보가java에 명확하게 선언되어 있어야 한다.메소드의 이름메소드 시그니처 + return type메소드가 정의되어 있는 클래스메소드를 실행할 수 있는 바이트 코드.즉, 메소드를 부르기 위해서는 위와 같은 정보들이 자바 lang을 통해 고정되어 있고 이미 모든 것이 구현되어 있고 컴파일까지 완벽하게 되어 있어서 JVM에서 바이트코드를 읽기만 하면 된다.Java SE7 에서는 언어의 로직들이 컴파일러가 번역해준 메소드 실행 로직이 아닌, 런타임에서 메소드를 실행할 CALL TARGET을 정할 수 있도록 INVOKEDYNAMIC이라는 바이트코드를 추가하였다. Java 코드로 작성된 Lambda랑 INVOKEDYNAMIC 과 무슨 상관일까?invokedynamic의 예시를 보면, 마치 다양한 언어를 JVM 위에서 돌릴 수 있도록 추가된 바이트코드처럼 보인다.람다는 Java로 쓰여져있고, 람다를 통해 생성된 바이트코드를 보면 모두 컴파일 시점에 자바 메소드로 재분해(desugar) 확인할 수 있다.그렇다면 람다가 굳이 invokedynamic을 사용하도록 컴파일되는 이유는 무엇일까? 특정 translation strategy (람다를 → 가용가능한 자바 로직으로 변환하는 과정 및 전략) 에는 두가지 고려사항이 존재한다.미래의 최적화를 위해서 특정 전략으로 고정하지 않는 것. (즉, 최대한 바이트코드로 고정되어 컴파일 되지 않게 하는 것)특정 translation strategy (실제 자바 실행 로직으로 변환하는 과정 및 전략)을 런타임에서 결정할 수 있다. 따라서, 전략이 미래에 변해서 JVM 스팩이 업데이트 되었다고 하더라도 소스코드 수정이나, 재컴파일 없이 그대로 실행 가능하다.만약 람다가 컴파일 타임에서 완벽하게 변환되었다고 한다면, 나중에 수정사항이 있을 경우 프로젝트를 모두 재컴파일 해야 하는 일이 발생한다.클래스 파일 표현에 안정성을 가지는 방법람다는 두가지의 이점을 가지기 위해,'람다라는 표현 () → {}' 을 '실제 자바 메소드 실행 로직' (바이트코드로 나온 실행 메소드)로 컴파일 타임이 아닌 INVOKEDYNAMIC 을 통해서 런타임에서 로직을 연결하고 실행한다.이를통해서, 실제 람다 표현을 실행하는 로직을 결정하는 전략을 런타임에서 LAZY 하게 셜정할 수 있다는 것이 장점이라고 생각된다. Invokedynamic + lambda 실행public class SimpleLambda{ public static void main(String[] args){ Runnable lambda = invokedynamic( bootstrap = LambdaMetafactory, staticargs = [Runnable, lambda$0], dynargs=[]); lambda.run(); } private static void lambda$0(){ System.out.println(1); } }📚 참고: https://d2.naver.com/helloworld/4911107?fbclid=IwAR2KrFe7ksfRr4cDQWWGqvFpQDB6B4MVCh_zlMjZFZh5NVD5KSWHg8nV46U invokedynamic()Bootstrap Method (or BSM)부트스트랩 메소드는 invokedynamic() + bootstrap method()를 통해 들어온 정보를 기반으로, 호출 대상을 찾아서 연결하고 CallSite 객체를 반환한다.CallSite가 반환되기 전을 unlaced 상태(실제 로직이 연결되지 않은 상태)라고 하며, invokedynamic() 콜이 제대로 불려 실제로 linkage가 일어나면 **laced(연결된) 상태**라고 불린다.VM이 bootstrap method를 부를 때에, CallSite를 lazy 하게 반환하며, 반환 후에는 CallSite를 통해 람다와 실제 함수 구현 부분이 연결된다.연결을 한번 한 뒤에는 계속 연결상태가 되기 때문에, 따로 링킹을 시도하지 않는다.Lambda는 LambdaMetafactory.metafactory 라는 부트스트랩 메소드를 부른다.1-1). CallSite & MethodHandle이 CallSite 객체에는 실제 메소드를 실행시킬 메소드 (아까 바이트코드에서 봤던 private static method) 포함되어 있다.📚 참조 https://www.slideshare.net/DanHeidinga/invokedynamic-evolution-of-a-language-feature CallSite 가 멤버변수로 가지고 있는 실제 로직을 가리키고 있는 메소드는 MethodHandle로 표현된다.아래의 코드 예제를 보면, 일반적으로 Reflection API를 통해 메소드를 Method 객체를 얻은것과 비슷해 보이지만 훨씬 더 VM 레벨에서 작동하는 메소드이다. public MethodHandle getToStringMH() { MethodHandle mh = null; // 메소드 타입 부터 첫번째 파라미터는 Return type, 두번째부터는 메소드 파라미터들.. MethodType mt = MethodType.methodType(String.class); // 메소드 handles의 lookup() = "lookup context" MethodHandles.Lookup lk = MethodHandles.lookup(); try { // mh를 통해 toString메소드를 찾음. mh = lk.findVirtual(getClass(), "toString", mt); } catch (NoSuchMethodException | IllegalAccessException mhx) { throw (AssertionError)new AssertionError().initCause(mhx); } return mh; } 2) staticargs, dynargsstaticargs : 바이트 코드로 변환된 람다 실제 메소드 정보dynargs : 람다 스코프 외 외부변수 참조 시, dynargs에 포함됨. invokedynamic 이 부트스트랩 메소드를 여러 인자 요소들(메소드 정보, 메소드 구현체 정보, 타입 정보 등..)과 함께 부른다. BootstrapMethod는 CallSite를 반환한다. CallSite는 target method의 정보를 가지고 있는 method handler를 멤버 변수로 가지고 있고, 이 method handler를 통해 실제 동작하는 메소드와 연결된다.즉, lambda로 인해 컴파일 시점에 추가된 static/instance 함수 (예 : private static void lambda$0)를 LambdaMetaFactory가 MethodHandler를 통해 관리해주고 있는 형태로 볼 수 있다.Stream 소개Streamsequence of elements supporting sequential and parallel aggregate operations데이터를 담고 있는 저장소(컬렉션)이 아니다.Functional in nature, 스트림이 처리하는 데이터 소스를 변경하지 않는다.→ Functional 하다.→ 결과가 또다른 stream이 되는 것이지, 전달받은 데이터 자체가 변경되는 것이 아니다.스트림으로 처리하는 데이터는 오직 한번만 처리한다.→ 컨베이어 밸트에 항목이 한번 지나가는 것이라고 보면된다. (한번 지나면 끝)무제한일 수도 있다. (Short Circuit 메소드를 사용해서 제한할 수 있다.)중개 오퍼레이션은 근본적으로 lazy 하다.→ stream에 사용하는 것은 2개로 나눌 수 있다. (중개, 종료)→ 중개 오퍼레이션은 lazy.. ???→ 중개형 오퍼레이션은 종료 오퍼레이션이 오기 전까지 실행되지 않는다. package me.sungbin.blog; import java.util.ArrayList; import java.util.List; import java.util.Locale; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { List<String> names = new ArrayList<>(); names.add("손흥민"); names.add("박지성"); names.add("이강인"); names.add("김민재"); names.add("조규성"); names.stream().map(x -> { System.out.println(x); return x.toUpperCase(Locale.ROOT); }); System.out.println("=========="); names.forEach(System.out::println); } }.map() 바디에 선언한 System.out.println 은 찍히지 않는 것을 볼 수 있다.종료형 오퍼레이션이 반드시 한번 와야 하며, 종료형 오퍼레이션이 오지 않으면 중계형 오퍼레이터는 의미가 없다. (실행x) 손쉽게 병렬 처리 할 수 있다.병렬처리를 하는 것이 모두 빠른 것이 아니다. 더 느릴 수 있다. Thread를 만들어서 Thread별로 병렬로 처리하고 수집하는 일련의 과정이 발생된다.데이터가 정말 방대하게 큰 경우 유용하게 사용될 수 있으나, 그게 아니라면 stream 권장Stream Pipline0 또는 다수의 중개 오퍼레이션 (intermediate operation)과 한개의 종료 오퍼레이션(terminal operation)으로 구성한다.스트림의 데이터 소스는 오직 터미널 오퍼레이션을 실행할 때만 처리한다.중개 오퍼레이션Stream을 리턴한다.Stateless / Stateful 오퍼레이션으로 더 상세하게 구분할 수도 있다.대부분 Stateless 지만 distinct나 sorted 처럼 이전 소스 데이터를 참조해야 하는 오퍼레이션은 Stateful 오퍼레이션이다.filter, map, limit, skip, sorted ...종료 오퍼레이션Stream을 리턴하지 않는다.collect, allMatch, count, forEach, min, maxStream API 걸러내기Filter(Predicate)예) 이름이 3글자 이상인 데이터만 새로운 스트림으로변경하기Map(Function) 또는 FlatMap(Function)예) 각각의 Post인스턴스에서 String Title만 새로운 스트림으로예) List<STream<String>> 을 String의 스트림으로생성하기generate(Supplier) 또는 Iterate(T seed, UnaryOperator)예) 10부터 1씩 증가하는 무제한 숫자 스트림예) 랜덤 int 무제한 스트림제한하기limit(long) 또는 skip(long)예) 최대 5개의 요소가 담긴 스트림을 리턴한다.예) 앞에서 3개를 뺀 나머지 스트림을 리턴한다.package me.sungbin.blog; import java.util.ArrayList; import java.util.List; import java.util.Locale; /** * @author : rovert * @packageName : me.sungbin.blog * @fileName : App * @date : 2/21/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/21/24 rovert 최초 생성 */ public class App { public static void main(String[] args) { List<Blog> developBlogs = new ArrayList<>(); developBlogs.add(new Blog(1L, "자바", "스트림 API", true)); developBlogs.add(new Blog(1L, "mysql", "롤백", true)); developBlogs.add(new Blog(1L, "spring boot", "DI", false)); developBlogs.add(new Blog(1L, "spring data jpa", "트랜잭션", false)); developBlogs.add(new Blog(1L, "spring security", "cors", false)); System.out.println("spring 으로 시작하는 블로그"); System.out.println("공개 블로그"); System.out.println("블로그 이름만 만들어서 스트림 만들기"); } } 1. spring으로 시작하는 수업 만들기System.out.println("spring 으로 시작하는 수업"); List<Blog> blogsByTitleIsSpringStartWith = developBlogs.stream() .filter(x -> x.getTitle().startsWith("spring")) .collect(Collectors.toList()); blogsByTitleIsSpringStartWith.forEach(x -> System.out.println(" > " + x.getTitle())); 2. secret되지 않는 블로그System.out.println("공개 블로그"); List<Blog> notSecretBlog = developBlogs.stream() .filter(x -> !x.isSecret()) .collect(Collectors.toList()); notSecretBlog.forEach(x -> System.out.println(" > " + x.getTitle())); 3. 블로그 이름만 모아서 스트림 만들기List<String> blogsByTitle = developBlogs.stream() .map(Blog::getTitle) .collect(Collectors.toList()); blogsByTitle.forEach(x -> System.out.println(" > " + x));연습 https://www.inflearn.com/course/the-java-java8public class StreamApp { public static void main(String[] args) { List<OnlineClass> springClasses = new ArrayList<>(); springClasses.add(new OnlineClass(1, "spring boot", true)); springClasses.add(new OnlineClass(2, "spring data jpa", true)); springClasses.add(new OnlineClass(3, "spring mvc", false)); springClasses.add(new OnlineClass(4, "spring core", false)); springClasses.add(new OnlineClass(5, "rest api development", false)); List<OnlineClass> javaClasses = new ArrayList<>(); javaClasses.add(new OnlineClass(6, "The Java, Test", true)); javaClasses.add(new OnlineClass(7, "The Java, Code manipulation", true)); javaClasses.add(new OnlineClass(8, "The Java, 8 to 11", false)); List<List<OnlineClass>> ssonEvents = new ArrayList<>(); ssonEvents.add(springClasses); ssonEvents.add(javaClasses); System.out.println("두 수업 목록에 들어있는 모든 수업 아이디 출력"); // todo System.out.println("10부터 1씩 증가하는 무제한 스트림 중에서 앞에 10개 빼고 최대 10개 까지만"); // todo System.out.println("자바 수업 중에 Test가 들어있는 수업이 있는지 확인"); // todo System.out.println("스프링 수업 중에 제목에 spring이 들어간 것만 모아서 List로 만들기"); // todo } } 1. 두 수업 목록에 들어있는 모든 수업 아이디 출력리스트를 항목으로 갖고 있는 것을 Flat 하게 변형한다. → 안에 있는 것들을 다 꺼낸다(?)FlatMap → 모든 항목들을 풀어내는 것System.out.println("두 수업 목록에 들어있는 모든 수업 아이디 출력"); ssonEvents.stream().flatMap(Collection::stream) .forEach(oc -> System.out.println(oc.getId())); 2. 10부터 1씩 증가하는 무제한 스트림 중에서 앞에 10개 빼고 최대 10개 까지만Stream.iterator → skip, limitSystem.out.println("10부터 1씩 증가하는 무제한 스트림 중에서 앞에 10개 빼고 최대 10개 까지만"); Stream.iterate(10, i -> i + 1) .skip(10) .limit(10) .forEach(System.out::println); 3. 자바 수업 중에 Test가 들어있는 수업이 있는지 확인Match (any, all ,...)System.out.println("자바 수업 중에 Test가 들어있는 수업이 있는지 확인"); boolean isTestClasses = javaClasses.stream().anyMatch(x -> x.getTitle().contains("Test")); System.out.println("isTestClasses : " + isTestClasses);4. 스프링 수업 중에 제목에 spring이 들어간 타이틀만 모아서 List로 만들기System.out.println("스프링 수업 중에 제목에 spring이 들어간 제목만 모아서 List로 만들기"); List<String> springTitleClasses = springClasses.stream() .filter(x -> x.getTitle().contains("spring")) .map(OnlineClass::getTitle) .collect(Collectors.toList()); springTitleClasses.forEach(x -> System.out.println(" > " + x));🛠 마무리이렇게 익명클래스, 람다, 함수형 프로그래밍, @FunctionalInterface, 스트림 API, 메서드 레퍼런스에 대해 알아보고 깊게 파보았다.그러면 질문에 대해 대답해보자. Q.자바의 람다식은 왜 등장했을까?자바의 람다식은 함수형 프로그래밍을 자바에 통합하기 위해 자바 8에서 도입됐다. 이는 한 개의 추상 메소드를 가진 인터페이스를 간결하게 구현할 수 있게 해주며, 코드 양을 줄이고 가독성을 향상시키는 이점을 제공한다. 람다식과 익명 클래스는 모두 익명 함수를 구현하는 방식이지만, 람다식은 더 간결한 문법과 함수형 인터페이스의 직접적 사용을 가능하게 한다. 람다식의 도입으로 자바에서도 함수형 프로그래밍 패러다임을 효율적으로 적용할 수 있게 됐다.람다식은 자바 8에서 처음 소개됐으며, 함수형 프로그래밍 개념을 자바에 도입하기 위한 목적으로 만들어졌다. 익명 클래스에 비해 람다식은 코드를 더 간결하게 만들고, 함수형 인터페이스를 이용해 메소드를 보다 직관적으로 표현할 수 있다. 람다식은 (매개변수) -> { 표현식 } 형태로 작성되며, 이를 통해 인터페이스의 구현체를 더욱 간단하게 작성할 수 있다.함수형 인터페이스는 단 하나의 추상 메소드를 가지는 인터페이스로, @FunctionalInterface 어노테이션을 사용해 명시적으로 정의할 수 있다. 이 인터페이스는 람다식을 통해 구현될 수 있으며, 자바는 java.util.function 패키지를 통해 다양한 함수형 인터페이스를 제공한다. 이를 통해 개발자는 보다 함수적인 프로그래밍 방식을 자바에서도 적용할 수 있게 됐다.람다식의 도입은 자바의 내부적인 동작 방식에도 영향을 미쳤다. 람다식은 invokedynamic 바이트코드 명령을 사용해 동적으로 메소드 타입과 메소드 핸들을 결정한다. 이는 람다식이 실행될 때마다 인터페이스의 메소드 호출이 아니라, 실제로 실행되는 함수형 인터페이스의 구현체를 동적으로 생성하고 호출하는 메커니즘을 가능하게 한다. 이 과정은 자바 가상 머신(JVM)의 성능 최적화와 밀접하게 관련되어 있으며, 람다식을 통한 함수형 프로그래밍의 효율적인 실행을 지원한다.Q. 람다식과 익명 클래스는 어떤 관계가 있을까? - 람다식의 문법은 어떻게 될까?문법은 위에서 설명을 했으니 위의 질문 혹은 상단 블로그에 참조바랍니다.람다식과 익명 클래스 모두 인터페이스의 구현체를 생성하는데 사용될 수 있지만, 람다식은 함수형 인터페이스에 한정된다는 점에서 차이가 있다. 람다식은 문법적으로 더 간결하며, 코드를 더 읽기 쉽게 만들어 준다. 반면, 익명 클래스는 여러 메소드를 오버라이드해야 할 때 또는 함수형 인터페이스가 아닌 경우에 여전히 유용하다.람다식은 자바의 함수형 프로그래밍 패러다임을 강화하는데 기여했으며, 익명 클래스보다 더 간결하고 표현력 있는 코드 작성을 가능하게 한다. 하지만, 익명 클래스는 람다식으로 대체할 수 없는 경우에 여전히 그 가치를 지닌다. 예를 들어, 여러 메소드를 구현해야 하거나, 슈퍼 클래스의 생성자를 호출해야 하는 경우 익명 클래스를 사용해야 한다.람다식의 도입으로 자바 개발자들은 보다 함수적인 접근 방식을 취할 수 있게 되었고, 이는 자바 프로그래밍 언어의 발전에 중요한 역할을 했다. 익명 클래스와 람다식은 각각의 사용 사례에 따라 선택적으로 사용되어, 자바 프로그래밍의 유연성을 높이는 데 기여한다.📚 참조https://inf.run/XKQg)https://inf.run/r9oUhttps://dreamchaser3.tistory.com/5https://d2.naver.com/helloworld/4911107?fbclid=IwAR2KrFe7ksfRr4cDQWWGqvFpQDB6B4MVCh_zlMjZFZh5NVD5KSWHg8nV46Uhttps://www.slideshare.net/DanHeidinga/invokedynamic-evolution-of-a-language-featurehttps://www.inflearn.com/course/the-java-java8#https://tourspace.tistory.com/11?category=788398

백엔드인프런워킹업스터디클럽람다함수형프로그래밍

채널톡 아이콘