자바를 공부하다보면 가비지 컬렉션(GC)은 빼놓을 수 없는 주제입니다.
뭔가 중요해 보이니까 구글링 해보고 GPT에게도 물어보고 집에 있는 기본 서적을 펼쳐보면서 열심히 공부를 하게 되는데요.
결론은 메모리 관리를 자동으로 해주고 Minor GC, Major GC 라는게 있고 여러가지 알고리즘이 있다. 정도로 결론이 납니다. (제가 그랬습니다.)
조금 더 깊게 알아보면 GC 튜닝이라는 것도 있어서 -XMS
, -XMX
, -XX:NewRatio
등의 옵션도 알게 되죠. 공부하면 할 수록 외계어를 읽는 것 같은 착각이 듭니다.
이번 글은 위와 같이 많이 알려진 내용들 말고 제가 개인적으로 느낀 가비지 컬렉션은 돈이다. 라는 내용을 가지고 이야기를 해보려 합니다.
신규 서비스를 런칭하기 위해 기능 구현을 마친 민씨에게 인프라팀 이씨가 와서 물었습니다.
이씨 : 민씨님, 이 서비스 RAM 어떻게 세팅하면 될까요?
개발자 민씨는 이 전 서비스와 규모가 비슷해 보이니 비슷하게 대답을 합니다.
민씨 : 8GB 정도 세팅하면 될 것 같습니다.
시간이 지나고 서비스를 런칭 했더니, 사용자가 너무 많아 8GB로는 역부족하여 서버가 다운되고 말았습니다.
메모리를 부리나케 업그레이드하고 서버를 다시 올렸지만 그 사이에 많은 사용자들이 사라졌습니다.
사장님 : 이거 메모리를 왜 이런 식으로 세팅한 거야?
이씨 : 민씨님이 8GB면 충분하다고 했습니다.
민씨 : ...
시말서 각이 나왔습니다.
실제로는 인프라팀에서 부하 테스트를 해보며 세팅을 적당히 해줄 것으로 예상이 됩니다만 개발자 민씨가 성장해서 10년 차 개발자 쯤 되면 어느 정도 예측을 할 수 있어야 하지 않을까요?
아래에 간단한 스프링 부트 기반의 서버 코드가 있습니다.
Calc
클래스와 HomeController
클래스를 가지고 HTTP GET /
요청을 하면 Calc
클래스의 객체를 생성하여 add()
메서드와 sub()
메서드를 호출하는 아주 간단한 서버입니다.
public class Calc {
public void add(int n1, int n2) {
int v = n1 + n2;
}
public void sub(int n1, int n2) {
int v = n1 - n2;
}
}
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
Calc calc = new Calc();
calc.add(1, 2);
calc.sub(1, 2);
return "home";
}
}
이 서버가 메모리를 얼마나 차지하는 지는 어떻게 알 수 있을까요?
제 생각에는 적당한 메모리를 세팅하고 터질 때 까지 요청을 해볼 수 있을 것 같습니다.
또는 수학적으로 접근하여 int
는 4byte
, 스프링 부트가 시작 될때 A MB
정도를 차지하니 합하면 X MB
식의 접근도 가능할 것 같습니다.
하지만 이런 방법 말고 사용자가 보기 편하도록 모니터링을 할 수 있는 도구가 있는데요.
자바에서 기본적으로 제공하는 모니터링 툴인 jstat
라는 것이 있습니다.
옵션에 대한 자세한 내용은 여기를 참고하세요.
위 서버를 jstat
로 모니터링 해보겠습니다.
jstat -gcutil -h5 [PID] 1000
처음에는 아무런 행동을 취하지 않았으므로 변화가 없습니다.
이번엔 새로고침을 하여 Calc
객체를 생성해보겠습니다.
아까와는 다르게 Eden영역(E)이 0.54%p 증가한 것을 볼 수 있습니다.
저는 GC가 정말로 일어나는지 모니터링 해보고 싶으니 Calc
객체가 1억개 생성될 수 있도록 아래와 같이 코드를 변경했습니다.
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
for (int i = 0; i < 100_000_000; i++) {
Calc calc = new Calc();
calc.add(1, 2);
calc.sub(1, 2);
}
return "home";
}
}
객체를 1억개 생성했더니 Eden영역이 순식간에 차는 것을 볼 수 있고, Minor GC 시간(YGCT)가 대략 0.07초 정도인 것을 확인할 수 있습니다.
주목해야 할 것은 이 GC가 일어난 시간입니다.
스탑 더 월드 Stop-The-World
GC가 수행될 때, 모든 애플리케이션의 쓰레드가 중지되는 현상을 의미하는 말입니다.
위 서비스는 Minor GC가 일어나는 시간이 0.07초입니다.
Major GC는 얼마나 걸릴까요? 자세한 시간은 서비스를 모니터링하면서 알아봐야 하겠지만 대략 0.5초 정도라고 생각해 보겠습니다.
0.07초, 0.5초 사람은 느낄 수도 없는 짧은 시간이지만 실시간을 중요시하는 서비스의 경우 충분히 곤란해질 수 있는 시간입니다.
만약에 0.01초도 중요한 주식, 코인, 외환 트레이딩 등의 서비스가 GC가 일어나서 서비스가 0.5초 동안 멈춘다면 어떻게 될까요?
상상만 해도 수십억의 돈이 사라질 것 같은 느낌이 듭니다.
이건 돈과 직결되는 아주 중요한 문제입니다.
극단적인 이러한 경우 말고도, DB, 여러 가지의 서버와 연결을 한 뒤 헬스 체크를 하고 있었는데 GC의 시간이 헬스 체크 시간보다 길어서 커넥션이 끊기게 된다면?
많은 비용을 소모하여 또다시 커넥션을 맺어야 합니다.
이러한 경우에는 GC 튜닝을 통하여 Old 영역을 더 늘려서 Major GC의 시간을 늘릴 것인지, Old 영역을 줄여 Major GC가 빠르고 많이 일어나게 할 것인지 등의 적절한 선택이 필요할 것입니다.
JVM과 GC를 공부하면서 이걸 대체 왜 알아야 하지? 하는 생각이 많이 들었습니다. "메모리 관리해 주는 애 알아서 어쩌라는 거지.." 싶었으니까요.
하지만 이 글에서 알아본 것처럼 돈과 직결된 문제라고 생각하면 꼭 알아야 할 중요한 내용이라고 생각되지 않나요?
실제로 GC를 튜닝하여 개선하기 까지는 머나먼 이야기겠지만, 모니터링 툴도 사용하고 메모리도 고려하면서 개발을 한다면 더 나은 개발자가 될 수 있을 것 같습니다.
감사합니다.