[Java] 객체와 메모리 사용 방식

파이썬에서는 .py 파일에 def main()하나만 있으면 되는데, 자바에서는 public static void main(String[] args)과 같이 Class를 생성하고 main함수 작성을 위해 앞에 달린 수많은 수식어구들 때문에 자바에 대한 거부감이 컸다. 도대체 자바는 뭐가 저렇게 많이 필요할까 싶었다. 최근 파이썬으로만 풀던 알고리즘 문제를 자바로 풀어보고, Spring Boot로 오랜만에 간단한 웹개발을 하면서 어차피 잘 만든 프로그램은 Java든 Python이든 올바르게 구조화되어있어야 한다고 느꼈다. 그리고 자바의 public static void와 같은 수식어구는 구조화를 잘 하기 위해 도와주는 것이라는 생각이 들었다. 자바로 객체지향, 디자인패턴 등을 공부해야겠다고 느꼈다. 일단, 최근에 정리되지 않았던 객체화에 대한 개념과 public static void와 같은 수식어구를 ‘스프링 입문을 위한 자바 객체지향의 원리와 이해’ 책을 바탕으로 객체지향에 대해 정리해보겠다. 작년에 읽은 책인데, 직관적이고 통찰력있는 설명이 담겨있었다. 이번 편에서는 객체지향을 이해하는데에 기초가 되는 Java의 메모리에 대해서 우선 정리해보겠다.

프로그램이 메모리를 사용하는 방식

하나의 프로그램에서 메모리는 아래처럼 두 부분으로 나뉘어 사용된다.

코드 실행 영역 데이터 저장 영역

거의 모든 프로그래밍 언어의 공통된 메모리 사용 방식이다. 객체지향 프로그램에서는 ‘데이터 저장 영역’을 다시 아래와 같이 스태틱 영역 + 스택 영역 + 힙 영역으로 나누어 사용한다. 책에서는 이 영역을 T메모리라 하고 있으나 공식적인 표현은 아닌것으로 확인되어 그냥 메모리영역이라고 하겠다.

코드 실행 영역 스태틱 영역 - class
스택 영역 - method 힙 영역 - object

스태틱 영역

스태틱 영역에는 개발자가 작성한 모든 클래스와 import된 모든 패키지들이 들어가는 영역이다.

스택 영역

클래스의 method들이 존재하는 영역이다. 클래스에서 중괄호를 만날 때마다 스택영역에 스택프레임이 생긴다. 하나의 스택 프레임 내부에는 또 다른 스택 프레임이 들어갈 수 있다. 예를 들어 a 메서드 내에 if문에 있다면 스택영역에 a 스택 프레임이 생기고, a 스택 프레임 내에 if 스택 프레임이 생긴다. 스택 프레임 내에는 각 스택에서 사용하는 변수에 대한 공간도 있다.

힙영역

힙 영역에는 클래스의 인스턴스가 존재한다. 클래스는 스태틱 영역에 존재하지만, 클래스를 바탕으로 생성된 객체(=클래스의 인스턴스)와 객체의 변수들은 힙 영역 내에 위치한다. 힙 영역 내에 있는 객체를 스태틱 영역에서 참조할 때에는 Call by Reference방식으로 힙 영역에 있는 객체의 주소를 참조하게 된다. 만약 모든 클래스의 인스턴스들이 동일한 값을 가지는 변수가 있다면, 힙영역에서 객체마다 변수를 저장하는 것이 아니라 public static int days = 365;와 같이 Class에 속한 static 변수로 생성해야 한다. 변수 뿐만 아니라 메서드의 경우에도, Class에 속하는 method는 static을 붙여준다. 여기서 static 변수는 익숙한데, static method는 좀 익숙하지 않았다. 책에서 말하는 바에 따르면 정적 메서드는 객체들의 존재 여부와 관계없이 쓸 수 있는 메서드이다. 실무에서는 클래스의 인스턴스를 생성하지 않고 바로 사용할 유틸리티성 메서드 라고 생각하면 된다. 예를들어 Math class에는 static method들이 굉장히 많다.

변수

각 영역 모두에서 변수는 존재할 수 있다. 메모리에 존재하는 위치에 따라 이름이 결정된다.

static 영역 클래스 멤버 변수
stack 영역 지역 변수
heap 영역 객체(인스턴스) 멤버 변수

각 변수들은 각 영역이 살아있는 동안 사용가능하다. 예를 들어, a 메서드 내의 for문 내에 있는 i라는 변수는, 스택영역 > a 스택 프레임 > for 스택 프레임 내에서만 사용 가능한 변수이다. 그리고 for문이 끝나면 스택 프레임이 사라지고 i 변수도 사라지게 된다. 또, 메서드 안에서 다른 메서들을 호출할 때에는 독립적인 스택 프레임이 생성된다. 즉, 메서드들 간에는 parameter와 return값만 주고받을 수 있고, 그 내부의 지역변수들은 기본적으로는 공유할 수 없다.
한편, static영역에 있는 스태틱 변수는 JVM이 종료될 때 까지 계속 살아있다. 그리고, heap 영역에 있는 객체 멤버 변수들은 객체가 GC될 때 같이 사라지게 된다.
만약, 메서드들 간에 공유해야하는 변수라면 클래스에 public static int a;와 같이 정의하여 static영역에 변수를 저장하고 스택 영역이나 힙 영역에서 a라는 변수를 공유해서 사용할 수 있다. 이런 변수를 ‘전역변수’라고 한다. 하지만 되도록 지역변수를 사용하여 객체, 메서드 간에 의존관계를 줄이는 것이 좋다. 전역변수를 사용하는 것은 객체지향에서 권고하지는 않는데, 읽기 전용 상수(예, pi)와 같은 값은 전역변수로 설정해도 괜찮다.

멀티쓰레드/멀티프로세스에서의 메모리

멀티 쓰레드에서는 메모리영역 중 스택영역을 스레드 개수만큼 나누어 사용한다. 즉, thread끼리 독립된 스태틱 영역을 제공한다. 단, 스태틱 영역과 힙영역은 공유해서 사용한다. Spring에서는 이것을 이용해서 쓰레드간 공유가 가능한 변수 등은 static 영역에, 그렇지 않은 것은 각 쓰레드의 stack영역에 두게 된다. 이와는 반대로 멀티 프로세스에서는 각 T메모리 영역 전체가 아예 분리되어 있다.

스태틱 영역
스택 영역 힙 영역
thread1 thread2

(Servelet은 각 요청당 thread를 통해 처리하고, CGI는 각 요청당 process를 통해 처리한다.)
멀티스레드에서는 스택영역만을 분리해서 사용하기 때문에 멀티스레드 프로그램에서 전역변수 사용을 조심해야 한다. A쓰레드에서 전역변수를 사용하고 변경하였다면, B쓰레드에서는 변경된 전역변수를 사용하게 된다. 이 점을 유의해야 한다.

지금까지 프로그램/자바에서 메모리를 사용하는 구조에 대해 살펴보았는데, 이는 객체지향을 이해하는데에 큰 도움이 된다. 다음 편에는 객체지향의 4대 특징에 대해서 정리해보겠다.