본문 바로가기
Study/Study Alone

[STUDY] 1주차

by 햄리뮤 2024. 12. 6.
반응형

으어어어 스터디 시작이다!

힘내자!

[Q]

  1. 자바는 왜 컴파일러가 기계어를 만들어내는 대신 JVM 을 통해서 중간 형태(.class)의 명령어들을 실행할까?
  2. 객체지향의 4대 원리를 조사해봅시다.
  3. 힙영역과 스택영역의 차이는 무엇일까요? 스택 영역이라는 이름은 어디에서 유래한걸까요?
  4. 초기화가 되지 않은 String 타입의 변수에서 값을 읽어오면 어떻게 될까요? NullPointerException 은 어떤 상황에 발생할까요?
  5. 연산자들의 우선순위에 대해서 알아봅시다.

[A]

1. 자바는 왜 컴파일러가 기계어를 만들어내는 대신 JVM 을 통해서 중간 형태(.class)의 명령어들을 실행할까?

플랫폼 독립성 (Write Once, Run Anywhere)

  • 자바의 주요 설계 목표는 한 번 작성한 코드를 여러 플랫폼에서 실행할 수 있다는 것 이다! (자바의 큰 장점!)
  • 컴파일러가 생성하는 바이트코드는 특정 운영 체제나 하드웨어가 아니라, JVM이라는 가상 머신에서 실행되도록 설계되어있다!
  • 이로 인해 자바 애플리케이션은 다양한 플랫폼에서 추가적인 수정 없이 실행 될 수 있다! JVM만 설치되어 있다면 바이트코드를 (바이트코드를 기계어로 변환 하는 JIT 컴파일로) 실행할 수 있음!

이식성(Protability)

  • JVM은 각 운영 체제와 하드웨어에 맞게 구현된다.
  • 자바 프로그램은 기계어로 번역되지 않고, 하드웨어에 의존하지 않는 바이트코드로 컴파일된다. JVM이 바이트코드를 해당 플랫폼의 기계어로 변환해 실행하기 때문에 이식성이 뛰어나다!

보안(Security)

  • JVM은 프로그램 실행 전에 바이트코드를 검증(Bytecode Verification)한다. 이를 통해 악의적인 코드 실행을 방지하고, 메모리 접근 오류와 같은 문제를 줄일 수 있다.
  • 직접 기계어로 컴파일된 프로그램은 실행 시 이런 검증 단계를 거치지 않기 때문에 보안 취약점이 더 많을 수 있다.

가상 머신의 추상화(Abstraction)

  • JVM은 하드웨어나 운영 체제의 세부 사항을 숨겨준다. 개발자는 특정 플랫폼의 세부 사항을 신경 쓰지 않고, 표준화된 환경에서 개발할 수 있다.
  • 이는 다양한 플랫폼 간의 차이를 고려하지 않아도 되는 장점을 제공한다.

동적 최적화(Dynamic Optimization)

  • JVM은 실행 중에 프로그램의 성능을 최적화 하는 JIT(Just-In-Time) 컴파일러를 사용한다. JIT는 바이트 코드를 기계어로 변환하고, 자주 실행되는 코드를 더 빠르게 실행되도록 최적화 한다.
  • 이로 인해 실행 속도가 인터프리터 기반 언어(컴파일 대신 한줄씩 해석(interpret) 하면서 실행하는 언어. Python, JavaScript 등)에 비해 훨씬 빠르며, 정적 컴파일 언어와 비슷한 수준의 성능을 제공한다.

멀티플랫폼 개발 생태계 지원

  • JVM이 표준화된 인터페이스를 제공하기 때문에, 자바뿐만 아니라 Kotlin, Scala, Groovy 등 JVM 기반 언어들이 같은 플랫폼에서 실행될 수 있다.
  • 다양한 언어와 툴이 조화롭게 동작하는 환경을 제공한다.

