개요
- 몇 천개의 url을 검사하는 시스템을 개발해야 하는 일이 생겼다. 간단하게 scheduler를 사용해서 RestTemplate으로 통신 후 StatusCode에 따라 검수하는 시스템을 만들었다. 로컬에서 테스트한 결과 대략 40분이 걸렸다. 시간이 너무 오래걸린다 느껴 url를 10개 list로 나눠서 multi-threading 으로 진행해서 시간을 15분으로 단축시켰다. 하지만 multi-thread로 구현하면서 부족함을 느껴 process/thread를 공부하고 글을 작성하게 되었다.
Process
- OS로 부터 자원(메모리) 를 받아 실행하는 프로그램이다. 실질적으로 작업을 수행하는 것이 Thread 이다.
- 모든 Process는 최소한 하나 이상의 Thread가 존재하면, 둘 이상의 Thread를 가진 Process를 Multi-Threaded Process 라고한다.
Multi-Thread
- Multi-Thread활용된 사례
- 메신저로 채팅하면서 파일을 다운받으며 음성대화를 나눌 수 있는 이유가 Multi-Threading을 사용해서 여러개의 일을 할 수가 있다. 만약, Single-Thread로 작성을 했다면 채팅을 하다가 파일을 다운받으려면 멈추고 파일을 다운하고 다시 채팅을 해야한다.
- 여러 사용자에게 서비스를 해주려면 Multi-Thread로 작성하는게 필수적이다. 그래야 하나의 서버 프로세스가 여러개의 Thread를 생성해 사용자의 요청이 일대일로 처리되서 스무스하게 진행된다.
- 장점
- CPU의 사용률을 향상시킨다.
- 자원을 보다 효율적으로 사용할 수 있다.
- 사용자에 대한 응답성이 향상된다.
- 작업이 분리되어 코드가 간결해진다.
- 단점
- 여러 Thread가 같은 Process에서 자원을 공유해 동기화(synchronization) 문제가 발생될 수 있다.
- 교착상태(deadlock: 두 쓰레드가 자원을 점유한 상태에서 서로 상대편이 점유한 자원을 사용하려고 기다리느라 진행이 멈춰있는 상태)이 발생될 수 있다.
- 결론
- Single-Thread로 프로그램을 작성하면 사용자의 요청 마다 새로운 프로세스를 생성하고 매 프로세스마다 메모리 공간과 생성되는 시간이 소모되 많은 수의 사용자 요청을 처리하기 어렵다. 그래서 이 때 Multi-Thread를 사용하면 시간과 메모리가 절약되고 더 빠른 서비스가 제공된다. 물론 단점(밑에서 설명)도 있다. 그래서, 적절하게 상황에 맞게 고려하고 고려해서 Multi-Thread로 진행할지 Single-Thread로 진행할지 고려해봐야한다.
Thread 구현 방법
- 방법 1: Thread클래스를 상속
class MyThread extends Thread {
public void run () {
} // Thread클래스의 run ()을 오버라이딩
}
public static void main(String[] args) {
Thread thread = new MyThread();
}
Thread를 상속받으면 위와 같이 Thread 객체를 생성할 수 있다.
- 방법 2: Runnable 인터페이스를 구현
class MyThread implements Runnable {
public void run() {
} // Runnable인터페이스의 run()을구현
}
public static void main(String[] args) {
Thread thread = new Thread(new myThread());
}
Runnable 인터페이스를 구현하면 위와 같이 Thread 객체 안에 구현한 Runnable을 넘기면 Thread 객체를 생성할 수 있다. 그 이유는 밑처럼 Thread 클래스 내부를 살펴보면 생성자를 통해서 Runnable객체를 생성자에 넘겨서 생성하게 되어있어서 Runnable인터페이스를 구현 후 new Thread()안에 인자로 구현한 객체를 넘겨서 사용해야한다.
public class Thread {
private Runnable r; // Runnable을 구현한클래스의 인스턴스를 참조하기 위한 변수
public Thread(Runnable r) {
this.r = r;
}
public void run () {
if(r! = null)
r. run () ; // Runnable인터페이스를 구현한 인스턴스의 run ()을 호출
}
...
}
일반적으로 Runnable인터페이스를 구현하는 방법이 재사용성이 높고 코드의 일관성을 유지할 수 있기 때문에 객체지향적인 방법이라 할 수 있다. Thread 클래스에서 특정 함수를 override 하지 않는 이상 인터페이스를 구현하는게 좋다. 상속(extends)은 한 조상클래스밖에 안되기 때문에 다형성을 활용하기 위해선 Runnable 인터페이스를 구현하는게 좋다. 사실상, 두 방법 다 run()함수의 몸통을 채워넣기 위해 해야하는 일이라 위 두가지 방법 중 어떤 방법을 사용해서 Thread를 구현해도 상관없다.
Thread 실행시키기
public class MyThread1 extends Thread {
public void run () {
for(int i=0;i < 25; i++){
System.out.println("Thread 상속");
}
} // Thread클래스의 run ()을 오버라이딩
}
public class MyThread2 implements Runnable{
public void run() {
for(int i=0;i < 25;i++){
System.out.println("Runnable 구현");
}
}
}
public static void main(String[] args) {
Thread thread1 = new MyThread1();
Thread thread2 = new Thread(new MyThread2());
thread1.start(); // 상속 (extends)
thread2.start(); // 인터페이스 구현(implements)
}
- Thread를 생성하면 실행이 바로 되지않는다. 위처럼 Thread객체를 생성하고 start() 를 호출해야 Thread가 실행된다.
- start()가 호출되었다고 바로 실행되지 않는다, 실행 대기 상태 -> 자신의 차례가되야 실행이 된다. (OS의 스케줄러가 작성한 스케줄에의해 실행 순서가 결정)
ThreadExl_l tl = new ThreadExl_l();
tl.start();
tl.start();
- Thread는 한번 실행되면 다시 실행이 불가한다. 다시 실행할 경우 IllegalThreadStateException이 발생한다.
Thread 2개를 생성 후 실행 시키면 위처럼 결과가 나온다. 순서가 보장되지않고 출력이 된다. 위처럼 나오면 Thread가 잘 실행되었다.
결론
- Process는 실행하는 프로그램, Thread는 그 안에 실질적으로 작업을 수행하는 것이다. 개인적으로 필자는 multi-threading을 개발하면서 자주 쓰는거 같지는 않다. 하지만, 분명 개발할 때 한 프로세스안에 동시적인 일을 실행시킬일이 있을 때 multi-threading을 사용한다. Thread를 사용하기 위해서는 적절히 알아야 상황에 맞게 사용할 수 있는 거 같다. 아니면 오히려 독(병목현상, 동시성 문제)이 생길 수 있다. 간단하게 thread는 무엇인가? 와 어떻게 thread를 실행시키나? 를 알아봤다. 다음 글에는 thread 병목현상/동시성 문제 관련해서 글을 작성해야겠다.
'Java' 카테고리의 다른 글
[Spring] 예외처리(ExceptionHandling)하는 방법 (0) | 2023.03.12 |
---|---|
[JAVA] JUnit 5 사용해보자! (0) | 2023.02.27 |
[Spring] @Autowired를 사용하지말고 다양한 의존성 주입을 알아보자 (0) | 2023.01.25 |
[JAVA] SOLID란? - 좋은 객체지향 설계 5가지 원칙 (0) | 2023.01.08 |
[JAVA] 객체 지향이란? - 특징 알고 넘어가기 (0) | 2023.01.07 |