Post

🐹 8. Thread & Lock

15. μŠ€λ ˆλ“œμ™€ 락

[ μžλ°”μ˜ μŠ€λ ˆλ“œ ]

μžλ°”μ˜ λͺ¨λ“  μŠ€λ ˆλ“œλŠ” java.lang.Thread 클래슀 객체에 μ˜ν•΄ μƒμ„±λ˜κ³  μ œμ–΄λœλ‹€.

독립적인 μ‘μš© ν”„λ‘œκ·Έλž¨μ΄ 싀행될 λ•Œ, main() λ©”μ„œλ“œλ₯Ό μ‹€ν–‰ν•˜κΈ° μœ„ν•œ ν•˜λ‚˜μ˜ μ‚¬μš©μž μŠ€λ ˆλ“œ(user thread)κ°€ μžλ™μœΌλ‘œ λ§Œλ“€μ–΄ μ§€λŠ”λ°, 이 μŠ€λ ˆλ“œλ₯Ό μ£Ό μŠ€λ ˆλ“œ(main thread)라고 λΆ€λ₯Έλ‹€.

μžλ°”μ—μ„œ μŠ€λ ˆλ“œλ₯Ό κ΅¬ν˜„ν•˜λŠ” 방법 두 κ°€μ§€λŠ” λ‹€μŒκ³Ό κ°™λ‹€.

  • java.lang.Runnable μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜κΈ°
  • java.lang.Thread μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜κΈ°

Runnable μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜λŠ” 방법

Runnable μΈν„°νŽ˜μ΄μŠ€λŠ” λ‹¨μˆœν•œ ꡬ쑰λ₯Ό 가진닀.

1
2
3
public interface Runnable {
    void run();
}

이 μΈν„°νŽ˜μ΄μŠ€λ₯Ό μ‚¬μš©ν•΄ μŠ€λ ˆλ“œλ₯Ό λ§Œλ“€κ³  μ‚¬μš©ν•˜λ €λ©΄ λ‹€μŒμ˜ 과정을 거쳐야 ν•œλ‹€.

  1. Runnable μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜λŠ” 클래슀λ₯Ό λ§Œλ“ λ‹€. 이 클래슀의 κ°μ²΄λŠ” Runnable 객체가 λœλ‹€.
  2. Thread νƒ€μž…μ˜ 객체λ₯Ό λ§Œλ“€ λ•Œ, Thread의 μƒμ„±μžμ— Runnable 객체λ₯Ό 인자둜 λ„˜κΈ΄λ‹€.

    이 Thread κ°μ²΄λŠ” 이제 run() λ©”μ„œλ“œλ₯Ό κ΅¬ν˜„ν•˜λŠ” Runnable 객체λ₯Ό μ†Œμœ ν•˜κ²Œ λœλ‹€.

  3. 이전 λ‹¨κ³„μ—μ„œ μƒμ„±ν•œ Thread 객체의 start() λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•œλ‹€.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 1번 μˆ˜ν–‰(Runnable μΈν„°νŽ˜μ΄μŠ€ κ΅¬ν˜„)
public class RunnableThreadExample implements Runnable {
    public int count = 0;

    public void run() {
        System.out.println("RunnableThread starting");
        try {
            while (count < 5) {
                Thread.sleep(500);
                count++;
            }
        } catch (InterruptedExceptiono exc) {
            System.out.println("RunnableThread interrupted.");
        }
        System.out.println("RunnableThread terminating.");
    }
}

public static void main(String[] args) {
    RunnableThreadExample instance = new RunnableThreadExample();
    Thread thread = new Thread(instance);  // 2번 μˆ˜ν–‰
    thread.start();  // 3번 μˆ˜ν–‰		

    /* μŠ€λ ˆλ“œ κ°œμˆ˜κ°€ 5κ°œκ°€ 될 λ•ŒκΉŒμ§€ 천천히 κΈ°λ‹€λ¦°λ‹€. */
    while (instance.count != 5) {
        try {
            Thread.sleep(250);
        } catch (InterruptedException exc) {
            exc.printStackTrace();
        }
    }
}

