본문 바로가기

Programming/자바

자바 재정리 - 멀티 스레드

프로세스, 스레드
  • 프로세스

: 운영체제에서 실행 중인 하나의 애플리케이션

 

=> 애플리케이션 실행 시 운영체제로부터 메모리 할당받아 애플리케이션의 코드 실행

 

 

 

 

  • 멀티 프로세스

: 운영체제 내 여러 프로세스 / 애플리케이션 단위의 멀티 태스킹

 

=> 운영체제에서 할당받은 각각의 메모리로 실행하기에 프로세스들은 서로 독립적

 

ex. 워드, 엑셀 동시 사용 중, 워드 오류 생겨도 엑셀 사용 가능

 


멀티프로세스⊃프로세스1⊃멀티 스레드
                      ⊃프로세스2⊃싱글 스레드

 

 

 

 

  • 멀티 스레드

: 한 프로세스 내 여러 스레드 (여러 코드 실행 흐름) / 애플리케이션 내부에서의 멀티 태스킹

 

=> 하나의 프로세스 내부에 생성됨 => 한 스레드에서 예외 발생시 전체 프로세스 종료 (다른 스레드 영향)

 

ex. 메신저 내에서 파일전송 스레드 예외 발생 시 채팅 스레드도 함께 종료되며 메신저 프로세스 자체가 종료

 

 


주기억장치 - RAM (메모리)
보조기억장치 - 하드디스크
중앙처리장치 - CPU

 

 

 

 

메인 스레드

: main() 메소드 실행 시 시작되는 스레드 (=메인 메소드)

 

모든 자바 애플리케이션에서 메인 스레드 반드시 존재함

 

작업 스레드 (객체) 생성 -> 병렬로 코드 실행 가능 (동시에 처리하는 것으로 보이지만 실제로는 번갈아가며 처리함)

 

실행 중인 스레드가 하나라도 있으면 프로세스 종료 X

=> 메인 스레드 종료, 작업 스레드 실행 중 -> 프로세스 종료 X

 

 

 

 

작업 스레드 생성, 실행
  • 스레드 생성 전
import java.awt.Toolkit;

public class ThreadEx1 {

    public static void main(String[] args) {
        
        Toolkit toolkit = Toolkit.getDefaultToolkit(); //객체 생성, static
        
        for (int i=1;i<=5;i++) {
           toolkit.beep(); //비프음 발생
            try {
                Thread.sleep(500); //0.5초 일시정지(ms) //try, catch문 필수
            } catch (InterruptedException e) {
            }  
        }
        
        for (int i=1;i<=5;i++) {
            System.out.println("띵!!");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }  
        }
        
    }

}

=> 2개 for문 순차 실행

 

 

 

  • java.lang.Thread 클래스로부터 직접 생성 - 1. 익명 객체 구현
import java.awt.Toolkit;

public class ThreadEx1 {

    public static void main(String[] args) {
    
        //작업 스레드
        Thread thread = new Thread(new Runnable() {
        //작업스레드 생성시 Runnable 인터페이스 구현 객체 만들어 대입     
            @Override
            public void run() {  //run() 재정의해 작업스레드 실행코드 작성
                Toolkit toolkit = Toolkit.getDefaultToolkit(); 
                for (int i=1;i<=5;i++) {
                    toolkit.beep(); 
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                    }  
                }
            }
        });
        thread.start(); //스레드 시작 or 실행 대기 상태(다른 스레드 실행 시)

       //메인 스레드
        for (int i=1;i<=5;i++) {
            System.out.println("띵!!");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }  
        }
        
    }

}

//
띵!!
띵!!
띵!!
띵!!
띵!!
=> 소리, 숫자 동시 실행

 

 

 

  • java.lang.Thread 클래스로부터 직접 생성 - 2. 인터페이스 구현클래스 사용
import java.awt.Toolkit;

public class BeepTask implements Runnable { //Runnable 인터페이스 구현클래스

    @Override
    public void run() {
        
        Toolkit toolkit = Toolkit.getDefaultToolkit(); 
        for (int i=1;i<=5;i++) {
            try {
                toolkit.beep(); 
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }  
        }//for

    }
    
}


