WEB/Design Pattern

행동 관련 디자인 패턴

Tony Lim 2023. 1. 29. 14:23

책임 연쇄 패턴

public static void main(String[] args) {
    RequestHandler chain = new AuthRequestHandler(new LoggingRequestHandler(new PrintRequestHandler(null)));
    Client client = new Client(chain);
    client.doWork();
}

spring security에서 쓰이는 FilterChain 처럼 여러 handler chain을 만든것이다. 하나의 클래스가 하나의 책임을 지니고 조건에 맞으면 일을 처리하고 다름 handler에게 넘겨주는 방식이다.

@WebFilter(urlPatterns = "/hello")
public class MyFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("게임에 참하신 여러분 모두 진심으로 환영합니다.");
        chain.doFilter(request, response);
        System.out.println("꽝!");
    }
}
@ServletComponentScan
@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

@WebFilter는 javax.servlet에서 제공하는 것이고 이것을 Spring에서 제공하는 @ServletComponentScan에서 읽어들여와서 Controller에서 /hello를 처리하기전에 필터역할을 하게 해준다.


커맨드 패턴

요청을 캡슐화 하여 호출자(invoker) 와 수신자(receiver)를 분리하는 패턴

myapp이 invoker , game = receiver가 된다.

invoker들은 Command 만 바라보고 사용할때 구체적인 Command구현체를 inject는 받고 사용하면 된다.

기존에는 reciver들을 일일이 직접 주입받고 관련 동작들을 실행시켜야했다. 즉 reciver의 코드가 바뀌면 모든 invoker들의 코드가 바꿔야한다. 하지만 command 가 중간에 껴있으면 이것을 추상화했으니 invoker의 코드는 바뀔 필요가 없다. 그리고 다른곳에서 command를 쓸수있으니 재사용성이 높아진다.

ExecutorService executorService = Executors.newFixedThreadPool(4);
executorService.submit(light::on);

Runable(command)에 를 ExecutorService(client) 에게 넘겨주어서 실행하게 하는것이 동일하다.


인터프리터 패턴

반복되는 문제 패턴을 언어 또는 문법으로 정의하고 확장할 수 있다.

자주 등장하는 문제를 간단한 언어로 정의하고 재사용하는 패턴

public static void main(String[] args) {
    PostfixExpression expression = PostfixParser.parse("xyz+-");
    int result = expression.interpret(Map.of('x', 1, 'y', 2, 'z', 3));
    System.out.println(result);
}
public class PostfixParser {

    public static PostfixExpression parse(String expression) {
        Stack<PostfixExpression> stack = new Stack<>();
        for (char c : expression.toCharArray()) {
            stack.push(getExpression(c, stack));
        }
        return stack.pop();
    }

    private static PostfixExpression getExpression(char c, Stack<PostfixExpression> stack) {
        switch (c) {
            case '+':
                return new PlusExpression(stack.pop(), stack.pop());
            case '-':
                PostfixExpression right = stack.pop();
                PostfixExpression left = stack.pop();
                return new MinusExpression(left, right);
            default:
                return new VariableExpression(c);
        }
    }
}

위예시를 보면 xyz -> VariableExpression 으로 들어가게되고 

+ 나오니 yz 꺼내서 PlusExpression으로 넣고

- 나오니 x , PlusExpression 꺼내고 MinusExpression을 넣고 최종적으로 MinusExpression 하나만 pop하게 된다.

public class MinusExpression implements PostfixExpression {

    private PostfixExpression left;

    private PostfixExpression right;

    public MinusExpression(PostfixExpression left, PostfixExpression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret(Map<Character, Integer> context) {
        return left.interpret(context) - right.interpret(context);
    }
}

map에서 주어진 value를 꺼내 뺄샘을 수행하게 된다.

public interface PostfixExpression {


    int interpret(Map<Character, Integer> context);

