Java 18

배열 선언, 생성, 인덱스로 여러 값 다루기

여러 변수 대신 배열을 쓴다는 말은 알겠지만, 배열 생성과 인덱스 접근이 실제 평균 계산과 어떻게 이어지는지 헷갈린다.

근거 · 교안 p124-p128

배열 선언, 생성, 인덱스로 여러 값 다루기 대표 이미지

1장: 여러 변수의 불편함, 배열의 필요성

솔라의 손가락이 노트북 화면의 한 문장에서 멈췄다. 화면에는 방금 막 읽은 문장이 또렷했다.

‘배열은 같은 자료형의 여러 값을 하나의 이름으로 저장하는 구조다.’

솔라는 미간을 살짝 찌푸렸다. 문장 자체는 어렵지 않았다. 변수를 여러 개 만드는 대신, 하나로 묶는다는 뜻이겠지. 학생 5명의 성적을 저장해야 한다면 score1, score2, score3, score4, score5 이렇게 다섯 개를 만드는 대신, scores라는 이름 하나로 퉁친다는 이야기. 편리하다는 건 알겠다.

하지만 그게 다였다. 그래서, 어쨌다는 거지?

“언니.”

옆에서 조용히 자기 코드를 다듬던 루나가 고개를 들었다.

“배열이 여러 변수 대신 쓰는 건 알겠어. 그런데 이게 도대체 어떻게 ‘하나의 이름’으로 뭘 한다는 건지 감이 안 와. score1부터 score5까지 쓰나, scores 하나로 묶나, 어차피 값은 다섯 개잖아. 결국 그 다섯 개를 일일이 다뤄야 하는 건 똑같지 않아?”

솔라의 질문에는 ‘이게 정말로 효율적인 게 맞냐’는 의심이 섞여 있었다. 개념은 알겠는데, 그 개념이 실제 문제를 해결하는 데 어떻게 힘을 발휘하는지, 그 연결고리가 보이지 않았다.

루나는 잠시 생각하더니, 솔라의 노트북 쪽으로 몸을 돌렸다.

“그럼, 직접 한번 해보는 게 어때? 아주 간단한 문제로.”

루나는 솔라의 화면에 떠 있는 빈 코드 편집기를 가리켰다.

“학생 다섯 명의 시험 성적이 있다고 치자. 85점, 92점, 78점, 100점, 67점. 이 다섯 명의 평균 성적을 구하는 프로그램을, 일단 우리가 아는 방식만으로 짜보는 거야. 배열은 쓰지 말고.”

“음… 그냥 변수 다섯 개 만들어서 하면 되지?”

솔라는 자신 있게 키보드에 손을 올렸다. 어려운 과제는 아니었다. 그녀는 익숙하게 코드를 타이핑하기 시작했다.

int score1 = 85;
int score2 = 92;
int score3 = 78;
int score4 = 100;
int score5 = 67;

일단 다섯 개의 점수를 각각의 정수형 변수에 담았다. 이제 합계를 구할 차례.

int sum = 0;
sum = score1 + score2 + score3 + score4 + score5;

여기까지는 괜찮았다. 조금 귀찮긴 해도 못 할 정도는 아니었다. 솔라는 마지막으로 평균을 계산하는 코드를 추가했다.

double average = (double) sum / 5;
System.out.println("평균 점수: " + average);

실행 버튼을 누르자, 콘솔 창에 ‘평균 점수: 84.4’라는 결과가 깔끔하게 출력됐다. 솔라는 의기양양하게 루나를 쳐다봤다.

“봐, 됐잖아. 배열 같은 거 없어도 충분히 할 수 있는데?”

“응, 완벽해. 그럼 여기서 학생이 두 명 더 늘어나면 어떡할까? 7명으로.”

루나의 말에 솔라의 손가락이 잠시 머뭇거렸다.

“그야… 변수 두 개 더 만들고, sum 계산하는 곳에 score6score7을 더해주면 되지. 나누는 숫자도 7로 바꾸고.”

“만약 학생이 50명이라면?”

“50명?”

솔라는 순간적으로 자기가 짜야 할 코드를 머릿속에 그려보았다. int score1;부터 int score50;까지 50줄의 변수 선언. 그리고 sum = score1 + score2 + score3 + … 끝도 없이 이어질 덧셈. 어딘가에서 변수 하나라도 빼먹거나 오타라도 나면… 생각만 해도 끔찍했다.