import java.awt.Toolkit;

public class ThreadEx2 {

    public static void main(String[] args) {
    
        //작업 스레드
        Runnable bt = new BeepTask();
        //다형성 => Beeptask 생성, Runnable로 선언 => Runnable 내 메소드만 실행됨
        Thread thread = new Thread(bt);
        thread.start();
        
        //메인 스레드
        for (int i=1;i<=5;i++) {
            System.out.println("띵!!");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }  
        }
        
    }

}

 

 

 

 

  • Thread 하위클래스로부터 생성 - 3. Thread 상속
public class BeepThread extends Thread { //Thread 클래스 상속

    @Override
    public void run() {
        
        Toolkit toolkit = Toolkit.getDefaultToolkit(); 
        for (int i=1;i<=5;i++) {
            try {
                toolkit.beep(); 
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }  
        }//for
    }

}


public class ThreadEx3 {

    public static void main(String[] args) {
    
        //작업 스레드
        Thread thread = new BeepThread();
        thread.start();
        
        //메인 스레드
        for (int i=1;i<=5;i++) {
            System.out.println("띵!!");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }  
        }
        
    }

}​

 

 

 

 

스레드 이름

메인스레드 이름 => main

 

작업스레드 생성 시 "Thread-n" 자동 설정 

=> Thread 클래스 setName() 메소드로 변경 가능 (cf. getName() 으로 이름 전달받음)

 

setName(), getName()

=> Thread의 인스턴스 메소드 (스레드 객체 참조 필요)

=> 객체 참조 없이는 Thread 정적 메소드 currentThread() 사용해 현재 실행되는 스레드 참조 얻기 가능

 

 

  •  
 
public class ThreadA extends Thread{

    //생성자
    public ThreadA() {
        setName("ThreadA");  //인스턴스 메소드
    }
    
    @Override
    public void run() {
        for (int i=1;i<=5;i++) {
            System.out.println(getName()+"가 한 작업");
        }
    }
    
}


public class ThreadB extends Thread {

    //기본 생성자
    
    @Override
    public void run() {
        for (int i=1;i<=5;i++) {
            System.out.println(getName()+"가 한 작업"); //Thread-1
        }
    }
    
}


public class ThreadEx4 {

    public static void main(String[] args) {
        
        Thread mainThread = Thread.currentThread();  
        //코드 실행하는 현재 스레드의 참조객체 얻음  //현재의 스레드가 무엇인지 확인
        System.out.println("프로그램 시작 스레드 이름 : "+ mainThread);

        ThreadA ta = new ThreadA();  //스레드 생성
        System.out.println("작업 스레드 이름 : "+ta.getName());
        ta.start();

        ThreadB tb = new ThreadB();
        System.out.println("작업 스레드 이름 : "+tb.getName());
        tb.start();
        
    }

}

//
프로그램 시작 스레드 이름 : Thread[main,5,main]
작업 스레드 이름 : ThreadA
ThreadA가 한 작업
ThreadA가 한 작업
ThreadA가 한 작업
ThreadA가 한 작업
ThreadA가 한 작업
작업 스레드 이름 : Thread-1
Thread-1가 한 작업
Thread-1가 한 작업
Thread-1가 한 작업
Thread-1가 한 작업
Thread-1가 한 작업

 

 

 

 

스레드 우선순위

스레드 개수가 코어의 수보다 많을 때, 스레드를 어떤 순서로 실행할지 결정 => 스레드 스케줄링

-> 번갈아가며 스레드들의 run() 메소드 조금씩 실행

 

 

  • setPriority()

: 우선순위 부여하는 메소드

 

우선순위 1~10까지 부여되며, 숫자 클수록 우선순위 높음

 

thread.setPriority(우선순위); 

 

 

ex.

thread.setPriority(Thread.MAX_PRIORITY); //10

thread.setPriority(Thread.NORM_PRIORITY); //5

thread.setPriority(Thread.MIN_PRIORITY); //1

 

 

 

  •  
public class CalThread extends Thread{

    //생성자
    public CalThread(String name) {
        setName(name);  //스레드 이름 변경
    }
    
