본문 바로가기

함수형 자바

[JAVA] 함수형 디자인 패턴 - Chapter 14

디자인 패턴은 특정한 문제 상황에 대해 어떻게 해결할 수 있을지에 대한 가이드라인을 제공하며, 검증 되어 있으며 형식화되어 반복적으로 적용 가능한 솔루션이다.

 

디자인 패턴은 총 세가지 그룹으로 분류가 가능하다.

 

생성 패턴 (Creational Patterns)

객체를 생성하는 방법을 다루며, 객체 생성의 복잡성을 줄이고, 코드의 유연성을 높인다.

ex) 싱글턴 패턴, 팩토리 메소드 패턴

 

행동 패턴 (Behavioral Patterns)

객체 간의 상호작용과 책임 분배를 다룬다.

객체 간의 알고리즘이나 책임을 어떻게 분산시킬지를 정의한다.

ex) 옵저버 패턴, 전략 패턴

 

 

구조 패턴 (Structural Patterns)

다양한 객체들이 효과적으로 조화를 이루도록 도와주며, 객체 간의 관계를 정리한다.

ex) 어댑터 패턴, 데코레이터 패턴

 

이중 아래에서 좀 더 자세히 다룰 패턴은

팩토리 패턴 (생성), 데코레이션 패턴 (구조), 전략 패턴 (행동), 빌더 패턴 (생성) 이다.

 

팩토리 패턴

생성 패턴중 하나로, 객체를 직접 생성하지 않고 팩토리를 사용하여 객체를 생성하는 방법이다.

세부 사항을 드러내지 않고 객체의 인스턴스를 생성한다.

 

팩토리 패턴의 주요 장점

  • 객체 생성 로직의 캡슐화: 객체 생성 로직을 팩토리 클래스에 모아서 코드의 유지보수를 용이하게 한다.
  • 코드의 유연성: 객체 생성 방식을 변경할 때 클라이언트 코드를 수정할 필요가 없다.
  • 객체 생성의 일관성: 객체 생성 방식이 일관되게 관리되어, 객체 생성에 대한 변경이 전체 시스템에 미치는 영향을 최소화한다.

 

// 도형 공통 인터페이스
interface Shape {
	int corners();
	Color color();
	ShapeType type();
}

//원하는 객체 타입을 식별
public enum ShapeType {
	CIRCLE,
	TRIANGLE,
	SQUARE,
	PENTAGON;
}

// 구체적인 도형 레코드
record Circle(Color color) implements Shape {

	public int corners() {
		return 0;
	}

	public ShapeType type() {
		return ShapeType.CIRCLE;
	}

}

// 팩토리 클래스
class ShapeFactory {

	public static Shape newShape(ShapeType type, Color color) {

		Objects.requireNonNull(color);

		return switch (type) {
			case CIRCLE -> new Circle(color);
			case TRIANGLe -> new Triangle(color);
            	...
			default -> throw new IllegalArgumentException("Unknown type : " + type);
		}

	}
}

// 클라이언트 코드
public class Main {
	public static void main(String[] args) {
		Shape circle = ShapeFactory.newShape(Color.RED);
	}
}

 

 

다만, 위의 객체 지향적 접근 방식에는 팩토리와 enum간의 상호의전성이 있어 변경에 취약하다. (새로운 ShapeType이 추가되면 factory가 이를 고려해야 된다.)

이런 취약성을 줄이기 위해 함수형 접근 방식을 사용 할 수 있다.

 

함수적인 접근 방식

// 도형 공통 인터페이스
interface Shape {
	int corners();
	Color color();
	ShapeType type();
}

//팩토리를 직접 enum으로 구현
//팩토리 함수 도입
public enum ShapeType {
	CIRCLE(Circle::new),
	TRIANGLE(Triangle::new),
	SQUARE(Square::new),
	PENTAGON(Pentagon::new);
    
    //전용 인스턴스 생성 메서드가 있어 public이 아니어도 된다
    private final Function<Color, Shape> factory;
    
    ShapeType(Function<Color, Shape> factory) {
    	this.factory = factory;
    }
    
    public Shape newInstance(Color color) {
    	Objects.requireNonNull(color);
        return this.factory.apply(color);
    
}


// 클라이언트 코드
public class Main {
	public static void main(String[] args) {
		Shape circle = ShapeType.CIRCLE.newInstance(Color.RED);
	}
}

 

 

 

