멀티스레드 사용 시 가장 주의할 점은

여러 스레드가 같은 자원에 동시에 접근할 때 발생하는 동시성 문제다.

대표적인 공유 자원은 인스턴스의 필드(멤버 변수) 다.

 

 

synchronized 키워드를 붙이면, 한 번에 하나의 스레드만 실행할 수 있는 코드 구간을 만들 수 있다.

 

public class BankAccountV2 implements BankAccount {

  private int balance; // 잔액

  public BankAccountV2(int balance) {
    this.balance = balance;
  }

  @Override
  public synchronized boolean withdraw(int amount) {
    log("거래 시작: " + getClass().getSimpleName());
    log("[검증 시작] 출금액: " + amount + ", 잔액:" + balance);

    if (balance < amount) {
        log("[검증 실패] 출금액: " + amount + ", 잔액:" + balance);
        return false;
    }
    log("[검증 완료] 출금액: " + amount + ", 잔액:" + balance);
    sleep(1000); // 출금 소요 시간 1초
    balance -= amount;

    log("[출금 완료] 출금액: " + amount + ", 잔액:" + balance);
    log("거래 종료");
    return true;
  }

  @Override
  public synchronized int getBalance() {
      return balance;
  }
}

 

 

모든 인스턴스는 자신만의 Lock 을 가지고 있다.

  • 이것을 모니터 락(monitor lock) 으로도 불린다.
  • 객체 내부에 있는데 개발자가 확인하기는 어렵다.
  • t1 스레드 작업이 종료되면, synchronized 블럭을 나갈 때, BankAccount 인스턴스의 락을 반납한다.
  • BLOCKED 상태로 락 획득을 대기하는 t2 스레드는 자동으로 락을 획득한다.
    • t2 스레드 상태변경: BLOCKED → RUNNABLE
    • [참고] BLOCKED 상태인 스레드가 여러개일 때, 락을 획득하는 순서는 보장되지 않는다.
  • t2 스레드는 검증 로직을 통과하지 못한다. 락을 반납하면서 return 한다.
  • volatile 를 사용하지 않아도, synchronized 안에서 접근하는 변수의 메모리 가시성 문제는 해결된다.

 

BLOCKED 상태 스레드는

  • 락이 풀릴 때 까지 무한 대기한다
  • 중간에 인터럽트를 걸 수 없다
  • BLOCKED 상태인 스레드가 여러개일 때, 락을 획득하는 순서는 보장되지 않는다
  • → 순서를 모르니까 특정 스레드가 엄청 오래 기다리게 될 수도 있다.

 

synchronized 코드 블럭

synchronized 는 전체적으로 보면 성능이 떨어질 수 있다.

따라서 동시에 실행할 수 없는 코드 구간은 꼭! 필요한 곳으로 한정해서 설정해야 한다.

 

 

특정 코드 블럭만 synchronized 를 붙이자

  • 메서드 단위가 아니라 특정 블럭에 최적화하여 적용할 수 있다.
public boolean withdraw(int amount) {
    log("거래 시작: " + getClass().getSimpleName());

    synchronized (this) {
        log("[검증 시작] 출금액: " + amount + ", 잔액:" + balance);
        if (balance < amount) {
            log("[검증 실패] 출금액: " + amount + ", 잔액:" + balance);
            return false;
        }
        log("[검증 완료] 출금액: " + amount + ", 잔액:" + balance);
        sleep(1000); // 출금 소요 시간 1초
        balance -= amount;
        log("[출금 완료] 출금액: " + amount + ", 잔액:" + balance);
    }
    
    log("거래 종료");
    return true;
}
  • synchronized (this) {} 임계 영역을 코드 블럭으로 지정한다
  • synchronized (this) : 여기서 괄호 안에 들어가는 값은 락을 획득할 인스턴스의 참조다.
    • 여기서는 BankAccountV3(x001) 의 인스턴스의 락을 사용하므로 이 인스턴스의 참조인 this 를 넣는다.
728x90

'프로그래밍 > JAVA' 카테고리의 다른 글

volatile  (0) 2025.04.03
스레드 생명 주기  (0) 2025.04.03
스레드 생성과 실행  (0) 2025.04.03
List  (0) 2025.03.28
스레드 생성하기  (0) 2025.03.28

 

메모리 가시성 문제

  • 멀티스레드에서 어려운 문제 중 하나가 메모리 가시성 문제다.
  • 한 스레드가 변경한 값이 다른 스레드에서는 언제 보이는지에 대한 문제다
  • 무슨 문제이고 왜 발생하고 어떻게 해결할 지 살펴보자.

일반적으로 생각하는 메모리 접근 방식

실제 메모리 접근 방식

  • main 스레드가 runFlag = false 실행하면,
  • main 스레드를 실행하는 CPU 코어의 캐시 메모리의 runFlag = false 부터 반영한다.
  • 그래서 메인 메모리의 runFlag 값이 즉시 반영되지 않는다!

CPU 캐시 메모리

  • 캐시 메모리를 사용하면 CPU 처리 성능을 개선할 수 있다.
  • 코어1 캐시 메모리에 있는 runFlag 의 값이 언제 메인 메모리에 반영될까?
  • 메인 메모리에 변경된 runFlag 값이 언제 CPU 코어2의 캐시 메모리에 반영될까?

→ 둘 다 “ 알 수 없다.” CPU의 설계 방식에 따라 다르다.

  • 주로 컨텍스트 스위칭이 될 때, 캐시 메모리가 갱신된다.
  • 하지만, 이것이 갱신을 보장하는 것은 아니다.

 

 

값을 읽을 때, 값을 쓸 때 모두 메인 메모리에 직접 접근하자!

자바에서는 volatile 키워드로 이런 기능을 제공한다.

 

volatile 키워드 적용하기

  • 캐시 메모리 접근하지 말고, 메인 메모리 접근해!
public class VolatileFlagMain {

