[디자인 패턴] 객체지향 디자인 패턴 + 활용 코드

Benjamin·2023년 6월 13일
0

CS

목록 보기
9/10

Singleton pattern

  • 소프트웨어를 만들다보면 어떤 클래스의 객체가 해당 프로세스내에서 딱 하나만 만들어져야할 때

ex) 사용자가 내가 만든 앱을 사용하는데, 다크모드 설정을 해두면 다른 페이지로 이동해도 다크모드가 계속 유지되어있어야 함.
-> 세팅을 관리하는 객체는 반드시 같은것을 사용해야한다! = 객체가 하나만 만들어지도록


위 이미지를 예시를 보면, Settings.java에서 darkMode를 설정하고 접근할 수 있게 되어있다.
그리고 보여지는 페이지(FirstPage.java, SecondPage.java)를 만드는데, 이때 위처럼 Settings 객체를 각각 만들어 사용하기때문에 MyProgram.java의 main을 실행해보면 첫번째 페이지의 다크모드가 두번째 페이지에는 적용되지 않는다.

💡이 문제를 해결하기위해 Singleton이 사용되는것이다!

Settings.java의 코드를 변경해보자.

  • 생성자를 private으로 선언한다
    -> 다른 클래스에서 new로 생성하지못하게 된다.

  • static으로 클래스 자기자신의 객체를 하나둔다.

    이 이미지를 보자.
    static이 아닌 변수나 메소드들은 객체가 생성될때마다 메모리의 공간을 새로 차지하지만, static으로 선언된 것들은 객체가 얼마나 만들어지든 메모리의 지정된 공간에 딱 하나만 존재한다.

(컴파일 할 때부터 이 요소가 차지할 메모리 용량을 알 수 있게 정해져있기때문에 동적요소과 대비되는 개념으로 static(정적)이라고 불린다.)

정적 메소드 getSettings()를 만드는데,이 부분이 중요하다.
처음에 settings 객체는 null로 초기화되어있다.
getSettings()를 보면 if(settings == null)을 체크하고있다. 이 뜻은, settings가 아직 그 상태라면(즉 다른곳에서 getSettings()를 실행하기 전이라면) Settings객체를 선언해서 settings변수에 넣어주고 반환하며, 어디선가 이미 getSettings()를 실행해서 Settings객체가 만들어진 상황이라면 그걸 그대로 반환한다.

각 페이지에서 settings변수에 값을 넣는 부분을 위처럼 변경하자.

Settings.java의 static 메소드(getSettings())는 이미 메모리의 정적 공간에 자리를 차지하고있는 상태이기때문에, 해당 객체를 생성하지않아도 위 이미지처럼 바로 클래스에서 불러낼 수 있다.

이제 실행해보면, 두 페이지에 다크모드 설정이 동일한것을 볼 수 있다.
이는 FirstPage 에서 정적 settings에 값을 넣었고, SecondPage는 이걸 그대로 가져다쓰기때문이다.

그냥 정적변수들을 쓰지 왜 싱글턴을 쓸까?

  • 인터페이스의 사용이나 lazy loading 등 싱글턴으로 할 수 있는것들이 더 많기때문이다.

(하지만 이렇게 작성하면 멀티쓰레드 환경에서 오류가 발생할 소지가있다.
기본적인 코드를 알아봤고, 싱글턴을 안전하게 사용할 수 있는 방법은 다음에 알아보도록 하자.)

Strategy pattern

  • '전략패턴'이라고 불린다.

예시를 보자.

검색화면을 만든다고 가정해보자. 체크된 버튼들 중 하나를 눌러서 모드를 설정하고, 선택된 모드에 따라 검색버튼을 눌렀을 때 실행되는 검색의 방식이 결정되도록 하는 가정이다. 즉, 프로그램 실행 중 모드가 바뀔때마다 전략(검색이 이루어지는 방식)이 수정된다는것이다.

