Java 25
static과 인스턴스 멤버: 객체 vs. 클래스, 누구에게 속할까?
Integer.parseInt나 main처럼 객체 없이 호출되는 것과 Car 객체를 만들어 호출하는 것이 왜 다른지 헷갈린다.
근거 · 교안 p177-p181
1장: 루나: 내 자동차와 네 자동차는 다르지, 솔라?
1. 루나: 내 자동차와 네 자동차는 다르지, 솔라?
솔라의 손가락이 노트북 트랙패드 위에서 멈칫했다. 화면에는 몇 줄 안 되는 간단한 코드가 떠 있었다.
Car myCar = new Car();
myCar.color = "red";
Car yourCar = new Car();
yourCar.color = "blue";
분명 코드는 이해했다. ‘myCar’라는 자동차 객체를 만들고 색을 빨강으로 칠한다. ‘yourCar’라는 또 다른 자동차를 만들어 파랑으로 칠한다. 하지만 솔라의 미간에 잡힌 주름은 풀리지 않았다. 코드를 어떻게 읽는지가 문제가 아니었다. 왜 이렇게 동작해야만 하는지, 그 근본적인 이유를 알 수 없어 답답했다.
“자동차의 색을 정하려면, 꼭 new Car()로 진짜 자동차를 만들어야만 하네….”
솔라는 혼잣말을 중얼거렸다. 얼마 전 봤던 다른 코드가 머릿속을 스쳤다. 숫자로 된 문자열을 진짜 숫자로 바꾸던 Integer.parseInt("123") 같은 코드. 거기서는 new Integer() 같은 걸 본 기억이 없었다.
“어떤 건 객체를 만들어서 쓰고, 어떤 건 그냥 쓰고. 그냥 외워야 하는 건가? ‘인스턴스 멤버와 static 멤버는 소속, 개수, 접근 방식이 다르다’는 설명은 봤는데… 그래서 이게 왜 다르냐고.”
그때, 방에 들어온 언니 루나가 생각에 잠긴 솔라의 책상 위를 가만히 보았다. 루나는 말없이 선반으로 걸어가, 손바닥만 한 크기의 하얀색 장난감 자동차 모형 두 개를 가져와 솔라의 노트북 옆에 나란히 놓았다.
“솔라, 잠깐만.”
루나는 서랍에서 빨간색과 파란색 마커도 꺼내 자동차 모형 옆에 건넸다.
“이 차들, 하나는 빨간색으로, 다른 하나는 파란색으로 칠해줄래?”
솔라는 갑작스러운 부탁에 의아한 표정을 지었지만, 순순히 마커를 집어 들었다. 잠시 바스락거리는 소리와 함께, 하나의 모형은 쨍한 빨간색 옷을, 다른 하나는 시원한 파란색 옷을 입었다.
“다 됐어, 언니. 근데 이건 갑자기 왜?”
루나는 빙그시 웃으며 솔라에게 질문을 돌려주었다.
“고마워. 자, 그럼 이제 ‘그 자동차’의 색깔은 뭐야?”
“응? 어떤 자동차 말이야?”
솔라는 당연하다는 듯이 되물었다.
“이건 빨간색이고, 저건 파란색이지. ‘그 자동차’라고 하면 어떤 걸 말하는지 알 수 없잖아.”
바로 그 순간, 솔라의 눈이 동그래졌다. 너무나 명백한 사실이었다. 루나는 아무 말 없이 솔라의 노트북 화면을 가리켰다.
솔라의 시선이 두 개의 자동차 모형과 화면 속 코드를 오갔다. 빨간 자동차, 파란 자동차. 그리고 myCar, yourCar. 퍼즐 조각이 맞춰지는 느낌이었다.
“아…!”
솔라는 나지막한 탄성을 내뱉었다. 그녀는 빨간색 자동차 모형을 집어 들었다.
“이게 myCar구나. 그리고 new Car()는… 이 하얀 모형을 상자에서 꺼내는 행동 같은 거였어.”
솔라는 이어서 파란색 자동차 모형을 들고 말했다.
“그리고 이건 yourCar. 이것도 똑같은 설계도로 만들었지만, 엄연히 다른 자동차지. 그래서 서로 다른 색을 가질 수 있는 거고.”
솔라는 깨달았다는 듯 목소리를 높였다.
“‘자동차의 색깔’이라는 속성은 ‘자동차 설계도’에 있는 게 아니었어. 설계도로 만들어낸 ‘실제 자동차 하나하나’에 속한 거였네. 내 차의 색깔과 언니 차의 색깔은 당연히 다를 수 있으니까! 그래서 myCar.color라고 어떤 차의 색인지 꼭 짚어서 물어봐야 했던 거야.”
이제 솔라는 myCar.color = "red";라는 코드가 다르게 보였다. 이것은 단순한 문법이 아니었다. 세상에 단 하나뿐인 myCar라는 객체에 ‘빨강’이라는 고유한 상태를 부여하는, 지극히 현실적인 행위였다. ‘객체를 만들어 호출한다’는 규칙은, 그 기능이나 속성이 개별 객체에 단단히 묶여있기 때문에 생겨난 필연적인 결과였다.
책상 위, 빨갛고 파란 자동차 모형이 방금 얻은 깨달음을 증명하듯 선명하게 빛나고 있었다. 명쾌해진 기분도 잠시, 솔라의 머릿속에 풀리지 않은 원래의 질문이 다시 고개를 들었다.
“좋아, 자동차 색깔은 완벽히 이해했어. 각자 다르니까 객체가 꼭 필요하지. 그런데… Integer.parseInt()는? ‘내 숫자’와 ‘네 숫자’가 따로 있는 것도 아닌데, 왜 그건 객체 없이 바로 쓸 수 있었을까? 그 기능은 대체 누구한테 속해 있는 거지?”
2장: 루나: 우리 모두의 약속은 어디에 적어야 할까?
솔라의 시선은 빨갛고 파란 자동차 모형 너머, 허공에 머물러 있었다. myCar와 yourCar가 왜 각자의 색을 가져야 하는지는 알았지만, Integer.parseInt()의 수수께끼는 풀리지 않은 채 머릿속을 맴돌았다. 그 기능은 누구의 소유도 아닌 유령처럼 느껴졌다.
그때 루나가 방 구석에서 작은 화이트보드와 마커를 가져와 솔라의 책상 위에 조용히 내려놓았다. 쌩한 보드 표면과 자동차 모형의 강렬한 색감이 묘한 대비를 이루었다. 루나는 이어서 솔라가 필기하던 개인 노트를 화이트보드 옆에 나란히 두었다. 두 개의 다른 필기 공간이 솔라 앞에 놓였다.
“솔라, ‘내 자동차 색은 빨강’이라는 정보는 어디에 적혀있지?”
루나가 자동차 모형을 가리키며 물었다.
“내 노트에 있겠지. 이건 myCar에 대한 거니까.”
솔라는 자기 노트를 톡톡 치며 대답했다. 지난번 대화로 얻은 명쾌한 깨달음이었다. ‘내 것’에 대한 정보는 내 공간에 기록되는 것이 당연했다.
“좋아. 그럼 새로운 규칙을 하나 만들자. ‘문자열 “123”을 숫자 123으로 바꾼다’는 규칙이야. 이 규칙은 어디에 적어두는 게 좋을까? 네 노트에 적어볼래?”
솔라는 잠시 망설였다. 마커를 들어 자기 노트에 그 규칙을 적는 시늉을 해보았다. 그러다 문득 손을 멈췄다.
“아니… 내 노트에 적으면 나만 볼 수 있잖아. 언니도 이 규칙을 써야 한다면, 내 노트를 맨날 빌려보거나, 아니면 언니 노트에 똑같이 베껴 써야 하네. 둘 다 불편한데.”
솔라의 시선이 자연스럽게 옆에 놓인 화이트보드로 향했다.
“여기에 적어야지. 이 화이트보드에. 그래야 너나 나나, 누구든 필요할 때 와서 보고 똑같은 방식으로 쓸 수 있으니까.”
솔라는 자기도 모르게 답을 찾았다는 듯 화이트보드를 손가락으로 가리켰다. ‘내 것’과 ‘네 것’을 구분하는 정보가 아닌, 우리 ‘모두’가 함께 따라야 할 약속이나 공용 도구는 개인 노트가 아닌 공용 칠판에 있어야 했다.
“바로 그거야.”
루나가 마커 뚜껑을 열어 화이트보드 중앙에 또박또박 적었다.
Integer 클래스의 공용 칠판
* 규칙: parseInt("문자열") -> 숫자
“Integer.parseInt()는 바로 이 공용 칠판에 적힌 규칙 같은 거야. 어떤 특정한 숫자 객체에 속한 기능이 아니야. ‘숫자로 된 글자를 진짜 숫자로 바꾸는 방법’은 ‘내 숫자’든 ‘네 숫자’든 달라져서는 안 되는, 모두에게 동일하게 적용되는 약속이지.”
솔라의 눈이 다시 한번 커졌다. 유령 같았던 함수의 소속이 드디어 밝혀지는 순간이었다.
“아! 그럼 Integer.parseInt()를 호출할 때 new Integer()를 안 했던 건… 굳이 ‘내 숫자’라는 객체를 만들 필요가 없어서 그런 거였구나! 이건 특정 객체의 상태를 다루는 게 아니라, 그냥 Integer라는 클래스, 이 공용 칠판에 딱 붙어있는 하나의 도구였던 거네.”
“맞아. 그렇게 클래스 자체에 고정되어, 객체를 만들지 않고도 쓸 수 있는 멤버를 우리는 static 멤버라고 불러. ‘정적인’, ‘고정된’이라는 뜻 그대로, 클래스라는 공간에 딱 붙박여 있는 거지. 모든 객체가 이 칠판을 함께 공유해서 보거나, 아예 객체가 없어도 클래스 이름만 대면 누구든 쓸 수 있는 공용 기능이야.”
솔라는 고개를 끄덕였다. static이라는 단어가 더 이상 낯선 암호가 아니라 ‘공용 칠판’이라는 구체적인 이미지로 다가왔다.
“그럼 프로그램이 시작될 때 호출되는 main 메서드도…! 프로그램을 실행하는 데 ‘내 객체’ ‘네 객체’가 어디 있어. 시작점은 딱 하나, 모두가 동의한 유일한 문이어야 하니까. 그러니 당연히 static일 수밖에 없구나. 객체를 만들기도 전에 약속된 출발선에서 프로그램을 시작시켜야 하니까!”
이제 Integer.parseInt()나 main 메서드 앞의 static은 마법이 아니었다. 개별 객체에 묶일 필요가 없는, 오히려 묶여서는 안 되는 기능들을 위한 지극히 합리적인 설계였다.
문제가 명확해지자, 솔라의 머릿속에는 자연스럽게 다음 질문이 떠올랐다.
“알겠어. 이제 ‘객체 고유 속성’과 ‘클래스 공유 기능’의 차이는 확실히 알겠어. 그런데… 내가 직접 무언가를 만들 땐 어떡하지? 새로 만들려는 기능이 자동차 색깔처럼 각자 달라야 하는 건지, 아니면 숫자 변환 규칙처럼 모두가 함께 써야 하는 건지… 그 경계가 애매한 경우도 있을 것 같은데. 언제 뭘 선택해야 하는 거지?”
3장: 루나: 객체의 개별성 vs. 클래스의 공유: 언제 무엇을?
솔라는 화면의 커서를 깜빡이며 고민에 잠겼다. 머릿속에서는 자동차 모형과 공용 칠판이 명확한 대비를 이루고 있었지만, 막상 눈앞의 빈 줄에 새로운 코드를 채워 넣으려니 손가락이 움직이지 않았다. 가상의 학생 관리 프로그램을 위한 Student 클래스를 만드는 중이었다.
class Student {
// 이름, 학년... 이건 학생마다 다르니까.
String name;
int grade;
// 그럼... 총 학생 수는?
???
}
‘이름’이나 ‘학년’은 빨간 자동차, 파란 자동차처럼 학생마다 고유한 값이니 당연히 객체에 속해야 했다. 하지만 ‘총 학생 수’는 달랐다. 이건 학생 개개인의 정보라기보다는, ‘학생’이라는 집단 전체에 대한 정보였다. 솔라는 잠시 static 키워드를 떠올렸다. ‘객체 생성 없이 쓸 수 있으니 편하겠지’라는 생각이 스쳤지만, 이내 고개를 저었다. 그건 단순히 편의성 문제로 결정할 일이 아닌 것 같았다.
그때, 방으로 들어온 루나가 노트북 앞에서 끙끙대는 솔라를 발견했다. 루나는 코드 대신, 솔라가 앉은 의자 옆에 놓인 작은 공구 상자와 방 한쪽에 묵직하게 자리 잡은 조립용 작업대를 번갈아 보았다.
“솔라. 우리 저번에 사 둔 DIY 책장, 오늘 조립해 볼까?”
루나가 가리킨 작업대 위에는 아직 포장도 뜯지 않은 책장 부품 상자가 놓여 있었다. 솔라는 잠시 노트북에서 눈을 떼고 기지개를 켰다.
“좋아. 마침 머리도 아팠는데.”
루나는 공구 상자를 열어 망치 두 개를 꺼내 하나를 솔라에게 건넸다.
“자, 이건 네 망치. 이건 내 망치. 이제 각자 판자를 하나씩 맡아서 나사를 박아보자. 작업은 저기 넓은 작업대에서 하고.”
솔라는 망치를 받아 들고 작업대로 향했다. 널찍하고 튼튼한 작업대는 둘이 함께 부품을 펼쳐놓고 작업하기에 충분했다. 솔라는 자기 몫의 판자에 나사를 대고 망치로 통통 두드리기 시작했다. 루나도 옆에서 자신의 망치로 다른 판자를 조립했다.
망치질을 하던 솔라가 문득 루나에게 물었다.
“언니, 혹시 망치 하나 더 있어? 나사 박는 위치를 표시할 연필이 필요한데, 내 공구 상자에 없네.”
“아니, 망치는 이게 전부야. 연필은 저기 작업대 서랍에 있을 걸? 공용으로 쓰는 거니까.”
루나의 말에 솔라의 망치질이 순간 멎었다. 망치. 연필. 작업대. 솔라의 머릿속에서 무언가 번개처럼 연결됐다.
“아…!”
솔라는 들고 있던 망치를 내려다보았다. 이 망치는 분명 ‘내 것’이었다. 내가 사용하고, 내 손에 맞게 길들여진다. 루나의 망치와는 별개의 도구다. 하지만 작업대는 ‘우리 것’이다. 나 혼자 쓰려고 옮기거나 독점할 수 없다. 우리가 함께 쓰는 공용 공간이다. 서랍 속 연필도 마찬가지다.
솔라는 중얼거렸다.
“망치는… 학생의 ‘이름’이나 ‘학년’ 같은 거구나. 학생 객체마다 하나씩 가져야 하는 고유한 속성이야. 내 이름과 언니 이름이 다르듯이.”
솔라의 시선이 널찍한 작업대로 옮겨갔다.
“그리고 이 작업대는… ‘총 학생 수’ 같은 정보인 거고! 학생이 새로 등록되거나 제적될 때마다 이 작업대에서 무언가 바뀌는 거야. 모든 학생 객체가 이 하나의 정보를 공유하고 참조하는 거지. 특정 학생 한 명에게 속한 게 아니라, Student 클래스라는 이 작업 공간 자체에 속한 거였어.”
‘편리해서’가 아니었다. ‘그게 맞아서’였다. 데이터의 성격과 역할 자체가 소속을 결정하고 있었다. 개별 객체가 각자 가져야 할 상태인지, 아니면 클래스 전체가 공유하고 관리해야 할 상태인지에 대한 근본적인 질문이었던 것이다. static은 문법적 편의를 위한 지름길이 아니라, ‘공유 자원’임을 명시하는 이정표였다.
솔라는 다시 자기 자리로 돌아와 망설임 없이 키보드를 두드렸다. 아까는 비어있던 ??? 자리가 명확한 코드로 채워졌다.
class Student {
// 개인 도구(망치)처럼, 학생별로 가지는 속성
String name;
int grade;
// 공용 작업대처럼, 모든 학생이 공유하는 정보
static int studentCount = 0;
// 새 학생이 만들어질 때마다 공용 작업대에 기록한다
public Student(String name, int grade) {
this.name = name;
this.grade = grade;
studentCount++; // 총 학생 수를 1 늘린다
}
}
이제 솔라는 새로운 기능을 마주했을 때 무엇을 물어야 할지 알았다. ‘이건 망치인가, 작업대인가?’ 이 질문 하나로 static과 인스턴스 멤버 사이에서의 오랜 방황을 끝낼 수 있을 것 같았다. 코드는 더 이상 암기해야 할 규칙의 나열이 아니었다. 현실의 대상을 가장 적절한 형태로 모델링하는, 논리적인 설계의 결과물이었다.