문서 읽는 데 134분 · day05

Day05: 배열

전체 21강 중 5강 · 자바 기초
난이도 · 입문

안녕하세요! 여러분의 Java 가이드, 홍순구 튜터입니다.

Day 5에 오신 걸 환영합니다! 지난 시간에 우리는 for, while, do-while로 같은 작업을 빠르게 반복하는 법을 배웠어요. 중간에 break로 탈출하고 continue로 한 번만 건너뛰는 흐름 제어까지 익혔죠. 그리고 Step 4에서 enhanced for를 다루며 배열을 살짝 맛봤습니다.

지난 시간 마지막에 제가 세 가지 질문을 던졌던 거 기억하시나요? 팔로워 50명의 이름을 String[] followers = {... 50개 ...} 이렇게 일일이 적어야 할까요? 배열을 중간에 늘리거나 줄일 수 있을까요? 인덱스 범위를 벗어나면 어떻게 될까요?

오늘 이 세 가지를 전부 풀어드릴게요! 배열의 선언부터 순회, 안전한 인덱스 다루기, 그리고 Arrays 유틸리티까지 — 배열이라는 도구를 끝까지 파헤쳐 봅시다.

배열을 이해하는 가장 쉬운 비유는 학교 사물함이에요. 복도에 사물함이 일렬로 쭉 늘어서 있죠? 각 사물함에는 0번, 1번, 2번... 번호가 매겨져 있고, 그 안에 학생 한 명의 짐이 들어가요. 사물함 전체를 부르는 이름은 하나(3학년 1반 사물함)이지만, 그 안의 칸은 번호로 구분합니다.

자바의 배열도 똑같아요. followers라는 이름표 하나 아래에 50개의 칸이 줄지어 있고, 각 칸은 0번부터 49번까지 번호가 매겨져 있어요. 변수 50개를 따로 선언하는 대신, 사물함 하나에 50칸을 묶어두는 거죠.

🎯 학습 목표

  • 1차원 배열을 세 가지 방식으로 선언하고 초기화할 수 있다
  • 배열을 두 가지 방식(기본 for, enhanced for)으로 순회할 수 있다
  • 인덱스 범위를 벗어나면 어떤 일이 일어나는지 알고, if로 안전하게 막을 수 있다
  • Arrays 유틸리티(sort, copyOf, toString)로 배열을 편하게 다룰 수 있다
  • 2차원 배열로 표 형태의 데이터(3×3 피드 그리드)를 표현할 수 있다
  • 가변 인자(varargs)로 0개부터 여러 개까지 유연하게 값을 받을 수 있다

오늘은 7개 Step으로 나눠서 천천히 살펴볼 거예요. Step 1에서는 배열을 만드는 세 가지 방법을 알아보고, Step 2에서는 두 가지 순회 방식을 비교합니다. Step 3에서 인덱스 범위를 벗어났을 때 일어나는 에러를 직접 마주하고, Step 4에서는 Arrays 유틸리티로 배열을 정렬·복사·출력하는 법을 배워요. Step 5에서 2차원 배열로 3×3 피드 그리드를 만들어 보고, Step 6에서 가변 인자(varargs)를 다룬 뒤, Step 7에서 해시태그 필터 + Top 3 정렬 실습으로 마무리합니다.

자, 그럼 출발해 볼까요?


Step 1: "50명을 하나하나 변수로 담을 수 없어" — 배열의 탄생

자, 첫 Step에서는 왜 배열이 필요한가? 부터 풀어볼게요. 변수만 알고 있는 우리가 어떤 벽에 부딪히는지, 그리고 배열이 그 벽을 어떻게 뚫고 지나가는지를 코드로 직접 보면서 익혀봅시다.

여러분이 인스타그램 같은 앱을 만든다고 상상해 보세요. 지금 화면에 떠 있는 사용자의 팔로워 이름을 5명만 보여준다고 하면 이렇게 쓸 수 있겠죠?

String follower1 = "장원영";
String follower2 = "카리나";
String follower3 = "윈터";
String follower4 = "민지";
String follower5 = "하니";

견딜 만합니다. 그런데 팔로워가 50명이 되면요? 변수 이름을 follower1 부터 follower50 까지 일일이 만들어야 합니다. 1000명이 되면? 100만 명이 되면? 손가락이 먼저 항복하겠죠.

게다가 이걸 다 만들어 놓고도 "이 중에서 이름이 '카리나' 인 사람의 위치를 찾아라" 같은 작업을 하려면 50개의 변수를 모두 if 문으로 비교해야 합니다. 코드가 끔찍해져요.

이런 불편함을 해결해주는 도구가 바로 배열(Array) 이에요. 한 줄로 정의하면 이렇습니다.

배열이란? 같은 타입의 데이터를 하나의 이름표 아래 줄지어 보관하는 도구.

이름표 하나 (followers) 만 기억하면, 그 안의 50개 칸은 번호 (0번, 1번, 2번...) 로 꺼내 쓸 수 있어요. 오프닝에서 보여드린 사물함 비유 그대로입니다.

배열을 만드는 기본 문법

자바에서 배열을 선언하는 가장 기본 형태는 이렇게 생겼어요.

타입[] 이름;

뒤에 붙은 대괄호 [] 가 "이건 한 개가 아니라 여러 개를 묶은 거예요" 라는 표시예요. String 한 개를 담는 변수는 String name; 이고, String 을 여러 개 묶어서 담는 배열은 String[] names; 가 되는 거죠.

그리고 실제로 값을 채우는 방법은 세 가지 가 있어요. 각 방식이 어떤 상황에 어울리는지 하나씩 살펴봅시다.

방식 1: 리터럴 — 값을 미리 다 알 때

가장 직관적인 형태부터 보여드릴게요.

String[] followers = {"장원영", "카리나", "윈터", "민지", "하니"};

중괄호 {} 안에 콤마로 값을 쭉 나열하면 끝이에요. "팔로워 5명의 이름을 지금 당장 알고 있다" 같은 상황이라면 이게 제일 편합니다. 이걸 배열 리터럴 (literal) 이라고 불러요. 값을 글자 그대로 적어 넣는다 는 뜻이에요.

배열의 칸은 0번부터 번호가 매겨져요. 그래서 첫 번째 팔로워를 꺼낼 때는 이렇게 씁니다.

System.out.println(followers[0]); // 장원영
System.out.println(followers[1]); // 카리나

followers[0] 처럼 대괄호 안에 번호를 적으면 그 위치의 값이 튀어나와요. 이 번호를 인덱스 (index) — 한국어로는 색인 이라고 합니다.

방식 2: new + 크기만 — 칸만 먼저 만들어두기

값을 아직 모르는데 "일단 5칸짜리 배열을 준비해두자" 싶을 때는 이 방식을 써요.

int[] likes = new int[5];

new int[5]정수 5개를 담을 수 있는 빈 배열을 새로 만들어줘 라는 뜻이에요. new 라는 키워드는 새것을 만들어달라 는 자바의 신호입니다. (자세한 이야기는 Day 8 클래스에서 다시 만나요.)

여기서 비전공자가 가장 많이 놀라는 부분 하나. "빈 배열이라고 했는데 그 안에는 뭐가 들어있을까요?"

자바는 친절하게도 타입에 맞는 자동 초기값 을 채워줘요.

타입 자동 초기값
int, long 같은 정수 0
double, float 같은 실수 0.0
boolean false
String 같은 객체 null (값이 없다는 뜻)

그래서 int[] likes = new int[5]; 직후에 likes[0] 을 출력하면 0 이 나와요. 나중에 값을 채우고 싶으면 이렇게 할당해주면 됩니다.

likes[0] = 42;
likes[4] = 128;

likes[0] = 42;0번 칸에 42를 넣어줘 라는 뜻이에요. 변수에 값을 대입하던 것과 똑같은데, 변수 이름 대신 배열 이름 + 인덱스 로 위치를 지정해준다는 점만 다른 거예요.

방식 3: new + 값 — 둘을 합친 명시적 형태

마지막은 방식 1과 방식 2를 합쳐놓은 모습이에요.

boolean[] isFollowing = new boolean[]{true, false, true, true};

new boolean[] 으로 불리언 배열을 새로 만든다 는 걸 명시하고, 뒤에 {...} 로 값을 채워요. 방식 1과 결과는 똑같습니다. 다만 new 를 명시적으로 적어주는 형태라서, 메서드에 배열을 즉석으로 만들어 넘기거나 할 때 자주 쓰여요. (메서드는 다음 시간에 본격적으로 다뤄요.)

세 방식을 한 코드로 실행해보기

자, 이제 세 방식을 모두 모아놓은 코드를 직접 실행해봅시다. IntelliJ 에서 day05 디렉토리에 ArrayDeclarationBasics.java 파일을 만들고 아래 코드를 그대로 입력해보세요.

// day05/ArrayDeclarationBasics.java
void main() {
    // 배열 선언/초기화 3가지 방식
    System.out.println("=== 인스타그램 배열 3가지 만들기 ===");
    System.out.println();

    // 방식 1: 리터럴 (값을 알고 있을 때 가장 편함)
    String[] followers = {"장원영", "카리나", "윈터", "민지", "하니"};
    System.out.println("[방식 1] 리터럴 — 팔로워 목록");
    System.out.println("첫 팔로워: " + followers[0]);
    System.out.println("마지막 팔로워: " + followers[followers.length - 1]);
    System.out.println("총 인원: " + followers.length + "명");
    System.out.println();

    // 방식 2: new + 크기만 (값은 나중에 채움, 정수는 0으로 자동 초기화)
    int[] likes = new int[5];
    System.out.println("[방식 2] new + 크기 — 좋아요 카운터 5개");
    System.out.println("처음 값 (자동 0 채움): " + likes[0]);
    System.out.println("마지막 칸: " + likes[likes.length - 1]);
    System.out.println("크기: " + likes.length + "칸");
    likes[0] = 42;
    likes[4] = 128;
    System.out.println("값 채운 뒤 첫 칸: " + likes[0]);
    System.out.println("값 채운 뒤 마지막 칸: " + likes[4]);
    System.out.println();

    // 방식 3: new + 값 (방식 1과 비슷하지만 명시적)
    boolean[] isFollowing = new boolean[]{true, false, true, true};
    System.out.println("[방식 3] new + 값 — 팔로우 여부 4명");
    System.out.println("첫 사람 팔로우 중? " + isFollowing[0]);
    System.out.println("마지막 사람 팔로우 중? " + isFollowing[isFollowing.length - 1]);
    System.out.println("배열 크기: " + isFollowing.length);
}

실행 결과는 이렇게 나와요.

=== 인스타그램 배열 3가지 만들기 ===

[방식 1] 리터럴 — 팔로워 목록
첫 팔로워: 장원영
마지막 팔로워: 하니
총 인원: 5명

[방식 2] new + 크기 — 좋아요 카운터 5개
처음 값 (자동 0 채움): 0
마지막 칸: 0
크기: 5칸
값 채운 뒤 첫 칸: 42
값 채운 뒤 마지막 칸: 128

[방식 3] new + 값 — 팔로우 여부 4명
첫 사람 팔로우 중? true
마지막 사람 팔로우 중? true
배열 크기: 4

방식 1은 값을 그대로 적어 넣고, 방식 2는 크기만 정해서 만들어둔 뒤 나중에 값을 채웠고, 방식 3은 두 방식을 절반씩 섞었어요. 세 방식 모두 결과적으로 만들어진 배열 은 똑같은 모습이에요.

.length 와 마지막 인덱스의 관계

코드 중간에 자꾸 등장한 followers.length 가 뭔지 짚고 갈게요. .length배열의 칸 개수 를 알려주는 값이에요. 팔로워가 5명이면 followers.length5 가 됩니다.

여기서 비전공자가 가장 헷갈리는 지점이 나옵니다. "칸이 5개니까 마지막 인덱스는 5번이겠죠?" 아니에요! 인덱스는 0번부터 시작하니까, 5칸짜리 배열의 마지막 인덱스는 4 번입니다.

배열 크기 인덱스 범위 마지막 인덱스
5칸 0 ~ 4 length - 1 = 4
10칸 0 ~ 9 length - 1 = 9
100칸 0 ~ 99 length - 1 = 99

그래서 코드에서 마지막 칸을 꺼낼 때는 followers[followers.length - 1] 라는 표현이 자주 등장해요. 칸 개수에서 1을 뺀 값이 마지막 인덱스다 — 이 공식을 외워두면 평생 써먹습니다.

🙋 학생 질문 — "왜 인덱스는 0부터 시작해요? 1부터 세는 게 자연스럽지 않나요?"

정말 좋은 질문이에요! 사실 일상에서는 "첫 번째, 두 번째, 세 번째" 처럼 1부터 세는 게 익숙하죠. 그런데 컴퓨터 입장에서는 0부터 시작하는 게 훨씬 편하답니다.

배열은 메모리 위에 칸들이 줄지어 늘어선 구조예요. "첫 칸의 위치" 를 기준으로 "거기서 몇 칸 떨어진 위치인가?" 를 계산해서 값을 찾아요. 첫 칸은 0칸 떨어진 위치 니까 인덱스 0, 두 번째 칸은 1칸 떨어진 위치 니까 인덱스 1, 이런 식이죠.

이건 자바뿐 아니라 C, Python, JavaScript 등 거의 모든 프로그래밍 언어가 따르는 규칙이에요. 처음엔 어색해도, 며칠만 코드를 써보시면 0부터 세는 게 더 자연스럽게 느껴질 거예요.

💡 튜터의 결론

세 방식 모두 결국 같은 배열을 만들어내요. 값을 미리 알면 방식 1, 칸만 먼저 만들고 싶으면 방식 2, 명시적으로 표현하고 싶으면 방식 3 을 고르시면 됩니다. 인덱스는 0부터, 마지막 칸은 length - 1 — 이 두 가지만 확실히 잡고 가세요.

세 방식 한눈에 비교

방식 언제 쓰나 예시
1. 리터럴 {...} 값을 미리 다 알고 있다 String[] tags = {"#일상", "#카페"};
2. new T[N] 크기만 정하고 나중에 채운다 int[] scores = new int[100];
3. new T[]{...} 명시적으로 표현하고 싶을 때 int[] x = new int[]{1, 2, 3};

방식 1을 가장 자주 쓰고, 방식 2는 "값을 반복문으로 채워 넣을 때" 자주 등장해요. 방식 3은 가끔 보는 정도지만, 코드에서 마주쳤을 때 당황하지 않으려고 익혀두는 거예요.

자, 이제 배열을 만들 수 있게 됐어요! 그런데 배열의 진짜 위력은 만든 다음에 나타나요. 5칸짜리 배열의 값을 하나씩 꺼내려고 followers[0], followers[1], followers[2]... 를 계속 적는 건 변수 50개 만들던 시절과 다를 게 없잖아요?

다음 Step 에서는 반복문으로 배열을 한 번에 훑는 두 가지 방법 을 비교해볼게요. 지난 시간에 잠깐 맛본 enhanced for 가 다시 등장합니다!


Step 2: "배열을 한 바퀴 돌자" — 기본 for vs enhanced for

자, 이제 진짜 본론이에요. Step 1 마지막에 제가 던진 불만 기억하시죠? 5칸짜리 배열의 값을 하나씩 꺼내려고 likes[0], likes[1], likes[2]... 손으로 적기 시작하면, 결국 변수 50개 만들던 시절과 다를 게 없잖아요. 배열을 만들었으면 반복문으로 한 번에 훑어야 그 위력이 살아나요.

지난 시간 Day 4의 마지막 Step 에서 enhanced for 라는 이름을 잠깐 들으셨어요. 그때는 "아, 이런 게 있구나" 정도로만 넘어갔는데, 오늘 이 친구가 다시 등장합니다. 그리고 한 가지 더 — Day 4 초반에 익힌 기본 for 도 배열과 만나면 새로운 얼굴을 보여줘요.

오늘 Step 2의 목표는 단순해요. 배열을 한 바퀴 도는 두 가지 방법을 비교하고, 언제 무엇을 쓸지 까지 익혀보는 거예요.

방법 1: 기본 for + 인덱스

먼저 우리가 Day 4에서 익숙해진 기본 for 부터 살펴볼게요. 배열을 만났을 때 가장 표준적인 형태는 이렇게 생겼어요.