우선 버튼들이 위 이미지 속 코드처럼 각 메소드들을 실행한다고 가정하고, 패턴 활용없이 단순하게 짜보자.

  • Mode mode = 현재 선택되어있는 모드

검색 버튼이 눌려서 searchButton객체의 onClick()이 실행되면 mode에 어떤 값이 들어있느냐에따라서 해당 동작이 행해진다.

실제 코드는 위 이미지보다 훨씬 더 길고 복잡할텐데, 각각에 수정사항이 생기거나 새로운 기능들이 추가되면 onClick()을 그때그때 다시 수정해줘야한다.

이런 설계는 우아하지않다! 🧐
SW가 커지고 복잡해질수록 코드를 분석하고 관리하기 어려워지며, 클래스마다 역할지정을 뚜렷히해서 모듈화 된 소프트웨어를 구축해가야하는 객체지향의 철학에도 어긋나기때문이다.

💡 전략 패턴으로 해결할 수 있다.

전략패턴은 Mode마다의 동작 하나하나를 모듈로 따로 분리해서, 버튼들을 누를때마다 검색버튼을 누를 때 실행될 검색 모듈을 갈아끼워주는 방식으로 코드를 짜는것이다.

전략패턴의 코드를 살펴보자!

onClick()내의 실행코드를 다 없애고, SearchStrategy로 만든 객체의 search()만 실행해도록 했다.

(인터페이스는 그 자체로 객체를 만들 수 없지만, 특정 인터페이스를 implement한 클래스는 해당 인터페이스에서 지정한 메소드를 필수로 선언해야한다.)

버튼들의 역할은 어떻게 될까?

이처럼 searchButton객체의 검색 전략을 각각의 것으로 갈아끼워넣는것이다.

이제, 만약 이미지 검색 방식이 변경되면 해당하는 클래스 내용을 찾아 수정하면되고, 새로운 검색방식이 추가되면 SearchStrategy를 implement한 새 클래스를 만들어서 연결해주면된다.

📚 이처럼 옵션들마다의 행동들을 모듈화해서 독립적이고 상호 교체 가능하게 만드는 것이 전략패턴이다.

State pattern

이 패턴의 설계 구조는 Strategy와 비슷하다.

이 둘이 어떻게 다른지 설명하자면, 전략 패턴은 어떤 동일한 틀 안에 있는 특정 작업의 방식, 모드를 바꿔줄 때 사용하고, State 패턴은 TV가 꺼져 있을 때 누르면 켜지고, 켜진상태에서 누르면 꺼지는 버튼처럼 특정상태마다 다르게 할 일을 나아가서 그 상태들 자체를 그 상태마다 실행시 할 일과 함께 하나하나 모듈화해서 지정해둘 때 쓴다.

앱 화면의 다크모드 여부를 껐다 켰다하는 스위치 프로그래밍 예제를 살펴보자.

📚 Strategy 패턴이, 지정된 특정 메소드가 모듈화된 모드에 따라 다르게 실행되도록 하는거라면 State패턴은 그 메소드가 실행될 때 모드도 전환되도록 하는 것이라고 이해해도 좋다.

Command pattern

예제를 찾아보면, Strategy와 유사하다.
근본적인 차이는 다음과같다.전략 패턴은 같은 일을 하되 알고리즘이나 방식이 갈아끼워지는거라면, 커맨드 패턴은 하는 일 자체가 다른것이다.

따라서 커맨트 패턴이 사용되는 모습은 다양하다.
전략 패턴의 예제처럼, 모드 변경에따라 명령을 갈아끼워넣는 식으로 작성하기도하고, 위키피디아에 나와있는것처럼 다른 명령을 심어주는 식으로 짤수도있고, 이후 살펴볼예제처럼 여러 명령들을 목록으로 실어보내서 차례로 실행시킬 수도 있다.

❓ 의문점

전략처럼 명령을 갈아끼우는건뭐고, 위키피디아처럼 명령을 심어주는건 뭔지 정확히 이해가 잘 안간다..