μœ„ μ½”λ“œμ—μ„œ μ‹€μ œλ‘œ ν•΄μ•Όν•˜λŠ” 일은 run() λ©”μ„œλ“œλ₯Ό κ΅¬ν˜„ν•˜λŠ” 것 뿐이닀. 그러면 main λ©”μ„œλ“œλŠ” ν•΄λ‹Ή 클래슀의 μΈμŠ€ν„΄μŠ€(instance)λ₯Ό new Thread(obj)의 인자둜 λ„˜κΈ°κ³  start()λ₯Ό ν˜ΈμΆœν•œλ‹€.

Thread 클래슀 상속

Runnable 이외에 Thread 클래슀λ₯Ό μƒμ†λ°›μ•„μ„œ μŠ€λ ˆλ“œλ₯Ό λ§Œλ“€ μˆ˜λ„ μžˆλ‹€. 그러렀면 거의 항상 run() λ©”μ„œλ“œλ₯Ό μ˜€λ²„λΌμ΄λ“œ(override)ν•΄μ•Ό ν•˜λ©°, ν•˜μœ„ 클래슀의 μƒμ„±μžλŠ” μƒμœ„ 클래슀의 μƒμ„±μžλ₯Ό λͺ…μ‹œμ μœΌλ‘œ ν˜ΈμΆœν•΄μ•Ό ν•œλ‹€.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* μŠ€λ ˆλ“œ 상속 */
public class ThreadExample extends Thread {
    int count = 0;

    public void run() {
        System.out.println("Thread starting");
        try {
            while (count < 5) {
                Thread.sleep(500);
                System.out.println("In Thread, count is " + count);
                count++;
            }
        } catch (InterruptedException exc) {
            System.out.println("Thread interrupted.");
        }
        System.out.println("Thread terminating");
    }
}

public class ExampleB {
    public static void main(String args[]) {
        ThreadExample instance = new ThreadExample();
        instance.start();

        while (instance.count != 5) {
            try {
                Thread.sleep(250);
            } catch (InterruptedException exc) {
                exc.printStackTrace();
            }
        }
    }
}

Runnable μΈν„°νŽ˜μ΄μŠ€ κ΅¬ν˜„κ³Ό λ‹€λ₯Έ 점은 μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜λŠ” λŒ€μ‹  Thread 클래슀λ₯Ό μƒμ†λ°›μ•˜κ³ , λ”°λΌμ„œ μΈμŠ€ν„΄μŠ€ μžμ²΄μ—μ„œ start()λ₯Ό 직접 ν˜ΈμΆœν•˜κ²Œ λœλ‹€.

Thread 상속 vs Runnable μΈν„°νŽ˜μ΄μŠ€ κ΅¬ν˜„

μŠ€λ ˆλ“œλ₯΄ 생성할 λ•Œ Runnable μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜λŠ” 것이 Threadλ₯Ό μƒμ†λ°›λŠ” 것보닀 μ„ ν˜Έλ˜λŠ” μ΄μœ κ°€ 두 가지 μ‘΄μž¬ν•œλ‹€.

  • μžλ°”λŠ” 닀쀑 상속(multiple inheritance)λ₯Ό μ§€μ›ν•˜μ§€ μ•ŠλŠ”λ‹€. λ”°λΌμ„œ Thread 클래슀λ₯Ό μƒμ†ν•˜κ²Œ 되면 ν•˜μœ„ ν΄λž˜μŠ€λŠ” λ‹€λ₯Έ 클래슀λ₯Ό 상속할 μˆ˜κ°€ μ—†λ‹€. ν•˜μ§€λ§Œ Runnable μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜λŠ” ν΄λž˜μŠ€λŠ” λ‹€λ₯Έ 클래슀λ₯Ό 상속할 수 μžˆλ‹€.
  • Thread 클래슀의 λͺ¨λ“  것을 μƒμ†λ°›λŠ” 것이 λ„ˆλ¬΄ λΆ€λ‹΄λ˜λŠ” κ²½μš°μ—λŠ” Runnable을 κ΅¬ν˜„ν•˜λŠ” 편이 λ‚˜μ„μ§€λ„ λͺ¨λ₯Έλ‹€.