    static PostfixExpression minus(PostfixExpression left , PostfixExpression right) {
        return context -> left.interpret(context) - right.interpret(context);
    }
    static PostfixExpression plus(PostfixExpression left , PostfixExpression right) {
        return context -> left.interpret(context) + right.interpret(context);
    }

    static PostfixExpression variable(Character left) {
        return context -> context.get(left);
    }
}

interface에 static 함수로 lambda를 리턴하게 해서 작성할 수있다.


이터레이터 패턴

List<Post> posts = board.getPosts();
Iterator<Post> iterator = posts.iterator();

List = Aggreagte Interface 역할을 하게 된다. ArrayList 가 ConcreteAggregator 가 된다.

Iterator를 사용하면 Aggreate 클래스가 어떤 구현체를 사용하는지 아니면 어떤 인터페이스인지 관심을 가질 필요가 없고 Iterator만을 통해 순회하면 된다.

// TODO 가장 최신 글 먼저 순회하기
Iterator<Post> recentPostIterator = board.getRecentPostIterator();
while(recentPostIterator.hasNext()) {
    System.out.println(recentPostIterator.next().getTitle());
}

Itertor구현체중에 최신순으로 sort해놓은 Iterator를 통해 하고싶은 작업을 하게된다.

        XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
        XMLEventReader reader = xmlInputFactory.createXMLEventReader(new FileInputStream("Book.xml"));

        while (reader.hasNext()) {
            XMLEvent nextEvent = reader.nextEvent();
            if (nextEvent.isStartElement()) {
                StartElement startElement = nextEvent.asStartElement();
                QName name = startElement.getName();
                if (name.getLocalPart().equals("book")) {
                    Attribute title = startElement.getAttributeByName(new QName("title"));
                    System.out.println(title.getValue());
                }
            }
        }

xml을 만들거나 읽을때쓰게되는 StAx api의 예시 , 현재 Iterator 처럼 동작하는것을 알 수 있다.

cursor 기반 = 하나의 인스턴스가 존재하고 안에 내부 내용이 바뀐다. 메모리를 덜 쓰게 된다.

Iterator 기반 = 지나갈때마다 하나의 인스턴스가 계속 만들어진다. -> immutable 하게 인스턴스를 생성하기에 다루기가 수월하다.


중재자 패턴

Colleague 가 다른 Colleauge를 참조하지 않고 Mediator를 통해서만 서로를 참조하게 된다. 

여러 컴포넌트 간의 결합도를 중재자를 통해 낮출수 있다.

public class Guest {

    private Restaurant restaurant = new Restaurant();
    private CleaningService cleaningService = new CleaningService();

    public void dinner() {
        restaurant.dinner(this);
    }

    public void getTower(int numberOfTower) {
        cleaningService.getTower(this, numberOfTower);
    }
}

기존에는 guest가 직접 모든 서비스에 의존성을 지니고 있었다.

public class Guest {

    private Integer id;

    private FrontDesk frontDesk = new FrontDesk();

    public void getTowers(int numberOfTowers) {
        this.frontDesk.getTowers(this, numberOfTowers);
    }

    private void dinner(LocalDateTime dateTime) {
        this.frontDesk.dinner(this, dateTime);
    }

    public Integer getId() {
        return id;
    }
}

중재자인 FrontDesk에게 모든 의존성을 물려놓고 Guest는 FrontDesk(Mediator) 에게만 의존하고 서비스를 사용한다.

spring에서는 DispatcherServlet에서 중재자 역할을 하고 의존성들을 물고 있다.


메멘토 패턴

캡슐화를 유지하면서 객체 내부 상태를 외부에 저장하는 방법

객체 상태를 외부에 저장했다가 해당 상태로 다시 복구할 수 있다.

Originator = Game 

Memento = GameSave , 불변 객체로 내부정보를 의미한다.

public final class GameSave {

    private final int blueTeamScore;

    private final int redTeamScore;

    public GameSave(int blueTeamScore, int redTeamScore) {
        this.blueTeamScore = blueTeamScore;
        this.redTeamScore = redTeamScore;
    }

