본문 바로가기

CS Study

8월 3주차


공통 질문

1. 상속과 컴포지션
2. 추상클래스와 인터페이스는 언제 쓰는가?

3. 자바 메모리 구조

     - 스레드


3개 질문이지만 꼬리에 꼬리에 꼬리에 꼬리에.. 토론

 

자바 메모리 구조 이야기하다가

.class 파일을 jvm이 읽는데 java는 독립적이지만 jvm은 os에 영향을 받기 때문에.. 이런 이야기를 하다가

내가 그럼 jar 파일은 .class 파일로 되어있으니 이미 컴파일한 jar파일을 다른 os에 올리면 문제가 되는가?를 물었고..

다음은...

 

1. JAR & WAR
2. GC

3. JVM 동작 방식


 

1. 상속과 컴포지션

 

상속보다 컴포지션을 사용하라 (Favor Composition Over Inheritance)

 

객체 지향 설계에서 권장되는 원칙

실제 프로그래밍에서는 상속보다 컴포지션을 선호하는 경향

유지보수성, 재사용성, 확장성을 높이기 위해 컴포지션을 더 많이 사용하라

 


 

컴포지션(Composition)

개념:

  • 객체가 다른 객체를 자신의 멤버 변수로 포함하여 기능을 재사용
  • 큰 클래스나 복잡한 클래스가 여러 작은 클래스의 기능을 조합하여 구성

특징:

  • "has-a" 관계
    • 컴포지션은 일반적으로 "has-a" 관계를 나타냅니다.
    • Car 클래스는 Engine 클래스를 멤버 변수로 가질 때
    • "차는 엔진을 가지고 있다"는 관계를 형성
  • 유연성
    •  구성 요소를 쉽게 교체하거나 확장할 수 있어 유연성이 높아집니다.
  • 부모 의존성 감소
    • 클래스 간의 결합도가 낮아짐
    • 부모 클래스의 변경이 직접적인 영향을 미치지 않음
// 클래스 1
class Engine {
    void start() {
        System.out.println("Engine starts.");
    }
}

// 클래스 2
class Car {
    private Engine engine;

    Car() {
        this.engine = new Engine();// 클래스 1을 가져옴
    }

    void startCar() {
        engine.start();// 클래스 1 함수 사용
        System.out.println("Car starts.");
    }
}

 


 

상속(Inheritance)

개념:

  • 부모 클래스의 속성과 메서드를 자식이 물려받음
  • 상속을 통해 자식 클래스(서브 클래스)는 부모 클래스의 기능을 재사용하거나 확장

특징:

  • "is-a" 관계
    • 상속 관계는 일반적으로 "is-a" 관계
    • Dog 클래스는 Animal 클래스를 상속받을 때
    • "개는 동물이다" 관계
  • 재사용성
    • 부모 클래스의 코드와 기능을 자식 클래스에서 재사용
  • 다형성(Polymorphism)
    • 자식 클래스는 부모 클래스의 타입으로 참조
    • 자식은 부모가 가진 함수를 Overriding(재정의)

단점:

  • 부모 클래스의 변경이 자식 클래스에 영향
  • 코드 강하게 결합
  • 상속 구조 깊으면, 코드 가독성, 유지보수 어려움
// 부모 클래스
class Animal {
    void sound() {
        System.out.println("Some generic animal sound");// 이미 sound 함수 있음
    }
}

// 자식 클래스
class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Bark");// // sound 함수 -> 자식이 Override(재정의)
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Dog();// 자식 객체 생성
        myAnimal.sound(); // "Bark" 출력 -> 자식이 Override(재정의) 내용으로 출력
    }
}

2. 추상클래스와 인터페이스는 언제 쓰는가?

추상 클래스, 인터페이스 --> 객체 지향 프로그래밍 다형성


 

  • 추상 클래스:
    • 상속 계층 구조에서 사용
    • 기본 구현을 제공하고, 클래스 간의 강한 연관성이 있을 때 적합합니다.
    • 상태(필드)와 구현된 메서드를 포함 할 수 있습니다.
  • 인터페이스:
    • 클래스가 특정 기능을 구현해야 하지만, 클래스 간에 연관성이 없는 경우 사용
    • 다중 상속을 구현해야 할 때, 행동 계약을 설정할 때 적합합니다.
    • 상태를 가질 수 없으며, 기본적으로 구현이 없는 메서드로 구성됩니다.

 

 


 

 

