나의 Java 매뉴얼

II. 구태여 시스템 메모리 따로 프로그래밍 따로 공부해야 할까?

by Younggi Seo

2022년 6월을 앞두고 다시 본 이 글은, '메써드'라고 표기하면서까지 3년 전쯤에 처음 자바를 배우고 이것이 뭔지 어렵싸리 정의 내린 필자의 궁색한 글인데, 파이썬의 명령을 통해 정리하면 아래와 같다.


메서드는 아직 메모리 상에 객체로 생성되기 전의 함수인데, 어떠한 객체나 리스트가 이 메서드를 호출해 그 메서드만의 속성을 부여받을 수 있다. 즉, 설계도에서 실제로 기능을 구현할 수 있는 ‘속성’이라고 생각하면 된다. 이를테면 정수 입력을 확인하는 데 필요한 문자열 메서드인 isdigit()을 가지고 메서드를 활용하는 법이다. 이 메서드를 이용하려면 객체에 메서드 호출 메시지를 보내야 한다. 문자열 객체 s에 isdigit() 메서드 호출 메시지를 보내는 형식은 다음과 같다.

s.isdigit()


메시지를 보낼 객체 s 뒤에 점(.)을 찍고 호출할 메서드 isdigit()을 이어 붙인다. isdigit() 메서드는 문자열이 모두 숫자이면 True를 리턴하고, 숫자가 아닌 문자가 하나라도 있으면 False를 리턴하는 함수 메서드이다. 몇 개의 사례를 가지고 실행해 보면 아래와 같다.

>>> '365.0'.isdigit()
False
>>> '-17'.isdigit()
False
>>> '365'.isdigit()
True




메써드는 쉽게 말해 어떠한 절차방식(method)이 포함된 기본단위의 실행 블록이다. 그것을 간단하게 함수 f(x)에 어떠한 값을 투입하면 이 함수 내부의 로직에 의해 산출되는 결괏값을 뱉어내는 자판기로 비유해봤었다. 여기서 의문을 가졌다. 왜, 어떠한 요소(argument)를 투입하여 결괏값(return value)이 산출되는 방식의 구조 단위로 메써드라는 프로그래밍의 기본 틀을 만들었지? 그리고 생각하기 번거롭게 메써드를 '호출'하라고 표현해서 사용자가 보기에는 프로그램의 전체 실행 순서가 앞뒤로 왔다 갔다 하게 하여 머리 속의 로직을 꼬이게 만들었을까? 사실은 호출이 아니라 그 메써드의 주소값을 통해서 메써드가 할당되어 있는 메모리의 위치로 점프하는 것인데도 불구하고 말이다.



'객체지향 방식'이라는 말을 자바 책마다 처음부터 언급되어 있는데 굳이 그럴 필요가 있는 건지 모르겠다. 왜냐하면 그것은 단지 자바라는 언어의 특징에 대한 설명으로 끝내는 책들이 많기 때문이다. 왜 객체지향적으로 만들었는지에 대한 까닭을 친절하게 해설해준 책은 드물어서 감히 프로그래밍 초짜가 그 이유를 내뱉어 보겠다. 시스템 메모리 구조와 동작 방식을 그대로 따라가게 만든 방식이 객체지향 개발에서 필요한 객체 생성과 사용이고 이것이 메모리 운용 상 더욱 효율적이기 때문에 객체 지향적인 언어인 자바가 만들어지지 않았을까이다. 메모리에 할당되는 변수나 메써드를 하나의 객체로 본다면 기존의 메모리 작동원리에 따라서 구현되는 변수와 메써드 호출이 이루어질 수 있도록 프로그래밍 언어를 만들면 효율적이지 않겠는가?



현대사에서 제임스 고슬링이라는 사람을 아인슈타인과 같은 급(혹은 클래스)의 천재로까지 기록하지 않는 까닭으로 내 생각에는 프로그래밍 언어를 개발하는 사람들은 컴퓨터 내부구조의 동작원리에 가장 충실하게 소프트웨어를 만드는 사람들이라서(이것만도 엄청난 능력자들;;) 기존의 사고방식을 뛰어넘어 새로운 이론을 창조해낸 사람들이 아니서라는 추측을 해 본다.



자바가 작동되는 방식이 곧 메모리가 작동하는 방식을 따라간다는 가설 하에서 자바의 메써드와 클래스를 통해 메모리 구조를 설명하겠다. 객체지향 프로그래밍 언어에서 클래스는 흔히들 객체에 대한 설계도 혹은 서술(description)이라고 한다. 역시 두리뭉실한 개념이다. '클래스 = 클래스의 멤버인 변수 + 클래스의 멤버인 메써드' 즉, 클래스는 변수와 메써드로 이루어진 메써드보다 넓은 범위(scope)의 틀이다. 주기억장치 구조에 빗대어 표현하자면 사용자가 물건을 보관하기 위해 메모리에서 힙이라고 불리는 임시 기억공간에 자신이 선언한 데이터 타입 크기만큼의 공간을 할당받기 전의 상태라고 할 수 있다. 즉, 사용자가 물건 맡기고 볼 일을 보고 나올 때 다시 꺼내가도록 하기 위한 구획으로 그 볼 일을 볼 수 있도록 앞서 먼저 계획한 일련의 변수와 프로세싱이 들어있는 하나의 객체이다.