    @Override
    public void run() {  //스레드 실행 내용
        for (long i=0;i<1100000000L;i++) {
            
        }
        System.out.println(getName());
    }
    
}


public class PriorityEx1 {

    public static void main(String[] args) {

        for (int i=1;i<=10;i++) {
            Thread thread = new CalThread("계산 작업 "+i);
            if (i==7) {
                thread.setPriority(Thread.MAX_PRIORITY);
            }else {
                thread.setPriority(Thread.MIN_PRIORITY);
            }
            thread.start();
        }//for
        
    }

}

//

계산 작업 7
계산 작업 8
계산 작업 3
계산 작업 4
계산 작업 2
계산 작업 10
계산 작업 6
계산 작업 9
계산 작업 5
계산 작업 1
=> 7이 1순위로 끝남

 

 

 

 

동기화 메소드 (synchronized)

: 2개 이상의 스레드가 클래스 객체 공유할 때, 사용 중인 객체를 다른 스레드가 변경할 수 없도록 잠금

 

 

임계 영역 : 단 하나의 스레드만 실행할 수 있는 코드 영역

 

 

cf. 동기화 블록 (동기화 메소드가 더 활용도 높음)
void method(){
  ..
  synchronized(공유객체){ => 메소드 내 특정 블록만 동시 실행 불가
  }
  ..
 }

 

 

  • ex. 계산기 클래스 공유
public class Calculator {
        
        private int memory;  //접근제한 -> getter, setter 필요

        public int getMemory() {
            return memory;
        }

        public void setMemory(int memory) {
            this.memory = memory;
            try {
                Thread.sleep(2000);  //2초
            } catch (InterruptedException e) {  //튕김 현상 예외 처리
                
            } 
            System.out.println(Thread.currentThread().getName()+" : "+this.memory);
            //현재 실행하고 있는 스레드의 이름
        }
        
}


public class UserJob1 extends Thread {

    //객체 필드
    private Calculator calc;  //user1 & user2 계산기 공유

    public void setCalc(Calculator calc) {
        this.setName("UserJob1");
        this.calc = calc;
    }

    @Override
    public void run() {
        calc.setMemory(100);
    }

}


public class UserJob2 extends Thread {

    //객체 필드
    private Calculator calc;  //user1 & user2 계산기 공유

    public void setCalc(Calculator calc) {
        this.setName("UserJob2");
        this.calc = calc;
    }

    @Override
    public void run() {
        calc.setMemory(50);
    }

}


public class CalculatorEx1 {

    public static void main(String[] args) {
        
        Calculator calc = new Calculator();  //인스턴스 객체 생성
        
        UserJob1 job1 = new UserJob1();  //스레드 생성
        job1.setCalc(calc);
        job1.start();
        
        UserJob2 job2 = new UserJob2();  //스레드 생성
        job2.setCalc(calc);
        job2.start();
        
    }

}

//
UserJob2 : 50
UserJob1 : 50 => 1번 스레드 실행 중에 2번 스레드 실행돼 오류

 

 

 

=> 해결

  • Calculator 클래스 동기화 메소드
public class Calculator {
        
        private int memory;  

        public int getMemory() {
            return memory;
        }

        public synchronized void setMemory(int memory) { //공유 객체 
        //임계 영역 : 단 하나의 스레드만 실행
            this.memory = memory;
            try {
                Thread.sleep(2000); 
            } catch (InterruptedException e) {  
                
            } 
            System.out.println(Thread.currentThread().getName()+" : "+this.memory);

        }
        
}

//

UserJob1 : 100
UserJob2 : 50

 

 

 

 

스레드 상태

Thread 클래스의 getState() 메소드 통해 스레드 상태 얻을 수 있음

 

 

NEW : 스레드 객체 생성

RUNNABLE : 실행 또는 실행대기

TIME_WAITING : sleep

BLOCKED : 동기화블록으로 잠금됨

TERMINATED : 실행 마침

 

 

 

  •  
public class TargetThread extends Thread{
    
    @Override
    public void run() {
        
        //작업 1
        for (long i=0;i<3100000000L;i++) {
            
        }        
        //1초 일시정지
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        
        }        
        //작업 2
        for (long i=0;i<3100000000L;i++) {
            
        }

    }

}


