해당 포스팅은 한빛 미디어 헤드퍼스트 디자인 패턴(에릭 프리먼, 엘리자베스 롭슨 저)를 통해 공부한 내용을 정리한 블로그입니다.

아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
좋은 프로그래머는 자기 두뇌를 사용한다. 그러나 좋은 가이드라인은 모든 케이스를 고려해야만 하는 노력을 줄여준다.
(Francis Glassborow, 개발자)

Combined Pattern


여러 패턴을 함께 사용하여 더욱 강력한 객체지향 디자인 패턴

  • 패턴 몇개를 결합한다고 무조건 복합 패턴이 되는 것이 아님.
  • 여러가지 문제의 일반적인 해결법을 제시해야함.
    • ex) MVC패턴
    • 여러 패턴에 대해 적용 가능한 시나리오 작성.
      • 같은 역할을 하는 클래스에 대해 기능 통일 → 상속
      • 다른 클래스에서 같은 인터페이스를 사용하도록 연결하는 역할 → 어댑터 패턴
      • 다양한 클래스를 한데 묶어 소리를 낸 횟수 세려면 → 데코레이터 패턴
      • 데코레이터에서 객체를 제대로 포장하지 않으면 원하는 행동 추가 불가능. 데코레이터로 감싸는 부분을 하나로 빼내서 캡슐화 → 팩토리 패턴
      • 다양한 클래스를 한데 관리 ⇒ 반복자 패턴, 구성패턴
      • 하나의 클래스에 대해 관리 ⇒ 옵저버 패턴

Model - View - Controller 알아보기

View

  • Model을 표현하는 방법을 제공
  • 화면에 표시할 때 상태와 데이터는 Model에서 가져오기

Controller

  • Controller에서 Model을 조작
  • 사용자가 인터페이스를 건드리면 그 행동이 Controller에게 전달
  • 사용자에게 입력을 받으며 입력받은 내용이 Model에게 의미 전달.

Model

  • View에게 상태 변화 통기
  • 필요한 상태, 데이터, 애플리케이션 로직은 Model에 들어있음.

동작

  • 사용자는 View에만 접촉 가능
  • Controller가 Model에게 상태를 변경하라고 요청
    • 사용자의 행동을 받아서 해석
    • 의미해석, 모델 조작 결정.
  • Controller가 View를 변경해달라고 요청
    • 결과로 View에게 바꿔달라고 요청할 수 있음.
  • 상태가 변경되면 Model이 View에게 사실 전달
    • 사용자가 한 행동으로 내부적인 변화 등으로 모델이 변경되면 뷰에게 상태가 변경되었다고 요청
  • View가 Model에게 상태 요청

모델-뷰-컨트롤러에 사용되는 패턴 알아보기

  • 컴포지트 패턴
    • 디스플레이는 여러 단계로 겹쳐있는 윈도우, 패널, 텍스트로 구성
    • 각 디스클레이는 복합 객체나 잎버튼이 될 수 있음.
    • 최상위 뷰 구성요소에게만 화면을 갱신
  • 전략 패턴
  • 옵저버 패턴
    • 상태가 변경되었을 때 그 모델과 연관된 객체들에게 연락.
    • 모델을 뷰와 컨트롤러로부터 분리

 

해당 포스팅은 한빛 미디어 헤드퍼스트 디자인 패턴(에릭 프리먼, 엘리자베스 롭슨 저)를 통해 공부한 내용을 정리한 블로그입니다.

아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
좋은 프로그래머는 자기 두뇌를 사용한다. 그러나 좋은 가이드라인은 모든 케이스를 고려해야만 하는 노력을 줄여준다.
(Francis Glassborow, 개발자)

Proxy Pattern


접근을 제어하고 관리 => 기본적으로 프록시가 하는 역할.

  • 원격 객체의 프록시만 넘기면 다른 머신에서도 상태 확인 가능.

 Proxy

  • 진짜 객체를 대신하는 역할.
  • 네트워크와 떨어져있는 클래스의 정보를 주고 받아야 함.
  • 서비스 역할 필요.
  • 네트워크로 들어오는 요청 수용
  • 프록시 객체의 레퍼런스를 받아오는 기능 필요

원격 프록시의 역할


원격 프록시 diagram

  • 원격 객체의 로컬 대변자 역할. => 어떤 메소드를 호출하면 다른 원격 객체에서 그 메소드 호출을 전달하는 객체.
    • 원격 객체 => JVM의 힙에 존재하는 객체(다른 주소 공간에서 돌아가고있는 객체)
    • 네트워크 통신과 같은 low level 작업은 프록시 객체에서 관리.

모니터링 코드에 원격 프록시 추가하기.

  • 다른 JVM에 들어있는 객체의 메소드를 호출할 수 없음.
  • RMI(Remote Method Invocation)를 이용하여 프록시 만들기
💡 모니터링 코드란
이전 State Pattern에서 예시를 통해 든 GumballMachine에서의 상태를 확인하는 코드 

원격 메소드 기초

