요즘 UMC에서 개발자-디자이너 간의 팀 매칭을 위해 포트폴리오를 만들고 있다.
그래서 Tech Stack들을 적고 있는데, 그 중 Java를 상/중/하를 뭘로 적어야 할지 몰라 찾아보았었다.
일반적으로 통용되는 기준인지는 모르겠지만, 우리의 친구 GPT에게 물어본 Java 실력(?)의 기준은 다음과 같았다.
- 상
다양한 자료구조를 활용해 객체지향 설계 기반의 비즈니스 로직을 구현할 수 있습니다. 성능 최적화와 메모리 관리 경험이 있습니다.
- 중
Java를 사용해 Spring Boot 기반 CRUD API를 설계·구현할 수 있습니다. 공식 문서를 참고해 라이브러리를 적용하고, 디버깅 및 에러 대응이 가능합니다.
- 하
Java 기본 문법과 제어문을 이해하고, 간단한 예제 프로젝트나 콘솔 애플리케이션을 작성해본 경험이 있습니다.
기술 면접을 위해 가비지 컬렉션을 공부하면서 그래서 이걸 어디에 쓰라고...? 라는 생각을 했었는데, 프레임워크처럼 자바를 통해서도 최적화가 가능한거였다 🙃
메모리를 관리하면 "상"이 될 수 있다는 있다는 말에 솔깃해져서, 머리 한 구석에서 굴러다니고 있던 가비지 컬렉터에 다시 관심을 가지게 되었다.
가비지 컬렉션의 구조는 해당 글을 통해 학습하는 중이여서, 우선은 가바지 컬렉션에서 왜 메모리가 필요한지에 대해서만 설명해보려고 한다.
가비지 컬렉션(GC)은 자바의 메모리를 관리해 주기 때문에 메모리 관리에 크게 신경쓸 필요가 없다는 장점이 있다.
수동으로 메모리를 관리해야 하는 C언어를 예시로 들어보자.
#include <stdlib.h>
int main() {
// 메모리 할당
int* arr = (int*)malloc(5 * sizeof(int));
if(arr == NULL) {
return 1; // 할당 실패 처리
}
// 메모리 사용
for(int i=0; i<5; i++){
arr[i] = i * 10;
}
// 메모리 해제
free(arr);
return 0;
}
C언어를 사용할 때는 malloc()으로 직접 할당 후 free()로 해제해야 한다.
해제하지 않게 되면 메모리 누수가 발생한다.
반면 자바는 객체를 생성할 때 자동으로 메모리를 할당하고, free()와 같이 명시적으로 메모리를 해제하지 않아도 된다.
이 과정을 GC(Garbage Collecter)가 담당하는 것이다.
public class MemoryDemo {
public static void main(String[] args) {
// 메모리 할당
int[] arr = new int[5];
// 메모리 사용
for(int i=0; i<5; i++){
arr[i] = i * 10;
}
// 명시적 해제 불필요
// 가비지 컬렉터가 자동 처리
}
}
그래서 정리하면,
GC의 장점
GC가 메모리를 알아서 관리한다 ☺️
GC의 장점
GC가 메모리를 알아서 관리한다 🤔
이런 느낌같다. 기계는 사람이 방심하는 순간 문제를 일으키기에 개발자가 미리 이를 대비하고 있어야 하는 것이다..
GC는 참조가 완전히 끊긴 객체만 회수 가능하다.
따라서 공통적으로 참조를 해제하지 않으면 메모리 누수가 발생할 수 있다.
1. static 사용
static으로 객체를 선언하면 루트 참조(root reference)가 되어 서버를 재시작하지 않는 이상 메모리에서 해제되지 않는다. 사용 후에는 clear()나 remove()를 통해 해제해야 한다.
2. 리스터 미해제
이벤트 리스너가 등록된 객체 또한 GC 대상이 되지 않는다. 사용 후에는 removeActionListener() 등을 통해 해제해야 한다.
3. 자원 미반납
File I/O, JDBC Connection, Socket 등 네이티브 리소스(Native Resource) 또한 close() 등을 통해 Connection/Statement/ResultSet을 반납해야 한다.
Native Resource란?
Java 힙(Heap) 바깥에서 운영체제(OS)나 하드웨어 차원으로 확보되는 자원을 의미한다.
이 자원들은 GC가 관리하지 않기 때문에 명시적으로 해제해야 한다.
내가 해야 하는 역할은 객체의 참조를 끊는 것이다. GC가 수거하는 범위 내로 사용이 끝난 객체들을 던져 넣어야 한다 ⛹️⛹️
1. 사용 후 참조 해제
객체의 사용이 끝나면, 참조를 명시적으로 null로 할당하거나 컬렉션에서 제거하여 참조를 끊어줘야 한다.
static 컬렉션, 싱글톤 객체, 이벤트 핸들러(리스너, 콜백)이 대표적인 예시이다.
list.clear();
button.removeActionListener(myListener);
2. try-with-resources
파일, DB 커넥션 등의 외부 자원을 try-with-resources를 활용해 자동으로 반납하게 하자.
try-with-resources는 Java 7부터 추가된 구문으로, AutoCloseable을 구현한 자원을 자동으로 닫아준다.
오래된 예제코드나 대학교 수업 수강 시 try-catch-finally를 볼 수 있는데, 이는 이제 레거시 방식으로 간주되니 사용을 지양하는 것이 좋다.
(이런걸 누가 사용하냐고? 내가 사용했었다 😂 물론 작동은 하지만, 자원 누수 위험과 코드 가독성 측면에서 사용을 지양하는 것이 좋다고 한다.)
3. 약한 참조(Weak Reference) 활용
캐시 등에서 객체를 오래 보관해야 할 때, WeakReference나 WeakHashMap을 사용하면 GC가 필요에 따라 객체를 회수할 수 있다.
4. 객체 생명주기 최소화
객체를 가능한 한 짧은 스코프에서 사용하도록 하자. 객체를 메서드 내부에서만 사용하면 GC가 쉽게 회수할 수 있다고 한다.
scope란?
변수가 유효하게 접근될 수 있는 코드상의 범위
입문 단계에서 사용하는 세가지의 변수 유효 범위는
1. Method Scope
2. Class Scope
3. Block Scope
가 있다.
실제로는 로컬 변수, 파라미터, 멤버, static, 패턴 변수 등으로 구분해야 한다는데, 아직 잘 이해하지 못해 공식 문서 링크를 첨부하겠다.
3. 메모리 누수 탐지 도구 활용
1. 실시간 모니터링: VisualVM의 메모리 그래프로 추세 확인
2. 덤프 분석: Eclipse Memory Analyzer로 누수 경로 시각화
3. GC 로그: -Xlog:gc* 플래그로 GC 활동 패턴 분석
현재 인텔리제이를 사용하고 있어, 먼저 프로파일러를 통해 메모리를 분석해보려 한다.
구체적인 프로파일 입문기는 다음 글을 통해 확인하실 수 있습니다 🙆♀️
목표를 정해서 공부하니 학습하는 게 더 재미있다. 모니터링도 해보고 싶다.
개발하면서 점점 모니터링의 필요성을 느끼고 있는데, api를 개발하다가 무한 로딩을 만나면 삽질이 시작되기 때문이다.
여담
Scope를 찾기 위해 자바의 공식 문서를 찾는데, 한참을 찾아도 나오지 않고, 대신 Oracle이 Java를 설명해주고 있었다.
알고보니 Java는 원래 선마이크로시스템즈(Sun Microsystems)에서 만들었고, 이후 오라클(Oracle)이 인수해서 지금까지 공식적으로 관리해오고 있다고 한다.