자바에서 모든 프로그램의 시작점은 다른 클래스를 부를 수 있는 핸들링 클래스 내의 주 메써드(main method)이다. 자바에서 프로그램이 실행되는 방법은 두 가지가 있다. 일단, 'new'라는 연산자를 통해 참조 변수에 주소값을 저장(메모리에 '새로운' 위치값을 생성)한다. 이 참조 변수에 저장된 위치값의 클래스를 찾아가서 클래스 내의 변수가 실행 가능한 객체로 저장된 값이나 혹은 찾아간 메써드 내의 리턴 값을 출력명령에 따라 컴파일 이후 콘솔 창에 결괏값이 보이기도 하고 안보이기도 한다. 그리고 특정 목적을 위해 만든 키워드 중 일반 지정자(modifier)인 'static'(main 메써드 형식에 포함)을 통해 누군가에게 호출받지 않더라도 유일하게 실행 가능한 주(main) 메써드 내에서 출력문을 만들 수 있다. 그래서 메인 메써드는 프로그램 내에서 시작점이며 반드시 '하나만', '꼭' 존재해야 한다.



이 두 키워드를 통해 선언되는 객체가 메모리에 할당된다. 메모리에 할당되지 않으면 실행할 수 없다. 이 까닭은 일단 '메인 메써드=스택', '클래스=힙' 구조를 설명하고 다음 편에서 알아보겠다. 자바에서 실행 버튼을 누르면 해당 클래스명의 클래스가 실행한다. 클래스 내의 메인 메써드는 말 그대로 주 메써드이기 때문에 가장 먼저 실행되고 사용자가 코딩을 볼 때도 여기부터 보고 프로그래밍 구조를 파악하는 것이 효율적이다. 다음은 메인 메써드의 첫 번째 실행문이다. 이때 아래와 같이 참조(형) 변수 선언 및 초기화가 이루어진다면,


Class_type reference_variable_name = new Class_name();


메인 메써드 지역 내의 참조 변수(reference_variable)가 '스택'이라고 불리는 가상의 메모리 공간을 생성한다. 메인 메써드 지역 내에서 선언된 변수이므로 '지역 변수'라고 부르고 이 메인 메써드 범위에서 선언된 변수는 가상의 메모리 공간에 차례대로 쌓인다. 마치 건초더미를 차곡차곡 쌓는다는 영어 단어의 의미인 'stack'을 메모리의 한 종류로 분류하였다. 그리고 new라는 키워드를 통해 이 참조 변수에 대입하는 클래스(혹은 초기 생성자 메써드)의 내용들을 힙 메모리로 새로 올린다. 메모리에 다 올리면 reference_variable_name이라는 참조 변수에 대입한 클래스의 주소값이 저장된다. 왜냐하면 메모리에 올린 클래스의 내용물(변수와 메써드)에 접근하기 위해서는 메모리 내의 위치(주소값)가 필요하고 그 위치 정보인 주소값을 참조하라는 참조 변수에 저장하는 것이다. 그래서 만약 한 예로 아래와 같이 실제 명령문이 실행되면,


Object object_var = new Object("argument1", "argument2");


new Object() 부분은 Object 초기 생성자 메써드(해당 클래스명과 동일)의 생성물, 즉 Object 괄호 안에 (실)인자 값들의 타입과 일치하는 변수(파라미터 혹은 매개 변수)가 있는 Object 메써드(생성자, Constructor)에 이 값들이 보내진다. 그리고 만약, 이 값('가인자')들을 인스턴스 변수(자기 자신에 해당, this)에 대입한다면 메인 메모리인 힙(Heap)에 '실인자' 값들을 심을 수(할당 및 생성, Instance) 있다. 그러면 메모리에 심은 인자들을 콘솔 창에 출력 가능한 상태가 된다. 끝으로 메모리 상에 공간 할당을 필요로 하는 이 데이터 메써드의 내용물(변수와 메써드의 값들)이 심어지는 위치의 주소 값이 object_var라는 변수에 저장된다. object_var를 아까 전부터 계속 참조(형) 변수라고 불렀었고 말 그대로 메모리의 주소가 저장되어 주소값을 참조할 수 있는 변수이다. 자바에서 모든 데이터는 데이터 타입이 필요하므로 이 변수의 타입 역시 Object*라는 클래스형(인스턴스화 되는 클래스명과 동일)으로 앞에 적혀있다. 여기까지 메모리 구조에 따른 본래 클래스(구현이 안된 프로토 타입 형태)였던 객체가 실행이 가능한 인스턴스 객체로 바뀌는 과정이다.