<출처 : 위키피디아의 커맨드 패턴 내용>

/*the Invoker class*/
public class Switch {
    private Command flipUpCommand;
    private Command flipDownCommand;

    public Switch(Command flipUpCmd,Command flipDownCmd){
        this.flipUpCommand=flipUpCmd;
        this.flipDownCommand=flipDownCmd;
    }

    public void flipUp(){
         flipUpCommand.execute();
    }

    public void flipDown(){
         flipDownCommand.execute();
    }
}

/*Receiver class*/

public class Light{
     public Light(){  }

     public void turnOn(){
        System.out.println("The light is on");
     }

     public void turnOff(){
        System.out.println("The light is off");
     }
}


/*the Command interface*/

public interface Command{
    void execute();
}


/*the Command for turning on the light*/

public class TurnOnLightCommand implements Command{
   private Light theLight;

   public TurnOnLightCommand(Light light){
        this.theLight=light;
   }

   public void execute(){
      theLight.turnOn();
   }
}

/*the Command for turning off the light*/

public class TurnOffLightCommand implements Command{
   private Light theLight;

   public TurnOffLightCommand(Light light){
        this.theLight=light;
   }

   public void execute(){
      theLight.turnOff();
   }
}

/*The test class*/
public class TestCommand{
   public static void main(String[] args){
       Light light=new Light();
       Command switchUp=new TurnOnLightCommand(light);
       Command switchDown=new TurnOffLightCommand(light);

       Switch s=new Switch(switchUp,switchDown);

       s.flipUp();
       s.flipDown();
   }
}

예제로, 로봇이 할 일을 미리 순서대로 입력한 뒤 출발버튼을 누르면 이를 차례로 수행하는 학습용 장난감을 프로그래밍해보자.

Robot 클래스에는 메소드로 로봇이 할 수 있는 일들이 있다.

많은 예제들에서 인터페이스를 사용하지만 이 예제에서는 abstract 클래스 (추상 클래스)를 사용한다.

추상 클래스

일반 부모 클래스는 메소드뿐만 아니라 멤버 변수도 물려준다.
인터페이스와 비교해서 제한되는 점이라면 아래와같다.

  • 인터페이스는 한 클래스가 다중상속을 받을 수 있지만(몇 개의 자격증이든 딸 수 있지만), 클래스는 다중상속을 받을 수 없다.(상속해줄 부모님은 여럿 둘 수 없다)

    abstract는 지병이 있어서 밖에 돌아다니지 못한다고 생각하자.

    스스로는 객체를 생성해서 활동하지 못하지만, 상속받은 자식 클래스들이 물려받은 메소드와 변수를 그대로 또는 자기 방식대로 수정해서 그리고 필요시 자기만의 변수와 메소드를 추가해서 활동한다.

다시 Robot 예제를 살펴보면, Command 추상 클래스는 robot 변수, setRobot(), execute()가 있다.
execute()는 추상메서드라 인터페이스의 메소드처럼 부모 클래스가 아닌 자식클래스에서 구현된다. (부모가 시키기만했지 가르쳐주지는 않은것이다)

Command를 상속받은 클래스 3개를 살펴보자. 각 클래스를 사용해 로봇에게 내릴 명령을 객체로 저장할 수 있다.

이것들을 총괄할 RobotKit 클래스다.

커맨드 패턴을 잘 응용하면 각종 생산성 툴처럼 여러 작업을 수행한 뒤, 뒤로가기나 앞으로가기를 구현하는 등 다양한 방식으로 활용될 수 있다.

Adapter pattern

어답터란 형식이 다른 둘 사이에 연결돼서 이 둘이 호환될 수 있도록 해주는 도구이다.

객체지향 프로그래밍에서 어답터는 보통 인터페이스가 서로 다른 객체들이 같은 형식 아래 작동할 수 있도록 하는 역할을 한다.