  public static void main(String[] args) {
      MyTask task = new MyTask();
      Thread t = new Thread(task, "work");
      log("runFlag = " + task.runFlag);
      t.start();

      sleep(1000);
      log("runFlag 변경 시도");
      task.runFlag = false;
      log("runFlag = " + task.runFlag);
      log("main 종료");
  }

  static class MyTask implements Runnable {
  
      volatile boolean runFlag = true;

      @Override
      public void run() {
          log("task 시작!");
          while(runFlag) {
              // runFlag 가 false 되면 탈출하자
          }
          log("task 종료");
      }
  }
}
  • runFlag 변수를 false 바꾸는 순간 즉시 메인 메모리에 반영한다.
  • 여러 스레드에서 같은 값을 읽고 써야 한다면, volatile 키워드를 붙이자.
  • 단, 캐시 메모리를 쓸 때보다는 성능이 느려지는 단점이 있기 때문에 꼭 필요한 곳에서만 사용해야 한다.
728x90

'프로그래밍 > JAVA' 카테고리의 다른 글

synchronized  (0) 2025.04.03
스레드 생명 주기  (0) 2025.04.03
스레드 생성과 실행  (0) 2025.04.03
List  (0) 2025.03.28
스레드 생성하기  (0) 2025.03.28

 

스레드는 생성 → 시작 → 종료되는 생명주기를 가진다.

스레드 생명 주기

 

  • 일단 실행 가능한 상태가 되면, 일시 중지 상태로 바뀔 수 있다
    • 참고: 자바에서 스레드의 ‘일시 중지 상태’ 라는 것은 없다.
    • 스레드가 기다리는 상태를 묶어서 설명하기 위해 사용한 용어다.

5가지 상태가 있다는 정도만 알아두자. 뒤에서 자세히 다루기 때문이다.

 

1. New (생성만 된 상태)

  • Thread 객체가 생성되지만, start()메서드가 호출되지 않은 상태다.
  • 예: Thread thread = new Thread(runnable)

2. Runnable (실행 가능 상태)

  • 보통 실행 상태라고 부른다
  • start() 메서드가 호출되면, 스레드는 이 상태로 들어간다.
  • 이 상태에서 스레드는 실제로 CPU에서 실행될 수 있다. (이 상태에서만 CPU 스케쥴러에 들어가요!)
  • 운영체제의 스케쥴러가 각 스레드에 CPU 시간을 할당하여 실행하기 때문에, Runnable 상태에 있는 스레드는 스케쥴러의 실행 대기열에 포함되어 있다가 차례로 CPU에서 실행된다.
    • 즉, 운영체제 입장에서 보면, CPU가 실제로 실행하는 스레드와 OS 스케쥴링 큐에 대기한 스레드 둘 다 포함이다.

3. Blocked (차단 상태)

  • 동기화 락을 얻기 위해 기다리는 상태
  • 예를 들어, synchronized 블록에 진입하기 위해 을 얻어야하는 경우 이 상태에 들어간다.
    • 락은 스레드 하나만 가지고 있을 수 있다.

4. Waiting (대기 상태)

  • 스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태
  • 스레드는 다른 스레드가 notify(), notifyAll() 메서드를 호출하거나, join()이 완료될 때 까지 기다린다.
  • wait(), join() 메서드가 호출되면 이 상태가 된다.
  • 예: object.wait()

5. Timed Waiting (시간 제한 대기 상태)

  • 스레드가 특정 시간 동안만 대기하는 상태
  • 주어진 시간이 경과하거나, 다른 스레드가 해당 스레드를 깨우면 이 상태에서 벗어난다.
  • sleep(long millis), wait(long timeout), join(long millis) 메서드 호출 시 이 상태가 된다.
  • 예. Thread.sleep(1000);

6. Terminated (종료 상태)

  • 스레드 실행이 완료된 상태.
  • 스레드가 정상적으로 종료되거나 예외가 발생한 경우에 Terminated 가 된다.
  • 스레드가 종료되면 다시 시작할 수 없다.
728x90

'프로그래밍 > JAVA' 카테고리의 다른 글

synchronized  (0) 2025.04.03
volatile  (0) 2025.04.03
스레드 생성과 실행  (0) 2025.04.03
List  (0) 2025.03.28
스레드 생성하기  (0) 2025.03.28

 

HelloThread - 스레드 생성

스레드를 생성해보자.

클래스에 Thread 를 상속 받고 run 메서드를 재정의하면 된다!

public class HelloThread extends Thread {
  @Override
  public void run() {
      System.out.println(
      Thread.currentThread().getName() + " : run()");
	}
}

 

HelloThreadMain

  • 해당 코드를 실행하는 스레드 이름을 출력한다 Thread.currentThread().getName()
    • 그래서 메인 메서드를 실행하는 main 이라는 스레드명이 제일 처음에 출력된다.
    • 누가 이 코드를 실행하는지 스레드명을 확인하면서 학습하자.
public static void main(String[] args) {
  /** Thread.currentThread().getName() -> 스레드명이 main 이라고 출력된다 */
  System.out.println(Thread.currentThread().getName() + ": main() start");
  
  HelloThread helloThread = new HelloThread(); // HelloThread 객체를 생성
  System.out.println(Thread.currentThread().getName() + ": start() 호출 전 ");
  
  helloThread.start(); // start() 를 호출한다 ! 
  // 이 시점부터 main 스레드와 Thread-0 이 동시에 실행된다.
  
  System.out.println(Thread.currentThread().getName() + ": start() 호출 후 ");

  System.out.println(Thread.currentThread().getName() + ": main() end");
}
  • HelloThread 인스턴스를 생성하고 HelloThread helloThread = new HelloThread()
    • 이것은 인스턴스 생성만 됬을 뿐, 스택 프레임이 할당된 게 아니다.
  • start() 를 호출한다. helloThread.start()

 

중요 - 어느 시점에 새로운 스택 프레임이 생성될까 

  • helloThread.start() 를 호출하면, HelloThread 의 스택 프레임이 생성된다.
  • (main 스레드와는 별도의 스택 프레임이다)
  • 그리고 HelloThread 가 run() 를 호출한다.
  • 주의할 점은 main 스레드가 아니라, HelloThread 가 run() 을 호출한다는 점이다.
  • main 스레드는 단지 helloThread.start() 호출을 통해서 다른 스레드에게 일을 시작하라고 지시할 뿐이다.

 