원격 메소드 기초

  • 로컬 객체의 메소드를 호출하면 그 요청을 원격 객체에 전달해주는 시스템을 디자인.
  • 통신을 처리해주는 보조 객체가 필요하고 보조 객체를 사용.
    • 클라이언트는 로컬 객체의 메소드만 알면 됨.
    • 클라이언트 보조 객체의 메소드를 호출 => 서비스 보조 객체 호출(1)
    • 진짜 원격 서비스인 척하는 객체 => 클라이언트 보조 객체
    • 클라이언트 보조 객체는 서버에 연락을 취하고(2) 메소드 호출에 관한 정보를 전달하고 서버로부터 리턴되는 정보를 기다림.
    • 서버 보조 객체는 소켓으로 연결된 보조 객체로부터 요청을 받아오고(2) 호출 정보를 해석해서 진짜 메소드 호출(3)
    • 이후 반환되는 과정은 3 -> 2 -> 1 순으로 전달.

RMI 개요


  • 클라이언트와 서비스 보조 객체를 만들어주는 JAVA API
  • 네트워킹 및 입출력 관련 코드를 작성하지 않아도 JVM 메소드를 호출하듯 원격 메소드 호출 가능
  • 클라이언트보조 객체 => 스텁(stub)
    서버 보조 객체 => 스켈레톤(skeleton)

원격 서비스 만들기

원격 인터페이스 만들기

import java.rmi.*;

public interface MyRemote extends Remote {
	public String sayHello() throws RemoteException;
}
  1. 클라이언트가 원격으로 호출할 메소드 정의.
  2. 인터페이스를 서비스의 클래스 형식으로 사용
  3. java.rmi.Remote 확장 및 Remote 클래스 상속
  4. 모든 메소드를 RemoteException을 던지도록 선언.
  5. 원격 메소드의 인자나 반환 값은 반드시 primitive 또는 Serializble 형식으로

서비스 구현 클래스 만들기.

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class GumballMachine extends UnicastRemoteObject implements GumballMachineRemote {

	State soldOutState;
	State noQuarterState;
	State hasQuarterState;
	State soldState;
	State winnerState;

	State state = soldOutState;
	int count = 0;
	String location;

	public GumballMachine(String location, int numberGumballs) throws RemoteException {

		soldOutState = new SoldOutState(this);
		noQuarterState = new NoQuarterState(this);
		hasQuarterState = new HasQuarterState(this);
		soldState = new SoldState(this);
		winnerState = new WinnerState(this);

		this.location = location;
		this.count = numberGumballs;
		if (numberGumballs > 0) {
			state = noQuarterState;
		}
	}
}

import java.rmi.Naming;

public class GumballMachineTestDrive {