깊게 생각해보기

  1. 바이트코드 검증 말고 다른 검증은 없을까? 그리고 왜 다른 검증은 선택하지 않았을까?
    • 소스코드 검증 - 소스코드를 직접 검증하는 방식도 가능하지만, 문제점이 있음
      • 다양한 언어 처리 문제: JVM은 자바 외에도 Kotlin, Scala 등 다양한 언어를 지원한다.
      • 소스코드 검증은 각 언어에 맞게 별도 로직이 필요하다.
      • 느린 속도: 소스코드 검증은 복잡하고 시간이 많이 소요될 수 있다.
    • 런타임 검증 - 프로그램 실행 중에 실시간으로 검증하는 방식도 있지만 단점이 큼
      • 실행 중에 오류가 발생하면 이미 시스템에 영향을 줄 수 있어 위험하다.
      • 런타임에 추가적인 검증 비용이 들기 때문에 성능이 저하될 수 있다.
    • 네이티브 코드 검증 -  바이트코드가 아닌 네이티브 기계어를 직접 검증하는 방식
      • CPU 아키텍처마다 명령어 구조가 다르기 때문에 검증이 복잡해진다.
      • 플랫폼 독립성을 잃게 된다.
  2. JVM이 다른 플랫폼에서 실행 될때 바이트코드 검증을 통해 어떤 방식으로 오류를 방지할 수 있는 것 일까?
    • 로컬 변수에 접근하기 전에 올바르게 초기화 되었는지 확인.
    • 스택의 상태(데이터의 타입, 크기 등)를 추척해 적절하게 사용되고 있는지 확인.
    • 허용되지 않은 메서드 호출이나 필드 접근을 차단.
    • 배열 범위를 벗어난 접근을 방지.
  3. JVM이 숨겨주는 세부사항은 어떤 것들이 있을까? 그리고 이를 어떻게 알고 숨겨주는 것 일까?
    • 숨기는 부분
      • 메모리 관리
        • 메모리 할당 및 해제 방식(C언어처럼 수동으로 메모리를 관리하지 않아도 됨).
        • 스택과 힙 같은 메모리 구조를 운영 체제에 따라 적절히 설정.
        • 숨기는 방법
          • JVM은 힙과 스택 영역을 자동으로 관리한다.
          • 가비지 컬렉션을 통해 더이상 사용하지 않는 메모리를 자동으로 해제한다.
      • 스레드 관리
        • 운영 체제마다 스레드의 생성, 스케줄링, 동기화 방식이 다름.
        • 숨기는 방법
          • 자바는 Thread 클래스와 synchronized 키워드를 통해 스레드 작업을 단순화 한다.
          • JVM은 운영 체제의 네이티브 스레드 API를 내부적으로 호출해 자바의 표준 스레드 인터페이스를 구현한다.
      • 파일 시스템 접근
        • 운영 체제별 파일 경로 형식,파일 처리 방식의 차이.
        • 숨기는 방법
          • 자바의 File 클래스나 Paths API는 운영 체제의 파일 경로 형식을 추상화 한다.
          • 내부적으로 JVM이 적절한 OS 호출을 통해 파일을 읽고 쓴다.
      • 네트워크 통신
        • 네트워크 소켓 생성 및 통신 방식(OS마다 다른 네이티브 구현).
        • 숨기는 방법
          • 자바는 Socket 클래스와 같은 고수준 API를 제공.
          • JVM은 내부적으로 TCP/IP 소켓을 처리하고, 운영 체제와 직접 통신한다.
      • 기계어 명령 처리
        • CPU 아키텍쳐와 명령어 세트 차이.
        • 숨기는 방법
          • JVM 바이트코드는 하드웨어에 독립적인 명령어로 작성된다.
          • 실생 시 JIT 컴파일러가 이를 해당 플랫폼의 기계어로 변환한다.
      • 운영 체제별 차이점
        • 운영 체제마다 다른 API 호출 방식, 스케줄링 정책, 리소스 제한.
        • 숨기는 방법
          • JVM 구현체가 운영 체제별로 커스터마이징 된다.
    • JVM은 어떻게 알고 숨겨줄까?
      • 바이트코드의 추상화
        • 자바 프로그램은 .java 파일에서 바이트코드(.class)로 컴파일 된다.
        • 이 바이트코드는 JVM이 이해할 수 있는 하드웨어 독립적인 명령어 집합으로, 하드웨어나 운영 체제의 세부 사항이 포함되지 않습니다.
      • 플랫폼별 JVM 구현
        • Oracle, OpenJDK 등의 JVM 구현체는 각 플랫폼의 세부 사항을 처리하는 코드를 포함한다.
        • 예를 들어, Window에서 JVM은 Window API를 호출해 파일 작업을 처리하고, Linux에서는 POSIX API를 호출하도록 설계 된다.
      • 표준화된 자바 클래스 라이브러리
        • java.* 패키지는 개발자가 플랫폼에 독립적인 코드를 작성할 수 있도록 표준화된 인터페이스를 제공한다.
        • JVM은 이 표준 API와 네이티브 운영 체제 호출 사이를 매핑한다.
    • 결론
      • JVM은 바이트코드의 추상화와 플랫폼에 특화된 구현을 조합해 하드웨어와 OS의 차이를 숨긴다.
      • 자바 개발자는 운영체제의 복잡한 세부사항을 몰라도 된다.
  4. JVM은 다양한 플랫폼에서 동작하도록 설계 되었는데 단점은 어떤 것 들이 있을까?
    • JVM은 운영 체제에 독립적이지만, 이로 인해 플랫폼의 특화된 기능(GPU 계산 최적화 등)을 효율적으로 활용하지 못할 수 있다.
    • 추가적인 추상화 계층(JVM)이 존재하기 때문에, 네이티브 코드에 비해 실행 초기 속도가 느리고, 메모리 사용량도 많아질 수 있다.
  5. JIT는 각각의 플랫폼에서 어떤 기준으로 최적화를 하는 것 일까?
    • 프로파일링 데이터(실행 통계)를 기반으로 최적화를 적용한다. 자주 호출되거나 반복적으로 실행되는 코드를 핫스팟(Hotspot)으로 식별하여 네이티브 코드로 컴파일하거나 인라이닝(함수를 코드에 직접 삽입) 같은 최적화를 수행한다.

 