* Object는 모든 클래스의 조상(super class)이라서, 이 클래스 하위에는 어떠한 클래스라도 포함할 수 있다.


이 복잡한 메모리 작동 원리가 머릿속으로 너무나 안 그려져서 그저 형식의 순서만을 외우고 코딩하는 코더가 이제 나는 아니다! 시스템 메모리에서 프로그램이 동작하기 위한 가상의 메모리 공간이 생성되고 그 공간은 다시 상위 메모리(Stack)와 하위 메모리(Heap)로 나뉜다. 스택 영역은 메써드의 로직이 동작하기 위한 주소값과 인자(argument)들을 임시로 저장하는 가상공간이며 프로세스 상태를 저장하는 데에도 사용된다. 반면에 힙 영역은 프로그램이 동작할 때 필요한 클래스 범위 내의 내용물(혹은 회원)들이었던 멤버 변수, 멤버 메써드로 불리는 데이터 정보들을 역시 임시로 저장하는 데 사용된다.



좀 더 세부적으로 보면 메써드 인자들의 저장소인 스택은 임시로 저장되기 때문에 메인 메써드에서 실행된 특정 메써드 호출 이후 복귀하기 위한 복귀 주소(Return Address, 메인 메써드에서 실행된 명령문 다음 라인)를 저장, 혹은 메인 메써드에서 실행시키기 위해서 부른 특정 클래스의 주소값을 저장해서 그 클래스에 접근하거나 접근한 클래스의 메써드(힙에 있음)의 심어져 있는 인자(argument)를 전달받기 위해 사용된다. 사용되고 나면 자동으로 삭제되는 특징이 있다.


반면에 힙은 프로그램이 실행될 때까지 알 수 없는 크기인 가변적인 양의 데이터(멤버 변수와 메써드)를 저장하고 프로그램의 프로세스가 사용할 수 있도록 미리 예약되어 있는 메인 메모리의 영역이다. 프로그램들에 의해 할당되었다가 회수되는 작용이 자바의 연산자 키워드인 new를 통해 동적(알아서 필요한 양만큼)으로 되풀이된다. 그리고 일반적으로 프로그램 종료 후, 해당 힙 영역에 생성된 데이터들은 스택에 저장된 참조 변수가 참조하는 경우에만 사용될 수 있고, 참조하는 참조 변수가 없다면(참조 변수 값이 null일 때) 해당 데이터는 필요 없다고 판단하여 자바에서는 쓰레기 수거자(Gabage Collector)에 의해 자동 제거된다.



다시 아까 전의 명령문을 조금 더 확장하여 두 개의 클래스로 나누어서,


1) 객체 생성을 할 수 있는 메인 메써드가 포함된 클래스(Handling Class) 파일의 실행문

public class Handling_Class {

public static void main(String [] args){

//객체 생성을 위한 파라미터의 인자들을 Worker 메써드로 보내기(Object 메써드 호출)

Object object_var = new Object("argument1", "argument2");

//object_var에 저장된 주소값을 확인하여 이 주소값에 위치한 클래스 내의 getInstance_var1 메써드 호출(메써드 리턴값 메모리에 할당.)

object_var.getInstance_var1();

//getInstance_var1 메써드(함수)를 통과하는 인자값의 로직을 거쳐 출력된 리턴값을 firstIn_lastOut이라는 문자열 변수에 저장 (아래도 동일)

String firstIn_lastOut=object_var.getInstance_var1();

String lastIn_firstOut=object_var.getInstance_var2();

//인자값이 저장된 각각의 변수를 출력함.

System.out.println(lastIn_firstOut+" "+firstIn_lastOut)

}

}


2) 객체 생성을 통해 인자들이 거쳐가는 메써드(Worker Method)가 포함된 클래스의 명령문

public class Object {

//멤버(인스턴스) 변수 선언

String instance_var1, instance_var2;


//Worker 메써드(초기 생성자)인 Object(상위 클래스명과 동일) 내, set*을 통해 파라미터 변수(인자값들이 각각 투입) 세팅

public Object(String setStack1, String setStack2){

//Stack 메모리에 임시 저장되어 있는 파라미터 setStack1, 2의 인자들을 instance_var1, 2 변수에 대입하여 객체 생성

instance_var1=setStack1;

instance_var2=setStack2;

}

//역시 Worker 메써드인 Object 내에서 get*을 인스턴스 변수명 앞에 붙여 변수값(투입된 인자값들) 조회

public String getInstance_var1() {

return instance_var1;

}

public String getInstance_var2() {

return instance_var2;

}

}


