JVM은 자바 프로그램을 실행하는 가상 머신입니다.
핵심 특징은 다음과 같습니다.
- 운영체제에 독립적이어서 같은 바이트코드로 여러 OS에서 실행할 수 있습니다.
- 가비지 컬렉터가 메모리 회수를 자동으로 처리해 개발자가 직접 메모리를 관리할 필요가 없습니다.
Class Loader- 컴파일된
.class바이트코드를 읽어 메모리에 적재합니다.
- 컴파일된
Runtime Data Area- JVM이 사용하는 메모리 영역입니다.
- 대표적으로 Heap, Stack, Method Area, PC Register, Native Method Stack이 있습니다.
Execution Engine- 적재된 바이트코드를 해석하거나 기계어로 변환해 실행합니다.
- Interpreter, JIT Compiler, GC가 여기에 포함됩니다.
- 개발자가 작성한
.java파일을javac가.class바이트코드로 컴파일합니다. - Class Loader가 바이트코드를 JVM 메모리 영역에 로드합니다.
- Execution Engine이 바이트코드를 해석하거나 JIT 컴파일하여 실행합니다.
| 구분 | Compiler | Interpreter |
|---|---|---|
| 동작 방식 | 소스 코드 전체를 한 번에 번역 | 실행 시 한 줄씩 읽으며 번역 |
| 실행 파일 | 생성됨 | 보통 별도 실행 파일 없음 |
| 실행 속도 | 빠름 | 상대적으로 느림 |
| 오류 발견 시점 | 실행 전 문법 오류 확인 가능 | 실행 중 해당 지점에서 오류 확인 |
| 예시 | C, C++ | Python, JavaScript |
자바는 하이브리드 방식입니다.
- 컴파일 단계에서
javac가 소스 코드를 바이트코드로 변환합니다. - 실행 단계에서 JVM이 바이트코드를 해석하며 실행합니다.
- 자주 실행되는 코드는 JIT 컴파일러가 기계어로 최적화하여 성능을 높입니다.
GC는 힙 영역에 생성된 객체 중 더 이상 사용되지 않는 객체를 자동으로 찾아 메모리를 회수하는 기능입니다.
- GC Root에서 출발해 참조가 이어지는 객체를
Reachable로 판단합니다. - 참조가 닿지 않는 객체를
Unreachable로 보고 수거 대상으로 판단합니다.
Mark and Sweep- 사용 중인 객체를 표시하고, 표시되지 않은 객체를 제거합니다.
Generational GC- 대부분의 객체는 금방 사라진다는 가정으로 Young/Old 영역을 나눠 관리합니다.
- Young 영역에서 주로 Minor GC가 발생합니다.
- 오래 살아남은 객체는 Old 영역으로 이동합니다.
- Old 영역 정리 시 Full GC 또는 Major GC가 발생하며 Stop-the-world 시간이 길어질 수 있습니다.
객체지향의 목적은 유지보수성, 확장성, 재사용성을 높이는 데 있습니다.
- 복잡한 시스템에서 핵심 개념만 뽑아 모델링하는 것입니다.
- 예: 여러 자동차의 공통 속성을 추출해
Car로 표현
- 데이터와 해당 데이터를 다루는 메서드를 하나로 묶는 것입니다.
- 외부에서 내부 상태를 함부로 건드리지 못하게 보호합니다.
- 부모 클래스의 속성과 기능을 자식 클래스가 물려받아 재사용하거나 확장하는 것입니다.
- 코드 중복을 줄이고 재사용성을 높입니다.
- 같은 인터페이스로 서로 다른 구현을 다룰 수 있는 성질입니다.
- 오버라이딩은 런타임 다형성, 오버로딩은 컴파일 타임 다형성으로 설명할 수 있습니다.
| 구분 | 캡슐화 | 은닉화 |
|---|---|---|
| 핵심 | 데이터와 기능을 묶음 | 외부 접근을 제한함 |
| 초점 | 구조화 | 보호 |
| 구현 | 클래스 단위로 묶음 | 접근 제어자 사용 |
| 관계 | 은닉화를 가능하게 하는 방법 | 캡슐화로 얻는 효과 |
제네릭은 클래스나 메서드에서 사용할 타입을 컴파일 시점에 지정하는 기능입니다.
- 타입 안정성을 높일 수 있습니다.
- 불필요한 캐스팅을 줄일 수 있습니다.
오버라이딩- 상위 클래스의 메서드를 하위 클래스에서 재정의하는 것입니다.
- 메서드 이름, 파라미터, 반환 타입 규칙을 따라야 합니다.
오버로딩- 같은 이름의 메서드를 파라미터 타입이나 개수를 다르게 하여 여러 개 정의하는 것입니다.
추상 클래스extends로 상속받습니다.- 공통 상태와 공통 기능을 일부 구현해 하위 클래스에 확장 포인트를 제공합니다.
- 필드와 생성자를 가질 수 있습니다.
인터페이스implements로 구현합니다.- 클래스 간 공통 규약을 정의하는 데 적합합니다.
- 다중 구현이 가능합니다.
클래스- 객체를 만들기 위한 설계도입니다.
객체- 클래스를 기반으로 실제 메모리에 생성된 인스턴스입니다.
private- 같은 클래스 내부에서만 접근 가능합니다.
default- 같은 패키지 내부에서만 접근 가능합니다.
protected- 같은 패키지 또는 다른 패키지의 자식 클래스에서 접근 가능합니다.
public- 어디서나 접근 가능합니다.
객체지향 설계에서 유지보수성과 확장성을 높이기 위한 5가지 원칙입니다.
SRP- 클래스는 하나의 책임만 가져야 합니다.
OCP- 확장에는 열려 있고 변경에는 닫혀 있어야 합니다.
LSP- 하위 타입은 상위 타입을 대체할 수 있어야 합니다.
ISP- 범용 인터페이스 하나보다 역할에 맞는 작은 인터페이스 여러 개가 낫습니다.
DIP- 구체 구현보다 추상화에 의존해야 합니다.
- 값을 복사해서 전달합니다.
- 메서드 안에서 값을 바꿔도 원본 변수에는 영향이 없습니다.
- 참조 자체를 전달해 같은 대상을 함께 바라봅니다.
- 메서드 안에서 변경한 내용이 원본에도 영향을 줍니다.
자바는 Call by Value입니다.
- 기본형은 값 자체를 복사합니다.
- 객체는 참조값을 복사합니다.
- 그래서 메서드 안에서 객체 내부 상태를 바꾸면 원본이 바뀐 것처럼 보이지만, 다른 객체를 재할당해도 호출자 쪽 참조 자체는 바뀌지 않습니다.
직렬화- 객체를 바이트 스트림으로 변환하는 과정입니다.
- 파일 저장, 네트워크 전송 등에 사용됩니다.
역직렬화- 바이트 스트림을 다시 객체로 복원하는 과정입니다.
자바에서는 보통 Serializable 인터페이스로 직렬화 가능 여부를 표시합니다.
==- 두 참조가 같은 객체를 가리키는지 비교합니다.
equals()- 두 객체가 논리적으로 같은 값인지 비교합니다.
핵심 규약은 equals()가 true라면 hashCode()도 반드시 같아야 한다는 점입니다.
이유는 HashMap, HashSet 같은 해시 기반 컬렉션이 먼저 hashCode()로 버킷을 찾고, 그 다음 equals()로 실제 동일한지 확인하기 때문입니다.
주의할 점은 반대로 hashCode()가 같다고 해서 equals()도 반드시 true인 것은 아니라는 점입니다. 해시 충돌은 가능하기 때문입니다.
Checked Exception- 컴파일 시점에 확인됩니다.
- 예외 처리를 강제합니다.
- 예:
IOException,SQLException
Unchecked Exception- 런타임 시점에 확인됩니다.
RuntimeException계열이며 처리 강제가 없습니다.- 예:
NullPointerException,IndexOutOfBoundsException
static은 인스턴스가 아니라 클래스에 속한다는 의미입니다.
- 객체를 생성하지 않아도 접근할 수 있습니다.
- 모든 인스턴스가 같은 값을 공유합니다.
- 면접에서는 보통 "클래스 로딩 시점에 올라가며 클래스 단위로 1개만 존재한다"고 설명하면 충분합니다.
참고로 메모리 배치는 JVM 구현에 따라 세부사항이 다를 수 있어, 단순히 "공유되는 클래스 레벨 자원"이라고 설명하는 편이 안전합니다.
POJO는 특정 프레임워크에 강하게 종속되지 않은 평범한 자바 객체를 뜻합니다.
- 특정 기술 규약을 강제하는 상속에 묶이지 않습니다.
- 특정 인터페이스 구현이 필수는 아닙니다.
- 핵심 비즈니스 로직이 기술 구현보다 객체 자체에 집중됩니다.
스프링은 POJO 중심으로 애플리케이션을 만들 수 있게 도와주는 프레임워크입니다. 개발자는 비즈니스 로직에 집중하고, DI나 AOP 같은 기술적 관심사는 스프링이 지원합니다.
리플렉션은 런타임에 클래스의 메타데이터를 조회하고 조작할 수 있게 해주는 기능입니다.
- 클래스 정보 조회
- 생성자를 통한 동적 객체 생성
- 메서드 호출
- 필드 값 조회 및 변경
- Spring의 DI와 컴포넌트 스캔
- JPA의 엔티티 생성 및 필드 매핑
- Jackson/Gson의 직렬화, 역직렬화
- JUnit의 테스트 메서드 탐색
Stream API는 컬렉션이나 배열 데이터를 선언형으로 처리할 수 있게 해주는 기능입니다.
- 원본 데이터를 직접 수정하지 않습니다.
- 한 번 최종 연산이 수행된 스트림은 재사용할 수 없습니다.
- 내부 반복 방식이라 코드가 간결합니다.
- 중간 연산은 지연 평가되며, 최종 연산 시점에 실제 수행됩니다.
- 생성:
stream(),Arrays.stream() - 중간 연산:
filter,map,sorted,distinct - 최종 연산:
collect,forEach,count,reduce
- 장점
- 가독성이 좋아지고 병렬 처리 확장이 쉽습니다.
- 단점
- 단순 반복보다 오버헤드가 있을 수 있고 디버깅이 불편할 수 있습니다.