for (int i = 0; i < likes.length; i++) {
    System.out.println(likes[i]);
}

조건식이 세 칸으로 나뉘어 있죠? 한 칸씩 풀어볼게요.

  • int i = 0 — 인덱스 변수 i 를 0부터 시작
  • i < likes.lengthi 가 배열 칸 개수보다 작은 동안만 계속
  • i++ — 한 바퀴 돌 때마다 i 를 1씩 증가

i 는 0, 1, 2, 3, 4 순서로 변해가요. 그리고 매번 likes[i] 로 그 위치의 값을 꺼내는 거예요. 5칸짜리 배열이면 정확히 5번 반복하고 멈춥니다.

여기서 인덱스 변수 i 가 핵심이에요. i 덕분에 "지금 몇 번째 칸을 보고 있는지" 를 우리가 알 수 있어요. 그래서 "1번째 게시물:", "2번째 게시물:" 처럼 순서 번호와 함께 출력 하고 싶을 때 기본 for 가 빛을 발합니다.

사람은 1부터 세는 게 익숙하잖아요? 그래서 출력할 때는 i + 1 을 보여주는 트릭을 많이 써요. 인덱스는 0부터, 표시는 1부터 — 이 작은 차이를 메우는 방법이에요.

for (int i = 0; i < likes.length; i++) {
    System.out.println((i + 1) + "번째 게시물: " + likes[i] + "개");
}

i 자체는 0~4 인데, i + 1 을 출력하니까 화면에는 1~5가 찍혀요. 컴퓨터의 0-based 감각과 사람의 1-based 감각을 살짝 다리 놓아준 셈이에요.

방법 2: enhanced for

이번엔 Day 4에서 잠깐 만났던 그 친구를 다시 불러올게요.

for (int like : likes) {
    System.out.println(like);
}

처음 보면 콜론 : 이 낯설죠. 이 문법을 그대로 한국어로 풀어보면 이렇게 읽혀요.

likes 배열에서 값을 하나씩 꺼내서 like 라는 변수에 담고, 중괄호 안 코드를 실행해라.

5칸짜리 배열이면 다섯 번 반복돼요. 첫 번째 바퀴에는 like = 42, 두 번째에는 like = 128... 마지막에는 like = 210 이 들어와요. 그리고 다 돌면 자동으로 멈춥니다.

기본 for 와 비교하면 한눈에 보여요. 조건식 세 칸 (int i = 0; i < likes.length; i++) 이 다 사라지고, "꺼낼 곳""담을 변수" 만 남았어요. 인덱스 관리를 자바가 알아서 해주니까 코드가 훨씬 짧아져요.

특히 "값들을 다 더해줘", "값들을 다 출력해줘" 처럼 인덱스가 필요 없는 작업 에 잘 어울려요. 합계를 구하는 코드를 보면 차이가 분명해져요.

int total = 0;
for (int like : likes) {
    total = total + like;
}

i 라는 카운터가 등장하지 않아요. "값 하나씩 꺼내서 total 에 더해" 라는 의도가 코드에 그대로 드러나죠.

한 가지 함정 — enhanced for 는 인덱스를 모른다

이렇게 깔끔한 enhanced for 에도 한 가지 약점이 있어요. 지금 몇 번째 칸을 보고 있는지 알 수가 없다 는 거예요.

콜론 왼쪽에는 만 들어가요. "이 값이 배열의 몇 번째인가?" 를 묻고 싶어도, enhanced for 는 답해주지 않아요. "1번째 게시물:" 처럼 순서 번호와 함께 출력하려면 결국 기본 for 로 돌아가야 해요.

그리고 또 하나, enhanced for 의 변수는 임시 복사본 이에요. like 에 새 값을 대입해도 원본 배열은 안 바뀌어요. 값을 진짜로 수정하고 싶으면 likes[i] = ... 형태로 인덱스를 직접 써야 하고, 그러려면 기본 for 가 필요해요.

두 방법을 함께 — 통합 실행 코드

자, 이제 두 방법을 모두 모아놓은 실행 예제를 직접 돌려봅시다. IntelliJ 에서 day05 디렉토리에 ArrayTraversalComparison.java 파일을 만들고 아래 코드를 그대로 입력해보세요.

// day05/ArrayTraversalComparison.java
void main() {
    // 같은 배열을 두 가지 방법으로 순회
    int[] likes = {42, 128, 7, 95, 210};
    System.out.println("=== 게시물 좋아요 — 두 가지 방법으로 보기 ===");
    System.out.println();

    // 방법 1: 기본 for + 인덱스 (몇 번째인지 알고 싶을 때)
    System.out.println("[방법 1] 기본 for — 게시물 번호와 함께");
    for (int i = 0; i < likes.length; i++) {
        System.out.println((i + 1) + "번째 게시물: " + likes[i] + "개");
    }
    System.out.println();

    // 방법 2: enhanced for (값만 필요할 때 더 깔끔)
    System.out.println("[방법 2] enhanced for — 합계 계산");
    int total = 0;
    for (int like : likes) {
        total = total + like;
    }
    System.out.println("총 좋아요: " + total + "개");
    System.out.println("평균: " + (total / likes.length) + "개");
    System.out.println();

    // 두 방법을 같이 — 인덱스도 필요하고 값도 누적할 때
    System.out.println("[두 방법 합치기] 가장 인기 있는 게시물 찾기");
    int max = 0;
    int bestIndex = 0;
    for (int i = 0; i < likes.length; i++) {
        if (likes[i] > max) {
            max = likes[i];
            bestIndex = i;
        }
    }
    System.out.println("최고 인기: " + (bestIndex + 1) + "번 게시물 (" + max + "개)");
}

실행하면 이런 결과가 나와요.

=== 게시물 좋아요 — 두 가지 방법으로 보기 ===

[방법 1] 기본 for — 게시물 번호와 함께
1번째 게시물: 42개
2번째 게시물: 128개
3번째 게시물: 7개
4번째 게시물: 95개
5번째 게시물: 210개

[방법 2] enhanced for — 합계 계산
총 좋아요: 482개
평균: 96개

[두 방법 합치기] 가장 인기 있는 게시물 찾기
최고 인기: 5번 게시물 (210개)

방법 1은 번호와 값을 함께 보여주고, 방법 2는 인덱스 없이 합계만 깔끔하게 계산했어요. 마지막 블록은 두 방법을 합친 모습이에요 — 가장 인기 있는 게시물을 찾으려면 뿐 아니라 몇 번째 인지 도 알아야 하니까, 기본 for 를 써서 bestIndex 변수에 위치를 기억해뒀어요.

언제 무엇을 쓰나 — 비교 표

이쯤에서 두 방법을 한눈에 정리해볼게요.

상황 추천 이유
값만 합치거나 출력 enhanced for 코드가 간결, 인덱스 관리 불필요
몇 번째인지 알아야 함 기본 for 인덱스 i 를 직접 사용 가능
일부만 순회 (앞 3개만) 기본 for i < 3 처럼 범위 조절 자유
거꾸로 순회 기본 for i-- 로 역순 진행 가능
값을 바꿔야 함 (arr[i] = ...) 기본 for enhanced for 변수는 임시 복사본

표만 봐도 보이듯, enhanced for 는 한 가지 작업 (전체를 값만 훑기) 에 특화 되어 있고, 기본 for 는 모든 상황에 다 통하는 만능 도구 예요. 그래서 처음에는 기본 for 를 익숙하게 만든 다음, "아, 여기서는 인덱스가 필요 없네" 싶을 때 enhanced for 로 바꿔 쓰는 흐름이 자연스러워요.

🙋 학생 질문 — "enhanced for 에서 `like` 변수의 값을 바꾸면 원본 배열도 바뀌나요?"

좋은 질문이에요! 결론부터 말씀드리면 원본은 안 바뀌어요.

이런 코드를 한 번 상상해볼게요.

int[] likes = {42, 128, 7, 95, 210};
for (int like : likes) {
    like = like * 2; // 두 배로 만들어볼까?
}
System.out.println(likes[0]); // 결과는? 42 (그대로!)

like 라는 변수는 배열 칸 자체가 아니라 그 칸의 값을 복사해 담아둔 임시 변수 예요. like 에 새 값을 넣어도 그건 임시 변수만 바뀌고, 원본 배열의 0번 칸은 여전히 42 그대로예요.

원본 배열을 진짜로 수정하고 싶으면 이렇게 인덱스로 직접 접근해야 해요.

for (int i = 0; i < likes.length; i++) {
    likes[i] = likes[i] * 2; // 이렇게 해야 원본이 바뀐다
}

이게 enhanced for 의 한계예요. "값을 읽기만 할 때" 는 편리한데, "값을 바꿔야 할 때" 는 기본 for 로 돌아와야 해요.

참고로, 정수 (int) 같은 기본형은 항상 복사본 으로 동작하고, 객체 배열에서는 이야기가 조금 더 복잡해져요. 이 부분은 Day 8 클래스에서 객체를 본격적으로 다룰 때 다시 살펴볼게요.

💡 튜터의 결론

인덱스가 필요하면 기본 for, 값만 필요하면 enhanced for. 이 한 줄만 외워도 80% 는 해결돼요. 그리고 enhanced for 는 "읽기 전용" 이라는 점만 추가로 기억해두시면 충분합니다.

여기까지 오면 배열을 자유롭게 다룰 수 있게 된 거예요. 만들고, 한 바퀴 돌리고, 값을 꺼내 쓰고. 그런데 한 가지 함정이 아직 남아있어요.

인덱스를 잘못 쓰면 프로그램이 즉시 멈춥니다. 5칸짜리 배열에서 5번 칸을 찾으면 어떻게 될까요? "마지막은 4번이라고 했는데 깜빡하고 5번을 적으면?" 다음 Step 에서 그 함정의 정체를 직접 마주하고, if 문으로 안전하게 피해 가는 방법까지 다뤄볼게요.


Step 3: "배열의 길이를 넘어가면?" — 인덱스 범위와 안전 패턴

Step 2 마지막에 던진 그 질문, 이제 진짜로 마주할 시간이에요.

5칸짜리 배열에 followers[5] 를 적으면 어떻게 될까요? 사물함이 0번부터 4번까지 다섯 칸 있는데, 우리는 갑자기 "5번 사물함 좀 열어주세요" 하고 손을 내미는 거예요. 그런 사물함이 없는데도요. 자바는 이럴 때 눈치껏 처리하지 않아요. "없는 칸이니까 그냥 빈 값 줄게~" 하는 일은 절대 일어나지 않습니다. 그 대신 즉시 프로그램을 멈추고 빨간 글씨로 에러를 뱉어요.

처음에는 "왜 이렇게 까칠해?" 싶을 수 있는데, 사실 이건 자바가 친절한 거예요. 잘못된 값이 슬쩍 통과해서 한참 뒤에야 이상한 결과로 드러나는 것보다, 그 즉시 "여기 잘못됐어요!" 하고 알려주는 편이 훨씬 디버깅하기 좋거든요.

ArrayIndexOutOfBoundsException 이라는 친구

자바가 뱉는 그 에러의 이름이 좀 길어요. ArrayIndexOutOfBoundsException 이라고 하는데, 한 단어씩 떼서 읽으면 의미가 그대로 보여요.

  • Array — 배열의
  • Index — 인덱스가
  • Out Of Bounds — 경계를 벗어났다
  • Exception — 예외 (= 자바가 알리는 비정상 상황)

번역하면 "배열 인덱스가 경계를 벗어났어요" 가 돼요. 이름 그대로의 뜻이라 한 번 외우기만 하면 잊어버리기 어렵습니다.

여기서 "예외 (Exception)" 라는 단어가 새로 등장했죠? 자바에서 "평소엔 일어나면 안 되는 일이 벌어졌을 때" 알리는 방식이에요. 우리는 아직 예외를 직접 다루는 방법은 배우지 않았어요. Day 17 이후에 본격적으로 살펴볼 거니까, 지금은 "이런 게 있구나" 정도로만 받아들이시면 충분합니다.

실제로 5칸짜리 배열에 followers[5] 를 시도하면 콘솔에 이런 메시지가 떠요.

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException:
    Index 5 out of bounds for length 5
    at ArrayBoundsAndErrors.main(ArrayBoundsAndErrors.java:38)

긴 영어 메시지에 처음엔 압도될 수 있는데, 핵심은 두 줄이에요.

  • Index 5 — 우리가 요청한 인덱스 번호
  • for length 5 — 배열의 칸 개수

"길이가 5인 배열한테 5번 인덱스를 달라고 했네요?" 라고 자바가 친절히 알려주는 거예요. 그리고 마지막 줄의 at ArrayBoundsAndErrors.main(ArrayBoundsAndErrors.java:38)"이 사고가 38번째 줄에서 일어났어요" 라는 위치 안내예요. 에러가 무섭게 생겼지만, 잘 읽으면 어디서, 무엇이, 왜 잘못됐는지 다 알려주는 친절한 신고서입니다.

안전 패턴 1 — .length 로 끝까지 순회

그럼 이런 사고를 어떻게 막을까요? 첫 번째 패턴은 우리가 Step 2에서 이미 익힌 그 형태예요. 반복문에서 배열 끝을 .length 로 잡는 것.

for (int i = 0; i < followers.length; i++) {
    System.out.println("  [" + i + "] " + followers[i]);
}

여기서 핵심은 <<= 가 아니라는 점 이에요. 칸이 5개면 인덱스는 0, 1, 2, 3, 4 — 마지막은 4번이지 5번이 아니죠. i < 5 로 멈춰야 0부터 4까지만 안전하게 통과하고, i = 5 시점에는 반복문에 들어가지도 않고 끝나요. 만약 실수로 <= 를 적으면 마지막 한 바퀴에서 followers[5] 를 건드리려다가 그 빨간 에러를 마주합니다.

.length 라는 표현 자체도 안전장치예요. 만약 배열 크기가 바뀌어도 (예: 팔로워가 5명에서 10명이 됐어도) 코드를 하나도 안 고치고 계속 동작해요. 숫자 5 를 직접 적었다면 다 깨졌겠죠.

안전 패턴 2 — if 로 범위 확인 후 접근

두 번째 상황은 좀 달라요. 사용자가 직접 번호를 입력하는 경우 예요. 인스타그램에서 "7번째 팔로워 보여줘" 같은 요청이 들어왔다고 해볼게요. 그런데 우리 배열엔 5명밖에 없어요. 이대로 followers[7] 을 호출하면? 또 그 빨간 에러죠.

그래서 "접근하기 전에 안전한지 먼저 확인하기" 가 두 번째 패턴이에요.

int[] requestedIndexes = {0, 2, 4, 7, -1};
for (int idx : requestedIndexes) {
    if (idx >= 0 && idx < followers.length) {
        System.out.println("  요청 " + idx + "번 -> " + followers[idx]);
    } else {
        System.out.println("  요청 " + idx + "번 -> 범위 밖! 접근 안 함");
    }
}

if 조건이 두 가지를 동시에 확인하고 있어요.

  • idx >= 0 — 음수가 아닌지 (인덱스에 -1 같은 게 들어오면 안 돼요)
  • idx < followers.length — 배열 크기보다 작은지

&& 로 두 조건을 묶어서 둘 다 통과한 경우에만 배열에 접근해요. 한쪽이라도 어긋나면 else 로 빠져서 "접근 안 함" 으로 처리하죠. 빨간 에러가 뜨지 않고, 프로그램이 평화롭게 다음 줄로 넘어갑니다.

비전공자에게 한 줄로 외울 만한 규칙은 이거예요. 사용자가 입력한 값은 절대 그대로 믿지 마세요. 음수가 들어올 수도, 너무 큰 숫자가 들어올 수도 있어요. 배열에 접근하기 전에 한 번 거르는 습관을 들이면 빨간 에러를 만날 일이 거의 사라집니다.

안전 패턴 3 — 마지막 인덱스는 length - 1

세 번째는 패턴이라기보단 공식 재확인 이에요. Step 1에서도 한 번 짚었지만, 여기서 다시 한 번 확실히 짚고 갈게요.

System.out.println("  .length = " + followers.length);
System.out.println("  마지막 인덱스 = " + (followers.length - 1));
System.out.println("  마지막 사람 = " + followers[followers.length - 1]);