객체 생성을 위한 new Object 부분이 실행되어 Object 클래스의 객체들(멤버 변수 및 메써드)이 메모리에 할당 및 실행 가능한 상태(Instance)가 된다는 것은 Object 클래스 내의 멤버 변수(인스턴스 변수)와 멤버 메써드(Worker 메써드)가 힙 메모리에 생성된다는 말과 같다. 이렇게 특정 객체 생성에 의해서 힙에 객체가 인스턴스화 되고, 이때 생성된 멤버 변수를 '인스턴스 변수'라고도 부른다. 인스턴스 변수는 메써드 내의 지역 변수와 달리 변수값이 없으면 초기값(0이나 null 혹은 'false')이 자동으로 설정된다. 그리고 메인 메써드 내에서 new Object 괄호 안에 있는 인자들의 순서와 같이 String 타입의 "argument1", "argument2"가 같은 타입의 setStack1과 setStack2에 임시 저장된 후 멤버(인스턴스) 변수 instance_var1, instance_var2에 대입되는 것을 확인할 수 있다. 2)에 있는 클래스의 명칭과 1)의 아래에 있는 객체 생성 시 입력한 클래스형이 일치하며 2)에서 멤버 변수들의 데이터 타입(String)이 객체 생성 시, 호출되는 Worker 메써드인 Object 괄호 안의 인자들의 타입과 개수 그리고 순서(만약 타입이 다르다면)까지 동일하다. 반대로 같은 메써드명에 이것들이 서로 다르면 같은 클래스 내에서 초기 생성자 메써드 이후에 만드는 동일한 명칭의 메써드에서는 오버 로딩(파라미터 인자들 덮어쓰기)이 된다.



여기까지 설명한 것을 정리해보면, 메인 메써드에서 참조 변수를 통해 Worker Method가 포함된 초기 생성자(같은 클래스 타입)를 부를 때, 참조형 변수의 타입에 해당하는 클래스의 위치를 스택의 주소값(사용할 옷장 칸막이 위치)으로 저장하고 클래스의 데이터(변수와 메써드)를 힙의 메모리 공간(옷장의 옷걸이를 걸 수 있는 여분의 구획)으로 생성하고 메인 메써드의 실행이 끝나면서 제거된다. 그리고 힙에 저장된 특정 메써드(옷장 안의 옷걸이)의 인자값(옷)들을 임시 저장하는 메모리의 가상공간으로 스택(옷장상단의 간이박스)에 쌓이며 이후 그 인자값들이 메써드에 투입되어(옷을 옷걸이에 걸음.) 나온 결괏값이 힙(옷걸이를 걸 수 있는 거치대)이라는 동적 메모리 공간에 인스턴스화 되어 실행(옷이 걸린 옷걸이를 옷장 밖으로 냄.) 및 출력(객체가 옷을 입고 나감.) 가능한 상태가 된다. 클래스(프로토 타입의 옷을 잊지 않은 객체) 내, 그리고 메써드 밖에서 정의되는 변수(옷을 입을 객체)들은 메인 메써드 실행 시 메모리에 로딩되며 이 과정을 인스턴스(구현된 객체) 생성이라고 부르며, 이것은 특정 메써드의 호출(옷걸이 옷을 꺼내기 전)에 앞서 이루어진다.



다시 말해, 매써드란 실행을 위해 메인 메모리에 할당하기 전의 어떠한 로직이 구현될 수 있는 코딩 영역이다. 즉, 이 공간은 어떠한 요소가 투입되어야 비로소 생성 가능한 구조체이기 때문에 어떠한 값(투입요소, 인자), 쉽게 말해 "메시지(옷?)를 패싱" 해줘야 데이터(결괏값, 리턴값->외출가능)로 생성되어 메모리에 적재 후 실행 가능한 상태가 된다.


*setParameter_name

setter라고 부르는 메써드 호출 시 기본적으로 생성되는 메써드(생성자)의 인자 값들의 수정 및 저장하는 용도로 set뒤에는 대문자로 파라미터 변수명을 입력해서 명칭으로 메써드 생성 목적을 알 수 있다. 리턴 값이 없으며 파라미터 변수의 타입과 변수명을 해당 메써드명 뒤의 괄호 안에 명시함.



*getParameter_name

getter라고 부르는 메써드 호출 시 setter 메써드의 인자 값들을 조회하는 용도로 get뒤에는 대문자로 파라미터 변수명을 입력한다. 값의 조회가 목적이므로 리턴 값이 필요하며, 대신에 인자값을 새로 세팅할 필요는 없으므로 해당 메써드의 괄호 안의 값은 없다.











매거진의 이전글나의 Java 매뉴얼