추상 클래스 사용 시기

 

  • 공통 기능을 공유하고 싶을 때:
    • 여러 클래스에서 공통적으로 사용하는 메서드와 필드를 정의할 때 유용합니다.
  • 기본 동작을 제공하고 싶을 때:
    • 공통적인 기본 동작을 구현하고, 하위 클래스에서 추가적인 구현이 필요한 경우에 적합합니다.
  • 상속 계층에서 일관성을 제공할 때:
    • 상속 계층을 통해 코드의 일관성을 유지하고, 하위 클래스에 공통 동작을 강제하고 싶을 때 사용합니다.

 

 

인터페이스 사용 시기

 

  • 계약을 정의하고 싶을 때:
    • 특정 기능이나 계약을 클래스에게 강제하고 싶을 때 사용합니다. 예를 들어, Comparable 인터페이스는 객체를 비교할 수 있는 기능을 제공해야 한다는 계약을 정의합니다.
  • 다중 상속이 필요할 때:
    • 자바는 클래스의 다중 상속을 지원하지 않지만, 인터페이스를 통해 다중 상속이 가능합니다.
  • 다양한 클래스에서 동일한 동작을 제공할 때:
    • 여러 클래스에서 공통적인 메서드를 제공하지만, 구현이 다를 수 있는 경우에 유용합니다.

 

현대 자바 설계 경향

  • 인터페이스를 많이 사용하는 경향:
    • 자바 8 이후 default 메서드와 static 메서드를 지원하면서 인터페이스의 기능이 강화되었습니다.
    • 인터페이스에서도 일부 구현을 제공할 수 있게 되었으며,
    • 다중 상속의 유연성 덕분에 많은 경우에 인터페이스를 사용합니다.
  • 추상 클래스는 필요할 때 사용:
    • 추상 클래스는 공통적인 상태와 동작을 공유하는 데 유용하지만, 상속 구조가 고정되는 단점이 있습니다.
    • 많은 경우에는 인터페이스와 함께 조합하여 사용하는 것이 좋습니다.

 

 

 

 


 

추상 클래스 (Abstract Class)

abstract class Animal { // 추상 클래스
    abstract void makeSound(); // 추상 메서드

    void sleep() { // 구현된 메서드
        System.out.println("Sleeping...");
    }
}

class Dog extends Animal { // 자식 클래스?
    @Override
    void makeSound() {
        System.out.println("Bark");
    }
}

// Dog 는 
// makeSound() 무조건 재정의
// sleep() 은 그냥 써도 됨

특징:

  • 부분 구현 제공
    • 구체 메서드 포함 가능
      • 구현된 메서드는
        • 하위 클래스 재정의도 가능, 재정의 안해도 됨
    • 추상 메서드(구현되지 않은 메서드)
      • 하위 클래스에서 반드시 재정의
  • 상속
    • 한 클래스만 상속받을 수 있다(단일 상속)
  • 상태(필드) 포함 가능
    • 필드(인스턴스 변수)
    • 이 필드를 사용하는 메서드 정의 가능
  • "is-a" 관계
    • 상속 관계

언제 사용하는가:

  1. 기본 구현을 제공하면서도 서브 클래스에서 재정의할 메서드가 있을 때:
    • 일부 메서드를 기본적 구현하고
    • 서브 클래스가 특정 메서드만 Overriding(재정의)하여 사용하는 구조가 필요할 때
  2. 클래스 간의 강한 연관성이 있을 때:
    • 부모-자식 간의 강한 연관성
    • 모든 동물은 공통의 행동과 속성을 가질 수 있다
      • Animal 추상 클래스
        • 공통
          • 필드
            • 입 
            • 다리
          • 메서드
            • 말한다
            • 먹는다
            • 걷는다
      • Human 자식 클래스
      • Dog 자식 클래스

 

