1일차에서는 객체끼리 대화(메시징)하는 법을 배웠다면, 오늘은 조금 더 세련된 대화법 "나중에 결정하기"에 대해 배웠다.
1) OCP(Open-Closed Principle) 준수 - 새로운 이동 수단이 추가되어도 기존 코드는 수정할 필요 없음. 새로운 클래스 하나만 더 만들면 끝
2) 런타임 교체 가능 - 프로그램 실행 중에 따라 알고리즘을 유연하게 바꿀 수 있음
3) 단위 테스트 용이 - 각 전략이 독립된 클래스이므로, 특정 알고리즘만 따로 테스트하기 매우 편함
🍔우리는 햄버거 프랜차이즈 사장! 주방에 만능 조리 기계를 들였는데, 이 기계는 빵이랑 패티는 기본으로 깔아주지만 중간에 들어갈 핵심 비법이 뭔지는 모름
-> 그래서 BurgerRecipe라는 약속(인터페이스)을 만듬
// "뭐가 될진 모르겠지만, 아무튼 '조리(cook)' 할 수 있는 레시피"
public interface BurgerRecipe {
void cook();
}
public class HamburgerChef {
private BurgerRecipe recipe;
// 어떤 레시피든 '장착(set)' 하면 해당 메뉴로 변신!
public void setRecipe(BurgerRecipe recipe) {
this.recipe = recipe;
}
public void makeBurger() {
System.out.println("--- 🍔 조리 시작 ---");
System.out.println("# 번(빵)을 따뜻하게 굽습니다.");
System.out.println("# 육즙 가득한 패티를 올립니다.");
if (recipe != null) {
// "중간에 뭘 넣을진 모르겠지만, 아무튼 너의 레시피대로 해!" (다형성)
recipe.cook();
}
System.out.println("# 빵을 덮고 포장합니다.");
System.out.println("--- ✅ 조리 완료 ---\n");
}
}
public class CheeseBurgerRecipe implements BurgerRecipe {
@Override
public void cook() {
System.out.println("# 체다 치즈를 듬뿍 올리고 녹입니다.");
}
}
public class BulgogiBurgerRecipe implements BurgerRecipe {
@Override
public void cook() {
System.out.println("# 단짠단짠 불고기 소스를 바릅니다.");
System.out.println("# 양상추를 듬뿍 올립니다.");
}
}
HamburgerChef machine = new HamburgerChef();
// 치즈버거 레시피 장착 & 조리
machine.setRecipe(new CheeseBurgerRecipe());
machine.makeBurger();
// 불고기버거 레시피로 교체 & 조리
machine.setRecipe(new BulgogiBurgerRecipe());
machine.makeBurger();
만약 신메뉴가 100개 나오면 100개의 클래스 파일(.java)을 일일이 만들어야 할까?
-> 익명 클래스와 람다로 해결
마치 우리가 배달 음식을 먹을 때 굳이 그릇을 사지 않고 일회용 용기를 쓰고 버리는 것처럼 일회용 레시피 -> 익명 클래스
public class Main {
public static void main(String[] args) {
HamburgerChef machine = new HamburgerChef();
// 💥 별도의 .java 파일 없이, 여기서 바로 만든다! (일회용)
BurgerRecipe shrimpRecipe = new BurgerRecipe() {
@Override
public void cook() {
System.out.println("# 통통한 새우 패티에 타르타르 소스를 듬뿍!");
}
};
machine.setRecipe(shrimpRecipe);
machine.makeBurger();
}
}
-> Main 클래스 안에서 BurgerRecipe 인터페이스를 즉석에서 구현하는 모습
// 파라미터 자리에서 바로 new 하는 패기!
machine.setRecipe(new BurgerRecipe() {
@Override
public void cook() {
System.out.println("# 부드러운 아보카도를 슬라이스해서 올립니다.");
}
});
machine.makeBurger();
장점: 파일이 줄었다 -> 클래스 폭발 문제가 해결됨
단점: 너무 길다 -> 우리가 진짜 하고 싶은 핵심 로직은 딱 한줄
-> 이걸 해결할 수 있는 게 람다(Lambda)
// Before: 익명 클래스 (5줄)
BurgerRecipe avocado = new BurgerRecipe() {
@Override
public void cook() {
System.out.println("# 아보카도 추가");
}
};
// After: 람다 표현식 (1줄) ⚡️
BurgerRecipe avocado = () -> System.out.println("# 아보카도 추가");
안전장치: @FunctionalInterface
- 람다는 오직 하나의 추상 메서드만 가진 인터페이스여야만 사용할 수 있음(그래야 컴파일러가 아 이 메서드구나 하고 추측 가능)
- 혹시라도 나중에 동료가 실수로 메서드를 추가해서 내 람다 코드가 깨지는 걸 막기 위해 인터페이스 위에 안전장치를 걸어둠
// "이 인터페이스는 람다 전용이니까 절대 건드리지 마!" @FunctionalInterface public interface BurgerRecipe { void cook(); // void eat(); // ❌ 이거 주석 풀면 컴파일 에러 발생! 우리를 지켜줍니다. 🛡️ }
HamburgerChef machine = new HamburgerChef();
// 1. 치즈버거 람다로 조리
machine.setRecipe(() -> System.out.println("# 체다 치즈 투하!"));
machine.makeBurger();
// 2. 베이컨버거 람다로 조리
machine.setRecipe(() -> System.out.println("# 바삭한 베이컨 추가!"));
machine.makeBurger();
💡 요약
- String 파라미터는 주방장한테 포스트잇 한 장 써주는 것 -> 치즈라고 써있으면 주방장은 그냥 치즈만 갖다 놓음
- 람다는 주방장한테 조리법 동영상을 보여주는 것 -> 복잡한 시나리오를 통째로 입력할 수 있게 됨
- 단순히 이름을 바꾸고 싶은 게 아니라 상황에 따라 변하는 복잡한 행동을 통째로 조립하고 싶은 것
이것이 동작 파라미터화
// "사과를 검사하는 방법"을 추상화
public interface ApplePredicate {
boolean test(Apple apple);
}
// 메인 메서드는 이제 '검사 방법(p)'을 받아서 실행만 합니다.
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
...
if (p.test(apple)) { // "이 사과 합격인가요?"라고 물어봄
result.add(apple);
}
...
}
// "무거운 사과 골라줘" -> OK, 무거운 사과 전략(람다) 투입!
filterApples(inventory, a -> a.getWeight() > 150);
// T: 무엇이든 들어올 수 있다!
public static <T> List<T> filter(List<T> list, GenericPredicate<T> p) { ... }
// 숫자 중 짝수만 골라줘!
filter(numbers, (Integer i) -> i % 2 == 0);
💡 결론
1) 하드코딩: 변화에 대응 불가능
- 녹색 사과만 골라줘!
public static 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; }2) 값 파라미터화: 조건이 바뀌면 대처 불가능
- 빨간 사과도 골라줘!
// 색깔(Color)을 파라미터로 받아서 유연해졌다! (값 파라미터화) public static List<Apple> filterApplesByColor(List<Apple> inventory, Color color) { ... if (apple.getColor().equals(color)) { ... } ... }3) 동작 파라미터화: 어떤 조건이든 대응 가능
4) 제네릭: 어떤 객체든 적용 가능
- 변하지 않는 로직(필터링)과 자주 변하는 로직(조건)을 분리하는 것
// Lambda: x를 받아서 Math.abs에 넣어라
Function<Integer, Integer> f = x -> Math.abs(x);
// Method Reference: Math의 abs를 써라 (x는 알아서 들어간다)
Function<Integer, Integer> f = Math::abs;
// Lambda: 파라미터 없이, tv 객체의 turnOn을 실행해라
Command cmd = () -> tv.turnOn();
// Method Reference: tv의 turnOn을 실행해라
Command cmd = tv::turnOn;
// Lambda: idol 한 명을 받아서(i), 그 사람의 이름(getName)을 꺼내라
Function<Idol, String> f = i -> i.getName();
// Method Reference: Idol 클래스의 getName을 써라 (주어인 i가 생략됨)
Function<Idol, String> f = Idol::getName;

2회차 강의를 들으면서 확실히 난이도가 있다고 느꼈다.
하지만 튜터님의 찰떡 비유 덕분에 이해가 잘 되었다.
람다와 메서드 참조 부분이 잘 이해가 가지 않아서 다시 복습 해야겠다고 느꼈다.