public class StatePrintThread extends Thread {

    private Thread target;
    
    public StatePrintThread(Thread target) {
        this.target=target;
    }
    
    @Override
    public void run() {
        
        while(true) {
            Thread.State state = target.getState(); //상태 가져오기
           
            System.out.println("타겟 스레드 상태 "+state);
            
            if (state==Thread.State.NEW) {  //스레드 생성
                target.start(); //스레드 실행
            }
            
            if(state==Thread.State.TERMINATED) {  //스레드 마침 => while 탈출
                break;
            }
            
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    
    }
    
}


public class ThreadStateEx1 {

    public static void main(String[] args) {
        
        StatePrintThread stateThread = new StatePrintThread(new TargetThread());
        stateThread.start(); //스레드 실행
    
    }

}

//

타겟 스레드 상태 NEW
타겟 스레드 상태 RUNNABLE
타겟 스레드 상태 RUNNABLE
타겟 스레드 상태 TIMED_WAITING
타겟 스레드 상태 TIMED_WAITING
타겟 스레드 상태 RUNNABLE
타겟 스레드 상태 RUNNABLE
타겟 스레드 상태 TERMINATED

 

 

 

 

스레드 상태 제어
  • sleep() - 일시 정지

: 실행 중인 스레드 일정 시간 멈춤

 

 

 

 

  • yield() - 실행 양보
  •  
public class ThreadJob1_yield extends Thread{
    
    public boolean stop=false; //종료 여부
    public boolean work=true; //작업 진행 여부
    
    @Override
    public void run() {
        while(!stop) {
            if (work) {
                System.out.println("ThreadJob1 일 작업중");
            }else {
                Thread.yield(); //다른 스레드에게 실행 양보
            }
        }
        System.out.println("ThreadJob1 종료");
    }
    
}


public class ThreadJob2_yield extends Thread{
    
    public boolean stop=false; //종료 여부
    public boolean work=true; //작업 진행 여부
    
    @Override
    public void run() {
        while(!stop) {
            if (work) {
                System.out.println("ThreadJob2 일 작업중");
            }else {
                Thread.yield(); //다른 스레드에게 실행 양보
            }
        }
        System.out.println("ThreadJob2 종료");
    }
    
}


public class YieldEx1 {

    public static void main(String[] args) {
        
        ThreadJob1_yield tJob1 = new ThreadJob1_yield();
        ThreadJob2_yield tJob2 = new ThreadJob2_yield();
        
        //2개 스레드 동시 실행
        tJob1.start();
        tJob2.start();
        
        //메인 스레드 2초 동안 일시정지 / 작업스레드 1 & 2 실행
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {}
        
        tJob1.work=false;  //tJob2 스레드만 실행
        
        //메인 스레드 2초 동안 일시정지 / 2 실행
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {}
        
        tJob1.work=true;  //tJob1 스레드 재실행
        
        //메인 스레드 2초 동안 일시정지 / 1 & 2 실행
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {}
        tJob1.stop = true; //스레드 종료
        tJob2.stop = true; //
        
    }

}

//

1, 2 동시 (2초)

2만 (2초)

1, 2 동시 (2초)

ThreadJob2 종료
ThreadJob1 종료

 

 

 

 

  • join() - 다른 스레드의 종료 기다림

다른 스레드 종료된 후 코드 실행

 

 

  •  
public class SumThread extends Thread {
    
    private int sum; //getter, setter 필요

    public int getSum() {
        return sum;
    }
    
    @Override
    public void run() {
        for (int i=1;i<=100;i++) {
            sum+=i;
        }
    }
    
}


public class JoinEx1 {

    public static void main(String[] args) {
        //작업스레드
        SumThread sumt = new SumThread();  //스레드 생성
        sumt.start();  

        //메인스레드
        System.out.print("1~100 합 : "+sumt.getSum());
        
    }

}

//

1~100 합 : 0   //start하자마자 run 메소드, 출력 메소드 거의 동시 실행 => sum에 값 안 들어왔을 때 출력

 

 

=> 해결 