칸이 5개 (length = 5) 이면 마지막 인덱스는 4 예요. 5가 아니에요. 인덱스는 0부터 세니까 한 칸 빼주는 거죠. 마지막 사람을 꺼내고 싶을 때 followers[5] 라고 적으면 또 빨간 에러를 만나고, followers[followers.length - 1] 이라고 적어야 정확히 마지막 사람이 나옵니다.

이 패턴이 좋은 이유는, 배열 크기가 어떻게 바뀌어도 "항상 마지막" 을 가리킨다는 거예요. 5명이면 4번, 10명이면 9번, 50명이면 49번. length - 1 만 기억하면 매번 정확합니다.

한 번에 다 실행해보기

지금까지 본 세 패턴을 한 파일에 모아서 직접 돌려볼게요.

// day05/ArrayBoundsAndErrors.java
void main() {
    // 배열의 인덱스 범위 — 안전하게 다루기
    String[] followers = {"장원영", "카리나", "윈터", "민지", "하니"};
    System.out.println("=== 배열 인덱스 안전하게 쓰기 ===");
    System.out.println();

    // 1) .length 로 끝까지 안전하게 순회
    System.out.println("[안전 패턴 1] .length 로 끝까지");
    System.out.println("팔로워 수: " + followers.length + "명");
    for (int i = 0; i < followers.length; i++) {
        System.out.println("  [" + i + "] " + followers[i]);
    }
    System.out.println();

    // 2) if 로 범위 체크 후 접근 (사용자가 입력한 번호가 안전한지 확인)
    System.out.println("[안전 패턴 2] if 로 범위 확인 후 접근");
    int[] requestedIndexes = {0, 2, 4, 7, -1};
    for (int idx : requestedIndexes) {
        if (idx >= 0 && idx < followers.length) {
            System.out.println("  요청 " + idx + "번 -> " + followers[idx]);
        } else {
            System.out.println("  요청 " + idx + "번 -> 범위 밖! 접근 안 함");
        }
    }
    System.out.println();

    // 3) 자주 하는 실수: .length 가 아니라 .length - 1 이 마지막 인덱스
    System.out.println("[헷갈리는 부분] 마지막 인덱스는 length - 1");
    System.out.println("  .length = " + followers.length);
    System.out.println("  마지막 인덱스 = " + (followers.length - 1));
    System.out.println("  마지막 사람 = " + followers[followers.length - 1]);
    System.out.println();

    // 4) (실험용) 일부러 범위 밖 접근 — 주석 풀어보기
    //    실행하면 ArrayIndexOutOfBoundsException 이 발생해요.
    //    실제 코드에서는 절대 이렇게 두지 않아요.
    //
    // String wrong = followers[5];
    // System.out.println(wrong);

    System.out.println("(맨 아래 주석을 풀어서 일부러 범위 밖 접근 시 어떤 에러가 뜨는지 확인해보세요)");
}

IntelliJ에서 Run 버튼을 누르면 콘솔에 이런 결과가 떠요.

=== 배열 인덱스 안전하게 쓰기 ===

[안전 패턴 1] .length 로 끝까지
팔로워 수: 5명
  [0] 장원영
  [1] 카리나
  [2] 윈터
  [3] 민지
  [4] 하니

[안전 패턴 2] if 로 범위 확인 후 접근
  요청 0번 -> 장원영
  요청 2번 -> 윈터
  요청 4번 -> 하니
  요청 7번 -> 범위 밖! 접근 안 함
  요청 -1번 -> 범위 밖! 접근 안 함

[헷갈리는 부분] 마지막 인덱스는 length - 1
  .length = 5
  마지막 인덱스 = 4
  마지막 사람 = 하니

(맨 아래 주석을 풀어서 일부러 범위 밖 접근 시 어떤 에러가 뜨는지 확인해보세요)

세 패턴이 다 안전하게 통과한 게 보이시죠? 7번과 -1번 같은 "있을 수 없는 요청" 도 if 가드 덕분에 평화롭게 처리됐어요. 빨간 에러는 한 줄도 안 떴습니다.

한번은 일부러 깨뜨려 보세요

코드 맨 아래에 보면 주석으로 가려둔 두 줄이 있어요.

// String wrong = followers[5];
// System.out.println(wrong);