  • factory는 도형 생성에 사용되는 메소드 참조(Circle::new, Triangle::new, 등).
  • factory 필드는 Function<Color, Shape> 타입으로, 색상 정보를 받아서 도형 객체를 생성하는 함수
  • newInstance 메소드는 factory.apply(color)를 호출하여 새로운 Shape 객체를 생성

 

이는 팩토리와 enum을 결합하여 어떤 팩토리 메서드를 사용할 지 결정하는 과정이 ShapeType과 직접 연결된 팩토리 메서드로 대체된다.

enum값이 추가될 때마다 팩토리를 구현하도록 강제하게 되었다.

 

데코레이션 패턴

구조 패턴중 하나로, 객체의 기능을 동적으로 추가할 수 있게 해주는 디자인 패턴이다.

기존의 클래스에 새로운 기능을 추가하면서도 기존의 코드와의 호환성을 유지할 수 있게 해준다.

데코레이션 패턴을 사용하면 기능의 확장이 필요할 때 상속 대신 객체 조합을 통해 확장을 할 수 있다.

 

컴포넌트(Component)

기능을 제공하는 기본 인터페이스 또는 추상 클래스이다.

 

interface CoffeMaker {
	List<String> getIngredients();
    Coffe prepare();
}

 

콘크리트 컴포넌트(Concrete Component)

기본적인 구현체로, 실제로 기능을 제공하는 클래스이다.

class BlackCoffeMaker implements CoffeMaker {
	
    @Override
    public List<String> getIngredients() {
    	return List.of("Robusta Beans", "water");
    }
    
    @Override
    public Coffe prepare() {
    	return new BlackCoffe();
    }
    
}

 

데코레이터(Decorator)

컴포넌트 인터페이스를 구현하며, 원래의 컴포넌트 객체를 포함(참조)하고 추가적인 기능을 제공하는 클래스이다.

abstract class Decorator implements CoffeMaker {

	private final CoffeMaker target;
    
    public Decorator(CoffeMaker target) {
    	this.target = target;
    }
    
    @Override
    public List<String> getIngredients() {
    	return this.target.getIngredients();
    }
    
    @Override
    public Coffe prepare() {
    	return this.target.prepare();
    }
    
 }

 

콘크리트 데코레이터(Concrete Decorator)

추가적인 기능을 구현하는 데코레이터의 구체적인 구현체이다.

class AddMilkDecorator extends Decorator {

	private final MilkCarton milCarton;
    
    public AddMilkDecorator(CoffeMaker target, MilkCarton milkCarton) {
    	super(target);
        this.milkCarton = milkCarton;
    }
    
    @Override
    public List<String> getIngredients() {
    	List<String> newIngredients = new ArrayList<>(Super.getIngredients());
        newIngredients.add("Milk");
        return newIngredients;
    }
    
    @Override
    public Coffee prepare() {
    	Coffee coffee = super.prepare();
        coffee = this.milkCarton.pourInto(coffee);
        return coffee;
    }
 }

 

 

// 클라이언트 코드
public class Main {
	public static void main(String[] args) {
		CoffeeMaker coffeMaker = new BlackCoffeeMaker();
        
        CoffeMaker decoratedCoffeMaker = new AddMilkDecorator(coffeeMaker, new MilkCarton());
        
        Coffee cafeConLeche = decoratedCoffeeMaker.prepare();
        
        //추가로 데코레이터를 구현하기 위해서는 이미 장식된 decoratedCoffeMaker를 또 감싸야 된다.
        
        CoffeMaker decoratedCoffeMaker2 = new AddSugerDecorator(decoratedCoffeMaker);
	}
}

 

팩토리 패턴의 주요 장점

  • 기능의 동적 확장: 객체의 기능을 런타임에 동적으로 추가할 수 있다.
  • 개방-폐쇄 원칙(Open/Closed Principle) 준수: 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있다.
  • 유연한 조합: 여러 데코레이터를 조합하여 객체에 다양한 기능을 추가할 수 있다.

 

팩토리 패턴의 주요 단점

  • 복잡성 증가: 많은 데코레이터가 사용되면 객체 구조가 복잡해질 수 있다.
  • 디버깅 어려움: 데코레이터 체인이 길어지면 문제를 추적하고 디버깅하기 어려울 수 있다.

함수적인 접근 방식

명시적인 중첩과 구체적인 구현 타입을 드러내지 않아도 된다.

다만, 관련된 코드를 함께 그룹화해야 하므로, 연관된 기능들은 하나의 파일에 위치시켰다.

 

//축소를 통한 다중 데코레이션
final class Barista {
	
    public static CoffeMaker decorate(CoffeeMaker coffeeMaker, Function<CoffeeMaker, CoffeeMaker> ... decorators) {
    	
        Function<CoffeeMaker, CoffeeMaker> reducedDecorations = 
        	Arrays.stream(decorators)
            	.reduce(Function.identity(), Function::andThen);
                
         
        return reducedDecorations.apply(coffeeMaker);
 }
 
 //정적 펙토리 메서드를 갖춘 Decorations 편읭 타입
 final class Decorations {
 
 	public static Function<CoffeeMaker, CoffeMaker> addMilk(MilkCarton milkCarton) {
    	return coffeeMaker -> new AddMilkDecorator(coffeeMaker, milkCarton);
    }
    
    public static Function<CoffeeMaker, CoffeMaker> addSugar() {
    
    	return AddSugarCoffeeMaker::new;
    }
    
 }
 
 //클라이언트 코드
 // 클라이언트 코드
public class Main {
	public static void main(String[] args) {
		CoffeeMaker coffeeMaker = Barista.decorate(new BlackCoffeeMaker(), 
        	Decorations.addMilk(milkCarton),
            	Decorations.addSugar());
        
        Coffee coffee = coffeeMaker.prepare();
}