인터페이스 (Interface)

interface Drivable { // 인터페이스
    void drive();// drive() 구현 안함
}

class Car implements Drivable {// 인터페이스 받음
    @Override
    public void drive() {// 무조건 drive() 재정의
        System.out.println("Car is driving");
    }
}

class Boat implements Drivable {// 인터페이스 받음
    @Override
    public void drive() {// 무조건 drive() 재정의
        System.out.println("Boat is driving");
    }
}



특징:

  • 구현 없음 (Java 8 이전)
    • 기본적으로 메서드의 구현을 포함하지 않음
    • 메서드의 시그니처(이름, 매개변수, 반환 타입)만 정의
    • Java 8 이후
      • 일부 구현을 포함할 수 있음
      • default 메서드
      • static 메서드
  • 다중 구현 가능
    • 다중 상속
    • 한 클래스가 여러 인터페이스를 구현
  • 상태(필드) 없음
    • 인스턴스 변수를 가질 수 없음
    • 상수만 정의 가능
  • "can-do" 또는 "is-capable-of" 관계
    •  인터페이스는 특정 기능이나 행동을 명시
    • "can-do" 관계

언제 사용하는가:

  1. 서로 관련 없는 클래스들이 특정 행동을 공유할 때:
    • 여러 클래스가 특정 기능을 구현해야 하지만, 클래스 계층 구조에서의 연관성이 없을 때 인터페이스를 사용
    • Car와 Boat는 모두
    • Drivable 인터페이스 구현
  2. 다중 상속이 필요할 때:
    • 클래스가 다양한 행동을 구현해야 할 때 인터페이스를 사용
  3. 행동 계약을 정의할 때:
    • 특정 클래스들이 반드시 구현해야 하는 메서드를 정의
    • 공통의 행동 계약을 설정

 


 

Interface 내부의 default, static method

 

  • 인터페이스에 새로운 기능을 추가
  • 기존의 구현 클래스에 영향을 주지 않음

 

default method

특징:

  • Interface 내에서 구현된 메서드
  • 재정의 안해도 됨 -> 기본적으로 제공되는 메서드
  • 재정의 가능
  • 새로운 메서드를 추가하면서도, 기존의 구현 클래스에 영향을 미치지 않음
interface Vehicle { // Vehicle 인터페이스
    void drive();

    // default 메서드 : 구현 되어있음
    default void startEngine() {
        System.out.println("Engine started.");
    }
}

class Car implements Vehicle {
    @Override
    public void drive() {
        System.out.println("Car is driving.");
    }
}
// Car는
// 무조건 drive() 재정의
// startEngine() 함수 사용 가능

class Bike implements Vehicle {
    @Override
    public void drive() {
        System.out.println("Bike is driving.");
    }

    // default 메서드 재정의
    @Override
    public void startEngine() {
        System.out.println("Bike doesn't have an engine!");
    }
}

// Bike는
// 무조건 drive() 재정의
// startEngine() 함수 사용 가능하지만 재정의 가능

public class Main {
    public static void main(String[] args) {
        Vehicle car = new Car();
        car.startEngine();  // "Engine started." 출력
        car.drive();        // "Car is driving." 출력

        Vehicle bike = new Bike();
        bike.startEngine(); // "Bike doesn't have an engine!" 출력
        bike.drive();       // "Bike is driving." 출력
    }
}

 


static method

특징:

  • 객체를 생성하지 않고도 Interface 이름을 통해 직접 호출 가능
  • static 메서드는 Interface 내에서 고유하게 동작
  • Interface를 구현하는 클래스에서 접근, 재정의 불가능
interface Calculator { // 인터페이스 Calculator
    int add(int a, int b);// 반드시 재정의

    // static 메서드 : 재정의 불가능
    static int multiply(int a, int b) {
        return a * b;
    }
}

class SimpleCalculator implements Calculator {// SimpleCalculator 구현 클래스
    @Override
    public int add(int a, int b) {// 반드시 add(int a, int b) 재정의
        return a + b;
    }
}

