디자인 패턴
생성패턴
객체의 생성 방식을 결정하는 패턴
구조 패턴
객체를 조합해 더 큰 구조나 강화된 객체를 만드는 패턴
행동패턴
객체 간의 책임과 통신을 다루는 패턴
자주 사용되는 패턴
1. 팩토리 패턴
객체를 생성하는 방법의 세부 사항을 드러내지 않고 객체의 인스턴스를 생성한다.
도형 생성 예제
- Shape: 도형의 모서리, 색상, 타입을 정의하는 공통 인터페이스
- ShapeType: 도형의 타입과 색상을 식별하는 열거형
- ShapeFactory: 타입과 색상을 기반으로 도형을 생성하는 Factory
객체 지향 접근 방식
public interface Shape {
int corners();
Color color();
ShapeType type();
}
public enum ShapeType {
CIRCLE, TRIANGLE, SQUARE
}
public enum Color {
RED, BLUE, GREEN
}
public record Circle(Color color) implements Shape {
@Override
public int corners() { return 0; }
@Override
public ShapeType type() { return ShapeType.CIRCLE; }
}
public class ShapeFactory {
public static Shape newShape(ShapeType type, Color color) {
Objects.requireNonNull(color);
return switch (type) {
case CIRCLE -> new Circle(color);
case SQUARE -> new Square(color);
case TRIANGLE -> new Triangle(color);
default -> throw new IllegalArgumentException("Unknown type: " + type);
};
}
}
public class FactoryEx {
public static void main(String[] args) {
Shape circle = ShapeFactory.newShape(ShapeType.CIRCLE, Color.GREEN);
Shape square = ShapeFactory.newShape(ShapeType.SQUARE, Color.BLUE);
Shape triangle = ShapeFactory.newShape(ShapeType.TRIANGLE, Color.RED);
}
}
- 만약 새로운 ShapeType이 추가된다면 구체 타입 클래스(ex PENTAGON) 추가 및 ShapeType와 Factory의 수정이 필요하다.
- 그렇지 않으면 구체적인 구현 타입이 존재하더라도 switch 문에 의해 IllegalArgumentException 이 발생할 수 있다.
함수적 접근 방식
- 결국 ShapeType 에 있는 타입들로 객체가 생성되기 때문에 Factory 클래스의 기능을 ShapeType에 구현할 수 있다.
// ShapeType - factory 함수 구현
public enum ShapeType {
CIRCLE(Circle::new),
TRIANGLE(Triangle::new),
SQUARE(Square::new);
public final Function<Color, Shape> factory;
ShapeType(Function<Color, Shape> factory) {
this.factory = factory;
}
// 인스턴스 생성 메서드
public Shape newInstance(Color color) {
Objects.requireNonNull(color);
Function<Shape, Shape> shapePrint =
shape -> {
System.out.println("type: " + shape.type() + "\ncolor: " + shape.color() + "\ncorners: " + shape.corners());
return shape;
};
return this.factory.andThen(shapePrint).apply(color);
}
}
- 추가하고자 하는 도형 타입이 있다면 추가할 상수만 선언하면 되는데,
- 상수 선언 시 컴파일러가 팩토리 메서드 제공을 강제하기 때문에 추가될 항목에 대해 누락될 가능성을 없애준다.
Shape circle = ShapeType.CIRCLE.newInstance(Color.RED);
Shape triangle = ShapeType.TRIANGLE.newInstance(Color.GREEN);
Shape square = ShapeType.SQUAD.newInstance(Color.BLUE);
2. 데코레이션 패턴
객체의 동작을 런타임에서 변경할 수 있도록 하는 구조 패턴
커피 제조 및 동적 재료 추가 예제
- CoffeeMaker: 커피를 제조하는 공통의 작업을 정의
- BlackCoffeeMaker: 특정 커피 즉, 블랙 커피를 제조하는 구체적 작업을 정의
- Decorator: 제조 중인 커피에 재료를 추가할 수 있는 기능 정의
- AddMilkDecorator: 제조 중인 커피에 스팀 우유를 추가하는 기능 정의
- MilkCarton(우유갑)의 pourInfo(붓는 행위)를 통해 커피에 우유 추가
- AddSugarDecorator: 제조 중인 커피에 설탕 추가 기능 정의
객체 지향 접근방식
public enum Coffee {
BLACK_COFFEE
}
// 커피 제조 공통 기능 정의
public interface CoffeeMaker {
List<String> getIngredients();
Coffee prepare();
}
public class BlackCoffeeMaker implements CoffeeMaker {
@Override
public List<String> getIngredients() { return List.of("Robusta Beans", "Water");}
@Override
public Coffee prepare() { return Coffee.BLACK_COFFEE; }
}
// 재료 추가 기능 정의
public class Decorator implements CoffeeMaker {
private final CoffeeMaker target;
public Decorator(CoffeeMaker target) { this.target = target;}
@Override
public List<String> getIngredients() { return this.target.getIngredients(); }
@Override
public Coffee prepare() { return this.target.prepare(); }
}
public class AddMilkDecorator extends Decorator {
private final MilkCarton milkCarton;
public AddMilkDecorator(CoffeeMaker 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();
return this.milkCarton.pourInfo(coffee);
}
}
public class Main {
public static void main(String[] args) {
// Default BlackCoffee
CoffeeMaker coffeeMaker = new BlackCoffeeMaker();
System.out.println(coffeeMaker.getIngredients());
// Add Milk
CoffeeMaker decoratedCoffeeMaker = new AddMilkDecorator(coffeeMaker, new MilkCarton());
System.out.println(decoratedCoffeeMaker.getIngredients());
// Add Sugar
CoffeeMaker lastDecoratedCoffeeMaker = new AddSugarDecorator(decoratedCoffeeMaker);
System.out.println(lastDecoratedCoffeeMaker.getIngredients());
// [Robusta Beans, Water]
// [Robusta Beans, Water, Milk]
// [Robusta Beans, Water, Milk, Sugar]
}
}
함수적 접근 방식
- 제조할 커피를 먼저 생성하고, 데코레이터를 생성하여 제조 중인 커피를 장식한다.
- BlackCoffeeMaker, Decorator 모두 CoffeeMaker를 반환한다면
함수 합성을 사용해 하나의 동작으로 묶어보는 건 어떨까
public class Barista {
// 단일 함수
public static CoffeeMaker decorate(CoffeeMaker coffeeMaker,
Function<CoffeeMaker, CoffeeMaker> decorator) {
return decorator.apply(coffeeMaker);
}
}
// Add Milk
CoffeeMaker decoratedCoffeeMaker =
Barista.decorate(new BlackCoffeeMaker(),
coffeeMaker -> new AddMilkDecorator(coffeeMaker, new MilkCarton()));
// Add Sugar
CoffeeMaker finalCoffeeMaker = Barista.decorate(decoratedCoffeeMaker, AddSugarDecorator::new);
- 제조하고자 하는 커피와 추가할 재료를 하나의 동작으로 묶었다.
- 하지만 단일 함수로 작성하여 재료를 추가할 때마다 CoffeeMaker를 새로 생성한다.
- 한번에 여러 재료를 추가하고자 한다면 가변 함수를 사용할 수 있다.
public class Barista {
// 가변 함수
@SafeVarargs
public static CoffeeMaker decorate(CoffeeMaker coffeeMaker,
Function<CoffeeMaker, CoffeeMaker>... decorators) {
Function<CoffeeMaker, CoffeeMaker> reducedDecorations =
Arrays.stream(decorators)
.reduce(Function.identity(),
Function::andThen);
return reducedDecorations.apply(coffeeMaker);
}
}
- 데코레이션들은 Stream<Function<CoffeeMaker, CoffeeMaker>>을 사용해 배열로부터 스트림을 생성하고,
각각을 결합하여 모든 요소를 하나의 Function<CoffeeMaker, CoffeeMaker>로 축소한다. - 마지막으로 축소된 단일 데코레이션은 CoffeeMaker와 결합된다.
CoffeeMaker maker = Barista.decorate(new BlackCoffeeMaker(),
coffeeMaker -> new AddMilkDecorator(coffeeMaker, new MilkCarton()),
AddSugarDecorator::new);
- 가변 인자로 받을 수 있게 되어 코드가 단일 호출로 간소화되었지만,
재료추가의 구체적인 구현들을 한 메서드 안에 작성해야 한다. - 정적 팩토리 메서드를 사용하여 데코레이션을 그룹화하면
재료추가의 구체적인 구현을 공개할 필요가 없어지며, 추가 재료에만 집중할 수 있다.
public final class Decorations {
// Add Milk
public static Function<CoffeeMaker, CoffeeMaker> addMilk(MilkCarton milkCarton) {
return coffeeMaker -> new AddMilkDecorator(coffeeMaker, milkCarton);
}
// Add Sugar
public static Function<CoffeeMaker, CoffeeMaker> addSugar() {
return AddSugarDecorator::new;
}
}
CoffeeMaker maker = Barista.decorate(new BlackCoffeeMaker(),
Decorations.addMilk(new MilkCarton()),
Decorations.addSugar());
@SafeVarargs
가변 인자와 제네릭 사용 시 타입 안정성 문제를 경고한다.
자바에서 가변 인자 메서드는 내부적으로 배열을 사용하는데,
제네릭 타입의 배열은 타입 안정성을 완전히 보장하지 못한다. (공변, 불공변 차이)
예를 들어,
AddMilkDecorator는 Decorator의 하위 타입이고, Decorator는 결국 Object의 하위 타입이 때문에 가변인자 Decorator(Decorator...)는 타입 안전을 보장받을 수 있다.
제네릭의 경우 타입 정보는 컴파일 타임에만 존재하며, 런타임에는 타입 소거로 인해 사라진다.
가변 인자와 제네릭스의 조합으로 인해 발생할 수 있는 문제를 방지하려면,
해당 메서드가 타입 안전함을 보장할 수 있을 때 @SafeVarargs를 사용할 수 있다
3. 전략 패턴
4. 빌더 패턴
구조와 표현을 분리함으로써 복잡한 자료 구조를 만드는 데 사용되는 생성 패턴
회원 생성 예제
public record User(String email,
String name,
List<String> permissions) {
public User {
if (email == null || email.isBlank()) {
throw new IllegalArgumentException("'email' must be set.");
}
if (permissions == null) {
permissions = Collections.emptyList();
}
}
public static class Builder {
private String email;
private String name;
private final List<String> permissions = new ArrayList<>();
public Builder email(String email) {
this.email = email;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder addPermission(String permission) {
this.permissions.add(permission);
return this;
}
public User build() {
return new User(this.email,
this.name,
this.permissions);
}
}
public static Builder builder() {
return new Builder();
}
}
public class Main {
public static void main(String[] args) {
User.Builder builder = User.builder()
.name("성현")
.email("email@email");
User user = builder.addPermission("create")
.addPermission("edit")
.build();
System.out.println(user);
}
// User[email=email@email, name=성현, permissions=[create, edit]]
}
- 빌더의 각 컴포넌트는 호출 체인을 위해 Builder 인스턴스를 반환한다.
- 컬렉션 기반 필드에 대해서는 단일 요소를 추가할 수 있는 메서드를 제공한다.
함수적 접근 방식
- Consumer 기반의 -with 메서드를 사용해서 빌더를 함수적으로 표현해보자
public record User() {
//...
public static class Builder {
// ...
public Builder with(Consumer<Builder> builderFn) {
builderFn.accept(this);
return this;
}
public Builder withPermissions(Consumer<List<String>> permissionsFn) {
permissionsFn.accept(this.permissions);
return this;
}
public UserWith build() {
return new UserWith(this.email,
this.name,
this.permissions);
}
}
// ...
}
public class Main {
public static void main(String[] args) {
UserWith userWith = UserWith.builder()
.with(with -> {
with.email = "email2@email";
with.name = "성현";
})
.withPermissions(permissions -> {
permissions.add("create");
permissions.add("edit");
})
.build();
System.out.println(userWith);
}
}
'함수형 프로그래밍' 카테고리의 다른 글
[ch.15] 자바를 위한 함수형 접근 방식 (0) | 2024.08.13 |
---|---|
[ch.13] 비동기 작업 (0) | 2024.08.02 |
[ch.12] 재귀 (1) | 2024.07.20 |
[ch.11] 느긋한 계산법 (지연 평가) (0) | 2024.07.11 |
[ch.10] 예외 처리 (0) | 2024.07.04 |