*책 예제를 그대로 따라치는게 아닌 예제를 직접한번 만들어 보면서 학습을 해봤습니다.
동작 파라미터화 코드 전달하기
변화하는 요구사항은 소프트웨어 엔지니어링에서 피할 수 없는 문제! 시시각각 변하는 사용자의 요구
—> 동작 파라미터화 효과적인 대응 이 가능하다.
동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미, 이 코드 블록의 실행은 나중으로 미뤄진다. 어떻게 실행되는지 예제를 통해서 알아봅시다.
설명을 돕기 위한 기본셋팅입니다.
enum Color { WHITE, BLACK, RED, BLUE }
enum Model { GV70, GV80, G70, G80, G90 }
class Car{
private int price;
private Color color;
private Model model;
public Car(int price, Color color, Model model) {
this.price = price;
this.color = color;
this.model = model;
}
}
//Main문에서 차량 리스트 추가
List<Car> carList = Arrays.asList(
new Car(5500, Color.BLACK, Model.GV70),
new Car(8000, Color.RED, Model.GV80),
new Car(5000, Color.WHITE, Model.G70),
new Car(7000, Color.BLUE, Model.G80),
new Car(9000, Color.WHITE, Model.G90),
new Car(5600, Color.BLUE, Model.GV70),
new Car(6100, Color.BLACK, Model.G70)
);
검은차량 필터링
public static List<Car> filterBlackCar(List<Car> carList) {
List<Car> result = new ArrayList<>();
for (Car car : carList) {
if (car.getColor() == Color.BLACK) {
result.add(car);
}
}
return result;
}
만약 차를 구매하기 원하는 소비자가 검은색 차량이 아닌 흰색 차량을 필터링해서 보고 싶다면? 어떻게 수정해야 할까요?
크게 고민을 하지 않는다면 위와 같은 코드를 복사하여 if문을 수정하는 방법을 선택할 수 도 있습니다.
하지만 이런 방법은 새로운 색이 추가가 되거나, 소비자가 더 다양한 색의 필터를 요구하면 적절하게 대응할 수 없습니다.
그러면 어떻게 해결해야 할까요? → 비슷한 코드가 반복 존재하면 코드를 추상화를 하면 됩니다.
우선은 왜 코드를 추상화해야하는지 좋지 않은 예를 시작으로 하여 단계별로 알아보도록 하겠습니다.
색을 파라미터 화
코드를 반복하지 않고 다양한 색을 필터링할 수 있는 메서드를 만들려면 색을 파라미터화할 수 있도록 메서드에 파라미터를 추가하면 유연하게 대응이 가능합니다.
public static List<Car> filterCarByColor(List<Car> carList, **Color color**) {
List<Car> result = new ArrayList<>();
for (Car car : carList) {
if (**car.getColor() == color**) {
result.add(car);
}
}
return result;
}
//사용
List<Car> blackCars = filterCarByColor(carList, Color.BLACK);
List<Car> whiteCars = filterCarByColor(carList, Color.WHITE);
색을 매개변수로 주어 소비자가 원하는 대로 차량의 색을 필터링을 할 수 있게 되었습니다. 하지만 가격이 6000 이하인 차량의 요구사항이 주어지거나, 모델이 G70인 차량 다양하게 요구사항이 들어온다면 위와 같이 파라미터 정보를 추가하여 해결할 수 있겠지만, 생각해보면 결국 코드가 중복된다는 사실을 알 수 있습니다.
public static List<Car> filterCarByPrice(List<Car> carList, int price) {
List<Car> result = new ArrayList<>();
for (Car car : carList) {
if (car.getPrice() < price) {
result.add(car);
}
}
return result;
}
이는 소프트웨어 공학의 DRY(don’t repeat yourselft, 같은 것을 반복) 원칙을 어기는 것입니다.
탐색 과정을 고치려면 한 줄이 아닌 메서드 전체를 수정해야 하는 일이 생길 수 있어 엔지니어링적으로 비싼 대가를 치러야 합니다.
색과 가격, 모델 등을 따로 메서드로 합치는 방법도 있겠지만, 그렇게 된다면 어떤 기준으로 필터링할지 구분하는 또 다른 방법이 필요합니다. 따라서 어떤 기준으로 필터링할지 플래그를 추가하는 방법도 있습니다.
—> 하지만 실무, 실전에서는 절대 사용 X
가능한 모든 속성으로 필터링
실전에서는 절대 사용하지 말라는 플래그로 메서드를 구현해보겠습니다.
public static List<Car> filterCars(List<Car> carList, Color color,
int price, boolean flag) {
List<Car> result = new ArrayList<>();
for (Car car : carList) {
if ((flag && car.getPrice() < price) ||
(!flag && car.getColor() == color)) {
result.add(car);
}
}
return result;
}
List<Car> G70Cars = filterCars(carList, null, Model.G70, true);
List<Car> blackCars = filterCars(carList, Color.BLACK, null, false);
- 작성자도 보기 힘든 가독성을 가지고, true flase 가 뭘 하는지도 모르겠고 코드가 개판이 났습니다.
- 여기에 필터링 조건으로 가격이 추가가 된다면 벌써 머리가 아파집니다.
—> 이를 동작 파라미터화를 이용해서 해결해 보겠습니다
동작 파라미터화
앞선 코드를 보면 파라미터를 추가하는 방법이 아니라 변화 좀 더 유연하게 대응할 수 있는 방법이 필요하다는 것을 확인했습니다. 반복되는 코드를 추상화하기 위하여 선택 조건을 결정하는 인터페이스를 만들어 봅시다.
public interface CarPredicate{
boolean test(Car car);
}
→ *참 또는 거짓을 반환하는 함수를 프레디케이트(Predicate) 라고 합니다.
//7000만원 이하차량 선택
public class CarPircePredicate implements CarPredicate{
@Override
public boolean test(Car car) {
return car.getPrice() <= 7000;
}
}
//GV70 만 선택
public class CarModelPredicate implements CarPredicate {
@Override
public boolean test(Car car) {
return car.getModel() == Model.GV70;
}
}
- 조건에 따라 filter 메서드가 다르게 동작할 것이라고 예측할 수 있습니다. 이를 전략 디자인 패턴이라고 부릅니다. 전략 디자인 패턴은 각 알고리즘(전략이라 불리는)을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법입니다.
- 여기서 인터페이스 CarPredicate가 알고리즘 패밀리고, 구현체가 전략입니다.
그런데 이 인터페이스는 어떻게 다양한 동작을 수행할 수 있을까요, 메서드에서 인터페이스 객체를 받아 차량의 조건을 검사하도록 메서드를 만들어야 합니다.
—> 이렇게 동작 파라미터화 , 즉 메서드가 다양한 동작(or 전략)을 받아서 내부적으로 다양한 동작을 수행할 수 있습니다. 기존의 코드를 고쳐 어떤 좋은 점이 있는지 한번 확인해 봅시다!
추상적 조건으로 필터링
public static List<Car> filterCars(List<Car> carList, CarPredicate p) {
List<Car> result = new ArrayList<>();
for (Car car : carList) {
if (p.test(car)) {
result.add(car);
}
}
return result;
}
이전 코드에 비해 유연한 코드를 얻었으며, 가독성도 좋아지고 사용하기도 쉬워졌습니다. 필요한 대로 인터페이스를 구현하여 filterCars 메서드로 전달할 수 있습니다.
차량이 9000만 원 이하, 색은 파랑, 모델은 G80이라고 하면 적절하게 인터페이스를 구현하는 클래스를 만들면 됩니다.
public static class ChoiceCars implements CarPredicate {
@Override
public boolean test(Car car) {
return car.getModel() == Model.G80
&& car.getPrice() <= 9000
&& car.getColor() == Color.BLUE;
}
}
List<Car> ChoiceCars = filterCars(carList, new ChoiceCar());
//List<Car> priceCars = filterCar(carList, new CarPircePredicate());
//List<Car> modelCars = filterCar(carList, new CarModelPredicate());
//List<Car> ChoiceCars = filterCars(carList, 만든 객체 전달);
- 직접 만든 CarPredicate 객체에 의해 filterCars 메서드의 동작이 결정이 된다. 이것이 filterCars메서드의 동작을 파라미터화한 것이다.
이처럼 블록을 조립하듯이, 한 메서드가 다른 동작을 수행할 수 있도록 재활용할 수 있다. 유연한 API를 만들 때 동작 파라미터화가 중요한 역할을 합니다.
복잡한 과정 간소화
동작을 파라미터화 하여서 가독성도 좋아지고, 사용하기 쉬워졌지만 뭔가 새로운 동작을 만들어 filterCars 메서드로 전달하려면 인터페이스를 구현하는 여러 클래스를 정의한 다음에 인스턴스화를 해야 하는 번거로운 작업을 해야 합니다.
자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스 기법을 제공합니다.
익명 클래스
익명 클래스는 자바의 지역 클래스와 비슷한 개념으로, 말 그대로 이름이 없는 클래스입니다. 즉석에서 필요한 구현을 만들어서 사용이 가능합니다.
List<Car> redCars = filterCar(carList, new CarPredicate() {
@Override
public boolean test(Car car) {
return Color.RED == car.getColor();
}
});
- 새로운 클래스를 만들어 인스턴스화를 하는 작업이 없어졌지만, 익명클래스는 많은 공간을 차지합니다.
코드의 장황함(verbosity)는 나쁜 특성입니다. 왜냐하면 장황한 코드는 유지 보수하는데 시간이 오래 걸릴 뿐만 아니라, 코드를 보는 즐거움도 빼앗는 요소입니다. 따라서 한눈에 이해할 수 있어야 좋은 코드입니다.
람다 표현식 사용
자바 8의 람다 표현식을 이용하면 위 예제 코드를 다음처럼 간단하게 구현이 가능하게 됩니다.
List<Car> redCars = filterCar(carList, car -> Color.RED == car.getColor());
- 코드를 모르는 사람이 봐도 간결해짐을 느낄 수 있습니다. 이렇게 람다표현식을 사용하면 복잡성 문제를 해결할 수 있습니다.
리스트 형식으로 추상화
public interface Predicate<T> {
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> result = new ArrayList<>();
for (T e : list) {
if (p.test(e)) {
result.add(e);
}
}
return result;
}
List<Integer> numbers = new ArrayList<>();
for (int i = 0; i < 100; i++) {
numbers.add(i);
}
List<Integer> evenNumber = filter(numbers, i -> i % 2 == 0);
List<Car> carGV70 = filter(carList, car -> car.getModel() == Model.GV70);
제네릭스를 이용하여 다양한 타입의 객체를 다룰 수 있게 만들었다. 어떤 타입이든 한 가지 타입을 정해서 담을 수 있습니다.
- 타입 안정성 제공
- 타입체크와 형변환을 생략할 수 있어 코드가 간결해진다.
마무리
- 동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응하는 코드를 구현할 수 있다.
- 자바 8(람다)을 활용하여 인터페이스를 상속받아 여러 클래스를 구현하는 고생을 덜 수 있다.
- 자바 API의 많은 메서드는 정렬, 스레드, GUI 처리 등을 포함한 다양한 동작으로 파라미터화 할 수 있다.
'모던 자바 인 액션 스터디' 카테고리의 다른 글
모던 자바 인 액션 - 6장 스트림으로 데이터 수집 (0) | 2022.05.24 |
---|---|
모던 자바 인 액션 - 5장 스트림 활용 (0) | 2022.05.16 |
모던 자바 인 액션 - 4장 스트림 소개 (0) | 2022.05.09 |
모던 자바 인 액션 - 3장 람다 표현식 (0) | 2022.05.02 |