스레드 간의 실행 순서와 실행 기간을 둘 다 보장하지 않는다.

  • 스레드는 동시에 실행되기 때문에 실행 순서는 얼마든지 달라질 수 있다.
main: main() start
main: start() 호출 전 
main: start() 호출 후 
Thread-0 : run()
main: main() end
  • 실행 할 때마다 조금 달라진다. Thread-0 이 제일 밑에 출력될 때도 있다
main: main() start
main: start() 호출 전 
main: start() 호출 후 
main: main() end
Thread-0 : run()

정리

  • 스레드 객체를 생성하고, 반드시 start() 를 호출해야 스택 공간을 할당 받고 스레드가 작동한다.
  • 스레드는 순서와 실행 기간을 모두 보장하지 않는다! 이것이 멀티스레드다.
728x90

'프로그래밍 > JAVA' 카테고리의 다른 글

volatile  (0) 2025.04.03
스레드 생명 주기  (0) 2025.04.03
List  (0) 2025.03.28
스레드 생성하기  (0) 2025.03.28
Exception  (0) 2025.03.20

순서가 있고, 중복을 허용하는 자료구조를 List 라 한다.

  • ArrayList 와 LinkedList 는 사용자 입장에서는 기능이 똑같다.
  • 내부 구현만 다르므로 성능이 달라질 수 있다.

 

배열 리스트와 연결리스트 성능 비교

  ArrayList LinkedList
인덱스 조회 O(1) O(n)
데이터로 검색 O(n) O(n)
앞에 추가/삭제 O(n) O(1)
뒤에 추가/삭제 O(1) O(n)
평균 추가/삭제 O(n) O(n)

ArrayList 배열리스트

  • 배열로 구성
  • 인덱스를 통해 추가/삭제할 위치를 O(1) 로 빠르게 찾는다
  • 추가 시, 데이터를 전부 한 칸씩 밀어야 해서 O(n)으로 오래 걸린다
  • 배열이라서 메모리 상에서 연속적으로 위치하여 CPU 캐시 효율이 좋고 메모리 접근 속도가 빠르다.

LinkedList 연결리스트

  • 이중 연결리스트로 구현되어 있다
  • LinkedList 는 개별 노드가 앞/뒤 노드의 참조를 관리하므로 메모리 접근 속도가 배열에 비해서는 떨어진다
    • 인덱스를 제공하지 않으므로 순회해야 위치를 찾을 수 있다. O(n) 소요
    • 추가 시, O(n)으로 오래 걸린다
    • 해당 위치 찾는데 O(n) 소요되고, 참조 변경은 O(1) 소요
  • 만약, 데이터를 앞쪽에 자주 추가/삭제 할 일이 있다면, 연결 리스트를 고려하자
728x90

'프로그래밍 > JAVA' 카테고리의 다른 글

스레드 생명 주기  (0) 2025.04.03
스레드 생성과 실행  (0) 2025.04.03
스레드 생성하기  (0) 2025.03.28
Exception  (0) 2025.03.20
String 클래스  (1) 2025.03.20

HelloThread - 스레드 생성

스레드를 생성해보자.

클래스에 Thread 를 상속 받고 run 메서드를 재정의하면 된다!

public class HelloThread extends Thread {
  @Override
  public void run() {
      System.out.println(
      Thread.currentThread().getName() + " : run()");
	}
}

HelloThreadMain

  • 해당 코드를 실행하는 스레드 이름을 출력한다 Thread.currentThread().getName()
    • 그래서 메인 메서드를 실행하는 main 이라는 스레드명이 제일 처음에 출력된다.
    • 누가 이 코드를 실행하는지 스레드명을 확인하면서 학습하자.
public static void main(String[] args) {
  /** Thread.currentThread().getName() -> 스레드명이 main 이라고 출력된다 */
  System.out.println(Thread.currentThread().getName() + ": main() start");
  
  HelloThread helloThread = new HelloThread(); // HelloThread 객체를 생성
  System.out.println(Thread.currentThread().getName() + ": start() 호출 전 ");
  
  helloThread.start(); // start() 를 호출한다 ! 
  // 이 시점부터 main 스레드와 Thread-0 이 동시에 실행된다.
  
  System.out.println(Thread.currentThread().getName() + ": start() 호출 후 ");

  System.out.println(Thread.currentThread().getName() + ": main() end");
}
  • HelloThread 인스턴스를 생성하고 HelloThread helloThread = new HelloThread()
    • 인스턴스 생성만 됬을 뿐, 스택 프레임이 할당된 게 아니다.
  • start() 를 호출한다.

 

helloThread.start() 를 호출하면, HelloThread 의 스택 프레임이 생성된다.

(main 스레드와는 별도의 스택 프레임이다)

그리고 HelloThread 가 run() 를 호출한다.

주의할 점은 main 스레드가 아니라, HelloThread 가 run() 을 호출한다는 점이다.

main 스레드는 단지 helloThread.start() 호출을 통해서 다른 스레드에게 일을 시작하라고 지시할 뿐이다.

 

  • 스레드는 동시에 실행되기 때문에 실행 순서는 얼마든지 달라질 수 있다.
main: main() start
main: start() 호출 전 
main: start() 호출 후 
Thread-0 : run()
main: main() end
  • 실행 할 때마다 조금 달라진다. Thread-0 이 제일 밑에 출력될 때도 있다

정리 

  • 스레드 객체를 생성하고, 반드시 start() 를 호출해야 스택 공간을 할당 받고 스레드가 작동한다.
  • 스레드는 순서와 실행 기간을 모두 보장하지 않는다!

 

 

Runnable 인터페이스

  • 자바가 제공하는 스레드 실행용 인터페이스다.
public interface Runnable {
		void run();
}

어떻게 사용하는지 살펴보자.