	public static void main(String[] args) {
		GumballMachineRemote gumballMachine = null;
		int count;

		if (args.length < 2) {
			System.out.println("GumballMachine <name> <inventory>");
			System.exit(1);
		}

		try {
			count = Integer.parseInt(args[1]);

			gumballMachine =
					new GumballMachine(args[0], count);
			Naming.rebind("//" + args[0] + "/gumballmachine", gumballMachine);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
  1. 실제 작업 처리 클래스
  2. UnicastRemoteObject 확장.
  3. RemoteException을 선언하는 생성자 구현
  4. 서비스를 RMI 레지스트리에 등록

RMI 레지스트리 구성

  1. 클라이언트는 레지스트리로부터 프록시(스텁, 클라이언트 보조 객체)를 받아감.

원격 서비스 실행하기

  1. 서비스 객체 실행

Proxy Pattern


Proxy Pattern

특정 객체로의 접근을 제어하는 대리인 제공 => 프록시

프록시에 접근을 제어하는 방법

  1. 원격 프록시를 써서 원격 객체로의 접근을 제어
  2. 가상 프록시를 써서 생성하기 힘든 자원으로의 접근을 제어
  3. 보호 프록시를 써서 접근 권한이 필요한 자원으로의 접근을 제어
  • RealSubject와 Proxy 인터페이스를 제공하는 Subject 인터페이스 존재
  • 두 객체는 똑같은 인터페이스를 구현하기에 RealSubject가 들어가야할 자리에 Proxy를 대신 넣음
  • 진짜 작업은 RealSubject가 처리. 대변인 역할이자 접근 제어

원격 프록시와 가상 프록시 비교하기

원격 프록시

  • 다른 JVM에 들어있는 객체의 대리인에 해당하는 로컬 객체

가상 프록시

  • 생성하는데 많은 비용이 드는 RealSubject를 대신하는 프록시 객체

보호 프록시 만들기

  • java.lang.reflect 패키지 안에 프록시 기능이 내장
  • 즉석에서 하나 이상의 인터페이스를 구현하고 지정한 클래스에 메소드 호출을 전달하는 프록시 클래스 만들기 가능.
    • 진짜 프록시 클래스는 실행 중에 생성되므로 이러한 자바 기술을 동적 프록시라함.
  • 자바에서 Proxy 클래스를 생성해주므로 Proxy 클래스에게 무슨 일을 해야하는지 알려줄 방법이 필요함.
    • InvocationHandler에 넣어주기
    • 프록시에 호출되는 모든 메소드에 응답.
  • 클라이언트에서 아무 메소드나 마음대로 호출할 수도 있기에 보호 프록시가 필요
    • 접근 권한을 바탕으로 객체로의 접근을 제어하는 프록시
해당 포스팅은 한빛 미디어 헤드퍼스트 디자인 패턴(에릭 프리먼, 엘리자베스 롭슨 저)를 통해 공부한 내용을 정리한 블로그입니다.

아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
좋은 프로그래머는 자기 두뇌를 사용한다. 그러나 좋은 가이드라인은 모든 케이스를 고려해야만 하는 노력을 줄여준다.
(Francis Glassborow, 개발자)

객체지향


  • 새로운 클래스의 인터페이스가 기존 클래스의 인터페이스와 다를 때
  • 새로운 클래스의 인터페이스가 생긴다면 기존 시스템의 인터페이스를 사용할 수 없음.
  • 새로운 클래스와 기존 인터페이스를 연결해 줄 클래스 => Adapter
    • 두코드 모두 변화를 안가져도 됨

사용방법


public interface Duck {
    public void quack();
    public void fly();
}

public class MallardDuck implements Duck{
    @Override
    public void quack() {
        System.out.println("꽥");
    }

    @Override
    public void fly() {
        System.out.println("i can fly");
    }
}


public interface Turkey {
    public void gobble();
    public void fly();
}

public class WildTurkey implements Turkey {
    @Override
    public void gobble() {
        System.out.println("골골");
    }

    @Override
    public void fly() {
        System.out.println("짧은 거리를 날아요");
    }
}
  • 기존의 Duck 인터페이스가 존재했지만 Turkey 인터페이스가 새로 등장
  • 기존 Duck 인터페이스를 사용하는 시스템에서 Turkey 인터페이스를 사용할 수 없음. => Adapter 패턴을 적용

Adapter 적용

public class TurkeyAdapter implements Duck {
	Turkey turkey;
 
	public TurkeyAdapter(Turkey turkey) {
		this.turkey = turkey;
	}
    
	public void quack() {
		turkey.gobble();
	}
  
	public void fly() {
		for(int i=0; i < 5; i++) {
			turkey.fly();
		}
	}
}


public class DuckTestDrive {
	public static void main(String[] args) {
		Duck duck = new MallardDuck();

		Turkey turkey = new WildTurkey();
		Duck turkeyAdapter = new TurkeyAdapter(turkey);

		System.out.println("The SimpleAdapter.Turkey says...");
		turkey.gobble();
		turkey.fly();

		System.out.println("\nThe SimpleAdapter.Duck says...");
		testDuck(duck);

		System.out.println("\nThe SimpleAdapter.TurkeyAdapter says...");
		testDuck(turkeyAdapter);
	}

	static void testDuck(Duck duck) {
		duck.quack();
		duck.fly();
	}
}

Adapter Pattern


  • 클라이언트에게 타겟 인터페이스에게 request() 호출
  • 어댑터는 타겟 인터페이스를 구현해 어뎁티 인스턴스의 메소드를 호출
💡 주의
타겟 인터페이스의 크기에 따라 복잡해지지만 변경사항을 매번 고려해서 바꾸는 것보다는
어댑터 패턴을 구현하는 것이 효과적일 때가 있음.
  • 어댑터 패턴은 하나의 어댑터 적용하는 것.
    • 서비스가 커지면 어댑터 패턴 적용이 어려움.
    • 두개 이상의 어댑터를 적용하는 것은 파사드 패턴

정의

  • 특정 클래스 인터페이스를 클라이언트에서 요구하는 다른 인터페이스로 변환
  • 호환되지 않던 인터페이스도 같이 사용할 수 있음.
  • 특정 클라이언트가 구상 클래스에 의존하는 것이 아닌 인터페이스에 의존

Adapter Pattern

객체 어댑터와 클래스 어댑터


여태까지 구현했던 것이 객체 어댑터(상위 그림)
클래스 어댑터를 구현하기 위해선 다중 상속이 필요

객체 어댑터 VS 클래스 어댑터

  • 객체 어댑터
    • 구성 사용
    • 어댑티 클래스와 그 서브 클래스에 대해서 어댑터 역할 가능
  • 클래스 어댑터
    • 특정 어댑티 클래스에만 적용
    • 어댑티 전체를 다시 구현하지 않아도 된다는 장점
    • 서브 클래스라서 어댑티의 행동을 오버라이드 할 수 있음.
    • 어댑터 하나만 있으면 가능.

실전 적용


  • Enumeration 인터페이스
    • Enumeration을 리턴하는 elements() 메소드가 구현되어 있음.
    • 각 컬렉션의 모든 항목에 접근 가능
  • Iterator인터페이스
    • 컬렉션의 항목에 접근하고 그 항목을 제거할 수 있는 메소드 사용

Enumeration을 Iterator에 적응하기.

  • Iterator ⇒ Target Interface
    • hasNext()
    • next()
  • Enumeration ⇒ Adaptee Interface
    • hasMoreElements()
    • nextElement()
  • 각각의 요소에 대응. (hasNext()hasMoreElements() || next()nextElement())
  • 기존 코드에 있던 Enumeration도 새로운 코드에서는 Iterator처럼 보임.
  • 타겟 인터페이스와 어댑티 인터페이스의 메소드가 완벽히 1대1 매칭이 되지 않는 상황에서는 완벽하게 적용 불가능.

Facade Pattern


Facade -> 겉모양이나 외관

  • 인터페이스를 단순하게 바꾸기 위해서 인터페이스를 변경.
  • 하나 이상의 클래스 인터페이스를 깔끔하면서도 효과적인 퍼사드로 덮기.

홈시어터 만들기

  • 스크린, 팝콘기계, 조명, 음향 등… 너무 많은 클래스의 관리를 요함.
  • 다른 시스템에 적용할 때에도 여러개의 클래스 관리.
  • 시스템이 업그레이드 되면 코드 추가.

퍼사드 작동 원리 알아보기

  • 퍼사드 클래스는 서브 시스템 클래스를 캡슐화 하지 않음.
    • 그저 인터페이스 제공.
  • 클라이언트 구현과 서브 시스템을 분리할 수 있음.

정의

  • 서브시스템에 있는 일련의 인터페이스를 통합 인터페이스로 묶어준다.

Facade Pattern

example

public class Test {
    public static void main(String[] args) {
	    // 각각의 객체는 생략
        Amplifier amp = new Amplifier("amp");
        Tuner tuner = new Tuner("tuner", amp);
        StreamingPlayer player = new StreamingPlayer("cd", amp);
        Projector projector = new Projector("proj", player);
        Screen screen = new Screen("screen");
        TheaterLights lights = new TheaterLights("light");
        PopcornPopper popper = new PopcornPopper("popcorn");

        HomeTheaterFacade homeTheaterFacade = new HomeTheaterFacade(amp, tuner, player, projector, lights, screen, popper);

        homeTheaterFacade.watchMovie("어바웃타임");
        homeTheaterFacade.endMovie();
    }
}

public class HomeTheaterFacade {
    Amplifier amp;
    Tuner tuner;
    StreamingPlayer player;
    Projector projector;
    TheaterLights lights;
    Screen screen;
    PopcornPopper popper;

    public HomeTheaterFacade(Amplifier amp, Tuner tuner, StreamingPlayer player, Projector projector, TheaterLights lights, Screen screen, PopcornPopper popper) {
        this.amp = amp;
        this.tuner = tuner;
        this.player = player;
        this.projector = projector;
        this.lights = lights;
        this.screen = screen;
        this.popper = popper;
    }

    public void watchMovie(String movie) {
        System.out.println("start to watch movie");

        popper.on();
        popper.pop();

        lights.dim(10);

        screen.down();

        projector.on();
        projector.wideScreenMode();

        amp.on();
        amp.setStereoSound();
        amp.setStreamingPlayer(player);
        amp.setVolume(5);

        player.on();
        player.play(movie);
    }

    public void endMovie() {
        System.out.println("turn off movie");

        popper.off();

        lights.on();

        screen.up();

        projector.off();

        amp.off();

        player.stop();
        player.off();
    }
}
해당 포스팅은 한빛 미디어 헤드퍼스트 디자인 패턴(에릭 프리먼, 엘리자베스 롭슨 저)를 통해 공부한 내용을 정리한 블로그입니다.

아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
좋은 프로그래머는 자기 두뇌를 사용한다. 그러나 좋은 가이드라인은 모든 케이스를 고려해야만 하는 노력을 줄여준다.
(Francis Glassborow, 개발자)

문제 상황


  • 상당히 많은 객체의 인터페이스
    • 공통적인 인터페이스의 부재
    • 앞으로도 여러 인터페이스가 추가될 수 있음.

Command Pattern


캡슐화를 더 높은 수준으로
메소드 호출을 캡슐화

  • 각각의 메소드를 인터페이스 별로 분기처리하여서 작업하는 것은 유지보수성이 떨어짐
  • 작업을 요청하는 쪽(객체)과 그 작업을 처리하는 쪽(커맨드 객체)을 분리
  • 커맨드 객체를 추가하여 작업
    • 특정 객체에 관한 특정 작업 요청을 캡슐화
  • (객체와 커맨드 객체를 분리 시)패턴이 어떻게 돌아가는지 파악하기 어려움

주문하는 과정

  • 주문서는 주문 내용을 캡슐화
    • 주문서는 주문 내용을 요구하는 객체
    • 식사 준비에 필요한 orderUp() 메소드와 주방장 레퍼런스만 들어있음.
    • 주문을 받으면 주방장 레퍼런스(작업을 처리하는 객체)에게 orderUp() 을 호출해주기만 하면 됨.
  • 종업원은 주문서를 받고 orderUp()  호출
    • orderUp() 메소드를 호출하는 객체
    • 주문서의 내용, 주방장이 누구인지 => 아무도 관심이 없음.
    • takeOrder() 메소드에는 여러 고객의 주문서를 매개변수로 전달.
  • 주방장은 식사를 준비하는데 필요한 정보
    • 식사 준비 방법은 주방장만 알고 있음.

용어정리

앞으로 나오는 객체의 대한 용어를 정의.

  • Client
    • 커맨드 객체 생성
  • Command
    • 리시버에 전달할 일련의 행동(execute()함수)을 구성.
    • execute() 함수를 사용하여 행동을 캡슐화하고, 특정 행동을 처리
  • Invoker
    • 커맨드 객체를 쓰이기 전까지 보관. 
    • 클라이언트를 통해서 커맨드 객체를 넘겨받음.
  • Receiver
    • 행동 메소드 실행 

커맨드 객체 만들기

  • 커맨드 인터페이스 구현
    • 모두 같은 인터페이스 구현
    • execute() 메소드 정의
  • 조명을 켤 때 필요한 커맨드 클래스 구현
    • LightOnCommand 클래스 구현
      • 특정 커맨드 객체로 제어한 클래스의 인터페이스 전달.(light 인터페이스)
    • execute() 메소드 오버라이드

example

public class RemoteControlTest {
    public static void main(String[] args) {
        // Light 커맨드 객체
        SimpleRemoteControl remote = new SimpleRemoteControl(); // invoker
        Light light = new Light("1"); // receiver
        LightOnCommand lightOn = new LightOnCommand(light); // command

        remote.setCommand(lightOn); // invoker <- command
        remote.buttonWasPressed();

        // Garage 커맨드 객체
        GarageDoor garageDoor = new GarageDoor("2");
        GarageDoorOpenCommand garageDoorOpenCommand = new GarageDoorOpenCommand(garageDoor);

        remote.setCommand(garageDoorOpenCommand);
        remote.buttonWasPressed();
    }
}

public class SimpleRemoteControl {
    Command slot;
    public SimpleRemoteControl() {}

    public void setCommand(Command command) {
        slot = command;
    }

    public void buttonWasPressed() {
        slot.execute();
    }
}


// Receiver
public class Light {
    String location = "";

    public Light(String location) {
        this.location = location;
    }
    public void on() {
        System.out.println("조명이 켜졌습니다");
    }

    public void off() {

    }
}

// 행동을 캡슐화하기 위한 공통 interface
public interface Command {
    public void execute();
    public void undo();
}

// Command 객체(Light)를 실행할 Command class
public class LightOnCommand implements Command {
	Light light;

	public LightOnCommand(Light light) {
		this.light = light;
	}

	public void execute() {
		light.on();
	}

	public void undo() {
		light.off();
	}
}

커맨드 패턴 정의


💡 커맨드 패턴을 사용하면 요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화 가능.
  • 커맨드 객체는 일련의 행동을 특정 리시버와 연결. 요청을 캡슐화
  • 메타 커맨드 패턴 => 여러개의 명령을 매크로로 한번에 실행 가능

Command Pattern

Flow

  • ClientConcreteCommand(LightOnCommand, 커맨드 객체)를 생성하고 Receiver(Light, 리시버 객체)를 설정
  • Invoker(RemoteController, 인보커 객체)는 명령이 들어 있으며 execute() 호출로 커맨드 객체에게 특정 작업 수행 가능
  • Command는 모든 커맨드 객체가 구현해야하는 인터페이스
    • execute()로 메소드 호출
    • 리시버에게 특정 작업을 처리하라는 지시 전달.
  • Receiver는 요구사항을 수행할때 어떤 일을 처리해야 할 지.
  • ConcreteCommand는 특정 행동과 리시버를 연결
    • Invoker에서 execute() 호출로 요청하면 ConcreteCommand 객체에서 리시버의 메소드를 호출. 작업 처리.
💡 NoCommand 객체
NoCommand 객체는 일종의 null 객체.
널 객체는 딱히 리턴할 객체도 없고 클라이언트가 null을 처리하지 않게 하고 싶을 때 활용하면 좋음.
public class NoCommand implements Command {
	public void execute() { }
	public void undo() {}
}

'기타 > 디자인패턴' 카테고리의 다른 글

[Design Pattern] Template Method Pattern  (0) 2024.06.05
[Design Pattern] Adapter Pattern  (0) 2024.06.04
[Design Pattern] Singleton Pattern  (0) 2024.05.31
[Design Pattern] Factory Pattern  (0) 2024.05.29
[Design Pattern] Decorator Pattern  (0) 2024.05.28
해당 포스팅은 한빛 미디어 헤드퍼스트 디자인 패턴(에릭 프리먼, 엘리자베스 롭슨 저)를 통해 공부한 내용을 정리한 블로그입니다.

아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
좋은 프로그래머는 자기 두뇌를 사용한다. 그러나 좋은 가이드라인은 모든 케이스를 고려해야만 하는 노력을 줄여준다.
(Francis Glassborow, 개발자)

Singleton Pattern 사용이유


먼저 singleton pattern 사용 이유를 먼저 설명하겠습니다.

  • 하나만 있어도 충분히 돌아가는 또는 하나만 있어야 하는 객체에 사용
    • Thread pool, Cache, Logger
  • 이러한 객체가 두개이상 있으면
    • 프로그램이 이상하게 돌아갈 가능성.
    • 자원을 불필요하게 사용.
    • 결과에 일관성이 없어질 수 있음.
  • 정적 클래스와 메소드의 접근 변경자(public, private...)를 잘 다룰 줄 알아야함

전역변수

  • 싱글톤 패턴과 비슷.
  • 단점
    • 객체를 한번도 쓰지 않는다면 리소스 낭비
  • 싱글톤 패턴은 필요할 때만 객체를 생성 가능.

고전적인 싱글톤 패턴


public class Singleton {
    private static Singleton singleton; // Singleton 클래스의 하나뿐인 인스턴스를 저장하는 정적변수

    private Singleton() {} // 생성자를 private로 선언했으므 Singleton 클래스에서만 생성자 생성 가능
    
    public static Singleton getInstance() { // instance 호출
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}
  • singleton 은 클래스의 하나뿐인 정적변수
  • 인스턴스가 필요한 상황 전까지 인스턴스를 만들지 않고 필요할 때 인스턴스를 생성. ⇒ Lazy instantiation
  • 유일한 객체
  • public으로 지정된 생성자가 없음.
  • getInstance() 정적 메서드 존재. 정적 인스턴스 호출

싱글톤 패턴


💡 싱글턴 패턴
클래스 인스턴스를 하나만 만들고 그 인스턴스로의 전역 접근을 제공.

문제

MultiThread 동시성 문제

  • 멀티 스레딩에서의 문제가 생김.
    • 두 스레드에서 다른 객체가 생김.

해결책

synchronized 키워드 사용

  • synchronized 키워드만 추가하면 한 스레드가 메소드 사용을 끝나기 전까지 다른 스레드는 기다려야함.
  • 속도 문제가 생길 수 있음
public class Singleton {
	private static Singleton uniqueInstance;
 
	// other useful instance variables here
 
	private Singleton() {}
 
	public static synchronized Singleton getInstance() {
		if (uniqueInstance == null) {
			uniqueInstance = new Singleton();
		}
		return uniqueInstance;
	}
 
	// other useful methods here
	public String getDescription() {
		return "I'm a thread safe Singleton!";
	}
}

선언시 인스턴스 생성

public class Singleton {
    private static Singleton uniqueInstance = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return uniqueInstance;
    } 
}
  • JVM에서 Singleton의 인스턴스를 하나 생성

voliate 키워드 사용

  • DCL(Double-Checked Locking) 사용
  • 인스턴스가 있는지 확인 후 동기화 블록에 들어감
  • synchronized 내부의 블록에 한번 더 체크 하므로 동기화
public class Singleton {
	private volatile static Singleton uniqueInstance;
 
	private Singleton() {}
 
	public static Singleton getInstance() {
		if (uniqueInstance == null) {
			synchronized (Singleton.class) {
				if (uniqueInstance == null) {
					uniqueInstance = new Singleton();
				}
			}
		}
		return uniqueInstance;
	}
}

고찰


  • 클래스 로더마다 서로 다른 네임스페이스 정의하기 때문에 클래스 로더가 두개 이상이면 같은 클래스 여러번 로딩하기에 객체가 두개 이상 생길 수 있음.
  • 리플랙션, 직렬화, 역직렬화에서 문제가 생길 수 있음. ⇒ 체크하면서 개발해야함.
  • 싱글턴은 느슨한 결합 원칙에 위배됨.
  • 클래스는 기본적으로 하나의 책임만 져야함. 하지만 싱글턴은
    • 하나의 클래스가 자신의 인스턴스를 관리하는 일
    • 인스턴스를 사용하는 목적.
    • 이 두가지 책임을 가짐.

 

 

 

 

요약

 

'기타 > 디자인패턴' 카테고리의 다른 글

[Design Pattern] Adapter Pattern  (0) 2024.06.04
[Design Pattern] Command Pattern  (2) 2024.06.03
[Design Pattern] Factory Pattern  (0) 2024.05.29
[Design Pattern] Decorator Pattern  (0) 2024.05.28
[Design Pattern] Observer Pattern  (0) 2024.05.27
해당 포스팅은 한빛 미디어 헤드퍼스트 디자인 패턴(에릭 프리먼, 엘리자베스 롭슨 저)를 통해 공부한 내용을 정리한 블로그입니다.

아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
좋은 프로그래머는 자기 두뇌를 사용한다. 그러나 좋은 가이드라인은 모든 케이스를 고려해야만 하는 노력을 줄여준다.
(Francis Glassborow, 개발자)

Observer 패턴 이해하기


신문사 + 구독자 = Observer 패턴(publish-subscribe 패턴과 다름)

  • 구성요소
    • 신문사 => 주제(Subject)
    • 구독자 => 옵저버(Observer)
  • 주제에서는 중요한 데이터를 관리
  • 주제의 데이터가 바뀌면 옵저버에게 이벤트(데이터가 바뀌었다는 소식)를 전달.
  • 주제를 구독하고 있는 옵저버에게 소식이 전달되었기에 전달받은 내용을 옵저버는 갱신.(최신 데이터 유지)

작동원리

  • 주제는 옵저버들에게 데이터를 전달.
  • 어떤 객체가 주제의 데이터를 전달받고 싶어하면 주제를 구독하면 됨.(옵저버가 되는 형태)
  • 주제의 데이터를 그만 업데이트하고 싶으면, 주제의 구독을 해지.

Observer 패턴 

객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동적으로 내용이 갱신되는 방식.
일대다(one-to-many) 의존성을 정의(일: Subject, 다: Observer)

observer pattern class diagram

느슨한 결합


  • 한 객체가 다른 객체에 너무 의존적이면 단단한 결합이라 함.

느슨한 결합

  • 객체가 부서질 가능성이 낮음
  • 객체의 세세한 부분까지 알 필요가 없음
  • 상호작용할 수 있지만 잘 모르는 관계
  • 다른 객체와 상관없이 설계하면 변화에 더 잘 대응할 수 있는 디자인을 만들 수 있음 => 유연한 디자인

옵저버 패턴에 적용한 내용

  • 주제는 옵저버가 특정 인터페이스(Observer interface)를 구현한다는 사실만 알고 있음.
  • 옵저버는 언제든 추가/제거 가능
  • 새로운 형식의 옵저버를 추가해도 주제 변경이 필요가 없음.
  • 주제와 옵저버는 서로 독립적으로 재사용 가능.
  • 주제나 옵저버가 달라져도 서로에게 영향을 미치지 않음.
💡 상호작용하는 객체 사이에는 느슨한 결합을 사용해야함.

풀 방식으로 코드 바꾸기


  • 이전까지의 방식
    • 주제가 옵저버에게 데이터를 보내는 푸시(PUSH)
  • 현재 사용할 방식
    • 옵저버가 주제로부터 데이터를 당겨오는 풀(PULL)

코드


/* 
 * observer interface
 */
public interface Observer {
    public void update(float temp, float humidity, float pressure);
    public void update();
}

/* 
 * display interface
 */
public interface DisplayElement {
    public void display();
}

public class CurrentConditionsDisplay implements Observer, DisplayElement {

    private float temperature;
    private float humidity;
    private WeatherData weatherData;

    public CurrentConditionsDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    // PUSH 방식
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;

        display();
    }

    // PULL 방식
    public void update() {
        this.temperature = weatherData.getTemperature();
        this.humidity = weatherData.getHumidity();

        display();
    }

    public void display() {
        System.out.println("CurrentConditions Display");
        System.out.println("temperature = " + temperature + " humidity = " + humidity);

    }
}

/* 
 * subject interface
 */
public interface Subject {
    public void registerObserver(Observer o);
    public void removeObserver(Observer observer);
    public void notifyObservers();
}

public class WeatherData implements Subject {

    // 인스턴스 변수 선언
    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() {
        observers = new ArrayList<Observer>();
    }

    public void registerObserver(Observer o) {
        observers.add(o);
    }

    public void removeObserver(Observer o) {
        observers.remove(o);
    }

    public void notifyObservers() {
        // Push방식
        // Observer들에게 갱신 값 보내주기.
        for (Observer o : observers) {
            o.update(temperature, humidity, pressure);
        }

        // Pull
        // Observer에서 값 가져오기
        for (Observer o : observers) {
            o.update();
        }

    }

    public void measurementsChanged() {

        notifyObservers();
        /*
         * 현재 구조에서는 display들이 update 메소드를 요청하는데 하나로 묶을 수 있을 것 같음.
         * 최신 측정 값을 가져오는 시점을 판단하기 어려움
         * Observer 패턴 도입.
         */
    }

    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }

    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;

        measurementsChanged(); // 최신 측정값 Observer들에게 갱신 값 보내주기.
    }
}

