Java는 소스 코드를 컴파일하면 바이트코드(.class)로 변환되며, 이 바이트코드는 JVM(Java Virtual Machine) 위에서 실행됩니다. 각 운영체제별로 JVM이 구현되어 있기 때문에, 바이트코드만 있으면 어떤 운영체제에서도 동일한 Java 프로그램을 실행할 수 있어 플랫폼 독립적이라고 합니다.
꼬리질문:
AOT (Ahead-of-Time) 컴파일을 사용하면 가능합니다. GraalVM의 Native Image 도구를 사용하면 Java 애플리케이션을 기계어로 직접 컴파일하여, JVM이 없이도 실행 가능한 자체 실행 파일(native binary)로 만들 수 있습니다.
Java에서 클래스 로딩은 JVM이 클래스를 메모리에 올리는 과정이며, 로딩(Loading), 링크(Linking), 초기화(Initialization)의 3단계로 구성됩니다.
이 모든 과정은 클래스가 처음 참조될 때 지연 로딩(lazy loading) 방식으로 실행됩니다.
지연 로딩(Lazy Loading)은 객체나 클래스, 데이터를 실제로 사용할 때까지 로딩을 지연시키는 기법입니다. 메모리 사용량을 줄이고, 초기 로딩 속도를 높이는 데 도움이 됩니다.
꼬리질문:
Class.forName(String className)을 사용해 클래스 이름(패키지 포함)을 문자열로 전달하거나, ClassLoader.loadClass(String name)를 사용해 클래스를 로딩합니다.
개발 단계에서는 컴파일, 실행 단계에서는 JVM이 해석하거나 JIT 컴파일을 통해 실행합니다.
.java
파일을 javac
컴파일러를 사용해 바이트 코드로 변환꼬리질문:
JIT(Just-In-Time) 컴파일러는 Java 프로그램이 실행되는 동안, 자주 실행되는 바이트코드를 기계어(native code)로 변환해 실행속도를 향상시킵니다.
참조가 계속 남아 있는 객체는 GC가 수거하지 못해 메모리 누수가 발생할 수 있습니다.
이런 상황이 반복되면 Heap 메모리를 점점 차지하여 OOM(OutOfMemoryError)로 이어집니다.
꼬리질문:
WeakReference
를 사용합니다.먼저, Datadog을 사용하여 애플리케이션의 메모리 사용량을 실시간으로 모니터링하고 있습니다. 임계값을 초과하는 순간 슬랙(Slack)으로 알림을 받아 즉시 문제를 인지할 수 있도록 설정해 두었습니다. 이를 통해 메모리 사용량이 급격히 증가하는 문제를 빠르게 감지하고 대응할 수 있었습니다.
메모리 사용량이 비정상적으로 늘어난 경우, JVM GC 로그를 통해 GC가 잘 동작하는지 추적하고, VisualVM과 같은 Heap 분석 도구를 사용하여 메모리 덤프 를 분석했습니다. 이를 통해 누수의 의심 지점을 추적하고, 로그를 추가하여 어떤 객체가 계속해서 메모리를 차지하고 있는지 확인했습니다. 예를 들어, 특정 메서드에서 객체들이 명시적으로 수거되지 않거나, 자원 관리가 잘못된 부분을 발견할 수 있었습니다.
그런 다음, 메모리 누수가 발생한 부분에서 명시적으로 콜렉션을 정리하고, 참조를 끊어주기 위해 null 처리나 clear() 메서드를 활용했습니다. 이로써 메모리 사용량을 효율적으로 관리할 수 있었습니다. 또한, 메모리 누수가 다시 발생하지 않도록 객체의 라이프사이클을 명확히 관리하고, 불필요한 객체를 참조하지 않도록 코드를 최적화하는 작업을 진행했습니다.
추가적으로, JUnit을 이용해 자동화된 테스트를 구현하고, CI/CD 파이프라인에서 메모리 테스트를 자동화해 정기적으로 메모리 사용량을 점검하도록 했습니다. 이를 통해 새로운 기능이 추가될 때마다 메모리 관리가 제대로 이루어지는지 지속적으로 모니터링할 수 있었습니다.
ArrayList
, LinkedList
가 있습니다.HashSet
, TreeSet
(정렬된 순서로 저장) 등이 있습니다.HashMap
, TreeMap
, LinkedHashMap
등이 있습니다.꼬리질문:
HashSet
은 내부적으로 HashMap을 기반으로 동작합니다. 객체의 hashCode()
값으로 해시 버킷을 찾고, 같은 버킷 내에서는 equals()
메서드를 사용해 중복 여부를 판단합니다.
동기화(Synchronization)는 여러 스레드가 동시에 공유 자원에 접근할 때 데이터의 일관성과 무결성을 보장하기 위한 기법입니다. Java에서는 synchronized
키워드를 통해 임계영역(critical section)을 지정할 수 있습니다.
멀티스레드 환경에서 안전하게 처리하기 위해 다음과 같은 방법들을 사용할 수 있습니다:
synchronized
메서드나 블록java.util.concurrent
컬렉션 - ConcurrentHashMap꼬리질문:
ConcurrentHashMap은 전체 맵에 대해 락을 거는 방식 대신, 조각화된 락(Segment Locking) 방식을 사용합니다. 데이터를 여러 세그먼트(segment)로 나누고, 각 세그먼트에 대해 독립적인 락을 적용합니다. 이렇게 함으로써, 하나의 세그먼트에 대한 락을 걸어도 다른 세그먼트에는 영향을 미치지 않게 됩니다. 따라서 여러 스레드가 다양한 세그먼트에 동시에 접근할 수 있어 병목 현상을 줄일 수 있습니다.
여러 스레드가 동시에 put() 메서드를 호출하여 데이터를 병렬로 갱신할 때, ConcurrentHashMap을 사용하여 락 경합을 최소화하면서 동시성 문제를 해결했습니다. HashMap과 동일하게 키는 유일해야 하며, 값은 중복을 허용하지만, 여러 스레드가 동시에 값을 변경해도 일관된 상태를 보장해줍니다.