HelloRunnable implements Runnable

public class HelloRunnable implements Runnable {
  @Override
  public void run() { // run() 내에 작업 로직을 구현한다 
      System.out.println(Thread.currentThread().getName() + ": run()");
  }
}

 

HelloRunnableMain

  • 실행 결과는 기존과 같다.
  • 차이가 있다면, Thread 와 해당 스레드가 실행할 작업이 분리되어 있다는 점이다.
  • Thread 객체를 생성할 때, 생성자로 작업 클래스를 전달한다.
public class HelloRunnableMain {
  public static void main(String[] args) {
    System.out.println(Thread.currentThread().getName() + ": main() start");

    HelloRunnable runnable = new HelloRunnable(); // 작업 정의
    Thread thread = new Thread(runnable); // 스레드
    thread.start();

    System.out.println(Thread.currentThread().getName() + ": main() end");
  }
}

 

Runnable 인터페이스 구현하는 방식이 더 나은 이유!

Thread 클래스 상속 방식의 단점

  • 자바는 단일 상속만을 허용하므로, Thread 클래스를 상속받으면, 다른 클래스를 상속받을 수 없다.
  • 인터페이스를 사용하는 방식에 비해 유연성이 떨어진다.

Runnable 인터페이스를 구현하는 방식

  • 상속의 자유로움
  • 코드의 분리: 스레드와 실행할 작업은 분리하여 코드의 가독성을 높일 수 있다.
  • 여러 스레드가 동일한 Runnable 객체를 공유할 수 있어 자원 관리를 효율적으로 할 수 있다!
728x90

'프로그래밍 > JAVA' 카테고리의 다른 글

스레드 생성과 실행  (0) 2025.04.03
List  (0) 2025.03.28
Exception  (0) 2025.03.20
String 클래스  (1) 2025.03.20
자바 객체 지향  (0) 2025.03.13

목차

1. 예외 계층 

2. 예외 기본 규칙

3. 체크 예외

4. 언체크 예외 (런타임 예외)


1. 예외 계층

예외 처리는 아래 키워드를 사용한다

try, catch, finally, throw, thorws

  • Obejct: 자바에서 기본형을 제외한 모든 것은 객체다. 예외의 최상위 부모도 Object 다.
  • Throwable: 최상위 예외 객체다. 하위에 Exception 과 Error 가 있다.
  • Error: 메모리 부족이나 심각한 시스템 오류와 같은 애플리케이션에서 복구 불가능한 시스템 예외다.
    • 애플리케이션 개발자는 이를 잡으려고 해선 안 된다.
  • Exception
    • 애플리케이션 로직에서 사용할 수 있는 최상위 예외다.
  • 체크 예외
    • RuntimeException 을 제외하고는 모두 컴파일러가 체크하는 예외다
    • 개발자가 체크 예외를 처리하지 않으면 컴파일 오류가 발생한다.
  • 언체크 예외 (런타임 예외)
    • 컴파일러가 체크하지 않는다.
    • RuntimeException 과 그 자식 예외는 모두 언체크 예외다

주의

  • 상속 관계에서 부모 타입은 자식을 담을 수 있다. 이 개념이 예외 객체에서도 적용된다.
    • 상위 예외를 catch 로 잡으면, 하위 예외 까지 함께 잡는다.
  • 따라서 애플리케이션 로직에서는 Throwable 예외를 잡으면 안된다. 애플리케이션 개발자가 잡으면 안 되는 Error 예외 까지 같이 잡을 수 있기 때문이다.
  • 이런 이유로 애플리케이션 로직에서는Exception 부터 ‘처리가 필요한 예외’로 여기고 잡으면 된다.

 

2. [중요] 예외 기본 규칙

예외는 폭탄 돌리기와 같다. 잡아서 밖으로 던지거나, 처리하거나.
  • 예외를 처리하지 못하면, 자신을 호출한 곳으로 예외를 던져야 한다.
  • 예외를 잡거나 던질 때, 지정한 예외 뿐만 아니라 그 예외의 자식 예외도 함께 처리할 수 있다!
  • 예외를 처리하지 못하고 계속 던지면 어떻게 될까?
    • 자바 main() 밖으로 예외로 던지면, 예외 로그를 출력하면서 시스템이 종료된다.

 

3. 체크 예외

  • Exception 과 그 하위 예외 모두는 컴파일러가 체크하는 체크 예외다.
    • RuntimeException 은 제외다. 언체크 예외다.
  • 체크 예외는 잡아서 처리하거나, 자신을 호출한 곳으로 던져야 한다. 그렇지 않으면 컴파일 오류가 발생한다.

 

Exception 을 상속받은 예외는 체크 예외가 된다.

  • MyCheckedException 예외 클래스를 만들자.
    • Exception 을 상속받으면 된다.
public class MyCheckedException extends Exception {
    public MyCheckedException(String message) {
        super(message);
    }
}
  • super(message)
    • Exception 에는 message 를 보관하는 생성자가 있다.
    • Throwable 의 detailMessage 에 저장된다.
  • 보관한 message 를 꺼내려면, Throwable 에서 제공하는 getMessage() 메서드로 꺼낸다

 

throw VS throws 헷갈리지 말기

  • throws : 예외를 메서드 밖으로 던질 때 쓰는 키워드다
  • throw : 예외를 발생시킨다.
    • new 로 객체를 생성하고 예외를 발생시킬 수 있다
    • throw new MyCheckedException(”msg”)

 

Client class : 예외 발생하면 밖으로 던지기

  • call() throws MyCheckedException → MyCheckedException 가 발생하면 메서드 밖으로 던진다
  • throw new MyCheckedException("ex") → 예외 객체를 생성하여 예외를 발생시킨다.
public class Client {
  
  public void call() throws MyCheckedException { // throws 예외 던지기 

      // throw 예외 발생 
      throw new MyCheckedException("ex");
  }
}

 

 