public class Main {
    public static void main(String[] args) {
    // SimpleCalculator 객체 생성
        Calculator calculator = new SimpleCalculator();
        System.out.println("Addition: " + calculator.add(5, 10)); // 15 출력
        // SimpleCalculator 객체로 add()함수 사용

        // static 메서드 호출, 객체 생성 없이 가능
        System.out.println("Multiplication: " + Calculator.multiply(5, 10)); // 50 출력
    }
}

 


3. 자바 메모리 구조

  • JVM(Java Virtual Machine)이 프로그램을 실행할 때
  • 메모리를 어떻게 관리하고 사용하는지

 


 

stack 영역을 잘 보자.. 변수.. 주소값..
메서드, 힙 <--------> 스택

  • 공유 자원
    • Method Area
      • 클래스, 메서드 실행 코드
      • static 변수
      • 상수 풀
    • Heap
      • 객체
      • 배열
      • String 객체
  • 스레드별 독립적
    • Stack
      • 메서드 호출 시 생성되는
        • 지역 변수
        • 중간 연산 결과
        • 호출 정보
    • Program Counter Register
      • 현재 실행 중인 명령어의 주소를 저장
    • Native Method Stack
      • 네이티브 메서드의 호출 정보를 저장

 


1. 메소드 영역(Method Area)

  • 역할:
    • 공유 자원 저장
      • 클래스 정보 메서드
      • 변수 이름
      • 필드 정보
      • 메서드의 바이트코드
      • 상수 풀(constant pool)
    • JVM이 시작될 때 생성되며
    • 모든 스레드가 공유
  • 저장되는 것:
    • 클래스/인터페이스의 메타데이터
    • 클래스 변수(Static 변수)
    • 상수(Constant Pool)
    • 메서드, 생성자 코드

 

Static 변수:

  • 저장 위치: Static 변수는 **메소드 영역(Method Area)**에 저장됩니다. 클래스가 메모리에 로드될 때 한 번 초기화되며, 프로그램이 종료될 때까지 유지됩니다.
  • 역할: 클래스에 속하는 변수로, 클래스의 모든 인스턴스가 공유하는 값입니다. static 키워드를 사용하여 선언하며, 프로그램 실행 중에 값을 변경할 수 있습니다.
  • 특징: 모든 객체가 동일한 메모리 공간을 참조하며, 클래스 이름으로 직접 접근할 수 있습니다.
public class Example {
    static int count = 0; // static 변수

    public Example() {
        count++; // 값이 변경 된다
    }
}

 

 

상수(Constant Pool):

  • 저장 위치: 클래스 파일 내 또는 JVM 런타임 시 생성
    • 주로 상수(리터럴 값)와 심볼릭 참조(Symbolic Reference)를 저장하는 특수한 메모리 공간
    • 클래스 로드될 때 상수 풀 -> 메소드 영역
    • 런타임 상수 풀 -> 힙 영역
  • 역할: 동일한 값을 여러 번 사용할 때 메모리를 절약하고 성능을 향상시키기 위해 상수 값을 저장하고 재사용합니다.
    • 상수 풀에 저장된 값은 변경할 수 없는 불변(immutable) 데이터
  • 특징: 상수 풀에는 문자열 리터럴, 숫자 리터럴, 메서드 참조 등이 포함됩니다.
    • 예를 들어, 문자열 "Hello"가 코드에서 여러 번 사용되면, 이 문자열은 상수 풀에 한 번만 저장되고 재사용됩니다
