-
자바 데코레이터 패턴, 쓰레드, 멀티 쓰레드 프로그래밍언어/Java 2023. 7. 16. 16:23728x90반응형SMALL
데코레이터 패턴
- 객체에 동적으로 새로운 기능을 제공해 줄 수 있어 상속보다 유연한 구현이 가능합니다.
- 전체 클래스에 새로운 기능을 추가할 필요는 없지만, 각 객체에 새로운 기능을 추가해줘야 할 경우 사용합니다.
- 자신이 추가하려는 인터페이스를 포함하고 있으므로, 클라이언트에게 감춰진 형태로 사용 가능합니다.
- 데코레이터는 다른 데코레이터나 컴포넌트를 포함해야 합니다.
- 자바의 입출력 스트림은 decorator pattern입니다.
컴포넌트 & 데코레이터 - Component : 동적으로 추가할 가능성이 있는 객체에 대한 인터페이스입니다.
- Concrete Component : 추가적인 기능이 실제로 구현되어야 하는 클래스입니다.
- Decorator : 기능을 추가할 Component를 구성요소로 가지면서 Component 인터페이스를 구현한 클래스입니다.
- Concrete Decorator : 컴포넌트에 새롭게 추가할 기능을 실제 구현한 클래스입니다.
예시) 커피 머신
메뉴: - 아메리카노 - 카페 라떼 : 아메리카노 + 우유 - 모카 커피 : 아메리카노 + 우유 + 모카시럽 원두: - 케냐 - 에티오피아
위와 같은 경우 커피의 여러 종류를 구현해야 하므로 컴포넌트는 커피가 됩니다.
커피 원두와 같은 경우 케냐와 에티오피아로 나누어지게 됩니다. 그러므로 커피의 멤버 변수로 beans를 두고 brewing()이라는 메서드를 제공해줌으로써 KenyaAmericano, EtiopiaAmericano 두 클래스가 이를 구현하여 사용할 수 있도록 해줍니다.
위의 메뉴를 살펴보면, 아메리카노를 제외하고 각 메뉴가 다른 메뉴에서 어떤 재료가 하나씩 추가된 경우임을 알 수 있습니다. 그러므로 각 메뉴는 커피라는 멤버 변수를 가지고, 이 커피 메뉴에 추가적인 메뉴를 넣을 수 있는 메서드를 오버라이딩해 구현함으로써 동적으로 기능을 추가해주도록 해보겠습니다.
- 커피 클래스 (Component)
abstract class Coffee { protected String beans; public Coffee(String beans){ this.beans = beans; } public abstract void brewing(); }
- 에티오피아 커피 (Concrete Component)
class EtiopiaCoffee extends Coffee{ public EtiopiaCoffee(){ super("에티오피아"); } @Override public void brewing(){ System.out.println("에티오피아 커피입니다."); } }
- 케냐 커피 (Concrete Component)
class KenyaCoffee extends Coffee{ public KenyaCoffee(){ super("에티오피아"); } @Override public void brewing(){ System.out.println("에티오피아 커피입니다."); } }
- 커피 데코레이터 (Decorator)
class CoffeeDecorator extends Coffee { Coffee coffee; public CoffeeDecorator(Coffee coffee){ this.coffee = coffee; } @Override public void brewing(){ coffee.brewing(); } }
- 라떼 (Concrete Decorator)
class Latte extends Decorator{ public Latte(Coffee coffee){ super(coffee); } @Override public void brewing(){ super(); System.out.println("우유를 첨가합니다."); } }
- 모카 라떼 (Concrete Decorator)
class Mocha extends Decorator{ public Mocha(Coffee coffee){ super(coffee); } @Override public void brewing(){ super(); System.out.println("모카 가루를 첨가합니다."); } }
- 사용 예시
EtiopiaCoffee ec = new EtiopiaCoffee(); ec.brewing(); //에티오피아 커피입니다. Latte latte = new Latte(new KenyaCoffee()); latte.brewing(); //케냐 커피입니다. //우유를 첨가합니다. Mocha mocha = new Mocha(new Latte(new EtiopiaCoffee())); mocha.brewing(); //에티오피아 커피입니다. //우유를 첨가합니다. //모카 가루를 첨가합니다.
위와 같이 컴포넌트 인터페이스에 추가적으로 필요한 기능에 대해 구현을 해줌으로써 동적으로 클래스를 만들어주고 있습니다.
처음 데코레이터 패턴에 대해 생각했을 때, 상속과 다른 점을 떠올릴 수 없었습니다.
하지만, 위와 같이 기존 기능에 추가적으로 해야되는 행위들에 대해서 클래스를 상속 받는 것과 데코레이터 패턴을 사용할 때를 살펴보면 많은 차이가 있을 것입니다.
커피 원두 - 케냐 - 에티오피아 커피 종류 - 라떼 : 아메리카노 + 우유 - 모카 라떼 : 라떼 + 모카 가루 - 카라멜 마키아토 : 라떼 + 카라멜 - 휘핑 크림이 올라간 모카 라떼 : 모카 라떼 + 휘핑 크림 - 휘핑 크림이 올라간 카라멜 마키아토 : 카라멜 마키아토 + 휘핑 크림 - 시럽이 추가된 모카 라떼 : 모카 라떼 + 시럽 - 시럽이 추가된 카라멜 마키아토 : 카라멜 마키아토 + 시럽
위와 같이 어떤 메뉴에 대해 재료가 하나 추가되는 경우를 상상해보면
데코레이터를 통해 추가를 해준다면 위의 예시에서 휘핑 크림과 시럽이라는 Concrete Decorator를 추가해주기만 한다면 새로운 커피 제조가 쉽게 되는 것을 확인할 수 있습니다.
이를 상속을 통해 표현하게 된다면 각각의 메뉴를 모두 새롭게 정의해줘야 될 것입니다.
만약, 여기서 프라페라는 메뉴가 새롭게 추가된다면 이에 대한 Concrete Decorator만 만들어준다면, 이를 통해서 휘핑 크림이 올라간 프라페나 시럽이 추가된 프라페라는 메뉴를 추가적인 클래스 정의 없이도 사용할 수 있게 됩니다.
이처럼 정의된 메서드에서의 추가적인 기능만 필요할 경우 동적으로 새로운 기능을 추가할 수 있게 해주는 패턴이 데코레이터 패턴입니다.
데코레이터 패턴을 통해 유연하게 설계한다면 최상단에 너무 많은 기능을 정의할 필요가 없어지게 됩니다( 동적으로 추가해줄 수 있으므로). 하지만, 너무 많은 데코레이터가 존재하게 된다면 이를 이해하기 쉽지 않고 복잡해질 수 있으니 과도하게 사용하는 것도 좋지 않을 수 있습니다.
Thread
- 프로세스는 프로그램이 실행되면 운영체제로부터 메모리를 할당 받아 프로세스 상태가 됩니다.
- thread는 프로세스가 실제 작업을 수행하는 실행 단위입니다.
multi thread
- 여러 thread가 동시에 작동하여 여러 작업이 동시에 실행됩니다.
- thread는 자신 만의 context를 가지고 있고 thread간에 공유하는 자원이 있을 수 있습니다.
- 여러 thread가 자원을 공유할 경우 서로 자원을 차지하려는 race condition이 발생 할 수 있습니다.
- race condition이 발생하는 지역을 critical section이라고 합니다.
- critical section에서 공유하는 자원에 대한 동기화를 해주지 않으면 의도했던 대로 동작하지 않을 수 있습니다.
자바에서 thread 구현
- Thread 상속 (run 메서드에 실행할 코드를 작성해주면 됩니다.)
class MyThread extends Thread { public void run(){ System.out.println("쓰레드 실행"); } }
- Runnable 인터페이스 구현 (자바는 다중 상속이 안되므로 이미 다른 클래스를 상속 받았을 경우 사용)
class MyThread implements Runnable { @Override public void run(){ System.out.println("쓰레드 실행"); } } MyThread mth = new MyThread(); Thread th = new Thread(mth); th.start();
우선순위
- Thread.MIN_PRIORITY = 1 ~ Thread.MAX_PRIORITY = 10 (default : Thread.NORMAL_PRIORITY=5)
- 우선 순위가 높은 Thread가 CPU의 배분을 받을 확률이 높습니다.
setPriority()
로 우선순위를 설정 할 수 있고 getPriority()로 쓰레드의 우선순위를 알 수 있습니다.
MyThread mth = new MyThread(); mth.setPriority(Thread.MIN_PRIORITY); mth.start();
join()
- 동시에 두 개 이상의 Thread가 실행 될 때, 다른 Thread의 결과를 참조하여 실행해야 하는 경우 사용합니다.
join()
을 실행한 Thread가 not-runnable 상태가 됩니다.- 해당 Thread의 수행이 끝나면 runnable 상태로 돌아옵니다.
interrupt()
- 다른 Thread에 예외를 발생시키는 interrupt를 보냅니다.
- Thread가
join()
,sleep()
,wait()
함수에 의해 not-runnable 상태일 경우interrupt()
메서드를 호출하면 다시 runnable 상태가 될 수 있습니다.
semaphore
- critical section은 두 개 이상의 thread가 동시에 접근하는 경우 의도치 않게 흘러갈 수 있으므로 동시 접근을 막아야 합니다.
- semaphore는 시스템 객체로 get, release 두 기능이 있습니다.
- 한 순간 오직 하나의 thread만이 semaphore를 얻을 수 있고, semaphore를 얻지 못한 thread들은 대기(blocking) 상태가 됩니다.
- semaphore를 얻은 thread만이 critical section에 들어갈 수 있습니다.
동기화
- 두 개의 thread 가 같은 객체에 접근 할 경우, 동시에 수정하거나 사용하면 의도치 않은 결과 나올 수 있습니다.
- 동기화는 critical section에 접근한 경우 공유 자원을 lock하여 다른 thread의 접근을 제어하는 것입니다.
- 동기화를 잘 못 구현하면 deadlock에 빠질 수 있습니다.
synchronized 블럭
- 현재 객체 또는 다른 객체를 lock으로 만듭니다.
class Bank { public void withdraw(int money){ synchronized(this){ if(this.money-money>=0){ this.money-=money; } } } }
synchronized 메서드
- 현재 메서드가 속해있는 객체에 lock을 겁니다.
class Bank { public synchronized void withdraw(int money){ if(this.money-money>=0){ this.money-=money; } } }
wait(), notify()
- 아직은 유효하지 않은 리소스를 기다리기 위해 Thread가 wait 상태가 됩니다.
- wait 상태가 된 Thread는 다른 Thread에서
notify()
가 호출될 때까지 기다립니다. - 유효한 자원이 생기면
notify()
가 호출되고 wait하고 있는 Thread 중 무작위로 Thread를 재시작합니다. notifyAll()
이 호출되는 경우 wait()하고 있는 모든 Thread가 재시작됩니다. 이 경우 유효한 리소스만큼의 Thread만이 먼저 실행되고 자원을 갖지 못한 Thread는 다시 wait 상태가 됩니다.
public synchronized void withdraw(int money){ while(this.money<money){ wait(); } this.money-=money; } public synchronized void add(int money){ this.money+=money; notify(); }
만약 갖고 있는 돈보다 인출하고 싶은 돈이 더 크다면 wait 상태가 됩니다.
돈을 저장하게 되면 돈을 저장하고 wait 상태인 Thread들을 깨웁니다.
인출하려고 기다리고 있는 Thread 중, 가지고 있는 돈이 인출하려는 돈보다 큰 Thread만 자원을 할당받고 인출하게 됩니다.
오늘은 데코레이터 패턴과 자바에서의 쓰레드 활용 방법에 대해 알아보았습니다.
데코레이터 패턴과 같은 경우 처음에는 사용 이유에 대해 잘 알지 못했지만, 실제 예시를 들어가면서 이 패턴이 필요한 이유에 대해 알 수 있었습니다. 동적으로 필요한 기능을 가진 객체를 생성 할 수 있다는 부분에서 사용성이 높다는 생각이 들었습니다.
또한, 자바에서 멀티 쓰레드 환경을 만드는 방법과 공유 자원에 대한 thread-safe한 방법에 대해 알 수 있었습니다.
반응형LIST'언어 > Java' 카테고리의 다른 글
자바 Annotation (0) 2024.02.19 Lombok 살펴보기 (0) 2023.08.16 자바 스트림, reduce, 예외 처리, logging (2) 2023.07.14 자바 내부 클래스, 람다식, 함수형 프로그래밍, 함수형 인터페이스 (2) 2023.07.13 자바 자료구조, 제네릭, 컬렉션 (0) 2023.07.11