    public int getBlueTeamScore() {
        return blueTeamScore;
    }

    public int getRedTeamScore() {
        return redTeamScore;
    }
}
public static void main(String[] args) {
    Game game = new Game();
    game.setBlueTeamScore(10);
    game.setRedTeamScore(20);

    GameSave save = game.save();

    game.setBlueTeamScore(12);
    game.setRedTeamScore(22);

    game.restore(save);

    System.out.println(game.getBlueTeamScore());
    System.out.println(game.getRedTeamScore());
}

client에서 기존 game score를 저장했다가 originator객체의 필드 값을 변경하고 다시 저장된 Menento 를 기반으로 originator의 필드값을 복원한 예시이다.

public static void main(String[] args) throws IOException, ClassNotFoundException {
    // TODO Serializable
    Game game = new Game();
    game.setRedTeamScore(10);
    game.setBlueTeamScore(20);

    // TODO 직렬화
    try(FileOutputStream fileOut = new FileOutputStream("GameSave.hex");
    ObjectOutputStream out = new ObjectOutputStream(fileOut))
    {
        out.writeObject(game);
    }

    game.setBlueTeamScore(25);
    game.setRedTeamScore(15);

    // TODO 역직렬화
    try(FileInputStream fileIn = new FileInputStream("GameSave.hex");
        ObjectInputStream in = new ObjectInputStream(fileIn))
    {
        game = (Game) in.readObject();
        System.out.println(game.getBlueTeamScore());
        System.out.println(game.getRedTeamScore());
    }
}

여기서 GameSave.hex가 메멘토역할을 하게 되는것이다. 확장자는 아무의미없다.


옵저버 패턴

다수의 객체가 특정 객체 상태 변화를 감지하고 알림을 받는 패턴

발행(publish)  - 구독(subscribe) 패턴을 구현할 수 있다.

chatserver에게 User가 관심있는 주제와 함께 등록이된다.

chatserver를 통해 sendMessage를 하게되면 현재 보낸 메시지가 관심있는주제와함께 등록된 User들에게 전송(handleMessage)이된다

@Component
public class MyEventListener {

    @EventListener(MyEvent.class)
    public void onApplicationEvent(MyEvent event) {
        System.out.println(event.getMessage());
    }
}
@Component
public class MyRunner implements ApplicationRunner {

    private ApplicationEventPublisher publisher;

