실습 - 스레드 풀 구현하기
git/eomcs-java-project/mini-pms-36-a
먼저 자바에서 제공하고 있는 ExecutorService 대신, 직접 ThreadPool를 구현하여 사용할 것이다. 일단 com.eomcs.pms.util.concurrent 패키지에 ThreadPool 클래스를 정의한다. ThreadPool에는 다음과 같은 멤버들이 있다.
- Worker : Thread를 상속받아 정의한 스레드 클래스
- workers : Woker 객체들을 저장할 컬렉션 객체
- execute(Runnable) : 스레드가 실행할 코드를 run메서드로 갖고 있는 Runnable을 파라미터로 넘겨받아 풀에서 Worker 하나를 꺼내어 실행시킨다.
public class ThreadPool {
boolean stopping = false;
List<Worker> workers = new ArrayList<>();
class Worker extends Thread {
}
public void execute(Runnable task) {
}
public void shutdown() {
}
}
ThreadPool에서 갖는 Worker 클래스는 다음과 같다.
- task : 스레드가 실행할 run 메서드를 가진 Runnable 객체를 저장할 필드
- setTask(Runnable) : Runnable 객체를 받아 task에 저장 후 이 객체를 start시킨다.
- run() : start 되면 스레드가 분리된 후 바로 실행될 메서드로, start 되면 스레드가 분리된 후 바로 실행될 메서드로, 무한정으로 대기하다가 외부에서 notify 를 호출할 경우에만 작업을 실행한다.
- stopping / shutdown : 스레드풀을 종료하기 위한 장치로 보통 때에는 stopping이 false 상태였다가 스레드풀을 종료해야할 시점이 될 때, shutdown을 통해 stopping을 true로 바꿔준다.
class Worker extends Thread {
Runnable task;
public void setTask(Runnable task) {
this.task = task;
synchronized (this) {
this.notify();
}
}
@Override
public void run() {
}
}
run 메서드는 Thread의 run 메서드를 오버라이딩했기 때문에 이 객체에 대하여 start 메서드를 호출하면 자동으로 run 메서드가 실행된다.
메서드의 구체적인 구현 내용은 다음과 같다.
- 실행이 시작되자마자 wait 상태에 머물러 외부에서 이 객체에 대하여 notify를 호출할 때까지 기다린다. 이 과정에서 예외가 발생하면 스레드를 스스로 종료할 수 있도록 while문을 빠져나간다.
- 외부에서 이 객체에 대하여 notify를 호출하면 task에 대하여 run을 호출한다. task의 run 작업을 수행하는 중에 예외가 발생해도 스레드 자체는 종료되지 않도록 한다.
- task의 run이 실행 완료된 후에는 스스로를 workers에 다시 넣어준다.
- 이 메서드가 완료되지 않도록 while문에 실행되는 코드의 전체를 넣어 무한정 반복시킨다.
@Override
public void run() {
synchronized (this) {
while (true) {
try {
System.out.printf("[%s] - 스레드 대기 중...\n", this.getName());
this.wait();
System.out.printf("[%s] - 스레드 작업 시작!\n", this.getName());
} catch (Exception e) {
System.out.printf("[%s] - 스레드 실행 중 오류 발생!\n", this.getName());
break;
}
try {
task.run();
System.out.printf("[%s] - 스레드 작업 종료!\n", this.getName());
} catch (Exception e) {
System.out.printf("[%s] - %s\n",
this.getName(), e.getMessage());
} finally {
workers.add(this);
System.out.printf("[%s] - 스레드풀로 되돌아 감!\n", this.getName());
}
}
}
}
}
이것으로 Worker 클래스의 구현이 끝났다. 이제 ThreadPool에 있는 각 Worker 객체에 대하여 작업을 지시할 execute 메서드를 구현한다. execute 메서드의 구체적인 내용은 다음과 같다.
- Runnable task 파라미터 : 스레드가 실행할 작업을 갖는 Runnable 객체를 외부에서 파라미터로 넘겨줄 것이다.
- workers가 비어있다면, 새로운 Worker를 생성하고 바로 start 시켜준다. 그러면 Worker의 run 메서드가 실행될 것이다.
- 새로 생성된 Worker에 대하여 start을 실행한 후에 분리된 스레드에서 혹시라도 run 메서드 내부의 wait이 실행되지 못하고 main 스레드에게 CPU를 뺏길 가능성을 고려하여, 적어도 main 스레드에서 다음 코드(setTask를 호출하는 코드)를 실행하기 전에 wait 메서드가 실행될 수 있게끔 메인 스레드를 20밀리초간 재운다.
- 만약 workers에 Worker가 하나라도 있다면 가장 앞에 있는 Worker 객체를 하나 꺼낸다.
- 새롭게 생성한 Worker 객체에 대하여 혹은, workers에서 꺼낸 Worker 객체에 대하여 setTask(task)를 호출하여 Worker 객체에게 수행할 작업을 지정해주고 notify를 호출한다.
public void execute(Runnable task) {
Worker t;
if (workers.size() == 0) {
t = new Worker();
System.out.printf("[%s] - 스레드 생성!\n", t.getName());
t.start();
try {
Thread.sleep(20);
} catch (Exception e) {
}
} else {
t = workers.remove(0);
System.out.printf("[%s] - 스레드 꺼내서 재사용!\n", t.getName());
}
t.setTask(task);
}
이제 더 이상 작업을 받지 않고 스레드풀의 모든 스레드를 종료시키는 shutdown 메서드를 구현한다. shutdown 메서드의 구체적인 내용은 다음과 같다.
- stopping의 값을 true로 바꿔준다.
- workers에 스레드가 하나도 없을 때까지 하나씩 꺼내어 notify를 해준다.
- 그런데 만약 어떤 스레드가 수행중이여서 workers가 비어있는 잠깐의 순간에 while문을 빠져나올 가능성이 있으니 그 스레드들이 실행을 완료할 때까지 2초정도 기다려준 후, 다시한번 더 workers가 비게 될 때까지 Worker를 꺼내 notify를 해주는 코드를 한번더 실행시킨다.
public void shutdown() {
try {
this.stopping = true;
while (!workers.isEmpty()) {
Worker worker = workers.remove(0);
synchronized (worker) {
worker.notify();
}
}
Thread.sleep(2000);
while (!workers.isEmpty()) {
Worker worker = workers.remove(0);
synchronized (worker) {
worker.notify();
}
}
} catch (Exception e) {
System.out.println("스레드풀을 종료하는 중에 예외 발생!");
e.printStackTrace();
}
}
그럼 왜 모든 스레드에 notify를 시켜주는 가? 대기중인 스레드들이 종료되려면 Worker 객체가 스스로를 종료할 수 있게끔 wait 상태에서 벗어나야하기 때문이다.
그렇다면 wait 상태에서 벗어난 Worker 객체가 stopping 이 true일 경우에 스스로를 종료시킬 수 있도록 하는 코드를 넣는다.
@Override
public void run() {
synchronized (this) {
while (true) {
try {
System.out.printf("[%s] - 스레드 대기 중...\n", this.getName());
this.wait();
if (ThreadPool.this.stopping) {
break;
}
.
.
.
이렇게 하면 ThreadPool가 구현이 충분히 되었다. 이것을 이제 ServerApp에서 사용해보자.
일단 ThreadPool 객체를 생성하여 필드로 저장한다.
public class ServerApp {
...
ThreadPool threadPool = new ThreadPool();
...
그리고 직접 Thread를 생성하던 코드를 threadPool에서 execute 메서드를 호출하는 것으로 변경한다.
// 원래 코드
new Thread(() -> handleClient(clientSocket)).start();
// => 변경된 코드
threadPool.execute(() -> handleClient(clientSocket));
또한 서버가 종료되는 시점에 실행되고 있는 모든 스레드들이 종료될 수 있도록 threadPool에 대하여 shutdown 메서드를 호출한다.
public void service(int port) {
.
.
.
notifyApplicationContextListenerOnServiceStopped();
threadPool.shutdown();
}
실습 - 자바 스레드풀 사용하기
git/eomcs-java-project/mini-pms-36-b
방금은 스레드풀을 직접 구현하여 ServerApp에서 사용했으나 이것은 자바 API에서 제공하는 ExecutorService 클래스에 비하면 아무것도 아니다. 대표적으로 ExecutorService가 내부적으로 갖는 작업 큐를 통해 스레드 풀에 대하여 사용자가 지시한 작업을 모두 저장할 수 있는 기능을 구현하지 않았다.
따라서 이번에는 ServerApp에서 직접 만든 ThreadPool 대신 자바에서 제공하는 ExecutorService를 사용할 것이다. 일단 프로젝트에서 com.eomcs.util.concurrent 패키지와 함께 ThreadPool 클래스를 삭제한다.
그리고 ServerApp에서 ThreadPool 객체를 생성하여 사용하던 것을 ExecutorService 타입으로 바꿔주고 Executors에 대하여 newCachedThreadPool을 호출하여 크기가 정해지지 않은 스레드 풀을 생성한다.
// 원래 코드
ThreadPool threadPool = new ThreadPool();
// 변경된 코드
ExecutorService threadPool = Executors.newCachedThreadPool();
threadPool에 대하여 execute를 호출하는 코드는 동일하다. 그러나 ThreadPool의 shutdown 메서드 안에서 구현해준 것과 같이, 확실히 threadPool의 모든 스레드가 종료될 수 있도록 하는 장치가 필요하다. 이 코드는 ServerApp에서 shutdown을 호출하는 코드 밑에 구현한다.
- 최초로 threadPool에 대하여 shutdown을 호출한다.
- awaitTermination을 통해 모든 스레드가 종료될 때까지 10초간 기다리다가 모든 스레드가 종료되지 않으면 shutdownNow를 호출하여 강제 종료를 시도한다. 모든 스레드가 종료되었다면 서버 종료를 출력하고 프로그램을 끝낸다.
- 그리고 다시 모든 스레드가 종료되었는지 확인하기 위해 5초간 기다리다가 이번에도 스레드가 모두 종료되지 못했다면 강제 종료를 완료하지 못했다는 메시지를 띄운다. 만약 모든 작업을 종료하였다면 강제 종료하였다는 메시지를 띄운다.
- 종료 중에 예외가 발생하면 그에 대한 예외 처리를 해준다.
threadPool.shutdown();
try {
if (!threadPool.awaitTermination(10, TimeUnit.SECONDS)) {
System.out.println("아직 종료 안된 작업이 있다.");
System.out.println("남아 있는 작업의 강제 종료를 시도하겠다.");
threadPool.shutdownNow();
if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) {
System.out.println("스레드풀의 강제 종료를 완료하지 못했다.");
} else {
System.out.println("모든 작업을 강제 종료했다.");
}
}
} catch (Exception e) {
System.out.println("스레드풀 종료 중 오류 발생!");
}
System.out.println("서버 종료!");
DBMS
소프트웨어 프로그램의 데이터 관리에 대한 다양한 이슈/요구사항이 있다.
- 임의 위치의 값 변경
- 특정 조건에 해당하는 data 찾기
- 특정조건의 data 삭제
- 대량의 data를 여러 파일로 분산
- data와 data간의 관계를 관리
- 유효하지 않은 data 조작을 제어
- 데이터 저장 프로그램을 다른 프로그램으로 변경
개발자가 직접 Data 관리를 위해 파일을 조작하면서 이러한 요구사항을 모두 수용하려면 엄청난 양의 코딩과 중복 개발에 대한 문제가 생긴다.
파일 시스템이란?
데이터를 파일로 저장하여 관리하는 시스템을 말한다. 가장 단순하게는 엑셀 파일에 데이터를 입력하여 저장하거나 메모장으로 텍스트를 저장하는 방법이 있다.
따라서 데이터를 정교하게 다뤄야하는 프로그램들은 데이터 관리를 대신해주는 데이터 관리 전문 s/w를 사용해야 한다. 이 전문 s/w을 dataBase Management System, 즉 DBMS라고 한다.
DBMS (DataBase Management System)
DBMS는 말그대로 데이터베이스 관리 시스템으로 데이터 베이스를 사용자가 쉽고 효율적으로 다룰 수 있도롭 돕는 시스템이다. DBMS는 데이터베이스를 중앙에서 통합하여 관리하고 여러 사용자와 응용프로그램들은 하나의 DBMS와 통신하여 원하는 데이터를 얻는다.
예) Oracle, My-SQL, MS-SQL, DB2, Altibase, Cubrid, Tibero
Oracle의 유료 DBMS 제품 비용
개인용 Oracle DBMS - 2003년 형을 사면 한번만 결제해서 비용을 내고 바꿀때까지 비용을 안내다가 버전을 바꿀때에만 업그레이드 비용을 추가적으로 내면 된다.
기업용 Oracle DBMS - 처음 살때에는 1억상당의 금액을 내고 1년에 한번씩 첫 구입비용의 10~15% 라이센스 유지 비용을 내야한다. 그렇게 매년 라이센스 유지비용을 내지 않으면 나중에 보안 문제가 생겨도 유지보수가 안되므로 보안 문제를 보완한 버전을 새로 1억을 주고 사야한다. 따라서 보안문제에 민감한 기업들은 어쩔수없이 유지비용을 내면서 라이센스를 살수밖에 없는것이다.
그러나 구글과 같은 대기업들이 Oracle과의 계약을 끊고 오픈소스인 MariaDB를 많이 사용하기 시작했는데 그것은 보안상 문제가 생기더라도 구글에서 자체 엔지니어들이 소스코드를 확인하면서 유지보수를 할 수 있는 힘이 있기 때문이다.
DBMS의 대표적인 종류는 다음과 같다.
출처 : https://dololak.tistory.com/453
- RDBMS(Relational DBMS)
데이터의 형태로 흔히 표현되는 행(column), 열(Record)로 구성된 Table간의 관계를 나타낼 때 사용한다. 우리는 이렇게 표현된 데이터를 SQL을 사용하여 데이터 관리 및 접근을 한다.
그 밖의 특징들- 데이터 간에 1:1 관계를 유지한다.
- 데이터가 2차원 구조의 테이블로 구성된 관계형 모델을 사용한다.
- 한 테이블은 다수의 열로 구성된다.
- 각 열은 레코드 단위로 구성된다.
- 데이터간의 연결은 키의 중복으로 생성된다.
- 현재 판매되는 대부분의 DBM가 이 형식을 따른다.
- 높은 성능을 보이고 있으며 유연한 질의 능력을 갖는다.
-
NoSQL 데이터베이스(Not-Only SQL) - MongoDB
-
관계형 데이터베이스보다 덜 제한적인 일관성 모델을 이용한다.
-
키(key)와 값(value) 형태(단순한 데이터)로 저장되고, 키를 사용해 데이터 관리 및 접근을 한다.
-
읽고 쓰기가 빠르다는 장점이 있다.
-
-
ORDBMS - PostgreSQL
-
사용자 정의 타입, 참조타입, 중첩 테이블을 지원한다.
-
대단위 객체의 저장, 추출이 가능하다.
-
객체 간의 상속 관계를 지원하는 것이 가능하다.
-
단 효율적인 질의 처리가 미비하다.
-
actor가 App한테 데이터를 요청하면 App은 DBMS에게 데이터를 요청하고 DBMS는
- Data 저장/조회
- Data 변경/삭제/검색
- Client 인증/권한 관리
- data 간의 관계 제어해서 결함을 생기지 않게 제어(무결성 - Integrity(온전함))
- (멀티 스레딩 / 스레드풀)
- (Application과 stateful/stateless 방식의 통신)
위과 같은 작업을 수행한 후 App에게 적절한 응답을 한다.
DBMS 프로그램은 Application이 존재하는 로컬 컴퓨터 위에 서버 프로그램으로 존재하며 한 로컬 컴퓨터 안에서 DBMS는 서버, Application은 Client가 되어 서로 통신하는 형태를 띄고, 통신 언어는 SQL이다.
SQL(Structured Query Language)
SQL은 관계형 데이터베이스 관리 시스템(RDBMS)의 데이터를 관리하기 위해 설계된 특수 목적의 프로그래밍 언어이다. 관계형 데이터 베이스 관리 시스템에서 자료의 검색과 관리, 데이터베이스 스키마 생성과 수정, 데이터 베이스 객체 접근 조정 관리를 위해 고안되었다. 많은 수의 데이터베이스 관련 프로그램들이 SQL을 표준으로 채택하고 있다.
SQL은 DBMS들이 표준으로 채택하고 다는 것은 SQL이 특정 vendor의 DBMS에 종속되지 않음을 의미한다.
따라서 SQL로 작성하기만 한다면 Oracle, MS-SQL, MariaDB를 모두 사용할 수 있다.
다만 SQL 표준만으로는 모든 DBMS가 호환되지 않는다. 그 이유는 다음과 같다.
- DBMS마다 지원하는 SQL 버전이 다르다.
- 각 DBMS가 SQL의 특정 버전을 사용하고 있으면서도 그 버전을 완벽하게 지원하지 않을 수도 있다.
- 특정 DBMS 전용 문법이 사용되어서 해당 DBMS에서만 유효한 문법이 존재할 수도 있다.
- 같은 Vendor의 DMBS(대부분 같은 제품을 말한다.)라 하더라도, DBMS의 버전마다 사용할 수 있는 문법에 차이가 날 수 있다.
따라서 개발자는 사용할 DBMS에 적절한 SQL문을 작성해야한다. 각 DBMS가 지원하는 SQL 문법은 다음과 같다고 할 수 있다.
"SQL문 = SQL 표준 + DBMS 전용문법"
그럼 표준 SQL만 사용하면 모든 DBMS와 호환이 되는 것일까? 그것은 아니다. 그 이유는 다음과 같다.
- 어떤 DBMS는 지원하지 않는 SQL 표준이 있을 수 있다. SQL 중에서도 특정 DBMS에서 사용할 수 없는 문법을 사용해서는 안된다.
- DBMS 전용 문법은 해당 DBMS의 성능을 최적으로 사용할 수 있는 문법이다. 표준 SQL만 사용한다는 것은 이 이점을 포기하는 것이다.
따라서 개발자는 같은 어플리케이션을 프로그래밍할 때에도 현실적으로 DBMS마다 여러개의 SQL문을 작성하고 있다.
Mac에서 DBMS 설치
터미널에서 mariadb를 설치한다.
> brew install mariadb
mariadb 시작
brew services start mariadb
'국비 교육' 카테고리의 다른 글
2020.10.19 일자 수업 : 사용 사례, executor 프레임워크 (0) | 2020.10.20 |
---|---|
2020.10.13 일자 수업 : 옵저버 패턴, 네트워크 프로그램 실습 (0) | 2020.10.13 |
2020.10.12 일자 수업 : Observer 디자인 패턴 (0) | 2020.10.12 |