2020-10-20 TIL
스레드풀 구현
- mini-pms 36
스레드풀
- 스레드를 풀링 기법(pooling)을 이용하여 관리한다.
- 스레드를 사용한 후에 버리지 않고 재사용하여 가비지 생성을 줄인다.
풀링 기법(pooling)
- 사용한 객체를 버리지 않고 보관해 두었다가 재사용하는 객체 관리 기법이다.
- 동일한 객체를 자주 생성하고,
- 생성한 객체를 쓰고 버리는 상황에서 대량의 가비지가 생성되는 경우에 적합한다.
- GoF의 ‘디자인 패턴’에서 Flyweight 패턴의 한 예이다.
01) 스레드풀 정의
- notify() 와 wait() 는 싱크로되지 않은 곳(막혀있지 않은 곳)에서 사용하면 에러가 뜬다.
- synchronized (대상)
- 에외처리
@Override
public void run() {
while (true) {
// try-catch를 while 안에 넣으면 task.run()을 수행하다가 예외가 발생했을 경우 처리할 수 없다.
// 스레드는 유효하다.
try {
this.wait();
} catch (Exception e) {
System.out.println("스레드 실행 중 오류 발생");
break;
}
task.run();
}
}
- 스레드풀 만들기(java.util 따라하기)
public class ThreadPool {
List<Worker> workers = new ArrayList<>();
// 스레드 + 작업을 맡기고 + 깨우는 기능 + 작업 완료 후 잠자는 기능
class Worker extends Thread {
Runnable task;
public void setTask(Runnable task) {
// 스레드가 할 작업을 배정한 후
this.task = task;
synchronized (this) {
// 스레드를 깨운다.
this.notify();
}
}
@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());
// 이 스레드 객체에 대해 대기 상태로 있다가
// 이 스레드에게 깨어나라는 알림(notify()/notifyAll()이 온다면
// 즉시 runninig 상태로 되돌아간다.
} 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());
}
}
}
}
}
public void execute(Runnable task) {
Worker t;
if (workers.size() == 0) {
// 만약 스레드가 없다면 스레드를 새로 생성한다.
t = new Worker();
System.out.printf("[%s] - 스레드 생성\n", t.getName());
// 그리고 즉시 실행한다.
// - 실행하더라도 스레드 스스로 대기 상태로 갈 것이다.
t.start();
// 현재 main 스레드를 잠깐 멈추게 하여
// 새로 만든 스레드가 실행할 틈을 주자.
// 그래야만 새로 만든 스레드가 실행하자마자 대기 상태로 간다.
try {
Thread.sleep(20);
} catch (Exception e) {
// sleep() 중 발생한 예외는 무시한다.
}
} else {
// 만약 스레드가 있다면 스레드풀에서 스레드를 한 개 꺼낸다.
// - 스레드풀에서 꺼낸 스레드는 현재 대기 상태이다.
t = workers.remove(0);
System.out.printf("[%s] - 스레드 재사용\n", t.getName());
}
// 스레드를 깨워서 일을 시킨다.
// - 스레드에게 해야 할 작업을 배정하면 된다.
t.setTask(task);
}
}
02) 클라이언트가 접속하면 요청 처리를 ThreadPool에게 맡긴다.
- 같은 코드인데 정상 실행이 됐다가 안됐다가 하는것은 비동기 때문이다.
- 비동기에서는 누가 먼저 실행되는지 정확하게 알아야 한다.
- 소켓은 클라이언트가 연결되었다가 끊어지는 순간에 사라지기 때문에 재사용이 불가능하다.
- 스레드가 몇개의 작업을 처리할 수 있는지는 컴퓨터의 cpu 성능에 따라 다르다.
- 스레드를 몇개 생성할 수 있는지는 메모리에 따라 다르다.
- 메인 스레드가 죽어도 나머지 스레드(스레드풀)가 살아 있으면 종료되지 않는다.
public class ServerApp {
static boolean stop = false;
// 스레드풀 준비
ThreadPool threadPool = new ThreadPool();
static Map<String,Object> context = new Hashtable<>();
List<ApplicationContextListener> listeners = new ArrayList<>();
public void addApplicationContextListener(ApplicationContextListener listener) {
listeners.add(listener);
}
public void removeApplicationContextListener(ApplicationContextListener listener) {
listeners.remove(listener);
}
private void notifyApplicationContextListenerOnServiceStarted() {
for (ApplicationContextListener listener : listeners) {
listener.contextInitialized(context);
}
}
private void notifyApplicationContextListenerOnServiceStopped() {
for (ApplicationContextListener listener : listeners) {
listener.contextDestroyed(context);
}
}
public void service(int port) {
notifyApplicationContextListenerOnServiceStarted();
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("서버 실행 중...");
while (true) {
Socket clientSocket = serverSocket.accept();
if (stop) {
break;
}
// 스레드풀로 변경
threadPool.execute(() -> handleClient(clientSocket));
}
} catch (Exception e) {
e.printStackTrace();
}
notifyApplicationContextListenerOnServiceStopped();
}
.
.
.
}
03) 스레드풀
package com.eomcs.util.concurrent;
import java.util.ArrayList;
public class ThreadPool {
// 스레드풀의 종료 상태
boolean stopping = false;
// 스레드 목록을 담는 리스트
ArrayList<Worker> workers = new ArrayList<>();
// 작업을 수행하는 스레드
private class Worker extends Thread {
Runnable task;
public void setTask(Runnable task) {
this.task = task;
synchronized (this) {
this.notify();
}
}
@Override
public void run() {
synchronized (this) {
try {
while (true) {
this.wait();
// 스레드풀의 상태가 종료하는 상태라면, 스레드 실행을 멈춘다.
if (ThreadPool.this.stopping)
break;
task.run();
workers.add(this);
System.out.println("스레드풀로 되돌아 감!");
}
} catch (Exception e) {
System.out.printf("%s 스레드에서 오류 발생! - 스레드풀에서 제거.\n", this.getName());
workers.remove(this);
}
}
}
}
public void execute(Runnable task) {
Worker t = null;
if (workers.isEmpty()) {
t = new Worker();
t.start();
System.out.printf("새 스레드(%s) 생성 및 실행!\n", t.getName());
} else {
t = workers.remove(0);
System.out.printf("기존 스레드(%s) 사용!\n", t.getName());
}
t.setTask(task);
}
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();
}
}
}
- 내가 타이핑한거.
public class ThreadPool {
// 스레드풀의 종료 상태
boolean stopping = false;
List<Worker> workers = new ArrayList<>();
class Worker extends Thread {
Runnable task;
public void setTask(Runnable task) {
this.task = task;
synchronized (this) {
this.notify();
}
}
@Override
public void run() {
synchronized (this) {
while (true) {
try {
System.out.printf("[%s] - 스레드 대기 중...\n", this.getName());
this.wait();
if (ThreadPool.this.stopping) { // 스레드풀이 종료 상태라면
// 스레드는 깨어나는 즉시 실행을 멈춘다.
break;
}
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());
}
}
}
}
}
public void execute(Runnable task) {
if (stopping) {
// RuntimeException을 사용하면 public void execute(Runnable task) 에서 예외를 던지지 않아도 된다.
throw new RuntimeException("스레드풀이 종료 상태입니다.");
}
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);
}
public void shutdown() {
try {
this.stopping = true;
while (!workers.isEmpty()) { // 스레드풀에 대기 중인 스레드가 있다면
Worker worker = workers.remove(0); // 맨 앞에 있는 스레드를 꺼내서
synchronized (worker) {
// 스레드를 깨운다.
// 스레드는 깨어나면 stopping 상태에 따라 종료 여부를 결정하도록 프로그래밍 되어있다.
// worker 스레드를 보라.
worker.notify();
}
}
// 아직 스레드풀에서 대기하지 않고 현재 작업을 수행하는 스레드가 있을 수 있다.
// 그 스레드가 작업을 끝낼 때까지 좀 기다리자
Thread.sleep(2000);
// 다시 한 번 대기하고 있는 스레드를 종료해보자.
while (!workers.isEmpty()) { // 스레드풀에 대기 중인 스레드가 있다면
Worker worker = workers.remove(0); // 맨 앞에 있는 스레드를 꺼내서
synchronized (worker) {
// 스레드를 깨운다.
// 스레드는 깨어나면 stopping 상태에 따라 종료 여부를 결정하도록 프로그래밍 되어있다.
// worker 스레드를 보라.
worker.notify();
}
}
// 예외 처리를 한다.
} catch (Exception e) {
System.out.println("스레드풀을 종료하는 중에 예외 발생");
e.printStackTrace();
}
}
}
36-b
스레드풀을 자바에서 제공하는 것으로 변경
public class ServerApp {
static boolean stop = false;
// 스레드풀 준비
ExecutorService threadPool = Executors.newCachedThreadPool();
static Map<String,Object> context = new Hashtable<>();
List<ApplicationContextListener> listeners = new ArrayList<>();
public void addApplicationContextListener(ApplicationContextListener listener) {
listeners.add(listener);
}
public void removeApplicationContextListener(ApplicationContextListener listener) {
listeners.remove(listener);
}
private void notifyApplicationContextListenerOnServiceStarted() {
for (ApplicationContextListener listener : listeners) {
listener.contextInitialized(context);
}
}
private void notifyApplicationContextListenerOnServiceStopped() {
for (ApplicationContextListener listener : listeners) {
listener.contextDestroyed(context);
}
}
public void service(int port) {
notifyApplicationContextListenerOnServiceStarted();
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("서버 실행 중...");
while (true) {
Socket clientSocket = serverSocket.accept();
if (stop) {
break;
}
threadPool.execute(() -> handleClient(clientSocket));
}
} catch (Exception e) {
e.printStackTrace();
}
notifyApplicationContextListenerOnServiceStopped();
// 스레드풀을 종료한다.
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("모든 작업을 강제 종료했다.");
}
}
System.out.println("main() 종료!");
} catch (Exception e) {
// 예외는 무시한다.
}
}
.
.
.
}
DBMS
개발자가 직접 데이터 관리를 위해 파일을 조작할 때
- 임의의 위치에 있는 값을 변경?
- 특정 조건에 해당하는 데이터를 찾기?
- 특정 조건의 데이터 삭제?
- 대량의 데이터를 다루기 쉽게 어떤 기법으로 여러 파일로 분산할 것이냐?
- 데이터와 데이터 간의 관계를 어떻게 관리할 것이냐?
- 유효하지 않은 데이터 조작
- 많은 코딩, 중복 개발 => Data 관리 전문 S/W => DBMS
- 상용으로 쓸 수 없다.
DBMS
- 데이터 관리 전문 소프트웨어
- DataBase Management System
- 예)
- (외산) Oracle, My-SQL, MS-SQL, DB2,
- (국산) Altibase, Cubrid, Tibero
- MariaDB <- My-SQL 창시자
- Relational DBMS -> 데이터와 데이터 사이의 관계를 다루기 쉽게
- Object-Relational
- O-RDBMS -> PostgreSQL
- Object-Relational
- NoSQL-DBMS
- MongoDB <- BigData : 자잘하게 갯수가 많은 데이터
- https://poiemaweb.com/mongdb-basics
- 서로 연관이 없으면서 데이터 불륨이 큰 빅데이터를 다루기 위해서 사용한다.
- Data 저장 / 조회
- 임의의 위치에 있는 Data 변경 / 삭제 / 검색
- Client 인증 / 권한 관리
- Data 간의 관계를 제어
- -> 결함이 생기지 않게 제어(무결성 Integrity)
DBMS 와 SQL
- https://zzsza.github.io/development/2018/03/18/sql-for-everyone/
- https://brunch.co.kr/@minu-log/5
- DBMS 와 Client 의 통신에서 명령을 작성하는 문법이 SQL 이다.
- Structured Query Language
- 특정 DBMS로부터 독립적으로 사용한다.
- Select, From, Where, Group by, Having, Limit
표준 SQL + α
- 버젼별 표준 문법이 있다.
- DBMS 마다 지원하는 버젼에 차이가 있다.
- 모든 DBMS 가 SQL 표준을 완벽하게 지원하지 못한다.
- 또는 DBMS 마다 지원하는 SQL 버젼도 다르다
- 같은 DBMS라도 버젼마다 지원하는 SQL 버젼이 다르다.
- -> 개발자는 사용할 DBMS에 맞춰 SQL을 작성해야 한다.
SQL 과 개발 현실
- SQL 문 = 표준 SQL + DBMS 전용 문법
- -> 1) 같은 Vendor의 DBMS(같은 제품)라 하더라도 버젼마다 사용할 수 있는 문법이 차이가 난다.
- -> 2) DBMS가 다른 Vendor의 제품이라면 전용 문법이 다르기 때문에 동작이 안될 수 있다.
- -> 그렇다면 표준 SQL을 사용?
- DBMS 마다 사용 가능한 문법이 다를 수 있다.
- DBMS 전용 문법은 DBMS 성능을 극한으로 이용
- -> 표준 SQL만 사용하면 이런 이점을 누릴 수 없다.
- 개발자는 DBMS 상황에 맞춰 SQL을 변경해줘야한다.
DBMS 설치 (eomcs-docs-dbms)
- MariaDB <-> (접속하는 프로그램) MySql
- MariaDB 설치
[windows OS]
scoop 패키지 관리자를 이용하여 mariadb 설치
> scoop install mariadb
서비스에 등록
mysqld --install "서비스명"
> mysqld --install "mariadb"
서비스 시작
net start 서비스명
> net start mariadb (windows)
서비스 종료
net stop 서비스명
> net stop mariadb
[mac]
mariadb 설치
> brew install mariadb
mariadb 실행
> brew services start mariadb
> brew services stop mariadb
또는
> mysql.server start
> mysql.server stop
[linux]
mariadb 설치
> sudo apt update
> sudo apt install mariadb-server
> sudo systemctl status mariadb
> mysql -V
mariadb 설치 후 root 암호 변경
> sudo mysql_secure_installation
mariadb 실행
> service mariadb start
> service mariadb stop
> service mariadb status
- mysql 서버에 접속하기
로컬 MySQL 서버에 접속
mysql -u root -p Enter password: 암호입력
원격 MySQL 서버에 접속
mysql -h 서버주소 -u root -p Enter password: 암호입력
- mysql root 암호 변경
alter user ‘root’@’localhost’ identified by ‘1111’;
- MySQL 사용자 추가
CREATE USER ‘사용자아이디’@’서버주소’ IDENTIFIED BY ‘암호’;
로컬에서만 접속할 수 있는 사용자를 만들기:
CREATE USER ‘study’@’localhost’ IDENTIFIED BY ‘1111’; => 이 경우 stidu 사용자는 오직 로컬(서버를 실행하는 컴퓨터)에서만 접속 가능한다. => 다른 컴퓨터에서 실행하는 MySQL 서버에 접속할 수 없다는 것을 의미한다.
원격에서만 접속할 수 있는 사용자를 만들기:
CREATE USER ‘study’@’%’ IDENTIFIED BY ‘1111’; => 이 경우 study 사용자는 원력에서만 접속 가능하다.
- MySQL 사용자 목록 조회
select user, host from 데이터베이스명.테이블명; select user, host from mysql.user;
- MySQL 데이터베이스 생성
mariadb에서는 default 키워드를 사용하지 않는다.
CREATE DATABASE 데이터베이스명 CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE studydb CHARACTER SET utf8 COLLATE utf8_general_ci;
- MySQL 사용자에게 데이터베이스 사용 권한 부여
GRANT ALL ON 데이터베이스명.* TO ‘사용자아이디’@’서버주소’; GRANT ALL ON studydb.* TO ‘study’@’localhost’;
- 데이터베이스 목록 조회
show databases;
- 사용자 교체
quit or exit (프로그램 종료 후) mysql -u study -p (다시 실행)
- 기본으로 사용할 데이터베이스 지정하기
use 데이터베이스명 use studydb;
- 데이터베이스의 전체 테이블 목록 조회
show tables;
- 연습 ``` 사용자 생성 : user1 user1이 사용할 데이터베이스 생성: user1db
사용자 생성 : user2 user1이 사용할 데이터베이스 생성: user2db
사용자 생성 : user3 user1이 사용할 데이터베이스 생성: user3db
사용자 생성 : user4 user1이 사용할 데이터베이스 생성: user4db
사용자 생성 : user5 user1이 사용할 데이터베이스 생성: user5db
- mariadb 인스톨과 설정
✘ rsh@rsh ~
brew service stop mariadb
Error: Unknown command: service
✘ rsh@rsh ~
brew services stop mariadb
Stopping mariadb… (might take a while)
==> Successfully stopped mariadb (label: homebrew.mxcl.mariadb)
rsh@rsh ~
brew services start mariadb
==> Successfully started mariadb (label: homebrew.mxcl.mariadb)
rsh@rsh ~
mysql -u root -p
Enter password:
ERROR 1698 (28000): Access denied for user ‘root’@’localhost’
✘ rsh@rsh ~
sudo mysql_secure_installation
Password:
Sorry, try again.
Password:
NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MariaDB SERVERS IN PRODUCTION USE! PLEASE READ EACH STEP CAREFULLY!
In order to log into MariaDB to secure it, we’ll need the current password for the root user. If you’ve just installed MariaDB, and haven’t set the root password yet, you should just press enter here.
Enter current password for root (enter for none): OK, successfully used password, moving on…
Setting the root password or using the unix_socket ensures that nobody can log into the MariaDB root user without the proper authorisation.
You already have your root account protected, so you can safely answer ‘n’.
Switch to unix_socket authentication [Y/n] n … skipping.
You already have your root account protected, so you can safely answer ‘n’.
Change the root password? [Y/n] y New password: Re-enter new password: Password updated successfully! Reloading privilege tables.. … Success!
By default, a MariaDB installation has an anonymous user, allowing anyone to log into MariaDB without having to have a user account created for them. This is intended only for testing, and to make the installation go a bit smoother. You should remove them before moving into a production environment.
Remove anonymous users? [Y/n] y … Success!
Normally, root should only be allowed to connect from ‘localhost’. This ensures that someone cannot guess at the root password from the network.
Disallow root login remotely? [Y/n] y … Success!
By default, MariaDB comes with a database named ‘test’ that anyone can access. This is also intended only for testing, and should be removed before moving into a production environment.
Remove test database and access to it? [Y/n] y
- Dropping test database… … Success!
- Removing privileges on test database… … Success!
Reloading the privilege tables will ensure that all changes made so far will take effect immediately.
Reload privilege tables now? [Y/n] y … Success!
Cleaning up…
All done! If you’ve completed all of the above steps, your MariaDB installation should now be secure.
Thanks for using MariaDB!
rsh@rsh ~
brew services stop mariadb
Stopping mariadb… (might take a while)
==> Successfully stopped mariadb (label: homebrew.mxcl.mariadb)
rsh@rsh ~
mysql -u root -p
Enter password:
ERROR 2002 (HY000): Can’t connect to local MySQL server through socket ‘/tmp/mysql.sock’ (2)
✘ rsh@rsh ~
brew services start mariadb
==> Successfully started mariadb (label: homebrew.mxcl.mariadb)
rsh@rsh ~
mysql -u root -p
Enter password:
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 3
Server version: 10.5.6-MariaDB Homebrew
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type ‘help;’ or ‘\h’ for help. Type ‘\c’ to clear the current input statement.
MariaDB [(none)]> ```