    public MyRunner(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        publisher.publishEvent(new MyEvent("hello spring event"));
    }
}

Observer가 MyEventListener 에 대응이되고 MyRunner가 event를 publisher를 통해 보낸다.
spring에서 ApplicationContext가 사실상 EventPublisher다. 

ApplicationEventPublisher가 가장 구체적인 interface이고 ApplicationContext가 이걸 구현하고 있다.


상태 패턴

상태에 특화된 행동들을 분리해 낼 수 있으며 , 새로운 행동을 추가하더라도 다른 행동에 영향을 주지 않는다.

public void addReview(String review, Student student) {
    if (this.state == State.PUBLISHED) {
        this.reviews.add(review);
    } else if (this.state == State.PRIVATE && this.students.contains(student)) {
        this.reviews.add(review);
    } else {
        throw new UnsupportedOperationException("리뷰를 작성할 수 없습니다.");
    }
}

다음과 같이 state에 따라 해야할일이 분기처리가 복잡하게 될때 사용하게 된다.

public void addReview(String review, Student student) {
    this.state.addReview(review, student);
}
@Override
public void addReview(String review, Student student) {
    if (this.onlineCourse.getStudents().contains(student)) {
        this.onlineCourse.getReviews().add(review);
    } else {
        throw new UnsupportedOperationException("프라이빗 코스를 수강하는 학생만 리뷰를 남길 수 있습니다.");
    }
}

Private State에 addReview를 override를 해서 Context 클래스는 State에게 그냥 위임하면 알아서 state에 맞게 처리가 되는것이다.


전략 패턴

client가 BlueLirghtRedLight를 만들때 자신이 원하는 Strategy를 주입해 줄 수 있다.

public void blueLight() {
    if (speed == 1) {
        System.out.println("무 궁 화    꽃   이");
    } else if (speed == 2) {
        System.out.println("무궁화꽃이");
    } else {
        System.out.println("무광꼬치");
    }
}

기존 Client는 전략없이 하나의 blueLight메소드만 의존했고 해당 메소드에서 모든 전략을 처리하였다.

public static void main(String[] args) {
    BlueLightRedLight game = new BlueLightRedLight();
    game.blueLight(new Normal());
    game.redLight(new Fastest());
    game.blueLight(new Speed() {
        @Override
        public void blueLight() {
            System.out.println("blue light");
        }

        @Override
        public void redLight() {
            System.out.println("red light");
        }
    });
}

BlueLightReadLight를 사용할때 메소드와 함께 어떤 전략을 취할것 인지 넘겨주게 된다. Context는 어떤 전략을 취하든지 변경될 이유가 사라졌다.

Collections.sort(numbers, Comparator.naturalOrder());

탬플릿 패턴

security 설정을 초기화하는 init , configure 호출하는 builder 또한 일종의 탬플릿패턴이다. 

탬플릿 콜백은 abstract class를 상속하는 대신 , 해야할 일을 람다로 넣어주는 것이다.


비지터 패턴

기존 코드를 변경하지 않고 새로운 기능을 추가한다. 

Double Dispatcher가 일어난다. = 런타임에 어떤 구체적인 구현체에 따라 호출하는 메소드가 달라지는 dispatch 과정이 2번일어나게 된다.

public static void main(String[] args) {
    Shape rectangle = new Rectangle();
    Device device = new Pad();
    rectangle.accept(device);
}
public interface Shape {
    void accept(Device device);
}

첫번째 dispatch가 일어나는 시점이다. Shape이 어떤 구체클래스를 통해 accept가 호출 될지 결정난다.

public interface Device {
    void print(Circle circle);
    void print(Rectangle rectangle);
    void print(Triangle triangle);
}
public class Pad implements Device {
    @Override
    public void print(Circle circle) {
        System.out.println("Print Circle to Pad");
    }
    @Override
    public void print(Rectangle rectangle) {
        System.out.println("Print Rectangle to Pad");
    }
    @Override
    public void print(Triangle triangle) {
        System.out.println("Print Triangle to Pad");
    }
}
public class Rectangle implements Shape {
    @Override
    public void accept(Device device) {
        device.print(this);
    }
}

2번째 distpatch가 일어나는 시점이다. 어떤 Device의 print를 호출할지 결정나는 시점이다.

여기서 Shape가 Element에 해당이 되는것이고 추가되는 Device가 Visitor에 해당되는 것이다. 

새로운 device가 추가되더라도 client는 원하는 Eelement에 accept만 호출하면 된다.

메소드 오버로딩은 컴파일타임에 static하게 맵핑이된다. 예를들어 Shape.accpet의 인자로 구체적인 Device 구현체 클래스가 들어가야한다면 Device 타입으로 주입할 수 없다. 컴파일 에러가 나기 때문이다.

public static void main(String[] args) throws IOException {
    Path startingDirectory = Path.of("/Users/keesun/workspace/design-patterns");
    SearchFileVisitor searchFileVisitor =
            new SearchFileVisitor("Triangle.java", startingDirectory);
    Files.walkFileTree(startingDirectory, searchFileVisitor);
}

walkFileTree는 주입받은 FileVisitor구현체를 통해서 실제 행해야할 메소드들을 FileVisotr.#method 를 통해 실행하게 된다.

 

'WEB > Design Pattern' 카테고리의 다른 글

구조 관련 디자인 패턴  (0) 2023.01.27
객체 생성 관련 디자인 패턴  (0) 2023.01.26