네이버 클라우드 캠프 57일차 230714
#1 교육정리
1) ThreadPool : Gof의 FlyWeight
#2 ThreadPool : Gof의 FlyWeight
스레드풀의 구조와 Gof의 FlyWeight 패턴의 구조는 같습니다.
그래서 flyweight 패턴에 대해 먼저 살펴보겠습니다.
컴퓨터 프로그래밍 에서 Flyweight 소프트웨어 디자인 패턴은 데이터의 일부를 다른 유사한 개체와 공유하여 메모리 사용을 최소화하는 개체를 말합니다
개별적으로 저장될 경우 많은 양의 메모리를 사용하는 단순한 반복 요소가 있는 많은 수의 객체를 처리할 때 유용합니다. 공유 데이터를 외부 데이터 구조 에 보관 하고 사용할 때 임시로 개체에 전달하는 것이 일반적입니다 .
Flyweight의 주요 기능은 다음과 같습니다.
- 불변하고 상황에 독립적이며 공유 가능한 본질적인 상태를 저장합니다 (예: 주어진 문자 집합에서 문자 'A'의 코드).
- 다양하고 상황에 따라 다르며 공유할 수 없는 외부 상태(예: 텍스트 문서에서 문자 'A'의 위치) 를 전달하기 위한 인터페이스를 제공합니다.
위의 UML 클래스 다이어그램은 다음 을 보여줍니다.
Client가 Flyweight 패턴을 사용하는 클래스
객체를FlyweightFactory 생성 하고 공유하는 클래스Flyweight
Flyweight 외부 상태 를 취하고 작업을 수행하는 인터페이스.
고유 상태를 Flyweight1구현 하고 저장하는 클래스.
Flyweight 시퀀스 다이어그램은 다음과 같은 런타임 상호 작용을 보여줍니다.
객체 는 객체를 반환하는 를 Client호출 getFlyweight(key)합니다 .FlyweightFactoryFlyweight1.
operation(extrinsicState)반환된 Flyweight1객체를 호출한 후 Clientagain 은 를 호출 getFlyweight(key)합니다 .
FlyweightFactory는 이미 존재하는 개체를 반환 합니다 Flyweight1.
Flyweight 패턴 장점)
1. 메모리 절약: 공유 객체를 사용하여 메모리 사용량을 줄일 수 있습니다. 비슷한 객체를 여러 개 생성하는 대신, 하나의 객체를 공유하여 사용합니다.
2. 성능 향상: 객체를 공유하여 중복 생성을 피하고, 객체의 상태를 외부에서 관리함으로써 처리 속도를 향상시킵니다.
3.유연성: 외부 상태를 Flyweight 객체의 내부 상태에 전달하여 다양한 동작을 수행할 수 있습니다. 객체의 내부 상태와 외부 상태를 분리함으로써 객체의 재사용성을 높일 수 있습니다.
Flyweight 패턴은 주로 대규모 객체 집합을 다룰 때 유용하며, 예를 들어 그래픽 표현, 게임 엔진, 텍스트 처리 등에서 활용될 수 있습니다. 이 패턴은 메모리와 성능 최적화에 중점을 두고 객체의 공유와 관리를 효율적으로 처리할 수 있게 해줍니다.
구현 세부 사항)
Flyweight 패턴을 구현하는 방법에는 여러 가지가 있습니다. 한 가지 예는 외부 플라이웨이트 상태를 저장하는 개체가 변경될 수 있는지 여부인 가변성입니다.
불변 객체는 쉽게 공유할 수 있지만 상태가 변경될 때마다 새로운 외부 객체를 생성해야 합니다. 반대로 가변 객체는 상태를 공유할 수 있습니다. 가변성은 오래되고 사용되지 않는 개체의 캐싱 및 재초기화를 통해 더 나은 개체 재사용을 허용합니다. 공유는 일반적으로 상태가 매우 가변적일 때 실행 불가능합니다.
다른 주요 관심사에는 검색(최종 클라이언트가 플라이급에 액세스하는 방법), 캐싱 및 동시성이 포함됩니다 .
검색)
flyweight 개체를 생성하거나 재사용하기 위한 팩터리 인터페이스는 종종 복잡한 기본 시스템의 외관 입니다. 예를 들어, 팩토리 인터페이스는 플라이웨이트 생성을 위한 전역 액세스를 제공하기 위해 일반적으로 싱글톤 으로 구현됩니다 .
일반적으로 검색 알고리즘은 팩토리 인터페이스를 통해 새 객체를 요청하는 것으로 시작합니다.
요청은 일반적으로 객체의 종류에 따라 적절한 캐시 로 전달됩니다. 요청이 캐시의 개체에 의해 이행되면 다시 초기화되고 반환될 수 있습니다. 그렇지 않으면 새 개체가 인스턴스화됩니다. 개체가 여러 개의 외부 하위 구성 요소로 분할된 경우 개체가 반환되기 전에 함께 구성됩니다.
캐싱)
flyweight 개체를 캐시하는 방법 에는 유지 및 유지되지 않는 캐시의 두 가지가 있습니다 .
상태가 매우 가변적인 개체는 FIFO 구조 로 캐시할 수 있습니다 . 이 구조는 캐시를 검색할 필요 없이 캐시에서 사용하지 않는 개체를 유지합니다.
반대로 유지 관리되지 않은 캐시는 초기 오버헤드가 적습니다. 캐시의 개체는 컴파일 시간 또는 시작 시 대량으로 초기화됩니다. 개체가 캐시를 채우면 개체 검색 알고리즘에 유지 관리되는 캐시의 푸시/팝 작업보다 더 많은 오버헤드가 연관될 수 있습니다.
변경할 수 없는 상태의 외부 개체를 검색할 때 원하는 상태의 개체에 대한 캐시를 검색하기만 하면 됩니다. 그러한 개체가 없으면 해당 상태의 개체를 초기화해야 합니다. 변경 가능한 상태의 외부 개체를 검색할 때 사용된 개체가 없으면 캐시에서 사용되지 않은 개체를 검색하여 다시 초기화해야 합니다. 사용 가능한 사용되지 않는 개체가 없으면 새 개체를 인스턴스화하고 캐시에 추가해야 합니다.
외부 개체의 고유한 각 하위 클래스에 대해 별도의 캐시를 사용할 수 있습니다. 고유한 검색 알고리즘을 각 캐시와 연결하여 여러 캐시를 개별적으로 최적화할 수 있습니다. 이 개체 캐싱 시스템은 구성 요소 간의 느슨한 결합을 촉진하는 책임 패턴 체인 으로 캡슐화할 수 있습니다 .
동시성)
flyweight 개체가 여러 스레드에서 생성되는 경우 특별한 고려 사항을 고려해야 합니다. 값 목록이 유한하고 미리 알려진 경우 플라이웨이트를 미리 인스턴스화하고 경합 없이 여러 스레드의 컨테이너에서 검색할 수 있습니다. 플라이웨이트가 여러 스레드에서 인스턴스화되는 경우 두 가지 옵션이 있습니다.
flyweight 인스턴스화를 단일 스레드로 만들어 경합을 도입하고 값당 하나의 인스턴스를 보장합니다.
동시 스레드가 여러 플라이급 인스턴스를 생성하도록 허용하여 경합을 제거하고 값당 여러 인스턴스를 허용합니다.
클라이언트와 스레드 간에 안전하게 공유할 수 있도록 플라이웨이트 객체를 불변 값 객체 로 만들 수 있습니다 . 여기서 두 인스턴스는 값이 같으면 같은 것으로 간주됩니다.
지금까지 Flyweight에 대해서 살펴보았습니다.
이제는 스레드풀에 flyweight를 적용하는 개념 및 구조를 정리해보겠습니다.
ThreadPool)
먼저 ThreadPool이 만들어진 이유부터 알아보겠습니다.
Thread가 필요하면 new 키워드를 이용하여 각 Thread 인스턴스를 생성했었습니다. 한 번 사용하고 반납해야 하는 상황이라면 개별적으로 Thread 인스턴스를 생성하여 사용해도 상관없지만, Java 언어를 이용하여 많이 만들어지는 서버 애플리케이션은 수많은 Thread를 필요로 하며 초 단위로 Thread를 생성하여 사용하고 반납해야 합니다.
Java는 Thread 인스턴스의 start() 메서드가 호출되면서 커넣 스레드를 할당받아 사용합니다. 커널 스레드를 생성하고 반납하는 연산은 생각보다 비싼 연산입니다. 초 단위로 Thread를 몇 백개 생성하고 사용하고 반납하고 한다면 애플리케이션은 메모리 부족 현상이 발생할 수 있습니다. 그리고 반납한 Thread 인스턴스를 메모리에서 해제해야 하기 때문에 Garbage Collector도 굉장히 바빠지며 그만큼 CPU가 해야 할 일이 굉장히 많아집니다.
그래서 생성과 메모리 해제 비용을 줄이기 위해 Thread를 미리 생성해 놓고 필요할 때만 가져다가 사용하고 커넣 스레드를 반납하지 안호 재사용할 수 있도록 하는 Thread Pool이 만들어졌습니다.
위의 사용하는 이유와 함께 아래 그림을 참고하시면 이해하시는데 도움이 될겁니다.
그렇다면 ThreadPool이 무엇인지 왜 사용해야 하는지 이해가 좀 되셨을 겁니다.
아래는 수업 내용을 정리한 내용입니다.
1. ThreadPool 적용 전
스레드풀을 적용하기 전에는 각 클라이언트가 접속 한만큰 스레드가 생성되고 클라이언트의 요청에 응답을 완료하였다면 생성되었던 스레드는 가비지 즉 버려졌습니다.
이로인해 과도한 가비지를 생성하고 메모리를 낭비하게 됩니다.
2. ThreadPool 적용 후
스레드풀을 적용 후에는 클라이언트 접속으로 생성되었던 스레드가 스레드풀에 생성되어 있습니다.
그리고 스레드풀에서 serverApp으로 리턴해줍니다.
3. Thread 반납
스레드를 사용하고나서 버리지 않고 스레드풀에 리턴하여 보관합니다.
그리고 serverApp에서 다시 사용할 때 getThread() 하여 꺼내고 다시 리턴합니다.
이것이 바로 "객체 재사용" 이며, "Flyweight" 기법을 적용한 것입니다.
4. 개인 Project에 ThreadPool Class Diagram 적용
아래와 같은 구조로 ThreadPool을 개인프로젝트에 적용하였으며, 자세한 코드내용은 아래 제 git을 참조바랍니다.
git 주소 :
https://github.com/wooki37/bitcamp-study/tree/main/myapp/app-44-server/src/main/java/bitcamp/util
이제 마지막으로 코드를 통해 자세하게 더 알아보겠습니다.
코드 예제)
스레드 재사용 - 1단계) 스레드 재 사용전 - 매번 스레드 생성)
// 스레드 재사용 - 1단계) 스레드 재 사용전 - 매번 스레드 생성
package com.eomcs.concurrent.ex6;
import java.util.Scanner;
public class Exam0110 {
public static void main(String[] args) {
class MyThread extends Thread {
int count;
public void setCount(int count) {
this.count = count;
}
@Override
public void run() {
for (int i = count; i > 0; i--) {
System.out.println("==> " + i);
}
}
}
Scanner keyScan = new Scanner(System.in);
while (true) {
System.out.print("카운트? ");
String str = keyScan.nextLine();
if (str.equals("quit")) {
break;
}
int count = Integer.parseInt(str);
MyThread t = new MyThread();
t.setCount(count);
t.start();
// 카운트 할 때 마다 매번 스레드를 생성한다.
// => 실행 완료된 스레드는 가비지가 된다.
// => 가비지 컬렉터가 가비지가 된 스레드를 수집하여 해제시키기 전까지는
// 그 스레드를 위해 할당된 메모리를 사용할 수 없다.
// => 즉 스레드를 매번 생성하는 방식은
// 과다한 가비지를 생성하기 때문에 메모리 낭비를 일으킨다.
}
System.out.println("main 스레드 종료!");
keyScan.close();
}
}
실행 결과)
스레드 재사용 - 1단계) 스레드를 재 사용하려 시도)
// 스레드 재사용 - 1단계) 스레드를 재 사용하려 시도
package com.eomcs.concurrent.ex6;
import java.util.Scanner;
public class Exam0111 {
public static void main(String[] args) {
class MyThread extends Thread {
int count;
public void setCount(int count) {
this.count = count;
}
@Override
public void run() {
for (int i = count; i > 0; i--) {
System.out.println("==> " + i);
}
}
}
// 카운트를 수행할 스레드를 미리 만든다.
MyThread t = new MyThread();
Scanner keyScan = new Scanner(System.in);
while (true) {
System.out.print("카운트? ");
String str = keyScan.nextLine();
if (str.equals("quit")) {
break;
}
// 사용자가 카운트 값을 입력하면,
int count = Integer.parseInt(str);
// 기존에 생성한 스레드에 카운트 값을 설정한 후 실행을 시작시킨다.
t.setCount(count);
t.start();
// 문제점?
// - 한 번 실행이 완료된 Dead 상태의 스레드는 다시 시작시킬 수 없다.
// - 다시 시작하려고 하면 예외가 발생한다.
// - 즉 run() 메서드 호출이 끝나, Dead 상태가 된 스레드는 다시 start() 할 수 없다!
}
System.out.println("main 스레드 종료!");
keyScan.close();
}
}
실행 결과)
한번 실행 후 다시 실행하려면 IllegalThreadStateException error가 발생합니다.
스레드 재사용 - 2단계) sleep()/timeout 을 활용한 스레드 재사용)
// 스레드 재사용 - 2단계) sleep()/timeout 을 활용한 스레드 재사용
package com.eomcs.concurrent.ex6;
import java.util.Scanner;
public class Exam0120 {
public static void main(String[] args) {
class MyThread extends Thread {
int count;
public void setCount(int count) {
this.count = count;
}
@Override
public void run() {
System.out.println("스레드 시작했음!");
try {
// 스레드를 재사용하려면 다음과 같이 run() 메서드가 종료되지 않게 해야 한다.
while (true) {
// 사용자가 카운트 값을 입력할 시간을 주기 위해
// 10초 정도 스레드를 멈춘다.
Thread.sleep(10000);
System.out.println("카운트 시작!");
for (int i = count; i > 0; i--) {
System.out.println("==> " + i);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
MyThread t = new MyThread();
// 미리 스레드를 시작시켜 놓는다.
t.start();
Scanner keyScan = new Scanner(System.in);
while (true) {
System.out.print("카운트? ");
String str = keyScan.nextLine();
if (str.equals("quit")) {
break;
}
int count = Integer.parseInt(str);
t.setCount(count); // 스레드의 카운트 값을 변경한다.
// sleep()을 이용한 스레드 재활용 방식은
// 일정 시간이 지난 후 스레드가 작업하게 만드는 방식이다.
// 스레드가 잠든 사이에 작업할 내용을 설정해두면,
// 스레드가 깨어났을 때 변경 사항에 따라 작업을 수행한다.
// 이 방식으로 한 개의 스레드를 재활용하여 작업을 처리할 수 있지만,
// 문제는:
// => 스레드가 깨어날 때까지 작업이 바로 실행되지 않는다.
// => 작업을 시키고 싶지 않아도 깨어나면 무조건 작업할 것이다.
//
}
System.out.println("main 스레드 종료!");
keyScan.close();
}
}
실행 결과)
스레드 재사용 - 3단계) sleep()/timeout 을 활용한 스레드 재사용 II)
// 스레드 재사용 - 3단계) sleep()/timeout 을 활용한 스레드 재사용 II
package com.eomcs.concurrent.ex6;
import java.util.Scanner;
public class Exam0130 {
public static void main(String[] args) {
class MyThread extends Thread {
boolean enable;
int count;
public void setCount(int count) {
this.count = count;
// 카운트 값을 설정할 때 작업을 활성화시킨다.
this.enable = true;
}
@Override
public void run() {
System.out.println("스레드 시작했음!");
try {
while (true) {
System.out.println("스레드를 10초 동안 잠들게 한다!");
Thread.sleep(10000);
// 무조건 작업하지 말고,
// enable이 true일 때만 작업하게 하자!
if (!enable) {
continue;
}
System.out.println("카운트 시작!");
for (int i = count; i > 0; i--) {
System.out.println("==> " + i);
}
// 스레드에게 맡겨진 작업이 끝나면 비활성 상태로 설정한다.
enable = false;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
MyThread t = new MyThread();
t.start();
Scanner keyScan = new Scanner(System.in);
while (true) {
System.out.print("카운트? ");
String str = keyScan.nextLine();
if (str.equals("quit")) {
break;
}
int count = Integer.parseInt(str);
t.setCount(count);
// 이 버전은 다음과 같이 동작한다.
// => 스레드가 작업을 완료하면 10초 동안 잠든다.
// => 10초 후에 깨어났을 때 카운트 값이 설정되어 있지 않으면 다시 잠든다.
// => 카운트 값이 설정되면서 enable이 활성화 상태라면 작업을 실행한다.
// => 작업이 끝나면 enable을 비활성으로 만든 후 잠든다.
// 이전 버전에서는 깨어난 후 무조건 작업을 수행했지만,
// 이 버전은 카운트 값이 설정될 때만 작업하도록 개선하였다.
// 그러나 근본적인 문제는 해결되지 않았다.
// => 작업을 완료한 후 무조건 10초를 기다린다.
// => 스레드가 깨어난 후 작업이 없더라도 10초를 기다린다.
//
}
System.out.println("main 스레드 종료!");
keyScan.close();
}
}
실행 결과)
스레드 재사용 - 4단계) wait()/notify() 사용)
// 스레드 재사용 - 4단계) wait()/notify() 사용
package com.eomcs.concurrent.ex6;
import java.util.Scanner;
public class Exam0140 {
public static void main(String[] args) {
class ValueBox {
int count;
synchronized public void setCount(int count) {
this.count = count;
// 이 객체의 사용을 기다리는 스레드에게 작업을 시작할 것을 알린다.
//synchronized (this) {
this.notify();
//}
// 문법 주의!
// => notify()도 동기화 영역에서 호출해야 한다.
// => 안그러면 IllegalMonitorStateException 예외가 발생한다.
}
}
class MyThread extends Thread {
ValueBox valueBox;
public void setValueBox(ValueBox valueBox) {
this.valueBox = valueBox;
}
@Override
public void run() {
System.out.println("스레드 시작했음!");
try {
while (true) {
System.out.println("스레드 대기중...");
// wait()
// - 해당 객체에서 notify()를 통해 알림이 올 때까지 스레드의 실행을 멈추게 한다.
// - 이 메서드는 동기화 블록
// (한 번에 한 스레드만이 진입하도록 설정된 블록)에서만 호출할 수 있다.
//
// 문법 주의!
// => wait()/notify() 는 반드시 동기화 영역 안에서 호출해야 한다.
//
// 동기화 영역?
// => synchronized로 선언된 메서드
// 예) synchronized void m() {}
// => synchronized로 묶인 블록
// 예) synchronized(접근대상) {...}
//
synchronized (valueBox) {
valueBox.wait();
// valueBox 객체에 대해 사용하라는 신호가 올 때까지 이 스레드에게 기다리라는 명령이다.
// 즉 wait()를 호출한 스레드는 Not Runnable 상태에 진입한다.
// => 실행을 멈추고 CPU 사용권을 받지 않는 상태가 된다.
//
// 이 스레드를 잠에서 깨우는 방법?
// => 다른 스레드가 valueBox에 대해 notify()를 호출하면,
// wait() 로 신호를 기다리고 있는 스레드가 잠에서 깨어나
// 실행을 시작한다.
//
// 기다림을 끝내는 방법?
// => 즉 notify()를 통해 기다림이 끝났다는 것을 알림 받아야 한다.
//
}
System.out.println("카운트 시작!");
for (int i = valueBox.count; i > 0; i--) {
System.out.println("==> " + i);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
ValueBox valueBox = new ValueBox();
MyThread t = new MyThread();
t.setValueBox(valueBox);
t.start(); // 이 스레드는 main 스레드가 실행하라고 신호를 줄 때까지 기다린다
Scanner keyScan = new Scanner(System.in);
while (true) {
System.out.print("카운트? ");
String str = keyScan.nextLine();
if (str.equals("quit")) {
break;
}
valueBox.setCount(Integer.parseInt(str));
// setCount()
// - 사용자가 입력한 카운트 값을 설정할 때
// - main 스레드는 이 객체의 사용을 간절히 기다리는 다른 스레드에게
// 즉시 사용하라고 신호를 보낸다.
// - setCount() 메서드의 코드를 확인해 보라!
}
System.out.println("main 스레드 종료!");
keyScan.close();
}
}
실행 결과)
멀티 스레드 재사용 - Pooling 기법을 이용하여 생성된 객체를 재활용하기)
// 멀티 스레드 재사용 - Pooling 기법을 이용하여 생성된 객체를 재활용하기
package com.eomcs.concurrent.ex6;
import java.util.ArrayList;
import java.util.Scanner;
public class Exam0210 {
static class MyThread extends Thread {
ThreadPool pool;
int count;
public MyThread(String name, ThreadPool pool) {
super(name);
this.pool = pool;
}
public void setCount(int count) {
this.count = count;
synchronized (this) {
notify();
}
}
@Override
public void run() {
synchronized (this) {
try {
while (true) {
// 작업하라는 알림이 올 때까지 기다린다.
wait();
// 알림이 오면 작업을 실행한다.
for (int i = count; i > 0; i--) {
System.out.printf("[%s] %d\n", getName(), i);
Thread.sleep(2000);
}
// 작업이 끝났으면 스레드풀로 돌아간다.
pool.add(this);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
// MyThreadPool 과 MyThread 상호간에 참조를 피하기 위해
// 인터페이스를 준비했다.
interface ThreadPool {
Thread get();
void add(Thread obj);
}
static class MyThreadPool implements ThreadPool {
ArrayList<MyThread> list = new ArrayList<>();
public MyThreadPool() {
// 사용할 스레드 객체를 미리 생성한다.
// - 나중에 MyThread가 Pool로 다시 리턴될 수 있도록
// 스레드 객체를 생성할 때 Pool의 주소를 알려준다.
MyThread t1 = new MyThread("1번 스레드=>", this);
MyThread t2 = new MyThread("2번 스레드***>", this);
MyThread t3 = new MyThread("3번 스레드-->", this);
// 생성된 스레드를 컬렉션에 보관한다.
list.add(t1);
list.add(t2);
list.add(t3);
// 일단 무조건 스레드를 미리 실행해 놓는다.
t1.start();
t2.start();
t3.start();
}
// 스레드 풀에서 한 개의 스레드를 꺼낸다.
@Override
public MyThread get() {
if (list.size() > 0) { // 컬렉션에 남아 있는 스레드가 있다면,
return list.remove(0);
}
return null; // 없으면, null을 리턴한다.
// 현재 이 예제에서는 오직 3개의 스레드만 쓰도록 하였다.
}
// 스레드를 다 쓴 후에는 다시 스레드 풀에 돌려준다.
@Override
public void add(Thread t) {
list.add((MyThread) t);
}
}
public static void main(String[] args) {
// 스레드풀 준비!
MyThreadPool threadPool = new MyThreadPool();
Scanner keyScan = new Scanner(System.in);
while (true) {
System.out.print("카운트? ");
String str = keyScan.nextLine();
if (str.equals("quit")) {
break;
}
int count = Integer.parseInt(str);
// 스레드풀에서 스레드를 한 개 꺼낸다.
MyThread t = threadPool.get();
if (t == null) {
System.out.println("남는 스레드가 없습니다!");
continue;
}
// 스레드의 카운트를 설정한다. 그러면 카운트를 시작할 것이다.
t.setCount(count);
}
System.out.println("main 스레드 종료!");
keyScan.close();
}
}
실행 결과)