예외를 잡아서 처리하기

  • try catch 를 사용해서 예외를 잡으면 된다.
  • client.call() 호출하면, MyCheckedException 이 발생하여 예외 객체가 넘어온다.
  • catch 블럭 내에 예외 처리 로직을 작성한다.
public void callCatch() {

  try {
      client.call();
  } catch (MyCheckedException e) { // 예외 처리 로직
      
      System.out.println("예외 처리, message=" + e.getMessage());
      // 예외 처리 후, 정상 흐름으로 간다.
  }

  System.out.println("정상 흐름");
}

catch 의 대상에 없는 예외라면?

  • try 에서 잡은 예외가 catch 의 대상에 없으면 예외를 잡을 수 없다. 이때는 예외를 밖으로 던져야 한다.
  • catch 대상을 RuntimeException 으로 바꿔보면, 컴파일 에러가 발생한다!
    • unreported exception: exception.basic.checked.MyCheckedException;
    • must be caught or declared to be thrown → 잡거나 던진다고 선언하세요.

 

그 예외의 자식 예외도 함께 처리하기

  • MyCheckedException 은 Exception 을 상속 받은 체크 예외 클래스다.
  • catch 대상을 MyCheckedException 대신 Exception 으로 바꿔서 실행해보자.
public void callCatch() {

    try {
        client.call();
    } catch (Exception e) { // 예외 처리 로직
        
        System.out.println("예외 처리, message=" + e.getMessage());
    }

    System.out.println("정상 흐름");
}
  • 실행 해보면, MyCheckedException 예외를 catch 로 잡아서 처리한 것을 확인할 수 있다

 

예외를 메서드 밖으로 던지기 throws

  • client.call() 호출하면, MyCheckedException 이 발생하여 예외 객체가 넘어온다.
  • 여기서는 메서드에 선언한 throws 로 예외를 메서드 밖으로 던진다.
  • 체크 예외를 밖으로 던지려면, throws 예외를 메서드에 필수로 지정해야 한다.
public void catchThrow() throws MyCheckedException {
    client.call();
}

정리

  • 체크 예외는 개발자가 명시적으로 처리해야 한다. 처리 안하면, 컴파일 오류 난다.
  • try-catch 로 잡아서 처리 하거나, 메서드에 throws 를 지정해서 예외를 밖으로 던진다는 선언을 필수로 해야 한다.

 

4. 언체크 예외 (런타임 예외)

  • 컴파일러가 예외를 체크하지 않는다.
  • 언체크 예외는 체크 예외와 기본적으로 동일하다. 한 가지 차이가 있다. throws 를 생략할 수 있다.
  • throws 를 생략해도, 자동으로 예외를 밖으로 던진다.

체크 예외 VS 언체크 예외

  • 체크 예외: 예외를 처리하지 않는 경우, throws 로 던져야만 컴파일 오류가 안 난다.
  • 언체크 예외: throws 를 생략해도, 자동으로 예외를 밖으로 던진다.

 

언체크 예외 예제코드

  • 언체크 예외 클래스 RuntimeException 을 상속받은 예외는 언체크 예외가 된다.
public class MyUncheckedException extends RuntimeException{
    public MyUncheckedException(String message) {
        super(message);
    }
}
  • 언체크 예외를 발생시키는 Client 클래스
    • 체크 예외와는 다르게, 메서드에 throws 키워드를 생략했다.
public class Client {

    public void call() { // 메서드에 throws 키워드 생략 
        throw new MyUncheckedException("unckecked exception! ");
    }
}
  • Client 클래스 메서드를 호출하는 Service 클래스
public class Service {
    Client client = new Client();

    public void callCatch() { // 필요한 경우, 예외를 잡아서 처리할 수 있다.
        
        try {
            client.call();
        } catch (MyUncheckedException e) {
            System.out.println("예외 처리, message: " + e.getMessage());
        }
        System.out.println("정상 로직");
    }
    
    /** 
     * 예외를 잡지 않아도 컴파일 에러가 안나고, 상위로 올라간다. 
     * */
    public void callThrow() {
        client.call();
    }
}
  • 예외 잡아서 처리하는 메서드 callCatch() 호출하여 실행
public class UncheckedCatchMain {
    public static void main(String[] args) {
        Service uncheckedService = new Service();
        uncheckedService.callCatch();
        System.out.println("정상 종료");
    }
}
  • 실행 결과
예외 처리, message: my unckecked exception! 
정상 로직
정상 종료
  • 예외를 호출부로 던지는 callThrow() 메서드를 호출하여 실행
public class UncheckedCatchMain {
    public static void main(String[] args) {
        Service uncheckedService = new Service();
        uncheckedService.callThrow();
        System.out.println("정상 종료");
    }
}
  • 실행 결과
    • callThrow() 메서드에 throws 키워드가 없어도, 컴파일 오류가 나지 않는다.
    • callThrow() 메서드가 실행 되었고, callThrow() 에서 예외를 잡아서 처리하지 않았기 때문에 main() 으로 언체크 예외가 넘어왔다.
    • main() 에서도 MyUncheckedException 을 처리하지 않고 있어서 main() 도 예외를 밖으로 던진다.
    • callThrow() 까지만 실행 됬으니까, 다음 줄에 있는 “정상 종료” 출력이 실행되지 않는다.
    • 이렇게 되면 예외 정보와 스택 트레이스가 출력되면서 프로그램이 종료된다.
Exception in thread "main" exception.basic.unchecked.MyUncheckedException: my unckecked exception! 
	at exception.basic.unchecked.Client.call(Client.java:6)
	at exception.basic.unchecked.Service.callThrow(Service.java:23)
	at exception.basic.unchecked.UncheckedCatchMain.main(UncheckedCatchMain.java:7)

정리

  • 신경쓰고 싶지 않은 언체크 예외를 무시할 수 있다는 장점이 있다. throws 예외를 생략 가능!
  • 하지만, 컴파일러를 통해 예외 누락을 잡을 수는 없다.
  • 현대 애플리케이션에서는 체크 예외를 거의 쓰지 않는다.

인프런 김영한 자바 중급1 강의를 듣고 요약한 내용입니다. 

