Java는 모든 값을 call by value 방식으로 전달합니다.
primitive 타입은 실제 값을 복사해서 전달하므로 메서드 내에서 값을 변경해도 원래 변수에는 영향이 없습니다.
reference 타입은 객체의 주소값을 복사해서 전달하므로 메서드 내에서 객체의 필드를 변경하면 원래 객체도 바뀝니다. 하지만 메서드 안에서 새로운 객체를 할당해도 reference 변수는 여전히 기존 객체를 가리킵니다.
Java의 가비지 컬렉션은 더 이상 사용되지 않는 객체를 자동으로 제거해 메모리를 관리하는 기능입니다. JVM이 힙 메모리를 주기적으로 검사하여 참조가 끊긴 객체를 수거하며, 대표적인 방식으로는 Mark and Sweep, Generational GC, G1 GC 등이 있습니다. 자동 메모리 관리 덕분에 메모리 누수를 줄일 수 있지만, 성능 이슈가 있는 경우 GC 튜닝이 필요할 수 있습니다.
Mark and Sweep: 가장 기본적인 방식으로 먼저 객체 그래프를 따라가며 살아있는 객체에 표시(mark)를 하고, 표시되지 않은 객체를 힙에서 제거(sweep)합니다. 구조는 단순하지만 전체 힙을 스캔해야 하기 때문에 성능 저하가 발생할 수 있습니다.
Generational GC: 객체의 생존 기간을 기준으로 Young Generation과 Old Generation으로 나누어 관리합니다. 대부분의 객체는 생성된 직후 사라지기 때문에 Young 영역에서는 빠르고 자주 수거하며(Minor GC), 오래 살아남은 객체는 Old 영역으로 이동하여 상대적으로 느리고 비용이 큰 Major GC로 수거됩니다. 이 방식은 단명 객체가 많다는 가정을 활용해 성능을 높이는 데 효과적입니다.
G1 GC: Java 9 이후 기본으로 채택된 방식으로 힙을 고정된 크기의 여러 Region으로 나누어 관리하며, 가비지가 많은 영역을 우선적으로 수거하는 전략을 사용합니다. G1 GC는 동시에 여러 스레드가 GC를 수행하고 Stop-the-World 시간을 줄여주기 때문에 지연 시간에 민감한 애플리케이션에 적합합니다.
캡슐화는 변수와 메서드를 객체 내부에 감춰 직접 접근을 막는 것입니다. 데이터를 보호하고, 잘못된 접근을 막는 것이 목적입니다. Java에서는 private 접근 제어자와 getter/setter를 통해 캡슐화를 구현합니다.
상속은 기존 클래스를 물려받아 속성과 기능을 재사용하는 것입니다. 코드의 효율적인 재사용이 목적입니다. Java에서는 extends 키워드로 상속을 구현하며, 공통 기능을 상위 클래스에 모아 관리할 수 있습니다.
다형성은 같은 이름의 메서드가 객체에 따라 다르게 동작하는 것입니다. 하나의 코드로 다양한 동작을 처리하는 것이 목적입니다. Java에서는 오버라이딩과 업캐스팅을 통해 다형성을 실현할 수 있습니다.
추상화는 구현을 숨기고 필요한 기능만 외부에 보여주는 것입니다. 복잡한 코드를 감추고 사용을 단순하게 하는 것이 목적입니다. Java에서는 abstract 클래스나 interface를 통해 핵심 동작만 정의하고, 세부 구현은 따로 분리합니다.
JVM은 Java Virtual Machine의 약자로, 자바 바이트코드를 실행하는 가상 머신입니다. 자바 프로그램은 컴파일 후 바이트코드로 변환되며, 이 바이트코드는 JVM에서 실행됩니다. JVM은 클래스 로딩, 메모리 관리, 실행 엔진, 가비지 컬렉션 등을 담당하며, 운영체제에 독립적으로 동작하기 때문에 자바가 플랫폼에 독립적인 언어가 될 수 있게 합니다.
++ JVM 메모리 구조
JVM 메모리는 런타임 시 프로그램을 실행하기 위해 여러 영역으로 나뉘며, 대표적으로 메서드 영역, 힙, 스택, PC 레지스터, 네이티브 메서드 스택이 있습니다.
메서드 영역은 클래스의 메타정보와 static 변수, 상수 등을 저장하는 영역이며, 힙은 객체와 배열이 저장되는 공간으로 가비지 컬렉션의 대상이 됩니다. 스택은 메서드 호출 시 생성되는 프레임을 저장하며 지역 변수와 호출 정보를 포함합니다. PC 레지스터는 현재 실행 중인 명령어 주소를 저장하고, 네이티브 메서드 스택은 C나 C++과 같은 네이티브 코드 실행을 위한 스택입니다.
JVM은 기본적으로 바이트코드를 실행하지만, 운영체제나 하드웨어 수준의 기능을 호출해야 할 경우 JNI를 통해 C/C++ 등 네이티브 코드를 실행합니다. 이때 호출 정보를 저장하고 실행 흐름을 관리하기 위해 Native Method Stack이 필요합니다.
오버라이딩은 상속 관계에서 부모 클래스의 메서드를 자식 클래스가 동일한 시그니처로 재정의하는 것입니다. 런타임 시점에 실제 객체의 타입에 따라 호출되는 메서드가 결정되므로, Java의 다형성을 구현하는 핵심 요소입니다.
오버로딩은 같은 클래스 내에서 메서드 이름은 같지만 매개변수의 타입이나 개수가 다른 메서드를 정의하는 것입니다. 이는 컴파일 시점에 호출할 메서드가 결정됩니다.
static은 클래스 전체에서 공유하는 변수나 메서드라는 뜻으로, 객체를 만들지 않아도 사용할 수 있습니다.
final은 더 이상 바꿀 수 없다는 의미입니다. final 변수는 값을 바꿀 수 없고, final 메서드는 오버라이딩할 수 없으며, final 클래스는 상속이 안 됩니다.
클래스는 객체를 만들기 위한 설계도이고, 인스턴스는 그 설계도로부터 실제로 만들어진 객체입니다. 클래스는 메서드 영역에 저장되고, 인스턴스는 힙 영역에 할당됩니다. 인스턴스는 클래스 기반으로 생성되어 실제 동작 가능한 상태가 됩니다.
인터페이스는 메서드 시그니처만 선언하고 구현은 하지 않으며, 다중 구현이 가능해 서로 다른 계층의 클래스들 간 공통 기능을 정의하는 데 적합합니다.
추상 클래스는 일반 메서드와 필드도 가질 수 있어 공통 로직이나 상태를 부분적으로 공유해야 하는 경우에 사용됩니다.
클래스는 하나의 추상 클래스만 상속받을 수 있지만 인터페이스는 여러 개를 구현할 수 있기 때문에 유연한 설계가 필요한 경우 인터페이스를 더 많이 활용합니다.
++ 인터페이스를 상속받는 클래스가 두 개 이상일 때 충돌이 생기면 어떻게 하나요?
두 개 이상의 인터페이스에서 동일한 시그니처의 default 메서드를 상속받으면 컴파일 에러가 발생하며, 반드시 해당 메서드를 직접 오버라이딩하여 충돌을 해결해야 합니다.
람다 표현식은 메서드를 간단한 식으로 표현하는 문법으로, 함수형 인터페이스 구현을 간결하게 만들고 Java 코드에 함수형 스타일을 도입하는 데 사용됩니다.
++ 함수형 인터페이스
함수형 인터페이스는 오직 하나의 추상 메서드만 가지는 인터페이스입니다.
String은 불변 객체로 문자열 변경 시마다 새로운 객체가 생성되고, StringBuilder는 가변이지만 쓰레드 안전하지 않으며, StringBuffer는 가변이면서 쓰레드 안전합니다. 일반적으로 단일 쓰레드 환경에선 StringBuilder를, 멀티 쓰레드 환경에선 StringBuffer를 사용합니다.
++
String
불변 객체입니다. 한 번 만든 문자열은 바꿀 수 없습니다.
문자열을 더할 때마다 새로운 객체가 생성됩니다.
StringBuilder
가변 객체입니다. 문자열을 바꿔도 같은 객체 안에서 수정됩니다.
쓰레드 안전하지 않습니다.
StringBuffer
가변 객체이고, 내부 메서드에 동기화 처리가 되어 있습니다.
멀티 쓰레드 환경에서도 안전하게 사용할 수 있습니다.