“합계를 구하는 코드 한 줄이 화면을 꽉 채우겠네. 만약 학생이 300명인 학교 전체의 평균을 구해야 한다면?”

“말도 안 돼. 그걸 사람이 어떻게 일일이 쳐.”

솔라가 고개를 절레절레 저었다. 바로 그 순간이었다. ‘여러 변수를 사용하는 방식’이 가진 명백한 한계가 피부로 느껴졌다. 데이터의 개수가 적을 때는 문제가 없었지만, 그 수가 늘어나자 순식간에 비효율적이고 끔찍한 작업으로 변해버렸다.

“이제 불편한 게 뭔지 확실히 보이네.” 루나가 조용히 말했다. “문제는 단순히 변수가 많다는 게 아니야. ‘처리해야 할 데이터의 개수’가 늘어날 때마다, 우리가 ‘직접 수정해야 할 코드’도 똑같이 늘어난다는 거지.”

루나의 말은 솔라가 막연하게 느끼던 불편함의 정체를 정확히 짚어냈다. 변수를 50개 선언하는 것도, 그 50개를 더하는 것도 모두 ‘데이터의 수에 비례하는 반복 작업’이었다.

솔라는 아까 무심코 넘겼던 문장을 다시 떠올렸다.

‘하나의 이름으로 저장하는 구조.’

이제 그 문장이 다르게 보이기 시작했다.

“아… 알겠다. score1부터 score50까지 이름이 다 제각각이니까, 컴퓨터한테 ‘이 점수들 전부 더해!’라고 한 번에 시킬 수가 없는 거구나. 그래서 내가 score1 더하고, score2 더하고… 일일이 이름을 불러줘야만 했던 거고.”

솔라의 목소리에 깨달음이 묻어났다. ‘하나의 이름’으로 묶는다는 것은 단순히 코드를 짧게 줄여주는 편리함의 차원이 아니었다. 그것은 제각각 흩어져 있던 데이터들에게 ‘점수들’이라는 공동의 정체성을 부여하고, 그 그룹 전체를 한 번에 다룰 수 있는 열쇠였다.

“맞아.” 솔라가 스스로 결론을 내렸다. “하나의 이름으로 묶을 수만 있다면, ‘이름이 뭐든 간에 그 안에 있는 거 전부 다 더해’ 같은 명령을 내릴 수 있을지도 몰라. 학생이 5명이든, 50명이든, 300명이든 상관없이 말이야.”

방금 전까지 ‘그래서 어쨌다는 건데?’라며 시큰둥했던 자신이 우스워졌다. 반복 작업의 고통을 직접 겪고 나니, ‘하나의 이름’이라는 개념이 얼마나 강력한 해결책인지 비로소 이해할 수 있었다. 비효율을 식별하자 새로운 구조의 필요성이 명확해진 것이다.

솔라는 흥분된 얼굴로 루나를 보며 말했다.

“좋아. 그럼 이제 진짜로 그 ‘하나의 이름으로 된 구조’라는 걸 어떻게 만드는지 알려줘. 그 안에 값들은 어떻게 넣고, 또 어떻게 꺼내 쓰는 거야?”

2장: 배열 만들기: 선언, 생성 그리고 인덱스

솔라의 흥분 어린 질문이 끝나자, 루나는 말없이 코드 편집기를 바라보았다. 거기에는 방금 전 솔라가 끙끙대며 만들었던 score1부터 score5까지의 변수 선언과 덧셈 코드가 그대로 남아 있었다. 데이터의 수가 늘어날수록 끔찍한 반복 작업이 되리라는 걸 증명해 준, 이제는 그 역할을 다한 코드였다.

루나는 키보드를 끌어당겨 그 코드 전체를 선택하더니, 망설임 없이 Delete 키를 눌러 화면을 깨끗하게 비웠다. 텅 빈 편집기는 마치 새로운 도화지 같았다. 지난 방식의 한계를 확인했으니, 이제 완전히 새로운 그림을 그릴 차례라는 무언의 신호였다. 솔라는 그 빈 화면을 보며 숨을 골랐다. 이제 정말 그 ‘하나의 이름으로 된 구조’를 마주할 시간이었다.

“좋아, 그럼 이제 그 ‘구조’를 만들어 보자.”

루나가 말했다.