[ 동기화와 락 ]

ν•œ ν”„λ‘œμ„ΈμŠ€ μ•ˆμ—μ„œ μƒμ„±λœ μŠ€λ ˆλ“œλ“€μ€ 같은 λ©”λͺ¨λ¦¬ 곡간을 κ³΅μœ ν•œλ‹€. μŠ€λ ˆλ“œκ°€ μ„œλ‘œ 데이터λ₯Ό κ³΅μœ ν•  수 μžˆλ‹€λŠ” 점은 μž₯점이긴 ν•˜μ§€λ§Œ 두 μŠ€λ ˆλ“œκ°€ 같은 μžμ›μ„ λ™μ‹œμ— λ³€κ²½ν•˜λŠ” κ²½μš°μ—λŠ” λ¬Έμ œκ°€ λœλ‹€.

μžλ°”λŠ” 곡유 μžμ›μ— λŒ€ν•œ 접근을 μ œμ–΄ν•˜κΈ° μœ„ν•œ 동기화(synchronization) 방법을 μ œκ³΅ν•œλ‹€.

synchronized와 Lockμ΄λΌλŠ” ν‚€μ›Œλ“œλŠ” 동기화 κ΅¬ν˜„μ„ μœ„ν•œ 기본이 λœλ‹€.

λ™κΈ°ν™”λœ λ©”μ„œλ“œ

ν†΅μƒμ μœΌλ‘œ synchronized ν‚€μ›Œλ“œλ₯Ό μ‚¬μš©ν•  λ•ŒλŠ” 곡유 μžμ›μ— λŒ€ν•œ 접근을 μ œμ–΄ν•œλ‹€.

이 ν‚€μ›Œλ“œλŠ” λ©”μ„œλ“œμ— μ μš©ν•  μˆ˜λ„ 있고, νŠΉμ •ν•œ μ½”λ“œ 블둝에 μ μš©ν•  μˆ˜λ„ μžˆλ‹€. λ˜ν•œ μ—¬λŸ¬ μŠ€λ ˆλ“œκ°€ 같은 객체λ₯Ό λ™μ‹œμ— μ‹€ν–‰ν•˜λŠ” 것 λ˜ν•œ 방지해주쀀닀.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class MyClass extends Thread {
    private String name;
    private MyObject myObj;

    public MyClass(MyObject obj, String n) {
        name = n;
        myObj = obj;
    }

    public void run() {
        myObj.foo(name);
    }
}

public class MyObject {
    public **synchronized** void foo(String name) {
        try {
            System.out.println("Thread" + name + ".foo(): starting");
            Thread.sleep(3000);
            System.out.println("Thread" + name + ".foo(): ending");
        } catch (InterruptedException exc) {
            System.out.println("Thread" + name + ": interrupted.");
        }
    }
}

두 개의 MyClass μΈμŠ€ν„΄μŠ€κ°€ fooλ₯Ό λ™μ‹œμ— ν˜ΈμΆœν•  수 μžˆμ„κΉŒ? 상황에 따라 λ‹€λ₯΄λ‹€.

같은 MyObject μΈμŠ€ν„΄μŠ€λ₯Ό 가리킀고 μžˆλ‹€λ©΄ λ™μ‹œ ν˜ΈμΆœμ€ λΆˆκ°€λŠ₯ν•˜μ§€λ§Œ λ‹€λ₯Έ μΈμŠ€ν„΄μŠ€λ₯Ό 가리킀고 μžˆλ‹€λ©΄ κ°€λŠ₯ν•˜λ‹€.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* μ„œλ‘œ λ‹€λ₯Έ 객체인 경우 λ™μ‹œμ— MyObject.foo() 호좜이 κ°€λŠ₯ν•˜λ‹€. */
MyObject obj1 = new MyObject();
MyObject obj2 = new MyObject();
MyClass thread1 = new MyClass(obj1, "1");
MyClass thread2 = new MyClass(obj2, "2");
thread1.start();
thread2.start();