모르는 개념 정리

  • JIT(Just-In-Time) 컴파일은 무엇인가?
    • 핵심
      • 바이트코드를 실행 도중에 즉시(JIT) 기계어로 번역한다.
      • 번역된 기계어 코드는 캐싱되므로 같은 코드를 반복 실행할 때 더 빠르게 실행된다.
    • 장점
      • 빠른 실행 속도 - 코드 번경 후 곧바로 실행 가능하므로 디버깅이나 테스트에 유리하다.
        • 인터프리터(프로그래밍 언어의 코드를 한 줄씩 읽고, 해석하여 즉시 샐행하는 프로그램) 방식만 사용했을 때보다 훨씬 빠르다.
          • 인터프리터 vs 컴파일러
            • 컴파일러는 코드를 한 번에 번역하여 실행 파일(기계어)을 생성한 뒤 실행한다.
            • 반면, 인터프리터는 번역과 실행을 동시에 수행한다.
        • 반복적으로 실행되는 코드를 기계어로 변환해 실행 시간을 절약한다.
      • 동적 최적화
        • 실행 도중에 코드의 성능을 분석해 최적화를 수행한다.
        • 예: 불필요한 연산 제거, 루프 최적화 등.
      • 플랫폼 독립성 유지
        • JIT JVM 안에서 동작하기 때문에 Java의 플랫폼 독립성 특징을 해지치 않는다.
    • 동작 방식
      • 초기 실행
        • JVM은 처음엔 바이트코드를 인터프리터로 실행한다.
        • 한 줄씩 해석하고 실행하기 때문에 처음엔 속도가 느릴 수 있다.
      • 성능 최적화 단계
        • JIT 컴파일러가 프로그램의 핫스팟(Hotspot)을 찾아낸다.
          • 핫스팟: 자주 실행되는 코드(반복문, 주요 함수 등).
        • 이런 코드만 기계어로 번환하여 실행 속도를 높인다.
      • 캐싱
        • 변환된 기계어 코드는 저장되어 이후 실행 시 재사용 된다.
        • 이렇게 하면 반복적으로 실행되는 코드의 성능이 대폭 향상 된다.

2. 객체지향의 4대 원리를 조사해봅시다.

추상화

  • 정의
    • 중요한 정보만 남기고, 불필요한 세부 사항은 감추는 것을 말한다.
  • 장점
    • 시스템의 복잡성을 줄이고 이해하기 쉽게 만든다.
    • 유지보수와 확장이 용이해진다.