public class RuntimeConstantPoolExample {
    public static void main(String[] args) {
        // "Hello" 문자열 리터럴은 런타임 상수 풀에 저장됩니다.
        String str1 = "Hello";

        // 동일한 문자열 리터럴은 상수 풀에서 재사용됩니다.
        String str2 = "Hello";

        // 새로운 String 객체를 힙 영역에 생성합니다.
        String str3 = new String("Hello");

        // str1과 str2는 상수 풀에서 같은 참조를 가리킵니다.
        System.out.println(str1 == str2);  // 출력: true

        // str3은 새로운 객체이므로 상수 풀의 참조와 다릅니다.
        System.out.println(str1 == str3);  // 출력: false

        // str3의 intern() 메서드를 사용하여 상수 풀에서 동일한 리터럴을 참조하게 만듭니다.
        String str4 = str3.intern();

        // 이제 str1과 str4는 동일한 참조를 가리킵니다.
        System.out.println(str1 == str4);  // 출력: true
    }
}
// NUMBER와 TEXT 값은 클래스 상수 풀에 저장
public class Example {
    public static final int NUMBER = 10;
    public static final String TEXT = "Hello";
}

2. 힙 영역(Heap)

  • 역할:
    • 객체, 배열 저장되는 메모리 영역
    • 동적으로 할당된 메모리들이 저장
    • GC(Garbage Collector)에 의해 관리
    • 모든 스레드가 공유
  • 저장되는 것:
    • 인스턴스 변수(객체)
    • 배열
    • String 객체
    • 객체의 실제 데이터
  • Garbage Collection (GC)
    • 힙 영역에서 더 이상 참조되지 않는 객체들을 자동으로 메모리에서 제거하여 메모리 누수를 방지
    • GC가 실행되는 동안 애플리케이션의 성능에 영향을 줄 수 있다

 

 

3. 스택 영역(Stack)

  • 역할:
    • 각 스레드마다 별도로 할당
    • 메서드 호출 시 생성되는 스택 프레임(Stack Frame)을 저장
      • 스택 프레임
        • 지역 변수
        • 메서드의 호출과 관련된 정보
        • 반환 주소
    • LIFO(Last In, First Out) 방식으로 메모리를 관리합니다.
  • 저장되는 것:
    • 지역 변수(Local Variables)
    • 메서드 호출 정보(메서드 실행을 위한 스택 프레임)
    • 기본 데이터 타입(int, float 등)
    • 참조 변수(Reference Variable)

4. PC 레지스터(Program Counter Register)

  • 역할:
    • 현재 실행 중인 JVM 명령어의 주소를 저장하는 공간
    • 각 스레드마다 독립적으로 존재하며
    • 스레드가 어느 부분을 실행하고 있는지를 기록
  • 저장되는 것:
    • 현재 실행 중인 명령어의 주소

 


 

5. 네이티브 메소드 스택(Native Method Stack)

  • 역할:
    • 자바 외의 다른 언어(C, C++ 등)로 작성된 네이티브 메서드를 실행할 때 사용되는 메모리 공간입니다.
    • 네이티브 메서드 호출 시 필요한 스택 프레임이 저장됩니다.
  • 저장되는 것:
    • 네이티브 메서드 호출 정보
    • 로컬 변수

 

 


Thread 생성 방법은? 

1. Thread 클래스 상속 

2. Runnable 인터페이스 구현 (실제로 사용 많이 함) 

 


 

1. Thread class 상속

장점: 

  • run을 재정의만 하면 된다 -> 간단
  • Thread class 기능 다 쓸 수 있다

단점:

  •  상속 제한: 단일 상속만 가능, 다른 클래스를 상속 받을 수 없다 
// Thread를 상속받아 스레드의 동작을 정의하는 클래스
public class MyThread extends Thread {
    
    @Override
    public void run() {
        // 스레드가 실행할 작업을 정의
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getId() + " Value: " + i);
            try {
                // 1초 대기
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("Thread interrupted");
            }
        }
    }
    
    public static void main(String[] args) {
        // 스레드 객체 생성
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        
        // 스레드 시작
        thread1.start();
        thread2.start();
    }
}

 

2. Runnable 인터페이스 구현

유연, 유지보수 쉬움 

 

장점: 

  • 상속에서 자유롭다 
  • 작업 내용과 스레드 분리 
  • 작업과 실행을 분리 
  • 쓰레드 모든 기능 가져올 필요 없잖니, 원하는 작업만 코드 작성 -> 코드 가독성 증가 
  • 여러 스레드가 동일한 Runnable 객체를 공유 -> 자원관리 효율 
  • 작업을 생성자로 전달

 