728x90

'프로그래밍 > JAVA' 카테고리의 다른 글

List  (0) 2025.03.28
스레드 생성하기  (0) 2025.03.28
String 클래스  (1) 2025.03.20
자바 객체 지향  (0) 2025.03.13
자바 기본1  (0) 2025.03.13

목차 

1. String 클래스 구조 

2. String 클래스가 불변 객체인 이유 

3. StringBuilder

4. StringBuffer


1. String 클래스 구조

  • java 17 기준으로 String 클래스 구현 코드를 열어봤다.
  • 문자열은 문자 배열 byte[] 로 저장된다. 

 

(안 중요 하니까 참고) java9 부터 내부 구현을 char[] 에서 byte[] 로 저장하도록 바뀌었다고 한다. 

  • char 타입은 2byte 이다.
  • 그런데 보통 문자열로 저장되는 영어와 숫자는 1byte 단위로 메모리를 채운다.
    • 문자열이 단순 영어, 숫자로만 표현된 경우, 1byte 단위로 사용하게 된다.
    • 그렇지 않은 나머지의 문자의 경우는 2byte 인 UTF-16 인코딩을 사용한다.

-> 1byte 단위로 다루어서 메모리를 더 효율적으로 사용하도록 개선된 것이다. 

 

2. String 클래스가 불변 객체인 이유 

  • String 의 value는 자바 메모리 영역 중에 Method Area 메서드 영역에 저장된다. 
  • 메서드 영역 내에 문자열 상수 풀이 있다. String Constant Pool
  • 컴파일러는 문자열 리터럴을 컴파일 타임에 미리 최적화 해둔다. 
    • String Pool에 "hello" 라는 리터럴을 저장해뒀다고 하자. 이 값을 여러 인스턴스들이 같이 참조하고 있다.
    • "hello" -> "bye" 로 바뀐다면, 해당 값을 참조하고 있는 다른 인스턴스들도 영향을 받게된다!
  • String 클래스가 불변 객체이기 때문에 여러 객체가 동일한 문자열을 공유할 수 있고, 메모리 효율성을 높일 수 있다.

 

3. StringBuilder

  • 문자열 편집이 빈번한 경우, 대부분 StringBuilder 를 쓰게 된다. 
  • String 은 불변 객체라 값을 자주 바꾸면 너무 많은 인스턴스가 heap 메모리를 차지하게 되버린다. 
    • 연산 과정에서 쓰인 String 인스턴스는 GC의 대상이 된다. 
  • 문자열 편집이 끝나면 불변인 String 으로 변환해두자

StringBuilder 사용이 더 나은 경우 

  • 런타임에 편집 횟수가 정해지는 경우 (몇 만번 ..)
  • 조건문을 통해 런타임에 동적으로 문자열을 편집해야 하는 경우 
  • 매우 긴 문자열을 다룰 때 

 

4. StringBuffer

  • StringBuilder 와 StringBuffer 는 똑같은 기능을 제공한다. 
  • (멀티스레드 관련 내용이라 완전히 이해하지는 못했고 공부중이다.)
  • StringBuffer 는 멀티스레드 상황에서 안전하지만, 동기화 오버헤드로 속도가 느리다. 
  • StringBuilder는 멀티스레드 상황에서 안전하지 않지만, 동기화 오버헤드가 없으므로 비교적 속도가 빠르다.
728x90

'프로그래밍 > JAVA' 카테고리의 다른 글

List  (0) 2025.03.28
스레드 생성하기  (0) 2025.03.28
Exception  (0) 2025.03.20
자바 객체 지향  (0) 2025.03.13
자바 기본1  (0) 2025.03.13

 

오버로딩과 오버라이딩의 차이는 뭔가요? 

오버로딩은 메서드 이름, 반환 타입은 동일하지만, 매개변수만 다른 메서드를 구현하는 것입니다. 

 

오버라이딩은 부모클래스 메서드를 자식 클래스가 재정의 하는 것입니다.

이 경우, 메서드 시그니처는 동일합니다. 즉, 메서드 이름, 매개변수, 반환 타입이 같습니다.

 

static, final, private 키워드가 붙은 메서드는 오버라이딩이 불가합니다. 

static 키워드는 클래스 메서드니까 인스턴스 레벨에서 오버라이딩이 안 됩니다. 

final 키워드는 재정의 안된다는 의미이므로 오버라이드 금지입니다. 

private 메서드는 해당 클래스에서만 접근 가능하기 때문에 오버라이딩이 안 됩니다. 

 

 

다형성이 무엇이고, 왜 필요할까요? 

다형성은 서로 다른 객체를 특정 개념에 속하는 것으로 다루는 성질이다. 

예를 들어, 자식 클래스 객체를 부모 클래스 타입에 담아 참조할 수 있다. (부모 클래스는 자식 클래스타입을 담을 수 있다)

클래스 상속 관계를 통해 코드 중복을 줄이는 효과가 있다. 

 

상속은 무엇인가요? 

 

특정 클래스의 기능을 확장하는 기능이다. 

상속으로 코드 중복을 줄일 수 있다

 

자식 클래스 인스턴스를 생성하면, 메모리 내부에 자식과 부모클래스 각각 만들어진다.

→ 따라서 각각의 생성자도 모두 호출되어야 한다!

 

  • extends 상속한 자식 클래스의 생성자는 부모 클래스의 생성자를 반드시 호출해야 한다!
    • 부모 클래스 생성자를 호출할 때는 super() 를 사용하면 된다.
    • 단, 부모 클래스의 생성자가 기본생성자인 경우, super() 를 생략 가능하다.
    • 언젠가는 반드시 부모를 호출 해야 한다.

 

상속의 단점은 무엇이 있을까요? 

부모 클래스를 변경하고 싶을 때, 자식 클래스에 영향을 미칠 수 있다. -> 부모 클래스와 자식 클래스의 강한 결합

상속을 여러번 받게 된다면 복잡한 구조를 갖게 된다. 