/* 같은 objλ₯Ό 가리킀고 μžˆλŠ” κ²½μš°μ—λŠ” ν•˜λ‚˜λ§Œ fooλ₯Ό ν˜ΈμΆœν•  수 있고, 
 * λ‹€λ₯Έ ν•˜λ‚˜λŠ” 기닀리고 μžˆμ–΄μ•Ό ν•œλ‹€. */
MyObject obj = new MyObject();
MyClass thread1 = new MyClass(obj, "1");
MyClass thread2 = new MyClass(obj, "2");
thread1.start();
thread2.start();

정적 λ©”μ„œλ“œ(static method)λŠ” 클래슀 락(class lock)에 μ˜ν•΄ 동기화 λœλ‹€. 같은 ν΄λž˜μŠ€μ— μžˆλŠ” λ™κΈ°ν™”λœ 정적 λ©”μ„œλ“œλŠ” 두 μŠ€λ ˆλ“œμ—μ„œ λ™μ‹œμ— 싀행될 수 μ—†λ‹€. 섀사 ν•˜λ‚˜λŠ” fooλ₯Ό ν˜ΈμΆœν•˜κ³  λ‹€λ₯Έ ν•˜λ‚˜λŠ” barλ₯Ό ν˜ΈμΆœν•œλ‹€κ³  해도 말이닀.

1
2
3
4
5
6
7
8
9
10
11
12
public class MyClass extends Thread {
    ...
    public void run() {
        if (name.equals("1")) MyObject.foo(name);
        else if (name.equals("2")) MyObject.bar(name);
    }
}

public class MyObject {
    public static synchronized void foo(String name) { /* ... */ }
    public static synchronized void bar(String name) { /* ... */ }
}

이 μ½”λ“œλ₯Ό μ‹€ν–‰ν–ˆμ„ λ•Œ 좜λ ₯λ˜λŠ” κ²°κ³ΌλŠ” λ‹€μŒκ³Ό κ°™λ‹€.

1
2
3
4
Thread 1.foo(): starting
Thread 1.foo(): ending
Thread 2.bar(): starting
Thread 2.bar(): ending

λ™κΈ°ν™”λœ 블둝

이와 λΉ„μŠ·ν•˜κ²Œ, νŠΉμ •ν•œ μ½”λ“œ 블둝을 동기화할 μˆ˜λ„ μžˆλ‹€. μ΄λŠ” λ©”μ„œλ“œλ₯Ό λ™κΈ°ν™”ν•˜λŠ” 것과 μ•„μ£Ό λΉ„μŠ·ν•˜κ²Œ λ™μž‘ν•œλ‹€.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyClass extends Thread {
    ...
    public void run() {
        myObj.foo(name);
    }
}

public class MyObject {
    public void foo(String name) {
        **synchronized**(this) {
            ...
        }
    }
}

λ©”μ„œλ“œλ₯Ό λ™κΈ°ν™”ν•˜λŠ” 것과 λ§ˆμ°¬κ°€μ§€λ‘œ, MyObject μΈμŠ€ν„΄μŠ€ ν•˜λ‚˜λ‹Ή ν•˜λ‚˜μ˜ μŠ€λ ˆλ“œ 만이 synchronized 블둝 μ•ˆμ˜ μ½”λ“œλ₯Ό μ‹€ν–‰ν•  수 μžˆλ‹€. λ‹€μ‹œ 말해 thread1κ³Ό thread2κ°€ λ™μΌν•œ MyObject μΈμŠ€ν„΄μŠ€λ₯Ό κ°–κ³  μžˆλ‹€λ©΄, κ·Έ κ°€μš΄λ° ν•˜λ‚˜λ§Œ κ·Έ μ½”λ“œ 블둝을 μ‹€ν–‰ν•  수 μžˆλ‹€.

락

μ’€ 더 μ„Έλ°€ν•˜κ²Œ 동기화λ₯Ό μ œμ–΄ν•˜κ³  싢을 λ–„λŠ” 락(lock)을 μ‚¬μš©ν•œλ‹€. 락(λͺ¨λ‹ˆν„°(monitor)라고도 ν•œλ‹€)을 곡유 μžμ›μ— 뢙이면 ν•΄λ‹Ή μžμ›μ— λŒ€ν•œ 접근을 동기화할 수 μžˆλ‹€.