인터페이스는 자격증같은거라고 했었다. 예를 들어 양식 자격증을 가진 요리사는 '요리'를 하고, 파티셰 자격증을 가진 파티셰는 '제과'를 한다고 치자. 요리사들이 일하는 큰 양식당에서 디저트 메뉴를 제공하기위해 파티셰를 고용했다.
매니저가 누구한테는 '요리'해라, 누구한테는 '제과'해라 구분해서 말할 정신이 없다.
그래서 파티셰에서 어답터를 달아준다.

  • Adapter = 파티셰님은 앞으로 제가 "'요리'해주세요" 라고 하면, '제과'를 해주시면 됩니다.

이제 매니저는 누구에게든 '요리'를 시키면된다.

예제 1

Strategy 패턴의 검색버튼 예제를 다시보자.

사이트가 다른 서비스와 연결이되면서 '동영상'을 검색하는 옵션도 가져오게됐다고 가정하자.(동영상 버튼 추가)

그런데 이 동영상 검색방식은 다른 회사에서 짠거라 우리가 쓰던것과는 인터페이스가 다르다.

인터페이스 이름 뿐만아니라, 메소드명도 다르고 메소드의 형식도 다르다.

따라서 이렇게 SearchButton에서 사용할 수 있는 검색 방식으로 끼워넣어지는건 불가능한 형태다.
그렇다고 FindAlgorithm 인터페이스에 맞춰서 SearchButton 클래스를 수정하거나 새로 만드는것도 효율적인 방법은 아니다.

💡 이 문제는 어답터로 해결할 수 있다!

FindAlgorithm 인터페이스에 어답터를 끼워서 SearchStrategy들과 함께 사용될 수 있도록 해보자.

SearchFindAdapter란 클래스를 만든다.
SearchStrategy 인터페이스를 구현한 구현체이니까 SearchStrategy로서 작동한다.

멤버변수로 FindAlgorithm의 객체가 생성자를 통해 넣어지게된다.
그리고 search()를 실행하면 FindAlgorithm의 find()를 실행하면서 항상 글로벌로 검색하도록 값도 넣어준다.
"내가 search()하면, 너는 find(true)로 알아들으면 돼." 하고 알려주는것이다.

setModeMovie()에서 사용된 모습을 보자.
어답터안에 FindAlgorithm 객체를 넣어서 SearchStrategy 형태로 끼워지도록 하는걸 볼 수 있다.

예제 2

커맨드 패턴의 로봇 예제에도 적용해보자.

네모 칸을 보면, 같은 로봇인데 다른 RobotKit을 사용하는 한 사용자가 로봇이 뒤로 돌아서 이동하게하는 명령을 자동화 명령어로 만들어 공유했다.

그런데 보다시피 Order명령은 형식이 다르다.
명령 클래스(MoveBackOrder)가 Command라는 추상클래스에서 상속받은게 아니라, Order라는 인터페이스를 적용한 클래스이고, 또 Command 클래스처럼 Robot을 내부 변수로 갖는게아니라 run()명령어에 인자로 넣어준다.


따라서 우리의 RobotKit에서는 Order명령이 호환되지 않는다.

💡 이 문제를 어답터로 해결해보자!

어답터 코드는 위와같다.
멤버변수 Order객체를 생성자로 받아온다.

"내가 execute() 하면, 너는 갖고있는 robot으로 run()하면 돼." 라고 알려주는 것이다.

이렇게 어답터에 끼워서 명령 리스트에 넣어주면 잘 작동한다.

Proxy pattern

  • '대리인 패턴'

어느정도 규모가 있는 회사 대표는 만나기 어렵다.
어지간한 일은 비서, 대리인을 통해 처리가된다. 회사 대표가 몸소 등장해야 하는 일은 그만큼 중요도가 있고, 권한이 필요한 일이다.