상속은 부모 클래스의 구현을 숨길 수 없다.-> 캡슐화와 은닉을 깨뜨릴 수 있다. 

자바는 단일 상속만 허용하므로 신중하게 설계해야 한다. 

 

 

상속과 조합의 차이에 대해 설명해 주세요.

상속은 부모 클래스를 확장하여 자식 클래스를 작성하는 것이다. 

조합(또는 합성 composition) 은 새로운 클래스를 작성하고 조합할 클래스를 private 필드로 참조하는 방식이다. 

 

상속은 클래스 간의 정적인 관계지만 조합은 객체 사이의 동적인 관계다. 런타임에 동적으로 변경할 수 있다. 

 

instanceof 키워드란 무엇인가요? 

참조변수 instanceof 타입 -> 해당 타입으로 캐스팅 가능한지 boolean 타입으로 리턴하는 연산자다 

 

 

instanceof 키워드 사용 시 문제점이 무엇이 있을까요?

다형성을 제대로 활용하지 못한다. 

다형성은 구체 타입에 의존하지 않기 위함인데, instanceof 를 남용하면 다형성을 활용하지 못하게 된다. 

 

 

 

interface 란 무엇일까요? 

  • 추상메서드로 구성된 인터페이스 클래스다. 
  • 여러 클래스가 공통으로 가져야 하는 특정 규약을 약속할 때 사용한다. 
  • 하나의 클래스는 여러개의 interface 를 구현할 수 있다. 

 

interface 와 abstract class 는 어떤 차이가 있나요?

인터페이스

  • 추상 메서드로 구성되어 있다. 메서드 시그니처만 존재하고 body가 없다.
  • interface 를 implements 하여 어떤 메서드들을 포함한 클래스들을 다형성으로 다룰 수 있다.

추상클래스

  • 추상 메소드를 1개 이상 포함하고 있는 클래스다. 덜 구현된 클래스. 
  • 필드와 (메서드 body 를 포함한) 일반 메서드를 포함할 수 있다. 
  • 추상클래스를 extends 하여 재정의한 클래스를 정의하는 용도다.
  • 자바는 단일 상속만 허용하므로 하나의 추상클래스만 상속할 수 있다. 

[참고] 둘의 공통점 : 인스턴스를 생성할 수 없다. 

 

언제 interface 를 사용하고, 언제 abstract class 를 사용하나요? 

인터페이스 사용 예 

  • 특정 기능 구현을 강제하고 싶은 경우에 사용한다. 
  • 상수만 필요할 때 사용한다. 상수 선언 시, public static final 이 자동으로 생략되어 붙는다. 

추상클래스 사용 예 

  • 특정 메서드의 구현이 공통적으로 필요할 때 사용한다. (코드 중복 방지)
  • 자식클래스에 일부 메서드 구현을 강제하고 싶을 때 사용한다. 

 

final 키워드에 대해 설명해주세요. 

final 키워드 의미

  • 값을 재할당 못한다는 의미다. 

자바 상수 

  • 변하지 않는 값을 사용하기 위해 상수를 쓴다 
  • static final 키워드를 사용한다. 
  • 필드명을 대문자와 언더스코어로 구성한다. 

final 을 필드에 사용하는 경우

  • 생성자를 통해 한 번만 초기화할 수 있다. 

 

final 을 지역변수에 사용하는 경우 

  • 값을 최초 한 번만 할당할 수 있다. 
  • 재할당 할 수 없다. 
728x90

'프로그래밍 > JAVA' 카테고리의 다른 글

List  (0) 2025.03.28
스레드 생성하기  (0) 2025.03.28
Exception  (0) 2025.03.20
String 클래스  (1) 2025.03.20
자바 기본1  (0) 2025.03.13

 

Java의 특징에 대해서 설명해주세요  

OS에 상관 없이 이식성이 좋다. write once run anywhere 

  • 한 번 코드를 작성하고 컴파일 하면, OS에 맞는 jvm 설치 후 어디서나 실행할 수 있다. 
  • OS 호환성은 jvm 이 제공하기 때문에 사용자는 jvm 설치 후 자바 프로그램을 실행하기만 하면 된다. 

JVM (자바 가상 머신) 이 메모리를 관리해준다. -> jvm 가비지 컬렉션 기능

  • c언어와는 달리, 자바는 참조되지 않는 heap 영역을 발견하면 자동으로 메모리에서 해제해준다.
  • 따라서 메모리 누수 문제를 어느정도 줄일 수 있다.  

 

Java의 단점에 대해서 설명해주세요 

  • 자바 프로그램 실행을 위해 jvm 을 먼저 로드하고 초기화 해야 하므로 시작시간이 느린 편이다.
  • jvm 위에서 실행해야 하므로 c, c++ 같은 네이티브 언어보다 실행 시간이 느릴 수 있다.
  • 타 언어들에 비해 강타입 언어이며 문법이 많은 편이다.
  • 비교적 코드량이 장황한 편이라 코딩량이 많을 수 있다. 
  • 프론트, ui 개발에 한계를 가진다. 

 

Java 실행 과정에 대해서 설명해주세요 

자바 프로그램은 컴파일 후 실행 가능하다. 

 

1. 개발자가 .java 파일을 작성한다. 

2. 자바 컴파일러가 소스 코드를 컴파일 하여 .java 파일이 .class 파일로 컴파일 된다. 

3. 자바 소스 코드를 바이트코드로 변환하여 jvm 에서 더 빠르게 실행되도록 최적화 한다. 

4. jvm 이 자바 프로그램을 실행해준다. 

 

Java Bytecode에 대해서 설명해주세요.

자바 컴파일러(javac)가 .java 파일을 컴파일해서 .class 파일로 변환한다. 

.class 파일을 바이트코드라고 하고, JVM이 이해할 수 있는 형식이다. 

 

자바 프로그램 실행 시, JVM이 바이트 코드를 로드하고 해석하여 실행한다. 

 