'기타 > 디자인패턴' 카테고리의 다른 글

[Design Pattern] Command Pattern  (2) 2024.06.03
[Design Pattern] Singleton Pattern  (0) 2024.05.31
[Design Pattern] Factory Pattern  (0) 2024.05.29
[Design Pattern] Decorator Pattern  (0) 2024.05.28
[Design Pattern] Strategy Pattern  (0) 2024.05.27
해당 포스팅은 한빛 미디어 헤드퍼스트 디자인 패턴(에릭 프리먼, 엘리자베스 롭슨 저)를 통해 공부한 내용을 정리한 블로그입니다.

아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
좋은 프로그래머는 자기 두뇌를 사용한다. 그러나 좋은 가이드라인은 모든 케이스를 고려해야만 하는 노력을 줄여준다.
(Francis Glassborow, 개발자)

SimUduck


  • 모든 오리가 할 수 있는 동작 Duck 클래스에서 정의
    • quack()
    • swim()
    • display()
      • 모든 오리의 동작이 다르므로 추상 메소드에서 정의
  • MallardDuck 클래스와 RedHeadDuck 클래스가 Duck 클래스를 상속받아서 display override
  • 이후에 모든 오리에게 날 수 있는 동작을 정의하고 싶음
    • Duck 클래스에서 fly() 메소드 추가로 정의로 해결 가능

