HashMap: {1=Python, 2=JavaScript}
HashMap after put():
{1=Python, 2=JavaScript, 3=Java}
HashMap after replace():
{1=Python, 2=JavaScript}
put() 및 replace() 메서드의 구문은 HashMap에서 매우 유사합니다.
// syntax of put()
hashmap.put(key, value)
// syntax of replace()
hashmap.replace(key, value)
그리고 해시맵에 지정된 키에 대한 매핑이 포함되어 있으면 두 메서드 모두 지정된 키와 연결된 값을 바꿉니다. 그러나 해시맵에 지정된 키로 찾을 수 있는 값이 없는 경우
* put() 메서드는 지정된 Key와 Value에 대한 새 데이터를 삽입합니다.
* replace() 메서드는 null을 반환합니다.
import java.util.HashMap;
class Main {
public static void main(String[] args) {
// create an HashMap
HashMap<Integer, String> languages1 = new HashMap<>();
// insert entries to HashMap
languages1.put(1, "Python");
languages1.put(2, "JavaScript");
// create another HashMap similar to languages1
HashMap<Integer, String> languages2 = new HashMap<>();
// puts all entries from languages1 to languages2
languages2.putAll(languages1);
System.out.println("HashMap: " + languages1);
// use of put()
languages2.put(3, "Java");
System.out.println("HashMap after put():\n" + languages2);
// use of replace()
languages1.replace(3, "Java");
System.out.println("HashMap after replace():\n" + languages1);
}
}
Output
put() 메서드는 HashMap에 새 매핑(3 = Java)을 추가합니다. replace() 메서드는 어떤 작업도 수행하지 않습니다. (Key가 없어도 에러는 나지 않네요?)
HashMap: {1=Python, 2=JavaScript}
HashMap after put():
{1=Python, 2=JavaScript, 3=Java}
HashMap after replace():
{1=Python, 2=JavaScript}
Spring Batch의 JobParameter는 Long / String / Double / Date 타입들을 지원한다.
Enum / LocalDate / LocalDateTime은 지원하지 않는다.
그렇다면?
@Value의 특성을 이용하면 String을 매번 LocalDateTime으로 변경하지 않아도 된다. (아래 코드 참고)
setter메소드에 @Value를 선언 문자열로 받은뒤 원하는 타입으로 세팅
@Slf4j
@Getter
@NoArgsConstructor
public class CreateDateJobParameter {
private LocalDate localDate;
@Value("#{jobParameters[createDate]}")
public void setCreateDate(String createDate) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
this.createDate = LocalDate.parse(createDate, formatter);
}
}
@JobScope로 설정해놨기 때문에 Job이 실행될 때 Bean이 실행된다. 그러면 job실행되는 시점에 jobParameter Bean이 생성되서 값이 세팅된 다음에 CreateDateJobParameter에 Injection이 된다.
public class JObParameterBatchConfiguration {
private final CreateDateJobParameter jobParameter;
@Bean(BATCH_NAME + "jobParameter")
@JobScope
public CreateDateJobParameter jobParameter() {
return new CreateDateJobParameter():
}
}
그 다음부터는 아래와 같이 사용하면 된다.jobParameter.getCreateDate()만 호출하면 된다.
@Bean(name = BATCH_NAME + "_reader")
@StepScope
public JpaPagingItemReader<Product> reader() {
Map<String, Object> params = new HashMap<>();
params.put("createDate", jobParameter.getCreateDate()); // 어디에서나 LocalDate을 가져올 수 있음
return new JpaPagingItemReaderBuilder<Product>()
.name(BATCH_NAME + "_reader")
.entityManagerFactory(entityManagerFactory)
.pageSize(chunkSize)
.queryString("SELECT p FROM Product p WHERE p.createDate = :createDate")
.parameterValues(params)
.build();
}
@JobScope, @StepScope의 특징(Late Binding)을 활용해보기
@JobScope나 @StepScope가 선언된 Bean의 경우에는 Job이나 Step이 실행될 때 Bean이 생성되는 특징을 가진다. (일반 Spring처럼 애플리케이션이 로딩될 때 Bean이 생성되는 것이 아니라)
그 뜻은 애플리케이션 실행 후에도 동적으로 reader / processor / writer bean 생성이 가능하다는 뜻
예를 들아보자
정산하는 시스템에서 ERP 연동이 2-30개가 되는데 하는 일이 비슷
주문데이터 긁어와서 어디에 보내는 것~ 파라미터가 주문이냐 매출이냐 광고냐 그리고 읽어와야 될 테이블 이 값만 다르고 다른 건 전부 동일
그럴 때 마다 같은 클래스를 계속 생성할 수 없으니 LateBinding을 이용해서 파라미터로 주문으로 던지면 주문 테이블에서 읽어오는 리더로 바꿔서 배치를 돌리고, 광고면 광고 테이블에서 읽어오는 리더로 바꿔서 배치를 돌리는 것이다.
이미 답변이 충분히 나왔지만... 몇자 적자면 간단한 자료구조만 공부하셔도 알 수 있는 질문입니다. 배열이던 ArrayList던 자기가 사용할 공간(힙)을 얻을 때가 질문자가 말하는 성능의 포인트입니다.
배열을 사용해 5개만 사용하는 공간에 +1을 하고 싶다면 6개 공간을 만들고 앞에 5개를 복사하고 1 하는 값을 추가합니다. 즉 처음 5개 만드는 공간(성능), 두 번째 6개 만드는 공간입니다.
ArrayList라고 별반 다르지 않습니다. 위의 과정이 add에 들어 있습니다. add 할 때 공간이 부족하면 추가하고 이미 있던 것 복사하고 add 합니다. 그게 기본적으로 10(용량)이라고 초기화가 되어 있고요. 만약 arrayList에 들어갈 최소 크기를 안다면 new ArrayList(사이즈) 이렇게 하시면 입력하면서 메모리를 추가(기본 10) 하는 비용은 좀 줄겠죠... 물론 근데 저렇게까지 한다고 해도 엄청난 양을 넣지 않는 이상 밀리 세컨드도 차이 안 납니다.
그리고 remove 할 땐 그냥 해당 index 없앨 뿐입니다. 실제 할당받은 메모리를 다시 작게 하거나 하진 않습니다. 100000개 넣어 놓고 1개만 남기고 다 지우더라도 메모리 할당받은 건 유지하고 있습니다. 나중에 다시 쓸 수 있으니까요. 뭐 이런 게 많아지면 힙 메모리 없다고 나올 수 도 있겠죠...
아무튼~ 단순 배열로 해결할 수 있다면 그냥 그렇게 하시면 되구요.. 추가가 일어난다 그럼 그냥 잘 만들어진 자료구조 사용하면 됩니다.
자료구조 선택할땐 구조유형을 따져서 조회보단 in/out 자주 일어난다면 위분 말처럼 링크드리스트 쓰구요 반대로 in/out 보다 랜덤엑세스가 많다면 ArrayList쓰면 됩니다. ArrayList자체가 단순 배열과 다른거는 자료구조를 포함하고 있다는것 외에는 동일합니다.
정리하자면
1. 크기를 안다고 하면 String 배열을 이용해 지정해두면 좋겠지만 그게 성능에 크게 영향을 미치지는 않는다.
2. 배열에 add로 값을 추가하게 되면 새로 정의된 크기의 공간을 힙에 새로 할당해서 기존 값 복사하고 추가하는 것
3. 2번의 이유로 기존에 정의된 크기에서 오버되는 경우에는 성능상에 문제가 생길 수 있다. (디폴트 크기는 10)
4. remove의 경우에는 해당 index를 없앨 뿐이고, 실제 할당받은 메모리는 그대로 유지된다. (메모리 차지는 계속함)
5. 값을 추가하고 제거하는 케이스가 빈번할 경우에는 LinkedList를 쓰는 것이 낫다.
6. 생성된 List에 단순 접근하고 작업하는 경우에는 ArrayList를 쓰는 것이 낫다.
각 Controller에서는 메시지를 소비하고 정지하는 방법은 같지만 메시지를 생성하는 방법은 다른 Service들을 Injection 시킨다.
DemoService는 추상클래스로 consume()이라는 추상 메서드를 가진다.
DemoService의 다른 메서드는 자식 Service에서 사용되지만 consume() 추상 메서드는 서비스별로 다르기에 이 구조를 만든 것으로 보인다.
DemoService의 consume()을 구현한 클래스 또한 추상 클래스이다. DemoService의 자식 클래스는 OperatorDemoService, SubscriberDemoService이고 각각 consumer(), getSubscriber() 추상 메서드를 포함하고 있다.
메서드 구현을 자식 클래스에 맡기는 데 사용은 부모 클래스에서 한다.
예를 들어 SubscriberDemoService의 getSubscriber() 메서드는 추상 메서드이고, 해당 클래스의 cosume() 메서드에서 이 getSubscriber() 메서드가 사용되는데 getSubscriber() 메서드 구현은 자식이 담당한다.
각 클래스 별로 공통적으로 사용하는 코드가 있을 때는 AS-IS 관계인지에 따라 상속을 이용했고, AS-IS 관계가 아닌 경우 interface의 default 기능을 활용했다. 그리고 부모-자식 관계를 1 depth 이상으로 가져가려면 그 관계에 대해 치밀하게 생각해봤다는 것이겠지..? 일단 코드 작성 전에 클래스 간의 관계를 고려해 설계부터 잘 하고 개발에 착수해야 겠단 생각이 많이 들었던 코드이다.
이미 어느 정도 익숙하고, 많이 사용해왔던 모던 자바의 기법이지만 언제나 그렇듯 기본과 원리가 중요하다는 사실을
인지하면서 프로젝트가 마무리 되는 이 시점에 한 번 더 정리해보려고 한다.
기존에는 책을 대충 훑고 난 다음 사용했기 때문에 이번에는 좀 정독을 하면서 중요 부분은 포스팅해보려고 한다.
람다란 무엇인가?
메서드로 전달할 수 있는 익명함수를 단순화한 것 (이름없음)
기본 문법
(parameters) -> expression
(parameters) -> { statements; }
어디에 어떻게 람다를 사용할까?
함수형 인터페이스를 인수로 받는 메서드에 람다 표현식을 사용할 수 있다.
함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스이다.
람다 표현식으로함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로전체 표현식을 함수형 인터페이스의 인스턴스로 취급
// 함수형 인터페이스 예시
public interface Predicate<T> {
boolean test(T t);
}
public interface Comparator<T> {
int compare(T o1, T o2);
}
public interface Runnable {
void run();
}
public void execute(Runnable r) {
r.run();
}
execute(() -> {});
람다를 어떻게 활용할 수 있을까?
실행어라운드 패턴에 활용할 수 있다.
예를 들어 데이터베이스의 파일 처리를 보면 자원 열고 처리하고 자원 닫는 순서로 이루어 진다. 이 때 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 갖는데 이런 형태를 바로 실행 어라운드 패턴이라고 부른다.
초기화/준비 코드
작업 A
정리/마무리 코드
초기화/준비 코드
작업 B
정리/마무리 코드
중복되는 준비 코드와 정리 코드가 작업 A와 작업 B를 감싸고 있다.
// 한 줄씩 읽는 기존 코드
public String work() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("hi.txt"))) {
return br.readLine();
}
}
// 근데 상황에 따라 두 줄을 읽어야 된다는 요구사항이 들어왔다.
// 따라서 br.readLine(); 이 부분의 동작을 함수형 인터페이스를 사용하는 것으로 바꾼다.
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
public String work(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("hi.txt"))) {
return p.process(br);
}
}
String result = work((BufferedReader br) -> br.readLine());
String result2 = work((BufferedReader br) -> br.readLine() + br.readLine());
함수형 인터페이스와 람다의 활용
다양한 람다 표현식을 사용하려면 공통의 함수 디스크립터를 기술하는 함수형 인터페이스 집합이 필요하다.
함수형 인터페이스의 추상 메서드 시그니처를 함수 디스크립터(fuction descriptor)라고 한다.
자바 API는 Comparable, Runnable, Callable, Predicate, Consumer, Function 등의 함수형 인터페이스를 제공하고 있고, 이 인터페이스에 대한 자세한 설명은 이 포스팅에서 생략하도록 한다. (이와 관련된 포스팅은 따로 있음)
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
@FunctionalInterface
public interface Function<T,R> {
R apply (T t);
}
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
@FunctionalInterface
public interface Function<T> {
R apply (T t);
}
public <T,R> List<R> map(List<T> list, Function<T,R> f) {
List<R> result = new ArrayList<>();
for (T t: list) {
result.add(f.apply(t));
}
return result;
}
List<Integer> list = map(Arrays.asList("lamdas", "in", "action"),
(String s) -> s.length()
);
기본형 특화
자바의 모든 형식은 참조형(Object, Integer, List) 아니면 기본형(int, double, byte, char)가 있다.
제네릭 파라미터 Consumer<T> 의 T 같은 데에는 참조형만 사용할 수 있다는
제네릭의 내부 구현 방식으로 인해 자바에서는 기본형을 참조형으로 박싱하는 기능을 제공한다.
그리고 박싱과 언박싱이 자동으로 이뤄지는 오토박싱 기능도 제공하는데 (int가 Integer로)
이러한 변화과정은 비용이 소모된다. 박싱한 값은 기본형을 감싸는 Wrapper이며 이것은 Heap에 저장된다.
따라서 박싱한 값이 메모리를 더 소비하기 때문에 기본형을 가져올 때에도 메모리를 탐색해야 하는 과정을 거친다.
이런 이유로 오토박싱 동작을 피할 수 있도록 특별한 함수형 인터페이스를 제공한다.
Predicate<Integer> -> IntPredicate 이 외 DoublePredicate, IntConsumer, LongBinaryOperator, IntFuntion가 있다.
public interface IntPredicate {
boolean test(int i);
}
형식 추론
자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 이용해서 관련된 함수형 인터페이스를 추론해낸다!
그리고 이런 추론은 개발자가 람다 문법에서 파라미터 형식을 생략하더라도 추론이 가능하기 때문에
아래와 같이 두 가지 형태로 사용이 가능하며 개발자 스스로 어떤 코드가 가독성을 향상 시킬 수 있는지 결정해야 한다.
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
람다 캡처링
람다 표현식은 인수를 자신의 바디 안에서만 사용했다. 하지만 자유변수(외부에서 정의된 변수)를 활용할 수 있고,
이것을 람다캡처링(capturing lambda)라고 부른다.
int portNumber = 8289;
Runnable r = () -> System.out.println(portNumber);
하지만 이에는 제약사항이 있는데
지역 변수가 명시적으로 final로 선언되거나, final은 아니지만 실제론 final처럼 사용되어야 한다는 것이다.
위에서 portNumber가 다시 portNumber=8080으로 두 번 값을 할당하면 컴파일 에러가 발생하게 될 것이다.
왜 이런 제약사항이 있는가?
인스턴스 변수는 힙에 저장되고, 지역 변수는 스택에 위치하는데 람다가 만약 스레드에서 실행된다고 하면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드는 해당 변수에 접근하려 할 것이다. 따라서 자바 구현에서는 원래 변수에 접근을 허용하지 않고, 자유 지역 변수의 복사본을 제공하게 되었고 이 복사본의 값이 바뀌지 않아야 하므로 지역변수에는 값을 한 번만 할당해야 하는 것이다. (병렬화에도 관련이 있음)
클로저
함수의 비지역 변수를 자유롭게 참조할 수 있는 함수의 인스턴스
람다와 익명 클래스 모두 메서드의 인수로 전달될 수 있으며 자신의 외부 영역의 변수에 접근할 수 있다. 다만 람다와 익명클래스는 람다가 정의된 메서드의 지역 변수의 값은 바꿀 수 없다.
메서드 참조
메서드를 어떻게 호출해야 하는지 설명을 참조하기 보다는 메서드명을 직접 참조해서 가독성을 높인다. 예를 들어 Apple::getWeight는 Apple클래스에 정의된 getWeight의 메서드 참조이다. 결과적으로 메서드 참조는 람다 표현식 (Apple a) -> a.getWeight()을 축약한 것이다.
방법
1. 정적 메서드 참조 -> Integer::parseInt
2. 다양한 형식의 인스턴스 메서드 참조 -> String::length
3. 기존 객체의 인스턴스 메서드 참조 -> t::getValue (Transaction 객체를 할당받은 t 지역변수의 getValue 메서드)
Function인터페이스를 반환하는 andThen, compose 두 가지 디폴트 메서드를 제공한다.
andThen은 주어진 함수를 먼저 적용한 결과를 다음 함수의 입력으로, compose메서드는 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부함수의 인수로 제공한다. 즉, f.andThen(g)에서 andThen 대신에 compose를 사용하면 g(f(x))가 아니라 f(g(x))라는 수식이 된다.
Funtion<Integer, Integer> f = x -> x + 1;
Funtion<Integer, Integer> g = x -> x * 2;
Funtion<Integer, Integer> h = f.componse(g);
int result = h.apply(1); // 3을 반환한다.
대체로 프로그래밍 스타일은 명령형 프로그래밍 패러다임과 선언적 프로그래밍 패러다임으로 분류할 수 있습니다. 명령형 접근 방식은 프로그램을 최종 상태에 도달할 때까지 프로그램의 상태를 변경하는 일련의 명령문으로 정의합니다. 절차적 프로그래밍은 절차나 서브루틴을 사용하여 프로그램을 구성하는 명령형 프로그래밍의 한 유형입니다. 객체 지향 프로그래밍(OOP)으로 알려진 인기 있는 프로그래밍 패러다임 중 하나는 절차적 프로그래밍 개념을 확장한 것입니다. 대조적으로, 선언적 접근 방식은 일련의 명령문으로 제어 흐름을 설명하지 않고 계산 논리를 표현합니다. 간단히 말해서, 선언적 접근 방식의 초점은 프로그램이 달성해야 하는 방법보다는 프로그램이 달성해야 하는 것을 정의하는 것입니다. 함수형 프로그래밍은 선언적 프로그래밍 언어의 하위 집합입니다. 오늘날 인기 있는 프로그래밍 언어의 대부분은 범용 언어이므로 여러 프로그래밍 패러다임을 지원하는 경향이 있다는 것을 이해하는 것이 중요합니다.
3. 기본 원칙 및 개념
이 섹션에서는 함수형 프로그래밍의 몇 가지 기본 원칙과 이를 Java에 적용하는 방법을 다룹니다. 우리가 사용할 많은 기능이 항상 Java의 일부가 아니어서 함수형 프로그래밍을 효과적으로 실행하기 위해선 Java 8 이상을 사용하는 게 좋습니다.
3.1. 일급 및 고차 함수
프로그래밍 언어는 함수를 일급 시민(first-citizen)으로 취급하는 경우 일급 함수를 갖는다고 합니다. 함수를 변수에 할당하고, 다른 함수에 인수로 전달하고, 다른 함수의 값으로 반환하는 것이 포함됩니다. 이 속성을 사용하면 함수형 프로그래밍에서 고차 함수를 정의할 수 있습니다. 고차 함수는 함수를 인수로 받고 결과로 함수를 반환할 수 있습니다. 이를 통해 함수 구성 및 커링과 같은 함수 프로그래밍의 여러 기술을 추가로 사용할 수 있습니다.
함수를 마치 일반값처럼 사용해서 인수로 전달하거나, 결과로 반환받거나, 자료구조에 저장할 수 있음을 의미
자바 8이 이전 버전과 구별되는 특징 중 하나가 일급 함수를 지원한다는 점인데 :: 연산자로 메서드 참조를 만들거나 (int x) -> x + 1 같은 람다 표현식으로 직접 함숫값을 표현해서 메서드를 함숫값으로 사용할 수 있습니다.
Collections.sort 메서드에 사용자 지정 비교기를 제공해야 한다고 가정해 보겠습니다. 아래의 코드는 우리가 볼 수 있듯이 이것은 지루하고 장황한 기술입니다.
Collections.sort(numbers, new Comparator<Integer>() {
부작용은 메서드의 의도된 동작을 제외하고 무엇이든 될 수 있습니다. 예를 들어, 부작용은 값을 반환하기 이전에 local 또는 global 상태를 업데이트하거나 데이터베이스에 저장하는 것을 말할 수 있습니다. 순수주의자들은 또한 로깅도 부작용으로 취급하지만 우리 모두는 우리 만의 룰이 있습니다. 하지만 실제로 결과를 데이터베이스에 저장해야 할 수도 있습니다. 함수형 프로그래밍에는 순수한 함수를 유지하면서 부작용을 처리하는 기술이 있습니다.
3.3. 불변성
불변성은 함수형 프로그래밍의 핵심 원리 중 하나로, 엔터티를 인스턴스화한 후에는 수정할 수 없는 속성을 말합니다. 함수형 프로그래밍 언어에서는 이는 언어 수준(Language level)에서 지원되는데 자바에서는 변경할 수 없는 데이터 구조를 만들기 위해 스스로 결정해야 합니다.
Java 자체는 String과 같은 몇 가지 기본 제공 불변 유형을 제공합니다. 이것은 주로 보안상의 이유입니다. 클래스 로딩에서 String을 많이 사용하고 해시 기반 데이터 구조에서 키로 사용하기 때문입니다. 기본 래퍼(primitive wrapper) 및 수학 유형과 같은 몇 가지 다른 기본 제공 불변 유형이 있습니다.
그러나 우리가 Java에서 생성하는 데이터 구조는 어떻습니까? 물론, 그것들은 기본적으로 변하지 않고,, 불변성을 달성하기 위해 몇 가지를 변경해야 합니다. final 키워드의 사용은 그 중 하나이지만 여기서 그치지 않습니다.
항상 정확하게 원칙을 고수하긴 어렵다. 특히 데이터 구조가 복잡해지면 복잡해질수록.. 그러나 여러 외부 라이브러리를 사용하면 Java에서 immutable data를 더 쉽게 사용할 수 있습니다. 예를 들면 Immutables 및 Project Lombok은 Java에서 변경할 수 없는 데이터 구조를 정의하기 위해 도움을 줍니다.
3.4. 참조 투명성
참조 투명성은 아마 이해하기 어려운 함수형 프로그래밍 원칙 중 하나일 것입니다. 그러나 개념은 매우 간단합니다. 함수 외부의 영향을 받지 않는 것입니다. (= 함수의 결과는 입력 파라미터에만 의존하고, 함수 외 DB, 파일시스템, 원격 URL 등에서 데이터를 읽어 오지 않음 = 외부와 의존적이지 않은 코드 = 동일한 매개변수에 대해서는 항상 동일한 결과 = 예외가 없음) 이를 통해 고차 함수 및 지연 평가와 같은 함수형 프로그래밍에서의 몇 가지 강력한 기술을 사용할 수 있습니다. 이것을 더 잘 이해하기 위해 예를 들어 보겠습니다.
publicclassSimpleData {
private Logger logger = Logger.getGlobal();
private String data;
public String getData() {
logger.log(Level.INFO, "Get data called for SimpleData");
return data;
}
public SimpleData setData(String data) {
logger.log(Level.INFO, "Set data called for SimpleData");
this.data = data;
returnthis;
}
}
이것은 Java의 일반적인 POJO 클래스이지만 이것이 참조 투명성을 제공하는지 확인하는 데 관심이 있습니다. 다음 진술을 관찰합시다.
String data = new SimpleData().setData("Baeldung").getData();
logger.log(Level.INFO, new SimpleData().setData("Baeldung").getData());
logger.log(Level.INFO, data);
logger.log(Level.INFO, "Baeldung");
logger에 대한 세 번의 호출은 의미상 동일하지만 참조적으로 투명하지 않습니다. 첫 번째 호출은 부작용을 생성하므로 참조적으로 투명하지 않습니다. 이 호출을 세 번째 호출에서와 같이 해당 값으로 바꾸면 로그가 누락됩니다.
두 번째 호출도 SimpleData가 변경 가능하므로 참조적으로 투명하지 않습니다. 프로그램의 어느 곳에서나 data.setData를 호출하면 해당 값으로 대체되기가 어렵습니다.
따라서 기본적으로 참조 투명성을 위해서는 순수하고 변경할 수 없는 함수가 필요합니다. 이것은 우리가 이미 앞에서 논의한 두 가지 전제 조건입니다. 참조 투명성의 흥미로운 결과로써 우리는 컨텍스트가 없는 코드를 생산합니다. 다시 말해서, 우리는 해당 코드를 어떤 순서와 컨텍스트로든 실행할 수 있으며, 이는 다른 최적화 가능성으로 이어지게 됩니다.
4. 함수형 프로그래밍 기법
앞에서 논의한 함수형 프로그래밍 원칙을 통해 함수형 프로그래밍의 이점을 얻기 위한 여러 기술을 사용할 수 있습니다. 이 섹션에서는 이런 인기 기술 중 일부를 다루고 이를 Java에서 구현하는 방법을 이해해 보겠습니다.
4.1. 기능 구성
Function Composition은 단순한 함수를 결합하여 복잡한 함수를 구성하는 것을 말합니다. 이것은 실제로 자바의 funcional 인터페이스를 통해 달성됩니다.
일반적으로 단일 추상 메서드가 있는 모든 인터페이스는 functional 인터페이스로 사용할 수 있습니다. 그러므로 우리는 functional 인터페이스를 꽤나 쉽게 정의할 수 있지만 Java 8은 기본적으로 java.util.function 패키지에서 다양한 사용 사례에 대해 많은 기능적 인터페이스를 제공하고 있습니다.
이것을 더 잘 이해하기 위해 Function 인터페이스를 예로 들어보면 Function은 하나의 인수를 받아들이고 결과를 생성하는 간단하고 일반적인 기능의 인터페이스입니다.
또한 함수 구성에 도움이 되는 두 가지 기본 메서드인 compose 및 andThen을 제공합니다.
이 두 가지 방법 모두 여러 함수를 단일 함수로 구성할 수 있게 하지만 다른 의미를 제공합니다. compose는 인수에 전달된 함수를 먼저 적용한 다음 호출된 함수를 적용하고 andThen은 역으로 동일한 작업을 수행합니다.
여러 다른 함수형 인터페이스도 funcion composition에 사용될 여러 흥미로운 메소드를 가지고 있는데 Predicate 인터페이스의 기본 페소드 and, or, negate가 있습니다. 이러한 기능적 인터페이스는 단일 인수를 허용하지만 BiFunction 및 BiPredicate와 같은 2개의 인수도 가능하게 합니다.
4.2. 모나드 (추가 리서치 필요 - 이해 X)
공식적으로 모나드는 일반적으로 프로그램을 구조화할 수 있는 추상화입니다. 따라서 기본적으로 모나드는 값을 래핑하고, 일련의 변환을 적용하고, 모든 변환이 적용된 값을 돌려 받을 수 있게 합니다. (a monad allows us to wrap a value, apply a set of transformations, and get the value back with all transformations applied.)물론 모든 모나드가 따라야 하는 세 가지 법칙(왼쪽 항등, 오른쪽 항등, 연관성)이 있지만 자세한 내용은 다루지 않겠습니다.
Java에는 Optional 및 Stream과 같이 자주 사용하는 몇 가지 모나드가 있습니다.
Optional 을 모나드라고 부르는 이유는 무엇일까요? 여기에서 Optional을 사용하면 of를 사용해 값을 래핑하고 flatMap 메서드를 사용하여 다른 래핑된 값을 추가하는 변환을 적용하고 있습니다
원한다면 Optional이 모나드의 세 가지 법칙을 따른다는 것을 보여줄 수 있습니다. 그러나 비평가들은 Optional이 어떤 상황에선 모나드 법칙을 어긴다고 지적할 것입니다. 그러나 대부분의 실제 상황에서는 이 정도면 충분합니다.
모나드의 기본 사항을 이해한다면 Stream 및 CompletableFuture와 같은 Java에 다른 많은 예제도 있음을 깨닫게 될 것입니다. 우리는 로그(log) 모나드, 보고(report) 모나드 또는 감사(audit) 모나드와 같은 다양한 목표를 달성하기 위해 Java에서 자체 모나드 유형을 정의할 수 있습니다. 함수형 프로그래밍에서 부작용 처리(side-effect)에 대해 논의한 것을 기억하십니까? 모나드는 그것을 달성하기 위한 함수형 프로그래밍 기술 중 하나인 것처럼 보이네요.
4.3. 커링 (Currying)
Currying은 여러 인수를 취하는 함수를 단일 인수를 취하는 일련의 함수로 변환하는 수학적 기술입니다. 우리는 왜 함수형 프로그래밍에서 이 기술이 필요할까요? 모든 인수를 이용해 함수를 호출할 필요가 없는, 강력한 구성 기술을 제공하기 때문입니다.
게다가 커링 함수는 모든 인수를 받을 때까지 그 효과를 깨닫지 못합니다.
Haskell과 같은 순수 함수형 프로그래밍 언어에서는 커링이 잘 지원됩니다. 사실, 모든 함수는 기본적으로 커링이 됩니다. 그러나 Java에서는 그렇게 간단하지 않습니다.
Function<Double, Function<Double, Double>> weight = mass -> gravity -> mass * gravity;
logger.log(Level.INFO, "My weight on Mars: " + weightOnMars.apply(60.0));
위 코드는 우리는 행성에서 우리의 무게를 계산하는 함수를 정의한 것입니다. 우리의 질량은 동일하게 유지되지만 중력은 우리가 있는 행성에 따라 다릅니다. 특정 행성에 대한 함수를 정의하기 위해 중력만 전달하여 함수를 부분적으로 적용할 수 있습니다. 또한 이 부분적으로 적용된 함수를 임의의 구성에 대한 인수 또는 반환 값으로 전달할 수 있습니다.
Currying은 람다 식과 클로저라는 두 가지 기본 기능에 기초를 둡니다. 람다 표현식은 코드를 데이터로 취급하는 데 도움이 되는 익명 함수입니다. 우리는 이전에 기능적 인터페이스를 사용하여 구현하는 방법을 보았습니다.
이제 람다 식은 우리가 클로저로 정의하는 어휘 범위에서 닫힐 수 있습니다. 예를 들어 보겠습니다.
참고로, DoubleUnaryOperator는 applyAsDouble 이라는 메서드를 정의한다.
4.4. 재귀
재귀는 문제를 작게 나누는데 도움을 주는 좋은 기술입니다. 재귀의 주요 이점은 side-effect를 제거하는 데도 도움됩니다.
from 모던자바인액션
순수 함수형 프로그래밍 언어에서는 while, for 같은 반복문을 포함하지 않는다. 그런 함수형 프로그래밍의 장점이 분명히 있지만 무조건 반복보다는 재귀가 좋다고 주장은 옳지 않다. 왜냐하면 반복 코드보다 재귀 코드가 더 비싸기 때문, 재귀함수를 호출할 때마다 호출 스택에 각 호출시 생성되는 정보를 저장할 새로운 스택 프레임이 만들어진다. 즉, 재귀 팩토리얼의 입력값에 비례해서 메모리 사용량이 증가한다. 그러면 어떡할까? 함수형 언어에서는 '꼬리 호출 최적화' 라는 해결책을 제공한다.
// 일반 재귀 방식의 팩토리얼
staticlongfactorialRecursive(long n) {
return n == 1 ? 1 : n * factorialRecursive(n-1);
}
// 꼬리 재귀 팩토리얼
staticlongfactorialTailRecursive(long n) {
return factorialHelper(1, n);
}
staticlongfactorialHelper(long acc, long n) {
return n == 1 ? acc : factorialHelper(acc * n, n-1);
}
컴파일러가 하나의 스택 프레임을 재활용할 가능성이 생긴다. 재귀 호출 이후 추가적인 연산을 요구하지 않도록 구현하는 것이 핵심이다. 아쉽게도 자바는 이와 같은 최적화를 재공하지 않는다, 하지만 여러 컴파일러 최적화 여지를 남겨둘 수 있는 꼬리재귀를 이용하는 것이 좋다.
5. 함수형 프로그래밍이 왜 중요한가?
Java를 포함한 모든 언어에서 함수형 프로그래밍을 채택할 때의 가장 큰 이점은 순수 함수와 불변 상태입니다. 돌이켜 생각해보면 대부분의 프로그래밍 문제는 부작용과 변경 가능한 상태가 원인이 되고 있습니다. 따라서 그것들을 제거하기만 하면 우리 프로그램을 더 쉽게 읽고, 추론하고, 테스트하고, 유지 관리할 수 있습니다.
선언적 프로그래밍의 하위 집합인 함수형 프로그래밍은 고차 함수, 함수 구성 및 함수 연결과 같은 여러 구문을 제공합니다. Stream API가 데이터 조작을 처리하기 위해 Java 8에 가져온 이점을 생각해 보십시오.
함수형 프로그래밍을 사용하기 시작하기 전에 우리는 프로그램에 대해 함수 측면에서 생각하도록 훈련해야 합니다.
6. 자바는 함수형 프로그래밍에 적합 할까요?
Java에 진정한 함수 타입이 없다는 것은 함수형 프로그래밍의 기본 원칙에 위배됩니다. lamda expression으로 위장한 기능적 인터페이스는 적어도 구문적으로는 대부분 이를 보완하긴 하지만 Java가 기본적으로 mutable하고, immutable 타입으로 변경하려면 너무나도 많은 상용구를 작성해야 하는 것도 사실입니다.
Java에서 arguments에 대한 기본 전략은 eager이지만 lazy evalauation은 더 효과적이고 함수형 프로그래밍에서 추천되는 전략입니다. 우리는 연산자 단락 및 기능 인터페이스를 사용하여 Java에서 지연 평가를 달성할 수 있지만 더 복잡합니다.
type-erasure, 꼬리호출 최적화 등등이 완성된 형태가 아니기 때문에 Java는 함수형 프로그래밍에서 처음부터 프로그램을 시작하는 데 적합하지는 않습니다.
그러나 이미 Java로 작성된 기존 프로그램이 있는 경우(아마도 객체 지향 프로그래밍으로) 어떻게 될까요? 특히 Java 8에서 함수형 프로그래밍의 이점을 취하는 데에는 문제가 없겠습니다.
함수형 프로그래밍의 대부분의 이점은 Java 개발자에게 달려있습니다. 객체 지향 프로그래밍과 함수형 프로그래밍의 이점을 결합하면 먼 길도 쉽게 갈 수 있을 것입니다.
7. 결론
이 튜토리얼에서는 함수형 프로그래밍의 기초를 살펴보았습니다. 우리는 기본 원칙과 Java에서 이를 채택하는 방법을 다루었습니다. 또한 Java의 예제를 사용하여 함수형 프로그래밍에서 몇 가지 인기 있는 기술에 대해 논의했습니다.
마지막으로, 우리는 함수형 프로그래밍을 채택할 때의 이점 중 일부를 다루었고 Java가 이에 적합한지 답했습니다.