캡슐화

  • 정의
    • 데이터와 메서드를 하나의 객체로 묶고, 외부에서 접근을 제한하여 데이터를 보호하는 것을 의미한다.
    • 접근 제어자(private, protected, public)를 사용하여 필드와 메서드의 접근 범위를 제어한다.
    • 데이터를 직접 접근하지 않고, Getter/Setter 메서드를 통해 간접적으로 접근하게 한다.
  • 장점
    • 데이터의 무결성을 유지할 수 있다.
    • 코드의 재사용성과 유지보수성이 높아진다.

상속

  • 정의
    • 기존 클래스(부모 클래스)의 속성와 메서드를 다른 클래스(자식 클래스)에게 물려주는 것을 말한다.
    • 코드의 재사용성을 높이고 계층 구조를 형성한다.
  • 장점
    • 코드의 중복을 줄이고 재사용성을 높인다.
    • 계층 구조를 통해 논리적으로 설계를 정리할 수 있다.

다형성

  • 정의
    • 하나의 객체가 여러가지 형태를 가질 수 있는 성질을 말한다.
    • 동일한 메서드나 연산이 다양한 객체에서 다르게 동장할 수 있다.
  • 종류
    • 컴파일타임 다형성(오버로딩) - 같은 이름의 메서드가 다양한 매게변수로 동작하도록 하는 것.
    • 런타임 다형성(오버라이딩) - 부모 클래스의 메서드를 자식 클래스가 재정의하여 실행 시점에 다른 동작을 수행하도록 하는 것.
  • 장점
    • 코드의 유연성과 확장성을 높인다.
    • 객체간의 관계를 보다 유연하게 설계할 수 있다.

깊게 생각해보기

  1. 추상화로 인해서 클래스파일 자체가 많아지는 것은 최종적으로 프로그램에 큰 영향이 없을까?
    • 단기적으로 복잡해 보일 수 있지만, 적절하게 관리된다면 프로그램의 유지보수성, 확장성, 안정성 측면에서 긍정적인 효과를 가져온다.
    • 클래스 파일이 많아지는 것이 최종적으로 미치는 영향
      • 긍정적인 영향
        • 모듈화 및 재사용성
          • 추상화를 통해 기능이 명확히 분리된 클래스는 독립적으로 개발, 테스트, 재사용할 수 있다.
          • 새로운 요구사항이 생길 때 기존 코드를 수정하지 않고 기능 추가가 가능하다.
        • 유지보수성 향상
          • 클래스가 적절히 분리되어 있다면, 특정 클래스의 변경이 다른 부분에 미치는 영향을 최소화할 수 있다.
          • 코드가 분리되어 있으면 버그 수정과 확장이 용이하다.
        • 확장성
          • 추상 클래스를 기반으로 새로운 구현체를 추가하기 쉬워지므로, 프로그램 확장이 용이하다.
          • 인터페이스 기반 설계를 통해 다양한 구현을 사용할 수 있는 유연성을 확보할 수 있다.
      • 부정적인 영향(잘못된 설계의 경우)
        • 복잡성 증가
          • 클래스가 불필요하게 세분화되면 코드가 지나치게 복잡해지고, 개발자 간 협업시 혼란을 초래할 수 있다.
        • 성능 영향
          • 클래스가 많아지면서 JVM의 클래스 로딩 시간이나 메모리 사용량이 증가할 수 있다.
          • 하지만 대부분의 경우 이는 무시할 수 있을 정도로 작은 영향이며, JIT 컴파일러와 클래스 로더가 최적화를 수행한다.
        • 관리의 어려움
          • 잘못 설계된 시스템에서는 관련 없는 클래스 간의 종속성이 늘어나고, 코드를 추적하기 어려워질 수 있다.

3. 힙영역과 스택영역의 차이는 무엇일까요? 스택 영역이라는 이름은 어디에서 유래한걸까요?

