들어가기에 앞서
최근 사내에서 진행 중인 타입스크립트 스터디에서 데코레이터 패턴을 다룰 기회가 있었습니다. 자바스크립트는 함수형 언어로서 객체지향적인 패턴을 적용하는 경우가 상대적으로 적기 때문에, 이 패턴을 이해하는 데 어려움이 있을 수 있습니다. 저는 자바 개발자로서, 자바의 객체지향적인 관점에서 데코레이터 패턴을 풀어서 설명해주기 위해 이 글을 작성하게 되었습니다.
상속을 통한 기능 확장
햄버거를 예제 코드로 작성해 보았습니다.
interface Burger {
String getDescription();
double getCost();
}
class BasicBurger implements Burger {
@Override
public String getDescription() {
return "Basic Burger";
}
@Override
public double getCost() {
return 5.0;
}
}
class CheeseBurger extends BasicBurger {
@Override
public String getDescription() {
return "Cheese Burger";
}
@Override
public double getCost() {
return 6.5;
}
}
class BaconCheeseBurger extends CheeseBurger {
@Override
public String getDescription() {
return "Bacon Cheese Burger";
}
@Override
public double getCost() {
return 8.0;
}
}
class DoubleBaconCheeseBurger extends BaconCheeseBurger {
@Override
public String getDescription() {
return "Double Bacon Cheese Burger";
}
@Override
public double getCost() {
return 10.0;
}
}
public class Main {
public static void main(String[] args) {
Burger burger = new DoubleBaconCheeseBurger();
System.out.println(burger.getDescription() + " $" + burger.getCost());
}
}
설명:
- Burger 인터페이스는 getDescription()과 getCost() 메서드를 정의하여 모든 햄버거 클래스가 구현해야 하는 공통 기능을 제공합니다.
- BasicBurger는 기본 햄버거를 나타내며, 햄버거의 기본적인 기능을 제공합니다.
- CheeseBurger는 BasicBurger를 상속받아 치즈가 추가된 햄버거를 구현합니다.
- BaconCheeseBurger는 CheeseBurger를 상속받아 베이컨이 추가된 치즈 버거를 구현합니다.
- DoubleBaconCheeseBurger는 BaconCheeseBurger를 상속받아 패티가 두 개 추가된 베이컨 치즈 버거를 구현합니다.
상속의 단점
- 새로운 기능을 추가할 때 마다 계속해서 고유 클래스를 추가해야하는 문제가 생깁니다. 이런 구조는 클래스의 수를 늘리고, 코드의 유지보수를 어렵게 합니다.
데코레이터 패턴
데코레이터 패턴은 객체에 기능확장이 필요할 때 사용되는 패턴입니다. 데코레이터 패턴은 각 기능 독립된 클래스로 만들어, 상속의 남용 없이 조합할 수 있게 해줍니다.
데코레이터 패턴을 적용
interface Burger {
String getDescription();
double getCost();
}
class BasicBurger implements Burger {
@Override
public String getDescription() {
return "Basic Burger";
}
@Override
public double getCost() {
return 5.0;
}
}
abstract class BurgerDecorator implements Burger {
protected Burger decoratedBurger;
public BurgerDecorator(Burger burger) {
this.decoratedBurger = burger;
}
@Override
public String getDescription() {
return decoratedBurger.getDescription(); // 기본 버거의 설명을 가져옴
}
@Override
public double getCost() {
return decoratedBurger.getCost(); // 기본 버거의 가격을 가져옴
}
}
class Cheese extends BurgerDecorator {
public Cheese(Burger burger) {
super(burger);
}
@Override
public String getDescription() {
return decoratedBurger.getDescription() + ", Cheese"; // 치즈 추가 설명
}
@Override
public double getCost() {
return decoratedBurger.getCost() + 1.5; // 치즈 추가 가격
}
}
class Bacon extends BurgerDecorator {
public Bacon(Burger burger) {
super(burger);
}
@Override
public String getDescription() {
return decoratedBurger.getDescription() + ", Bacon"; // 베이컨 추가 설명
}
@Override
public double getCost() {
return decoratedBurger.getCost() + 2.0; // 베이컨 추가 가격
}
}
class DoublePatty extends BurgerDecorator {
public DoublePatty(Burger burger) {
super(burger);
}
@Override
public String getDescription() {
return decoratedBurger.getDescription() + ", Double Patty"; // 추가 패티 설명
}
@Override
public double getCost() {
return decoratedBurger.getCost() + 3.0; // 추가 패티 가격
}
}
public class Main {
public static void main(String[] args) {
Burger burger = new BasicBurger();
burger = new Cheese(burger); // 치즈 추가
burger = new Bacon(burger); // 베이컨 추가
burger = new DoublePatty(burger); // 더블 패티 추가
System.out.println(burger.getDescription() + " $" + burger.getCost());
}
}
개선된 코드 설명:
- Burger 인터페이스: 햄버거의 기본 기능인
getDescription()
과getCost()
를 정의합니다. - BasicBurger 클래스: 기본 햄버거의 기능을 구현합니다.
- BurgerDecorator 추상 클래스: 데코레이터 클래스의 기본 구조로, 다른 데코레이터들이 상속받을 수 있도록 합니다. 이 클래스는 기본 햄버거 객체를 감싸서 추가 기능을 제공합니다.
- Cheese, Bacon, DoublePatty 데코레이터 클래스: 각 데코레이터 클래스는 버거에 특정 재료(치즈, 베이컨, 더블 패티)를 동적으로 추가합니다.
장점:
- 기능을 추가할 때마다 새로운 클래스를 만들 필요가 없습니다.
- 여러 기능을 조합하기 위해 상속 계층을 무한히 늘리지 않고, 데코레이터 클래스를 사용하여 다양한 조합을 손쉽게 구현할 수 있습니다.
- 개방-폐쇄 원칙 준수: 새로운 기능을 추가할 때 기존 클래스는 수정하지 않고 새로운 데코레이터 클래스를 만들기만 하면 됩니다.
대표적인 예시
- 백준에서 입력을 받을 때 사용하는 io 클래스도 데코레이터 패턴이다.
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
// InputStreamReader를 사용해 System.in을 감싸고, 그 위에 BufferedReader를 추가
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
}
}
'Java' 카테고리의 다른 글
서블릿 필터 (0) | 2024.02.05 |
---|---|
[실습] 가상스레드 - java (0) | 2024.01.22 |
가상스레드 (0) | 2024.01.21 |