1. Java 기본
Q1. Java란 무엇인가요?
A1.
Java는 Sun Microsystems에서 개발한 객체지향 프로그래밍 언어이며, 현재는 Oracle에서 관리하고 있습니다. Java는 운영체제에 종속되지 않고 JVM(Java Virtual Machine) 위에서 실행되는 플랫폼 독립적인 언어입니다. 또한 객체지향 프로그래밍을 지원하며, Garbage Collection을 통해 메모리를 자동으로 관리하고 멀티스레드를 기본적으로 지원합니다. 이러한 특징 덕분에 높은 안정성과 유지보수성을 제공하며, 기업용 애플리케이션과 웹 백엔드 개발에서 가장 많이 사용되는 언어 중 하나입니다.
Q2. Java의 특징은 무엇인가요?
A2.
Java의 대표적인 특징은 플랫폼 독립성, 객체지향 프로그래밍 지원, 자동 메모리 관리, 멀티스레드 지원입니다. Java는 소스 코드를 운영체제별 기계어가 아닌 Byte Code로 컴파일하고, Byte Code를 JVM이 각 운영체제에 맞게 실행하기 때문에 하나의 프로그램을 여러 운영체제에서 실행할 수 있습니다. 또한 Garbage Collection을 통해 메모리를 자동으로 관리하여 개발자가 직접 메모리를 해제할 필요가 없으며, 풍부한 표준 라이브러리를 제공하여 개발 생산성이 높습니다. 이러한 특징 덕분에 Java는 안정성과 확장성이 중요한 기업용 백엔드 시스템에서 많이 사용됩니다.
Q3. Java의 장점과 단점은 무엇인가요?
A3.
Java의 장점은 플랫폼 독립성, 객체지향 프로그래밍 지원, Garbage Collection을 통한 자동 메모리 관리, 풍부한 표준 라이브러리, 높은 안정성과 유지보수성입니다. 또한 예외 처리와 다양한 API를 제공하여 대규모 프로젝트에서도 안정적인 개발이 가능합니다.
반면 단점으로는 JVM 위에서 실행되기 때문에 C나 C++ 같은 네이티브 언어보다 실행 속도가 다소 느릴 수 있습니다. 또한 Garbage Collection이 수행되는 동안 일시적으로 애플리케이션이 멈추는 Stop-The-World 현상이 발생할 수 있으며, JVM이 사용하는 메모리만큼 추가적인 자원이 필요합니다. 최근에는 JIT Compiler의 발전으로 성능 차이가 많이 줄어들었지만, 여전히 JVM을 거치는 구조 자체는 Java의 특징이자 단점이 될 수 있습니다.
Q4. Java는 왜 완전한 객체지향 언어가 아닌가요?
A4.
Java는 객체지향 프로그래밍을 지원하지만, 완전한 객체지향 언어는 아닙니다. 그 이유는 `int`, `double`, `char`, `boolean`과 같은 기본형(Primitive Type)이 존재하기 때문입니다. 완전한 객체지향 언어라면 모든 데이터가 객체여야 하지만, Java는 성능 향상을 위해 객체가 아닌 기본형을 제공합니다. 또한 기본형은 객체가 아니므로 메서드를 가질 수 없으며, `null`을 저장할 수도 없습니다. 다만 컬렉션과 제네릭처럼 객체가 필요한 경우에는 `Integer`, `Long`, `Double`과 같은 `Wrapper Class`를 사용하며, 오토박싱(Auto Boxing)과 언박싱(Unboxing) 기능을 통해 기본형과 객체 간의 변환을 자동으로 수행합니다.
Q5. JVM, JRE, JDK의 차이를 설명해 주세요.
A5.
JVM(Java Virtual Machine)은 Java의 Byte Code를 운영체제에 맞는 기계어로 변환하여 실행하는 가상 머신입니다.
JRE(Java Runtime Environment)는 JVM과 Java 프로그램 실행에 필요한 라이브러리를 포함한 실행 환경입니다. 따라서 Java 프로그램을 실행만 할 수 있습니다.
JDK(Java Development Kit)는 JRE에 개발 도구를 추가한 개발 환경입니다. javac 컴파일러, 디버거, 각종 개발 도구 등이 포함되어 있어 Java 프로그램을 개발하고 실행할 수 있습니다.
JDK 안에 JRE가 포함되어 있고, JRE 안에 JVM이 포함되는 구조입니다.
Q6. Java가 플랫폼 독립적인 이유는 무엇인가요?
A6.
Java는 운영체제에 맞는 기계어를 생성하지 않고, 중간 코드인 Byte Code를 생성합니다. 이후 각 운영체제에 설치된 JVM이 해당 Byte Code를 운영체제에 맞는 기계어로 해석하거나 JIT Compiler를 이용해 컴파일하여 실행합니다.
따라서 동일한 `.class` 파일을 Windows, Linux, macOS 등 서로 다른 운영체제에서도 별도의 수정 없이 실행할 수 있습니다.
이러한 구조를 "Write Once, Run Anywhere(WORA)"라고 하며, Java의 가장 큰 특징 중 하나입니다.
Q7. Java 프로그램의 실행 과정을 설명해 주세요.
A7.
Java 프로그램은 먼저 개발자가 작성한 소스 코드(`.java`)를 javac 컴파일러가 Byte Code(`.class`)로 변환합니다.
프로그램을 실행하면 JVM이 시작되고, Class Loader가 필요한 클래스 파일을 메모리에 로드합니다. 이후 Execution Engine이 Byte Code를 해석하거나 JIT Compiler를 통해 기계어로 변환하여 CPU에서 실행합니다.
실행 중 필요한 객체는 Heap 영역에 생성되며, 사용이 끝난 객체는 Garbage Collection이 자동으로 회수합니다.
따라서, Java 프로그램은 소스 코드 작성 → 컴파일 → 클래스 로딩 → 실행 → 메모리 관리의 과정을 거쳐 실행됩니다.
Q8. 컴파일 과정과 실행 과정을 설명해 주세요.
A8.
컴파일 과정에서는 개발자가 작성한 Java 소스 파일(`.java`)을 javac 컴파일러가 Byte Code인 `.class` 파일로 변환합니다.
생성된 Byte Code는 특정 운영체제에 종속되지 않는 중간 코드이며, 어떤 운영체제에서도 동일한 형태를 유지합니다. 실행 과정에서는 JVM이 Class Loader를 통해 `.class` 파일을 메모리에 로드하고, Execution Engine이 Byte Code를 실행합니다.
이때 Interpreter는 코드를 한 줄씩 해석하여 실행하고, 자주 실행되는 코드는 JIT Compiler가 기계어로 컴파일하여 캐시에 저장합니다. 이후에는 컴파일된 기계어를 직접 실행하므로 실행 속도가 향상됩니다.
즉, 컴파일 과정은 `.java`를 `.class`로 변환하는 과정이고, 실행 과정은 `.class`를 실제 CPU가 실행할 수 있는 기계어로 변환하여 실행하는 과정입니다.
Q9. 컴파일 타임과 런타임의 차이는 무엇인가요?
A9.
컴파일 타임은 소스 코드를 컴파일하는 시점을 의미하며, javac 컴파일러가 문법 오류나 타입 오류 등을 검사합니다. 예를 들어 존재하지 않는 변수 사용, 타입이 맞지 않는 대입과 같은 오류는 컴파일 단계에서 발견됩니다.
반면 런타임은 컴파일이 완료된 프로그램이 실제로 실행되는 시점입니다. 이 과정에서는 `NullPointerException`, `ArithmeticException`, `ArrayIndexOutOfBoundsException`과 같은 실행 중 오류가 발생할 수 있습니다.
따라서 컴파일 타임은 코드를 검사하고 Byte Code를 생성하는 단계이며, 런타임은 생성된 프로그램이 실제로 동작하는 단계입니다.
Q10. Byte Code란 무엇인가요?
A10.
Byte Code는 Java 소스 코드(`.java`)를 javac 컴파일러가 컴파일하여 생성하는 중간 코드입니다. 운영체제에 종속되는 기계어가 아니라 JVM이 이해할 수 있는 명령어 집합으로 이루어져 있으며, `.class` 파일 형태로 저장됩니다.
프로그램이 실행되면 JVM이 Byte Code를 읽어 Interpreter로 해석하거나 JIT Compiler를 통해 기계어로 변환하여 CPU에서 실행합니다. Java가 플랫폼 독립성을 가지는 이유도 운영체제마다 기계어를 생성하는 것이 아니라, 동일한 Byte Code를 생성하고 각 운영체제의 JVM이 Byte Code를 실행하기 때문입니다.
Q11. Java 소스 파일 하나에 여러 클래스를 작성할 수 있나요?
A11.
네, 하나의 Java 소스 파일에 여러 클래스를 작성할 수 있습니다. 다만 `public` 클래스는 파일당 하나만 존재할 수 있으며, 파일 이름은 반드시 `public` 클래스의 이름과 동일해야 합니다.
예를 들어 `User.java` 파일에는 `public class User`는 하나만 존재할 수 있지만, `Helper`나 `Util`과 같은 `default` 접근 제한자의 클래스는 함께 선언할 수 있습니다. 실무에서는 유지보수성과 가독성을 위해 일반적으로 하나의 클래스당 하나의 파일을 사용하는 것이 권장됩니다.
Q12. main() 메서드가 static인 이유는 무엇인가요?
A12.
`main()` 메서드는 Java 프로그램의 시작점(Entry Point)입니다. 프로그램이 실행되면 JVM은 아직 어떤 객체도 생성하지 않은 상태에서 가장 먼저 `main()` 메서드를 호출해야 합니다. 만약 `main()`가 인스턴스 메서드라면 먼저 객체를 생성해야 하는데, 어떤 객체를 생성해야 하는지 JVM은 알 수 없습니다. 따라서 객체 생성 없이 클래스 이름만으로 호출할 수 있도록 `main()` 메서드는 `static`으로 선언되어 있습니다.
따라서,`main()` 메서드가 `static`인 이유는 객체 생성 없이 프로그램을 시작할 수 있도록 하기 위해서입니다.
2. JVM과 메모리
Q13. JVM이란 무엇인가요?
A13.
JVM(Java Virtual Machine)은 Java 프로그램을 실행하기 위한 가상 머신입니다.
Java 소스 코드는 컴파일 과정을 거쳐 Byte Code(`.class`)로 변환되며, JVM은 이 Byte Code를 읽어 운영체제에 맞는 기계어로 변환하여 실행합니다. 또한 단순히 프로그램을 실행하는 것뿐만 아니라 클래스 로딩, 메모리 관리, Garbage Collection, 스레드 관리 등의 기능도 함께 수행합니다.
Java가 플랫폼 독립성을 가질 수 있는 이유도 운영체제마다 JVM이 존재하기 때문입니다. 동일한 Byte Code를 생성하면 각 운영체제의 JVM이 해당 환경에 맞게 실행해 주므로 하나의 프로그램을 여러 운영체제에서 사용할 수 있습니다.
Q14. JVM의 구조를 설명해 주세요.
A14.
JVM은 크게 Class Loader, Runtime Data Area, Execution Engine, Native Method Interface로 구성됩니다.
먼저 Class Loader가 실행에 필요한 `.class` 파일을 메모리에 로드합니다. 로드된 클래스는 Runtime Data Area에 저장되며, 이 영역에는 Heap, Stack, Method Area, PC Register, Native Method Stack이 포함됩니다.
이후 Execution Engine이 Byte Code를 실행합니다. 이 과정에서 Interpreter는 코드를 한 줄씩 해석하여 실행하고, 자주 실행되는 코드는 JIT Compiler가 기계어로 컴파일하여 성능을 향상시킵니다.
마지막으로 Native Method Interface(JNI)를 통해 Java가 아닌 C나 C++로 작성된 네이티브 라이브러리와 상호작용할 수 있으며, 이때 Native Method Stack이 함께 사용됩니다.
정리하자면, Class Loader가 클래스를 로드하고, Runtime Data Area에 저장한 뒤, Execution Engine이 실행하는 구조라고 이해하면 됩니다.
Q15. Class Loader란 무엇인가요?
A15.
Class Loader는 실행에 필요한 클래스 파일(`.class`)을 동적으로 메모리에 로드하는 역할을 하는 JVM의 구성 요소입니다.
Java는 프로그램 실행 시 필요한 클래스를 한 번에 모두 로드하는 것이 아니라, 실제 사용되는 시점에 필요한 클래스만 메모리에 로드합니다. 이를 동적 로딩(Dynamic Loading)이라고 합니다.
또한 동일한 클래스를 여러 번 로드하지 않도록 관리하며, 클래스의 중복 로딩을 방지합니다. 실행 과정에서는 Loading, Linking, Initialization의 순서로 클래스를 메모리에 적재한 후 프로그램이 해당 클래스를 사용할 수 있도록 준비합니다.
Q16. Class Loader의 동작 과정(Loading, Linking, Initialization)을 설명해 주세요.
A16.
Class Loader는 클래스를 메모리에 올릴 때 크게 Loading, Linking, Initialization의 세 단계를 수행합니다.
첫 번째는 Loading 단계입니다. 클래스 파일(`.class`)을 읽어 JVM 메모리에 로드하고, 해당 클래스에 대한 `Class` 객체를 생성합니다.
두 번째는 Linking 단계입니다. 이 과정은 다시 세 단계로 나뉩니다.
- Verification : 클래스 파일이 JVM 규칙에 맞는지 검증합니다.
- Preparation : `static` 변수에 기본값을 할당합니다.
- Resolution : 클래스가 참조하는 다른 클래스들을 실제 메모리 주소와 연결합니다.
마지막은 Initialization 단계입니다. 이 단계에서는 `static` 변수의 실제 값을 초기화하고, `static` 초기화 블록(`static {}`)을 실행합니다. 이 과정을 모두 마친 후에 클래스는 프로그램에서 사용할 수 있는 상태가 됩니다.
Q17. Execution Engine이란 무엇인가요?
A17.
Execution Engine은 Class Loader가 메모리에 로드한 Byte Code를 실제로 실행하는 JVM의 구성 요소입니다. Execution Engine은 Interpreter와 JIT Compiler를 함께 사용합니다. 초기에는 Interpreter가 Byte Code를 한 줄씩 해석하여 실행합니다. 이후 반복적으로 실행되는 코드는 JIT Compiler가 기계어로 컴파일하여 캐시에 저장합니다. 같은 코드가 다시 실행될 경우에는 해석 과정을 거치지 않고 컴파일된 기계어를 바로 실행하기 때문에 실행 속도가 크게 향상됩니다. 또한 실행 중 사용하지 않는 객체는 Garbage Collection을 통해 자동으로 제거하여 메모리를 관리하는 역할도 수행합니다.
Q18. Interpreter와 JIT Compiler의 차이는 무엇인가요?
A18.
Interpreter는 Byte Code를 한 줄씩 읽으면서 바로 실행하는 방식입니다. 별도의 컴파일 과정이 없기 때문에 프로그램 시작은 빠르지만, 같은 코드를 반복해서 실행할 때마다 다시 해석해야 하므로 성능이 떨어질 수 있습니다.
반면 JIT(Just-In-Time) Compiler는 자주 실행되는 코드를 기계어로 컴파일하여 메모리에 저장합니다.
이후 동일한 코드가 실행되면 Byte Code를 다시 해석하지 않고 컴파일된 기계어를 직접 실행하므로 성능이 크게 향상됩니다.
정리하자면, Interpreter는 초기 실행 속도가 빠르고, JIT Compiler는 반복 실행 시 높은 성능을 제공합니다. 현재의 JVM은 두 방식을 함께 사용하는 하이브리드 방식으로 동작합니다.
Q19. Runtime Data Area란 무엇인가요?
A19.
Runtime Data Area는 JVM이 프로그램을 실행하는 동안 사용하는 메모리 영역입니다. Java 프로그램이 실행되면 JVM은 필요한 메모리 공간을 여러 영역으로 나누어 관리하며, 각 영역은 서로 다른 역할을 수행합니다. Runtime Data Area는 Heap, JVM Stack, Method Area, PC Register, Native Method Stack으로 구성됩니다. 이 중 Heap과 Method Area는 모든 스레드가 공유하는 영역이며, JVM Stack, PC Register, Native Method Stack은 각 스레드마다 독립적으로 생성됩니다. 이러한 구조를 통해 JVM은 객체 생성, 메서드 호출, 클래스 정보 저장, 프로그램 실행 위치 등을 효율적으로 관리합니다.
Q20. Stack과 Heap 영역의 차이는 무엇인가요?
A20.
Stack과 Heap은 JVM에서 가장 중요한 메모리 영역으로, 저장하는 데이터와 메모리 관리 방식이 다릅니다.
Stack은 메서드 호출과 관련된 정보를 저장하는 영역입니다. 지역 변수, 매개변수, 반환 주소 등이 저장되며, 메서드가 호출될 때 Stack Frame이 생성되고 메서드가 종료되면 자동으로 제거됩니다.
반면 Heap은 객체와 배열이 생성되는 영역입니다. `new` 키워드를 사용하여 생성한 객체는 모두 Heap에 저장되며, 객체가 더 이상 사용되지 않으면 Garbage Collection이 자동으로 메모리를 회수합니다. 또한 Stack은 각 스레드마다 독립적으로 생성되지만, Heap은 모든 스레드가 공유하는 메모리 영역입니다.
정리하면 Stack은 메서드 실행을 위한 메모리, Heap은 객체를 저장하기 위한 메모리라고 이해하면 됩니다.
Q21. JVM Stack Frame이란 무엇인가요?
A21.
Stack Frame은 메서드가 호출될 때 JVM Stack에 생성되는 하나의 실행 단위입니다. 하나의 메서드가 호출될 때마다 하나의 Stack Frame이 생성되며, 메서드 실행에 필요한 지역 변수, 매개변수, 피연산자 스택(Operand Stack), 반환 주소 등의 정보를 저장합니다.
메서드 실행이 끝나면 해당 Stack Frame은 자동으로 제거됩니다.
예를 들어 `main()`에서 `methodA()`를 호출하고, `methodA()`가 다시 `methodB()`를 호출하면 JVM Stack에는 `main()`, `methodA()`, `methodB()` 순서로 Stack Frame이 쌓이게 됩니다.
이처럼 메서드 호출 순서대로 쌓이고 제거되는 구조를 LIFO(Last In First Out) 구조라고 합니다.
Q22. Method Area란 무엇인가요?
A22.
Method Area는 JVM이 클래스와 관련된 정보를 저장하는 메모리 영역입니다. 클래스의 이름, 메서드 정보, 필드 정보, 런타임 상수 풀(Runtime Constant Pool), static 변수 등이 저장됩니다.
프로그램이 실행되어 Class Loader가 클래스를 로드하면 해당 클래스의 정보가 Method Area에 저장됩니다. 이 영역은 모든 스레드가 공유하며, 프로그램 전체에서 하나만 존재합니다. 참고로 Java 8부터는 기존의 Permanent Generation(PermGen)이 제거되고 Metaspace가 도입되었습니다. 현재는 클래스 메타데이터가 Metaspace에 저장되며, 이는 운영체제의 네이티브 메모리를 사용합니다.
Q23. PC Register란 무엇인가요?
A23.
PC Register(Program Counter Register)는 현재 실행 중인 JVM 명령어의 주소를 저장하는 메모리 영역입니다. 프로그램은 수많은 명령어를 순차적으로 실행하는데, PC Register는 다음에 실행할 명령어의 위치를 기억하고 있습니다. 각 스레드는 독립적으로 실행되기 때문에 PC Register도 스레드마다 하나씩 생성됩니다.
이를 통해 여러 스레드가 동시에 실행되더라도 각각 자신의 실행 위치를 정확하게 기억하고 이어서 실행할 수 있습니다.
띠라서 PC Register는 현재 실행 중인 명령어의 위치를 저장하는 역할을 합니다.
Q24. Native Method Stack이란 무엇인가요?
A24.
Native Method Stack은 Java가 아닌 네이티브 메서드를 실행하기 위한 메모리 영역입니다.
Java 프로그램은 일반적으로 JVM 위에서 실행되지만, 운영체제의 기능이나 C/C++로 작성된 라이브러리를 사용해야 하는 경우가 있습니다. 이때 JNI(Java Native Interface)를 통해 네이티브 메서드를 호출하며, 해당 메서드의 실행 정보는 Native Method Stack에서 관리됩니다. 이 영역 역시 JVM Stack과 마찬가지로 스레드마다 독립적으로 생성됩니다.
실무에서는 직접 JNI를 사용하는 경우는 많지 않지만, 내부적으로 파일 입출력, 네트워크 통신, 운영체제 기능 호출 등에서 활용될 수 있습니다.
Q25. Heap 메모리의 Young Generation과 Old Generation을 설명해 주세요.
A25.
Heap 영역은 효율적인 Garbage Collection을 위해 일반적으로 Young Generation과 Old Generation으로 나누어 관리됩니다.Young Generation은 새롭게 생성된 객체가 저장되는 영역입니다. 대부분의 객체는 생성된 후 금방 사용이 끝나기 때문에 이 영역에서 빠르게 제거됩니다. Young Generation은 다시 `Eden`, `Survivor 0`, `Survivor 1` 영역으로 구성됩니다.
객체는 처음 `Eden` 영역에 생성되고, Minor GC가 발생하면 살아남은 객체는 `Survivor` 영역으로 이동합니다. 여러 번의 GC 이후에도 계속 살아남은 객체는 Old Generation으로 이동하게 됩니다. Old Generation은 오랫동안 살아남은 객체들이 저장되는 영역입니다. 이 영역이 가득 차면 Major GC 또는 Full GC가 발생하며, Young Generation보다 처리 시간이 오래 걸립니다.
이처럼 객체의 생존 기간에 따라 메모리를 분리하여 관리함으로써 Garbage Collection의 성능을 향상시킵니다.
Q26. Garbage Collection이란 무엇인가요?
A26.
Garbage Collection(GC)은 더 이상 사용되지 않는 객체를 자동으로 찾아 메모리를 회수하는 기능입니다. Java에서는 개발자가 직접 메모리를 해제하지 않아도 JVM이 객체의 참조 여부를 확인하여 사용되지 않는 객체를 제거합니다. 이를 통해 메모리 누수를 줄이고 프로그램의 안정성을 높일 수 있습니다.
Garbage Collection은 주로 Heap 영역을 대상으로 수행되며, 객체가 어떤 곳에서도 참조되지 않는 상태가 되면 회수 대상이 됩니다. Java의 가장 큰 장점 중 하나가 자동 메모리 관리이며, 이를 가능하게 하는 핵심 기능이 Garbage Collection입니다.
Q27. Garbage Collection은 언제 동작하나요?
A27.
Garbage Collection(GC)은 Heap 메모리가 부족해지거나 JVM이 메모리 회수가 필요하다고 판단했을 때 자동으로 수행됩니다. GC가 실행된다고 해서 모든 객체를 검사하는 것은 아닙니다. 일반적으로 새롭게 생성된 객체가 저장되는 Young Generation이 가득 차면 Minor GC가 먼저 발생합니다. Minor GC 이후에도 메모리가 부족하거나 Old Generation의 사용량이 증가하면 Major GC 또는 Full GC가 발생할 수 있습니다. GC의 실행 시점은 개발자가 직접 제어하는 것이 아니라 JVM이 메모리 사용량과 객체의 생명 주기 등을 고려하여 결정합니다. 참고로 `System.gc()`를 호출하여 GC를 요청할 수는 있지만, JVM이 반드시 즉시 GC를 수행하는 것은 아닙니다.
Q28. GC의 동작 과정을 설명해 주세요.
A28.
Garbage Collection은 크게 Mark, Sweep, Compact 과정을 통해 동작합니다.
먼저 Mark 단계에서는 GC Root로부터 시작하여 현재 참조되고 있는 객체를 모두 표시합니다.
다음 Sweep 단계에서는 표시되지 않은 객체, 즉 더 이상 참조되지 않는 객체를 메모리에서 제거합니다.
객체를 제거한 후 메모리 공간이 여기저기 흩어져 있으면 Compact 단계에서 살아있는 객체를 한쪽으로 이동시켜 메모리를 연속된 공간으로 정리합니다. 다만 모든 GC가 항상 세 단계를 모두 수행하는 것은 아니며, 사용하는 GC 알고리즘(G1 GC, ZGC, Serial GC 등)에 따라 동작 방식은 조금씩 다를 수 있습니다.
핵심은 사용 중인 객체는 유지하고, 더 이상 참조되지 않는 객체만 제거하여 Heap 메모리를 효율적으로 관리하는 것입니다.
Q29. Stop-The-World란 무엇인가요?
A29.
Stop-The-World(STW)는 Garbage Collection이 수행되는 동안 애플리케이션의 모든 스레드가 일시적으로 멈추는 현상을 말합니다. GC는 객체의 참조 상태를 정확하게 확인해야 하기 때문에, 객체가 계속 변경되는 상황에서는 정상적으로 메모리를 회수할 수 없습니다. 따라서 GC가 실행되는 동안에는 GC 스레드를 제외한 모든 애플리케이션 스레드가 잠시 중단됩니다.
Stop-The-World 시간이 길어질수록 사용자는 응답 지연을 경험하게 되므로, 서버 성능에 영향을 줄 수 있습니다.
최근 JVM은 G1 GC, ZGC, Shenandoah GC와 같은 최신 GC를 제공하여 Stop-The-World 시간을 최소화하도록 개선되고 있습니다.
따라서, Stop-The-World 자체는 정상적인 GC 과정의 일부이지만, 그 시간을 얼마나 줄이느냐가 GC 성능의 중요한 요소입니다.
Q30. StackOverflowError는 언제 발생하나요?
A30.
`StackOverflowError`는 JVM Stack의 공간이 부족해질 때 발생하는 오류입니다.
대표적인 원인은 무한 재귀 호출입니다. 메서드가 호출될 때마다 JVM Stack에는 새로운 Stack Frame이 생성됩니다. 하지만 종료 조건 없이 계속해서 메서드가 자기 자신을 호출하면 Stack Frame이 계속 쌓이게 되고, 결국 JVM Stack의 크기를 초과하여 `StackOverflowError`가 발생합니다.
또한 메서드 호출이 지나치게 깊어져 Stack 메모리가 부족한 경우에도 발생할 수 있습니다.
실무에서는 대부분 재귀 함수의 종료 조건이 잘못되었거나 순환 호출이 발생했을 때 자주 볼 수 있는 오류입니다.
Q31. OutOfMemoryError는 언제 발생하나요?
A31.
`OutOfMemoryError(OOM)`는 JVM이 더 이상 필요한 메모리를 확보하지 못할 때 발생하는 오류입니다.
가장 대표적인 경우는 Heap 메모리가 모두 사용되었는데도 새로운 객체를 생성해야 하는 상황입니다. 예를 들어 객체를 계속 생성만 하고 참조를 해제하지 않거나, 메모리 누수(Memory Leak)가 발생하면 GC가 메모리를 회수하지 못해 결국 `OutOfMemoryError`가 발생할 수 있습니다.
또한 Metaspace가 부족하거나, 너무 많은 스레드를 생성하여 네이티브 메모리가 부족한 경우에도 발생할 수 있습니다. 실무에서는 Heap Dump를 분석하여 어떤 객체가 메모리를 계속 점유하고 있는지 확인하고, 메모리 누수나 불필요한 객체 생성을 개선하여 문제를 해결합니다.
3. 변수와 타입
Q32. 기본형(Primitive Type)과 참조형(Reference Type)의 차이는 무엇인가요?
A32.
Java의 데이터 타입은 크게 기본형(Primitive Type)과 참조형(Reference Type)으로 나눌 수 있습니다. 기본형은 실제 데이터를 변수에 직접 저장하는 타입입니다. `byte`, `short`, `int`, `long`, `float`, `double`, `char`, `boolean` 총 8가지가 있으며, 객체가 아닌 실제 값 자체를 저장합니다.
반면 참조형은 객체가 저장된 메모리의 참조값(주소값)을 저장하는 타입입니다. `String`, 배열, 클래스, 인터페이스, `enum` 등이 모두 참조형에 해당합니다. 또한 기본형은 `null`을 저장할 수 없지만, 참조형은 객체를 참조하지 않는 상태를 나타내기 위해 `null`을 저장할 수 있습니다.
정리하자면, 기본형은 값을 저장하고, 참조형은 객체의 주소를 저장한다는 점이 가장 큰 차이입니다.
Q33. 기본형은 Stack에, 참조형은 Heap에 저장된다고 하는데 맞나요?
A33.
엄밀히 말하면 틀린 표현입니다.
많은 사람들이 "기본형은 Stack에 저장되고 참조형은 Heap에 저장된다"고 알고 있지만, 실제로는 참조형 변수도 Stack에 저장됩니다. 예를 들어, `Person person = new Person();` 코드에서 `person`이라는 변수는 JVM Stack에 저장됩니다.
반면 `new Person()`으로 생성된 객체 자체는 Heap 영역에 생성됩니다.
따라서,
- 기본형 변수 → Stack에 실제 값 저장
- 참조형 변수 → Stack에 참조값 저장
- 객체 → Heap에 저장
이 구조를 정확히 이해하는 것이 중요합니다.
Q34. 변수의 종류(지역변수, 인스턴스 변수, 클래스 변수)를 설명해 주세요.
A34.
Java의 변수는 선언 위치에 따라 지역변수(Local Variable), 인스턴스 변수(Instance Variable), 클래스 변수(Class Variable)로 구분됩니다. 지역변수는 메서드 내부에서 선언되는 변수입니다. 메서드가 호출될 때 생성되고 메서드가 종료되면 함께 제거됩니다. 또한 반드시 초기화한 후 사용해야 합니다. 인스턴스 변수는 클래스 내부에 선언되며 `static`이 없는 변수입니다. 객체가 생성될 때 함께 생성되며, 객체마다 각각 독립적인 값을 가집니다.
클래스 변수는 `static` 키워드가 붙은 변수입니다. 클래스가 메모리에 로드될 때 한 번만 생성되며, 모든 객체가 하나의 값을 공유합니다. 실무에서는 객체마다 다른 값을 저장할 때는 인스턴스 변수를 사용하고, 공통으로 사용하는 상수나 공유 데이터는 클래스 변수를 사용합니다.
Q35. 지역변수와 멤버변수의 차이는 무엇인가요?
A35.
지역변수는 메서드 내부에서 선언되는 변수이고, 멤버변수는 클래스 내부에서 선언되는 변수입니다.
지역변수는 메서드가 실행될 때 생성되고 종료되면 함께 제거됩니다. 또한 기본값이 자동으로 초기화되지 않기 때문에 반드시 값을 할당한 후 사용해야 합니다.
반면 멤버변수는 객체가 생성될 때 함께 생성되며, 초기화하지 않아도 타입에 맞는 기본값이 자동으로 저장됩니다.
예를 들어 `int`는 `0`, `boolean`은 `false`, 참조형은 `null`이 기본값으로 저장됩니다.
즉, 생성 시점, 생명 주기, 기본값 초기화 여부가 지역변수와 멤버변수의 가장 큰 차이입니다.
Q36. 기본형의 종류와 크기를 설명해 주세요.
A36.
Java의 기본형은 총 8가지이며, 정수형, 실수형, 문자형, 논리형으로 구분됩니다.
정수형은 `byte`(1Byte), `short`(2Byte), `int`(4Byte), `long`(8Byte)가 있습니다.
실수형은 `float`(4Byte)와 `double`(8Byte)가 있으며, 일반적으로 더 높은 정밀도를 제공하는 `double`을 많이 사용합니다.
문자형인 `char`는 2Byte 크기를 가지며, Unicode 문자를 저장합니다.
논리형인 `boolean`은 `true` 또는 `false` 값을 저장하며, JVM 구현에 따라 실제 메모리 크기는 달라질 수 있습니다.
이 중 정수 연산에서는 일반적으로 `int`, 실수 연산에서는 `double`이 기본적으로 많이 사용됩니다.
Q37. null이란 무엇인가요?
A37.
`null`은 참조형 변수가 어떤 객체도 참조하지 않는 상태를 의미하는 특별한 값입니다.
즉, 객체가 존재하지 않는 것이 아니라, 변수가 현재 아무 객체도 가리키고 있지 않다는 의미입니다.
예를 들어, `String str = null;` 코드에서 `str` 변수는 존재하지만, 어떤 `String` 객체도 참조하지 않습니다.
이 상태에서 `str.length()`처럼 객체의 메서드를 호출하면 `NullPointerException`이 발생합니다.
참고로`null`은 참조형에서만 사용할 수 있으며, 기본형(`int`, `double` 등)에는 저장할 수 없습니다.
Q38. Call by Value란 무엇인가요?
A38.
Call by Value는 메서드를 호출할 때 변수의 값을 복사하여 전달하는 방식입니다. Java에서는 기본형과 참조형 모두 Call by Value를 사용합니다. 기본형은 실제 값이 복사되어 전달되므로, 메서드 내부에서 값을 변경해도 원본 변수에는 영향을 주지 않습니다.
참조형은 객체 자체가 전달되는 것이 아니라 참조값이 복사되어 전달됩니다. 따라서 메서드 내부에서 객체의 필드 값을 변경하면 원본 객체도 변경되지만, 새로운 객체를 할당해도 원본 변수는 변경되지 않습니다.
정리하자면, Java에서는 항상 값을 복사해서 전달하며, 참조형이라고 해서 Call by Reference를 사용하는 것은 아닙니다.
Q39. Java는 Call by Reference를 지원하나요?
A39.
아니요. Java는 Call by Reference를 지원하지 않고, 모든 매개변수를 Call by Value 방식으로 전달합니다.
기본형은 실제 값이 복사되어 전달되므로, 메서드 내부에서 값을 변경해도 원본 변수에는 영향을 주지 않습니다.
참조형 역시 객체 자체가 전달되는 것이 아니라 객체의 참조값(주소값)이 복사되어 전달됩니다. 따라서 메서드 내부에서 객체의 필드 값을 변경하면 원본 객체에도 반영되지만, 새로운 객체를 생성하여 참조를 변경해도 원본 변수는 변경되지 않습니다.
예를 들어,
public void change(Person person) {
person = new Person();
}
위 코드에서 `person`은 새로운 객체를 참조하게 되지만, 이는 복사된 참조값이 변경된 것이므로 호출한 쪽의 변수에는 아무런 영향을 주지 않습니다. 따라서, Java는 항상 값을 복사하여 전달하며, 참조형도 예외가 아니기 때문에 Call by Reference는 지원하지 않습니다.
Q40. 형변환(Casting)이란 무엇인가요?
A40.
형변환(Casting)은 하나의 데이터 타입을 다른 데이터 타입으로 변환하는 것을 의미합니다. Java에서는 데이터의 범위가 작은 타입에서 큰 타입으로 변환하는 자동 형변환(Implicit Casting)과, 큰 타입에서 작은 타입으로 변환하는 강제 형변환(Explicit Casting)이 있습니다.
형변환은 서로 호환되는 타입 사이에서만 가능하며, 잘못된 형변환은 데이터 손실이나 예외를 발생시킬 수 있습니다.
실무에서는 숫자 타입을 변환하거나, 부모 타입과 자식 타입 사이에서 객체를 변환할 때 자주 사용됩니다.
Q41. 자동 형변환과 강제 형변환의 차이는 무엇인가요?
A41.
자동 형변환은 데이터 손실이 발생하지 않는 경우 컴파일러가 자동으로 수행하는 형변환입니다.
예를 들어 `int` 값을 `long` 변수에 저장하면 데이터의 표현 범위가 더 넓기 때문에 별도의 형변환 없이 자동으로 변환됩니다.
int num = 10;
long value = num;
반면 강제 형변환은 데이터 손실이 발생할 가능성이 있는 경우 개발자가 직접 형변환을 명시해야 합니다.
예를 들어 `long`을 `int`로 변환할 경우 값의 범위가 줄어들기 때문에 명시적인 캐스팅이 필요합니다.
long value = 100L;
int num = (int) value;
강제 형변환은 값이 표현 가능한 범위를 초과하면 데이터가 손실될 수 있으므로 주의해야 합니다.
4. Object와 String
Q42. Object 클래스란 무엇인가요?
A42.
Object 클래스는 Java의 모든 클래스가 상속받는 최상위 클래스입니다. Java에서 별도로 다른 클래스를 상속받지 않더라도 모든 클래스는 자동으로 Object 클래스를 상속받습니다. 따라서 Java의 모든 객체는 Object 클래스에 정의된 메서드를 사용할 수 있습니다.
대표적으로 객체의 문자열 표현을 반환하는 `toString()`, 객체의 동등성을 비교하는 `equals()`, 해시값을 반환하는 `hashCode()`, 객체를 복사하는 `clone()`, 클래스 정보를 반환하는 `getClass()` 등의 메서드가 있습니다. 따라서 Object 클래스는 모든 객체가 공통적으로 가져야 하는 기본 기능을 제공하는 클래스입니다.
Q43. Object 클래스의 주요 메서드를 설명해 주세요.
A43.
Object 클래스의 대표적인 메서드는 다음과 같습니다.
`equals()`는 두 객체의 논리적 동등성을 비교하는 메서드입니다.
`hashCode()`는 객체를 식별하기 위한 해시값을 반환하며, `HashMap`, `HashSet`과 같은 해시 기반 컬렉션에서 사용됩니다.
`toString()`은 객체를 문자열 형태로 표현하기 위한 메서드이며, 로그 출력이나 디버깅 시 자주 사용됩니다.
`clone()`은 객체를 복사하기 위한 메서드이며, 기본적으로 얕은 복사(Shallow Copy)를 수행합니다.
`getClass()`는 현재 객체의 클래스 정보를 담고 있는 Class 객체를 반환합니다.
또한 멀티스레드 환경에서 사용하는 `wait()`, `notify()`, `notifyAll()` 메서드도 `Object` 클래스에 정의되어 있습니다.
실무에서는 특히 `equals()`, `hashCode()`, `toString()`을 가장 자주 사용하며, 직접 재정의하는 경우도 많습니다.
Q44. ==와 equals()의 차이는 무엇인가요?
A44.
`==` 연산자는 두 값이 같은지 비교하는 연산자입니다.
기본형에서는 실제 값을 비교하지만, 참조형에서는 객체의 참조값(주소값)이 같은지를 비교합니다.
반면 `equals()`는 객체의 논리적인 동등성을 비교하기 위한 메서드입니다.
기본적으로 `Object` 클래스의 `equals()`는 `==`와 동일하게 참조값을 비교합니다.
하지만 `String`, `Integer` 등 많은 클래스에서는 `equals()`를 재정의하여 객체의 내용을 비교하도록 구현되어 있습니다. 예를 들어,
String a = new String("Java");
String b = new String("Java");
`a == b`는 서로 다른 객체이므로 `false`를 반환하지만,
`a.equals(b)`는 문자열의 내용을 비교하므로 `true`를 반환합니다.
따라서 객체의 내용을 비교할 때는 `equals()`를 사용하는 것이 올바른 방법입니다.
Q45. hash란 무엇인가요?
A45.
Hash는 임의의 데이터를 고정된 크기의 값(Hash Value)으로 변환하는 기술을 의미합니다.
예를 들어 문자열이나 객체와 같이 크기가 다양한 데이터를 일정한 크기의 숫자로 변환하여 빠르게 저장하고 검색할 수 있도록 합니다. Java에서는 `HashMap`, `HashSet`, `Hashtable`과 같은 해시 기반 컬렉션에서 Hash를 사용합니다. 예를 들어 `HashMap`에 데이터를 저장하면 먼저 Key의 `hashCode()`를 이용해 Hash 값을 계산한 후, 해당 Hash 값을 기반으로 데이터를 저장할 위치(Bucket)를 찾습니다.
이러한 방식 덕분에 데이터를 순차적으로 탐색하지 않고도 원하는 데이터를 매우 빠르게 찾을 수 있으며, 평균적으로 O(1)의 시간 복잡도를 가집니다. 다만 서로 다른 객체가 같은 Hash 값을 가지는 Hash Collision(해시 충돌)이 발생할 수 있으며, Java는 `equals()`를 이용해 최종적으로 같은 객체인지 다시 비교합니다.
따라서, Hash는 데이터를 빠르게 저장하고 검색하기 위한 기술이라고 이해하면 됩니다.
Q46. hashCode()란 무엇인가요?
A46.
`hashCode()`는 객체를 식별하기 위한 정수형 Hash 값을 반환하는 메서드입니다.
`Object` 클래스에 정의되어 있으며, 모든 Java 객체는 `hashCode()` 메서드를 가지고 있습니다.
주로 `HashMap`, `HashSet`과 같은 해시 기반 컬렉션에서 객체를 저장하거나 검색할 위치를 결정하기 위해 사용됩니다.
예를 들어 `HashMap`은 먼저 Key 객체의 `hashCode()`를 호출하여 저장할 Bucket을 찾고, 이후 같은 Bucket 안에서 `equals()`를 이용해 실제로 동일한 객체인지 확인합니다. 따라서 `hashCode()`는 객체의 저장 위치를 빠르게 찾기 위한 역할을 하고, `equals()`는 실제로 같은 객체인지 확인하는 역할을 합니다.
Q47. equals()와 hashCode()를 함께 재정의하는 이유는 무엇인가요?
A47.
`equals()`를 재정의하여 두 객체를 같은 객체라고 판단한다면, `hashCode()`도 반드시 같은 값을 반환해야 합니다.
그 이유는 `HashMap`, `HashSet`과 같은 해시 기반 컬렉션이 먼저 `hashCode()`를 이용해 저장 위치를 찾고, 이후 `equals()`를 호출하여 실제 동일한 객체인지 비교하기 때문입니다.
만약 `equals()`는 `true`를 반환하지만 `hashCode()`가 서로 다르다면, 서로 다른 Bucket에 저장되어 같은 객체임에도 찾지 못하거나 중복 저장되는 문제가 발생할 수 있습니다. 예를 들어, 회원 번호가 같은 두 `User` 객체를 동일한 객체로 판단하도록 `equals()`를 재정의했다면, `hashCode()` 역시 회원 번호를 기준으로 동일한 Hash 값을 반환하도록 함께 재정의해야 합니다. 따라서, 논리적으로 같은 객체라면 반드시 같은 `hashCode()`를 가져야 하며, 이것이 Java에서 `equals()`와 `hashCode()`를 함께 재정의해야 하는 이유입니다.
Q48. toString()은 왜 사용하는가요?
A48.
`toString()`은 객체를 문자열 형태로 표현하기 위한 메서드입니다. 기본적으로 `Object` 클래스의 `toString()`은 클래스 이름 + 해시코드 형태의 문자열을 반환합니다. 예를 들어 `User@4a54c0de`와 같은 값이 출력되는데, 이는 사람이 이해하기 어려운 정보입니다.
그래서 일반적으로 `toString()`을 재정의하여 객체의 주요 필드 값을 문자열로 반환하도록 구현합니다. 예를 들어 `User` 객체라면 이름, 나이, 이메일 등의 정보를 반환하도록 구현하면 로그 출력이나 디버깅 시 객체의 상태를 쉽게 확인할 수 있습니다.
실무에서는 로그를 출력하거나 디버깅을 할 때 매우 자주 사용되며, Lombok의 `@ToString`도 내부적으로 `toString()`을 자동 생성해 주는 기능입니다.
Q49. clone()이란 무엇인가요?
A49.
`clone()`은 객체를 복사하기 위한 메서드입니다.
`Object` 클래스에 정의되어 있으며, 사용하려면 해당 클래스가 `Cloneable` 인터페이스를 구현해야 합니다.
기본적으로 `clone()`은 얕은 복사(Shallow Copy)를 수행합니다. 얕은 복사에 의해, 기본형 필드는 값이 복사되지만, 참조형 필드는 참조값만 복사되므로 원본 객체와 복사된 객체가 같은 내부 객체를 공유하게 됩니다.
만약 참조형 객체까지 모두 새롭게 복사하려면 깊은 복사(Deep Copy)를 직접 구현해야 합니다. 최근 실무에서는 `clone()`보다 복사 생성자(Copy Constructor)나 정적 팩토리 메서드, 또는 라이브러리를 이용한 객체 복사를 더 많이 사용하는 편입니다.
Q50. Deep Copy와 Shallow Copy의 차이는 무엇인가요?
A50.
Shallow Copy(얕은 복사)는 객체를 복사할 때 참조형 필드의 참조값만 복사하는 방식입니다. 얕은 복사를 수행하면 새로운 객체는 생성되지만 내부의 참조형 객체는 원본과 동일한 객체를 공유합니다. 따라서 복사된 객체에서 내부 객체를 변경하면 원본 객체에도 동일한 변경이 반영될 수 있습니다.
반면, Deep Copy(깊은 복사)는 참조형 필드까지 모두 새로운 객체로 생성하여 복사하는 방식입니다. 깊은 복사를 수행하면 원본 객체와 복사된 객체가 완전히 독립적인 객체가 되므로 한쪽의 변경이 다른 객체에 영향을 주지 않습니다.
실무에서는 객체의 독립성이 중요한 경우에는 Deep Copy를 사용하고, 단순히 객체의 구조만 복사해도 되는 경우에는 Shallow Copy를 사용할 수 있습니다. 최근에는 `clone()`보다는 복사 생성자나 `Builder`, 또는 직렬화 라이브러리 등을 이용하여 Deep Copy를 구현하는 경우가 많습니다.
Q51. String이란 무엇인가요?
A51.
String은 Java에서 문자열을 표현하기 위한 클래스입니다. 문자열은 개발에서 가장 자주 사용하는 데이터 중 하나이기 때문에 Java는 String 클래스를 기본적으로 제공합니다. `String`은 참조형(Reference Type)이며, 내부적으로 문자 배열(`char[]`)을 기반으로 문자열을 관리합니다. 또한 한 번 생성된 문자열의 내용은 변경할 수 없는 Immutable(불변 객체)으로 설계되어 있습니다.
문자열 리터럴(`"Hello"`)을 사용하면 String Pool을 통해 동일한 문자열 객체를 재사용하여 메모리를 효율적으로 관리합니다. 실무에서는 문자열 변경이 거의 없는 경우에는 `String을` 사용하고, 문자열을 반복적으로 수정해야 하는 경우에는 `StringBuilder`를 사용하는 것이 일반적입니다.
Q52. String은 왜 Immutable(불변)인가요?
A52.
String은 한 번 생성되면 내부 값을 변경할 수 없는 Immutable(불변 객체)입니다.
이렇게 설계한 이유는 크게 메모리 효율성, 보안성, 스레드 안정성, Hash 안정성 때문입니다.
첫째, String Pool에서는 동일한 문자열 객체를 여러 곳에서 공유합니다. 만약 문자열을 변경할 수 있다면 한 곳에서 문자열을 수정했을 때 다른 객체에도 영향을 줄 수 있습니다.
둘째, 문자열은 데이터베이스 접속 정보, 파일 경로, URL 등 중요한 정보를 저장하는 데 많이 사용됩니다. 불변 객체이기 때문에 중간에 값이 변경되는 것을 방지하여 보안성을 높일 수 있습니다.
셋째, `String`은 불변 객체이므로 여러 스레드가 동시에 접근해도 동기화가 필요 없어 Thread Safe합니다.
마지막으로 `String`은 `HashMap`의 Key로 자주 사용되는데, 문자열이 변경된다면 `hashCode()`도 변경되어 정상적으로 객체를 찾지 못하는 문제가 발생할 수 있습니다.
따라서 String을 불변으로 설계함으로써 메모리 효율성, 보안성, 스레드 안정성, Hash 안정성을 모두 확보할 수 있습니다.
Q53. String은 왜 final 클래스인가요?
A53.
`String` 클래스는 `final`로 선언되어 있기 때문에 다른 클래스가 상속할 수 없습니다.
그 이유는 불변성(Immutable)을 보장하기 위해서입니다. 만약 `String`을 상속할 수 있다면 자식 클래스에서 문자열의 동작을 변경하거나 내부 데이터를 수정할 수 있게 되어 `String`의 불변성이 깨질 수 있습니다. 또한 String Pool을 통한 객체 공유, `hashCode()` 캐싱, 보안성 등의 특징도 보장하기 어려워집니다. 따라서 Java는 `String`을 `final` 클래스로 설계하여 문자열의 동작이 변경되지 않도록 하고, 안정성과 예측 가능성을 보장하고 있습니다.
Q54. String Pool이란 무엇인가요?
A54.
String Pool은 문자열 리터럴을 효율적으로 관리하기 위해 JVM이 사용하는 메모리 영역입니다.
예를 들어,
String a = "Java";
String b = "Java";
위 코드에서 "Java"라는 문자열은 String Pool에 한 번만 생성됩니다.
이후 동일한 문자열 리터럴이 사용되면 새로운 객체를 생성하지 않고, 기존 객체를 재사용합니다.
이러한 방식은 동일한 문자열 객체를 여러 변수가 공유할 수 있게 하여 메모리 사용량을 줄이고 객체 생성 비용을 절약하는 장점이 있습니다.
반면, `String a = new String("Java");` 처럼 `new` 키워드를 사용하면 Heap 영역에 새로운 `String` 객체가 생성됩니다.
즉, 문자열 리터럴은 String Pool을 사용하고, `new String()`은 새로운 객체를 생성한다는 점이 가장 큰 차이입니다.
Q55. String Pool은 Heap의 어디에 존재하나요?
A55.
String Pool은 Heap 메모리 내부에 존재합니다. 과거 Java 7 이전에는 PermGen(Permanent Generation) 영역에서 관리되었지만, Java 7부터는 Heap 영역으로 이동했습니다. 현재 Java에서는 문자열 리터럴을 생성하면 Heap 내부의 String Pool에 저장되며, 동일한 문자열이 존재하면 기존 객체를 재사용합니다. Java 8에서는 PermGen이 제거되고 Metaspace가 도입되었지만, String Pool은 계속해서 Heap 영역에서 관리됩니다. 따라서 현재는 "String Pool은 Heap 내부에 존재한다."입니다.
Q56. new String("abc")와 "abc"의 차이는 무엇인가요?
A56.
가장 큰 차이는 객체 생성 방식과 메모리 사용 방식입니다.
String a = "abc";
위 코드는 문자열 리터럴을 사용한 것으로, "abc"가 String Pool에 존재하면 기존 객체를 재사용하고, 없으면 새로 생성합니다.
반면,
String b = new String("abc");
는 먼저 `"abc"`가 String Pool에 있는지 확인하고, 이후 Heap 영역에 새로운 String 객체를 추가로 생성합니다.
즉, `new String()`은 항상 새로운 객체를 생성하기 때문에 메모리를 더 사용하게 됩니다.
실무에서는 특별한 이유가 없다면 `new String()`보다는 문자열 리터럴을 사용하는 것이 메모리 효율성과 성능 측면에서 유리합니다.
Q57. intern() 메서드는 무엇인가요?
A57.
intern()은 문자열을 String Pool에서 관리하도록 하는 메서드입니다.
예를 들어,
String str = new String("Java");
위 코드는 Heap에 새로운 객체를 생성합니다.
이후,
str.intern();
을 호출하면 String Pool에서 "Java"를 찾습니다.
이미 존재한다면 String Pool에 있는 객체의 참조를 반환하고, 존재하지 않는다면 해당 문자열을 String Pool에 등록한 후 그 참조를 반환합니다.
즉, `intern()`은 문자열을 String Pool 기준으로 관리하고 싶을 때 사용하는 메서드입니다. 다만 실무에서는 일반적인 문자열 처리에서는 거의 사용하지 않으며, 메모리 최적화가 필요한 특수한 상황에서만 사용하는 경우가 많습니다.
Q58. StringBuilder란 무엇인가요?
A58.
`StringBuilder`는 가변(Mutable) 문자열을 다루기 위한 클래스입니다.
`String`은 문자열을 변경할 때마다 새로운 객체를 생성하지만, `StringBuilder`는 기존 객체의 내부 버퍼를 직접 변경합니다. 따라서 문자열을 반복적으로 추가하거나 수정하는 작업에서는 불필요한 객체 생성이 발생하지 않아 `String`보다 훨씬 좋은 성능을 제공합니다.
`StringBuilder`는 동기화(Synchronization)를 지원하지 않기 때문에 Thread Safe하지 않습니다. 하지만 단일 스레드 환경에서는 가장 빠른 문자열 처리 방법으로 많이 사용됩니다. 실무에서는 반복문에서 문자열을 이어 붙이거나, 대량의 문자열을 조합해야 하는 경우 `StringBuilder`를 사용하는 것이 일반적입니다.
Q59. StringBuffer란 무엇인가요?
A59.
`StringBuffer`는 가변(Mutable) 문자열을 다루기 위한 클래스이며, 기능은 `StringBuilder`와 거의 동일합니다. 가장 큰 차이점은 동기화(Synchronization)를 지원한다는 점입니다. `StringBuffer`의 주요 메서드는 `synchronized`로 구현되어 있기 때문에 여러 스레드가 동시에 접근해도 데이터의 일관성을 보장할 수 있습니다. 하지만 동기화를 수행하는 만큼 `StringBuilder`보다 성능은 다소 느립니다.
실무에서는 대부분 단일 스레드 환경이므로 `StringBuilder`를 많이 사용하며, 여러 스레드가 하나의 문자열 객체를 공유해야 하는 특수한 경우에만 `StringBuffer`를 사용하는 경우가 있습니다.
Q60. String, StringBuilder, StringBuffer의 차이는 무엇인가요?
A60.
세 클래스의 가장 큰 차이는 불변성(Immutable)과 동기화(Synchronization)입니다.
`String`은 Immutable(불변 객체)입니다. 문자열을 변경하면 기존 객체를 수정하는 것이 아니라 새로운 객체를 생성합니다.
`StringBuilder`는 Mutable(가변 객체)이며, 내부 버퍼를 직접 수정하기 때문에 문자열을 반복적으로 변경하는 작업에서 가장 빠른 성능을 제공합니다. 대신 동기화를 지원하지 않아 Thread Safe하지 않습니다.
`StringBuffer` 역시 Mutable(가변 객체)이지만, 메서드에 동기화가 적용되어 있어 여러 스레드에서 안전하게 사용할 수 있습니다. 다만 동기화 비용 때문에 `StringBuilder`보다 성능은 다소 떨어집니다.
정리하면,
- `String` → 문자열 변경이 거의 없을 때 사용
- `StringBuilder` → 문자열 변경이 많고 단일 스레드 환경일 때 사용
- `StringBuffer` → 문자열 변경이 많고 멀티스레드 환경일 때 사용
실무에서는 대부분 `StringBuilder`를 사용하며, `StringBuffer`를 사용하는 경우는 많지 않습니다.
Q61. StringBuilder가 String보다 빠른 이유는 무엇인가요?
A61.
`StringBuilder`가 `String`보다 빠른 이유는 문자열을 변경할 때 새로운 객체를 생성하지 않기 때문입니다.
`String`은 Immutable(불변 객체)이므로 문자열을 이어 붙일 때마다 새로운 `String` 객체가 생성됩니다.
예를 들어,
String str = "";
for (int i = 0; i < 1000; i++) {
str += i;
}
위 코드는 반복문이 실행될 때마다 새로운 `String` 객체가 계속 생성됩니다.
객체 생성 비용뿐만 아니라 기존 문자열을 복사하는 비용도 발생하기 때문에 성능이 크게 저하됩니다.
반면 `StringBuilder`는 내부 버퍼를 사용하여 기존 객체를 그대로 유지한 채 문자열만 추가합니다.
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
이 경우에는 새로운 객체를 계속 생성하지 않고 내부 버퍼에 데이터를 추가하기 때문에 훨씬 효율적으로 동작합니다.
따라서 문자열 변경이 자주 발생하는 경우에는 `String`보다 `StringBuilder`를 사용하는 것이 성능상 훨씬 유리합니다.
Q62. StringBuilder는 내부적으로 어떻게 동작하나요?
A62.
StringBuilder는 내부적으로 가변 문자 배열(Buffer)을 사용하여 문자열을 관리합니다.
`append()`를 호출하면 새로운 String 객체를 생성하는 것이 아니라, 기존 버퍼의 뒤에 문자를 추가합니다.
버퍼의 공간이 부족해지면 더 큰 크기의 새로운 배열을 생성한 후 기존 데이터를 복사하여 확장합니다.
기본 생성자인 `new StringBuilder();`를 사용하면 기본적으로 16개의 문자를 저장할 수 있는 버퍼가 생성됩니다.
버퍼가 가득 차면 일반적으로 기존 크기 × 2 + 2 만큼 새로운 배열을 생성하여 확장합니다.
이처럼 `StringBuilder`는 문자열을 변경할 때마다 새로운 객체를 생성하지 않고 내부 버퍼만 관리하기 때문에 `String`보다 훨씬 뛰어난 성능을 제공합니다. 실무에서도 반복적인 문자열 조합이 필요한 경우에는 `String` 대신 `StringBuilder`를 사용하는 것이 권장됩니다.
