[F-Lab 모각코 챌린지 19일차] 디자인패턴 (continue2)

부추·2023년 6월 19일
0

F-Lab 모각코 챌린지

목록 보기
19/66

TIL

  1. 상태 패턴 : 내부 상태 구성에게 행동 위임
  2. 프록시 패턴 : 대리자를 이용해 다른 서버의 객체를 받아오거나 권한에 따라 객체의 메소드 호출을 제한하는 패턴
  3. MVC 패턴 : 사용자의 컨트롤러 조작 -> 컨트롤러의 모델 조작 -> 모델의 뷰 업데이트



1. 상태 패턴

객체가 가지는 "상태"를 기반으로 동작이 달라지는 상황이 있다고 가정해보자. 사람은 1) 피곤한 상태, 2) 배고픈 상태, 3) 더러운 상태 를 가진다. 각 상태에서 eat(), sleep(), shower()가 구현되는 모습을 다음과 같이 구현했다.

public class Person {
	// 1 : 피곤함
    // 2 : 배고픔
    // 3 : 더러움
	private int state = 1;
    
    public void eat() {
    	// 배고픔(2) 때만 실행 -> 더러움(3)
    	if (state==1) {
        	System.out.println("피곤한데 밥먹을 정신이 어딨니.");
        } else if (state==2) {
        	System.out.println("허겁지겁 먹는다");
            setState(3);
        } else if (state==3) {
        	System.out.println("이 상태로 먹으면 식중독 걸림");
        }
    }
    public void sleep() {
    	// 피곤함(1) 때만 실행 -> 배고픔(2)
    	if (state==1) {
        	System.out.println("쿨쿨쿨..");
            setState(2)
        } else if (state==2) {
        	System.out.println("배고프면 잠 안오는 스타일");
        } else if (state==3) {
        	System.out.println("찝찝해서 어떻게 자니!");
        }
    }
    public void shower() {
    	// 더러움(3) 때만 실행 -> 피곤함(1)
    	if (state==1) {
        	System.out.println("샤워고 뭐고 피곤하다고!!");
            setState(2)
        } else if (state==2) {
        	System.out.println("배고파서 샤워기 들 힘도 없다.");
        } else if (state==3) {
        	System.out.println("으음! 샤워! 상쾌해!");
            setState(1);
        }
    }
    
    public void setState(int state) {
    	this.state = state;
    }
}

뭐, 기능을 하고있는 것 같긴 한데. 만약 피곤함~더러움 이외에 상태가 더 추가된다면? 혹은 eat(), sleep(), shower() 이외에 다른 메소드가 추가된다면? 수정하고 고쳐야 할 코드가 한둘이 아닐 것이다. if문이 저렇게 좍- 늘어진 것도 맘에 안든다. "너희 지금 전혀 객체 지향을 하고있지 않아"

클래스 내부에 상태를 둬서 해당 상태에게 메소드 호출을 위임하는 상태 패턴을 사용해보자.

가장 먼저 상태를 나타내는 State 인터페이스를 만든다. 이 인터페이스는 상태에 따라 달라지는 eat(), sleep(), shower() 메소드를 가진다.

public interface State {
    void eat();
    void sleep();
    void shower();
}

그리고 이 인터페이스를 피곤한 상태, 더러운 상태, 배고픈 상태가 구현한다. 각 상태에서 인터페이스의 메소드 상세 구현 내용은 다르다.

@RequiredArgsConstructor
public class TiredState implements State {
    private final Person person;
    @Override
    public void eat() {
        System.out.println("피곤해서 뭐 먹을 생각도 안드네..");
    }

    @Override
    public void sleep() {
        System.out.println("야호! 드디어 잔다!");
        person.setState(person.getHungry());
    }

    @Override
    public void shower() {
        System.out.println("피곤해.. 샤워할 힘도 없다.");
    }
}

@RequiredArgsConstructor
public class DirtyState implements State {
    private final Person person;

    @Override
    public void eat() {
        System.out.println("이 상태로 뭐 먹으면 식중독 걸릴걸?");
    }

    @Override
    public void sleep() {
        System.out.println("찝찝해서 어떻게 자니!");
    }

    @Override
    public void shower() {
        System.out.println("아.. 개운해.. 상쾌해!!! 드디어 씻는다!");
        person.setState(person.getTired());
    }
}

@RequiredArgsConstructor
public class HungryState implements State {
    private final Person person;
    @Override
    public void eat() {
        System.out.println("아악! 먹을거다! 당장 먹어!");
        person.setState(person.getDirty());
    }