문제발생

  • fly() 메소드는 일부 Duck 클래스를 상속한 서브 클래스에만 되어야했는데 모든 서브 클래스에 적용이 다 되어버림

해결책


1. 인터페이스 설계

  • 모든 오리가 날지 못함 + 모든 오리가 꽥꽥거리면서 울지 않음
    • Flyable, Quackable 이라는 인터페이스 설계

2. 캡슐화

💡 애플리케이션에서 달라지는 부분을 찾아내고 달라지지 않는 부분과 분리.

 

  • 달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 캡슐화
    • 코드를 변경하는 과정에서 의도치 않게 발생하는 일을 줄이면서 시스템의 유연성을 향상.
  • 구현보다는 인터페이스에 맞춰 프로그래밍한다.
    • 각 행동은 인터페이스로 표현(FlyBehavior, QuackBehavior…)
    • 메소드는 Duck 클래스에서 구현X. 인터페이스에서 먼저 정의
💡 구현보다는 인터페이스에 맞춰 프로그래밍한다.

오리행동 통합하기


나는 행동과 꽥꽥거리는 행동을 Duck 클래스에서 정의한 메소드를 써서 구현하지 않고 다른 클래스로 위임하는 행동.

  • Duck 클래스에 FlyBehaviorQuackBehavior을 인스턴스 변수로 저장.
    • Duck 클래스는 인터페이스에서 구현한 fly()quack() 메소드를 실행하기 위한 메소드 정의(performFly(), performQuack())