“먼저, 우리는 컴퓨터에게 ‘이제부터 정수(int) 하나가 아니라, 정수 여러 개를 담을 수 있는 통을 사용할 거야’라고 알려줘야 해. 이걸 **선언(declaration)**이라고 불러.”

루나는 솔라에게 키보드를 다시 밀어주었다. 솔라는 침을 꿀꺽 삼키고 화면에 직접 입력했다.

int[] scores;

int 옆에 붙은 저 대괄호 []가 바로 ‘이건 하나가 아니라 여러 개를 담는 통이다’라는 약속이야. 변수 이름은 우리가 아까 얘기했던 대로 scores로 했고.”

int의 배열. 오케이, 이건 알겠어. 그냥 변수 선언이랑 비슷한데, 특별한 표시가 붙은 거네.”

솔라는 고개를 끄덕였다. 하지만 이걸로 끝이 아니었다. scores라는 이름표는 만들었지만, 정작 값을 담을 실체는 아직 없었다.

“이름표만 만들었지, 아직 서랍장을 만들진 않은 상태야. 이제 실제로 정수 다섯 개를 보관할 수 있는 공간을 만들어 달라고 요청해야 해. 이걸 **생성(creation)**이라고 해.”

루나가 다음 코드를 불러주었다.

scores = new int[5];

솔라는 코드를 따라 치면서 미간을 찌푸렸다. “new? 새로운 걸 만드니까? 그리고 [5]는… 아, 학생이 다섯 명이니까 칸을 다섯 개 만들어 달라는 뜻이구나.”

“맞아. new int[5]는 ‘정수를 담을 수 있는 칸 5개를 새로 만들어서 그 공간의 주소를 scores에게 연결해 줘’라는 명령이야.”

루나는 잠시 말을 멈추고 솔라가 두 줄의 코드를 이해하도록 기다려주었다.

int[] scores;        // 정수 '여러 개'를 담을 수 있는 변수 scores를 선언
scores = new int[5]; // 정수 5개를 담을 실제 공간을 만들고 scores와 연결

“아하! 그럼 이제 scores라는 이름 하나가 정말로 다섯 개의 칸을 가진 진짜 ‘구조’가 된 거네!”

솔라는 무릎을 탁 쳤다. ‘하나의 이름으로 여러 값을 저장하는 구조’라는 문장이 드디어 코드의 형태로 눈앞에 나타난 순간이었다. scores가 바로 그 ‘하나의 이름’이었고, new int[5]를 통해 만들어진 5개의 빈칸이 ‘여러 값’을 담을 공간이었다. 하지만 솔라의 원래 질문은 아직 끝나지 않았다.

“좋아, 구조는 만들었어. 그럼 이제 이 텅 빈 칸들에 점수를 어떻게 넣어? 85점, 92점… 이 값들을 말이야. 그리고 넣은 값은 또 어떻게 꺼내?”

“거기에 바로 **인덱스(index)**가 쓰여.”

루나는 종이를 한 장 꺼내더니 간단한 그림을 그렸다. 길쭉한 직사각형을 그리고 그 안을 다섯 칸으로 나누었다.

“컴퓨터는 이 칸들을 구분하기 위해 번호를 붙여. 아주 중요한 규칙이 있는데, 번호는 항상 0부터 시작해. 그래서 다섯 칸짜리 배열은 0번, 1번, 2번, 3번, 4번 칸을 갖게 되는 거야.”

루나는 각 칸 아래에 0, 1, 2, 3, 4를 차례로 적어 넣었다.

01234

“첫 번째 학생의 점수를 넣고 싶으면, scores의 0번 칸에 넣으면 돼. 이렇게.”

루나가 코드 예시를 보여주었다.

scores[0] = 85;

솔라는 아! 하는 소리를 내며 코드를 직접 입력하기 시작했다. 아까 외워두었던 점수들을 차례차례 scores 배열의 각 칸에 집어넣었다.

scores[0] = 85;
scores[1] = 92;
scores[2] = 78;
scores[3] = 100;
scores[4] = 67;

코드를 입력하는 솔라의 표정이 점점 밝아졌다. score1, score2처럼 이름이 제각각일 때와는 완전히 다른 느낌이었다. scores라는 통일된 정체성을 가진 데이터 묶음에, 번호표를 붙여 값을 넣는 기분이었다.

