코드를 짜다보면 요구사항은 바뀌기 마련이다. 그래서 프로그래밍 언어는 이를 지원하기 위해 끊임없는 발전을 이루어 왔다.
그러면 자바가 어떤식으로 진화했는지 예제를 통해 단계별로 알아보도록 하자.
가령, 아기들 중 몸무게가 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문 안의 내용만 바뀐것을 확인할 수 있다.
하지만 필터링 하는 로직만 변할 뿐 나머지 부분의 코드가 중복되는 부분이 너무 많다.
이럴때는 반복되는 코드를 추상화 해서 문제를 해결하면 효과적이다.
이러한 요구사항에 대응해서 고안할 수 있는 첫번째 방법으로는 변수를 파라미터화해서 매서드에 추가하기가 있다.
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 내부의 로직만 다를뿐 나머지 중복되는 부분이 너무 많다.
또한, 탐색 과정을 고쳐서 성능을 개선하려면 매서드 전체 구현을 고쳐야 하므로 비용이 많이 들어가게 된다.
위의 변수 파라미터를 늘려서 요구사항의 모든 속성을 파라미터로 받는 방식을 생각해 볼 수 있다.
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가 뭘 의미하는지 처음보는 입장에서 알 수가 없으며 요구사항이 늘어날수록 코드가 너무 복잡해지고 유지보수가 어려워진다.
이러한 문제점을 직면해서 고안된 것이 바로 동작 파라미터이다.
자바는 이런 요구 사항에 맞게 동작 파라미터화 를 이용하는 방식으로 진화했다.
동작 파라미터(behavior parameterization) 란 아직은 어떻게 실행될 것인지 결정하지 않은 코드 블록을 의미하며 이 코드 블럭은 나중에 프로그램에서 호출하는 방식으로 사용된다.
간단하게 말하자면 특정 작업을 하는 로직을 메서드의 파라미터로 받아서 메서드가 실행될때 인수로 받아온 로직을 수행하는 것을 뜻한다. 즉, 말 그대로 동작을 파라미터로 받아서 사용하는것을 의미한다.
좀 더 이해하기 쉽게 표현하면, 기존의 변수만 인수로 받아온것에 한계를 느끼고
특정 동작을 하는 함수를 인수로 받는 것
입니다.
그렇다면 자바에서는 어떻게 동작 파라미터화를 구현할 수 있을까?
아쉽게도 자바 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) 을 이용한 좋은 해결 방법이다.
또한, 컬렉션에서 조건을 만족하는 항목들을 찾는 로직 과 각 항목에 적용할 동작 구분 했다는 점이
동작 파라미터화의 강점이라고 할 수 있다.
즉, 위의 내용을 정리하면 메서드를 매개변수로 전달하기 위해 메서드를 특정 객체로 감싼 뒤에 전달하고 있다.
하지만 아직도 불만스러운 점이 있다.
바로, 로직을 새로 구현할 때 마다 여러 클래스를 구현해서 인스턴스화 해야한다는 점 이다.
로직을 새로 구현할때마다 새로 클래스를 구현하고 인스턴스화 하기에는 번거로운 면이 있다.
이를 해결하기 위해 익명 클래스 를 사용할 수 있다.
익명 클래스란 말 그대로 이름이 없는 클래스로,
이를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다.
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;
}
});
하지만, 익명 클래스를 사용하더라도 여전히 반복되는 코드가 많으며, 익명 클래스의 특성상
코드 블럭이 많아져 현재 스코프에서 어떤 변수나 메서드가 호출되는지 혼란을 야기할 수 있다.
위와 같이 익명 클래스까지 사용했지만 여전히 코드가 장황해지고 반복되는 부분이 많다는 문제점을 해결하지 못했다.
그래서 자바 8 부터 람다가 도입되어 드디어 메서드를 직접 함수의 매개변수로 전달이 가능해졌다.
List<Baby> result =
filterBabies(inventory, (Baby baby) -> baby.getWeight > 150);
람다를 통해, 드디어 간단하면서도 유연한 코드를 작성할 수 있게 되었다.
이제 마지막으로 예제에서 추상화 할 수 있는 부분을 모두 추상화해서 코드를 작성 해보도록 하자.
//추상화
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);
다음과 같이 값 타입과 필터링 로직 둘다 동적으로 받을 수 있는 유연한 코드 작성이 가능해진다.
즉, 람다는 메서드가 기존의 변수들을 매개변수로 받는 구조에서는 할 수 없었던,
메서드 내부의 특정 로직을 추상화하여 외부로 분리하는 작업
을 하고자하는 욕망에 의해
태어났으며 함수형 인터페이스를 구현하는 익명클래스
의 추상화된 형태라고 할 수 있다.
참고한 자료 : 모던 자바 인 액션 책