힙 영역(Heap)

  • 특징
    • 동적 메모리 할당: 프로그램 실행 중 개발자가 필요에 따라 메모리를 할당하고 해제하는 공간이다.
    • 크기: 힙은 상대적으로 크고, 고정된 크기가 아닌 시스템의 가용 메모리를 활용한다.
    • 데이터 저장: 객체, 배열과 같은 동적 데이터가 저장된다.
    • 관리 방식: 힙은 개발자가 직접 메모리를 할당(new 사용) 하고 필요 없으면 해제해야 하지만, Java 같은 언어는 가비지 컬렉터(Garbage Collector)가 자동으로 해제해 준다.
    • 접근 속도: 스택보다 느리다. 메모리 할당과 해제가 더 복잡하기 때문이다.
    • 생존 기간: 힙에 할당된 메모리는 프로그램이 끝날 때까지 유지 되거나, 가비지 컬렉터가 수거할 때까지 남아 있다.
  • 예제
    • arr 배열은 힙에 생성된다!
class Main {
    public static void main(String[] args) {
        int[] arr = new int[10]; // 힙에 메모리 할당
    }
}

스택 영역(Stack)

  • 특징
    • 정적 메모리 할당: 함수 호출 시점에 자동으로 할당되고, 함수가 끝나면 자동으로 해제된다.
    • 크기: 스택은 고정된 크기로 운영체제에서 관리하며, 보통 크기가 작다.
    • 데이터 저장: 메서드의 지역 변수, 매개변수, 함수 호출 스택 프레임(Stack Frame)이 저장된다.
    • 관리 방식: 개발자가 메모리를 직접 관리하지 않아도 된다. 함수 호출 시점에서 자동으로 메모리를 할당하고 해제 한다.
    • 접근 속도: 힙보다 빠르다. 메모리가 단순한 구조로 관리되지 때문이다.
    • 생존 기간: 스택에 저장된 변수는 함수가 종료되면 메모리에서 해제 된다.
  • 예제
    • x, y, z는 모두 스택에 저장된다!
class Main {
    public static void main(String[] args) {
        int x = 10; // 스택에 저장
        method(x);
    }

    static void method(int y) {
        int z = y + 1; // y와 z는 스택에 저장
    }
}

힙과 스택의 비교

구분 힙(Heap) 스택(Stack)
메모리 할당 방식 동적 메모리 할당(런타임) 정적 할당(컴파일 시점 결정)
관리 주체 개발자 또는 가비지 컬렉터 자동으로 관리
속도 느림(메모리 탐색 및 할당 오버헤드 존재) 빠름 (LIFO 구조로 접근)
데이터 저장 동적으로 생성된 객체 및 데이터 지역 변수, 매개변수, 함수 호출 정보
생명 주기 개발자가 할당/해제(new/delete)하거나 가비지 컬렉터가 처리 함수 종료 시 자동 해제
구조 비 선형적(메모리 관리자가 빈 공간에 할당) 선형적(Last-In-First-Out, LIFO)
가비지 컬렉션 있음 (Java나 Python 등에서 불필요한 메모리를 정리) 없음 (자동으로 해제됨)

스택이라는 이름의 유래

스택(Stack)이라는 이름은 데이터가 쌓이는 방식 (LIFO: Last In, First Out)에서 유래 되었다.

  • 스택 영역은 함수 호출 시 스택 프레임이라는 구조체가 스택에 쌓이고, 함수 종료 시 하나씩 제거된다. 이 동작 방식이 자료구조 스택과 유사하기 때문에 스택 영역이라 부른다.
    • 함수가 호출되면 스택에 프레임이 추가(Push) 된다.
    • 함수 실행이 끝나면 해당 프레임이 스택에서 제거(Pop) 된다.

4-1. 초기화가 되지 않은 String 타입의 변수에서 값을 읽어오면 어떻게 될까요? 

  • 지역 변수 (Local Variable)
    • 반드시 초기화해야 하며, 초기화 하지 않고 값을 읽으려고 하면 컴파일 에러가 발생한다.
public class Example {
    public static void main(String[] args) {
        String localVar; // 초기화하지 않음
        System.out.println(localVar); // 컴파일 에러 발생
    }
}
  • 인스턴스 변수(Instance Variable)
    • 초기화 하지 않으면 기본값으로 null이 자동으로 설정 된다.
public class Test {
    String str; // 초기화하지 않음, 기본값은 null

    public static void main(String[] args) {
        Test test = new Test();
        System.out.println(test.str); // 출력: null
    }
}

4-2. NullPointerException 은 어떤 상황에 발생할까요?

  • NullPointerException은 null 값을 참조하려는 잘못된 접근이 이루어질 때 발생한다.
  • 메서드 호출 시
    • null 값을 가진 객체로 메서드를 호출하려고 할 때.