이 주석을 풀어서 (// 두 개를 지워서) 한 번 실행해보세요. 콘솔에 그 빨간 ArrayIndexOutOfBoundsException 이 정말로 뜨는 걸 직접 보시는 거예요. "이 친구가 이렇게 생겼구나" 를 눈에 익혀두면, 나중에 다른 코드를 짜다가 이 에러를 만나도 "아 이거 그때 봤던 그거네" 하고 침착하게 줄 번호를 찾아갈 수 있어요.

에러는 무서운 게 아니에요. 에러 메시지를 읽는 습관 만 들이면 빨간 글씨가 오히려 친구처럼 느껴집니다. 어디서, 무엇이, 왜 잘못됐는지 다 적어주거든요. 비전공자가 가장 빠르게 성장하는 길은 "에러를 두려워하지 않기" 예요.

🙋 학생 질문 — "튜터님, 왜 `i <= followers.length` 가 아니라 `i < followers.length` 인가요? 끝까지 가는 거니까 `<=` 가 맞지 않나요?"

좋은 질문이에요. 직관적으로는 "끝까지" 가는 거니까 <= 가 맞아 보이죠. 그런데 자바 배열의 인덱스 규칙을 다시 떠올려보면 답이 나와요.

5칸짜리 배열의 인덱스는 0, 1, 2, 3, 4 예요. 마지막은 4번이지 5번이 아니에요. 그런데 .length칸의 개수 를 알려주니까 5 를 반환해요. 칸 개수와 마지막 인덱스가 한 칸씩 다른 거죠.

만약 i <= followers.length 라고 적으면, i = 5 인 마지막 바퀴에서 followers[5] 를 건드리려고 시도해요. 그런데 5번 칸은 없으니까 ArrayIndexOutOfBoundsException 이 터지는 거예요.

i < followers.length 로 적으면 i 가 0, 1, 2, 3, 4 까지만 가고 i = 5 시점에는 반복문에 들어가지 않고 끝나요. 딱 0~4 까지만 안전하게 통과합니다.

한 줄로 외우면 이래요. 칸 개수는 5, 마지막 인덱스는 4. 그래서 < 가 맞아요.

💡 튜터의 결론

배열을 다룰 때는 언제나 .length 로 경계를 잡으세요. 숫자 5 같은 값을 직접 적어두면 배열 크기가 바뀔 때마다 다 깨져요. 그리고 사용자 입력처럼 밖에서 들어온 값 으로 배열에 접근할 땐 반드시 if (idx >= 0 && idx < arr.length) 가드를 한 번 거치는 습관, 이 두 가지만 지키면 빨간 에러를 만날 일이 거의 없어요.

여기까지 오면 배열을 안전하게 만들고, 돌리고, 다루는 기본기는 다 갖추신 거예요. 그런데 매번 정렬하거나 복사하거나 출력할 때마다 손으로 반복문을 짜는 건 좀 귀찮죠. 자바는 이런 자주 쓰는 작업들을 묶어놓은 도구 상자를 미리 만들어뒀어요. 다음 Step 에서는 그 도구 상자 — Arrays 유틸리티 — 를 열어볼게요.


Step 4: "정렬하고, 복사하고, 문자열로 변환하자" — Arrays 유틸리티

Step 3 까지 오느라 고생 많으셨어요. 이제 배열을 안전하게 만들고, 한 바퀴 돌리고, 인덱스 함정도 피할 수 있게 됐어요. 그런데 잘 생각해보면, 우리가 자주 하는 작업이 몇 가지로 정해져 있다는 걸 눈치채셨을 거예요. 정렬 / 복사 / 출력 — 이 세 가지가 거의 매번 등장해요.

문제는 이걸 매번 직접 짜려면 for 문이 한참 들어간다는 점이에요. 좋아요 숫자 7개를 오름차순으로 줄 세우려고 직접 정렬 알고리즘을 짜고, 배열을 통째로 출력하려고 또 for 문 한 번 돌리고… 같은 코드를 반복해서 쓰는 일이 많아져요.

그래서 자바는 이런 자주 쓰는 작업들을 미리 묶어놓은 도구 상자를 준비해뒀어요. 이름이 Arrays 예요. 오늘은 이 도구 상자 안에서 가장 자주 쓰는 도구 세 개를 꺼내 써볼게요.

import 한 줄로 도구 상자 끌어오기

Arrays 도구 상자를 쓰려면 코드 맨 위에 한 줄을 적어줘야 해요.

import java.util.Arrays;

이 한 줄이 "java.util 이라는 곳에 있는 Arrays 라는 도구 상자를 우리 파일에서 쓸게요" 라는 선언이에요. import 는 영어 단어 그대로 "수입한다" 는 뜻인데, 다른 곳에 만들어둔 도구를 우리 파일로 끌어오는 그림이에요.

java.util 이라는 이름도 잠깐 짚어볼게요. 이건 자바가 기본으로 제공하는 유틸리티(utility, 유용한 도구) 모음 폴더 같은 개념이에요. 여기에는 Arrays 말고도 나중에 배울 다른 도구들이 잔뜩 들어 있어요. "폴더 이름" 에 가까운 이 단어를 정식으로는 패키지(package) 라고 부르는데, 이건 다음 시간 즈음에 본격적으로 다룰 예정이라 지금은 "도구 모음 폴더" 이미지만 잡고 가셔도 충분해요.

도구 1: Arrays.toString — 배열을 한 줄 문자열로

가장 먼저 꺼내볼 도구는 Arrays.toString 이에요. 이 도구는 배열의 내용을 사람이 읽을 수 있는 문자열로 바꿔주는 친구예요.

이게 왜 필요할까요? 그냥 배열을 System.out.println 으로 출력하면 어떻게 될까 직접 확인해볼게요.

int[] likes = {42, 128, 7};
System.out.println(likes);            // [I@1b6d3586 같은 이상한 문자열
System.out.println(Arrays.toString(likes));  // [42, 128, 7]

위쪽 줄을 실행하면 [I@1b6d3586 같은 정체불명의 문자열이 찍혀요. 이건 배열이 메모리에 저장된 주소 비슷한 정보라 사람이 보기엔 아무 의미가 없어요. 반면 Arrays.toString(likes) 을 거치면 [42, 128, 7] 처럼 깔끔하게 내용이 보여요.

코드를 짜다가 "이 배열에 지금 뭐가 들어있지?" 가 궁금할 때마다 쓰는, 디버깅용 단골 도구라고 생각하시면 돼요.

도구 2: Arrays.sort — 배열을 오름차순으로 줄 세우기

두 번째 도구는 Arrays.sort 예요. 이름 그대로 정렬해주는 도구인데, 작은 값부터 큰 값 순서로 (= 오름차순) 줄을 세워줘요.

int[] likes = {42, 128, 7, 95};
Arrays.sort(likes);
System.out.println(Arrays.toString(likes));  // [7, 42, 95, 128]

정렬이 끝나면 가장 작은 값은 likes[0] 에, 가장 큰 값은 likes[likes.length - 1] 에 있어요. 인덱스 위치만 알면 최저값·최고값을 그냥 꺼내 쓰면 돼요.

여기서 꼭 짚고 갈 점이 하나 있어요. Arrays.sort원본 배열을 직접 바꿔버려요. 새 배열을 만들어서 돌려주는 게 아니라, 원본 배열의 내용을 안에서 휘저어서 정렬된 상태로 만들어요. 코드에서도 보이듯이 Arrays.sort(likes); 한 줄 다음에 likes 를 다시 출력하면 이미 정렬된 상태예요.

이걸 영어로는 side effect (부작용) 이 있다고 표현해요. "부작용" 이라고 하니 나쁜 뜻 같지만, 여기서는 "함수를 호출했더니 원본 데이터까지 같이 바뀜" 정도의 중립적인 의미예요. 빠르고 메모리도 아끼지만, 원본을 보존하고 싶으면 따로 신경 써야 한다는 점은 기억해두세요. 이 얘기는 잠시 뒤에 한 번 더 나옵니다.

도구 3: Arrays.copyOf — 앞에서부터 N개 복사하기

세 번째 도구는 Arrays.copyOf 예요. 이 도구는 원본 배열의 앞에서부터 N개를 잘라서 새 배열 로 만들어줘요.

int[] likes = {7, 42, 95, 128};
int[] first = Arrays.copyOf(likes, 2);
System.out.println(Arrays.toString(first));  // [7, 42]

Arrays.copyOf(원본, 길이) 형태로 부르면, 원본의 앞 부분을 지정한 길이만큼 잘라서 새 배열을 돌려줘요. 여기서 중요한 점은 두 가지예요.

첫째, 원본은 그대로 유지돼요. 위 예시에서 likesArrays.copyOf 를 호출한 다음에도 여전히 [7, 42, 95, 128] 그대로예요. Arrays.sort 와는 정반대 성격이에요.

둘째, 원본보다 더 긴 길이를 적으면 뒤쪽은 자동으로 초기값 (int 라면 0) 으로 채워져요. 예를 들어 4칸짜리 배열에서 Arrays.copyOf(likes, 6) 을 하면 결과는 [7, 42, 95, 128, 0, 0] 이 돼요. Step 1 에서 배운 "배열을 새로 만들면 빈 칸은 0 으로 자동 초기화된다" 는 규칙이 여기에도 똑같이 적용돼요.

통합 실행 — 좋아요 숫자에 세 도구를 한꺼번에

이제 세 도구를 한 흐름으로 묶어볼게요. 게시글 7개의 좋아요 수가 있다고 가정하고, 그걸 정렬한 다음 상위 3개와 하위 3개를 뽑아내는 코드예요.

// day05/ArraysUtilityDemo.java
import java.util.Arrays;

void main() {
    // Arrays 도구 상자 — 정렬, 복사, 출력
    int[] likes = {42, 128, 7, 95, 210, 33, 67};
    System.out.println("=== Arrays 도구 상자 활용 ===");
    System.out.println();

    // Arrays.toString — 디버깅할 때 배열 통째로 보기
    System.out.println("[Arrays.toString] 배열을 한 줄 문자열로");
    System.out.println("원본: " + Arrays.toString(likes));
    System.out.println();

    // Arrays.sort — 오름차순 정렬 (원본 배열을 직접 정렬함)
    Arrays.sort(likes);
    System.out.println("[Arrays.sort] 오름차순 정렬 후");
    System.out.println("정렬됨: " + Arrays.toString(likes));
    System.out.println("최저 좋아요: " + likes[0]);
    System.out.println("최고 좋아요: " + likes[likes.length - 1]);
    System.out.println();

    // Arrays.copyOf — 상위 N개만 추리기 (원본은 그대로, 새 배열 반환)
    // 정렬은 오름차순이므로 뒤쪽이 큰 값. 뒤집어서 보려면 직접 인덱스를 거꾸로
    System.out.println("[Arrays.copyOf] 좋아요 상위 3개 추리기");
    int topN = 3;
    int[] top = new int[topN];
    for (int i = 0; i < topN; i++) {
        top[i] = likes[likes.length - 1 - i];
    }
    System.out.println("Top " + topN + ": " + Arrays.toString(top));
    System.out.println();

    // Arrays.copyOf 본래 형태도 살펴볼게요 — 앞에서부터 N개
    int[] firstThree = Arrays.copyOf(likes, 3);
    System.out.println("[Arrays.copyOf] 정렬된 배열 앞 3개 (가장 작은 3개)");
    System.out.println("앞 3개: " + Arrays.toString(firstThree));
}

실행하면 다음처럼 찍혀요.

=== Arrays 도구 상자 활용 ===

[Arrays.toString] 배열을 한 줄 문자열로
원본: [42, 128, 7, 95, 210, 33, 67]

[Arrays.sort] 오름차순 정렬 후
정렬됨: [7, 33, 42, 67, 95, 128, 210]
최저 좋아요: 7
최고 좋아요: 210

[Arrays.copyOf] 좋아요 상위 3개 추리기
Top 3: [210, 128, 95]

[Arrays.copyOf] 정렬된 배열 앞 3개 (가장 작은 3개)
앞 3개: [7, 33, 42]

코드 흐름을 한 번 읽어볼게요. 먼저 Arrays.toString 으로 원본 상태를 찍어서 "지금 배열에 뭐가 들어있는지" 확인했어요. 그다음 Arrays.sort 한 줄로 오름차순 정렬을 끝내고, 정렬된 배열의 0번 칸과 마지막 칸으로 최저·최고를 뽑았어요.

상위 3개를 뽑는 부분이 살짝 까다로워요. Arrays.copyOf앞에서부터 자르는데, 우리는 오름차순으로 정렬했으니 큰 값이 뒤쪽에 있죠. 그래서 큰 값 3개를 뽑으려면 for 문으로 인덱스를 뒤에서부터 거꾸로 읽어와야 해요. likes.length - 1 - i 가 바로 그 인덱스를 계산해주는 공식이에요. i 가 0, 1, 2 로 늘어나는 동안 뒤에서 첫 번째, 두 번째, 세 번째 칸을 차례로 가져오는 식이에요.

마지막엔 Arrays.copyOf 본래 사용법대로 앞 3개를 그냥 잘라봤어요. 정렬된 상태에서 앞 3개를 가져오면 가장 작은 3개가 나오는 흐름이에요.

세 도구 한눈에 정리

지금까지 본 세 도구를 표로 정리하면 이렇게 돼요. 처음 익힐 때 가장 헷갈리는 게 "원본을 바꾸냐, 안 바꾸냐" 인데, 이 표에서 그 차이를 콕 짚어둘게요.

도구 하는 일 원본 변경? 반환값
Arrays.toString(배열) 보기 좋은 문자열로 만들기 X String
Arrays.sort(배열) 오름차순 정렬 O (직접 변경) 없음 (void)
Arrays.copyOf(원본, N) 앞 N개 복사 X int[] (또는 해당 타입)

Arrays.sort 만 유일하게 원본을 직접 바꾼다는 점, 이거 하나만 머리에 넣어두면 나중에 헷갈릴 일이 거의 없어요.

🙋 학생 질문 — "튜터님, `Arrays.sort` 가 원본 배열을 바꾼다고 하셨는데, 그럼 원본을 보존하고 싶으면 어떻게 하나요?"

아주 좋은 질문이에요. 정렬은 하고 싶은데 원본은 손대고 싶지 않을 때가 분명히 있거든요. 예를 들어 "전체 게시글 목록은 원래 순서대로 두고, 인기 순으로 정렬한 별도 화면도 따로 보여주고 싶다" 같은 경우요.

이럴 때 쓰는 패턴이 바로 "Arrays.copyOf 로 사본을 먼저 떠놓고, 그 사본을 정렬하기" 예요. 짧게 보여드리면 이렇게 돼요.

int[] likes = {42, 128, 7, 95};
int[] sorted = Arrays.copyOf(likes, likes.length);  // 같은 길이로 통째 복사
Arrays.sort(sorted);                                 // 사본만 정렬

System.out.println("원본: " + Arrays.toString(likes));   // [42, 128, 7, 95] 그대로
System.out.println("정렬: " + Arrays.toString(sorted)); // [7, 42, 95, 128]

Arrays.copyOf(likes, likes.length)"원본 배열을 똑같은 길이로 통째 복사" 하는 흔한 관용구예요. 사본을 만든 다음에 거기에만 Arrays.sort 를 적용하면 원본은 안전하게 보존돼요. 이 패턴은 Step 7 종합 실습에서도 자연스럽게 다시 나올 거예요.

💡 튜터의 결론

Arrays.toString 은 디버깅에, Arrays.sort 는 정렬에, Arrays.copyOf 는 일부만 추리거나 복사할 때. 이 셋만 익혀두면 배열 관련 작업의 80% 는 깔끔하게 처리돼요. 그리고 Arrays.sort 만 원본을 바꾼다 는 점, 이 한 줄만 기억해두세요.

여기까지가 1차원 배열의 마지막이에요. 그런데 인스타그램 프로필 화면을 떠올려볼게요. 사진이 한 줄로 쭉 늘어서 있나요? 아니죠. 3×3 격자 로 가지런히 떠 있어요. 이렇게 표 형태 의 데이터를 다루려면 한 차원 더 올라간 배열이 필요해요. 다음 Step 에서는 2차원 배열 을 만나볼게요.


Step 5: "2차원 배열로 표를 만들자" — 인스타 프로필 3×3 피드 그리드

자, Step 4 까지 우리는 1차원 배열의 모든 것을 훑었어요. 선언하고, 순회하고, 인덱스를 안전하게 다루고, Arrays 유틸리티로 정렬·복사·출력까지. 1차원 배열이 일렬로 늘어선 사물함 이었다면, 이제 한 차원 더 올라갈 시간이에요.

인스타그램 앱을 열어서 누군가의 프로필 화면을 떠올려봅시다. 사진이 어떻게 진열돼 있죠? 3×3 격자 로 가지런히 떠 있어요. 가로 3장, 세로 3장. 만약 이걸 1차원 배열로 표현하면 어떻게 될까요? 가로 9칸짜리 배열을 만들고 "3번째 칸이 첫째 줄 끝, 6번째 칸이 둘째 줄 끝" 하고 손으로 계산해야 해요. 머릿속이 금세 헝클어지죠.

자바는 이걸 훨씬 깔끔하게 표현해주는 방법을 가지고 있어요. 바로 2차원 배열 이에요. 이름은 거창해 보이지만, 알고 보면 배열의 배열 일 뿐입니다.

2차원 배열 = 배열의 배열

2차원 배열은 말 그대로 1차원 배열 안에 또 1차원 배열이 들어있는 구조 예요.

큰 박스가 하나 있다고 상상해보세요. 그 안에 책장 칸이 가로로 3개 들어있고, 각 책장 칸 안에는 또 책이 3권씩 꽂혀 있어요. 책 한 권을 찾으려면 "몇 번째 책장 칸 → 그 안에서 몇 번째 책" 두 번을 짚어야겠죠?

2차원 배열도 똑같아요. 첫 번째 인덱스는 행 (row, 가로줄), 두 번째 인덱스는 열 (col, column 의 줄임 — 세로 칸) 을 가리켜요. 선언은 이렇게 합니다.

int[][] feedGrid;

대괄호 [] 가 두 개 붙어요. 1차원이 대괄호 한 개였으니, 2차원은 두 개. 차원이 늘어날수록 대괄호도 하나씩 늘어난다고 기억하면 편해요.

선언과 초기화 — 두 가지 방식

1차원 배열을 만들 때 값을 다 알 때빈 박스부터 만들 때 두 가지 방법이 있었죠? 2차원도 똑같이 두 가지 방식이 있어요.

방식 1 — 리터럴 (값을 다 알 때)

int[][] feedGrid = {
        {42, 128, 7},
        {95, 210, 33},
        {67, 150, 88}
};

중괄호 안에 또 중괄호 가 행마다 들어가요. 바깥 중괄호가 전체 배열, 안쪽 중괄호 하나가 한 행(가로줄) 이에요. 이 코드는 3행 3열짜리 격자를 한 번에 만들어줘요.

방식 2 — new 키워드 (빈 격자부터)

int[][] grid = new int[3][3];

3행 3열의 빈 격자가 만들어져요. 모든 칸은 1차원 때처럼 자동으로 0 으로 채워져요. 나중에 값이 정해지면 grid[r][c] = 값; 으로 한 칸씩 채우면 돼요.

좌표로 값 꺼내기

2차원 배열에서 특정 칸의 값을 꺼낼 때는 배열[행][열] 순서로 짚어줘요.

feedGrid[0][0] = 42     feedGrid[0][1] = 128    feedGrid[0][2] = 7
feedGrid[1][0] = 95     feedGrid[1][1] = 210    feedGrid[1][2] = 33
feedGrid[2][0] = 67     feedGrid[2][1] = 150    feedGrid[2][2] = 88

feedGrid[1][2] 를 읽어볼까요? "1번 행의 2번 열" — 즉 두 번째 가로줄의 세 번째 칸이니까 33 이에요. 인덱스는 1차원과 마찬가지로 0 부터 시작한다는 점, 잊지 마세요.

🙋 학생 질문 타임"튜터님, grid[2][3] 처럼 행/열 순서가 자꾸 헷갈려요. 외우는 비법이 있을까요?"

외우려고 하면 더 헷갈려요. 대신 이미지화 가 훨씬 잘 먹혀요. 바깥쪽 대괄호 = 큰 단위 (책장 칸 = 행), 안쪽 대괄호 = 작은 단위 (책장 안 책 = 열) 이렇게 떠올리세요. 큰 단위부터 짚고 작은 단위로 좁혀 들어간다 — 영어로는 outer-to-inner 순서라고도 해요. 우편번호도 시 → 구 → 동 → 번지 큰 단위부터 좁혀가잖아요. 그 흐름이에요.

.length — 행과 열을 각각 재기

1차원 배열에서 .length 로 칸 개수를 알아냈죠? 2차원에서는 .length 가 두 가지 의미로 쓰여요.

feedGrid.length          // 행 개수 = 3
feedGrid[0].length       // 첫 번째 행의 열 개수 = 3
feedGrid[1].length       // 두 번째 행의 열 개수 = 3

feedGrid.length전체 행 개수 를 알려줘요. 그리고 feedGrid[행번호].length그 행 안에 들어있는 열 개수 를 알려줘요. 왜 행마다 따로 길이를 재느냐고요? 사실 자바의 2차원 배열은 행마다 열 개수가 달라도 괜찮아요. 이런 걸 비정형 배열 (jagged array) 이라고 부르는데, Day 5 에서는 다루지 않을 거예요. 다만 "행마다 길이를 다르게 만들 수도 있구나" 정도만 머릿속에 둬도 충분합니다.

이중 for 루프 — 격자 전체 순회하기

2차원 배열을 처음부터 끝까지 훑으려면 for 안에 for 를 넣어요. Day 4 마지막에 잠깐 봤던 중첩 반복문 이 여기서 진짜 쓸모를 발휘합니다.

for (int row = 0; row < feedGrid.length; row++) {
    for (int col = 0; col < feedGrid[row].length; col++) {
        System.out.print(feedGrid[row][col] + " ");
    }
    System.out.println();
}

바깥 for 가 행을 하나씩 짚어가고, 안쪽 for 가 그 행 안의 열을 처음부터 끝까지 훑어요. 안쪽 for 가 끝나면 System.out.println() 으로 줄바꿈을 해줘서 격자 형태로 보이게 만들어요. 책을 읽을 때처럼 왼쪽에서 오른쪽으로 읽다가, 한 줄이 끝나면 다음 줄 맨 앞으로 내려가는 흐름이에요.

통합 실습 — 인스타 3×3 피드 좋아요 격자

이제 지금까지 배운 걸 합쳐서 진짜 인스타그램스러운 예제를 돌려봅시다. 어떤 인플루언서의 프로필에 들어갔다고 상상해보세요. 사진 9장이 3×3 으로 떠 있고, 각 사진마다 좋아요 숫자가 붙어 있어요. 우리는 이 좋아요 숫자를 격자로 출력하고, 총합·평균·줄별 합계까지 계산해볼 거예요.

// day05/TwoDimensionalArrays.java
void main() {
    // 2차원 배열 — 인스타 프로필 3x3 피드 썸네일의 좋아요 격자
    int[][] feedGrid = {
            {42, 128, 7},
            {95, 210, 33},
            {67, 150, 88}
    };
    System.out.println("=== 인스타 프로필 3x3 피드 좋아요 격자 ===");
    System.out.println();

    // 격자 형태로 출력 — 이중 for
    System.out.println("[좋아요 격자]");
    for (int row = 0; row < feedGrid.length; row++) {
        for (int col = 0; col < feedGrid[row].length; col++) {
            // 자릿수 맞춰서 보기 좋게
            int v = feedGrid[row][col];
            if (v < 10) {
                System.out.print("  " + v + " ");
            } else if (v < 100) {
                System.out.print(" " + v + " ");
            } else {
                System.out.print(v + " ");
            }
        }
        System.out.println();
    }
    System.out.println();

    // 총합 / 평균 — 모든 칸 합치기
    int total = 0;
    int count = 0;
    for (int row = 0; row < feedGrid.length; row++) {
        for (int col = 0; col < feedGrid[row].length; col++) {
            total = total + feedGrid[row][col];
            count++;
        }
    }
    System.out.println("총 좋아요: " + total + "개 (" + count + "개 게시물)");
    System.out.println("평균: " + (total / count) + "개");
    System.out.println();

    // 행마다 합계 — 줄 단위로 들여다보기
    System.out.println("[줄별 합계]");
    for (int row = 0; row < feedGrid.length; row++) {
        int rowSum = 0;
        for (int col = 0; col < feedGrid[row].length; col++) {
            rowSum = rowSum + feedGrid[row][col];
        }
        System.out.println((row + 1) + "째 줄 합계: " + rowSum + "개");
    }
}

실행하면 이렇게 나와요.

=== 인스타 프로필 3x3 피드 좋아요 격자 ===

[좋아요 격자]
 42 128   7 
 95 210  33 
 67 150  88 

총 좋아요: 820개 (9개 게시물)
평균: 91개

[줄별 합계]
1째 줄 합계: 177개
2째 줄 합계: 338개
3째 줄 합계: 305개

코드를 세 덩어리로 나눠서 살펴볼게요.

첫 번째 덩어리는 격자 출력 이에요. 이중 for 로 모든 칸을 훑되, 중간에 if-else 가 들어가 있죠? 좋아요 숫자가 1자리(7)·2자리(42·33·88 등)·3자리(128·210·150)로 들쭉날쭉하면 정렬이 어그러져요. 그래서 1자리 앞에는 공백 2칸, 2자리 앞에는 공백 1칸, 3자리는 그대로 — 이렇게 자릿수를 맞춰서 출력하면 화면에서 격자가 깔끔하게 정렬돼 보여요. 출력 결과를 보면 숫자들이 세로로도 가지런히 떨어져 있죠?

두 번째 덩어리는 총합과 평균 이에요. totalcount 두 변수를 0 으로 시작해두고, 이중 for 로 모든 칸을 돌면서 값을 더하고 개수를 세요. 1차원에서 했던 합계 패턴이 그대로 2차원으로 확장된 것뿐이에요. 9개 게시물의 좋아요 합 820, 평균 91 (정수 나눗셈이라 소수점은 잘려요).

세 번째 덩어리는 줄별 합계 예요. 바깥 for 한 번 돌 때마다 rowSum 을 0 으로 초기화하고, 안쪽 for 로 그 행만 더해요. 한 행이 끝나면 그 줄의 합계를 출력. 이렇게 하면 "어느 줄에 인기 사진이 몰려 있는지" 한눈에 보여요. 두 번째 줄이 338 개로 압도적이네요 — 210 짜리 사진이 거기 있었군요.

표 형태 데이터, 어디에 더 쓸까?

2차원 배열은 만들고 싶을 때 그냥 떠올리면 돼요. 인스타 피드 외에도 우리 일상에는 표 형태 데이터가 정말 많아요.

  • 게임판 — 오목판, 체스판, 지뢰찾기. 모두 행·열 좌표 로 칸을 짚어요.
  • 엑셀 시트 — 정확히 2차원 배열 구조 그대로예요. A1·B2 가 바로 [행][열].
  • 픽셀 이미지 — 가로 1920 × 세로 1080 모니터의 한 픽셀 한 픽셀이 2차원 배열의 한 칸. 컬러 이미지라면 RGB 값까지 들어가서 3차원이 되기도 해요.
  • 달력 — 한 달을 주(행) × 요일(열) 로 표현하면 그게 바로 2차원 배열이에요.

💡 튜터의 결론

2차원 배열은 표 형태 데이터 를 다룰 때 자연스럽게 떠올리면 돼요. 인스타 피드, 게임판, 엑셀 시트, 픽셀 이미지 — 전부 2차원 배열로 표현할 수 있어요. 바깥 인덱스는 큰 단위(행), 안쪽 인덱스는 작은 단위(열) 라는 순서만 확실히 잡으면, 차원이 더 늘어나도 (3차원, 4차원) 같은 원리로 확장돼요.

표 형태 데이터까지 다룰 수 있게 됐어요. 그런데 지금까지 우리가 만든 배열은 항상 "이 배열을 사용해줘" 라고 직접 만들어서 넘기는 형태였죠. 만약 어떤 함수가 "몇 개를 받을지 미리 정해두지 않고 유연하게 0개부터 100개까지 받고 싶어" 한다면 어떻게 해야 할까요? 다음 Step 에서는 그런 상황을 위한 가변 인자 (varargs) 를 다뤄볼게요. 다음 시간 메서드 단원으로 가는 다리이기도 합니다.


Step 6: "0개부터 100개까지 받아낸다" — 가변 인자 (varargs)

Step 5 까지 우리는 1차원 배열을 일렬로 늘어놓고, 2차원 배열로 표 형태까지 다뤄봤어요. 배열을 만들고, 순회하고, 안전하게 인덱스를 짚는 도구를 거의 다 익혔습니다. 이제 시야를 한 칸만 더 넓혀볼게요.

인스타그램에서 게시물 하나에 해시태그가 몇 개 붙는지 생각해보세요. 누군가는 단순하게 #일상 하나만 달고, 누군가는 #카페 #디저트 #브런치 #주말 #감성 ... 이런 식으로 열 개도 넘게 붙여요. 아예 해시태그 없이 그냥 사진만 올리는 경우도 있죠. 개수가 그때그때 다르다 는 게 핵심이에요.

지금까지 배운 방식으로도 처리는 됩니다. String[] tags = {"#카페", "#디저트"}; 처럼 배열을 미리 만들어서 통째로 넘기면 돼요. 그런데 호출할 때마다 new String[]{...} 으로 감싸야 한다는 게 좀 거추장스러워요. 해시태그 하나 넣고 싶을 때도 배열로 포장해야 한다니, 시키는 쪽 손이 자꾸 가죠.

자바는 이런 "개수가 미리 정해지지 않은 인자" 를 자연스럽게 받는 문법을 따로 마련해뒀어요. 이름은 가변 인자, 영어로는 varargs (variable arguments 의 줄임) 라고 부릅니다.

잠깐 — 이번 Step 은 메서드를 살짝 미리 보고 갑니다

varargs 를 배우려면 메서드 라는 개념을 짧게 짚고 넘어가야 해요. 본격적인 학습은 다음 시간이지만, 오늘은 맛만 봅시다.

메서드는 한 마디로 자주 쓰는 작업을 이름 붙여서 따로 떼어둔 작은 코드 블록 이에요. 지금까지 우리가 System.out.println(...) 을 호출할 때마다 화면에 글자가 찍혀 나왔잖아요? println 도 사실은 자바가 미리 만들어둔 메서드예요. 이름을 부르면 그 안에 적혀있는 일을 대신 해주는 거죠.

메서드를 직접 만드는 형태는 다음과 같아요.

static void 메서드이름(받을값타입 받을값이름) {
    // 여기서 할 일을 적어요
}

그리고 호출은 이렇게 합니다.

메서드이름(값);

자세한 규칙은 다음 시간에 본격적으로 다룰 거예요. 지금은 "이름 붙은 작은 코드 블록을 따로 떼어두고, 필요할 때 그 이름을 부르면 자동으로 실행된다" 이 정도만 머릿속에 두고 가셔도 충분합니다.

점 세 개 (...) 의 의미

varargs 의 문법은 정말 단순해요. 메서드가 받을 값 타입 뒤에 점 세 개 를 붙이면 됩니다.

static void printHashtags(String... tags) {
    System.out.println("해시태그 " + tags.length + "개");
    for (String tag : tags) {
        System.out.println("  " + tag);
    }
}

String... tags 를 풀어 읽으면 "0개 이상의 String 을 배열로 묶어서 받겠다" 는 뜻이에요. 메서드 안쪽에서 tags 는 그냥 String 배열 처럼 동작합니다. tags.length 로 개수도 알 수 있고, tags[0] 으로 인덱스 접근도 되고, for (String tag : tags) 로 순회도 되고요. Step 1~4 에서 배열을 다뤘던 방식 그대로예요.

차이는 호출하는 쪽 에서 일어나요. 배열을 따로 만들어서 넘길 필요 없이, 그냥 콤마로 값을 나열하면 자바가 알아서 배열로 묶어줍니다.

printHashtags("#일상");                // 1개
printHashtags("#카페", "#디저트");      // 2개
printHashtags();                       // 0개도 OK

세 호출 모두 같은 메서드를 부르고 있어요. 첫 번째 호출은 안쪽에서 tags.length1, 두 번째는 2, 세 번째는 0 이 됩니다. 부르는 쪽은 자유롭게, 받는 쪽은 항상 배열 — 이게 varargs 의 핵심이에요.

통합 실행 — 게시물 4개의 해시태그 출력하기

이제 실제로 돌려봅시다. 인플루언서 한 명이 게시물을 네 개 올렸다고 상상해보세요. 첫 번째는 해시태그 하나, 두 번째는 두 개, 세 번째는 네 개, 네 번째는 아예 없어요. 같은 printHashtags 메서드 하나로 네 가지 상황을 다 처리해볼게요.

// day05/VarargsDemonstration.java
void main() {
    // varargs — 개수가 정해지지 않은 인자를 배열처럼 받기
    // 메서드 자체는 다음 시간에 본격적으로 배워요.
    // 지금은 "배열을 자동으로 받는 메서드" 형태만 살짝 미리 보고 가요.
    System.out.println("=== 인스타 해시태그 다양한 개수로 출력하기 ===");
    System.out.println();

    System.out.println("[게시물 1] 해시태그 1개");
    printHashtags("#일상");
    System.out.println();

    System.out.println("[게시물 2] 해시태그 2개");
    printHashtags("#카페", "#디저트");
    System.out.println();

    System.out.println("[게시물 3] 해시태그 4개");
    printHashtags("#OOTD", "#패션", "#스트릿", "#가을");
    System.out.println();

    System.out.println("[게시물 4] 해시태그 0개");
    printHashtags();
}

// String... tags 는 "0개 이상의 String 을 배열로 받는다" 는 뜻.
// 메서드 안에서는 그냥 String[] 배열처럼 다루면 돼요.
static void printHashtags(String... tags) {
    System.out.println("  해시태그 " + tags.length + "개:");
    if (tags.length == 0) {
        System.out.println("    (없음)");
        return;
    }
    for (String tag : tags) {
        System.out.println("    " + tag);
    }
}

실행하면 이렇게 나와요.

=== 인스타 해시태그 다양한 개수로 출력하기 ===

[게시물 1] 해시태그 1개
  해시태그 1개:
    #일상

[게시물 2] 해시태그 2개
  해시태그 2개:
    #카페
    #디저트

[게시물 3] 해시태그 4개
  해시태그 4개:
    #OOTD
    #패션
    #스트릿
    #가을

[게시물 4] 해시태그 0개
  해시태그 0개:
    (없음)

코드를 두 덩어리로 나눠서 살펴볼게요.

윗부분은 main 안의 호출들 이에요. printHashtags("#일상") 부터 printHashtags() 까지 인자 개수가 1, 2, 4, 0 으로 제각각이죠? 같은 메서드를 네 번 부르는데 호출하는 모양이 매번 달라요. 이게 가능해진 이유가 바로 메서드 시그니처의 String... tags 덕분이에요.

아래쪽은 printHashtags 메서드 본체 예요. 안쪽에서는 tags 를 그냥 배열처럼 다뤄요. 먼저 tags.length 로 몇 개가 들어왔는지 출력하고, if (tags.length == 0) 으로 해시태그가 하나도 없는 경우 를 따로 처리해줍니다. 이때 등장하는 return;"이 메서드를 여기서 끝낸다" 는 신호예요. 0 개일 때 굳이 아래쪽 for 까지 갈 필요가 없으니 일찍 끝내버리는 거죠. return 키워드도 다음 시간 메서드 단원에서 본격적으로 다룰 예정이니, 지금은 "여기서 메서드 탈출" 정도로 기억해두시면 됩니다. 0 개가 아니면 enhanced for 로 태그를 하나씩 순회하면서 출력 — Step 2 에서 배운 그 패턴이에요.

varargs 의 제약 — 한 가지만 기억해두기

varargs 가 편리하긴 한데, 사용할 때 지켜야 할 규칙이 하나 있어요. 한 메서드 안에 varargs 는 딱 한 개 만 쓸 수 있고, 메서드 인자 중 항상 맨 마지막 에 와야 해요.

// OK — varargs 가 맨 마지막
static void log(String level, String... messages) { ... }

// 컴파일 에러 — varargs 가 마지막이 아님
static void log(String... messages, String level) { ... }

왜 이런 규칙이 있을까요? 자바가 "어디까지가 가변 인자고, 어디부터가 그 다음 인자인지" 구분할 수 있어야 하기 때문이에요. 만약 String... messages 가 중간에 오면 "어디까지 묶어야 할지" 컴파일러가 판단할 방법이 없거든요. 그래서 맨 마지막 이라는 약속을 깔아둔 거예요.

지금 당장 외울 필요는 없어요. 다음 시간 메서드를 본격적으로 다루면서 인자가 여러 개 섞이는 상황이 나올 때 자연스럽게 다시 만날 거예요.

자바 표준 라이브러리에도 varargs 가 들어 있어요

varargs 는 자바를 만든 사람들이 "이거 진짜 편하네" 하고 표준 라이브러리 곳곳에 심어둔 문법이에요. 예를 들어 우리가 매일 쓰는 System.out.printf(...) 도 사실 varargs 를 받고 있어요. Arrays.asList(1, 2, 3) 같이 몇 개든 자유롭게 콤마로 나열하는 호출들 대부분이 내부적으로는 varargs 예요.

이런 친구들은 Day 17 이후 컬렉션 단원에서 본격적으로 다시 만날 예정이에요. 지금은 "점 세 개 문법이 자바 곳곳에 깔려 있구나" 정도만 알아두시면 충분합니다.

🙋 학생 질문 타임"튜터님, varargs 가 결국 배열로 받아진다면, 그냥 처음부터 String[] 으로 받는 거랑 뭐가 다른가요?"

받는 쪽 입장에서는 거의 똑같아요. 안쪽에서 tags.length, tags[0], enhanced for 모두 동일하게 동작합니다. 차이는 호출하는 쪽의 편의 예요. String[] 으로 받으면 호출할 때마다 new String[]{"#카페", "#디저트"} 처럼 배열을 만들어서 넘겨야 해요. varargs 는 그냥 printHashtags("#카페", "#디저트") 콤마로 나열하면 자바가 자동으로 배열을 만들어서 묶어줘요. 결과는 똑같지만 쓰는 사람 손이 덜 가는 디자인이에요. 라이브러리를 만드는 입장에서 "이 메서드는 사용자가 자주, 다양한 개수로 부를 거야" 싶을 때 varargs 를 선택하면 호출 코드가 훨씬 깔끔해집니다.

💡 튜터의 결론

호출할 때 개수가 그때그때 다른 인자 를 받고 싶으면 varargs 를 떠올리세요. 메서드 시그니처에 점 세 개 (...) 만 붙이면 끝이에요. 받는 쪽에서는 배열처럼 다루고, 부르는 쪽은 콤마로 자유롭게 나열하면 됩니다. 다음 시간 메서드를 본격적으로 배우면서 더 깊이 만나볼 예정이에요.

이제 배열의 거의 모든 도구를 익혔어요. 선언과 초기화, 두 가지 순회 방식, 인덱스 안전 패턴, Arrays 유틸리티, 2차원 배열, 그리고 방금 본 varargs 까지. 마지막 Step 에서는 지금까지 배운 모든 걸 동원해서 진짜 인스타 시나리오 를 풀어볼 거예요. 해시태그로 게시물 필터링하고, 좋아요 순위 Top 3 뽑기 — 종합 실습으로 Day 5 를 마무리해봅시다.


Step 7: "인스타 필터 로직을 배열로 구현" — 종합 실습

Step 6 까지 오시느라 고생 많으셨어요. 배열 도구는 이제 거의 다 익혔으니, 이번 Step 에서는 그동안 배운 걸 전부 합쳐서 진짜 인스타 시나리오 하나 를 통째로 풀어볼 거예요.

상상해 볼게요. 여러분이 인스타그램 백엔드 개발자라고 가정해 봅시다. 사용자가 검색창에 #카페 를 입력하면, 해당 해시태그가 들어간 게시물을 찾아서 보여줘야 해요. 그리고 화면 사이드에는 인기 게시물 Top 3 도 함께 노출해야 합니다. 이 두 가지 기능을 오늘 배운 배열만으로 구현해 보는 거예요.

차근차근 풀어볼게요. 먼저 데이터를 어떻게 표현할지부터 정해야 합니다.

평행 배열 (parallel arrays) — 세 배열을 같은 인덱스로 묶는 패턴

게시물 하나를 표현하려면 정보가 여러 개 필요해요. 제목, 좋아요 수, 해시태그 이렇게 세 가지가 한 묶음으로 다녀야 합니다. 그런데 우리는 아직 클래스를 배우지 않았어요. 그러면 어떻게 표현할까요?

방법은 의외로 단순해요. 배열 세 개를 만들고, 같은 인덱스끼리 같은 게시물의 정보 로 약속하는 거예요. 이걸 평행 배열 (parallel arrays) 패턴이라고 불러요. 평행한 선처럼 나란히 달리는 배열 이라는 의미예요.

인덱스 | 제목                | 좋아요 | 해시태그
─────┼────────────────────┼─────┼────────────
 0    | 홍대 카페 투어       | 128  | #카페 #홍대 #주말
 1    | 주말 등산 일기       |  42  | #등산 #자연 #힐링
 2    | 신상 카페 라떼       | 210  | #카페 #라떼 #신상
 3    | 강아지 산책          |  95  | #강아지 #산책 #일상
 4    | 카페에서 책 읽기     |  67  | #카페 #책 #힐링

표를 보시면 감이 오시죠? postTitles[2], postLikes[2], postHashtags[2] 가 모두 세 번째 게시물 의 정보예요. 세 배열이 같은 인덱스를 공유하면서 마치 표의 한 행처럼 동작합니다.

이 패턴엔 한 가지 불편한 점이 있어요. 만약 누군가 postTitles 만 정렬하고 다른 배열은 안 건드리면, 인덱스 동기화가 깨지면서 데이터가 완전히 어긋나 버려요. 카페 투어 의 좋아요가 갑자기 강아지 산책 의 해시태그와 매칭되는 사고가 나는 거죠. 이 불편함은 Day 8 에서 클래스 라는 도구를 배우면서 깔끔하게 풀어드릴 예정이에요. 오늘은 일단 평행 배열로 출발해 봅시다.

미션 1: 해시태그 #카페 필터링

자, 첫 번째 미션이에요. 다섯 개 게시물 중에서 해시태그에 #카페 가 들어간 게시물만 골라내야 합니다. 어떻게 하면 될까요?

생각해보면 단순해요. 배열을 처음부터 끝까지 돌면서, 각 게시물의 해시태그 문자열 안에 #카페 라는 글자가 들어있는지 확인하면 됩니다. 그리고 들어있다면 출력하고 카운터를 하나 올리는 거예요.

여기서 새 도구 하나가 등장합니다. String.contains(...) 예요.

postHashtags[i].contains("#카페")

contains"문자열 안에 특정 글자가 들어있는지 확인" 해주는 String 도구예요. 들어있으면 true, 없으면 false 를 돌려줍니다. "#카페 #홍대 #주말" 이라는 긴 문자열 안에 "#카페" 가 들어있는지 묻는 거죠. 결과는 당연히 true 예요.

String 의 다양한 도구는 Day 17 이후에 본격적으로 다룰 예정이라, 오늘은 "contains 는 글자 포함 여부를 알려주는구나" 정도만 알아두시면 충분합니다.

핵심 코드 흐름은 이래요.

String target = "#카페";
int matched = 0;
for (int i = 0; i < postTitles.length; i++) {
    if (postHashtags[i].contains(target)) {
        matched++;
        System.out.println("  o " + postTitles[i] + " (좋아요 " + postLikes[i] + ")");
    }
}

matched 라는 카운터 변수를 0 으로 시작해서, 매칭될 때마다 ++ 로 하나씩 올려요. 그리고 매칭된 게시물의 제목과 좋아요 수를 같이 출력합니다. 여기서 핵심은 세 배열을 같은 인덱스 i 로 함께 접근 한다는 점이에요. postHashtags[i] 로 검사하고, postTitles[i]postLikes[i] 로 출력해요. 평행 배열의 진가가 여기서 발휘됩니다.

미션 2: 좋아요 Top 3 뽑기 — 원본은 그대로, 사본을 정렬

두 번째 미션은 좋아요 상위 3개 게시물을 뽑는 거예요. "그냥 Arrays.sort(postLikes) 한 줄이면 끝 아닌가요?" 하실 수도 있는데, 여기에 함정이 있어요.

만약 postLikes 원본을 정렬해 버리면 어떻게 될까요? postLikes[0] 의 값이 128 에서 42 로 바뀌어 버려요. 그런데 postTitles[0] 은 여전히 "홍대 카페 투어" 예요. "홍대 카페 투어의 좋아요는 42" 라는 엉터리 데이터가 만들어집니다. 인덱스 동기화가 깨진 거예요.

그래서 우리는 다른 방식을 써야 해요. 원본은 그대로 두고, 복사본을 만들어서 그 복사본만 정렬 하는 거예요. Step 4 에서 배운 Arrays.copyOf 가 여기서 빛을 발합니다.

int[] sortedLikes = Arrays.copyOf(postLikes, postLikes.length);
Arrays.sort(sortedLikes);

첫 줄에서 postLikes 와 똑같은 길이의 사본을 새로 만들어 sortedLikes 라는 새 이름표를 붙였어요. 둘째 줄에서 그 사본만 오름차순으로 정렬합니다. 원본 postLikes 는 여전히 {128, 42, 210, 95, 67} 그대로예요.

이제 정렬된 사본을 살펴보면 {42, 67, 95, 128, 210} 가 되어있어요. 뒤쪽으로 갈수록 큰 값 이죠. 그러면 가장 큰 값은 마지막 인덱스에, 두 번째 큰 값은 그 앞에 있겠네요. 이걸 수식으로 표현하면 이렇게 됩니다.

int topN = 3;
for (int rank = 1; rank <= topN; rank++) {
    int targetLikes = sortedLikes[sortedLikes.length - rank];
    for (int i = 0; i < postLikes.length; i++) {
        if (postLikes[i] == targetLikes) {
            System.out.println("  " + rank + "위. " + postTitles[i] + " (" + targetLikes + ")");
            break;
        }
    }
}

천천히 따라가 볼게요.

  • rank 를 1, 2, 3 으로 돌리면서 "몇 위를 뽑을 차례인지" 추적해요.
  • sortedLikes[sortedLikes.length - rank] 가 핵심이에요. rank=1 일 때 sortedLikes[4] (= 210), rank=2 일 때 sortedLikes[3] (= 128), rank=3 일 때 sortedLikes[2] (= 95). 뒤에서부터 하나씩 거슬러 올라가는 패턴이에요.
  • 안쪽 for 루프는 그 좋아요 값을 키 (key) 삼아 원본 postLikes 를 다시 뒤져요. "이 값이 어느 게시물의 것이지?" 를 찾는 거예요. 매칭되는 인덱스를 찾으면 postTitles[i] 로 제목을 가져옵니다.
  • break 를 잊지 마세요! 매칭을 한 번 찾으면 안쪽 for 를 즉시 빠져나옵니다. Day 4 에서 배운 그 break 가 여기서 다시 등장한 거예요. 만약 break 가 없으면 같은 게시물을 위해 안쪽 루프가 끝까지 도는 낭비가 생겨요.

통합 코드 — 실행해 보기

자, 이제 두 미션을 합쳐서 전체 코드를 실행해 봅시다.

// day05/InstagramFilterDemo.java
import java.util.Arrays;

void main() {
    // 종합 실습 — 게시물 5개를 해시태그로 필터링 + 좋아요 상위 3개 뽑기
    System.out.println("=== 인스타그램 게시물 필터 + 인기 정렬 ===");
    System.out.println();

    // 게시물 5개 — 같은 인덱스끼리 짝지어진 평행 배열
    String[] postTitles = {
            "홍대 카페 투어",
            "주말 등산 일기",
            "신상 카페 라떼",
            "강아지 산책",
            "카페에서 책 읽기"
    };
    int[] postLikes = {128, 42, 210, 95, 67};
    String[] postHashtags = {
            "#카페 #홍대 #주말",
            "#등산 #자연 #힐링",
            "#카페 #라떼 #신상",
            "#강아지 #산책 #일상",
            "#카페 #책 #힐링"
    };

    // 1) 해시태그 #카페 가 포함된 게시물 카운트
    String target = "#카페";
    System.out.println("[필터] " + target + " 가 들어간 게시물 찾기");
    int matched = 0;
    for (int i = 0; i < postTitles.length; i++) {
        // contains 는 문자열 안에 특정 글자가 들어있는지 확인하는 String 도구
        if (postHashtags[i].contains(target)) {
            matched++;
            System.out.println("  o " + postTitles[i] + " (좋아요 " + postLikes[i] + ")");
        }
    }
    System.out.println("총 " + matched + "개 게시물 발견 (전체 " + postTitles.length + "개 중)");
    System.out.println();

    // 2) 좋아요 상위 3개 뽑기 — 원본은 그대로, 복사본을 정렬
    System.out.println("[인기 순위] 좋아요 상위 3개");
    int[] sortedLikes = Arrays.copyOf(postLikes, postLikes.length);
    Arrays.sort(sortedLikes);
    System.out.println("정렬된 좋아요 (오름차순): " + Arrays.toString(sortedLikes));

    int topN = 3;
    System.out.println("Top " + topN + ":");
    for (int rank = 1; rank <= topN; rank++) {
        int targetLikes = sortedLikes[sortedLikes.length - rank];
        // 좋아요 값으로 원본 배열을 다시 뒤져서 제목 찾기
        for (int i = 0; i < postLikes.length; i++) {
            if (postLikes[i] == targetLikes) {
                System.out.println("  " + rank + "위. " + postTitles[i] + " (" + targetLikes + ")");
                break;
            }
        }
    }
}

IntelliJ 에서 실행 버튼을 눌러보시면 이런 결과가 출력돼요.

=== 인스타그램 게시물 필터 + 인기 정렬 ===

[필터] #카페 가 들어간 게시물 찾기
  o 홍대 카페 투어 (좋아요 128)
  o 신상 카페 라떼 (좋아요 210)
  o 카페에서 책 읽기 (좋아요 67)
총 3개 게시물 발견 (전체 5개 중)

[인기 순위] 좋아요 상위 3개
정렬된 좋아요 (오름차순): [42, 67, 95, 128, 210]
Top 3:
  1위. 신상 카페 라떼 (210)
  2위. 홍대 카페 투어 (128)
  3위. 강아지 산책 (95)

잘 동작하네요! 다섯 게시물 중 #카페 가 들어간 세 개를 찾아냈고, 좋아요 Top 3 도 정확히 신상 카페 라떼 (210) → 홍대 카페 투어 (128) → 강아지 산책 (95) 순서로 출력됐어요.

한 가지 한계 — 다음 단계로의 신호

마무리하기 전에 이 코드의 약점 하나 를 짚어드릴게요. 만약 좋아요 수가 똑같은 게시물이 두 개 있으면 어떻게 될까요? 예를 들어 "홍대 카페 투어""카페에서 책 읽기" 둘 다 좋아요가 128 이라면요?

지금 코드는 안쪽 for 에서 break 로 첫 매칭만 잡아요. 그래서 먼저 발견된 게시물 만 Top 에 출력되고, 같은 좋아요 수를 가진 다른 게시물은 무시됩니다. 1위와 2위가 똑같이 "홍대 카페 투어" 가 되는 사고가 날 수도 있어요.

이걸 풀려면 어떻게 해야 할까요? 결국 "게시물 자체를 정렬" 할 수 있어야 해요. 좋아요 값만 따로 정렬하는 게 아니라, 제목·좋아요·해시태그를 한 묶음으로 정렬하는 거예요. 그러려면 데이터 묶음 단위 를 만들 수 있는 도구가 필요한데, 그게 바로 Day 8 에서 만날 클래스 (class) 예요. Post 라는 데이터 묶음을 직접 정의하고 정렬하는 패턴은 그때 본격적으로 다룰 예정입니다.

🙋 학생 질문 타임"튜터님, 평행 배열 말고 한 게시물의 정보를 통째로 묶을 수 있는 방법은 없나요?"

아주 좋은 질문이에요. 결론부터 말씀드리면 있어요. 다음 시간 메서드를 배우면서 여러 값을 한 번에 다루는 함수 의 첫 맛을 보고, Day 8 클래스에서 그 답을 완벽하게 만나게 돼요. Post 라는 데이터 묶음 단위 를 직접 정의해서 제목·좋아요·해시태그 를 한 객체 안에 함께 담아두는 방식이에요. 평행 배열의 인덱스 동기화 부담이 사라지고, "두 번째 게시물의 좋아요" 가 아니라 "posts[1].getLikes()" 같이 자연스러운 문법으로 접근하게 됩니다. 오늘은 그 단계로 가기 위한 기초 체력 을 키운 거라고 생각해 주세요.

💡 튜터의 결론

오늘 배운 배열은 데이터를 줄지어 보관하는 가장 기본 도구 예요. 평행 배열 패턴으로도 꽤 많은 시나리오를 표현할 수 있지만, 데이터가 서로 묶여 다닐 때 는 다음 단계가 필요하다는 신호이기도 해요. 그 신호를 잡고 Day 8 로 향하면 됩니다. 그리고 지금까지 익힌 for, Arrays.copyOf, Arrays.sort, String.contains, break 같은 도구들은 클래스를 만나도 여전히 그대로 쓰여요. 기초가 든든하면 다음 단계가 가벼워집니다.

이렇게 7개 Step 으로 배열의 기본 도구를 전부 살펴봤어요. 변수 50개를 일렬로 묶는 사물함 비유에서 시작해서, 종합 실습으로 인스타 시나리오까지 풀어냈으니 오늘 분량은 충분히 채워졌네요. 다음은 마무리 섹션과 과제로 넘어가서 Day 5 를 깔끔하게 정리해 봅시다.


마무리

오늘 배운 내용을 정리해 볼게요.

  1. 배열의 탄생 — 같은 타입 데이터를 이름표 하나 아래 줄지어 보관. 선언 3가지 ({...} / new T[N] / new T[]{...})
  2. 배열 순회 — 기본 for (인덱스 필요) vs enhanced for (값만 필요)
  3. 인덱스 안전.length 로 경계 잡기, if 가드, length - 1 마지막 인덱스 공식
  4. Arrays 도구 상자toString / sort / copyOf
  5. 2차원 배열int[][] 로 표 형태 데이터, 이중 for[row][col] 순회
  6. 가변 인자 (varargs) — 점 세 개 ... 로 개수 미정 인자를 배열로 받기
  7. 종합 실습 — 해시태그 필터링 + 좋아요 Top 3, 평행 배열 패턴의 한계 확인

Day 2에서 기억하는 법, Day 3에서 판단하는 법, Day 4에서 반복하는 법을 배웠고, 오늘은 그 데이터를 여러 개 묶어서 보관하는 법을 익혔습니다. 기억 + 판단 + 반복 + 묶음 보관. 이제 콘솔에서 미니 인스타 분석기를 돌릴 만한 프로그램을 만들 수 있어요.

그런데 Step 7에서 살짝 본 평행 배열의 한계가 남아 있죠. 게시물 하나에 제목·좋아요·해시태그 세 정보가 따로따로 배열에 들어가 있어서, 정렬 한 번에 인덱스가 어긋날 수 있어요. 이걸 하나의 단위로 묶을 수 있다면 얼마나 편할까? — 그 답은 다음 시간 메서드, 그리고 Day 8 클래스 가 차근차근 준비하고 있어요.

다음 시간에는 메서드 를 배웁니다. 같은 작업을 이름으로 묶어두고 필요할 때마다 부르는 도구예요. 오늘 Step 6에서 살짝 맛본 static void printHashtags(String... tags) 같은 형태가 본격 등장합니다!


과제

오늘 배운 배열의 도구를 직접 손에 익혀볼 시간이에요. 난이도별로 세 개를 준비했어요.

과제 1: [기초] 좋아요 통계 계산기

오늘 첫 Step 에서 배운 1차원 배열 선언과 순회, 그리고 .length 를 활용해서 인스타 게시물의 좋아요 통계를 뽑아보는 과제예요. 어렵지 않으니까 한 번 막힘없이 끝까지 풀어보세요.

시작 데이터:

int[] likes = {42, 128, 7, 95, 210, 33, 67, 150, 88, 60};

10개 게시물의 좋아요 수가 들어 있어요.

출력해야 할 정보:

  1. 총 좋아요 수 (10개 게시물 전부 합한 값)
  2. 평균 좋아요 수 (정수 나눗셈으로 OK)
  3. 최대 좋아요 — 어떤 게시물이 1위인지 같이 출력 ("3번째 게시물이 1위 — 210개" 형태, 인덱스 + 1)
  4. 최소 좋아요 — 어떤 게시물이 꼴찌인지 같이 출력

힌트:

  • 합계는 enhanced for 가 깔끔해요 — 값만 필요하니까요.
  • 최대 / 최소를 찾으면서 동시에 "몇 번째 게시물인지" 도 기억해야 하니, 이때는 기본 for 로 인덱스를 같이 추적하면 편해요.
  • 비교 시작값은 첫 번째 원소 (likes[0]) 로 잡거나, Integer.MIN_VALUE / Integer.MAX_VALUE 로 잡거나 둘 다 OK.

과제 2: [응용] 해시태그 검색기 (대소문자 무시)

Step 7 종합 실습에서 사용했던 평행 배열 패턴을 다시 활용하는 과제예요. 이번에는 검색어를 받아서 매칭되는 게시물 제목을 출력하는 미니 검색기를 만들어 봅시다.

시작 데이터:

String[] postTitles = {"카페에서 책 읽기", "주말 등산", "강아지 산책", "신상 카페 라떼", "OOTD"};
String[] postHashtags = {"#카페 #책 #힐링", "#등산 #자연", "#강아지 #산책", "#카페 #라떼", "#OOTD #패션"};
String query = "#카페";

동작 규칙:

  • query 와 매칭되는 게시물 제목을 모두 출력하세요.
  • 매칭 판정은 postHashtags[i].contains(query) 로 충분해요.
  • 매칭되는 게시물이 0개일 때는 "검색 결과가 없습니다" 를 출력하세요.

힌트:

  • 매칭 개수를 카운터 변수로 세어두면 0개 처리가 쉬워져요.
  • postTitles.lengthpostHashtags.length 가 같다는 전제로 코드를 작성하면 돼요 (평행 배열 패턴).

도전 (선택): 검색어를 두 개로 늘려서, 두 해시태그가 모두 들어 있는 게시물만 골라보세요. 예: #카페 AND #책 → "카페에서 책 읽기" 만 매칭.

과제 3: [심화] 2차원 배열로 인스타 주간 좋아요 히트맵 만들기

Step 5 에서 배운 2차원 배열과 이중 for 를 본격적으로 활용하는 과제예요. 실제 인스타 운영자가 본다고 가정하고, "요일 × 시간대" 의 좋아요 분포를 분석해 봅시다.

시작 데이터:

int[][] heatmap = new int[7][24]; // 7일 × 24시간

배열을 빈 칸으로 두지 말고, 실제 데이터처럼 들쭉날쭉하게 미리 채워두세요. 힌트를 드리자면:

  • 새벽 (0~6시) — 작거나 0
  • 출근 시간대 (7~9시) — 작음
  • 점심 시간대 (12~13시) — 큼
  • 퇴근 / 저녁 (18~22시) — 가장 큼
  • 자정 (23시) — 다시 큼

요일도 평일 / 주말의 차이를 두면 더 재미있어요.

출력해야 할 정보:

  1. 격자 형태로 히트맵 출력 — 위쪽에 시간대 (0~23), 왼쪽에 요일 (월~일)
  2. 각 요일의 총 좋아요 (가로 합계 7개)
  3. 각 시간대의 총 좋아요 (세로 합계 24개)
  4. 가장 좋아요가 많이 찍힌 요일 + 시간 좌표 (예: "수요일 21시 — 320개")

힌트:

  • 격자 출력은 System.out.printf("%4d", value) 같이 자릿수를 맞춰주면 깔끔해요.
  • 행 / 열 두 방향 합계는 각각 별도의 1차원 배열에 누적시키면 편해요.
  • 최대값을 추적할 때 bestRow, bestCol 두 변수를 같이 갱신하면 좌표를 잃지 않아요.

도전 (선택): 시간대 합계 24개 중에서 가장 인기 있는 시간대 Top 3 를 출력해 보세요. Arrays.copyOf 로 복사본을 만든 뒤 Arrays.sort 로 정렬하면 큰 값 세 개를 뽑을 수 있어요. 정렬 후에는 원본과 비교해서 "몇 시인지" 를 역추적해야 하는데, 이 부분이 진짜 어려운 도전 과제입니다.


답안은 day05-answers.md 에 따로 준비되어 있어요. 본인 코드를 먼저 작성한 뒤 비교해보면 학습 효과가 큽니다.


생각해볼 주제

오늘 배운 배열 너머의 이야기를 두 가지 던져드릴게요. 정답이 정해진 질문이 아니라, 직접 생각해보고 본인의 답을 만들어보세요.

1. "배열의 크기를 나중에 늘릴 수 있다면?"

오늘 배운 배열은 한 번 만들면 크기가 고정 됩니다. new int[5] 로 5칸 짜리를 만들었으면, 그 배열은 평생 5칸이에요. 6번째 원소를 넣을 수 없죠.

그런데 인스타 게시물은 사용자가 새 글을 올릴 때마다 늘어나는 데이터예요. 오늘 게시물이 100개였다가, 한 시간 뒤에 101개가 되고, 또 한 시간 뒤에 105개가 되겠죠. 매번 new int[새로운크기] 로 새 배열을 만들고 값을 다 복사하는 방식 외에 다른 방법이 있을까요?

자바가 이 문제를 해결하기 위해 어떤 도구를 미리 준비해뒀을지 추측해 보세요. 그리고 그 도구가 내부적으로 어떻게 동작할지도 한번 상상해 보세요. (힌트: Day 17 즈음에 그 답을 만나게 됩니다)

2. "평행 배열 vs 하나의 묶음 — 어느 쪽이 더 좋을까?"

Step 7 종합 실습에서 게시물 5개를 세 개의 평행 배열 (postTitles, postLikes, postHashtags) 로 표현했어요. 세 배열이 같은 인덱스를 공유한다는 약속만 잘 지키면 동작은 하지만, 정렬할 때나 게시물 하나를 통째로 다른 함수에 넘길 때 좀 불편했죠.

만약 게시물 한 개의 제목 · 좋아요 · 해시태그하나의 묶음 으로 표현할 수 있다면 어떤 점이 편해질까요? 반대로 세 배열로 나눠두는 것이 더 좋은 상황은 없을까요? (예: 좋아요만 빠르게 합계 내고 싶을 때)

묶음으로 만드는 방법은 Day 8 클래스에서 본격적으로 배워요. 그 전에 두 방식의 장단점을 미리 머릿속에 그려보면, 클래스를 만났을 때 왜 필요한지 가 훨씬 잘 와닿을 거예요.

3. (선택, 짧게) "왜 인덱스는 0번 부터일까?"

Step 1 에서 0번부터 시작하는 이유를 메모리 주소 기반으로 살짝 설명했어요. 그런데 일상에서 "첫 번째" 라고 하면 보통 1부터 세잖아요. 실제로 1부터 시작하는 프로그래밍 언어도 있어요 — Lua, MATLAB, R 같은 언어들이 1 기반 인덱스를 써요.

0 기반과 1 기반은 각각 어떤 장단점이 있을까요? 세 가지 관점에서 생각해 보세요.

  • 수학적 일관성 — 메모리 주소 계산이나 "i 번째 원소까지의 거리" 같은 수식이 어느 쪽에서 더 깔끔한가
  • 프로그래머 직관 — 1부터 시작하는 일상 감각과 비교했을 때 학습 부담이 어느 쪽이 큰가
  • 과거 호환성 — C 언어 시절부터 자리잡은 관습이 자바·파이썬·자바스크립트로 이어진 흐름

정답이 있는 질문은 아니에요. 본인이 코드를 짜면서 느낀 점을 기준으로 본인만의 답을 만들어 보세요.

✅ 예시 답안정답 보기

본인 답안을 먼저 작성한 뒤 비교해보세요. 정답이 하나만 있는 건 아니에요. 여기 답안은 모범 사례 중 하나일 뿐, 본인만의 더 깔끔한 풀이가 있다면 그게 답입니다.


🎯 [과제 1 예시 답안] 좋아요 통계 계산기

10개 게시물의 좋아요 수에서 총합 / 평균 / 최대 (1위 인덱스) / 최소 (꼴찌 인덱스) 를 뽑아내는 과제입니다.

핵심 접근

값만 필요한 합계는 enhanced for 가 깔끔하고, 최대 / 최소는 "어느 게시물인지" 인덱스도 같이 기억해야 하니까 기본 for 가 자연스럽습니다. 비교 시작값은 첫 원소 likes[0] 로 잡는 게 가장 안전해요 (Integer.MIN_VALUE / MAX_VALUE 도 OK 지만, 빈 배열 처리는 어차피 별도 가드가 필요합니다).

예시 코드

// day05/extra/LikeStatistics.java  (학습 실습용 — 코드베이스 외부에서 작성 OK)
void main() {
    int[] likes = {42, 128, 7, 95, 210, 33, 67, 150, 88, 60};

    // 1) 총합 — 값만 필요하니까 enhanced for 가 자연스러워요
    int sum = 0;
    for (int like : likes) {
        sum += like;
    }

    // 2) 평균 — 정수 나눗셈으로 OK (소수점은 다음에 String.format 배우면 깔끔)
    int average = sum / likes.length;

    // 3) 최대 / 최소 + 인덱스 — 첫 원소를 기준으로 시작
    int maxLike = likes[0];
    int maxIndex = 0;
    int minLike = likes[0];
    int minIndex = 0;

    for (int i = 1; i < likes.length; i++) {
        if (likes[i] > maxLike) {
            maxLike = likes[i];
            maxIndex = i;
        }
        if (likes[i] < minLike) {
            minLike = likes[i];
            minIndex = i;
        }
    }

    // 4) 결과 출력
    System.out.println("=== 좋아요 통계 ===");
    System.out.println("총 좋아요: " + sum + "개");
    System.out.println("평균 좋아요: " + average + "개");
    System.out.println((maxIndex + 1) + "번째 게시물이 1위 — " + maxLike + "개");
    System.out.println((minIndex + 1) + "번째 게시물이 꼴찌 — " + minLike + "개");
}

실행 결과:

=== 좋아요 통계 ===
총 좋아요: 880개
평균 좋아요: 88개
5번째 게시물이 1위 — 210개
3번째 게시물이 꼴찌 — 7개

채점 포인트

포인트 설명 배점 가중
.length 활용 매직 넘버 10 대신 likes.length 로 평균 계산
두 루프의 의도 분리 합계는 enhanced for, 최대 / 최소 + 인덱스는 기본 for
인덱스 초기값 처리 likes[0] 으로 시작해 i = 1 부터 비교 (또는 i = 0 부터 시작해도 OK, 단 한 번의 자기 비교는 무해)
사람 친화 출력 "3번째" 처럼 maxIndex + 1 로 1 기반 표시
두 갱신을 한 루프에 묶기 최대 / 최소를 같은 for 안에서 동시에 추적

흔한 실수

  • max = 0 으로 초기화 → 음수 데이터가 들어오면 끝까지 갱신되지 않아 결과가 0 이 되어버려요. 첫 원소 또는 Integer.MIN_VALUE 로 시작하는 게 안전합니다.
  • 인덱스 출력에 + 1 누락 → 학생은 "5번째" 라고 기대하는데 "4번째" 가 나옵니다. 0 기반 ↔ 1 기반 변환은 출력 직전 한 번만.
  • 평균 계산을 sum / 10 으로 하드코딩 → 데이터 개수가 바뀌면 같이 깨집니다. 배열 크기는 항상 .length 로.
  • enhanced for 안에서 인덱스가 필요해서 외부 카운터 변수를 추가 → 이건 기본 for 를 쓰라는 신호예요. enhanced for 는 "인덱스 필요 없을 때" 의 도구입니다.

실무 개선 포인트 (심화)

  • 빈 배열 가드if (likes.length == 0) { ... } 한 줄을 위에 두면 likes[0] 접근에서 ArrayIndexOutOfBoundsException 이 터지는 사고를 막을 수 있어요. 이번 과제는 데이터가 고정이라 문제없지만, 실제 운영 데이터에선 0건이 정말 자주 들어옵니다.
  • 동률 처리 — 최대값이 두 게시물에 동시에 있다면? 지금 코드는 먼저 만난 쪽 만 1위로 잡습니다. "1위가 2개입니다" 처럼 동률을 알려주려면 maxLike 갱신 후에 다시 한 번 같은 값을 가진 인덱스를 모두 출력하는 두 번째 루프가 필요해요. 다음 시간에 배울 메서드 로 분리하면 깔끔해집니다.

🎯 [과제 2 예시 답안] 해시태그 검색기

평행 배열 두 개 (postTitles, postHashtags) 에서 query 와 매칭되는 게시물 제목을 모두 출력하고, 0개일 때는 "검색 결과가 없습니다" 를 보여주는 과제입니다.

핵심 접근

세 단계입니다 — (1) 카운터 변수 0 으로 시작 → (2) 기본 for 로 인덱스 i 를 돌면서 postHashtags[i].contains(query) 체크 → (3) 루프 종료 후 카운터가 0 이면 안내 메시지. 평행 배열의 핵심인 "같은 인덱스로 함께 접근" 패턴을 지키는 것이 중요해요.

예시 코드

// day05/extra/HashtagSearch.java  (학습 실습용)
void main() {
    String[] postTitles = {
            "카페에서 책 읽기", "주말 등산", "강아지 산책",
            "신상 카페 라떼", "OOTD"
    };
    String[] postHashtags = {
            "#카페 #책 #힐링", "#등산 #자연", "#강아지 #산책",
            "#카페 #라떼", "#OOTD #패션"
    };
    String query = "#카페";

    System.out.println("=== '" + query + "' 검색 결과 ===");

    int matched = 0;
    for (int i = 0; i < postTitles.length; i++) {
        if (postHashtags[i].contains(query)) {
            System.out.println("  - " + postTitles[i] + "  (" + postHashtags[i] + ")");
            matched++;
        }
    }

    if (matched == 0) {
        System.out.println("검색 결과가 없습니다");
    } else {
        System.out.println("총 " + matched + "개 게시물");
    }
}

실행 결과:

=== '#카페' 검색 결과 ===
  - 카페에서 책 읽기  (#카페 #책 #힐링)
  - 신상 카페 라떼  (#카페 #라떼)
총 2개 게시물

도전 (선택) — 두 검색어 모두 들어 있는 게시물만

// day05/extra/HashtagSearchAnd.java
void main() {
    String[] postTitles = {
            "카페에서 책 읽기", "주말 등산", "강아지 산책",
            "신상 카페 라떼", "OOTD"
    };
    String[] postHashtags = {
            "#카페 #책 #힐링", "#등산 #자연", "#강아지 #산책",
            "#카페 #라떼", "#OOTD #패션"
    };
    String queryA = "#카페";
    String queryB = "#책";

    System.out.println("=== '" + queryA + "' AND '" + queryB + "' 검색 ===");

    int matched = 0;
    for (int i = 0; i < postTitles.length; i++) {
        // && 단축 평가로 두 조건이 모두 참일 때만 매칭
        if (postHashtags[i].contains(queryA) && postHashtags[i].contains(queryB)) {
            System.out.println("  - " + postTitles[i]);
            matched++;
        }
    }

    if (matched == 0) {
        System.out.println("검색 결과가 없습니다");
    }
}

실행 결과:

=== '#카페' AND '#책' 검색 ===
  - 카페에서 책 읽기

채점 포인트

포인트 설명 배점 가중
평행 배열 동기 접근 postHashtags[i].contains(...) 로 체크하고 postTitles[i] 로 출력 — 같은 i 공유
0개 처리 matched 카운터가 0 이면 "검색 결과가 없습니다" 출력
루프 길이 기준 postTitles.length 사용 (하드코딩 5 금지)
String.contains 정확한 사용 postHashtags[i].contains(query) — 인자 방향 헷갈리지 않기
AND 검색 단축 평가 도전 과제에서 && 로 두 조건을 한 줄에 묶었는지

흔한 실수

  • contains 의 인자 방향 반대query.contains(postHashtags[i]) 로 쓰면 "#카페" 안에 "#카페 #책 #힐링" 이 들어 있는지" 를 묻는 꼴이라 항상 false 가 나와요. 문자열에서 부분을 찾는다 가 자연스러운 방향입니다.
  • 0개 처리를 빠뜨림 → 매칭이 하나도 없으면 출력이 헤더 한 줄만 나와서 사용자가 혼란스러워요. matched == 0 분기는 검색 기능의 필수 안전망입니다.
  • enhanced for 로 시작했다가 인덱스가 필요해서 곤란postHashtags 만 순회하면 postTitles[i] 를 못 가져옵니다. 평행 배열은 기본 for 가 정석.
  • 대소문자 차이 처리 안 함 — 이번 데이터는 모두 일치하지만, 사용자가 "#카페" 대신 "카페" 로 쳤다면? 실무에선 toLowerCase() 로 양쪽을 맞춰주는 게 일반적이에요.

실무 개선 포인트 (심화)

  • 검색 결과 정렬 — 좋아요 순으로 보여주고 싶으면? postLikes 까지 평행 배열로 같이 들고 와서, 매칭된 인덱스만 따로 모아 정렬해야 해요. 그런데 이걸 평행 배열로 하려면 "매칭된 인덱스 배열" + "매칭된 좋아요 배열" 두 개를 새로 만들어야 합니다. 클래스를 배우고 나면 Post 객체 한 묶음으로 정렬할 수 있어서 훨씬 깔끔해져요 (Day 8 미리보기).
  • 부분 일치 vs 정확 일치contains("#카페")"#카페라떼" 라는 해시태그에도 매칭됩니다 (의도와 다를 수 있음). 해시태그 단위로 정확히 매칭하려면 split(" ") 로 쪼개서 equals 비교를 해야 하는데, 이건 다음 시간에 배울 메서드 로 분리하면 깔끔합니다.

🎯 [과제 3 예시 답안] 주간 좋아요 히트맵 (2차원 배열)

int[7][24] 2차원 배열을 채워두고, 격자 출력 + 요일 합계 + 시간대 합계 + 가장 인기 좌표 (요일+시간) 를 뽑는 종합 과제입니다.

핵심 접근

이중 for 가 핵심이에요. (1) 행 / 열 합계는 1차원 배열 두 개에 누적 시키면서 동시에 진행하고, (2) 최대값 추적은 bestRow, bestCol, bestValue 세 변수를 같이 갱신 합니다. 출력 정렬은 printf("%4d", value) 로 자릿수를 맞춰주면 격자가 깔끔하게 나와요.

예시 코드

// day05/extra/WeeklyHeatmap.java  (학습 실습용)
void main() {
    String[] dayNames = {"월", "화", "수", "목", "금", "토", "일"};
    int[][] heatmap = new int[7][24];

    // 시간대 패턴을 미리 채워둡니다 (요일 가중치는 평일/주말 다르게)
    for (int row = 0; row < 7; row++) {
        boolean weekend = (row == 5 || row == 6);
        int weight = weekend ? 15 : 10;
        for (int col = 0; col < 24; col++) {
            int base;
            if (col >= 0 && col <= 6)        base = 2;   // 새벽
            else if (col >= 7 && col <= 9)   base = 5;   // 출근
            else if (col >= 12 && col <= 13) base = 18;  // 점심
            else if (col >= 18 && col <= 22) base = 25;  // 저녁 (가장 큼)
            else if (col == 23)              base = 20;  // 자정 근처
            else                             base = 8;   // 그 외
            heatmap[row][col] = base * weight;
        }
    }

    // 1) 격자 출력 — 위쪽 헤더 (시간대), 왼쪽 헤더 (요일)
    System.out.print("    ");
    for (int col = 0; col < 24; col++) {
        System.out.printf("%4d", col);
    }
    System.out.println();

    for (int row = 0; row < 7; row++) {
        System.out.print(dayNames[row] + "   ");
        for (int col = 0; col < 24; col++) {
            System.out.printf("%4d", heatmap[row][col]);
        }
        System.out.println();
    }

    // 2) 요일 합계 + 3) 시간대 합계 + 4) 최대 좌표 — 한 번의 순회로 모두 처리
    int[] rowSum = new int[7];
    int[] colSum = new int[24];
    int bestValue = heatmap[0][0];
    int bestRow = 0;
    int bestCol = 0;

    for (int row = 0; row < 7; row++) {
        for (int col = 0; col < 24; col++) {
            int value = heatmap[row][col];
            rowSum[row] += value;
            colSum[col] += value;
            if (value > bestValue) {
                bestValue = value;
                bestRow = row;
                bestCol = col;
            }
        }
    }

    // 요일 합계 출력
    System.out.println();
    System.out.println("=== 요일별 총 좋아요 ===");
    for (int row = 0; row < 7; row++) {
        System.out.println(dayNames[row] + "요일: " + rowSum[row]);
    }

    // 시간대 합계 출력
    System.out.println();
    System.out.println("=== 시간대별 총 좋아요 ===");
    for (int col = 0; col < 24; col++) {
        System.out.println(col + "시: " + colSum[col]);
    }

    // 최대 좌표 출력
    System.out.println();
    System.out.println("=== 가장 인기 있는 시점 ===");
    System.out.println(dayNames[bestRow] + "요일 " + bestCol + "시 — " + bestValue + "개");
}

실행 결과 (발췌 — 격자 + 핵심 결과):

       0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23
월     20  20  20  20  20  20  20  50  50  50  80  80 180 180  80  80  80  80 250 250 250 250 250 200
화     20  20  20  20  20  20  20  50  50  50  80  80 180 180  80  80  80  80 250 250 250 250 250 200
...
토     30  30  30  30  30  30  30  75  75  75 120 120 270 270 120 120 120 120 375 375 375 375 375 300
일     30  30  30  30  30  30  30  75  75  75 120 120 270 270 120 120 120 120 375 375 375 375 375 300

=== 요일별 총 좋아요 ===
월요일: 2840
...
토요일: 4260
일요일: 4260

=== 시간대별 총 좋아요 ===
0시: 170
...
20시: 2125
...

=== 가장 인기 있는 시점 ===
토요일 18시 — 375개

도전 (선택) — 시간대 Top 3

// 위 코드에서 colSum[24] 가 채워져 있다고 가정
int[] sortedHours = Arrays.copyOf(colSum, colSum.length);
Arrays.sort(sortedHours); // 오름차순 정렬

System.out.println("=== 시간대 Top 3 ===");
// 정렬된 배열의 뒤쪽 세 개가 가장 큰 값
for (int rank = 0; rank < 3; rank++) {
    int targetValue = sortedHours[sortedHours.length - 1 - rank];

    // 원본 colSum 에서 같은 값의 인덱스 (= 시간대) 를 역추적
    for (int hour = 0; hour < colSum.length; hour++) {
        if (colSum[hour] == targetValue) {
            System.out.println("  " + (rank + 1) + "위. " + hour + "시 — " + targetValue);
            break; // 첫 매칭만 출력 (동률 처리는 별도 로직 필요)
        }
    }
}

채점 포인트

포인트 설명 배점 가중
이중 for 의 행 / 열 의도 바깥 = 행 (요일), 안 = 열 (시간) — 의미 단위로 변수명도 row, col
합계 1차원 배열 두 개 rowSum[7], colSum[24] 로 동시 누적
최대 좌표 3 변수 동시 갱신 bestValue, bestRow, bestCol 을 같은 if 안에서 함께 갱신
printf("%4d", v) 자릿수 정렬 격자가 흐트러지지 않게 4자리 폭 고정
한 번의 순회로 합계 + 최대 동시 처리 행 합 / 열 합 / 최대 추적을 하나의 이중 for 안에서 마무리
데이터 채우기에 의미 부여 평일 / 주말 차이, 시간대별 가중치 — 단순 난수 대신 패턴
도전: Arrays.copyOf + Arrays.sort 원본 보존 후 사본 정렬 → 역추적으로 시간대 복원

흔한 실수

  • 행 / 열 크기 헷갈림new int[7][24] 인데 안쪽 루프를 col < 7 로 쓰면 인덱스가 어긋나서 ArrayIndexOutOfBoundsException 또는 데이터가 비어버려요. heatmap.length (행 수) 와 heatmap[0].length (열 수) 를 쓰면 안전합니다.
  • 격자 출력에서 자릿수 안 맞춤print(v + " ") 만 쓰면 두 자리 / 세 자리가 섞일 때 격자가 들쭉날쭉해요. printf("%4d", v) 한 줄로 깔끔.
  • 합계와 최대를 따로 두 번 순회 — 동작은 하지만 7 × 24 = 168 칸을 두 번 도는 셈이에요. 한 번의 이중 for 안에서 같이 처리하면 코드도 짧고 속도도 빨라집니다.
  • 도전 과제에서 원본을 정렬해버림 → 원본 colSum 의 순서가 깨져서 "몇 시인지" 를 역추적할 수 없게 돼요. Arrays.copyOf 로 사본을 만든 뒤 정렬 — 이게 핵심입니다.

실무 개선 포인트 (심화)

  • 데이터 출처를 외부 입력으로 — 지금은 코드 안에 시간대 가중치를 직접 넣어뒀지만, 실제론 DB 또는 파일에서 좋아요 이벤트 로그를 읽어와 채워 넣어야 해요. 다음 시간에 배울 메서드"히트맵을 채우는 책임" 을 분리하면, 입력 소스가 바뀌어도 출력 코드는 그대로 둘 수 있습니다.
  • 동률 처리 — Top 3 에서 같은 값이 여러 시간대에 동시에 있다면? 지금 코드는 break 로 첫 매칭만 잡습니다. 정확한 Top 3 를 원하면 "이미 출력한 인덱스" 를 추적하는 boolean 배열이 필요해요. 이런 인덱스 추적은 Day 17 에서 배울 컬렉션 (List, Set) 으로 옮기면 한결 자연스러워집니다.

💡 [생각해볼 주제 1 예시 답안] "배열의 크기를 나중에 늘릴 수 있다면?"

문제 상황 요약

배열은 한 번 만들면 크기가 고정됩니다. new int[5] 는 평생 5칸이에요. 그런데 인스타 게시물처럼 늘어나는 데이터 는 매번 새 배열을 만들고 복사해야 할까요? 자바가 이 문제를 어떻게 해결했을지 추측해보는 질문입니다.

튜터의 가이드 및 해설

핵심은 "고정 배열을 안에 두고, 가득 차면 더 큰 배열로 갈아탄다" 는 아이디어입니다.

자바가 진짜로 이 문제를 풀어둔 도구의 이름은 ArrayList 예요 (Day 17 에서 본격 학습). 하지만 그 내부 동작은 결국 배열입니다. 어떻게 가능할까요?

Option A — 매번 크기 +1 새 배열 만들기 (가장 단순)

[1, 2, 3] 에 4 를 추가
→ new int[4] 만들고 [1, 2, 3] 복사 + 마지막에 4 넣기

추가할 때마다 매번 모든 원소를 복사 합니다. 100개가 있으면 101번째 추가에 100번 복사. 1000번 추가하면 누적 복사 횟수가 50만 번. 너무 느려요.

Option B — 미리 큰 배열을 잡아두고, 가득 차면 두 배로 늘리기 (자바의 실제 전략)

크기 4 짜리 내부 배열로 시작 (실제 원소는 0개)
→ 추가 1, 2, 3, 4 — 4번째까진 그냥 채우기
→ 5번째 추가 시: 크기 8 배열을 새로 만들어 기존 4개 복사 후 5 넣기
→ 그다음 6, 7, 8 까진 그냥 채우기
→ 9번째에 또 두 배로 (16) ...

복사가 일어나는 시점이 점점 드물어집니다. 매번 복사하는 게 아니라 "가득 찼을 때만" 늘리고, "두 배씩" 늘리니까 1000개 추가에 복사 횟수가 약 10번 정도로 줄어요. 이걸 분할 상환 분석 (amortized analysis) 이라고 부르는데, 평균적으로는 "추가 1번 = 거의 일정한 비용" 으로 떨어집니다.

현업에서는 보통: ArrayList 가 사실상 동적 배열의 표준이에요. 다만 "몇 개 들어올지 미리 알고 있다면" 초기 용량을 지정 (new ArrayList<>(1000)) 해주는 게 좋습니다. 안 그러면 처음에 작은 배열로 시작해서 여러 번 복사하느라 시간을 낭비해요.

🎯 면접관을 홀리는 핵심 멘트

"동적 배열의 핵심은 '가득 찰 때마다 두 배로 늘리기' 입니다. 매번 +1 씩 늘리면 매 추가마다 전체를 복사해야 해서 O(n²) 인데, 두 배로 늘리면 평균 O(1) 로 떨어집니다. 자바의 ArrayList 가 정확히 이 전략을 쓰고, 내부에 평범한 배열을 들고 있다가 가득 차면 새 배열로 옮기는 식이에요."


💡 [생각해볼 주제 2 예시 답안] "평행 배열 vs 하나의 묶음 — 어느 쪽이 더 좋을까?"

문제 상황 요약

Step 7 에서 게시물 5개를 세 개의 평행 배열 (postTitles, postLikes, postHashtags) 로 표현했어요. 같은 인덱스를 공유한다는 약속만 잘 지키면 동작하지만, 정렬하거나 게시물 하나를 통째로 다른 곳에 넘길 때 불편했죠. 하나의 묶음으로 만들면 뭐가 좋아질까요?

튜터의 가이드 및 해설

핵심은 "같이 움직이는 데이터는 같이 묶어두는 게 자연스럽다" 는 직관입니다.

Option A — 평행 배열

String[] postTitles  = {"카페", "등산", ...};
int[]    postLikes   = {128,   42,   ...};
String[] postHashtags = {"#카페", "#등산", ...};

장점:

  • "좋아요만 빠르게 합계 내고 싶다" 같은 시나리오에서는 postLikes 배열만 순회하면 끝 — 다른 데이터가 끼어들지 않아요. CPU 캐시 친화적이라 빠릅니다.
  • 단순 데이터 처리 스크립트에서는 가장 가볍습니다.

단점:

  • 세 배열의 길이 / 순서를 수동으로 맞춰야 합니다. 하나만 정렬하거나 삭제하면 즉시 데이터가 깨져요.
  • "게시물 1개를 다른 메서드에 넘기기" 가 불가능해요. 세 변수를 따로따로 인자로 넘겨야 하는데, 인자가 5개 / 10개로 늘어나면 코드가 추해집니다.

Option B — 하나의 묶음 (Day 8 에서 배울 클래스)

// 이런 모양으로 묶을 수 있어요 (다음에 배웁니다)
Post post = new Post("카페에서 책 읽기", 128, "#카페 #책");
Post[] posts = { post1, post2, post3, ... };

장점:

  • 세 정보가 하나의 게시물 이라는 의미가 코드에 그대로 드러납니다.
  • 정렬할 때 Post 단위로 통째로 위치가 바뀌니까 데이터 동기화가 안 깨져요.
  • 게시물 한 개를 다른 함수에 넘길 때 Post 한 변수만 넘기면 됩니다.

단점:

  • 좋아요만 따로 빠르게 합계 내는 시나리오에서는 약간 비효율 (각 Post 안의 likes 만 꺼내야 하니까 메모리 접근 패턴이 흩어집니다).
  • 클래스 정의라는 약간의 "준비 작업" 이 필요해요.

현업에서는 보통: 묶음 (클래스) 이 압도적인 디폴트 입니다. 평행 배열은 "동작은 하지만 의도가 드러나지 않는 코드" 의 대표적인 안티패턴이에요. 다만 수치 연산이 극도로 무거운 영역 (게임 엔진, 머신러닝) 에서는 일부러 평행 배열을 쓰는 데이터 지향 설계 (Data-Oriented Design) 라는 기법도 있습니다. 좋아요 100만 개를 1ms 안에 합산해야 한다면 평행 배열이 유리해요.

🎯 면접관을 홀리는 핵심 멘트

"데이터를 묶을지 흩어둘지의 기준은 '같이 움직이는가' 입니다. 게시물의 제목·좋아요·해시태그처럼 함께 생성되고 함께 정렬되는 데이터는 하나의 객체로 묶어야 안전합니다. 반대로 대량 수치 연산처럼 한 필드만 반복 접근 하는 패턴에서는 평행 배열이 캐시 친화적이라 더 빠를 수 있어요. 일반 비즈니스 로직은 무조건 묶음, 성능 핫스팟은 측정 후 선택입니다."


💡 [생각해볼 주제 3 예시 답안] "왜 인덱스는 0번 부터일까?"

문제 상황 요약

일상에선 "첫 번째" 라고 하면 1부터 세는데, 배열은 arr[0] 부터 시작합니다. 그런데 Lua, MATLAB, R 같은 언어는 1 기반이에요. 0 기반과 1 기반은 각각 어떤 장단점이 있을까요?

튜터의 가이드 및 해설

세 관점으로 나눠 생각해볼게요.

1. 수학적 일관성 — 0 기반이 이긴다

배열의 메모리 주소 계산은 "시작 주소 + (인덱스 × 원소 크기)" 입니다.

0 기반: arr[0] = base + 0 × 4
        arr[3] = base + 3 × 4
1 기반: arr[1] = base + (1 - 1) × 4    ← -1 보정이 매번 끼어듭니다
        arr[3] = base + (3 - 1) × 4

"i 번째 원소까지의 거리" 같은 수식도 0 기반이 깔끔해요. 다익스트라가 "왜 0 부터 세어야 하는가" 라는 유명한 메모를 1982년에 남겼는데, 거기서도 이 수학적 일관성을 가장 큰 이유로 꼽았어요.

2. 프로그래머 직관 — 1 기반이 이긴다 (특히 초보 단계)

비전공자가 처음 배열을 만났을 때 "왜 5번째 원소가 arr[4] 인가요?" 가 가장 큰 장벽 중 하나예요. 일상 감각이랑 어긋나니까요. 사용자에게 보여주는 출력에서 index + 1 보정이 자주 등장하는 것도 이 이유고요 (오늘 과제 1 의 "5번째 게시물" 출력처럼).

MATLAB, R 같은 과학 계산 언어 들이 1 기반을 채택한 건 사용자가 프로그래머가 아니라 수학자 / 통계학자 이기 때문이에요. "행렬의 첫 행 = 1 행" 이 자연스러운 분야예요.

3. 과거 호환성 — 0 기반의 결정적 승리

C 언어가 1972 년에 0 기반을 채택한 뒤, C 의 후예 (C++, Java, JavaScript, Python, Go, Rust ...) 가 모두 따라갔어요. 지금 와서 1 기반으로 바꾸면 수십억 줄의 기존 코드 가 모두 깨집니다. 기술적으로 더 나은가와 별개로, 생태계 호환성이 0 기반을 사실상의 표준으로 굳혔어요.

현업에서는 보통: 비즈니스 로직 코드는 0 기반 그대로 두고, 사용자에게 보여주는 출력에서만 +1 보정 해줍니다. 변환은 출력 직전에 단 한 번만 — 중간 로직에 1 기반이 섞이면 어디는 0 기반이고 어디는 1 기반인지 헷갈려서 사고가 납니다.

🎯 면접관을 홀리는 핵심 멘트

"0 기반은 메모리 주소 계산이 깔끔하고 C 부터 이어진 호환성 자산이 큽니다. 1 기반은 일상 감각에 가깝지만 보정 비용이 매번 끼어들어요. 실무에선 내부 로직은 0 기반 그대로 두고, 사용자에게 보여줄 때만 +1 변환을 출력 직전에 한 번만 적용합니다. 변환 위치를 경계 에 모으는 게 핵심이에요."


답안은 모범 사례 중 하나 일 뿐, 본인만의 더 깔끔한 풀이가 있다면 그게 답이에요. 다음 시간엔 메서드를 배우면서 이 답안의 중복 코드를 더 깔끔하게 재구성 하는 법까지 다뤄볼게요.

더 배우려면

실무 프로젝트까지 가고 싶다면

팀스파르타 백엔드 부트캠프에서 인스타그램 클론을 풀스택으로 완성합니다.