“그럼 꺼내는 것도 똑같겠네? 세 번째 학생 점수가 궁금하면 scores[2]라고 부르면 되는 거지?”

솔라는 확인을 위해 직접 출력 코드를 작성했다.

System.out.println(scores[2]);

실행하자 콘솔에 ‘78’이라는 숫자가 찍혔다. 완벽했다. 배열을 선언하고, 필요한 만큼 공간을 생성하고, 인덱스를 이용해 값을 넣고 빼는 전체 과정이 머릿속에서 하나로 연결되었다.

솔라는 자신이 작성한 코드를 쭉 훑어보았다. 불편함을 느꼈던 과거의 코드와 지금의 코드는 근본적으로 달랐다. 이전에는 score1, score2 등 데이터 하나하나가 독립된 정체성을 가졌다면, 이제 scores라는 하나의 집합적 정체성이 생겼다. 그리고 [0], [1] 같은 인덱스는 그 집합 안에서 특정 요소를 골라내는 행위일 뿐이었다.

하지만 만족감도 잠시, 솔라는 새로운 불편함을 발견하고 말았다. 합계를 구하기 위해 코드를 짜려고 보니, 결국 이렇게 해야 했다.

int sum = scores[0] + scores[1] + scores[2] + scores[3] + scores[4];

“잠깐만, 언니.”

솔라가 고개를 갸웃했다.

“이름이 scores로 통일된 건 좋은데… 값을 넣을 때도 scores[0], scores[1]… 일일이 다 썼고, 합계를 구할 때도 결국 scores[0]부터 scores[4]까지 전부 다 써줘야 하네. 이것도 학생이 50명이면 + scores[49]까지 다 써야 하는 거 아냐? 뭔가… 아직 완전히 시원하지 않은데?”

3장: 배열과 반복문: 평균을 손쉽게

솔라의 손가락이 키보드 위에서 멈칫했다. 화면에는 scores라는 하나의 이름으로 다섯 개의 점수를 담은 배열이 선언되어 있었다. 만족감도 잠시, 합계를 구하기 위해 다음 한 줄을 입력하는 순간 그 느낌은 안개처럼 흩어졌다.

int sum = scores[0] + scores[1] + scores[2] + scores[3] + scores[4];

솔라는 자신이 막 작성한 코드를 멍하니 바라보았다. score1, score2… 를 scores[0], scores[1]… 로 바꿨을 뿐, 본질은 그대로였다. 이름표만 통일됐지, 결국 모든 요소를 일일이 호명하는 작업은 조금도 줄어들지 않았다. 데이터가 50개였다면, 이 덧셈 코드는 화면을 가득 채웠을 것이다.

“이건 아니잖아요.” 솔라가 답답한 표정으로 루나를 돌아보았다. “배열을 썼는데도 왜 아직도 이렇게 하나씩 다 더하고 있어야 해요? 분명 더 효율적인 방법이 있을 것 같은데…”

루나는 솔라의 불만 섞인 얼굴을 보고는, 화면에 떠 있는 덧셈 코드를 턱짓으로 가리켰다.

“그 줄을 다시 한번 잘 봐봐. 특히 대괄호 [] 안에 있는 숫자들.”

솔라는 다시 화면으로 눈을 돌렸다.

scores[0] + scores[1] + scores[2] + scores[3] + scores[4]

“숫자? 0, 1, 2, 3, 4… 그냥 순서대로인데요.”

“맞아. 아주 중요한 규칙이 보이지 않아? 0에서 시작해서 4가 될 때까지 1씩 순서대로 증가하는 숫자들. 우리가 해야 할 일은 이 숫자들을 바꿔가면서 sum에 계속 더하는, 완전히 똑같은 작업의 반복이야.”

루나의 말에 솔라의 머릿속에서 무언가 번쩍했다. ‘똑같은 작업의 반복’. ‘1씩 순서대로 증가하는 숫자’. 이 두 가지 키워드는 솔라가 이미 알고 있는 개념을 가리키고 있었다.

“아! 반복문! for 문 말하는 거지?”

“바로 그거야.”

순간, 따로 놀던 두 개의 조각이 하나로 맞춰지는 느낌이 들었다. scores[i]라는 형태로 배열의 요소에 접근할 수 있다는 사실과, for문 안에서 변수 i를 0부터 원하는 숫자까지 1씩 증가시킬 수 있다는 사실. 이 둘을 합치면, 반복되는 덧셈 코드를 마법처럼 자동화할 수 있을 것 같았다.

