자바에서 동적으로 코드를 전달하는 방법

jonghyun.log·2022년 12월 27일
0

JAVA

목록 보기
1/2
post-thumbnail

코드를 짜다보면 요구사항은 바뀌기 마련이다. 그래서 프로그래밍 언어는 이를 지원하기 위해 끊임없는 발전을 이루어 왔다.

그러면 자바가 어떤식으로 진화했는지 예제를 통해 단계별로 알아보도록 하자.

예제 설명

가령, 아기들 중 몸무게가 1kg이 넘는 아기들을 판별해서 리스트에 담아야 햐는 코드를 짜야한다고 생각해보자.

public static List<Baby> filterHeavyBabies(List<Baby> inventory){
	List<Baby> result = new ArrayList<>();
    for (Baby baby : inventory){
    	if(baby.getweight() > 100) {
        	result.add(baby);
        }
       return result;
     }
}

그런데 만약, 요구사항이 몸무게가 200 그램 초과인 아기들을 분류해야 하는것으로 바뀐다면 어떨까?
그러면 baby.getweight() > 100 이 부분이 바뀌어야 할것이다.
바뀐 코드를 적어보면 아마 다음과 같을것이다.

public static List<Baby> filterHeavyBabies(List<Baby> inventory){
	List<Baby> result = new ArrayList<>();
    for (Baby baby : inventory){
    	if(baby.getweight() > 200) {
        	result.add(baby);
        }
       return result;
     }
}

어떤가? 위 두개의 코드중 if문 안의 내용만 바뀐것을 확인할 수 있다.
하지만 필터링 하는 로직만 변할 뿐 나머지 부분의 코드가 중복되는 부분이 너무 많다.

이럴때는 반복되는 코드를 추상화 해서 문제를 해결하면 효과적이다.

1-1) 변수 파라미터화

이러한 요구사항에 대응해서 고안할 수 있는 첫번째 방법으로는 변수를 파라미터화해서 매서드에 추가하기가 있다.

public static List<Baby> filterHeavyBabies(List<Baby> inventory, int weight){
	List<Baby> result = new ArrayList<>();
    for (Baby baby : inventory){
    	if(baby.getweight() < weight) {
        	result.add(baby);
        }
       return result;
     }
}

List<Baby> heavybabies1 = filterHeavyBabies(inventory, 150);
List<Baby> heavybabies2 = filterHeavyBabies(inventory, 200);

그런데 만약, 요구사항이 몸무게 말고 성별이 여자인 아기들을 분류해야 하는 것으로 바뀐다면 어떨까?

enum Gender { MALE. FEMALE }

public static List<Baby> filterHeavyBabies(List<Baby> inventory, Gender gender){
	List<Baby> result = new ArrayList<>();
    for (Baby baby : inventory){
    	if(gender.equals(baby.getGender())) {
        	result.add(baby);
        }
       return result;
     }
}

위 처럼 성별을 파라미터로 받은 코드를 작성해볼 수 있겠다. 하지만, 위의 코드들은 if 내부의 로직만 다를뿐 나머지 중복되는 부분이 너무 많다.
또한, 탐색 과정을 고쳐서 성능을 개선하려면 매서드 전체 구현을 고쳐야 하므로 비용이 많이 들어가게 된다.

1-2) 변수 파라미터화 - 가능한 모든 속성으로 필터링하기

위의 변수 파라미터를 늘려서 요구사항의 모든 속성을 파라미터로 받는 방식을 생각해 볼 수 있다.

enum Gender { MALE. FEMALE }

public static List<Baby> filterBabies(List<Baby> inventory, int weight , Gender gender, boolean flag){
	List<Baby> result = new ArrayList<>();
    for (Baby baby : inventory){
    	if((flag && baby.getweight() < weight) || 
    		(!flag && gender.equals(baby.getGender())) {
        	result.add(baby);
        }
       return result;
     }
}

List<Baby> heavyBabies = filterBabies(inventory, 150, null, true);
List<Baby> maleBabies = filterBabies(inventory, 0, MALE, flase);

플래그(flag)란 원래 "깃발"이라는 뜻이지만
프로그래밍에서는 상태를 기록하고 처리 흐름을 제어하기 위한 boolean 타입 변수를 의미합니다.

하지만 위와 같은 코드로는 작성하지 않는것이 좋다. 우선 true와 false가 뭘 의미하는지 처음보는 입장에서 알 수가 없으며 요구사항이 늘어날수록 코드가 너무 복잡해지고 유지보수가 어려워진다.

이러한 문제점을 직면해서 고안된 것이 바로 동작 파라미터이다.

2) 동작 파라미터화

자바는 이런 요구 사항에 맞게 동작 파라미터화 를 이용하는 방식으로 진화했다.

동작 파라미터(behavior parameterization) 란 아직은 어떻게 실행될 것인지 결정하지 않은 코드 블록을 의미하며 이 코드 블럭은 나중에 프로그램에서 호출하는 방식으로 사용된다.

간단하게 말하자면 특정 작업을 하는 로직을 메서드의 파라미터로 받아서 메서드가 실행될때 인수로 받아온 로직을 수행하는 것을 뜻한다. 즉, 말 그대로 동작을 파라미터로 받아서 사용하는것을 의미한다.

좀 더 이해하기 쉽게 표현하면, 기존의 변수만 인수로 받아온것에 한계를 느끼고 특정 동작을 하는 함수를 인수로 받는 것입니다.

그렇다면 자바에서는 어떻게 동작 파라미터화를 구현할 수 있을까?

2-1) 함수형 인터페이스 사용

