동작 파라미터를 아십니까?
프로그래밍 시 사용하는 "동작"은 아래와 같은 것을 의미합니다.
a와 b를 곱하는 함수가 있다고 해봅시다.
def multiply_numbers(a, b):
result = a * b
return result
이 함수는 파라미터로 받은 곱하는 "동작"을 하는데용,
a와 b의 값에 따라서 multiply_numbers 함수의 결과 값은 달라지게 됩니다.
이때, a와 b를 동작 파라미터라고 하는 거잖슴~
result1 = multiply_numbers(2, 3) # a=2, b=3을 전달하여 2 * 3 = 6이 반환됨
result2 = multiply_numbers(5, 7) # a=5, b=7을 전달하여 5 * 7 = 35가 반환됨
이렇게 말이져.
a와 b의 값에 따라 result 값이 달라지게 됩니다.
즉, 동작 파라미터란
함수나 메서드의 호출 시 전달되는 값으로,
해당 함수 또는 메소드의 동작을 제어하거나 조절하는 데 사용됩니다.
그러면 동작 파라미터화가 무엇인지,
Java 8의 동작 파라미터화로 무엇을 할 수 있는지!!
Modern Java In Action 책을 참고로 정리해보려고 합니다
우리가 개발을 할 때 고민하는 내용은 아래와 같습니다.
🗝 유지보수가 쉬워야 하고 새로운 기능이 추가될 때 쉽게 구현이 되어야 한다.
🗝 변경되는 요구사항에 효과적으로 대응해야 할 방법이 필요하다.
동작 파라미터화
아직 어떻게 실행될지 결정되지 않은 코드 블록.
코드 블록에 따라 메서드 동작이 파라미터화 된다.
첫 번째 시도: 녹색 사과 필터링
public List<Apple> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if(Color.GREEN.equals(apple.getColor())) {
result.add(apple);
}
}
return result;
}
public class Apple {
private final Color color;
Apple(Color color) {
this.color = color;
}
public Color getColor() { return this.color;}
}
녹색 사과를 필터링하고 싶다~는 함수입니다.
파라미터로 받은 inventory에서 꺼낸 사과의 색상과 GREEN인지 비교를 하고 있다.
만약 빨간 사과를 필터링하고 싶다면? 색상이 3000개 늘어난다면?
추상화를 해보자
두 번째 시도: 색을 파라미터화
@Test
@DisplayName("녹색 사과 필터링")
void filterGreenApples() {
List<Apple> inventory = new ArrayList<>();
inventory.add(new Apple(Color.GREEN));
inventory.add(new Apple(Color.GREEN));
inventory.add(new Apple(Color.RED));
inventory.add(new Apple(Color.RED));
inventory.add(new Apple(Color.GREEN));
inventory.add(new Apple(Color.GREEN));
// 첫 번째 시도
// assertEquals(4, filterGreenApples(inventory).size());
// 두 번째 시도
assertEquals(2, filterGreenApples(inventory, Color.RED).size());
}
public List<Apple> filterGreenApples(List<Apple> inventory, Color color) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if(color.equals(apple.getColor())) { // color.GREEN 대신 color 파라미터와 비교!
result.add(apple);
}
}
return result;
}
public class Apple {
private final Color color;
Apple(Color color) {
this.color = color;
}
public Color getColor() { return this.color;}
}
파라미터로 색상을 받도록 변경했다
만약 색상뿐만 아니라 사과의 무게도 필터링을 하고 싶다면?
public List<Apple> filterGreenApplesByWeight(List<Apple> inventory, int weight) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if(apple.getWeight() > weight) {
result.add(apple);
}
}
return result;
}
무게를 필터링하는 함수가 추가됐다
inventory에서 사과를 꺼내서, 반복문을 돌며 색상을 필터링하는 부분이 중복된다.
DRY (don't repeat yourself) 원칙을 어긋난 코드잖슴~
세 번째 시도: 가능한 모든 경우의 수로 필터링한다
public List<Apple> filterApples(List<Apple> inventory, Color color, int weight, boolean flag) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if( (flag && apple.getColor().equals(color)) ||
(!flag && apple.getWeight() > weight)) {
result.add(apple);
}
}
return result;
}
flag가 난데없이 등장
책에서도 형편없는 코드라고 해서 웃겼다
어떤 기준으로 사과를 필터링할 건지 효과적으로 전달하는 방법이 필요하다
→ 동작 파라미터화로 유연성을 얻자.
public interface ApplePredicate {
boolean test (Apple apple);
}
어떤 속성에 기초해서 참과 거짓을 반환하는 프레디케이트 인터페이스를 생성했다.
인터페이스를 상속받아서 기능을 분리해 보자.
Predicate는 주로 조건을 검사하여 참 또는 거짓을 판단하는 함수이다.
public class AppleHeavyWeightPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return false;
}
}
public class AppleGreenColorPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return false;
}
}
전략 디자인 패턴을 사용한 예제이다.
ApplePredicate이 → 알고리즘 패밀리
상속받은 애들이 → 전략이 되는 것이다.
위에서 본 함수 filterApples() 에서 객체를 받아서 조건을 검사하도록 바꾸면 되잖슴~
메서드가 다양한 동작을 하게 된다 → 이게 동작 파라미터화잖슴~
전략 디자인 패턴
캡슐화하는 알고리즘 패밀리를 정의하고 런타임 시 알고리즘이 선택
네 번째 시도: 추상적 조건으로 필터링
public List<Apple> filterApples(List<Apple> inventory, ApplePredicate applePredicate) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if(applePredicate.test(apple))
result.add(apple);
}
return result;
}
Predicate 객체로 사과 검사 조건을 캡슐화한 예제이다.
전달한 ApplePredicate 객체에 의해 함수의 동작이 결정된다.
메서드의 동작이 파라미터화 되었다는 것이다.
...
근데 만약 빨간 무거운 사과를 필터링하고 싶다면?
public class AppleRedHeavyPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
/**
* 로직
*/
return false;
}
}
이런 클래스들이 계속해서 추가가 될 것이다.
중복을 어떻게 줄이고 복잡하지 않게 만들 수 있을까?를 고민해야 한다.
복잡한 과정 간소화해 보자
간소화하기 위한 방법으로 익명 클래스와 람다가 있다.
그중, 익명 클래스는 아래와 같은 의미를 가진다.
익명 클래스 ( Anonymous Class)
코드의 양을 줄일 수 있다
자바의 지역 클래스와 비슷한 개념으로, 이름이 없는 익명인 클래스란 뜻이다
클래스 선언과 인스턴스화를 동시에 할 수 있다
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// 버튼이 클릭되었을 때 수행되는 코드
}
});
버튼이 눌렸을 때 실행될 코드를 정의하였다.
별도의 클래스 이름은 갖지 않는다.
다섯 번째 시도: 익명 클래스
익명 클래스를 이용해서 다시 구현
메서드 동작을 익명 클래스로 만들어서 직접 파라미터화 했다.
익명 클래스로 작성해 주면, 인텔리제이에서 람다를 추천해 준다.
친절해 🎄
여섯 번째 시도: 람다 표현식
List<Apple> greenApples = filterApples(inventory, apple -> false);
매우 깔꼼..
람다는 다음 글에서 다시 자세히 다루겠다.
일곱 번째 시도: 리스트 형식으로 추상화
void filterList() {
List<Apple> inventory = new ArrayList<>();
List<Apple> redApples = filter(inventory, (Apple apple) -> Color.RED.equals(apple.getColor()));
}
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for (T t : list) {
if(predicate.test(t)) {
result.add(t);
}
}
return result;
}
형식 파라미터 T로 클래스를 생성할 수도 있다.
필터 메서드를 사용하여, 사과뿐만 아니라 바나나, 오렌지 등등에 사용할 수 있다.
실전 예제
그렇다면 동작 파라미터화를 다루는 실전 예제를 봐보자.
1) Comparator로 정렬하기
자바 8은 List에 sort 메서드가 포함되어 있다
Collections.sort는 물론 있음
java.util.Comparator 객체를 이용해서 sort 동작을 파라미터화 할 수 있다
List<Apple> inventory = new ArrayList<>();
List<Apple> redApples = filter(inventory, (Apple apple) -> Color.RED.equals(apple.getColor()));
inventory.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight() > o2.getWeight() ? o1.getWeight() : o2.getWeight();
}
});
익명 클래스를 사용한 예제이다.
만약 농부의 요구사항이 바뀌면
새로운 Comparator를 만들어서 sort 메서드에 전달해 주면 된다.
inventory.sort((o1, o2) -> o1.getWeight() > o2.getWeight() ? o1.getWeight() : o2.getWeight());
람다로 바꾸면 이렇게 할 수 있다.
2) Runnable로 코드 블록 실행하기
또 다른 예제를 봐보자.
자바 스레드를 이용하면 Runnable로 코드 블록을 병렬로 실행할 수 있다.
여러 스레드가 각자 다른 코드를 실행할 수 있다는 뜻이다.
자바 8 이전까지는 Thread 생성자에 객체만을 전달할 수 있으므로
보통 결과를 반환 안 하는 void run 같은 메서드를 포함하는 익명 클래스가
Runnable 인터페이스를 구현하도록 하는 게 일반적이었다고 한다.
void threadTest() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("FilterGreenApples.run");
}
});
}
람다로 바꾸면
void threadTest() {
Thread thread = new Thread(() -> System.out.println("FilterGreenApples.run"));
}
3) Callable를 결과로 반환하기
ExecutorService (자바 5부터 지원.)
ExecutorService는 스레드 풀을 관리하고 스레드 작업을 실행하는 데 사용되는 인터페이스다.
스레드 관리 및 작업 스케줄링을 추상화하고 효과적으로 다룰 수 있도록 도와줌.
ExecutorService에는 몇 가지 추상화 개념이 있음.
1) 스레드 풀
ExecutorService는 스레드 풀을 사용하여 작업을 처리함.
미리 스레드 풀 생성해 놓고, 작업을 분배해서 스레드를 재사용할 수 있도록 해줌.
스레드의 생성 및 소멸에 따른 오버헤드를 감소시키고 효율성을 높여줌.
2) 작업 큐
작업을 큐에 넣어두고 스레드 풀이 이를 꺼내어 실행함.
작업 큐는 스레드가 사용 가능할 때마다 작업을 꺼내어 실행하게 됨.
이러면 뭐가 좋으냐? 여러 작업을 비동기적으로 처리할 수 있다~
3) 작업 실행과 종료
ExecutorService는 submit() 메서드를 통해 작업을 제출하고,
작업이 완료될 때까지 기다리거나 결과를 받을 수 있는 Future 객체를 반환함.
종료는 shutdown() 또는 shutdownNow() 메서드로 한다.
→ Runnable 방식과 다른 점이당
4) 스레드 생명주기 관리
ExecutorService는 스레드 생명주기를 자동으로 관리까지 한다;;
스레드를 생성하고 종료하며, 예외가 발생한 경우에도 적절히 처리할 수 있습니다.
void callableTest() {
ExecutorService executorService = Executors.newCachedThreadPool();
Future<String> future = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return Thread.currentThread().getName();
}
});
}
킹다로 바꿔주면?
void callableTest() {
ExecutorService executorService = Executors.newCachedThreadPool();
Future<String> future = executorService.submit(() -> Thread.currentThread().getName());
}
이러하다.
결론
동작 파라미터화에서는 코드를 메서드 인수로 전달한다.
메서드 내부적으로 다양한 동작을 수행할 수 있도록 하기 위해서이다.
요구사항에 잘 대응하여 엔지니어링 비용 감소하자.
자바 8로 오며, 익명 클래스와 람다 등을 이용해서 코드를 깔끔하게 바꿀 수 있다!!
정렬, 스레드, GUI 처리 등을 다양한 동작으로 파라미터화 할 수 있다.
'BackEnd > 모던자바인액션' 카테고리의 다른 글
[모던자바인액션] Stream 스트림 활용 (4) | 2024.03.26 |
---|---|
[모던자바인액션] Stream 스트림 (6) | 2023.12.17 |
[모던자바인액션] 람다 (Lambda) (4) | 2023.12.03 |