해당 포스팅은 한빛 미디어 헤드퍼스트 디자인 패턴(에릭 프리먼, 엘리자베스 롭슨 저)를 통해 공부한 내용을 정리한 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
좋은 프로그래머는 자기 두뇌를 사용한다. 그러나 좋은 가이드라인은 모든 케이스를 고려해야만 하는 노력을 줄여준다. (Francis Glassborow, 개발자)
전략 패턴과 상태 패턴은 쌍둥이
상태 다이어그램과 비슷
다른 상태로 전환을 위해서는 어떤 동작이 필요
예외 케이스에 대해서도 항상 잘 생각해야함.
상태 값을 저장하는 인스턴스 변수를 만들고 메소드 내에서 조건문을 써서 다양한 상태를 처리
State Diagram
상태
동전 없음
동전 있음
알맹이 없음
알맹이 판매
상태를 저장하는 인스턴스 변수 정의
/*
* 알맹이 상태 저장
*/
final static int SOLD_OUT = 0; // 알맹이 없음
final static int NO_QUARTER = 1; // 동전 없음
final static int HAS_QUARTER = 2; // 동전 있음
final static int SOLD = 3; // 알맹이 내보내는 중
시스템에서 일어날 수 있는 모든 행동 정의
동전투입
동전 반환
손잡이 돌림
알맹이 내보냄
클래스 만들기
public class GumballMachine {
/*
* 알맹이 상태 저장
*/
final static int SOLD_OUT = 0; // 알맹이 없음
final static int NO_QUARTER = 1; // 동전 없음
final static int HAS_QUARTER = 2; // 동전 있음
final static int SOLD = 3; // 알맹이 내보내는 중
int state = SOLD_OUT;
int count = 0;
public GumballMachine(int count) {
this.count = count;
if (count > 0) {
state = NO_QUARTER;
}
}
/*
* 동전이 투입된 경우
*/
public void insertQuarter() {
if (state == HAS_QUARTER) {
System.out.println("동전은 하나만 넣어주세요"); // 동전이 이미 투입되어 있다면 이미 있다고
} else if (state == NO_QUARTER) {
state = HAS_QUARTER;
System.out.println("동전을 넣으셨습니당");
} else if (state == SOLD_OUT) {
System.out.println("매진되었습니다. 다음 기회에 이용해주세요");
} else { // state == SOLD
System.out.println("알맹이를 내보내는 중");
}
}
// 이후 부터는 행동 정의 생략...
/*
* 동전 반환
*/
public void ejectQuarter() {
}
/*
* 손잡이를 돌리는 경우
*/
public void turnCrank() {
}
/*
* 알맹이 내보내기
*/
public void dispense() {
}
public void refill(int numGumBalls) {
}
}
뽑기 기계 수정 요청
새로운 기능 추가 요구
현재 코드 ⇒ 관리 및 수정의 어려움
상태 별 행동을 별도의 클래스에 넣어두고 모든 상태에 대해 각각 자기가 할 일을 구현
⇒ 바뀌는 부분을 캡슐화
뽑기 기계가 현재 상태를 나타내는 상태 객체에게 작업을 넘기도록 ⇒ 구성
새로운 디자인 구성하기
기존 코드를 그대로 활용하는 대신 상태 객체들을 별도의 코드에 넣고 어떤 행동이 일어나면 현재 상태 객체에서 필요한 작업을 처리.
뽑기 기계와 관련된 모든 행동에 관한 메소드가 들어있는 State 인터페이스 정의
기계의 모든 상태를 대상으로 상태 클래스 구현.
상태에 대해서는 상태 클래스가 모든 책임을 갖도록
조건문 코드 없애고 상태 클래스에 작업 위임.
상태 패턴 적용
구조 살펴보기
해당 결과 도출
각 상태의 행동을 별개의 클래스로 국지화
관리하기 힘든 분기문 삭제
상태를 변경에는 닫혀있고 새로운 상태를 추가하는 확장에는 열려있도록. (OCP적용)
이해하기 좋은 코드 베이스와 클래스 구조 적용
public class GumballMachine {
/*
* 알맹이 상태 저장
*/
State soldOutState; // 알맹이 없음
State noQuarterState; // 동전 없음
State hasQuarterState; // 동전 있음
State soldState; // 알맹이 내보내는 중
State state;
int count = 0;
public GumballMachine(int count) {
soldOutState = new SoldOutState(this);
noQuarterState = new NoQuarterState(this);
hasQuarterState = new HasQuarterState(this);
soldState = new SoldState(this);
this.count = count;
if (count > 0) {
state = noQuarterState;
} else {
state = soldOutState;
}
}
/*
* 동전이 투입된 경우
*/
public void insertQuarter() {
state.insertQuarter();
}
/*
* 동전 반환
*/
public void ejectQuarter() {
state.ejectQuarter();
}
/*
* 손잡이를 돌리는 경우
*/
public void turnCrank() {
state.turnCrank();
state.dispense();
}
// State 생성
void setState(State state) {
this.state = state;
}
}
// State 예시
public interface State {
// 동전을 넣었을 때의 상태 정의
public void insertQuarter();
// 동전을 반환했을 때의 상태 정의
public void ejectQuarter();
// 손잡이를 돌렸을 때의 상태 정의
public void turnCrank();
// 알맹이가 나올 때의 상태 정의
public void dispense();
public void refill();
}
public class HasQuarterState implements State {
GumballMachine gumballMachine;
public HasQuarterState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() {
System.out.println("동전을 한개만 넣어주세요");
}
@Override
public void ejectQuarter() {
System.out.println("동전을 반환합니다");
gumballMachine.setState(gumballMachine.getNoQuarterState());
}
@Override
public void turnCrank() {
System.out.println("손잡이를 돌리셨습니다.");
gumballMachine.setState(gumballMachine.getSoldState());
}
@Override
public void dispense() {
System.out.println("동전을 넣어주세요");
}
public void refill() {}
}
State Pattern
💡 상태 패턴 객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있음. 클래스가 바뀌는 것과 같은 결과
해당 포스팅은 한빛 미디어 헤드퍼스트 디자인 패턴(에릭 프리먼, 엘리자베스 롭슨 저)를 통해 공부한 내용을 정리한 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
좋은 프로그래머는 자기 두뇌를 사용한다. 그러나 좋은 가이드라인은 모든 케이스를 고려해야만 하는 노력을 줄여준다. (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() 호출
어댑터는 타겟 인터페이스를 구현해 어뎁티 인스턴스의 메소드를 호출
💡 주의 타겟 인터페이스의 크기에 따라 복잡해지지만 변경사항을 매번 고려해서 바꾸는 것보다는 어댑터 패턴을 구현하는 것이 효과적일 때가 있음.
어댑터 패턴은 하나의 어댑터 적용하는 것.
서비스가 커지면 어댑터 패턴 적용이 어려움.
두개 이상의 어댑터를 적용하는 것은 파사드 패턴
정의
특정 클래스 인터페이스를 클라이언트에서 요구하는 다른 인터페이스로 변환
호환되지 않던 인터페이스도 같이 사용할 수 있음.
특정 클라이언트가 구상 클래스에 의존하는 것이 아닌 인터페이스에 의존
객체 어댑터와 클래스 어댑터
여태까지 구현했던 것이 객체 어댑터(상위 그림) 클래스 어댑터를 구현하기 위해선 다중 상속이 필요
객체 어댑터
구성 사용
어댑티 클래스와 그 서브 클래스에 대해서 어댑터 역할 가능
클래스 어댑터
특정 어댑티 클래스에만 적용
어댑티 전체를 다시 구현하지 않아도 된다는 장점
서브 클래스라서 어댑티의 행동을 오버라이드 할 수 있음.
어댑터 하나만 있으면 가능.
실전 적용
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 -> 겉모양이나 외관
인터페이스를 단순하게 바꾸기 위해서 인터페이스를 변경.
하나 이상의 클래스 인터페이스를 깔끔하면서도 효과적인 퍼사드로 덮기.
홈시어터 만들기
스크린, 팝콘기계, 조명, 음향 등… 너무 많은 클래스의 관리를 요함.
다른 시스템에 적용할 때에도 여러개의 클래스 관리.
시스템이 업그레이드 되면 코드 추가.
퍼사드 작동 원리 알아보기
퍼사드 클래스는 서브 시스템 클래스를 캡슐화 하지 않음.
그저 인터페이스 제공.
클라이언트 구현과 서브 시스템을 분리할 수 있음.
정의
서브시스템에 있는 일련의 인터페이스를 통합 인터페이스로 묶어준다.
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, 개발자)
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() 정적 메서드 존재. 정적 인스턴스 호출
싱글톤 패턴
💡 싱글턴 패턴 클래스 인스턴스를 하나만 만들고 그 인스턴스로의 전역 접근을 제공.
문제
멀티 스레딩에서의 문제가 생김.
두 스레드에서 다른 객체가 생김.
해결책
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;
}
}
고찰
클래스 로더마다 서로 다른 네임스페이스 정의하기 때문에 클래스 로더가 두개 이상이면 같은 클래스 여러번 로딩하기에 객체가 두개 이상 생길 수 있음.