String str = null;
System.out.println(str.length()); // NullPointerException 발생
  • 필드 접근시
    • null 객체의 맴버 필드에 접근하려고 할 때.
Example obj = null;
System.out.println(obj.instanceVar); // NullPointerException 발생
  • 배열 접근시
    • null 참조를 가진 배열에 접근하려고 할 때.
int[] arr = null;
System.out.println(arr.length); // NullPointerException 발생
  • Wrapper 클래스에서 언박싱 시
    • null 값을 가진 Wrapper 객체를 기본형으로 변환하려고 할 때.
Integer num = null;
int value = num; // NullPointerException 발생

NullPointerException 예방 방법

  • 초기화
    • 변수를 선언할 때 항상 적절한 값으로 초기화 한다.
  • null 체크
    • 메서드 호출이나 필드 접근 전에 null 인지 확인한다.
  • Optional 사용 (Java 8이상)
    • Optional 클래스를 사용하여 null 가능성을 안전하게 처리한다.

5. 연산자들의 우선순위에 대해서 알아봅시다.

  • 가장 높은 우선순위(묶음 연산자)
    • 괄호 ()로 묶인 부분은 무조건 먼저 계산된다.
int result = (3 + 5) * 2; // 괄호 안을 먼저 계산: 8 * 2 = 16
  • 단항 연산자
    • 변수나 값을 하나만 다루는 연산자들.
      • 증감 연산자: ++, --
      • 부호 연산자: +, -
      • 논리 부정: !
int a = 5;
int b = ++a + 2; // ++a 먼저 실행: a=6, b=8
  • 산술 연산자
    • 덧셈, 뺄셈보다 곱셈, 나눗셈, 나머지가 우선순위가 높다.
      • 곱셈/나눗셈/나머지: *, /, %
      • 덧셈/뺄셈: +, -
int result = 10 + 2 * 5; // 곱셈 먼저: 10 + 10 = 20
  • 비교 연산자
    • 대소 비교: <, >, <=, >=
    • 동등 비교: ==, !=
boolean isGreater = 5 > 3;  // true
boolean isEqual = 5 == 3;  // false

 

  • 논리 연산자
    • 논리 AND: && (둘 다 참일 때만 참)
    • 논리 OR: || (둘 중 하나만 참이면 참)
boolean result = (5 > 3) && (2 < 4); // true && true = true
  • 조건 연산자 (삼항 연산자)
    • 조건 ? 값1 : 값2 (조건이 참이면 값1, 거짓이면 값2)
int result = (5 > 3) ? 10 : 20; // 5 > 3이 참이므로 result = 10
  • 대입 연산자
    • 값을 변수에 할당하는 연산: =, +=, -=, *=, /=
int a = 5;  // a에 5를 대입
a += 3;     // a = a + 3 (a는 8)

예시

int a = 10, b = 5, c = 2;
int result = a - b + c * 3 > 10 ? a : b;

계산 순서:

 

  1. c * 3 → 2 * 3 = 6
  2. b + 6 → 5 + 6 = 11
  3. a - 11 → 10 - 11 = -1
  4. -1 > 10 → false
  5. 삼항 연산 → 결과는 b

결과: result = 5


[스터디 후 느낀점 및 반성]

흠... 정말 부끄럽게도 공부 + 블로그에 정리해놓았던 개념들도 어버버 하면서 "모르겠습니다!"의 반복이였다.....

멘붕이였다... 그래서 나도 모르게 "죄송합니다!" 라고 해버렸따.... 내스스로한테 죄송해야하는데 퓨ㅠㅠ 황금같은 시간을 좀더 활용하기 위해서 좀 제대로 공부를 해서 가야겠다는 생각을 했다...

근데 공부를 해도 까먹는것은 어떻게 해야하는 걸까? 너무 슬프다

 

반응형

'Study > Study Alone' 카테고리의 다른 글

[나혼자공부] 4주차 복습-2  (0) 2024.12.26
[나혼자공부] 4주차 복습-1  (2) 2024.12.25
[나혼자공부] 3주차 복습-2  (1) 2024.12.20
[나혼자공부] 3주차 복습-1  (0) 2024.12.19
[STUDY] 2주차  (0) 2024.12.16

댓글