디자인 패턴
- 반복해서 등장하는 프로그래밍 문제들에 대한 해법들을 패턴화 해놓은 것
- 패턴들을 숙지해놓으면 비슷한 문제가 생겼을 때 패턴들이 이정표가 되어준다.
- 생성 패턴 : 오브젝트의 생성에 관한 패턴
- 구조 패턴: 상속을 이용해 클래스/오브젝트를 조합하여 더 발전된 구조로 만드는 패턴
- 행동 패턴: 필요한 작업을 여러 객체에 분배하여 객체간 결합도를 낮추는 패턴
Builder Pattern
- 대표적인 생성 패턴
- 객체의 생성에 대한 로직과 표현에 대한 로직을 분리해준다
- 객체의 생성 과정을 유연하게 해준다
- 객체의 생성 과정을 정의하고 싶거나 필드가 많아 constructor가 복잡해질 때 유용
다음과 같은 field를 가진 User를 만든다고 해보자.
private int id;
private String name;
public String emailAddress;
public boolean isVerified;
public LocalDateTime createdAt;
public List<Integer> friendUserIds = new ArrayList<>();
User class 내에 빌더를 정의한다.
public static class Builder {
private int id;
private String name;
public String emailAddress;
public boolean isVerified;
public LocalDateTime createdAt;
public List<Integer> friendUserIds = new ArrayList<>();
private Builder(int id, String name) {
this.id = id;
this.name = name;
}
public User build() {
return new User(this);
}
public Builder withEmailAddress(String emailAddress){
this.emailAddress = emailAddress;
return this;
}
public Builder withVerified(boolean isVerified){
this.isVerified = isVerified;
return this;
}
}
유저 오브젝트 내에는 생성자를 빌더를 통해 구현한다.
public User(Builder builder) {
this.id = builder.id;
this.name = builder.name;
this.emailAddress = builder.emailAddress;
this.isVerified = builder.isVerified;
this.createdAt = builder.createdAt;
this.friendUserIds = builder.friendUserIds;
}
여기서 필수로 필요한 인자는 id와 name으로 설정한다면,
public static Builder builder(int id, String name) {
return new Builder(id, name);
}
이와 같은 메서드를 만들어 준다.
이와 같이 정의한 User를 생성해보자.
User user = User.builder(1, "name").build();
builder 메서드에서 id와 name을 다음과 같이 받고
public static Builder builder(int id, String name) {
return new Builder(id, name);
}
생성된 Builder 클래스는 build() 메서드를 통해 User 생성자를 호출하고
public User build() {
return new User(this);
}
생성자를 통해 객체가 생성된다.
public User(Builder builder) {
this.id = builder.id;
this.name = builder.name;
this.emailAddress = builder.emailAddress;
this.isVerified = builder.isVerified;
this.createdAt = builder.createdAt;
this.friendUserIds = builder.friendUserIds;
}
이렇게 만든 Builder 클래스에서 정의한 with 메서드는 다음과 같이 사용하면서 생성자에 인자값을 넣어줄 수 있다.
User.builder(2,"name2").withVerified(false).withEmailAddress("abc@emial.com");
원하는 것은 builder가 아니니까 build()를 호출해준다.
User.builder(2,"name2").withVerified(false).withEmailAddress("abc@emial.com").build();
with메서드는 이렇게 되어 있다.
public Builder withVerified(boolean isVerified){
this.isVerified = isVerified;
return this;
}
만약 default 값을 필드에 정의해주고 싶다면 Builder 내에서 필드를 초기화해주면 된다.
그런데 with~ 필드가 하나하나 만들어준다면, 필드가 많아질수록 계속 늘어나게 되는 문제가 생긴다.
이를 함수형 프로그래밍을 이용해 개선해보자.
public Builder with(Consumer<Builder> consumer) {
consumer.accept(this);
return this;
}
즉 컨슈머를 현재 builder에 적용을 시켜줌으로써
컨슈머를 통해서 필드를 set 해주고
return this 한다.
그렇다면 이를 어떻게 사용할까?
User.builder(3, "name3").with(builder -> {
builder.emailAddress = "123@email.com";
builder.isVerified = true;
builder.createdAt = LocalDateTime.now();
}).build();
이와 같이 하나의 setter 기능을 하는 consumer로서 통일시킬 수 있다.
Decorator Pattern
- 구조 패턴
- 용도에 따라 객체에 기능을 계속 추가(decorate)할 수 있게 해준다.
이 패턴을 통해 Price Processor를 만들어보자.
하나의 기능을 가진 processor로부터 여러 기능을 계속 덧붙여서 조합된 processor를 만드는 것이다.
먼저 다음과 같은 인터페이스를 구현한다.
public interface PriceProcessor {
Price process(Price price);
// 자신 다음에 실행될 프로세서를 받아온다.
// 호출시 새로운 프라이스 프로세서를 호출해주는데, Functional interface이므로 람다를 이용해 자신 먼저 작업을 하고
// 그 다음에 next로 들어온 process를 호출해주는 새로운 priceprocess를 호출해서 리턴해준다.
default PriceProcessor andThen(PriceProcessor next){
return price -> next.process(process(price));
}
}
위의 인터페이스를 구현하는 3가지 각기 다른 프로세서를 구현해보자.
public class TaxPriceProcessor implements PriceProcessor {
@Override
public Price process(Price price) {
return new Price(price.getPrice() + ", then applied tax");
}
}
public class DiscountPriceProcessor implements PriceProcessor {
@Override
public Price process(Price price) {
return new Price(price.getPrice() + ", then applied discount");
}
}
public class BasicPriceProcessor implements PriceProcessor {
@Override
public Price process(Price price) {
return price;
}
}
이와 같은 프로세서를 호출하고 인터페이스 프로세서에 default로 구현해놓은 메서드를 통해 연결한다.
BasicPriceProcessor basicPriceProcessor = new BasicPriceProcessor();
DiscountPriceProcessor discountPriceProcessor = new DiscountPriceProcessor();
TaxPriceProcessor taxPriceProcessor = new TaxPriceProcessor();
PriceProcessor decoratedPriceProcessor = basicPriceProcessor.andThen(discountPriceProcessor);
이 프로세서는 본인의 작업을 수행하고 이후 discount 작업을 수행한다.
이렇게 생성된 데코레이터 프로세서를 통해 price를 처리해보자.
Price processedPrice = decoratedPriceProcessor.process(originalPrice);
즉, basic processor가 discount 기능을 갖게 된 것이다.
여기에 tax를 처리하는 기능을 추가하려면 또 한번 데코레이팅을 해줄 수 있다.
PriceProcessor decoratedPriceProcessor2 = basicPriceProcessor.andThen(discountPriceProcessor).andThen(taxPriceProcessor);
이렇게 기능을 추가해줄 수 있다.
그런데 기능이 계속 늘어난다면 클래스가 너무 많아지는 문제가 생긴다. 이를 람다를 이용해 간편하게 처리해보자.
PriceProcessor decoratedPriceProcessor3 = basicPriceProcessor
.andThen(price -> new Price(price.getPrice() + " apply another process"));
Strategy Pattern
- 대표적인 행동 패턴
- 런타임에 어떤 전략(알고리즘)을 사용할지 선택할 수 있게 해준다.
- 전략들을 캡슐화하여 간단하게 교체할 수 있게 해준다.
예를 들어 다음과 같은 이메일 보내는 서비스를 개선해보자.
public class EmailService {
public void sendPlayWithFriendsEmail(User user) {
user.getEmailAddress().ifPresent(email ->
System.out.println("Sending 'Play With Friends' email to " + email));
}
public void sendMakeMoreFriendsEmail(User user) {
user.getEmailAddress().ifPresent(email ->
System.out.println("Sending 'Make More Friends' email to " + email));
}
public void sendVerifyYourEmailEmail(User user) {
user.getEmailAddress().ifPresent(email ->
System.out.println("Sending 'Verify Your Email' email to " + email));
}
}
먼저 EmailProvider라는 인터페이스를 만든다.
public interface EmailProvider {
String getEmail(User user);
}
email Provider가 인터페이스이기 때문에 구현체를 다음과 같이 주입하여 유연하게 필드를 초기화할 수 있다.
public class EmailSender {
private EmailProvider emailProvider;
public EmailSender setEmailProvider(EmailProvider emailProvider) {
this.emailProvider = emailProvider;
return this;
}
public void sendEmail(User user) {
String email = emailProvider.getEmail(user);
System.out.println("Sending " + email);
}
}
Email Provider 구현체는 다음과 같이 구현한다.
public class VerifyYourEmailAddressEmailProvider implements EmailProvider {
@Override
public String getEmail(User user) {
return "'Verify Your Email Address' email for " + user.getName();
}
}
public class MakeMoreFriendsEmailProvider implements EmailProvider {
@Override
public String getEmail(User user) {
return "'Make More Friends' email for " + user.getName();
}
}
이제 이 EmailProvider를 사용하는 예시를 살펴보자.
EmailSender emailSender = new EmailSender();
EmailProvider verifyYourEmailAddressEmailProvider = new VerifyYourEmailAddressEmailProvider();
EmailProvider makeMoreFriendsEmailProvider = new MakeMoreFriendsEmailProvider();
생성자를 통해 생성한 후
emailSender.setEmailProvider(verifyYourEmailAddressEmailProvider);
users.stream()
.filter(user -> !user.isVerified())
.forEach(emailSender::sendEmail);
emailSender에 해당하는 emailProvider를 주입해준다.
유저에게 이메일을 보내도록 한다.
users.stream()
.filter(user -> !user.isVerified())
.forEach(emailSender::sendEmail);
이때 이와 같은 메서드를 똑같이 매번 작성하더라도 emailSender에 어떤 provider를 넣어주느냐에 따라 다른 로직을 수행하게 된다.
Template Method Pattern
- 또 하나의 대표적인 행동 패턴
- 상위 클래스는 알고리즘의 뼈대만을 정의하고 각 단계의 세부 내용은 하위 클래스에게 정의를 위임하는 패턴
- 즉 상위 클래스는 템플릿을 정의하고, 디테일은 하위 클래스들이 정의한다.
- 알고리즘의 구조 즉 뼈대를 변경하지 않고 세부 단계들을 유연하게 변경할 수 있게 해준다.
템플릿 역할을 하는 추상 클래스로서 유저 서비스를 만들어보자. 이 유저 서비스는 알고리즘의 뼈대를 담당한다.
public abstract class AbstractUserService {
protected abstract boolean validateUser(User user);
protected abstract void writeToDB(User user);
public void createUser(User user){
if (validateUser(user)){
writeToDB(user);
} else {
System.out.println("error");
}
}
}
여기서 어떤 식으로 유저를 검증하고, 어떤 식으로 DB에 쓸 것인지 그 내용은 하위 클래스에서 정의한다.
이 서비스를 상속하는 유저 서비스를 만들어보자.
public class UserService extends AbstractUserService{
@Override
protected boolean validateUser(User user) {
System.out.println("validating user " + user.getName());
return user.getName() != null && user.getEmailAddress().isPresent();
}
@Override
protected void writeToDB(User user) {
System.out.println("write user");
}
}
이제 디테일을 이렇게 정의 했을 때 로직 플로우는 추상 클래스를 따르게 된다.
다음과 같이 사용한다.
User alice = User.builder(1, "Alice")
.with(builder -> {
builder.emailAddress = "alice@fastcampus.co.kr";
builder.isVerified = false;
builder.friendUserIds = Arrays.asList(201, 202, 203, 204, 211, 212, 213, 214);
}).build();
UserService userService = new UserService();
InternalUserService internalUserService = new InternalUserService();
// 큰 틀은 같지만 디테일은 다른 두 createUser()
userService.createUser(alice);
internalUserService.createUser(alice);
똑같은 템플릿을 통해 진행이 되지만 디테일하게 구현한 내부 메서드가 다르기 때문에 다른 로직을 수행한다.
함수형 프로그래밍을 이용해 좀 더 유연하게 구현해보자.
함수형 인터페이스를 필드로 갖는다.
public class UserServiceInFunctionalWay {
private final Predicate<User> validateUser;
private final Consumer<User> writeToDB;
public UserServiceInFunctionalWay(Predicate<User> validateUser, Consumer<User> writeToDB) {
this.validateUser = validateUser;
this.writeToDB = writeToDB;
}
정의한 predicate와 consumer를 생성자를 통해 공급 받는다.
UserServiceInFunctionalWay userServiceInFunctionalWay = new UserServiceInFunctionalWay(
user -> {
System.out.println("Validating user " + user.getName());
return user.getName() != null && user.getEmailAddress().isPresent();
},
user -> {
System.out.println("Writing user " + user.getName() + " to DB");
});
기존 abstract를 구현한 구현체들의 디테일들을 이렇게 바로 구현한다.
이제 기존의 호출과 같은 방식으로 다음과 같이 호출한다.
userServiceInFunctionalWay.createUser(alice);
Chain of Responsibility Pattern
책임 연쇄 패턴
- 행동 패턴의 하나
- 명령과 명령을 각각의 방법으로 처리할 수 있는 처리 객체들이 있을 때
-> 처리 객체들을 체인으로 엮는다
-> 명령을 처리 객체들이 체인의 앞에서부터 하나씩 처리해보도록 한다
-> 각 처리 객체는 자신이 처리할 수 없을 때 체인의 다음 처리 객체로 명령을 넘긴다.
-> 체인의 끝에 다다르면 처리가 끝난다.
- 새로운 처리 객체를 추가하는 것으로 매우 간단히 처리 방법을 더할 수 있다.
Order가 주어졌을 때 step들의 체인을 통과하면서 각 단계를 거쳐가면서 프로세스 되도록 만들어보자.
먼저 OrderProcessStep을 이렇게 정의한다.
public class OrderProcessStep {
private final Consumer<Order> processOrder;
private OrderProcessStep next;
public OrderProcessStep(Consumer<Order> processOrder) {
this.processOrder = processOrder;
}
public OrderProcessStep setNext(OrderProcessStep next) {
if (this.next == null) {
this.next = next;
} else {
this.next.setNext(next);
}
return this;
}
public void process(Order order) {
processOrder.accept(order);
Optional.ofNullable(next)
.ifPresent(nextStep -> nextStep.process(order));
}
}
현재 next가 set이 되어 있지 않다면 next는 새로 들어온 next가 된다.
만약 set이 되어 있다면 다음 orderProcess step에서 set next를 콜해준다. 계속해서 다음이 있다면 다음을 호출하고, 맨 마지막에 가서는 next가 없기 때문에 그때 거기서 next를 set하게 된다.
linkedList와 유사하다.
마지막에 process 메서드를 통해 order를 실행해준다. 받아온 컨슈머를 통해 실행하고, 다음 스텝이 있다면 next를 실행한다.
이 OrderProcessStep을 통해 워크플로우를 구현해보자.
OrderProcessStep initializeStep = new OrderProcessStep(order -> {
if (order.getStatus() == OrderStatus.CREATED) {
System.out.println("Start processing order " + order.getId());
order.setStatus(OrderStatus.IN_PROGRESS);
}
});
OrderProcessStep setOrderAmountStep = new OrderProcessStep(order -> {
if (order.getStatus() == OrderStatus.IN_PROGRESS) {
System.out.println("Setting amount of order " + order.getId());
order.setAmount(order.getOrderLines().stream()
.map(OrderLine::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add));
}
});
위와 같이 OrderStatus에 따라 다른 로직을 수행하는 스텝을 구현한다.
initializeStep은 초기화를 담당한다.
이후 setOrderAmountStep은 OrderStatus가 progress 상태라면 로직을 수행한다.
다른 스텝도 만들어보자.
OrderProcessStep verifyOrderStep = new OrderProcessStep(order -> {
if (order.getStatus() == OrderStatus.IN_PROGRESS) {
System.out.println("Verifying order " + order.getId());
if (order.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
order.setStatus(OrderStatus.ERROR);
}
}
});
OrderProcessStep processPaymentStep = new OrderProcessStep(order -> {
if (order.getStatus() == OrderStatus.IN_PROGRESS) {
System.out.println("Processing payment of order " + order.getId());
order.setStatus(OrderStatus.PROCESSED);
}
});
Order의 Amount가 0보다 작을 경우 verify에 실패하는 verify 스텝과 만약 verify를 통과했다면 payment를 수행하는 스텝이다.
이렇게 만든 스텝들은 앞에서 구현한 setNext를 통해 체인으로 연결된다.
OrderProcessStep chainedOrderProcessSteps = initializeStep
.setNext(setOrderAmountStep)
.setNext(verifyOrderStep)
.setNext(processPaymentStep)
.setNext(handleErrorStep)
.setNext(completeProcessingOrderStep);
public OrderProcessStep setNext(OrderProcessStep next) {
if (this.next == null) {
this.next = next;
} else {
this.next.setNext(next);
}
return this;
}
이렇게 하나의 체인으로 만들어진 스텝은 각 스텝이 자신에게 할당된 역할만을 담당한다.
reference. https://fastcampus.co.kr/dev_red_lsh
'Programming > Java, Spring' 카테고리의 다른 글
스프링 프로젝트 API Server Error 처리하기 (0) | 2023.07.09 |
---|---|
StringBuilder와 String 클래스의 문자열 만드는 효율 차이 (0) | 2023.06.21 |
자바 함수형 프로그래밍 Scope, Closure&Curry, Lazy Evaluation, Function Composition (0) | 2023.06.20 |
자바 Stream, max&min count, match, find, reduce, collectors, to map, grouping by, partitioning by, for each, parallel stream (0) | 2023.06.20 |
Java Stream, filter, map, sorted, distinct, flatmap (0) | 2023.06.19 |