  •  
public class JoinEx1 {

    public static void main(String[] args) {
            
        SumThread sumt = new SumThread();  //작업스레드 생성
        sumt.start();  
        try {
            sumt.join();  //메인스레드 일시 정지 (sumt 스레드 실행 끝날 때까지 기다림)
        } catch (InterruptedException e) {
        }

        System.out.print("1~100 합 : "+sumt.getSum());
        
    }

}

//
1~100 합 : 5050

 

 

 

 

  • wait(), notify(), notifyAll() - 스레드 간 협업

공유 객체 => 두 스레드가 작업할 내용을 각각 동기화 메소드로 구분 -> 교대로 번갈아가며 실행

 

 

스레드A 작업 완료 => notify() => 스레드B 실행대기 상태로 (일시정지에서) => wait() => 스레드A 일시정지

 

 

wait() 대신 wait(long timeout) / wait(long timeout, int nanos)

=> notify() 호출 없이 지정시간 지나면 일시정지 스레드가 실행대기 상태로 자동 변경

 

notifyAll()

=> wait()에 의해 일시정지된 모든 스레드들 실행대기 상태로 변경

cf. notify : 한 개 스레드만 실행대기 상태로 변경

 

 

 

  •  
public class ThreadWorkA extends Thread {
    
    private WorkObject object;
    
    public ThreadWorkA(WorkObject object) {
        this.object=object;
    }
    
    @Override
    public void run() {
        for (int i=1;i<=10;i++) {
            object.methodA();
        }
    }
    
}


public class ThreadWorkB extends Thread {
    
    private WorkObject object;
    
    public ThreadWorkB(WorkObject object) {
        this.object=object;
    }
    
    @Override
    public void run() {
        for (int i=1;i<=10;i++) {
            object.methodB();
        }
    }
    
}


//ThreadWorkA, ThreadWorkB의 공유 객체
public class WorkObject {
    
    public void methodA() {
        System.out.println("ThreadWorkA의 작업 실행");
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
        }
    } 
    
    public void methodB() {
        System.out.println("ThreadWorkB의 작업 실행");
        try {
            Thread.sleep(300);  //sleep 없으면 너무 빨라서 A B 순차 출력됨
        } catch (InterruptedException e) {
        }
    }
    
}


public class WorkObjectEx1 {

    public static void main(String[] args) {
        
        WorkObject shared = new WorkObject(); //공유 객체 생성
        ThreadWorkA workA = new ThreadWorkA(shared);
        ThreadWorkB workB = new ThreadWorkB(shared);
        workA.start();
        workB.start();
        
    }

}​

//

ThreadWorkA의 작업 실행
ThreadWorkA의 작업 실행
ThreadWorkA의 작업 실행
~
ThreadWorkA의 작업 실행
ThreadWorkA의 작업 실행
ThreadWorkA의 작업 실행
ThreadWorkB의 작업 실행
ThreadWorkB의 작업 실행
ThreadWorkB의 작업 실행
~
ThreadWorkB의 작업 실행
ThreadWorkB의 작업 실행
ThreadWorkB의 작업 실행
=> sleep 있으면 섞여서 출력 / 없으면 주로 순차 출력

 

 

 

=> 협업

  •  
//ThreadWorkA, ThreadWorkB의 공유 객체
public class WorkObject {

    public synchronized void methodA() {  //동기화 메소드 (A부터 실행됨)
        System.out.println("ThreadWorkA의 작업 실행");
        notify();
        //일시정지->실행대기 => 다른 동기화 메소드에 적용 (각 메소드마다 한번씩 수행하도록)
        try {
            wait();  //ThreadWorkA 일시정지
        } catch (InterruptedException e) {
        }
    } 
    
    public synchronized void methodB() {
        System.out.println("ThreadWorkB의 작업 실행");
        notify();
        try {
            wait();  //ThreadWorkB 일시정지
        } catch (InterruptedException e) {
        }
    }
    
}

//
ThreadWorkA의 작업 실행
ThreadWorkB의 작업 실행
ThreadWorkA의 작업 실행
~
ThreadWorkB의 작업 실행
ThreadWorkA의 작업 실행
ThreadWorkB의 작업 실행

 

 

 

 

  • stop 플래그, interrupt() - 스레드의 안전한 종료