Multi Thread 

1) helloThread.start() 호출 전까지는 단순한 객체 덩어리 

2) start()를 호출해야 thread의 stack 공간을 새로 만든다 

3) main이 helloThread.start() 을 호출하는게 아니라 너가 알아서 해! 위임함 

4) main, Thread-0 이 동시에 실행 된다 

5) 스레드는 순서, 실행 기간을 모두 보장하지 않는다 

6) 이것이 멀티스레드

 


문제

클래스 변수에 저장하면 동시성 문제가 발생할 수 있다

 

클래스 변수 ->  method area -> 모든 객체 인스턴스가 공유하는 변수

멀티스레드 환경에서는 여러 스레드가 동시에 이 변수를 읽고 쓰게 되는데,

이 과정에서 데이터의 무결성이 깨질 수 있습니다.

 


해결 방법

동시성 문제를 방지하기 위해 synchronized, ReentrantLock, Atomic 클래스 등을 사용

 

 

동시성 문제가 발생하는 이유

  1. 공유 자원:
    • method area
      • 여러 스레드가 동시에 변수에 접근
  2. 레이스 컨디션(Race Condition):
    • 여러 스레드가 동시에 클래스 변수의 값을 읽거나 수정할 때,
    • 각 스레드가 작업을 완료하는 순서가 예측 불가능
    • 연산 결과가 올바르지 않게 되는 문제가 발생
  3. 원자성 문제:
    • 클래스 변수에 대한 연산이 원자적이지 않다면
    • 읽기, 수정, 쓰기 연산이 동시에 일어날 때
    • 중간 상태의 값이 다른 스레드에 의해 읽혀지거나 변경될 수 있다
    • 이로 인해 데이터가 손상될 수 있습니다.

 

동시성 문제 방지 방법

  1. synchronized 키워드 사용:
    • 메서드나 블록에 synchronized를 사용
    • 한 번에 하나의 스레드만 해당 코드 블록에 접근할 수 있도록 합니다.
public class Example {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }
}

 

2. ReentrantLock 사용:

  • java.util.concurrent.locks.ReentrantLock을 사용하여 동시성을 제어할 수 있습니다.
  • ReentrantLock은 더 세밀한 제어와 기능을 제공
  • lock()과 unlock()을 통해 명시적으로 락을 관리
import java.util.concurrent.locks.ReentrantLock;

public class Example {
    private static int count = 0;
    private static ReentrantLock lock = new ReentrantLock();

    public static void increment() {
        lock.lock();// 락
        try {
            count++;
        } finally {
            lock.unlock();// 언락
        }
    }
}

 

 

3. Atomic 클래스 사용:

  • java.util.concurrent.atomic 패키지에서 제공하는 AtomicInteger, AtomicLong 등의 클래스를 사용
  • 원자성을 보장하는 연산 가능
  • 이 클래스들은 내부적으로 원자적 연산을 제공하여 동시성 문제를 방지
import java.util.concurrent.atomic.AtomicInteger;

public class Example {
    private static AtomicInteger count = new AtomicInteger(0);

    public static void increment() {
        count.incrementAndGet();
    }
}

 

 

4. volatile 키워드 사용:

  • volatile 키워드는 변수의 값을 읽고 쓰는 작업이 다른 스레드에서 즉시 반영 되게 만든다
  • 하지만 이 키워드는 원자성을 보장하지 않음
  • 단순한 읽기/쓰기 만 가능
public class Example {
    private static volatile int count = 0;

    public static void increment() {
        count++;
    }
}
//count++ 연산은 여전히 원자적이지 않으므로 volatile만으로는 동시성 문제를 완전히 해결하지 못합니다.

'CS Study' 카테고리의 다른 글

8월 4주차 GC  (0) 2024.08.19
8월 4주차 JDK, JRE, JVM  (0) 2024.08.19
8월 4주차 jar, war  (0) 2024.08.19
8월 2주차 - 개인 질문  (0) 2024.08.05
8월 2주차 - 공통 질문  (0) 2024.08.05