트이타에서 와방 유명한 후르츠 리스트에 정리된 질문 목록인데, 질문만 있고 답변은 없어서 내가 직접 답변을 달아보면서 공부하면 되겠네 싶어서 써본다..
이 글에서는 JAVA 관련 면접 질문들에 대한 내 나름의 답변을 정리해볼 예정이다.
완벽한 정답이 아닐 수도 있지만, 공부하면서 이해한 내용을 내 방식대로 풀어써보려고 한다.
이 시리즈 활용법
이런 분들에게 추천:
- 면접 준비 중인데 답변이 막막한 분
- Spring 개념을 실무와 연결하고 싶은 분
- 남의 정리본 말고 내 언어로 소화하고 싶은 분
주의사항:
- 이건 공식 문서가 아니라 내 이해를 정리한 글
- 틀린 부분 있으면 댓글로 알려주세요!
- 더 좋은 답변 있으면 공유해주세요
노란색 : 아에 모름 ! (공부해야함 !)
연노랑 : 반쯤 앎 -> 말로 설명할 수 있어야 함
흰색 : 앎
Spring / 스프링
📌 Java 기초 개념
JAVA-041 : JVM이 정확히 무엇이고, 어떤 기능을 하는지 설명해 주세요.
JVM(Java Virtual Machine)은 자바 바이트코드를 실행하기 위한 가상 컴퓨터입니다.
핵심 기능은
첫째, 플랫폼 독립성 제공입니다. OS에 종속적인 네이티브 코드가 아닌 바이트코드를 실행함으로써 '한 번 작성해 어디서든 실행'하는 환경을 구현합니다.
둘째, 자동 메모리 관리입니다. 가비지 컬렉션(GC)을 통해 힙 영역의 미사용 메모리를 스스로 정리하여 개발자의 부담을 줄여줍니다.
마지막으로, JIT 컴파일러를 통해 자주 실행되는 코드를 기계어로 캐싱함으로써 인터프리터 방식의 속도 저하를 보완하는 성능 최적화 기능도 수행합니다
=> 왜 이런 답변이 가능한지를 좀 더 추가함 1
- 플랫폼 독립성 -> Write Once, Run Anywhere (WORA) 철학, 추상화 레이어에 대한 내용
- 자바 소스 코드($.java$)는 컴파일러를 통해 바이트코드($.class$)로 변환됩니다. JVM은 이 바이트코드를 해당 OS가 이해할 수 있는 기계어로 해석해줍니다. 즉, 개발자는 OS마다 코드를 새로 짤 필요가 없습니다.
- 주요 구성 요소와 기능
- Class Loader: 컴파일된 바이트코드를 JVM 내로 로드하고 링크하는 역할.
- Runtime Data Areas (Memory): 프로그램 실행 시 데이터를 저장하는 영역 (Heap, Stack, Method Area 등).
- Execution Engine: 로드된 바이트코드를 실제로 실행 (Interpreter, JIT Compiler 방식 사용).
- 자동 메모리 관리
- JVM이 더 이상 사용되지 않는 객체를 추적하여 자동으로 삭제함으로써 메모리 누수(Memory Leak) 를 방지합니다.
JAVA-042 : 그럼, 자바 말고 다른 언어는 JVM 위에 올릴 수 없나요?
jvm은 특정 언어에 종속된 것이 아니라 jvm 스펙을 따르는 바이트코드를 실행하는 인터페이스이기 때문에 가능합니다.
jvm 위에서 도는 언어들을 예를 들자면 코틀린, 스칼라, 그루비 등이 있습니다.
- 여기서 나는 GraalVM 2을 언급하려고 했는데 인공지능이 JVM 보다는 차세대 유니버설 VM으로 보는 시각이 있었다.
- 위 내용을 기반으로 oracle을 뒤져보니까 ahead-of-time Native Image 컴파일 기능을 갖춘 고급 JDK GraalVM로, 기존 HotSpot JVM을 기반으로 하되 추가 기능(Native Image, 다중 언어 지원 등)을 제공 하는 확장된 형태라고한다.
- Oracle 공식 문서에 따르면 GraalVM은 JDK, JIT 컴파일러(Graal compiler), Native Image 등을 포함 3 GraalVM하며, 일반 JDK처럼 사용할 수 있고 java 런처가 Graal을 마지막 계층 컴파일러로 하는 JVM을 실행 GraalVM합니다.
JAVA-043 : 반대로 JVM 계열 언어를 일반적으로 컴파일해서 사용할 순 없나요?
=> 다시 공부함
네, 가능합니다. 크게 두 가지 방법이 있습니다.
첫째, GraalVM의 Native Image 기술을 사용하면 Java 바이트코드를 빌드 시점에 특정 플랫폼용 네이티브 바이너리로 AOT 컴파일할 수 있습니다. 이 경우 일반적인 JVM은 포함되지 않고, 메모리 관리와 스레드 스케줄링 같은 최소한의 런타임 기능만 담은 Substrate VM이 실행 파일에 포함됩니다. 초기 구동 속도가 빠르고 메모리 점유율이 낮아 서버리스나 컨테이너 환경에 적합합니다. 다만 closed-world assumption으로 인해 리플렉션이나 동적 클래스 로딩은 빌드 시점에 미리 설정해야 하는 제약이 있습니다.
둘째, HotSpot 외의 다른 JVM 구현체를 사용할 수 있습니다. Eclipse OpenJ9(IBM 기여), Azul Zing 등이 있는데, 이들은 JVM 스펙을 준수하면서도 GC나 JIT 컴파일러를 다르게 구현하여 메모리 효율성이나 시작 속도를 개선했습니다. 또한 임베디드 환경에서는 Java ME처럼 경량화된 JVM 구현을 사용할 수도 있습니다.
- 여기서 AOT(Ahead-Of-Time) 컴파일 및 GraalVM Native Image 이게 맞는건지 확인이 필요했음, 나는 graalVM이 jvm이 맞다고 생각해서 일반적인 컴파일이 맞을까 고민해 봄... 4 → Native Image는 JVM 없이 실행되며, Substrate VM이라는 최소 런타임이 포함된다는 설명이 공식 문서와 일치.
Native Image is a technology to ahead-of-time compile Java code to a standalone executable, called a native image. This executable includes the application classes, classes from its dependencies, runtime library classes, and statically linked native code from JDK. It does not run on the Java VM, but includes necessary components like memory management, thread scheduling, and so on from a different runtime system, called "Substrate VM"
- Substrate VM의 역할
"Substrate VM is an internal project name for the technology behind GraalVM Native Image." Oracle
"Substrate VM is the name for the runtime components (like the deoptimizer, garbage collector, thread scheduling etc.). The resulting program has faster startup time and lower runtime memory overhead compared to a JVM." Oracle
→ "최소한의 런타임"이라는 표현이 정확. 메모리 관리, 가비지 컬렉터, 스레드 스케줄링 등 필수 기능만 포함. - OpenJ9 등 대체 JVM 구현체
"Eclipse OpenJ9 (originally published as IBM J9) is a high performance, scalable, Java virtual machine (JVM) implementation that is fully compliant with the Java Virtual Machine Specification." Wikipedia
"Eclipse OpenJ9™ is a high performance, enterprise calibre, flexibly licensed, openly governed cross platform Java Virtual Machine (JVM) extending and augmenting the runtime technology components from the Eclipse OMR project." Eclipse
→ OpenJ9가 HotSpot과 다른 JVM 구현체라는 설명이 정확. - Java ME (경량 JVM)
"Java Platform, Micro Edition (Java ME) provides a robust, flexible environment for applications running on embedded and mobile devices in the Internet of Things: micro-controllers, sensors, gateways, mobile phones, personal digital assistants (PDAs), TV set-top boxes, printers and more." Oracle
"At the base of the Java ME architecture is the Java Virtual Machine (JVM), a lightweight version tailored for embedded and mobile devices." phoenixNAP
→ "경량화된 JVM"에 대한 언급이 Java ME 맥락에서 정확.
뭐라는지 모르겠어서 다음에 다시 볼 예정 ㅠ
JAVA-044 : VM을 사용함으로써 얻을 수 있는 장점과 단점에 대해 설명해 주세요.
VM의 핵심은 하드웨어 자원의 추상화와 격리입니다.
장점은 첫째, 하드웨어 독립성입니다. 가상화 계층을 통해 OS나 하드웨어 제약 없이 동일한 실행 환경을 구축할 수 있습니다.
둘째, 자원 효율성입니다. 하나의 물리 서버를 논리적으로 분할하여 여러 독립된 환경으로 사용할 수 있습니다.
단점은 오버헤드에 따른 성능 손실입니다. 하드웨어를 직접 제어하는 것이 아니라 가상화 레이어를 거쳐야 하므로 네이티브 환경보다 속도가 느릴 수 있습니다. 또한, 가상 머신 구동 자체를 위해 소모되는 추가적인 시스템 자원(CPU, 메모리)이 존재한다는 점이 한계입니다.
장점
- 하드웨어 독립성 (추상화): 물리적인 하드웨어 사양이 달라도 VM 내부에서는 동일한 환경을 보장받습니다. (예: "내 컴퓨터에서는 되는데 서버에선 안 돼"라는 상황 방지)
- 그럼 컨테이너와의 차이점은 ?
- VM과 Docker 모두 하드웨어 독립성을 제공하지만, VM은 하드웨어 자체를 가상화하여 무겁고, Docker는 운영체제 수준에서 환경을 격리하여 훨씬 효율적이고 빠릅니다
- VM의 방식 (완전 격리): 아예 가짜 컴퓨터를 통째로 하나 빌려주는 것과 같습니다. 하드웨어부터 OS까지 통째로 가상화하기 때문에, 호스트 OS가 윈도우든 리눅스든 상관없이 그 안에서 돌아가는 Guest OS 환경은 완벽히 동일합니다. 대신 무겁고 느리죠.
- Docker의 방식 5 (환경 격리): OS를 새로 까는 게 아니라, "실행에 필요한 모든 파일(라이브러리, 설정 등)" 을 이미지라는 단위로 꽁꽁 싸매서 어디서든 동일하게 푸는 방식입니다. OS의 핵심(커널)은 빌려 쓰되, 담벼락(격리)만 쳐서 독립성을 유지합니다. 그래서 훨씬 가볍고 빠릅니다.
- 그럼 컨테이너와의 차이점은 ?
- 자원의 효율적 격리 및 배분: 하나의 물리 서버를 여러 개의 VM으로 쪼개서 사용할 수 있어, 서버 한 대에서 여러 개의 독립된 서비스를 안전하게 운영할 수 있습니다.
- 유연성 및 이식성: VM의 상태를 스냅샷(Snapshot)으로 찍어 그대로 복사하거나 다른 물리 장치로 손쉽게 옮길 수 있습니다.
단점
- 성능 저하 (Overhead): 물리 하드웨어와 실행 프로그램 사이에 VM이라는 계층이 하나 더 존재하므로, 명령을 전달하고 번역하는 과정에서 성능 손실이 발생합니다.
- 자원 낭비: 각 VM마다 자체 OS(Guest OS)를 구동해야 하거나, VM 구동을 위한 기본적인 메모리 점유가 필요하므로 실제 물리 자원을 100% 온전하게 서비스에만 쓰지 못합니다.
- 설정 복잡도: 가상화 환경을 관리하기 위한 소프트웨어나 네트워크 설정이 복잡해질 수 있습니다.
JAVA-045 : JVM과 내부에서 실행되고 있는 프로그램은 부모 프로세스 - 자식 프로세스 관계를 갖고 있다고 봐도 무방한가요?
아니요, JVM과 그 내부 프로그램은 부모-자식 프로세스 관계가 아닙니다.
OS 관점에서 JVM 자체가 하나의 단일 프로세스이며, 그 내부에서 실행되는 자바 프로그램은 해당 프로세스 자원을 공유하는 멀티 스레드 방식으로 구동됩니다.
자바 프로그램이 실행될 때 별도의 프로세스가 생성되는 것이 아니라, JVM 프로세스 내에서 main 스레드를 포함한 여러 스레드가 생성되어 바이트코드를 실행하는 구조이기 때문입니다.
- 왜 부모-자식 프로세스가 아닌가요?
OS 입장에서 프로세스란 독립된 메모리 공간을 가진 실행 단위입니다. 부모-자식 관계가 형성되려면, 부모 프로세스가 fork() 같은 시스템 콜을 통해 별도의 메모리 공간을 가진 '새로운 프로세스'를 생성해야 합니다.
- 현상: 우리가 자바 프로그램을 실행하면 OS는
java.exe(또는java)라는 단 하나의 프로세스만 띄웁니다. - 구조: JVM이 곧 프로세스 그 자체이며, 그 내부에서 실행되는 자바 코드(main 메서드 등)는 JVM이라는 프로세스가 할당받은 메모리 안에서 실행되는 일부일 뿐입니다.
- 비유: 부모-자식 관계가 '집주인과 옆집에 사는 자식' 관계라면, JVM과 프로그램은 '집(JVM)과 그 안에서 활동하는 거주자(프로그램)' 관계에 가깝습니다.
- 그럼 어떤 관계라고 불러야 하나요?
JVM 내부에서 실행되는 자바 프로그램의 흐름은 스레드(Thread) 단위로 관리됩니다.
- 공유 메모리: 자바 프로그램 내부에서 생성된 객체들은 JVM 프로세스의 Heap 영역을 공유합니다. 프로세스끼리는 메모리를 엄격히 공유하지 않지만, 스레드끼리는 공유한다는 점이 핵심입니다.
- 제어권: JVM은 실행 엔진(Execution Engine)을 통해 자바 코드를 실행하고, 가비지 컬렉션(GC)이나 스레드 스케줄링을 직접 관리합니다. 즉, JVM은 프로그램을 실행해 주는 '런타임 환경(Runtime Environment)'입니다.
- 예외적인 경우 (주의!)
자바 코드 내에서 ProcessBuilder나 Runtime.getRuntime().exec()를 사용하여 외부 명령어나 다른 자바 프로그램을 실행한다면, 그때는 실제로 OS 수준에서 부모 프로세스(기존 JVM) - 자식 프로세스(새로 뜬 프로그램) 관계가 형성됩니다. 하지만 일반적인 프로그램 실행 자체는 단일 프로세스 모델입니다.
📌 Java 키워드와 객체지향
JAVA-046 : final 키워드를 사용하면, 어떤 이점이 있나요?
final은 값이나 동작이 변경되지 않도록 해서 안정성을 높여주는 키워드라고 알고 있습니다.
final 변수는 값 변경을 막고, final 메서드는 오버라이딩을 막아서 동작이 바뀌지 않게 하며, final 클래스는 상속을 막아 설계 의도를 유지할 수 있습니다.
JAVA-047 : 그렇다면 컴파일 과정에서, final 키워드는 다르게 취급되나요?
final 키워드는 메모리 영역을 바꾸는 역할은 아니고, 컴파일 시점에 재할당이나 상속, 오버라이딩을 금지하는 제약 조건으로 사용됩니다.
다만 static final 상수의 경우에는 컴파일 타임 상수로 처리되어 인라인될 수 있어서 static과 헷갈릴 수 있습니다.
=> 이게 너무 헷갈려서 좀 더 정리해봄
1. 컴파일 과정에서 다르게 취급됨
final 변수는 컴파일러가 아주 좋아합니다.
- 상수 최적화 (Constant Folding): 컴파일러는 final로 선언된 기본 타입 변수의 값이 절대 변하지 않는다는 것을 알고, 코드 내에서 그 변수를 사용하는 부분을 아예 실제 값으로 바꿔버리기도 합니다. (예: final int x = 10; -> 코드의 모든 x를 10으로 미리 치환)
- 인라이닝 (Inlining): final 메서드는 오버라이딩이 안 되므로, 컴파일러나 JVM이 메서드 호출 대신 메서드 본문을 직접 끼워 넣어 성능을 높일 수 있습니다.
2. 다른 구역에 올려지지는 않음 static final의 경우 다른 구역에 올라감
- static 변수: 인스턴스가 아닌 클래스에 속하며, 메모리의 Method Area(또는 Metaspace)라는 별도 영역에 딱 하나만 생성됩니다.
- 일반 final 변수 (Instance final): 객체(인스턴스)가 생성될 때마다 메모리의 Heap 영역에 각각 생성됩니다. 다만 한 번 값이 정해지면 못 바꿀 뿐입니다.
- static final (상수): 클래스 레벨에서 공유되면서 절대 변하지 않는 값으로, Method Area의 'Constant Pool' 등에서 관리됩니다.
JAVA-048 : 인터페이스와 추상 클래스의 차이에 대해 설명해 주세요.
인터페이스는 구현 클래스들이 반드시 지켜야 하는 역할이나 규약을 정의하는 데 목적이 있고, 다중 구현이 가능해서 객체 간 결합도 6를 낮출 수 있습니다.
반면 추상 클래스는 공통된 상태나 구현을 함께 제공하면서 상속을 통해 기능을 확장하는 데 더 적합합니다.
=> 인터페이스와 추상 클래스의 차이를 JVM 동작 관점에서 좀 더 찾아봄
【기본 차이점】
- 상속: 인터페이스는 다중 구현, 추상 클래스는 단일 상속
- 메서드: 인터페이스는 추상 메서드 중심, 추상 클래스는 구현 메서드 포함 가능
- 변수: 인터페이스는 상수만, 추상 클래스는 인스턴스 변수 가능
- 설계 의도: 인터페이스는 "계약(CAN-DO)", 추상 클래스는 "공통 특성(IS-A)"
【JVM 내부 동작】
- 인터페이스의 default 메서드 처리
- Java 8 이전: 인터페이스는 메서드 시그니처만 가짐
- Java 8+: default 메서드로 구현 코드 포함 가능바이트코드 레벨 7
- default 메서드는 인터페이스 클래스 파일에 실제 구현 코드가 저장됨
- invokespecial이 아닌 invokeinterface 명령어로 호출
- 다중 인터페이스에서 같은 default 메서드 충돌 시, 컴파일 타임에 명시적 오버라이딩 강제 (다이아몬드 문제 해결)
- 추상 클래스의 생성자 호출 체인
- 추상 클래스는 직접 인스턴스화 불가능하지만 생성자 존재
① 자식 클래스 생성자 진입
② super() 호출 (명시하지 않아도 컴파일러가 자동 삽입)
③ 부모(추상 클래스) 생성자 실행- 인스턴스 변수 초기화
- 초기화 블록 실행
④ 자식 클래스 생성자 본문 실행이 과정은 JVM의 Method Area에 저장된 클래스 메타데이터를 참조하여 Heap에 객체를 생성합니다.
JAVA-049 : 왜 클래스는 단일 상속만 가능한데, 인터페이스는 2개 이상 구현이 가능할까요?
클래스는 상태와 구현을 함께 가지기 때문에 다중 상속을 허용하면 메서드나 필드 충돌 같은 문제가 발생할 수 있습니다.
특히 다이아몬드 문제8로 인해 어떤 구현을 사용할지 모호해지기 때문에 Java는 클래스의 다중 상속을 제한합니다.반면 인터페이스는 구현이 아닌 규약 중심이어서 상태 충돌 위험이 없고, 구현 클래스가 직접 책임지고 구현을 선택할 수 있기 때문에 여러 개를 구현할 수 있습니다.
클래스 인터페이스 구분 상속 구현 목적 코드 재사용, 공통 특성 계약 정의, 다형성 관계 IS-A (강한 결합) CAN-DO (약한 결합) 상태 인스턴스 변수 보유 상태 없음 구현 구현 메서드 포함 기본적으로 선언만 충돌 가능성 높음 (필드 + 메서드) 낮음 (메서드만, 명시적 해결) JAVA-050 : 리플렉션에 대해 설명해 주세요.
- 리플렉션이란?
- 구체적인 클래스 타입을 몰라도, 컴파일된 바이트 코드를 통해 해당 클래스의 메서드, 필드, 생성자에 접근할 수 있게 해주는 자바 API입니다.
- 보통 자바에서 객체를 만들 때는 Person p = new Person();처럼 이름을 직접 보고 만듭니다. 하지만 리플렉션은 "거울(Reflection)에 비친 모습"을 보고 객체의 정보를 알아내는 기술이라고 이해했심
- 왜 사용하나요? (언제 이런 마법이 필요할까?)
우리가 코드를 짤 때는 클래스 이름을 다 알지만, 프레임워크(Spring, Hibernate) 입장에서는 우리가 어떤 이름으로 클래스를 만들지 미리 알 수 없습니다.
- 동적 객체 생성: 실행 중(Runtime)에 사용자가 만든 클래스 이름을 읽어서 객체를 생성해야 할 때.
- 어노테이션 처리: @Service, @Test 같은 어노테이션이 붙어 있는지 확인하고, 붙어 있다면 특정 로직을 실행할 때.
- 리플렉션이 없다면 어노테이션은 그냥 코드 옆에 달린 주석(Comments)과 다를 바가 없게 된다고 합니다.
- private 접근: 원래는 접근할 수 없는 private 필드나 메서드도 리플렉션을 쓰면 강제로 열어서 값을 바꿀 수 있습니다. (테스트 코드 등에서 사용)
- Spring, JPA, Jackson 등이 리플렉션 기반이라고 합니다
- 리플렉션의 단점 (양날의 검)
- 성능 오버헤드 : 일반 메서드 호출보다 느림 (약 10~50배)
- 안전성 저하 : 컴파일 타임 체크 불가능
- 캡슐화 파괴 : private 필드/메서드 접근 가능
JAVA-051 : 의미만 들어보면 리플렉션은 보안적인 문제가 있을 가능성이 있어보이는데, 실제로 그렇게 생각하시나요? 만약 그렇다면, 어떻게 방지할 수 있을까요?
1. 실제로 보안 문제가 있나요?
네, 있습니다. 리플렉션을 사용하면 setAccessible(true)라는 코드를 통해, 외부에서 절대 접근할 수 없는 private 필드나 메서드를 강제로 열어서 값을 읽거나 수정할 수 있습니다. 이는 객체의 무결성을 해치고 중요한 데이터를 노출시킬 위험이 있습니다.
2. 어떻게 방지할 수 있을까요?
자바는 이를 방어하기 위한 장치를 마련해 두었습니다.
- Security Manager 활용: 자바 실행 시 보안 관리자를 설정하면, 리플렉션을 통해 private 멤버에 접근하려 할 때 SecurityException을 발생시켜 차단할 수 있습니다.
- Module System (Java 9+): 최신 자바 버전에서는 모듈화를 통해 외부에서 리플렉션으로 접근 가능한 패키지를 엄격하게 제한합니다. 허용되지 않은 모듈은 리플렉션으로도 들여다볼 수 없습니다.
JAVA-052 : 리플렉션을 언제 활용할 수 있을까요?
우리가 직접 코드를 짤 때는 거의 안 쓰지만, 우리가 사용하는 '유명한 도구들'은 모두 리플렉션 덕분에 존재합니다.
1. 프레임워크 및 라이브러리 제작 (가장 대표적)
- Spring Framework: @Service, @Controller라고 적어만 둬도 스프링이 알아서 객체를 만들고 관리하죠? 스프링이 실행될 때 리플렉션으로 클래스들을 쫙 훑어서 해당 어노테이션이 붙은 클래스를 찾아내기 때문입니다.
- JUnit (테스트 도구): 우리가 만든 테스트 메서드 위에 @Test를 붙이면, JUnit이 리플렉션으로 그 메서드를 찾아내서 실행해 줍니다.
2. JSON 직렬화/역직렬화 (Jackson, GSON)
자바 객체를 JSON 문자열로 바꾸거나 그 반대로 바꿀 때, 라이브러리는 리플렉션을 사용해 객체의 필드 이름과 값을 읽어옵니다.
3. 동적 프록시 (Dynamic Proxy)
실행 중에 특정 인터페이스를 구현하는 가짜 객체(Proxy)를 만들어 기능을 가로챌 때 사용합니다. (AOP, 로그 기록 등)
JAVA-053 : static class와 static method를 비교해 주세요.
- static method (정적 메서드): 객체를 생성하지 않고도 클래스명.메서드명으로 바로 호출할 수 있는 메서드입니다. (예: Math.abs())
- static class (정적 내부 클래스): 클래스 안에 클래스를 선언할 때 사용합니다. 바깥 클래스의 인스턴스 없이도 독립적으로 생성할 수 있는 내부 클래스를 말합니다.
JAVA-054 : static 을 사용하면 어떤 이점을 얻을 수 있나요? 어떤 제약이 걸릴까요?
이점 (Pros)
- 공유: 모든 객체가 공통적으로 사용하는 값이나 기능을 하나만 만들어 공유할 수 있어 메모리 효율적입니다.
- 편의성: new 키워드 없이 바로 쓸 수 있어 유틸리티 함수(계산, 변환 등)를 만들 때 매우 편합니다.
제약 및 단점 (Cons)
- 메모리 부담: 프로그램 종료 시까지 메모리에 남아 있습니다. 너무 많이 남발하면 GC(Garbage Collector)의 관리 대상에서 제외되어 메모리 부족(OOM)을 유발할 수 있습니다.
- 객체지향 설계 위반: static은 객체의 상태를 가지지 않으므로, 지나친 사용은 절차지향적인 코드를 만듭니다.
- static 내에서는 non-static 접근 불가: static 영역은 미리 올라가기 때문에, 나중에 생성될 일반 인스턴스 변수를 참조할 수 없습니다.
JAVA-055 : 컴파일 과정에서 static 이 어떻게 처리되는지 설명해 주세요.
- 컴파일 단계: 자바 컴파일러(javac)가 소스 코드를 바이트 코드(.class)로 변환합니다. 이때 static 멤버들에 대한 정보가 기록됩니다.
- 클래스 로딩 단계: JVM의 클래스 로더(Class Loader)가 클래스 파일을 읽어 메모리에 올릴 때, static 키워드가 붙은 변수와 메서드는 Method Area(또는 Metaspace)라는 별도의 공유 메모리 영역에 즉시 할당됩니다.
- 초기화: 실제 객체(new)가 생성되기도 전에 메모리에 이미 존재하므로, 프로그램 어디서든 즉시 접근이 가능해지는 것입니다.
- 메모리 배치
【JVM 메모리 구조】 ┌──────────────────────────────────┐ │ Method Area (Metaspace) │ ← static 저장! ├──────────────────────────────────┤ │ Example.class 메타데이터 │ │ static int staticVar = 10 │ ← 모든 인스턴스가 공유 │ static void staticMethod() │ └──────────────────────────────────┘ ┌──────────────────────────────────┐ │ Heap │ ← 인스턴스 저장! ├──────────────────────────────────┤ │ Example 객체 #1 │ │ int instanceVar = 20 │ ├──────────────────────────────────┤ │ Example 객체 #2 │ │ int instanceVar = 30 │ └──────────────────────────────────┘📌 Java 예외 처리
JAVA-056 : Java의 Exception에 대해 설명해 주세요.
Java의 예외는 Throwable을 최상위로 하는 계층 구조로, Error(복구 불가)와 Exception(복구 가능)으로 나뉩니다.
Exception은 다시 - Checked Exception: IOException, SQLException (컴파일 시 처리 강제)
- Unchecked Exception: RuntimeException 계열 (처리 선택)
Checked는 외부 요인(I/O, 네트워크)으로 발생하고 복구 가능하여 명시적 처리를 강제합니다.
Unchecked는 프로그래밍 오류로 발생하고 대부분 복구 불가능하여 코드로 예방해야 합니다.실무에서는 Spring의 @ExceptionHandler로 전역 처리하고, Unchecked를 주로 사용하여 코드를 간결하게 유지합니다.
Throwable │ ┌─────────────┴─────────────┐ │ │ Error Exception │ │ ┌────┴────┐ ┌───────────┴────────────┐ │ │ │ │ OutOfMemory Stack Checked RuntimeException Error Overflow Exception (Unchecked) Error │ │ ┌────┴────┐ ┌────────┴────────┐ │ │ │ │ IOException SQL NullPointer IllegalArgument Exception Exception Exception-> Java의 Exception은 프로그램 실행 중 발생할 수 있는 비정상적인 상황을 객체로 표현한 것입니다.
예외는 Throwable을 상속받으며, 애플리케이션에서 처리 가능한 예외는 Exception 계열입니다.
예를 들어 null 참조 시 NullPointerException이 발생할 수 있습니다.JAVA-057 : 예외처리를 하는 세 방법에 대해 설명해 주세요.
- 예외 복구 (try-catch): 예외가 발생해도 프로그램을 정상 상태로 돌려놓는 방식입니다. => 예외를 직접 찾아서 처리
- 예외 회피 (throws): throw가 아니라 throws 키워드를 사용해 호출한 쪽으로 예외를 던져버리는 방식입니다. => 호출자에게 예외처리 책임 전가
- 자원 자동 반납 (try-with-resources): Java 7부터 도입된 방식으로, Closeable을 구현한 객체를 자동으로 닫아줍니다. ( AutoCloseable 리소스 자동 해제)
예외를 처리하는 방법으로는 try-catch를 통해 직접 처리하는 방법과, throws를 사용해 호출자에게 예외를 위임하는 방법이 있습니다.
또한 try-with-resources를 사용하면 9 자원을 자동으로 해제하면서 예외를 처리할 수 있습니다.JAVA-058 : CheckedException, UncheckedException 의 차이에 대해 설명해 주세요.
Checked Exception은 컴파일 시점에 처리 여부를 강제하는 예외이고, Unchecked Exception은 런타임에 발생하며 처리 여부가 강제되지 않는 예외입니다.
일반적으로 복구 가능한 경우 Checked Exception을 사용합니다.【차이점】
- Checked: IOException 등, 외부 요인, 복구 가능
- Unchecked: NPE 등, 프로그래밍 오류, 코드로 예방
실무에서는 Spring이 대부분 Checked를 Unchecked로 변환하여 (DataAccessException 등) 코드를 간결하게 만듭니다.
DB 예외 등은 대부분 복구 불가능하므로 Unchecked가 적합합니다.JAVA-059 : 예외처리가 성능에 큰 영향을 미치나요? 만약 그렇다면, 어떻게 하면 부하를 줄일 수 있을까요?
- 왜 성능에 영향을 미치나요? 예외가 발생하면 JVM은 Stack Trace를 생성합니다. 이때 현재 실행 중인 스레드의 호출 스택을 전부 캡처하는데, 이 작업이 매우 무겁습니다.
- 어떻게 부하를 줄일까요?
- 흐름 제어용으로 쓰지 마라: 정상적인 비즈니스 로직(예: 반복문 탈출)을 예외로 처리하는 것은 금물입니다.
- 미리 검사하라 (Look-ahead): 예외가 발생할 상황을 if문으로 미리 체크하는 것이 try-catch에서 예외를 던지는 것보다 훨씬 빠릅니다. (예: if (obj != null) 사용)
- Stack Trace 억제: 정말 성능이 중요하다면 fillInStackTrace() 메서드를 오버라이딩하여 스택 트레이스 생성을 막을 수 있습니다.
📌 Java 동시성과 스레드
- 너무 어렵다 !
JAVA-060 : Synchronized 키워드에 대해 설명해 주세요.
모니터 락(Monitor Lock) 을 사용해 임계 영역(Critical Section)에 대한 상호 배제(Mutual Exclusion) 를 보장하는 키워드.
- 한 스레드가 synchronized 블록 진입 시 락 획득 → 다른 스레드는 대기
- 블록 종료 시 자동으로 락 해제 (예외 발생 시에도)
JAVA-061 : Synchronized 키워드가 어디에 붙는지에 따라 의미가 약간씩 변화하는데, 각각 어떤 의미를 갖게 되는지 설명해 주세요.
위치 락 대상 예시 인스턴스 메서드 this
(해당 인스턴스)synchronized void method() static 메서드 Class 객체 static synchronized void method() 블록 (this) this synchronized(this) { } 블록 (객체) 지정한 객체 synchronized(lockObj) { } 블록 (Class) Class 객체 synchronized(MyClass.class) { } // 인스턴스 락 - 같은 인스턴스 내에서만 동기화 synchronized void instanceMethod() { } // 클래스 락 - 모든 인스턴스에서 동기화 static synchronized void staticMethod() { }JAVA-062 : 효율적인 코드 작성 측면에서, Synchronized는 좋은 키워드일까요?
좋지 않은 경우가 많다
단점 설명 Blocking 락 대기 스레드가 BLOCKED 상태로 CPU 낭비 Coarse-grained 메서드 전체 락은 과도한 범위 공정성 無 대기 순서 보장 안됨 (기아 상태 가능) Read/Write 구분 無 읽기끼리도 블로킹 Deadlock 위험 부주의한 사용 시 교착 상태 JAVA-063 : Synchronized 를 대체할 수 있는 자바의 다른 동기화 기법에 대해 설명해 주세요.
1. java.util.concurrent.locks
ReentrantLock lock = new ReentrantLock(true); // 공정성 보장 lock.lock(); try { // critical section } finally { lock.unlock(); }- tryLock(): 논블로킹 시도
- lockInterruptibly(): 인터럽트 가능
2. ReadWriteLock
ReadWriteLock rwLock = new ReentrantReadWriteLock(); rwLock.readLock().lock(); // 읽기는 동시 허용 rwLock.writeLock().lock(); // 쓰기는 배타적3. Atomic 클래스 (CAS 기반, Lock-free)
AtomicInteger counter = new AtomicInteger(0); counter.incrementAndGet(); // 락 없이 원자적 연산4. Concurrent Collections
- ConcurrentHashMap, CopyOnWriteArrayList 등
5. volatile
- 가시성만 보장 (원자성 X), 단순 플래그에 적합
JAVA-064 : Thread Local에 대해 설명해 주세요.
스레드별 독립적인 변수 저장소 - 동기화 없이 스레드 안전성 확보
ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); // 각 스레드가 자신만의 인스턴스 사용 dateFormat.get().format(new Date());- 동작 원리
- 각 Thread 객체 내부에 ThreadLocalMap 보유
- ThreadLocal을 key로, 값을 value로 저장
- 주요 용도
- 비thread-safe 객체 (SimpleDateFormat 등)
- 트랜잭션 컨텍스트 (Spring의 @Transactional)
- 사용자 인증 정보 (SecurityContext)
- ⚠️ 주의사항
// 스레드 풀 사용 시 반드시 제거 threadLocal.remove(); // 메모리 누수 방지- 스레드 재사용 환경(WAS)에서 remove() 필수
- InheritableThreadLocal: 자식 스레드에 값 전파 가능
📌 Java Stream과 함수형 프로그래밍
JAVA-065 : Java Stream에 대해 설명해 주세요.
데이터 컬렉션을 선언적으로 처리하는 API (Java 8+)
List<String> result = users.stream() .filter(u -> u.getAge() > 20) // 중간 연산 .map(User::getName) // 중간 연산 .sorted() // 중간 연산 .collect(Collectors.toList()); // 최종 연산핵심 특징
특징 설명
Lazy Evaluation 최종 연산 호출 전까지 중간 연산 실행 안함 일회용 한 번 소비하면 재사용 불가 원본 불변 원본 컬렉션 수정하지 않음 내부 반복 반복 로직을 내부에서 처리
JAVA-066 : Stream과 for ~ loop의 성능 차이를 비교해 주세요,일반적인 성능 비교
단순 반복: for-loop > Stream (약 1.5~2배 빠름)for-loop이 빠른 이유:
- 오버헤드 없음 (객체 생성, 메서드 호출 X)
- JIT 최적화에 유리 (단순한 바이트코드)
- primitive 타입 직접 사용 (박싱/언박싱 X)
Stream 오버헤드:
- Spliterator, Pipeline 객체 생성
- 람다 → 익명 클래스 인스턴스
- 중간 연산마다 래퍼 객체 가능성
그러나 현실에서는...
// 대부분의 경우 차이 미미 (수 마이크로초) // 병목은 보통 I/O, DB, 네트워크상황 추천
단순 반복, 성능 극한 for-loop 가독성, 복잡한 파이프라인 Stream 대용량 + CPU 바운드 parallelStream() JAVA-067 : Stream은 병렬처리 할 수 있나요?
가능하다 - parallelStream() 또는 .parallel()
list.parallelStream() .filter(...) .map(...) .collect(...);내부 동작
- ForkJoinPool.commonPool() 사용 (기본 스레드 수: CPU 코어 - 1)
- Spliterator로 데이터 분할 → 병렬 처리 → 결과 병합
⚠️ 주의사항
// 잘못된 사용 - 공유 상태 변경 List<Integer> result = new ArrayList<>(); stream.parallel().forEach(result::add); // 💥 race condition // 올바른 사용 List<Integer> result = stream.parallel().collect(Collectors.toList());효과적인 경우 비효율적인 경우 대용량 데이터 소량 데이터 (오버헤드 > 이득) CPU 바운드 작업 I/O 바운드 작업 ArrayList, 배열 LinkedList (분할 비용 높음) stateless 연산 순서 의존적 연산
JAVA-068: 함수형 인터페이스
JAVA-068 : Stream에서 사용할 수 있는 함수형 인터페이스에 대해 설명해 주세요.
단일 추상 메서드(SAM)를 가진 인터페이스 - 람다 표현식의 타겟 타입
주요 인터페이스 (java.util.function)
인터페이스 메서드 설명 Stream 사용처
Predicate<T> boolean test(T) 조건 판단 filter() Function<T,R> R apply(T) 변환 map() Consumer<T> void accept(T) 소비 forEach() Supplier<T> T get() 생성 generate() BiFunction<T,U,R> R apply(T,U) 2인자 변환 reduce() Comparator<T> int compare(T,T) 비교 sorted() UnaryOperator<T> T apply(T) 같은 타입 변환 map() Predicate<String> notEmpty = s -> !s.isEmpty(); Function<String, Integer> length = String::length; Consumer<String> printer = System.out::println;JAVA-069 : 가끔 외부 변수를 사용할 때, final 키워드를 붙여서 사용하는데 왜 그럴까요? 꼭 그래야 할까요?
규칙: effectively final 이어야 함
int count = 0; // effectively final (값 변경 안됨) list.forEach(item -> System.out.println(count)); // ✅ OK int count = 0; count++; // 값 변경됨 list.forEach(item -> System.out.println(count)); // ❌ 컴파일 에러왜 그럴까?
1. 캡처 방식 - 값 복사
int count = 10; // 람다는 count의 '값'을 복사해서 가져감 Runnable r = () -> System.out.println(count); // 원본 count와 람다 내 count는 별개2. 변경 허용 시 문제
- 람다 실행 시점 ≠ 정의 시점 (지연 실행 가능)
- 어떤 값을 사용해야 할지 모호
- 멀티스레드 환경에서 동기화 문제
꼭 final 붙여야 하나?
명시적 final은 선택사항 - effectively final이면 됨
// 둘 다 OK final int a = 1; int b = 2; // 이후 변경 없으면 effectively final list.forEach(item -> System.out.println(a + b));변경이 필요하면?
// 배열이나 객체로 우회 int[] counter = {0}; list.forEach(item -> counter[0]++); // AtomicInteger 사용 AtomicInteger count = new AtomicInteger(0); list.forEach(item -> count.incrementAndGet());- final로 선언되지 않았다.
- 초기화를 진행한 후에 다시 할당하지 않았다.
- 전위(prefix) 또는 후위(postfix)에 증감 또는 감소 연산자가 사용되지 않았다.
📌 Java 가비지 컬렉션
JAVA-070 : Java의 GC에 대해 설명해 주세요.
가비지컬렉션은 사용이 끝난 객체를 메모리에서 해제하는 역할을 합니다
가비지 컬렉터는 언제, 어떻게, 무엇을 컬렉션 할지에 대한 방안이 있습니다
먼저 무엇을 해제할지에 대한 내용을 말씀드리자면, 여러 알고리즘이 있으나 현대에서는 GC루트로부터 해당 객체가 도달가능한지에 대한 도달가능성 분석 알고리즘을 통해서 쓰레기 객체를 판별합니다
어떻게로 보자면 마크-카피, 마크-컴펙트, 마크-스윕의 알고리즘이 있습니다 신세대의 경우 마크-카피, 구세대의 경우 마크-컴펙트를 사용하고 있습니다.
GC가 언제 수행되는지는 JVM과 GC 종류에 따라 다릅니다.JAVA-071 : finalize() 를 수동으로 호출하는 것은 왜 문제가 될 수 있을까요?
finalize()는 GC에 의해 호출 시점이 보장되지 않고, 객체가 다시 참조되면서 부활할 수 있기 때문에 예측 불가능한 동작을 유발합니다.
이로 인해 성능 저하와 애플리케이션 안정성 문제가 발생할 수 있어 현재는 사용이 권장되지 않습니다.JAVA-072 : 어떤 변수의 값이 null이 되었다면, 이 값은 GC가 될 가능성이 있을까요?
변수의 값이 null이 되었다는 것은 해당 객체에 대한 참조가 끊어졌다는 의미입니다. 만약 다른 참조가 없다면, 그 객체는 GC 대상이 될 수 있습니다.
JVM 밑바닥까지 파헤치기 3장을 읽으세요 다들 !
📌 Java 메서드 오버라이딩
JAVA-073 : equals()와 hashcode()에 대해 설명해 주세요.
equals() : 논리적 동등성(Logical Equality) 비교
// Object 기본: 참조 비교 (==) public boolean equals(Object obj) { return (this == obj); }hashCode() : 해시 기반 컬렉션에서 버킷 위치 결정용 정수값 반환
// HashMap 동작 흐름 hashCode() → 버킷 결정 → equals()로 실제 동등성 확인핵심 계약 (Contract)
규칙 설명 equals가 true → hashCode 동일 필수 (위반 시 HashMap 오작동) hashCode 동일 → equals는 true일 수도 아닐 수도 해시 충돌 허용 둘 다 재정의하거나 둘 다 안하거나 하나만 재정의 금지 JAVA-074 : 본인이 hashcode() 를 정의해야 한다면, 어떤 점을 염두에 두고 구현할 것 같으세요?
1. equals에 사용된 필드만 사용
@Override public int hashCode() { return Objects.hash(id, name); // equals에서 비교하는 필드 }2. 일관된 결과 : 객체 상태 불변 시 항상 같은 값 반환
3. 좋은 분포 (성능)
// 나쁜 예: 모든 객체가 같은 버킷 → O(n) public int hashCode() { return 1; } // 좋은 예: 필드별 가중치로 분포 개선 public int hashCode() { int result = 17; result = 31 * result + id; result = 31 * result + (name != null ? name.hashCode() : 0); return result; }4. 31을 쓰는 이유
- 홀수 소수 → 해시 충돌 감소
- 31 * x == (x << 5) - x → JVM 최적화
5. 실무: IDE/Lombok 활용
@EqualsAndHashCode // LombokJAVA-075 : 그렇다면 equals() 를 재정의 해야 할 때, 어떤 점을 염두에 두어야 하는지 설명해 주세요.
5가지 규약 준수
규약 설명 반사성 x.equals(x) == true 대칭성 x.equals(y) == y.equals(x) 추이성 x=y, y=z → x=z 일관성 상태 불변 시 결과 불변 null 비교 x.equals(null) == false 구현 템플릿
@Override public boolean equals(Object o) { // 1. 동일 참조 체크 (성능) if (this == o) return true; // 2. null & 타입 체크 if (o == null || getClass() != o.getClass()) return false; // 3. 형변환 후 필드 비교 Person person = (Person) o; return id == person.id && Objects.equals(name, person.name); }주의사항
instanceof vs getClass()
// instanceof: 상속 관계 허용 (리스코프 치환 원칙 위배 위험) if (!(o instanceof Person)) return false; // getClass(): 정확한 타입만 (권장) if (o == null || getClass() != o.getClass()) return false;대칭성 위반 예시
// Point.equals(ColorPoint) ≠ ColorPoint.equals(Point) // 상속 시 equals 재정의는 신중하게hashCode()도 반드시 함께 재정의
- 해당 velog가 잘 설명되어 있다 [본문으로]
- 그랄 vm이 머 하는 놈인가 하면, 네이티브 이미지를 만들어주는데 실행 파일을 아예 기계어로 미리 컴파일(AOT)하여, JVM의 단점인 느린 초기 구동 속도를 해결해 준다고 한다. [본문으로]
- https://www.graalvm.org/latest/getting-started/ [본문으로]
- 공식문서 참고 https://www.graalvm.org/22.0/reference-manual/native-image/Limitations/ [본문으로]
- 프로세스를 격리한다고 한다. [본문으로]
- 객체 지향 설계에서 모듈(객체)들이 서로 너무 깊이 의존하지 않고 독립성을 유지하도록 설계하여, 코드 변경 시 연쇄적인 영향을 최소화하고 유지보수 및 확장성을 높이는 것을 의미 - 멘헤라 객체를 생성하지 않는다 ! [본문으로]
- - 직접 확인해보기
# 바이트코드 확인 javap -v YourClass.class # 더 상세히 보기 javap -c -p -v YourClass.class- 다이아몬드 문제란?
다중 상속 시 같은 메서드를 가진 두 부모 클래스를 상속받으면, 어떤 부모의 메서드를 사용해야 할지 모호해지는 문제입니다. [본문으로]- 이펙티브 자바 아이템 9. try-finally보다는 try-with-resources를 사용하라 [본문으로]
'취준 이모저모 > 면접 준비 이모저모' 카테고리의 다른 글
| 면접 준비 이모저모 - Spring (0) | 2026.01.14 |
|---|