run() 메소드 모두 실행시 스레드는 자동 종료 / 실행 중인 스레드를 즉시 종료할 때에는 stop 플래그 or interrupt()

 

stop 플래그 : boolean 이용해 run() 메소드 종료 유도

interrupt() : 스레드가 sleep() 메소드로 일시정지 상태에 있을 때 => InterruptedException 예외 발생시킴

 

 

 

  •  
1. Stop 플래그

public class StopThread extends Thread {

    private boolean stop; //stop 플래그 필드

    public void setStop(boolean stop) { //
        this.stop = stop;
    }
    
    @Override
    public void run() {
        while(!stop) { //setStop(true) => while문 탈출
            System.out.println("현재 작업중..");
        }
        System.out.println("자원 정리");
        System.out.println("실행 종료");
    }
    
}


public class StopThreadEx1 {
    public static void main(String[] args) {

    StopThread stopthread = new StopThread();
    stopthread.start();  
    
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {}
    
    stopthread.setStop(true); //
    }
}
//
현재 작업중..
현재 작업중..
현재 작업중..
현재 작업중..
~
현재 작업중..
현재 작업중..
자원 정리
실행 종료



2. interrupt 메소드

public class InterruptThread extends Thread {

    @Override
    public void run() {
        try {
            while(true) {
                System.out.println("현재 작업 중");
                Thread.sleep(1); //sleep()으로 일시정지 
            }
        } catch(InterruptedException e) {} //일시정지 상태에서 예외 발생 => while 탈출
        
        System.out.println("자원 종료");
        System.out.println("실행 종료");
    }
    
}


public class InterruptThreadEx1 {

    public static void main(String[] args) {

        InterruptThread thread1 = new InterruptThread();
        thread1.start();
        try {
            Thread.sleep(1000); //메인스레드 1초 일시정지 (작업스레드 1초 실행)
        } catch (InterruptedException e) {}
        thread1.interrupt(); //일시정지 시 예외 발생시키기 (1초 후)
        
    }

}
//
현재 작업 중
현재 작업 중
현재 작업 중
현재 작업 중
~
현재 작업 중
현재 작업 중
현재 작업 중
자원 종료
실행 종료

 

 

 

데몬 스레드 : 주 스레드의 작업을 돕는 보조역할

주 스레드 종료 시 데몬 스레드 강제 자동 종료됨

 

 

 

  •  
public class AutoSaveThread extends Thread { 
    
    public void save() {
        System.out.println("작업 내용을 저장");
    }
    
    @Override
    public void run() {
       while(true) {
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {}
           save(); //1초마다 실행
       }
    }

}


public class AutoSaveThreadEx1 {

    public static void main(String[] args) {

        AutoSaveThread autoSave = new AutoSaveThread();
        autoSave.start();
        try {
            Thread.sleep(3000); //3초간 작업스레드 실행
        } catch (InterruptedException e) {}
        System.out.println("메인 스레드 종료 !");
        
    }

}
//
작업 내용을 저장
작업 내용을 저장
메인 스레드 종료 !
작업 내용을 저장
작업 내용을 저장
~ 반복
 

 

 

=> 해결 

  • 데몬 스레드 사용
public class AutoSaveThreadEx1 {

    public static void main(String[] args) {

        AutoSaveThread autoSave = new AutoSaveThread();
        autoSave.setDaemon(true); //start() 전 //데몬스레드로 세팅
        autoSave.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {}
        System.out.println("메인 스레드 종료 !");
        
    }

}

//
작업 내용을 저장
작업 내용을 저장
메인 스레드 종료 !

'Programming > 자바' 카테고리의 다른 글

자바 재정리 - 람다식  (0) 2022.09.08
자바 재정리 - 제네릭  (0) 2022.09.08
자바 재정리 - API  (0) 2022.09.05
자바 재정리 - 예외 처리  (0) 2022.08.26
자바 재정리 - 중첩 클래스, 중첩 인터페이스  (0) 2022.08.26