솔라는 흥분하며 기존의 덧셈 코드를 지우고, 새로운 코드를 구상하기 시작했다.

“일단 합계를 담을 변수 sum을 0으로 만들어두고…“

int sum = 0;

“그 다음에 for문을 쓰는 거야. 0부터 4까지 1씩 커지는 변수 i를 만들어서…“

for (int i = 0; i <= 4; i++) {
    // 여기에 반복할 코드를 넣으면 돼!
}

이제 가장 중요한 마지막 퍼즐 조각이 남았다. 반복문 안에서 무엇을 해야 할까? 솔라는 잠시 고민하다가 무릎을 탁 쳤다.

sum에다가 scoresi번째 값을 계속 더해주면 되겠다!”

솔라는 망설임 없이 코드를 완성했다.

for (int i = 0; i <= 4; i++) {
    sum = sum + scores[i]; // sum += scores[i]; 와 같음
}

코드를 완성하고 나니, 솔라는 저도 모르게 감탄사를 내뱉었다. 단 세 줄의 코드가 아까의 길고 지저분했던 덧셈 한 줄을 완벽하게 대체했다. i가 0일 때 sumscores[0]이 더해지고, 다음 반복에서 i가 1이 되면 sumscores[1]이 더해진다. 이 과정이 i가 4가 될 때까지 자동으로 반복될 것이다.

“한 가지 더 좋은 방법이 있어.”

루나가 솔라의 코드에서 i <= 4 부분을 가리켰다.

“만약 학생 수가 7명으로 늘어나면 저 숫자를 6으로 바꿔야겠지? 50명이면 49로 바꾸고. 이것도 결국 수동 작업이잖아. 배열은 자기가 몇 개의 칸을 가졌는지 스스로 알고 있어. 배열이름.length 라고 물어보면 돼.”

솔라는 루나의 설명을 듣고 for문의 조건식을 바로 수정했다.

for (int i = 0; i < scores.length; i++)

이제 이 코드는 완벽해졌다. 배열의 길이가 5든, 50이든, 300이든 상관없이 항상 정확하게 모든 요소를 순회하며 합계를 구할 것이다. 솔라는 전체 평균 계산 코드를 완성하고 실행 버튼을 눌렀다.

public class ArrayTest {
    public static void main(String[] args) {
        int[] scores = {85, 92, 78, 100, 67}; // 선언과 생성을 한번에
        int sum = 0;

        for (int i = 0; i < scores.length; i++) {
            sum += scores[i];
        }

        double average = (double) sum / scores.length;

        System.out.println("총점: " + sum);
        System.out.println("평균 점수: " + average);
    }
}

콘솔에 ‘총점: 422’, ‘평균 점수: 84.4’가 정확하게 출력되었다.

솔라는 자신이 작성한 코드를 오랫동안 바라보았다. 비로소 모든 것이 명확해졌다. ‘배열은 그저 여러 변수를 묶어둔 편리한 상자일 뿐’이라는 생각은 완전히 틀렸다.

배열의 진정한 힘은 하나의 이름 아래 여러 값을 묶어두는 것에서 그치지 않았다. 그것은 반복문과 결합하여 그 여러 값들을 ‘자동으로’, ‘한 번에’ 처리할 수 있는 구조적 힘을 제공하는 데 있었다. scores라는 이름은 반복문에게 처리할 대상의 ‘집합’을 알려주고, scores.length는 반복문에게 ‘몇 번’을 돌아야 할지 알려주며, scores[i]는 반복문에게 ‘이번 차례에 누구를’ 처리할지 알려주는 완벽한 시스템이었던 것이다.

“이제 알겠어.”

솔라가 조용히, 하지만 확신에 찬 목소리로 말했다.

“데이터가 몇 개든 상관없이 평균을 구할 수 있는 코드를 짤 수 있어. 이게 바로 ‘구조’의 힘이구나.”

솔라는 첫 장에서 마주했던, ‘하나의 이름으로 저장하는 구조’라는 문장의 진짜 의미를 온전히 자신의 것으로 만들었다. 그것은 더 이상 암기해야 할 정의가 아니라, 반복의 고통을 해결해 준 강력한 도구의 이름이었다.