public abstract class Duck {
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;

    // 모든 오리의 동작을 여기서 정의

    void performQuack() {
        quackBehavior.quack();
    }

    void setQuackBehavior(QuackBehavior quackBehavior) {
        this.quackBehavior = quackBehavior;
    }

    void performFly() {
        flyBehavior.fly();
    }

    void setFlyBehavior(FlyBehavior flyBehavior) {
        this.flyBehavior = flyBehavior;
    }

    void swim() {
        System.out.println("헤엄!");

    }

    // 오리에 따른 description
    abstract void display();
}

/*
 * 오리 날기 정의
 * 인터페이스로 정의 후 각각의 날개의 방법대로 정의.
 */
public interface FlyBehavior {
    void fly();
}

public class FlyNoWay implements FlyBehavior {
    @Override
    public void fly() {
        System.out.println("i can't fly!");
    }
}

public class FlyWithWings implements FlyBehavior{
    @Override
    public void fly() {
        System.out.println("i can fly!");
    }
}

/*
 * 오리 울음소리 정의
 * 인터페이스로 정의 후 각각의 소리마다 class로 정의
 */
public interface QuackBehavior {
    void quack();
}

public class Quack implements QuackBehavior{
    @Override
    public void quack() {
        System.out.println("꽥꽥!");
    }
}