Java의 인터프리터 방식과 JIT 컴파일 방식에 대해 설명해주세요 

  • JVM은 두 방식을 함께 사용한다. 
  • 처음 프로그램이 실행될 때 바이트코드를 한 줄씩 읽어서 즉시 실행한다. 
  • JVM은 자주 사용되는 코드를 모니터링하고 해당 코드는 네이티브 기계어로 컴파일하여 캐시에 저장해둔다. 
  • 같은 코드가 실행될 때 인터프리터 대신 컴파일된 코드가 실행된다. 

 

사용해본 Java 버전과 특징 그리고 왜 그 버전을 사용했는지 설명해주세요. 

자바 17 버전

  • 이전 버전에 비해 컨테이너 환경을 위한 최적화가 향상됬다. 
  • JIT 컴파일러 최적화로 전반적인 실행 속도가 향상됬다. 
  • record class, sealed class 등의 새로운 기능으로 코드 작성이 간결해졌다. 

record class

  • 생성자, getter, toString(), equals(), hashCode() 를 전부 자동으로 생성해준다.
  • 불변 객체를 간편하게 만들 수 있다. 

 

sealed class

  • 해당 부모클래스를 상속 가능한 클래스를 제한할 수 있다. 
  • 의도하지 않은 상속 구조를 방지할 수 있다. 

 

Java 8, 11, 17 버전에 대해 아는대로 설명해주세요. 

java 가 지원하는 대표적인 LTS(Long-Term Support) 버전이다. 

(일반적으로 3년마다 LTS 버전이 출시된다) 

 

 

JDK 와 JRE에 대해서 설명해주세요.

JRE: java runtime environment 

자바 프로그램이 실행되기 위한 환경

 

JDK: java development kit

자바 개발에 필요한 모음이다 

개발을 위해 런타임은 꼭 필요하므로 JRE 를 포함한다. 

 

 

동일성과 동등성에 대해 설명해주세요 

동일성: 같은 메모리 주소를 가진 똑같은 인스턴스인지 여부 

동등성: 같은 타입인지 여부 또는 같은 수준인지 여부

 

equals() 와 == 의 차이점은 무엇일까요? 

== 연산자는 물리적으로 똑같은 주소를 가진 인스턴스인지 비교한다. 동일성을 비교한다 

equals() 는 기본적으로 동일성을 비교하지만, 특정 값을 비교하도록 사용자가 오버라이딩 할 수 있다. 

 

예를 들어, 클래스마다 동등성에 대한 기준이 다를 것이기 때문에 equals 오버라이딩 해서 써야한다. 

 

HashCode 를 설명하고, equals() 와 hashCode() 의 차이점에 대해 설명해주세요 

  • HashCode 란, 데이터를 해시함수에 넣은 결과값 숫자를 뜻한다. 해시 함수의 결과값.
  • 모든 자바 클래스의 부모 클래스인 Object 클래스는 hashCode() 메서드를 구현해놨다.
    • 이 메서드를 그대로 사용하기 보다는 오버라이딩(재정의) 하여 사용한다.
    • Object 클래스의 equals() 기본 구현은 == 연산자로 참조값를 비교한다. 
    • Object 클래스의 hashCode() 기본 구현은 객체의 참조값을 기반으로 해시 코드를 생성한다.
    • 즉, 인스턴스가 다르면 해시 코드도 다르다. 

 

왜 equals() 외에 hashCode() 도 재정의해야 하나요? 

 

개발자가 hashCode() 를 재정의 하지 않으면, Object 클래스에 정의된 Object.hashCode() 가 호출된다. 

즉, 객체의 참조값을 기반으로 해시코드를 리턴하는 것이다. 

HashSet 의 경우, hashCode() 값을 기반으로 비교하는데, 값이 아니라 참조값으로 비교하게 되버린다. 

 

toString() 에 대해서 설명해주세요. 

객체의 정보를 문자열로 제공하는 메서드

→ 디버깅과 로깅에 활용할 수 있다. 

 

자바에서 메인 메서드는 왜 static 으로 되어 있을까요? 

JVM이 프로그램을 실행할 때 가장 처음으로 메인 메서드를 찾는다. 

따라서 런타임에 이미 자바 메서드 영역에 올라와 있어야 찾을 수 있으므로 static 으로 되어 있다. 

 

 

상수와 리터럴에 대해서 설명해주세요.

상수는 변하지 않는 변수를 의미한다.

final 키워드를 붙여서 재할당 할 수 없게 한다. 

 

리터럴은 고정된 구체적인 값을 의미한다.

상수와는 달리 변수가 아니라서 그 값 자체를 의미한다. 

 

 

Primitive Type과 Reference Type 에 대해서 설명해주세요 

primitive type 는 정해진 메모리 크기 만큼 할당하는 타입이다. 

int, double, boolean 같은 기본 자료형이다. 

reference type 은 참조값이다. 객체 자체가 복사되는 것이 아니라, 참조 주소를 가진 복사본이 전달된다. 

참조 자체를 변경해도 원본 객체는 영향받지 않는다. 

 

자바는 Call by Value 일까요? 아니면 Call by Reference 일까요?

자바는 기본적으로 값에 의한 호출이다. 메서드에 인자로 전달된 변수의 값만 복사해서 호출한다. 

참조에 의한 호출은 참조값을 인자로 전달하는 것이다. 이 경우, 참조하는 객체를 변경하면 원본 객체도 변경. 

 

 

자바 직렬화에 대해서 설명해주세요. 

자바 객체를 바이트 스트림으로 변환하는 과정이다. 파일 전송 시 필요하다. 

역직렬화는 바이트 스트립을 다시 객체로 변환하는 과정이다. 

 

자바 직렬화는 꽤 리소스가 소모되는 되는 작업이라 속도라 느리다. 

그래서 JSON, XML 같은 직렬화 방식을 자주 쓴다. 

728x90

'프로그래밍 > JAVA' 카테고리의 다른 글

List  (0) 2025.03.28
스레드 생성하기  (0) 2025.03.28
Exception  (0) 2025.03.20
String 클래스  (1) 2025.03.20
자바 객체 지향  (0) 2025.03.13

+ Recent posts