Q1. 가비지 컬렉션이란?
가비지 컬렉션은 자바의 메모리 관리 방법 중의 하나로 JVM의 Heap 영역에서 동적으로 할당했던 메모리 중 필요 없게 된 메모리 객체를 모아 주기적으로 제거하는 프로세스를 말합니다.
GC 알고리즘 종류로는 Serial GC, Parallel GC, G1 GC, ZGC를 알고 있습니다.
Q2. 동일성과 동등성에 대해 설명해주세요.
동등성은 논리적으로 객체의 내용이 같은지를 비교하는 개념입니다. 자바에서는 equals() 메서드를 사용하여 객체의 동등성을 비교합니다.
동일성은 두 객체가 메모리 상에서 같은 객체인지 비교하는 개념입니다. 자바에서는 == 연산자를 사용하여 객체의 동일성을 비교합니다. == 연산자는 객체의 레퍼런스(참조)를 비교하므로, 두 변수가 동일한 객체를 가리키고 있는지를 확인합니다.
Q3. 일급 컬렉션이 무엇인가요?
일급 컬렉션(First-Class Collection)은 하나의 컬렉션을 감싸는 클래스를 만들고, 해당 클래스에서 컬렉션과 관련된 비즈니스 로직을 관리하는 패턴을 말합니다. 일급 컬렉션 클래스에 로직을 포함하거나 비즈니스에 특화된 명확한 이름을 부여할 수 있습니다. 또한, 불필요한 컬렉션 API를 외부로 노출하지 않도록 할 수 있으며, 컬렉션을 변경할 수 없도록 만든다면 예기치 않은 변경으로부터 데이터를 보호할 수 있습니다.
Q4. Checked Exception과 Unchecked Exception에 대해서 설명해주세요.
Checked Exception은 컴파일 시점에 확인되며, 반드시 처리해야 하는 예외입니다.
자바에서는 IOException, SQLException 등이 이에 속합니다.
Checked Exception을 유발하는 메서드를 호출하는 경우, 메서드 시그니처에 throws를 사용하여 호출자에게 예외를 위임하거나 메서드 내에서 try-catch를 사용하여 해당 예외를 반드시 처리해야 합니다.
Unchecked Exception은 런타임 시점에 발생하는 예외로, 컴파일러가 처리 여부를 강제하지 않습니다. 자바에서는 RuntimeException을 상속한 예외들이 해당됩니다. 일반적으로 프로그래머의 실수나 코드 오류로 인해 발생합니다.
Q5. Error와 Exception의 차이는 무엇인가요?
Error는 주로 JVM에서 발생하는 심각한 문제로, OutOfMemoryError, StackOverflowError 등 시스템 레벨에서 발생하는 오류입니다. 이는 일반적으로 프로그램에서 처리하지 않으며, 회복이 어려운 오류에 속하며, 애플리케이션 코드에서 복구할 수 없는 심각한 문제를 나타냅니다. 따라서 예외 처리를 하지 않으며, 보통 프로그램을 종료해야 합니다.
반면, Exception은 프로그램 실행 중 발생할 수 있는 오류 상황을 나타냅니다. 대부분의 경우 회복 가능성이 있으며, 프로그램 내에서 예외 처리를 통해 오류 상황을 제어할 수 있습니다. Exception은 다시 Checked Exception과 Unchecked Exception으로 나눌 수 있습니다.
Q6. JVM에서 GC 대상 객체를 판단하는 기준은 무엇인가요?
GC는 특정 객체가 사용 중인지 아닌지 판단하기 위해서 도달 가능성(Reachability) 라는 개념을 사용하는데요. 특정 객체에 대한 참조가 존재하면 도달할 수 있으며, 참조가 존재하지 않는 경우에 도달할 수 없는 상태로 간주합니다. 이때, 도달할 수 없다는 결론을 내린다면 해당 객체는 GC의 대상이 됩니다.
도달 가능성 판단에 있어 힙 영역에 있는 객체에 대한 참조를 확인하게 되는데요. 이 경우는 4가지 케이스가 존재합니다.힙 내부 객체 간의 참조, 스택 영역의 변수에 의한 참조, JNI에 의해 생성된 객체에 대한 참조(네이티브 스택 영역), 메서드 영역의 정적 변수에 의한 참조가 이에 해당됩니다. 이때, 힙 내부 객체 간의 참조를 제외한 나머지를 Root Set이라고 합니다. Root Set으로부터 시작한 참조 사슬에 속한 객체들은 도달할 수 있는 객체이며, 이 참조 사슬과 무관한 객체들은 도달하기 어렵기 때문에 GC에 대상이 됩니다.
Q7. 자바에서 클래스 정보는 어떻게 알아낼 수 있나요?
자바에서 클래스 정보를 가져오기 위해서 Reflection API를 사용할 수 있습니다. reflection 패키지에서 제공하는 클래스를 사용하면, JVM에 로딩되어 있는 클래스와 메서드의 정보를 읽어올 수 있습니다. 대표적으로 Class 클래스, Method 클래스, Field 클래스가 존재합니다.
Reflection API를 사용하면 구체적인 클래스의 타입을 몰라도, 클래스의 정보에 접근할 수 있습니다. 개발자는 이러한 특성을 이용하여 인스턴스를 감싸는 프록시를 만들거나, 사용자로부터 전달된 값을 처리할 메서드를 유연하게 선택하는 등 다양한 구현을 할 수 있습니다. Reflection API는 특히 프레임워크나 라이브러리를 개발하는 과정에서 사용되는 경우가 많습니다. 프레임워크나 라이브러리의 개발자는 사용자가 작성한 클래스에 대한 정보를 알 수 없기 때문입니다.
Q8. try-with-resources에 대해 설명해 주세요.
커넥션, 입출력 스트림과 같은 자원을 사용한 후에는 자원을 해제해서 성능 문제, 메모리 누수 등을 방지해야 하는데,try-with-resources는 이러한 자원을 자동으로 해제해줍니다.
try-with-resources가 정상적으로 동작하려면 AutoCloseable 인터페이스를 구현한 객체를 사용해야 하고, try() 괄호 내에서 변수를 선언해야 합니다.
try-catch-finally 대신 try-with-resources를 사용해야 하는 이유는 무엇인가요?
try-catch-finally는 finally 블록에서 close()를 명시적으로 호출해야 합니다. 하지만 close() 호출을 누락하거나 이 과정에서 또 다른 예외가 발생하면 예외 처리가 복잡해지는 문제가 있습니다.
또한 여러 개의 자원을 다룰 경우, 먼저 close()를 호출한 자원에서 에러가 발생하면 다음에 close()를 호출한 자원은 해제되지 않습니다. 이를 해결하려면 추가적인 try-catch-finally가 필요하기 때문에 가독성이 떨어지고, 실수할 가능성이 높습니다.
try-with-resources를 사용하면 try-catch-finally의 문제를 해결할 수 있습니다.
- try 블록이 종료될 때
close()를 자동으로 호출해서 자원을 해제합니다. - finally 블록 없이도 자원을 안전하게 정리하기 때문에 코드가 간결해집니다.
- try 문에서 여러 자원을 선언하면, 선언된 반대 순서로 자동 해제됩니다.
Q9. 자바에서 Object 타입인 value를 String으로 타입 캐스팅하는 것과 String.valueOf()를 사용하는 것의 차이점은 무엇인가요?
두 방식 모두 String 타입으로 변환하는 것은 동일하지만, 동작 방식과 예외 처리에서 차이가 있습니다.
(String) value로 타입 캐스팅 하는 것은 value가 String 타입이 아닌 경우 ClassCastException이 발생하며, value가 null인 경우 그대로 null을 반환하여 이후 메서드를 호출할 때 NullPointerException이 발생합니다. 타입 캐스팅은 타입 안정성이 부족하기 때문에 캐스팅하는 타입이 확실할 때만 사용해야 합니다.
Object intValue = 10;
String str1 = (String) intValue; // ClassCastException
Object nullValue = null;
String str2 = (String) nullValue; // null
str2.concat("maeilmail"); // NullPointerException
String.valueOf(value)는 value가 String 타입이 아닌 경우 value.toString()을 호출하여 String으로 변환하며, value가 null인 경우 “null” 문자열을 반환합니다.
Object intValue = 10;
String str1 = String.valueOf(intValue); // "10"
Object nullValue = null;
String str2 = String.valueOf(nullValue); // "null"
str2.concat("maeilmail"); // "nullmaeilmail
타입 캐스팅에서 발생하는 예외는 런타임 시점에 발생하기 때문에 String.valueOf()가 더 안전하고 예외를 방지할 수 있습니다.
Q10. 자바에서 제네릭의 공변, 반공변, 무공변에 대해 설명해 주세요.
자바에서 제네릭(Generic) 은 기본적으로 무공변(Invariant) 입니다. 무공변이란 타입 S, T가 있을 때 서로 관계가 없다는 것을 의미합니다. S와 T가 서로 상속 관계이면 공변성이 있지만 제네릭은 상속 관계가 호환되지 않습니다. 따라서 타입이 정확히 일치하지 않으면 컴파일 에러가 발생합니다.
public class Animal {
}
public class Cat extends Animal {
}
List<Animal> animals = new ArrayList<Cat>(); // 컴파일 에러
List<Cat> cats = new ArrayList<Animal>(); // 컴파일 에러
무공변은 타입 안정성을 보장하지만 타입의 유연성이 부족하다는 단점이 있어, 자바에서는 와일드카드(?)와 extends, super 키워드로 공변과 반공변을 지원합니다.
공변(Covariant) 은 S가 T의 하위 타입일 때 S는 T가 될 수 있다는 것을 의미합니다. 제네릭에서는 <? extends T>를 사용하여 하위 타입을 허용하고 읽기 전용으로 사용할 수 있습니다. 쓰기는 null만 가능합니다.
반공변(Contravariant) 은 S가 T의 하위 타입일 때 T는 S가 될 수 있다는 것을 의미합니다. 제네릭에서는 <? super S>를 사용하여 상위 타입을 허용하고 쓰기 전용으로 사용할 수 있습니다. 읽기는 Object 타입으로만 가능합니다.
PECS란 무엇인가요?
PECS(Producer Extends, Consumer Super) 는 제네릭에서 와일드카드의 상위 또는 하위 경계를 설정할 때 사용하는 가이드라인입니다. 객체를 생산할 때는 <? extends T>를 사용하고, 소비할 때는 <? super T>를 사용합니다.
public void produce(List<? extends Animal> animals) { // animals가 생산자 역할
for (Animal a : animals) {
System.out.println(a);
}
}
public void consume(List<? super Cat> cats) { // cats가 소비자 역할
cats.add(new Cat());
}
<?>와 <Object>의 차이점은 무엇인가요?
<?>와 <Object>는 모든 타입을 수용하는 것처럼 보이지만 동작 방식에 차이가 있습니다.
<?>는 모든 타입을 메서드 인자로 받을 수 있지만 null 외에는 값을 추가할 수 없기 때문에 읽기 전용으로 사용됩니다. <Object>는 <Object> 외의 타입을 메서드 인자로 받을 수 없지만 모든 객체를 추가할 수 있기 때문에 읽기, 쓰기 모두 가능합니다.
Q11. 자바 프로그램이 실행되는 흐름을 설명해 주세요.
우리가 작성한 .java 파일은 JDK에 포함된 javac(java compiler) 를 통해 컴파일됩니다. 이 과정에서 JVM이 이해할 수 있는 바이트 코드로 변환되어 .class 파일이 생성됩니다. 이후부터는 JVM이 담당하는데요. 먼저 클래스 로더(Class Loader) 가 바이트 코드를 JVM 메모리에 동적으로 로드합니다. 로드된 바이트 코드는 Method Area에 저장되며, 이때 로딩(Loading), 링킹(Linking), 초기화(Initialization) 단계를 거칩니다. 그다음, 실행 엔진(Execution Engine) 이 로드된 바이트 코드를 실행합니다. 하지만 바이트 코드는 컴퓨터가 읽을 수 없기 때문에 인터프리터(Interpreter) 와 JIT 컴파일러(Just-In-Time Compiler) 를 함께 사용하여 기계어로 변환합니다. 인터프리터는 바이트 코드를 한 줄씩 읽어서 실행하는 방식이고, JIT 컴파일러는 자주 실행되는 메서드(Hotspot)를 감지하면 해당 메서드 전체를 네이티브 코드로 변환하여 캐싱합니다.
Q12. ThreadLocal에 대해 설명해 주세요.
ThreadLocal은 Java에서 각 스레드마다 독립적인 변수를 저장할 수 있도록 도와주는 클래스입니다. 보통 여러 스레드가 공유 자원을 사용하면 동시성 문제가 발생할 수 있는데, ThreadLocal을 사용하면 스레드별로 데이터를 분리할 수 있어 동기화 없이 안전하게 활용할 수 있습니다. 각 스레드는 자신만의 ThreadLocalMap을 가지고 있고 ThreadLocal을 키로 사용하여 값을 저장합니다. 즉, 하나의 스레드에서 여러 개의 ThreadLocal을 사용할 수 있으며, ThreadLocal은 현재 스레드의 ThreadLocalMap을 제어하는 역할을 합니다.
Spring 생태계에서는 ThreadLocal을 사용하여 트랜잭션 동기화 관리(TransactionSynchronizationManager), 사용자 인증 정보 관리(SecurityContextHolder), 웹 요청의 attribute 관리(RequestContextHolder) 등의 기능을 제공하고 있습니다.
ThreadLocal의 장점은 무엇인가요?
각 스레드는 ThreadLocal에 접근할 때 다른 스레드와 격리된 값을 가질 수 있습니다. 그리고 공유 자원이 없기 때문에 synchronized 키워드 등을 사용해서 동기화할 필요가 없습니다.
이 페이지는 면접 준비를 위한 정리 페이지입니다.