프로그래밍에서 사용되는 클래스들 중에서도 인터넷에서 받아와야 해서 시간이 걸리거나 메모리를 많이 차지하거나 하는 등의 이유로 객체로 여럿 생성하기가 부담이 되는 것들이 있다.
그럴때에는 그 클래스의 "Proxy" 대리자 역할을 하는 클래스를 따로 둬서 가벼운 일은 그 대리인이 처리하고, 대표가 직접 나와야하는 무거운 작업을 할 때 비로소 실제 클래스를 생성해서 사용하는 것이다.

예제를 살펴보자.

유튜브를 들어가서 뜨는 영상 리스트들에 마우스를 올려두면, 해당 영상의 프리뷰가 실행되는것을 볼 수 있다.

비슷한 형식으로, 영상들의 썸네일이 뜨되, 기본적으로는 제목이 나타나고 마우스 오버시 유튜브처럼 프리뷰가 재생되는 사이트를 만드려고한다고 가정하자.

제목을 화면에 나타내는건 가벼운 작업

  • 프리뷰를 보여주려면, 영상 데이터를 받아와야한다. (무거운 작업)

썸네일을 담당하는 객체는 제목과 프리뷰를 두 메소드를 통해 각각 보여주도록 하되, 썸네일이 처음 화면에 나타날 때는 일단 제목만 보여줄 수 있는 프록시로 생성되게하고, 프리뷰로 보여주는 무거운 작업은 실제 클래스가 담당하도록 해서 프록시 객체로 생성된 썸네일에 커서를 올릴 때, 실제 클래스가 호출돼서 프리뷰를 보여주게 한다.

  • Thumbnail 인터페이스를 둔다 - 제목을 보여주는 메소드와 프리뷰를 재생하는 메소드를 모두 둔다.
  • 실제 클래스인 RealThumbnail 클래스, 대리인 클래스인 ProxyThumbnail 클래스 모두 Thumbnail인터페이스를 적용한다.
  • RealThumbnail 클래스는 생성 과정에서 시간이 걸리는 작업(영상을 받아오는 작업)을 수행한다.
    -> 실제 영상 데이터를 받아와 갖고있기 때문에 제목을 보여주는 showTitle뿐만 아니라, 프리뷰를 재생하는 showPreview 메소드도 실제로 실행할 수 있다.
  • ProxyThumbnail은 영상을 받아오지 않기 때문에 생성하는데 시간이 걸리지도 않고, 객체도 가볍다.

  • ProxyThumbnail은 RealThumbnail과 같은 인터페이스를 적용했기 때문에 showPreview()도 갖고는있다. 하지만 직접 실행하지는 않는다.

  • ProxyThumbnail에 멤버변수 RealThumbnail이 있는데, 이는 ProxyThumbnail이 생성될 때에는 일단 값이 지정되지않고 null이다.

  • showTitle()같은 가벼운 작업은 프록시가 스스로한다.

  • 영상 데이터를 필요로하는 showPreview()는 프록시 능력을 벗어나는 일이다.
    그때, 실제 썸네일인 RealThumbnail 객체를 생성해서 실제 썸네일 객체를 통해 실행하는것이다.
    대표님을 불러오는 것!

이제 사용되는 부분의 코드를 보자.

MyProgram.java를 보면, ProxyThumnail 객체를 생성하고 ArrayList에 넣어서 화면에 나타내보면 하나하나 제목이 표시된다. (주석으로 표현)

그리고 마우스를 올릴때에야 실제 객체 생성시 출력되는 문자열들이 나오는것을 볼 수 있다. (주석으로 표현)
한번 RealThumbnail로 생성한 썸네일은, 다시 영상을 받아오지 않는것도 알 수 있다 (프리뷰 실행 주석의 3번째 줄은 '~의 영상 데이터 다운'없이 '~의 프리뷰 재생'이 바로 뜸)

이처럼 필요할때에만 실제 객체를 생성하기때문에 효율적이고 유연한 프로그래밍이 가능해진다.

프록시 패턴은 다른 여러 방식으로 응용되기 때문에 예제들을 찾아보면서 다양한 활용 예를 공부해보자!


출처
https://www.youtube.com/watch?v=lJES5TQTTWE

0개의 댓글