    @Override
    public void sleep() {
        System.out.println("배고파서 잠도 안오는데,,");
    }

    @Override
    public void shower() {
        System.out.println("배고픈데 샤워는 무슨 샤워.");
    }
}

State 객체의 내부 구성으로 Person객체가 들어가는 것을 확인하자. 행위를 위임받은 상태 객체에서 Person의 상태를 업데이트하기 위함이다.

Person 객체 자체에서 메소드를 호출한 후 상태를 업데이트 해도 되는데, 그렇게 되면 상태를 확인하는 if문을 제거하려고 상태 패턴을 사용하는 의미가 퇴색된다. 상태가 정적으로만 업데이트 된다면 고려해봐도 되지만..(ex. 어떤 상황에서든지 진흙탕에서 놀면 더러운 상태가 된다든가 하는) 그런 상황은 그냥 최소화하는게 나을듯?

Anyway! 업데이트된 Person 객체 코드를 까보자!

@Getter
public class Person {
    private State currentState;
    private State dirty, hungry, tired;

    public Person() {
        dirty = new DirtyState(this);
        hungry = new HungryState(this);
        tired = new TiredState(this);

        currentState = tired;
    }

    public void sleep() {
        currentState.sleep();
    }
    public void eat() {
        currentState.eat();
    }
    public void shower() {
        currentState.shower();
    }

    public void setState(State state) {
        currentState = state;
    }
}

dirty, hungry, tired 객체를 각각 구성요소로 사용했는데 꼭 이럴 필요는 없다. State 인터페이스의 메소드를 스태틱으로 두고 객체 호출마다 Person 객체를 넘겨받는 식으로 구현해도 된다.

잠을 자면 피곤이 풀리지만 배고프고, 밥을 먹으면 배고픈 상태에선 벗어나지만 더러워지고, 샤워를 하면 더러움은 풀리지만 졸리고.. 일련의 상태 변경이 마치 연옥같다. 인간이란 참~,,




2. 프록시 패턴

프록시 패턴은 대리자 패턴이다. 클라이언트 객체가 다른 객체의 메소드를 이용하고 싶을 때, 프록시를 거쳐 프록시는 기존 객체가 제공하는 기능과 100% 같은 기능을 제공한다.

굳이 프록시를 이용해야 하는 이유엔 어떤 것들이 있을까?

  1. 다른 서버의 객체 기능을 이용해야 할 때. 직접 객체를 현재 어플리케이션이 돌아가고 있는 JVM 위로 부를 수 없으므로 프록시를 이용해야 한다.(원격 프록시)
  2. 사용자의 객체 접근을 제한하고 싶을 때. 사용자에게 허락되지 않는 메소드를 호출하려 하거나, 보안상 객체의 특정 부분의 접근을 막아야하는 경우에 실제 객체 호출과 클라이언트 사이에 프록시를 둘 수 있다.
  3. 생성에 비용이 큰 객체를 호출하는 동안 가짜 객체를 return할 수 있도록 중간에 프록시를 둘 수 있다.
  4. 객체의 내용을 중간에 저장하여 실제 객체에 접근하지 않고도 객체의 기능을 이용할 수 있는 "캐시"로써 프록시를 이용할 수도 있다.
  5. 실제 객체의 메소드 호출 전후로, 종단 로직과 상관 없는 횡단 로직을 수행시키고 싶을 때 사용할 수 있다.

외에 진짜 객체의 복잡도를 숨기기 위해(퍼사드와 유사), 동기화를 위해 등등 프록시를 이용할 이유는 많다.

다만 자바 어플리케이션 개발자 입장에서 실제로 프록시 패턴을 구현하기 위해 코드를 바닥부터 짜는 일은 거의 없고, 정말 프록시 기능을 구현한다면 (특히 동적 프록시) java.lang.reflect 안에 있는 Subject와 Invocator를 구현하는 식으로 구성할 수 있다고 한다.


구성 요소로 실제 객체를 감싼 뒤에 객체 메소드를 그대로 호출하는 것, 혹은 전후로 로그를 찍거나 트랜잭션 처리를 하는 등의 횡단 로직을 처리하는 것이 프록시의 끝이라 딱히.. 예제 코드를 덧붙이지 않았다.