public class Squeak implements QuackBehavior{
    @Override
    public void quack() {
        System.out.println("삑삑!");
    }
}

 

Example

public class RedheadDuck extends Duck {

    public RedheadDuck() {
        flyBehavior = new FlyWithWings();
        quackBehavior = new Squeak();
    }

    @Override
    void display() {
        System.out.println("RedheadDuck!");
    }
}
  • RedheadDuck 클래스는 Duck 클래스를 상속.
  • 상속자에서 날기 동작과 울음소리를 정의.
    • 날기 동작과 울음소리는 인터페이스로 정의되어 있는 것에 클래스로 할당.
  • display()는 Duck 클래스에서 추상메소드로 정의하였기 때문에 클래스에서 정의.
💡 특정 구현에 맞춰서 프로그래밍하면 안됨.
하지만 new FlyWithWings() 처럼, 특정 객체에 구현되어 있는 구상 인스턴스를 만들었음. ⇒ 잘못되었지만 1장이라 이처럼 표현

동적으로 행동 지정하기

  • Duck 클래스에 Setter method 정의(setQuackBehavior(), setFlyBehavior())

두 클래스를 합치는 방법

💡 상속(as-is)보다는 구성(has-is)을 활용
  • A에는 B가 있다. ⇒ Duck 클래스에 FlyBehavior와 QuackBehavior가 있어서 Duck 클래스에 나는 행동과 꽥꽥거리는 행동을 위임. ⇒ 구성

전략패턴


알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해주는 패턴

클라이언트로부터 알고리즘을 분리해서 독립적으로 분리.

요약 :

변화가 생길 기능(행동:behavior)을 예상하여 이를 캡슐화(및 추상화) 한다.

캡슐화 후 각 기능별로 구현해두고(ex: fly 중 noWay, WithWing, Rocket 등)

각 객체별로 사용할 기능을 (쉽게) 교환 받는다.

각 기능들이 추상화되어 있어 재사용성이 좋고, 수정이 용이하다.

 

기억 추적 :

서브클래스는 단순히 super 클래스를 상속받는식의 구현이 아닌,

interface, abstract 등을 이용하여 기능의 동작만 받음.

각 기능은 인터페이스를 구현하여 미리 정의해두고 모델은 이를 조합하여 기능을 만들어가는 방식.

'기타 > 디자인패턴' 카테고리의 다른 글

[Design Pattern] Command Pattern  (2) 2024.06.03
[Design Pattern] Singleton Pattern  (0) 2024.05.31
[Design Pattern] Factory Pattern  (0) 2024.05.29
[Design Pattern] Decorator Pattern  (0) 2024.05.28
[Design Pattern] Observer Pattern  (0) 2024.05.27

+ Recent posts