Java 20
메서드: 반복을 줄이고 약속을 만드는 코드의 설계
코드를 위에 묶어놓고 아래에서 호출한다는 구조가 왜 필요한지, 반환형과 매개변수가 어떤 역할인지 흐릿하다.
근거 · 교안 p134-p141
1장: 왜 묶어야 하나요? - 반복되는 코드의 비효율성
1장. 왜 묶어야 하나요? - 반복되는 코드의 비효율성
솔라는 모니터에 떠 있는 코드 편집기와 짧은 메모를 번갈아 보며 미간을 찌푸렸다. 메모에는 ‘메서드: 특정 기능을 수행하는 코드 묶음’이라고 적혀 있었다. 간결한 정의였지만, 솔라에게는 와닿지 않았다. 코드는 위에서 아래로 흐르며 순서대로 실행되는 것이 당연해 보였다. 그런데 굳이 코드의 일부를 다른 곳에 묶어두고, 필요할 때마다 다시 불러오는 구조는 왜 필요한 걸까. 오히려 흐름을 끊고 코드를 더 복잡하게 만드는 것 같았다.
“흐음…”
나지막한 신음 소리에 옆에서 조용히 책을 읽던 루나가 고개를 들었다.
“왜 그래? 어디 막혔어?”
“아니, 막혔다기보다… 이해가 안 돼서. 메서드라는 거 말이야. 기능을 묶어놓고 호출한다는데, 그냥 필요한 곳에 코드를 쭉 쓰면 되는 거 아냐? 왜 굳이 일을 두 번 하는 것처럼 구조를 쪼개놓는 거지?”
솔라는 의자를 돌려 루나를 마주 보며 불만을 토로했다. 마치 잘 닦인 길을 가다가 갑자기 나타난 우회로 표지판을 만난 기분이었다.
루나는 솔라의 말을 잠자코 듣더니, 책을 덮고 솔라의 옆으로 다가왔다. 그리고는 모니터를 가리켰다.
“그럼 간단한 거 하나만 만들어 볼래? 사용자 세 명에게 환영 메시지를 출력해 주는 거야. 대신 각 메시지 위아래로 별표 열 개짜리 구분선을 넣어서 예쁘게 꾸며줘.”
“세 명한테? 그건 쉽지.”
솔라는 자신만만하게 키보드를 잡았다. ‘앨리스’, ‘밥’, ‘찰리’에게 환영 인사를 건네는 코드를 짜기 시작했다. 먼저 앨리스를 위한 코드 블록을 작성했다.
System.out.println("**********");
System.out.println("Alice님, 환영합니다!");
System.out.println("**********");
완벽했다. 솔라는 만족스러운 미소를 지으며 이 세 줄짜리 코드 블록을 그대로 복사했다. 그리고 아래에 두 번 더 붙여넣었다. 이제 이름만 바꾸면 끝이었다.
System.out.println("**********");
System.out.println("Alice님, 환영합니다!");
System.out.println("**********");
System.out.println("**********");
System.out.println("Bob님, 환영합니다!");
System.out.println("**********");
System.out.println("**********");
System.out.println("Charlie님, 환영합니다!");
System.out.println("**********");
“다 됐어. 어때?”
솔라가 의기양양하게 말하자, 루나는 잠시 코드를 들여다보더니 말했다.
“음, 멋지네. 그런데 생각해 보니 별표보다 등호(=)가 더 세련돼 보일 것 같아. 구분선을 전부 등호 열 개로 바꿔줄 수 있을까?”
“뭐? 전부 다?”
솔라의 손가락이 순간 멈칫했다. 별거 아닌 수정이었지만, 갑자기 귀찮은 일이 되어버렸다. 솔라는 한숨을 쉬며 첫 번째 **********를 찾아 커서를 옮겼다. 지우고, 등호 열 개를 입력했다. ==========. 다시 아래로 내려가 똑같은 작업을 반복했다. 총 여섯 번의 수정. 세 번째 줄을 수정할 때쯤엔 짜증이 밀려왔다.
“아, 귀찮아. 그냥 복사해서 붙여넣을걸.”
투덜거리며 겨우 여섯 개의 구분선을 모두 수정했다.
System.out.println("==========");
System.out.println("Alice님, 환영합니다!");
System.out.println("==========");
System.out.println("==========");
System.out.println("Bob님, 환영합니다!");
System.out.println("==========");
System.out.println("==========");
System.out.println("Charlie님, 환영합니다!");
System.out.println("==========");
“자, 이제 됐지?”
솔라가 퉁명스럽게 말하자 루나는 빙그레 웃으며 고개를 끄덕였다.
“만약 환영할 사람이 세 명이 아니라 삼백 명이었다면 어땠을까? 그리고 구분선을 다시 물결표(~)로 바꿔달라고 했다면?”
“으악, 상상만 해도 끔찍해!”
솔라는 자기도 모르게 소리쳤다. 삼백 명이면 육백 개의 줄을 일일이 찾아 바꿔야 한다는 뜻이었다. 실수를 한 번이라도 하면 어떤 줄은 등호로, 어떤 줄은 물결표로 뒤죽박죽이 될 게 뻔했다.
그 순간, 솔라의 머릿속에 번개처럼 스치는 생각이 있었다.
“만약에… 이 ‘구분선 출력하는 일’을 한 군데에만 만들어 놓고, 필요할 때마다 ‘그거 해줘!’라고 말할 수 있다면… 그럼 바꿀 때도 그 한 군데만 바꾸면 전부 다 바뀌는 거잖아?”
솔라의 눈이 동그래졌다. 방금 전까지 코드를 묶는 것이 흐름을 방해하고 복잡하게 만든다고 생각했지만, 이제는 정반대의 생각이 들었다. 반복되는 일을 하나로 묶어두지 않는 것이야말로 진짜 비효율적이고 위험한 일이었다.
루나는 솔라의 얼굴에 떠오른 깨달음을 읽었다.
“바로 그거야, 솔라. 방금 네가 말한 ‘한 군데에 만들어 놓고 필요할 때마다 부르는 것’. 그게 바로 메서드가 필요한 이유야. 반복되는 작업에 이름을 붙여주고, 우리는 그 이름만 부르면 되는 거지.”
솔라는 자신의 코드를 다시 바라봤다. 이제 System.out.println("=========="); 이라는 코드는 그냥 ‘출력문’으로 보이지 않았다. ‘수정하기 귀찮은 반복 패턴’으로 보이기 시작했다. 솔라는 처음으로 코드 묶음의 필요성을 피부로 느꼈다.
“알겠어. 왜 묶어야 하는지는 이제 확실히 알겠어. 훨씬 효율적이네. 그런데… 그 ‘약속’은 어떻게 만드는 거야? ‘구분선 출력하기’라는 기능 묶음을 컴퓨터한테 어떻게 알려주지? 언니가 말한 재사용성이나 가독성이 좋아진다는 말도 아직은 막연하게 느껴지고.”
2장: 어떻게 약속하나요? - 메서드 정의의 의미
솔라의 질문에 루나는 말없이 솔라가 작성했던 코드를 화면에 다시 띄웠다. 그리고는 System.out.println("=========="); 이라고 쓰인 줄 중 하나를 복사했다. 다음 순간, 솔라가 깜짝 놀랐다. 루나가 여섯 개의 구분선 출력 코드를 망설임 없이 전부 지워버렸기 때문이다. 환영 메시지 위아래로 휑하니 빈 줄만 남았다. 코드는 순식간에 망가진 것처럼 보였다.
루나는 이어서 코드 편집기 옆에 작은 새 창을 열고, 방금 복사했던 코드 한 줄을 붙여넣었다. 마치 어지러운 방에서 똑같은 물건들을 모아 하나의 상자에 담는 듯한 움직임이었다. 한쪽에는 구멍이 숭숭 뚫린 원래 코드가, 다른 한쪽에는 외따로 떨어진 구분선 코드 한 줄이 놓였다. 이질적인 두 개의 창이 화면을 분할하자, ‘분리’와 ‘묶음’의 개념이 글자가 아닌 그림으로 보이기 시작했다.
“네가 말한 ‘한 군데에 만들어 놓은 것’이 바로 이거야.”
루나가 새 창에 덩그러니 놓인 코드 한 줄을 가리키며 말했다.
“이제 저 비어있는 곳들에서 이 기능을 ‘부르기’만 하면 돼. 그러려면 컴퓨터와 약속을 해야지. ‘구분선 출력하기’라는 일에 대해 우리가 어떻게 부를 거고, 그 일은 어떤 방식으로 작동할지 명확하게 알려줘야 해.”
솔라는 고개를 끄덕였다. ‘약속’. 아까는 막연하게 들렸던 단어가 이제는 구체적인 과제처럼 느껴졌다. 저 외로운 코드 한 줄에 생명을 불어넣는 주문처럼.
“좋아, 약속을 만들어 보자. 제일 먼저, 이 기능의 이름이 필요해. 우리가 이 일을 뭐라고 부르면 좋을까? ‘구분선 출력하기’라는 뜻을 잘 담아서.”
솔라는 잠시 고민했다. “‘구분선 그리기’… drawDividerLine? 아니면 printDivider?”
“printDivider 좋네. 그럼 이제 두 번째 약속. 이 기능이 자기 일을 다 마치고 나서, 우리에게 무언가 결과물을 돌려주는 게 있을까? 예를 들어 계산 기능이라면 계산 결과를, 검색 기능이라면 검색된 데이터를 돌려주겠지. 이 printDivider는 어때?”
솔라는 루나가 띄운 새 창의 코드를 쳐다봤다. System.out.println("==========");. 화면에 등호를 찍고 그걸로 끝이었다. 자신에게 무언가를 되돌려 주지는 않았다.
“아니, 그냥 화면에 출력하고 끝이야. 돌려주는 건 없어.”
“맞아. 그 ‘돌려주는 것 없음’이라는 약속을 void라는 키워드로 표현해.”
루나가 키보드를 가져가 printDivider라는 이름 앞에 void를 적었다.
void printDivider
“마지막 약속. 이 기능을 실행하기 위해 우리가 미리 줘야 하는 재료가 있을까? 예를 들어 ‘더하기’ 기능이라면 숫자 두 개를 줘야 하는 것처럼.”
솔라는 다시 코드를 봤다. 필요한 건 없었다. 그냥 정해진 등호 열 개를 출력하면 그만이었다.
“아니, 필요한 재료는 없어.”
“그럴 땐 괄호 안을 비워두면 돼. ‘이 기능을 실행하는 데 아무것도 필요 없습니다’라는 약속이지.”
루나는 printDivider 뒤에 빈 괄호 ()를 추가하고, 중괄호 {}로 코드의 몸체를 감쌌다. 솔라가 아까부터 보고 있던 외로운 코드 한 줄이 그 안으로 쏙 들어갔다.
void printDivider() {
System.out.println("==========");
}
“이게 바로 ‘구분선 출력’ 기능에 대한 약속, 즉 메서드 정의야.”
솔라는 완성된 코드 블록을 보았다. void, printDivider, (). 뿔뿔이 흩어져 있던 문법 규칙들이 하나의 ‘약속’이라는 의미로 연결되는 순간이었다. 전에는 그저 외워야 할 기호의 나열로 보였던 것들이 이제는 기능의 정체성을 설명하는 설계도처럼 읽혔다.
“그럼… 환영 메시지도 똑같이 할 수 있겠네?”
솔라는 자신감이 붙어 환영 메시지 코드를 메서드로 만들어보기로 했다.
“이름은 printWelcomeMessage로 하고, 이것도 화면에 출력만 하고 끝이니까 돌려주는 건 없으니 void.”
거기까지는 순조로웠다. 하지만 이내 막혔다.
“그런데… 본문에 System.out.println("Alice님, 환영합니다!"); 라고 쓰면, 이 메서드는 앨리스만 환영하잖아. 밥이나 찰리는 어떡하지?”
스스로 질문을 던진 솔라는 곤란한 표정을 지었다. ‘반복’을 피하기 위해 메서드를 만드는데, 또 다른 종류의 반복—앨리스용, 밥용, 찰리용 메서드를 따로 만들어야 할 판이었다.
루나가 조용히 입을 열었다.
“좋은 지적이야. printDivider는 누가 실행하든 늘 똑같은 일을 하면 됐어. 하지만 printWelcomeMessage는 일을 하려면 외부의 도움이 필요해. ‘누구에게’ 인사할지 그 ‘이름’을 알려줘야 하잖아.”
루나는 printWelcomeMessage()의 괄호를 손가락으로 가리켰다.
“이 괄호가 바로 외부에서 필요한 재료를 받는 통로야. 이 통로로 ‘이름’이라는 재료가 들어올 거라고 약속해 주는 거지.”
그 말에 솔라의 머릿속이 환해졌다. 괄호는 그냥 장식이 아니었다. 메서드 안과 밖을 이어주는 문이었다.
“아! 그럼 이 괄호 안에 ‘문자열 이름이 들어올 거야’라고 써주면 되는구나!”
솔라는 직접 코드를 수정했다.
void printWelcomeMessage(String name) {
System.out.println(name + "님, 환영합니다!");
}
이제 System.out.println 안의 "Alice"는 외부에서 들어올 값을 담는 변수 name으로 바뀌었다. (String name) 이라는 선언은 더 이상 복잡한 문법이 아니었다. ‘이 기능을 사용하려면, 반드시 문자열(String) 하나를 주셔야 합니다. 그러면 저는 그 값을 name이라는 이름표를 붙여 받아서 사용하겠습니다’라는 명백한 약속이었다.
솔라는 자신이 만든 두 개의 메서드를 나란히 바라봤다.
void printDivider() {
System.out.println("==========");
}
void printWelcomeMessage(String name) {
System.out.println(name + "님, 환영합니다!");
}
이제야 비로소 재사용성과 가독성이 좋아진다는 말의 의미를 어렴풋이 알 것 같았다. 복잡한 출력 코드는 printDivider와 printWelcomeMessage라는 이해하기 쉬운 이름으로 바뀌었다. 내부가 어떻게 돌아가는지 몰라도 이름만 보면 뭘 하는지 짐작할 수 있었다. 이것이 가독성이었다. 그리고 이 약속들만 있으면 어디서든 같은 기능을 다시 쓸 수 있었다. 이것이 재사용성이었다.
하지만 솔라의 시선은 이내 처음의 망가진 코드로 돌아갔다. 구멍이 숭숭 뚫린 채 남아있는 그곳. 멋진 약속들을 설계했지만, 아직 그 약속을 사용하는 법을 알지 못했다.
“언니, 이제 알겠어. void는 ‘돌려줄 것 없음’이라는 약속, 괄호 안은 ‘필요한 재료’ 목록이라는 약속. 이렇게 기능의 설계도를 만든 거구나. 그런데… 이 설계도, 어떻게 사용해? 저 비어있는 곳에서 어떻게 이 printDivider를 불러?”
3장: 약속을 실행하는 방법 - 메서드 호출
솔라의 모니터 화면은 둘로 나뉘어 있었다. 한쪽에는 printDivider와 printWelcomeMessage라는 두 개의 잘 만들어진 ‘약속’이 놓여 있었다. 다른 한쪽에는 그 약속이 절실히 필요한, 여섯 군데나 구멍이 숭숭 뚫린 원래 코드가 있었다. 마치 잘 조립된 엔진 부품과, 엔진이 빠진 텅 빈 자동차 몸체가 따로 놓여있는 듯한 이질적인 풍경이었다.
솔라는 텅 빈 공간을 채워 넣어야 했다. ‘구분선 출력’ 기능의 설계도는 완성했으니, 이제 그 설계도대로 실제 구분선을 찍어내야 했다. 솔라는 잠시 고민하더니, printDivider 메서드의 몸체, 즉 {} 안에 있는 System.out.println("=========="); 코드를 드래그해서 복사했다. 그리고는 원래 코드의 첫 번째 빈 줄에 붙여넣으려 했다. 어쨌든 실행되어야 할 코드는 저것이니까. 그게 가장 직관적인 방법 같았다.
그때, 솔라의 손을 루나가 가볍게 막아 세웠다.
“잠깐. 그렇게 하면 우리가 애써 약속을 만든 의미가 없어져.”
루나는 솔라가 복사하려던 코드가 아니라, 그 코드 덩어리에 붙여준 ‘이름’을 손가락으로 가리켰다. 바로 printDivider 라는 이름이었다.
“우리가 저 기능 묶음에 이름을 붙여준 이유가 뭘까? ‘이거 해줘’라고 부르기 위해서였잖아. 컴퓨터에게도 똑같이 하면 돼. 그저 그 약속의 이름을 불러주기만 하면 되는 거야.”
솔라는 루나의 손가락 끝에 적힌 printDivider라는 이름을 물끄러미 바라봤다. ‘이름을 부른다.’ 너무 간단해서 오히려 생각지 못했던 방법이었다. 솔라는 복사했던 코드를 지우고, 텅 비어있던 첫 번째 줄에 메서드의 이름을 타이핑했다.
printDivider
그리고 정의할 때와 마찬가지로, 뒤에 괄호 ()를 붙이고 문장의 끝을 알리는 세미콜론 ;을 찍었다.
printDivider();
// ... 앨리스 환영 메시지가 들어갈 자리
// ... 구분선이 들어갈 자리
“이렇게?”
솔라가 반신반의하며 묻자, 루나는 말없이 고개를 끄덕였다. 솔라는 이번엔 앨리스에게 환영 인사를 건네기 위해 printWelcomeMessage라는 이름을 불렀다. 그런데 이번에는 그냥 이름을 부르는 것만으로 부족했다. 이 약속은 ‘누구에게’ 인사할지 알려줘야 하는 약속이었기 때문이다.
솔라는 약속의 정의를 다시 확인했다. (String name). ‘문자열 재료가 필요합니다.’라는 약속. 솔라는 괄호 안에 "Alice"라는 실제 재료를 넣어주었다.
printDivider();
printWelcomeMessage("Alice");
printDivider();
마치 자판기에 동전을 넣고 버튼을 누르자 약속된 음료수가 나오는 것처럼, printWelcomeMessage("Alice")라는 호출은 name이라는 통로로 “Alice”를 밀어 넣어, “Alice님, 환영합니다!”라는 결과물을 화면에 출력해 줄 터였다.
솔라는 순식간에 나머지 코드도 채워나갔다. 더 이상 길고 지저분한 코드를 복사하고 붙여넣을 필요가 없었다. 그저 약속된 이름들을 부르기만 하면 되었다.
// Alice 환영
printDivider();
printWelcomeMessage("Alice");
printDivider();
// Bob 환영
printDivider();
printWelcomeMessage("Bob");
printDivider();
// Charlie 환영
printDivider();
printWelcomeMessage("Charlie");
printDivider();
코드가 완성되자, 솔라는 잠시 숨을 골랐다. 1장에서는 아홉 줄의 코드가 똑같은 패턴으로 세 번이나 반복되어 총 스물일곱 줄에 달했지만, 이제는 아홉 줄의 명확한 ‘호출’문으로 바뀌어 있었다. ‘구분선 출력’, ‘환영 메시지 출력’, ‘구분선 출력’. 코드만 읽어도 무엇을 하는지 한눈에 들어왔다. 이것이 바로 루나가 말했던 ‘가독성’이었다.
“아…”
낮은 탄성이 솔라의 입에서 흘러나왔다. 메서드를 정의하는 것은 기능의 설계도를 그리는 일이었다. 그리고 메서드를 호출하는 것은 그 설계도에 생명을 불어넣어 실제로 동작하게 만드는, 약속의 ‘실행’이었다. 이 두 가지가 합쳐져야 비로소 반복되는 코드의 저주에서 벗어나 재사용 가능한 부품을 손에 넣게 되는 것이었다.
그때, 루나가 장난기 어린 목소리로 물었다. 1장에서 솔라를 경악하게 했던 바로 그 질문이었다.
“그런데 역시 등호보다는 별표가 더 예쁜 것 같아. 다시 별표로 바꿔줄 수 있을까?”
만약 1장의 솔라였다면 키보드를 집어 던졌을지도 모른다. 하지만 지금의 솔라는 달랐다. 그녀는 망설임 없이 printDivider 메서드의 정의가 있는 곳으로 스크롤을 옮겼다. 그리고 단 한 줄만 수정했다.
void printDivider() {
System.out.println("**********"); // "==========" 에서 "**********" 로 수정
}
솔라는 코드를 실행했다. 그러자 마법처럼 여섯 개의 구분선이 모두 별표로 바뀌어 출력되었다. 단 한 곳을 고쳤을 뿐인데, 그 약속을 불렀던 모든 곳에 변경 사항이 반영된 것이다.
솔라는 만족스러운 미소를 지으며 화면을 바라봤다. 이제 코드는 더 이상 골치 아픈 반복 작업 덩어리가 아니었다. 언제든 수정하고 재사용할 수 있는, 잘 정리된 도구 상자처럼 보였다. 솔라는 이제 코드에서 반복되는 패턴을 발견하면, 주저 없이 생각할 것이다. ‘좋아, 이건 약속을 만들어야겠군.’