2025-11-15
오늘 배운 것
- JVM(Java Virtual Machine)
- 개념
- 자바 바이트코드를 OS/하드웨어 상관없이 실행시키는 가상 머신
- 자바 프로그램은 다른 언어와 다르게 코드 → 바이트 코드 → JVM → 기계어 이 과정을 거치기 때문에 OS 독립성이 생긴다.
- 따라서 자바 파일은 운영체제가 아닌 JVM 위에서 실행되는 프로그램이다
- 전체 구조
- Class Loader
- Runtime Data Area
- Excution Engine
- JVM 메모리 구조
- 메서드 영역
- 클래스 정보(필드, 메서드, static), 상수 풀(Constant Pool)
- JVM 시작 시 생성, JVM 전체가 공유
- 힙
- 객체를 저장하는 영역
- new 로 만든 객체가 여기에 올라간다
- GC가 동작하는 영역
- Young/Old generation으로 나뉜다
- 스택
- 스레드마다 각각 존재
- 메서드 호출 시 생성되는 스택 프레임 저장
- 지역 변수, 매개변수 저장
- 메서드 종료 시 해당 프레임 삭제
- PC 레지스터
- 현재 실행 중인 JVM 명령어의 주소 저장 (스레드마다 1개)
- 네이티브 메서드 스택
- C/C++로 작성된 네이티브 코드 실행을 위한 공간
- 메서드 영역
- 클래스 로더
- JVM이 .class 바이트 코드를 읽어들이는 역할
- 작동 순서
- Loading
- .class 파일을 읽어 메서드 영역에 저장
- Linking
- Verify : 바이트 코드 검증
- Prepare: static 변수 기본값 준비
- Resolve : 참조를 실제 주소로 연결
- Initialization
- static 초기화 코드 실행
- static 블록 실행
- Loading
- 클래스 로더 3단계
- Bootstrap ClassLoader
- Extension ClassLoader
- Application ClassLoader
- 실행 엔진
- 바이트 코드를 실제 기계어 수준에서 실행하는 장치
- 구성
- 인터프리터
- 바이트코드를 한 줄씩 해석 / 실행
- 시작은 빠르지만 반복 실행 시 느림
- JIT(Just-In-Time) 컴파일러
- 자주 실행되는 코드를 기계어로 변환해 캐싱
- 한 번 기계어로 바뀌면 매우 빠르게 실행됨
- 인터프리터 + JIT 하이브리드 방식
- Garbage Collector(GC)
- 힙 영역의 불필요한 객체를 자동으로 정리
- mark → sweep → compact 과정으로 이루어짐
- 인터프리터
- JVM의 힙 구조(Young/Old)
-
힙은 GC 효율을 높이기 위해 세 부분으로 나뉜다.
Young — Eden + Survivor(S1,S2) Old — 오래 살아남은 객체 - Young Generation
- 대부분의 객체는 금방 생성 후 사라짐 → Minor GC에서 빠르게 정리됨
- Old Generation
- survivor 영역에서 여러 번 살아남은 객체 → Major GC / Full GC 대상
-
- Stop-The-World(STW)
- GC가 실행될 때 모든 스레드가 멈추는 시간을 의미
- GC는 불가피하게 STW를 발생시키기 때문에 GC튜닝을 통해 STW 최소화가 핵심 목표
- 개념
- Java 실행과정(JDK/JRE/JVM)
- JDK /JRE / JVM 관계
-
자바 개발 환경은 계층 구조
[JDK] └─ [JRE] └─ [JVM] - JDK(Java Development Kit)
- 개발용 패키지
- 컴파일러(javac), 디버거, 빌드 도구 등 포함
- 개발 시 반드시 필요
- JRE (Java Runtime Environment)
- 실행 환경 패키지
- JVM + 표준 라이브러리(rt.jar 등)
- 실행만 필요할 때 사용(개발엔 부족)
- JVM (Java Virtual Machine)
- 바이트코드를 실제로 실행하는 엔진
- OS/CPU에 맞춰 구현되며 플랫폼 독립성 제공
-
-
자바 실행 과정
소스(.java) ↓ (javac 컴파일) 바이트코드(.class) ↓ (Class Loader 로딩) JVM 내부 메모리(Method Area) ↓ (Interpreter + JIT) 실행.java파일 작성- 개발자가 자바 소스 코드를 작성
- 컴파일 : javac →
.class생성- 명령 :
javac Main.java - 결과
- 바이트코드(.class) 파일 생성
- JVM이 이해할 수 있는 중간 언어
- 어떤 OS에서도 동일
- 이 과정은 JDK에 포함된 컴파일러가 수행
- 명령 :
- 클래스 로딩
- 클래스 로더가 .class 파일을 읽어 JVM 메모리(메서드 영역)에 저장
- 과정
- Loading : .class 읽기
- Linking
- Verify(검증)
- Prepare(static 기본값 확보)
- Resolve(참조 주소 연결)
- Initialization
- static 변수 초기화
- static 블록 실행
- JRE 내부의 JVM이 담당
- JVM 메모리 적재
- 클래스 로더로 읽은 데이터들은 JVM 내부의 메모리 영역에 배치
- 메서드 영역 : 클래스/메서드 정보, static 저장
- 힙 : new 객체 저장
- 스택 : 메서드 호출 시 프레임 저장
- 클래스 로더로 읽은 데이터들은 JVM 내부의 메모리 영역에 배치
- 실행 엔진에 의해 실제 실행
- 실행 엔진이 바이트 코드를 실제 기계어로 전환하며 실행
- 구성요소
- 인터프리터 : 한줄씩 해석 (빠른 시작, 반복 시 느림)
- JIT 컴파일러 : 자주 실행되는 코드 전체를 머신 코드로 변환
-
실행 흐름
바이트코드 → (인터프리터) 한 줄씩 실행 → 반복 많으면 → (JIT 컴파일러) 전체 메서드를 기계어로 변환 → 매우 빠른 실행
- Garbage Collector 동작
- 힙에 더 이상 참조되지 않는 객체를 수집해 정리
- Young → Minor GC
- Old → Major GC / Full GC
- 이때 STW 발생 가능
- 힙에 더 이상 참조되지 않는 객체를 수집해 정리
- JDK /JRE / JVM 관계
- OOP 핵심 개념
- 객체 지향이란?
- 프로그램을 객체 단위로 나누어 각 객체가 데이터(속성) + 기능(메서드)를 갖도록 구성하는 프로그래밍 패러다임
- 객체와 클래스
- 클래스(Class)
- 객체를 만들기 위한 설계도
- 필드(속성), 메서드(행동)를 정의
- 객체(Object)
- 클래스의 실제 인스턴스
- 메모리에 로딩된 실체
- 클래스(Class)
- 객체지향의 4대 특징
- 캡슐화(Encapsulation)
- 데이터(필드)와 기능(메서드)을 하나로 묶는 것
- 외부에서 직접 필드에 접근하지 못하도록 보호(private)
- 필요한 API만 공개(getter/setter)
- 효과
- 보안성 향상
- 코드 응집도 증가
- 데이터 무결성 유지
- 상속(Inheritance)
- 부모 클래스의 속성과 메서드를 자식 클래스가 물려받음
- 코드 재사용성과 확장성 증가
- 주의점
- 과한 상속으로 결합도 증가로 유지보수 어려워짐
- 다형성(Polymorphism)
- 같은 메서드 호출이 객체 타입에 따라 다르게 동작하는 성질
- 정적 다형성 (컴파일 시) → 오버로딩
- 같은 이름의 메서드를 매개변수 형태로 구분(개수, 타입, 순서)
- 동적 다형성 (런타임) → 오버라이딩
- 부모 메서드를 자식이 재정의
- 접근제어자 완화는 가능(부모보다 더 큰 범위로)
- public > protected > default > private
- 애초에 private는 상속이 안됌
- 동적 바인딩(dynammic dispatch) 기반으로 동작
- 실행 중에 어떤 객체인지에 따라 메서드 결정
- 추상화(Abstraction)
- 불필요한 내부 구현을 감추고 객체가 가진 핵심적인 기능만 노출
- 방법
- 인터페이스
- 추상클래스
- 효과
- 복잡도 감소
- 실제 동작이 아닌 무엇을 할 수 있는지에 집중
- 캡슐화(Encapsulation)
- 인터페이스 vs 추상 클래스
- 인터페이스
- 행동의 규칙 정의
- 다중 구현 가능
- default, static 메서드 제공 가능
- 추상 클래스
- 공통 속성 + 공통 메서드 + 추상 메서드 혼합 가능
- 단일 상속만 가능
- 인터페이스
- 객체지향의 중요성(장점)
- 코드 재사용성 : 공통 구조 재활용
- 유지보수성 : 변경에 강한 구조
- 확장성 : 기능 추가 쉬움
- 모듈화 : 관리 책임 분리 가능
- 객체지향 관련 개념
- 메시지 : 객체간의 메서드 호출
- 책임 : 객체가 맡아야 할 역할
- 의존성 : 객체 간 관계
- 응집도 : 객체가 한 책임에 집중하는 정도
- 결합도 : 객체 간 의존성 정도
- 객체 지향이란?
- SOLID
- S - 단일 책임 원칙 (Single Responsibility Principle)
- 정의
- 클래스는 오직 하나의 책임만 가져야 한다.
- 이유
- 여러 책임을 함께 가지면 변경 시 연쇄적으로 영향을 받음
- 체크 포인트
- 클래스가 바뀌는 이유가 2개 이상인가(예: 클래스 이름이 2단어 이상?) → SRP 위반
- 정의
- O - 개방-폐쇄 원칙 (Open-Closed Principle)
- 정의
- 확장에는 열려있고 변경에는 닫혀있어야한다
- 기존 코드를 수정하지 않고 새로운 기능 추가 가능 해야 한다
- 방법
- 인터페이스를 통해 다형성 사용
- 전략 패턴, 템플릿 메서드 패턴 등으로 확장 포인트 분리
- 예
- 조건문으로 계속 이러면 이거 저러면 저거 하면 OCP 위반
- 정의
- L - 리스코프 치환 원칙 (Liskov Substitution Principle)
- 정의
- 부모 타입 객체를 사용하는 곳에 자식 타입 객체를 넣어도 정상적으로 동작해야 한다
- 대체 가능성
- 의미
- 메서드 오버라이딩에서 부모가 보장한 행동을 무너뜨리면 LSP 위반
- LSP 위반 예
- 부모 양수 반환, 자식 음수 반환
- 부모는 예외 안던짐, 자식은 예외 새로 던짐
- 부모는 null 허용, 자식은 null 안 허용
- 정의
- I - 인터페이스 분리 원칙 (Inteface Segregation Principle)
- 정의
- 클라이언트는 사용하지 않는 메서드가 있는 인터페이스에 의존하면 안된다.
- 의미
- 큰 인터페이스를 여러 개로 분리해야 한다
- 불필요한 기능이 섞여 있는 큰 인터페이스는 ISP 위반
- 예
- Animal 인터페이스에 walk, fly, swim을 모두 넣는 것은 문제
- 새는 날 수 있지만, 고래는 못남 → 불필요한 의존 발생
- 정의
- D - 의존 역전 원칙 (Dependency Inversion Principle)
- 정의
- 상위 모듈은 하위 모듈에 의존하면 안된다
- 둘 다 추상화(인터페이스)에 의존해야 한다
- 추상화는 구현체에 의존하면 안된다.
- 의미
- 구현체가 바뀌어도 상위 레이어는 영향을 받아선 안됨
- 스프링 DI/IoC 설계의 핵심 원리
- 정의
- S - 단일 책임 원칙 (Single Responsibility Principle)
- Thread-safe
- 정의
- 여러 스레드가 동시에 접근해도 결과가 항상 정상이고 데이터 무결성이 보장되는 상태
- 구현방법
- Synchronized
- 자바의 기본 동기화 키워드
synchronized void add() {}- 한 스레드만 접근 가능(뮤텍스)
- 객체/메서드/블록 단위 락을 획득
- 단점 : 성능 저하 (락 경쟁, 블로킹)
- volatile
- 공유 변수의 가시성 보장
- CPU 캐시가 아닌 메인 메모리에서 직접 읽고 씀
- 재배치 방지
- 하지만 원자성 보장 x
- count++ 같은 연산은 volatile로는 안전해지지 않음
- Atomic Classes
java.util.concurrent.atomic.*- CAS(Compare-And-Swap) 기반
- Lock-free 방식으로 원자적 연산 제공
- 예
- AtomicInteger
- AtomicLong
- AtomicReference
- Concurrent Collections
- 멀티스레드 안전한 자료구조
- ConcurrentHashMap
- CopyOnWriteArrayList
- ConcurrentLinkedQueue
- 특징
- 부분 락 혹은 Lock-free 구조
- Hashtable 같은 “전체 락” 보다 훨씬 효율적
- Locks
- ReentrantLock, ReadWriteLock, StampedLock
- 장점
- synchronized 보다 세밀한 제어
- 타임아웃, 조건 변수(Condition) 사용 가능
- 공정(fair) 모드 지원
- 단점
- finally에서 unlock 꼭 필요 (실수할 수 있음)
- Immutable(불변 객체) 구조 활용
- 공유 자원을 애초에 변경 불가능하게 만들어 경쟁 자체를 제거
- Thread-safe 구현 가장 쉬운 방법
- 예 : String 불변
- Synchronized
- 자바의 Thread 구현방법
-
Thread 클래스 상속
class MyThread extends Thread { public void run() {} } -
Runnable 구현
class MyTask implements Runnable { public void run() {} } new Thread(new MyTask()).start(); -
ExecutorService(실무 표준)
ExecutorService exec = Executors.newFixedThreadPool(10); exec.submit(() -> doWork());
-
- 동시성 vs 병렬성
- 동시성 : 여러 작업이 겉보기에 동시에 진행됨 (실제로는 빠르게 전환)
- 병렬성 : 여러 작업이 실제로 동시에 실행됨 (멀티코어에서만 가능)
- 실무 스레딩에서 중요한점
- 가능하면 공유 상태 최소화
- synchronized 최소 사용
- 상태 변화가 필요하면 Atomic 또는 Concurrent 구조 사용
- ExecutorService 사용이 표준
- 락을 사용할 때는 반드시 unlock 보장 필요
- 정의
- Java Memory Model(JMM)
- 개념
- 멀티스레드 환경에서 메모리 읽기/쓰기를 어떻게 보장할지 정한 규칙
- 정의
- 어떤 값이 언제 다른 스레드에게 보이는지 (가시성)
- 명령이 어떻게 재배치 되는지
- 어떤 연산이 원자성이 있는지
- 락/volatile 등이 어떤 방식으로 동작하는지
- 멀티스레드에서 발생하는 이상 동작의 대부분은 JMM 때문
- 필요한 이유
- 자바는 JVM 위에서 실행되고, JVM은 CPU의 캐시/파이프라인 최적화 때문에 여러 문제가 발생할 수 있다
- 문제
- CPU 캐시로 인해 최신 값이 보이지 않음 (가시성)
- 스레드 A는 x = 10을 캐시에 저장
- 스레드 B는 같은 데이터의 옛값 x = 0을 보고 있을 수 있으므
- 명령어 재배치
-
컴파일러와 CPU는 성능 때문에 코드 순서를 최적화함
initialized = true; data = 100; // 아래로 재배치 될 수 있는데 이것이 멀티 스레드 환경에서 큰 문제가 될 수 있음 data = 100; initialized = true;
-
- CPU 캐시로 인해 최신 값이 보이지 않음 (가시성)
- JMM이 해결하려는 3대 문제
- 원자성(Atomicity)
- 한 번에 일어나는 연산을 보장 → 중간 상태를 못보게 함
- 원자성 보장되는 기본 연산
- 읽기,쓰기,참조 변경
- 32bit 정수 연산
- 원자성 보장 x
- count++
- a = a+1
- long, double(64bit) 일 JVM
- 가시성
- 한 스레드의 쓰기가 다른 스레드에게 언제 보이는지 정의
-
예
boolean flag = false; Thread A: flag = true; Thread B: while (!flag) { } // 무한 루프 돌 수도 있음 (캐시 때문)
- 순서성
- 코드 순서가 실제 실행 순서를 보장하는가?
- JMM이 없으면 코드순서 != 실행순서
- 원자성(Atomicity)
- 제공하는 키워드/메커니즘
- volatile
- 가시성,순서성 보장
- 원자성 x
- synchronized
- 락 기반
- 원자성, 가시성, 순서성 보장
- 완전한 Thread-safe 보장 키워드 → 단 성능 비용 있음
- final
- final 필드는 생성자 완료 후 값이 다른 스레드에 항상 보인다
- Happens-before 규칙
- X, Y사이에 happens-before 관계가 있으면 X의 연산 결과는 반드시 Y에서 관측 됨
- 예
- 동일 스레드 내 코드 순서 → 보장됨
- 락 해제 → 락 획득
- volatile write → volatile read
- 스레드 start() 이전 → start된 스레드의 실행
- 스레드 종료 → join() 이후
- volatile
- JMM에서 발생하는 오류
- double-checked locking (JDK 1.5 이전)
- 재배치 때문에 싱글통이 깨질 수 있음 → volatile로 해결
- visibility 문제
- 다른 스레드가 값을 안읽어감
- atomicity 문제
- count++ 경쟁 상태
- double-checked locking (JDK 1.5 이전)
- 개념
- 비동기(Async) / 병렬 프로그래밍
- 동기/비동기/블로킹/논블로킹
- 동기
- 작업이 순서대로 실행
- 하나가 끝나야 다음 작업 시작
- 비동기
- 요청 후 즉시 다음 작업 진행
- 결과는 나중에 콜백/이벤트로 받음
- 블로킹
- 작업이 완료될 때까지 스레드 멈춤
- 논블로킹
- 작업 시작후 스레드가 멈추지 않고 계속 진행
- 조합
- 동기 + 블로킹 : 전통방식, 순서대로 다기다림
- 동기 + 논블로킹 : 요청은 즉시 끝나지만 결과는 기다려야 함
- 비동기 + 블로킹 : 거의 없음
- 비동기 + 논블로킹 : 가장 현대적인 방식
- 동기
- 자바에서 비동기 구현
- Thread/Runnable
- 직접 스레드를 만들어 비동기 실행
- 단순하지만 실무에서 잘 사용 안함
- ExecutorService(스레드 풀 기반)
- 장점
- 스레드 재사용
- 관리 쉬움
- 대규모 서버 구현 가능
- 장점
- Future
- 비동기 결과를 객체로 관리
- 단점
- 완료되지 않으면 get() 시 블로킹
- 콜백 없음
- 함수형 프로그래밍 불가능
- CompletableFuture (Java 8+ 핵심)
- 비동기 + 논블로킹을 가장 깔끔하게 구현하는 도구
- 특징
- 콜백 기반 비동기
- 체이닝 가능
- 내부적으로 ForkJoinPoll 사용(기본)
- Thread/Runnable
- 비동기 vs 병렬
- 비동기
- 스레드 안멈춤
- 결과를 나중에 받음
- 콜백 / 이벤트 기반
- 병렬
- 여러 스레드가 실제로 동시에 실행
- 멀티코어에서만 가능
- 비동기
- Java 비동기 개념
-
콜백 : 결과를 나중에 전달 받는 함수
future.thenAccept(result -> print(result)); - Promise(Future/CompletableFuture) : 비동기 결과 역할을 하는 객체
- Event Loop : 단일 스레드로 이벤트 처리하는 구조
- 자바에서 java.nio , Netty, Reactor에서 사용
- Non-bloking I/O(NIO)
- 자바 NIO는 논블로킹 소켓을 지원
- 특징
- 스레드 하나가 여러 소켓을 감시
- 대규모 연결 처리 가능 → 대규모 채팅 서버에서 사용되는 방식
- Selector 기반
-
- 스프링에서 비동기 (Spring @Async)
- 원리
- 내부적으로 TaskExecutor(스레드 풀) 사용
- 반환 타입을 CompletableFuture로 둘 수 있음
- 주의
- 같은 클래스 내부 메서드 호출은 프록시 적용 안됨
- 원리
- 비동기/병렬이 필요한 이유
- I/O 작업이 많은 서버에서 높은 처리량 확보
- 스레드를 블로킹 시키지 않음
- CPU-bound/IO-bound 작업 분리
- 대규모 트래픽 처리 가능
- 동기/비동기/블로킹/논블로킹
-
JCF(Java Collection Framework) 핵심 자료구조
Collection ├── List │ ├── ArrayList │ ├── LinkedList │ └── Vector (거의 안 씀) ├── Set │ ├── HashSet │ ├── LinkedHashSet │ └── TreeSet ├── Queue │ ├── PriorityQueue │ └── ArrayDeque └── Deque └── ArrayDeque Map ├── HashMap ├── LinkedHashMap └── TreeMap- List
- ArrayList
- 내부 구조
- 배열기반 동적 배열
- Index 접근 매우 빠름
- 시간 복잡도
- get: O(1)
- add : 평균 O(1), resize O(n)
- remove : O(n) (중간 삭제 시 뒤요소들 당김)
- 특징
- 랜덤 액세스 빠름
- 많은 추가/삭제에는 비효율적
- 스레드 안전 X
- 내부 구조
- LinkedList
- 내부 구조
- 이중 연결 리스트
- 시간 복잡도
- get : O(n)
- add/remove : 위치만 알면 O(1)
- 특징
- 삽입/삭제 많을 때 유리
- 메모리 사용량 많음
- 스레드 안전 X
- 내부 구조
- ArrayList
- Set
- HashSet
- 내부구조
- HashMap 기반, key만 사용
- 특징
- 중복 허용 X
- 순서 보장 X
- contains 매우 빠름 (O(1))
- 내부구조
- LinkedHashSet
- HashSet + 삽입 순서 유지
- 내부적으로 순서 유지용 이중 연결 리스트 사용
- TreeSet
- Red-Black Tree기반
- 자동 정렬
- 범위 탐색 가능
- 시간 복잡도
- add/remove/contains : O(log n)
- HashSet
- Map
- HashMap
- 내부 구조
- 배열 + 연결 리스트 + 트리(레드-블랙 트리) 혼합
- 자바 8부터 버킷 충돌이 많아지면 체인이 트리로 변환
- 시간 복잡도
- 평균 : O(1)
- 최악 : O(log n) → 트리 변환 시
- 특징
- 순서 보장 X
- null key 1개 가능, null value 여러개 가능
- 내부 구조
- LinkedHashMap
- HashMap + 삽입 순서 유지
- LRU 캐시 구현 시 많이 사용
- TreeMap
- Red-Black Tree 기반 Map
- 자동 정렬됨
- 시간복잡도 : 모든 연산 O(log n)
- 특징
- key 범위 탐색 용이
- 정렬 기반 맵이 필요할 때 사용
- HashMap
- Queue/Deque
- PriorityQueue
- 내부 구조
- 이진 힙 기반
- 기본은 min-heap
- 시간 복잡도
- 삽입/삭제 : O(log n)
- 내부 구조
- ArrayDeque
- 내부 구조
- 배열 기반 원형 큐
- 특징
- LinkedList보다 훨씩 빠른 큐/스택
- 스택으로 쓰면 Stack 보단 안전하고 빠름
- null 저장 불가(API 안정성)
- 내부 구조
- PriorityQueue
- Thread-safe 컬렉션
- Vector(List)
- 전체 메서드 synchronized
- 성능 매우 낮아 거의 사용 x
- Hashtable (Map)
- 전체 락 → 성능 낮음
- HashMap + 동기화 필요 시 사용 x (비추천)
- Concurrent Collections
- ConcurrentHashMap
- 부분 락(시그먼트 락) → 고성능
- 실무에서 thread-safe Map의 표준
- CopyOnWriteArrayList
- 쓰기 시 마다 전체 복사 → 일기 많은 경우 최적
- ConcurrentLinkedQueue
- Lock-free 큐
- ConcurrentHashMap
- Vector(List)
- List