μŠ€λ ˆλ“œκ°€ ν•΄λ‹Ή μžμ›μ„ μ ‘κ·Όν•˜λ €λ©΄ μš°μ„  κ·Έ μžμ›μ— λΆ™μ–΄ μžˆλŠ” 락을 νšλ“ν•΄μ•Ό ν•œλ‹€. νŠΉμ • μ‹œμ μ— 락을 μ₯κ³  μžˆμ„ 수 μžˆλŠ” μŠ€λ ˆλ“œλŠ” ν•˜λ‚˜λΏμ΄λ‹€. λ”°λΌμ„œ ν•΄λ‹Ή κ³΅μœ μžμ›μ€ ν•œ λ²ˆμ— ν•œ μŠ€λ ˆλ“œλ§Œμ΄ μ‚¬μš©ν•  수 μžˆλ‹€.

μ–΄λ–€ μžμ›μ΄ ν”„λ‘œκ·Έλž¨ λ‚΄μ˜ μ΄κ³³μ €κ³³μ—μ„œ μ‚¬μš©λ˜μ§€λ§Œ ν•œ λ²ˆμ— ν•œ μŠ€λ ˆλ“œλ§Œ μ‚¬μš©ν•˜λ„λ‘ λ§Œλ“€κ³ μž ν•  λ•Œ 주둜 락을 μ΄μš©ν•œλ‹€.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class LockedATM {
    private Lock lock;
    private int balance = 100;

    public LockedATM() {
        lock = new ReentrantLock();
    }

    public int withdraw(int value) {
        lock.lock();
        int temp = balance;
        try {
            Thread.sleep(100);
            temp = temp - value;
            Thread.sleep(100);
            balance = temp;
        } catch (InterruptedExceptino e) {
        lock.unlock();
        return temp;
    }

    public int deposit(int value) {
        lock.lock();
        int temp = balancee;
        try {
            Thread.sleep(100);
            temp = temp + value;
            Thread.sleep(300);
            balance = temp;
        } catch (InterruptedException e) { }
        lock.unlock();
        return temp;
    }
}

락을 μ‚¬μš©ν•˜λ©΄ 곡유된 μžμ›μ΄ 예기치 μ•Šκ²Œ λ³€κ²½λ˜λŠ” 일을 막을 수 μžˆλ‹€.

(Thread.sleep은 λ°œμƒ κ°€λŠ₯ν•œ λ¬Έμ œμ μ„ 보이기 μœ„ν•΄ μΌλΆ€λŸ¬ 느리게 λ§Œλ“ κ²ƒ - μ‚¬μš© μ•ˆν•΄λ„λ¨)

[ κ΅μ°©μƒνƒœμ™€ κ΅μ°©μƒνƒœ 방지 ]

κ΅μ°©μƒνƒœ(deadlock)

첫 번째 μŠ€λ ˆλ“œλŠ” 두 번째 μŠ€λ ˆλ“œκ°€ λ“€κ³  μžˆλŠ” 객체의 락이 풀리기λ₯Ό 기닀리고 있고, 두 번째 μŠ€λ ˆλ“œ μ—­μ‹œ 첫 번째 μŠ€λ ˆλ“œκ°€ λ“€κ³  μžˆλŠ” 객체의 락이 풀리기λ₯Ό κΈ°λ‹€λ¦¬λŠ” 상황(μ—¬λŸ¬ μŠ€λ ˆλ“œκ°€ κ΄€κ³„λ˜μ–΄ μžˆμ„ λ•Œ λ°œμƒν•  수 μžˆλ‹€.)

λͺ¨λ“  μŠ€λ ˆλ“œκ°€ 락이 풀리기λ₯Ό 기닀리고 있기 λ•Œλ¬Έμ—, λ¬΄ν•œ λŒ€κΈ° μƒνƒœμ— λΉ μ§€κ²Œ λœλ‹€.

ꡐ착 μƒνƒœκ°€ λ°œμƒν•˜λ €λ©΄, λ‹€μŒμ˜ λ„€ 가지 쑰건이 λͺ¨λ‘ μΆ©μ‘±λ˜μ–΄μ•Ό ν•œλ‹€.

  • μƒν˜Έ 배제(mutual exclusion)

    : ν•œ λ²ˆμ— ν•œ ν”„λ‘œμ„ΈμŠ€λ§Œ 곡유 μžμ›μ„ μ‚¬μš©ν•  수 μžˆλ‹€.(μ’€ 더 μ •ν™•νžˆ μ΄μ•ΌκΈ°ν•˜μžλ©΄, 곡유 μžμ›μ— λŒ€ν•œ μ ‘κ·Ό κΆŒν•œμ΄ μ œν•œλœλ‹€. μžμš°λ„ˆμ˜ 양이 μ œν•œλ˜μ–΄ μžˆλ”λΌλ„(ν•œ 개 이상) κ΅μ°©μƒνƒœλŠ” λ°œμƒν•  수 μžˆλ‹€.)

  • λ“€κ³  기닀리기(hold and wait)

    : 곡유 μžμ›μ— λŒ€ν•œ μ ‘κ·Ό κΆŒν•œμ„ κ°–κ³  μžˆλŠ” ν”„λ‘œμ„ΈμŠ€κ°€, κ·Έ μ ‘κ·Ό κΆŒν•œμ„ μ–‘λ³΄ν•˜μ§€ μ•Šμ€ μƒνƒœμ—μ„œ λ‹€λ₯Έ μžμ›μ— λŒ€ν•œ μ ‘κ·Ό κΆŒν•œμ„ μš”κ΅¬ν•  수 μžˆλ‹€.

  • μ„ μ·¨(preemption) λΆˆκ°€λŠ₯

    : ν•œ ν”„λ‘œμ„ΈμŠ€κ°€ λ‹€λ₯Έ ν”„λ‘œμ„ΈμŠ€μ˜ μžμ› μ ‘κ·Ό κΆŒν•œμ„ κ°•μ œλ‘œ μ·¨μ†Œν•  수 μ—†λ‹€.

  • λŒ€κΈ° μƒνƒœμ˜ 사이클(circular wait)

    : 두 개 μ΄μƒμ˜ ν”„λ‘œμ„ΈμŠ€κ°€ μžμ› 접근을 κΈ°λ‹€λ¦¬λŠ”λ°, κ·Έ 관계에 사이클(cycle)이 μ‘΄μž¬ν•œλ‹€.

ꡐ착 μƒνƒœλ₯Ό λ°©μ§€ν•˜κΈ° μœ„ν•΄μ„  μœ„ 쑰건듀 쀑 ν•˜λ‚˜λ₯Ό μ œκ±°ν•˜λ©΄ λœλ‹€. ν•˜μ§€λ§Œ 이듀 쑰건 κ°€μš΄λ° μƒλ‹Ήμˆ˜λŠ” 만쑱되기 μ–΄λ €μš΄ 것이라 κΉŒλ‹€λ‘­λ‹ˆλ‹€.

곡유 μžμ› 쀑 λ§Žμ€ κ²½μš°κ°€ ν•œ λ²ˆμ— ν•œ ν”„λ‘œμ„ΈμŠ€λ§Œ μ‚¬μš©ν•  수 있기 λ•Œλ¬Έμ—(ex. ν”„λ¦°ν„°) 1번 쑰건은 μ œκ±°ν•˜κΈ° μ–΄λ ΅λ‹€. λŒ€λΆ€λΆ„μ˜ κ΅μ°©μƒνƒœ 방지 μ•Œκ³ λ¦¬μ¦˜μ€ 4번 쑰건, 즉 λŒ€κΈ° μƒνƒœμ˜ 사이클이 λ°œμƒν•˜λŠ” 일을 λ§‰λŠ” 데 초점이 맞좰져 μžˆλ‹€.

This post is licensed under CC BY 4.0 by the author.