아쉽게도 자바 8 이전에는 메서드를 함수의 파라미터로 받을수가 없었기 때문에

한개의 메서드만을 가진 인터페이스 를 정의하고 그 인터페이스를 상속받은 객체를 파라미터로 받은 후
그 객체 안의 메서드를 호출해서 동작을 파라미터로 받는 형식으로 이러한 문제를 해결했다.

한개의 메서드만을 가진 인터페이스함수형 인터페이스 라고 합니다.

//선택 조건을 결정하는 인터페이스 정의
public interface BabyPredicate {
	boolean test (Baby baby);
}

//인터페이스를 상속받은 클래스를 구현
public class BabyHeavyWeightPrediate implements BabyPredicate {
	public boolean test(Baby baby){
    	return baby.getWeight > 150;
    }
}

public class BabyGenderMalePrediate implements BabyPredicate {
	public boolean test(Baby baby){
    	return MALE.equals(baby.getGender())
    }
}

//상속받은 객체를 파라미터로 받아 사용
public static List<Baby> filterBabies(List<Baby> inventory, BabyPredicate p) {
	
    List<Baby> result = new ArrayList<>();
    
    for(Baby baby: inventory) {
    	if(p.test(baby) {
        	result.add(baby);
        }
    }
}

//실제 코드로 사용
BabyHeavyWeightPrediate babyHeavyWeightPrediate = new BabyHeavyWeightPrediate();
List<Baby> heavyBabies = filterBabies(inventory, babyHeavyWeightPrediate);

BabyGenderMalePrediate babyGenderMalePrediate = new BabyGenderMalePrediate();
List<Baby> maleBabies = filterBabies(inventory, babyGenderMalePrediate);

참고) 참 거짓을 반환하는 함수를 Predicate(프레디케이트) 라고 합니다.

이제, 변화하는 요구사항에 유연한 코드를 만들 수 있게 되었다!
요구사항이 변화하면 BabyPredicate 를 상속받은 클래스를 새로 구현해서 사용하면 된다.
이는 자바의 다형성(Polymolphism) 을 이용한 좋은 해결 방법이다.
또한, 컬렉션에서 조건을 만족하는 항목들을 찾는 로직각 항목에 적용할 동작 구분 했다는 점이
동작 파라미터화의 강점이라고 할 수 있다.

즉, 위의 내용을 정리하면 메서드를 매개변수로 전달하기 위해 메서드를 특정 객체로 감싼 뒤에 전달하고 있다.

하지만 아직도 불만스러운 점이 있다.
바로, 로직을 새로 구현할 때 마다 여러 클래스를 구현해서 인스턴스화 해야한다는 점 이다.

2-2) 익명 클래스 사용하기

로직을 새로 구현할때마다 새로 클래스를 구현하고 인스턴스화 하기에는 번거로운 면이 있다.
이를 해결하기 위해 익명 클래스 를 사용할 수 있다.

익명 클래스란 말 그대로 이름이 없는 클래스로,
이를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다.

public static List<Baby> filterBabies(List<Baby> inventory, BabyPredicate p) {
	
    List<Baby> result = new ArrayList<>();
    
    for(Baby baby: inventory) {
    	if(p.test(baby) {
        	result.add(baby);
        }
    }
}

// 익명 클래스 사용
List<Baby> heavyBabies = filterBabies(inventory, new BabyPredicate() {
	public boolean test(Baby baby) {
    	return baby.getWeight > 150;
    }
});

하지만, 익명 클래스를 사용하더라도 여전히 반복되는 코드가 많으며, 익명 클래스의 특성상
코드 블럭이 많아져 현재 스코프에서 어떤 변수나 메서드가 호출되는지 혼란을 야기할 수 있다.

2-3) 람다 표현식 사용

위와 같이 익명 클래스까지 사용했지만 여전히 코드가 장황해지고 반복되는 부분이 많다는 문제점을 해결하지 못했다.
그래서 자바 8 부터 람다가 도입되어 드디어 메서드를 직접 함수의 매개변수로 전달이 가능해졌다.

List<Baby> result = 
    filterBabies(inventory, (Baby baby) -> baby.getWeight > 150);

람다를 통해, 드디어 간단하면서도 유연한 코드를 작성할 수 있게 되었다.

2-4) 최종 추상화 하기

이제 마지막으로 예제에서 추상화 할 수 있는 부분을 모두 추상화해서 코드를 작성 해보도록 하자.

//추상화
public interface Predicate<T> {
	boolean test(T t);
}

public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
   List<T> result = new ArrayList<>();
   for (T e : list) {
   		if(predicate.test(e)){
        	result.add(e);
    	}
   }
   return result;
 }
    
   
//추상화한 코드 사용
List<Baby> babyInventory = List.of(baby1, baby2, baby3, baby3);
List<Baby> femaleBabies = 
	filter(babyInventory, (Baby baby) -> FEMALE.equals(baby.getGender()));
    
    
List<Computer> computerInventory = List.of(computer1, computer2, computer3);   
List<Computer> cheapcomputers = 
	filter(computerInventory, (Computer computer) -> computer.getCost() < 200);

다음과 같이 값 타입과 필터링 로직 둘다 동적으로 받을 수 있는 유연한 코드 작성이 가능해진다.

정리

즉, 람다는 메서드가 기존의 변수들을 매개변수로 받는 구조에서는 할 수 없었던,
메서드 내부의 특정 로직을 추상화하여 외부로 분리하는 작업을 하고자하는 욕망에 의해
태어났으며 함수형 인터페이스를 구현하는 익명클래스의 추상화된 형태라고 할 수 있다.

참고한 자료 : 모던 자바 인 액션 책

0개의 댓글