스프링의 AOP, JPA의 @Transactional 등이 프록시 기반으로 동작한다는 것은 알아두자. (사실 JPA 트랜잭션 자체가 AOP를 이용한..) AOP가 적용된 메소드는 실제 메소드의 클래스 객체를 통해 호출되는 대신 프록시 객체가 감싼 형태로 호출된다.. 정도로 이해할 수 있다.




3. MVC 패턴

Model-View-Controller 패턴의 약자로, 사용자 인터페이스를 제공하는 디바이스부터 웹 어플리케이션에 이르기까지 전반적인 프로젝트에서 이용하는 구성 패턴이다. MVC 패턴의 각 컴포넌트에 대한 설명은 개발자마다 다르지만 전반적으로 아래와 같이 설명할 수 있다.

  • Model : 어플리케이션에서 실제로 이용되는 컴포넌트와 관련한 비즈니스 로직을 담당한다. View와 Controller에 필요한 인터페이스를 제공한다.
  • View : 모델에게 제공받은 로직의 결과를 눈에 보이는 결과물로써 사용자에게 제공한다.
  • Controller : 어플리케이션의 앞단에서, 사용자로부터 입력을 받고 적절한 model method를 호출하여 어플리케이션이 동작할 수 있도록 한다.

간단하게 view를 통해 사용자에게 결과를 보여주고, controller로 input을 받고, model로 로직을 처리하는 구조-라고 정리할 수 있을 것 같다.


초 슈퍼 간단한 예제를 하나 만들어봤다. 이름하여 사용자에게 숫자를 받아 오늘 운세를 출력해주는 프로그램이다.

로직을 다루는 Model이다.

public class Model {
    private View view;
    private final String [] message = {
            "최고의 날인 것 같아요! 뭘 해도 잘될듯 *^^*",
            "좋은 날이에요! 매사에 자신감을 갖고 임하세요!",
            "어떻게 하냐에 따라 달라지는 날입니다. 될 수 있는 만큼 긍정적으로 생각합시다!",
            "주의하는게 좋겠어요. 운이 따르지 않는 날이 될 것 같네요.",
            "모든 일에 조심하세요. 오늘 하루 만큼은 당신에게 좋지 않은 기운이 있습니다."};

    public void setView(View view) {
        this.view = view;
    }
    public String getMessage(int index) {
        return message[index];
    }

    public void generateMessage(int seed) {
        String message = getMessage(new Random(seed).nextInt(5));
        updateView(message);
    }

    public void updateView(String message) {
        view.printFortune(message);
    }
}

updateView() 메소드는 어떻게 보면 옵저버 패턴이 사용되었다. setView()를 통해 옵저버를 설정한 뒤, 메인 로직인 generateMessage() 호출이 끝나면 updateView를 통해 view 의 문구를 업데이트 할 수 있도록 했다.

뷰 객체를 보자.

@RequiredArgsConstructor
public class View {
    private final Controller controller;

    public void getInput() {
        System.out.println("랜덤한 숫자를 입력해주세요!");
        controller.getInput();
    }

    public void printFortune(String message) {
        System.out.println(message);
    }
}

모델이 호출한 printFortune()은 말 그대로 결과를 출력하는 함수였던 것이다. 이제 사용자 인터페이스를 제공하는 컨트롤러를 보자.

@RequiredArgsConstructor
public class Controller {
    private final Model model;

    public void getInput() {
        Scanner scanner = new Scanner(System.in);
        int input = scanner.nextInt();

        model.generateMessage(input);
    }
}

예시 어플리케이션은 인풋을 받아서 결과를 출력하는 모델 하나만 있으니 상관없지만, 모델이나 기능이 여러가지일 경우 컨트롤러가 사용자 요청/input에 대해 적절한 모델을 호출해야한다.

이제 실제로 MVC 객체를 만들어 운을 시험해보자!

public class FortuneTeller {
    public static void main(String[] args) {
        Model model = new Model();
        Controller controller = new Controller(model);
        View view = new View(controller);

        model.setView(view);

        view.getInput();
    }
}

돌려보면?

랜덤한 숫자를 입력해주세요!
123
어떻게 하냐에 따라 달라지는 날입니다. 될 수 있는 만큼 긍정적으로 생각합시다!

음.. 세 번 돌렸는데 계속 같은 결과다. 오늘은 그저 그런 날인 것 같다.




REFERENCE

헤드퍼스트 디자인패턴 10,11,12장

profile
부추튀김인지 부추전일지 모를 정도로 빠싹한 부추전을 먹을래

0개의 댓글