본문 바로가기

2차원 배열과 향상된 for

행과 열로 가입자 수를 배치하고 가변 길이 행을 안전하게 순회하며 지역별 가입 현황을 집계합니다.

한 지역의 채널별 가입자 수는 1차원 배열로 충분하지만, 여러 지역을 함께 비교하려면 행과 열이 있는 표가 편합니다.

자바의 2차원 배열은 거대한 한 덩어리 표가 아니라 배열을 원소로 가지는 배열입니다.

이 구조를 이해하면 직사각형 표뿐 아니라 행마다 길이가 다른 배열도 안전하게 다룰 수 있습니다.


2차원 배열의 구조

src/RectangularGrid.java
public final class RectangularGrid {
    public static void main(String[] args) {
        int[][] signupCounts = new int[2][3];

        signupCounts[0][0] = 20;
        signupCounts[0][1] = 30;
        signupCounts[0][2] = 40;
        signupCounts[1][0] = 15;
        signupCounts[1][1] = 25;
        signupCounts[1][2] = 35;

        for (int row = 0; row < signupCounts.length; row++) {
            for (int column = 0; column < signupCounts[row].length; column++) {
                System.out.print(signupCounts[row][column] + " ");
            }
            System.out.println();
        }
    }
}
20 30 40
15 25 35

signupCounts.length는 바깥 배열의 길이, 즉 행 수 2입니다.

signupCounts[row].length는 선택한 행의 안쪽 배열 길이, 즉 그 행의 열 수 3입니다.

signupCounts[1][2]를 읽을 때 먼저 둘째 행 배열을 고르고, 그 배열의 셋째 원소를 고릅니다.

인덱스는 두 단계 모두 0부터 시작합니다.

다음 두 선언은 같은 직사각형 모양을 만듭니다.

int[][] created = new int[2][3];
int[][] literal = {
        {0, 0, 0},
        {0, 0, 0}
};

그러나 변수 created가 직접 숫자 여섯 개를 담는다고 생각하면 안 됩니다.

바깥 배열은 두 개의 int[] 참조를 담고, 각 참조가 길이 3의 숫자 배열을 가리킵니다.

그래서 행 하나를 다른 배열로 교체할 수도 있습니다.

created[1] = new int[] {7, 8};

이 순간 전체 모양은 첫 행 길이 3, 둘째 행 길이 2인 가변 길이 배열이 됩니다.


가변 길이 행

lab/RaggedArrayBug.java
public final class RaggedArrayBug {
    public static void main(String[] args) {
        int[][] signupCounts = {
                {20, 30, 40},
                {15},
                {25, 35}
        };

        for (int row = 0; row < signupCounts.length; row++) {
            for (int column = 0; column < signupCounts[0].length; column++) {
                System.out.println(signupCounts[row][column]);
            }
        }
    }
}
실패 관찰
20
30
40
15
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1

안쪽 반복 조건이 늘 signupCounts[0].length를 사용해 첫 행의 길이 3을 모든 행에 강요했습니다.

둘째 행에는 원소가 하나뿐이므로 column이 1이 되는 순간 예외가 납니다.

현재 행을 선택한 뒤 signupCounts[row].length를 사용해야 합니다.

for (int row = 0; row < signupCounts.length; row++) {
    for (int column = 0; column < signupCounts[row].length; column++) {
        System.out.println(signupCounts[row][column]);
    }
}

가변 길이 배열은 잘못된 배열이 아닙니다.

지역마다 운영하는 가입 채널 수가 다르거나 좌석 행마다 좌석 수가 다를 때 자연스러운 모델입니다.

문제는 실제 구조를 확인하지 않고 직사각형이라고 단정하는 데 있습니다.

바깥 배열의 세 참조가 길이 3, 1, 2의 행 배열을 가리킨다. 열 경계는 선택한 행의 길이이며 올바른 순회는 6개 값을 한 번씩 방문한다.

RaggedArrayBug의 가변 행 배열에서 참조 관계와 현재 행의 인덱스 경계를 구분합니다. 화살표는 참조이며 실행 순서나 물리 메모리 주소가 아닙니다.

ARRAY · BOUNDARIES

행마다 다른 길이, 현재 행의 경계로 순회

바깥 배열은 세 행의 참조를 담습니다. 각 행의 길이3·1·2에 맞춰 열을 읽으면 여섯 값을 빠짐없이 방문합니다.

int[][] signupCounts · 화살표는 행 배열을 가리키는 참조입니다.

가변 행 배열의 참조와 열 경계 바깥 배열의 row 0, 1, 2는 각각 [20,30,40], [15], [25,35]인 별도 int 배열을 가리키며 길이는 3, 1, 2다. 바깥 배열 · length = 3 숫자가 아닌 행 참조 3개 참조 참조 참조 row 0 signupCounts[0] row 1 signupCounts[1] row 2 signupCounts[2] int[] · length = 3 20 column 0 30 column 1 40 column 2 int[] · length = 1 15 column 0 column 1은 없음 row 1의 첫 범위 초과 int[] · length = 2 25 column 0 35 column 1
행마다 다른 길이, 현재 행의 경계로 순회 바깥배열 row0/1/2참조→별도length3/1/2 행배열; 실제6값과현재행column경계,합165 바깥 배열 참조 3개 별도 행 배열 row0 참조 length=3 20 0 30 1 40 2 row1 참조 length=1 15 0 row2 참조 length=2 25 0 35 1 column < 현재 행 length 3+1+2=6회 · 합계165 row1에는 column1 없음

바깥 length = 3 각 슬롯에는 행 배열의 참조가 있습니다.

row 0 → 길이 3의 int[]
[20, 30, 40] · column 0, 1, 2
row 1 → 길이 1의 int[]
[15] · column 0만 존재하며 column 1은 범위 밖입니다.
row 2 → 길이 2의 int[]
[25, 35] · column 0, 1
현재 행을 기준으로
signupCounts[row]를 고른 뒤 column < signupCounts[row].length를 검사합니다.
첫 행 길이를 재사용하면
signupCounts[0].length인 3을 모든 행에 적용해 row=1, column=1에서 ArrayIndexOutOfBoundsException이 발생합니다.

올바른 순회: 20 → 30 → 40 → 15 → 25 → 35 · 방문 3+1+2=6회 · 합계 165


향상된 for 순회

인덱스 자체가 필요하지 않고 모든 값을 한 번씩 읽기만 한다면 향상된 for가 의도를 더 선명하게 드러냅니다.

src/EnhancedForMemberGrid.java
public final class EnhancedForMemberGrid {
    public static void main(String[] args) {
        int[][] signupCounts = {
                {20, 30, 40},
                {15},
                {25, 35}
        };

        int total = 0;
        for (int[] region : signupCounts) {
            for (int value : region) {
                total += value;
            }
        }

        System.out.println("total=" + total);
    }
}
total=165

바깥 반복에서 region은 현재 행 배열을 가리키고, 안쪽 반복에서 value는 현재 원소 값을 복사해 받습니다.

value = 0으로 바꾸어도 원본 배열 원소는 바뀌지 않습니다.

값을 수정하려면 인덱스로 접근해야 합니다.

for (int row = 0; row < signupCounts.length; row++) {
    for (int column = 0; column < signupCounts[row].length; column++) {
        signupCounts[row][column] += 5;
    }
}

따라서 선택 기준은 간단합니다.

위치를 출력하거나 특정 원소를 교체해야 하면 기본 for, 모든 값을 읽어 합계처럼 축약하면 향상된 for가 적합합니다.

무조건 짧은 문법을 고르는 것이 아니라 작업에 필요한 정보가 무엇인지 먼저 판단합니다.


행 출력 리팩터링

행마다 별도의 반복문을 작성하면 행을 추가할 때 출력 코드도 추가해야 합니다.

for (int column = 0; column < signupCounts[0].length; column++) {
    System.out.println(signupCounts[0][column]);
}
for (int column = 0; column < signupCounts[1].length; column++) {
    System.out.println(signupCounts[1][column]);
}

바깥 반복을 도입하면 행 수가 바뀌어도 로직은 그대로입니다.

이것이 여기서의 리팩터링입니다.

관찰되는 결과는 유지하면서 구조를 바꾸어 중복과 변경 지점을 줄입니다.

리팩터링 전후에 작은 입력의 출력과 합계를 비교해야 값 누락이나 중복 집계를 발견할 수 있습니다.


지역별 가입 현황

행은 지역, 열은 가입 채널로 정하면 같은 열이 같은 채널을 의미합니다.

표의 축을 코드 가까이에 적어 두면 숫자의 의미를 잃지 않습니다.

src/RegionalSignupGrid.java
public final class RegionalSignupGrid {
    public static void main(String[] args) {
        String[] regions = {"Seoul", "Busan", "Daejeon"};
        String[] channels = {"Web", "Mobile", "Partner"};
        int[][] signupCounts = {
                {40, 20, 0},
                {30, 0, 50},
                {45, 25, 35}
        };

        if (regions.length != signupCounts.length) {
            throw new IllegalStateException("region count differs");
        }

        int grandTotal = 0;
        for (int row = 0; row < signupCounts.length; row++) {
            if (signupCounts[row].length != channels.length) {
                throw new IllegalStateException("channel count differs at row " + row);
            }

            int regionTotal = 0;
            for (int value : signupCounts[row]) {
                regionTotal += value;
            }
            grandTotal += regionTotal;
            System.out.println(regions[row] + "=" + regionTotal);
        }

        for (int column = 0; column < channels.length; column++) {
            int channelTotal = 0;
            for (int row = 0; row < signupCounts.length; row++) {
                channelTotal += signupCounts[row][column];
            }
            System.out.println(channels[column] + "=" + channelTotal);
        }
        System.out.println("all=" + grandTotal);
    }
}
Seoul=60
Busan=80
Daejeon=105
Web=115
Mobile=45
Partner=85
all=245

지역 합계는 한 행을 고정하고 열을 움직입니다.

채널 합계는 한 열을 고정하고 행을 움직입니다.

같은 표라도 질문에 따라 순회 방향이 달라집니다.

0은 해당 지역에서 그 채널로 가입한 회원이 없다는 실제 집계값입니다.

아직 집계하지 않은 칸과 구분해야 한다면 별도의 입력 여부 배열이 필요합니다.

이 모델은 지역과 채널 수가 고정된 보고서에 알맞습니다.

회원 한 명씩 계속 추가하는 서비스라면 이후 장에서 배우는 객체와 컬렉션이 더 자연스럽습니다.

데이터 모양을 먼저 정하고 배열 차원을 정해야 합니다.

같은 가입 현황을 행으로 합치면 지역, 열로 합치면 채널이다

같은 지역×채널 값에서 행별 60·80·105, 열별 115·45·85를 합산하여 총계 245에 도달합니다. 원본 표를 고정하고 현재 누산 축과 합계를 변화시키면 순회 방향을 이해하기 쉽습니다.

RegionalSignupGrid의 지역과 채널별 9개 값, 행 합계 60·80·105, 열 합계 115·45·85와 전체 합계 245의 대응표.

지역별 합계는 row를 고정하고 column을 움직이며, 채널별 합계는 column을 고정하고 row를 움직입니다. 두 집계는 같은 9개 값을 사용합니다.

ARRAY · BOUNDARIES

지역은 행, 채널은 열: 같은 표의 두 합계

지역 합계는 한 행을 따라, 채널 합계는 한 열을 따라 계산합니다. 두 방향의 전체 합계는 모두245입니다.

RegionalSignupGrid · 행은 지역, 열은 채널 · 오른쪽은 행 합계, 아래쪽은 열 합계
지역 / 채널WebMobilePartner지역 합계
Seoul4020060
Busan3005080
Daejeon452535105
채널 합계1154585전체 245
지역은 행, 채널은 열: 같은 표의 두 합계 9개 실제값의 지역행합60/80/105와채널열합115/45/85,두전체합245; 합계방향선이지실행순서아님 행=row · 지역 / 열=column · 채널 화살표는 해당 행·열의 합계 Web Mobile Partner 0 40 20 0 60 1 30 0 50 80 2 45 25 35 105 행 합 115 45 85 열 합 0=Seoul · 1=Busan 2=Daejeon 행: 60+80+105=245 열: 115+45+85=245
지역은 행, 채널은 열: 같은 표의 두 합계 9개 실제값의 지역행합60/80/105와채널열합115/45/85,두전체합245; 합계방향선이지실행순서아님 행=row · 지역 / 열=column · 채널 화살표는 해당 행·열의 합계 Web Mobile Partner 0 40 20 0 60 1 30 0 50 80 2 45 25 35 105 행 합 115 45 85 열 합 0=Seoul · 1=Busan 2=Daejeon 행: 60+80+105=245 열: 115+45+85=245

RegionalSignupGrid 3개 지역 × 공통 채널 3개

Seoul · 행 합계 60
Web 40 · Mobile 20 · Partner 0
Busan · 행 합계 80
Web 30 · Mobile 0 · Partner 50
Daejeon · 행 합계 105
Web 45 · Mobile 25 · Partner 35

채널별 열 합계: Web 115 · Mobile 45 · Partner 85 · 전체 245

지역 합계 · 한 행
row를 고정하고 column을 움직입니다. 60 + 80 + 105 = 245
채널 합계 · 한 열
column을 고정하고 row를 움직입니다. 115 + 45 + 85 = 245

0도 실제 가입 수입니다. 이 보고서는 각 행 길이가 channels.length와 같음을 검사한 직사각형 모델이며, 미집계 여부는 별도 정보가 필요합니다.

이 예의 조건

  • 화살표는 합계 방향이며 실제 루프 실행 순서를 고정하지 않습니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
29개 실제 값의 지역 행 합 60/80/105와 채널 열 합 115/45/85, 두 전체 합 245; 합계 방향 선이며 실행 순서가 아님
3행=row · 지역
4열=column · 채널
원 도표의 설명과 주석
구분내용
설명9개 실제 값의 지역 행 합 60/80/105와 채널 열 합 115/45/85, 두 전체 합 245; 합계 방향 선이며 실행 순서가 아님
전체 읽기 정보같은 가입 현황을 행으로 합치면 지역, 열로 합치면 채널이다 RegionalSignupGrid의 지역과 채널별 9개 값, 행 합계 60·80·105, 열 합계 115·45·85와 전체 합계 245의 대응표. 지역별 합계는 row를 고정하고 column을 움직이며, 채널별 합계는 column을 고정하고 row를 움직입니다. 두 집계는 같은 9개 값을 사용합니다. ARRAY · BOUNDARIES 지역은 행, 채널은 열: 같은 표의 두 합계 지역 합계는 한 행을 따라, 채널 합계는 한 열을 따라 계산합니다. 두 방향의 전체 합계는 모두 245입니다. RegionalSignupGrid · 행은 지역, 열은 채널 · 오른쪽은 행 합계, 아래쪽은 열 합계 지역 / 채널 Web Mobile Partner 지역 합계 Seoul 40 20 0 60 Busan 30 0 50 80 Daejeon 45 25 35 105 채널 합계 115 45 85 전체 245 RegionalSignupGrid 3개 지역 × 공통 채널 3개 Seoul · 행 합계 60 Web 40 · Mobile 20 · Partner 0 Busan · 행 합계 80 Web 30 · Mobile 0 · Partner 50 Daejeon · 행 합계 105 Web 45 · Mobile 25 · Partner 35 채널별 열 합계: Web 115 · Mobile 45 · Partner 85 · 전체 245 지역 합계 · 한 행 row를 고정하고 column을 움직입니다. 60 + 80 + 105 = 245 채널 합계 · 한 열 column을 고정하고 row를 움직입니다. 115 + 45 + 85 = 245 0 도 실제 가입 수입니다. 이 보고서는 각 행 길이가 channels.length 와 같음을 검사한 직사각형 모델이며, 미집계 여부는 별도 정보가 필요합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
지역별 합계

행의 세 채널 값을 합칩니다.

  • Seoul: 40+20+0=60
  • Busan/Daejeon: 30+0+50=80 /45+25+35=105
  • 같은 지역의 채널 값을 하나의 행 합으로 모읍니다.
채널별 합계

열의 세 지역 값을 합칩니다.

  • Web: 40+30+45=115
  • Mobile/Partner: 45 /85
  • 같은 입력을 다른 축으로 묶어 채널 합을 만듭니다.
전체 대조

행 합과 열 합을 각각 합칩니다.

    • 모든 셀을 한 번씩 더해 두 전체 합이 일치합니다.

    같은 가입 현황을 행으로 합치면 지역, 열로 합치면 채널이다

    같은 지역×채널 값에서 행별 60·80·105, 열별 115·45·85를 합산하여 총계 245에 도달합니다. 원본 표를 고정하고 현재 누산 축과 합계를 변화시키면 순회 방향을 이해하기 쉽습니다.

    RegionalSignupGrid의 지역과 채널별 9개 값, 행 합계 60·80·105, 열 합계 115·45·85와 전체 합계 245의 대응표.

    지역별 합계는 row를 고정하고 column을 움직이며, 채널별 합계는 column을 고정하고 row를 움직입니다. 두 집계는 같은 9개 값을 사용합니다.

    ARRAY · BOUNDARIES

    지역은 행, 채널은 열: 같은 표의 두 합계

    지역 합계는 한 행을 따라, 채널 합계는 한 열을 따라 계산합니다. 두 방향의 전체 합계는 모두245입니다.

    RegionalSignupGrid · 행은 지역, 열은 채널 · 오른쪽은 행 합계, 아래쪽은 열 합계
    지역 / 채널WebMobilePartner지역 합계
    Seoul4020060
    Busan3005080
    Daejeon452535105
    채널 합계1154585전체 245
    지역은 행, 채널은 열: 같은 표의 두 합계 9개 실제값의 지역행합60/80/105와채널열합115/45/85,두전체합245; 합계방향선이지실행순서아님 행=row · 지역 / 열=column · 채널 화살표는 해당 행·열의 합계 Web Mobile Partner 0 40 20 0 60 1 30 0 50 80 2 45 25 35 105 행 합 115 45 85 열 합 0=Seoul · 1=Busan 2=Daejeon 행: 60+80+105=245 열: 115+45+85=245
    지역은 행, 채널은 열: 같은 표의 두 합계 9개 실제값의 지역행합60/80/105와채널열합115/45/85,두전체합245; 합계방향선이지실행순서아님 행=row · 지역 / 열=column · 채널 화살표는 해당 행·열의 합계 Web Mobile Partner 0 40 20 0 60 1 30 0 50 80 2 45 25 35 105 행 합 115 45 85 열 합 0=Seoul · 1=Busan 2=Daejeon 행: 60+80+105=245 열: 115+45+85=245

    RegionalSignupGrid 3개 지역 × 공통 채널 3개

    Seoul · 행 합계 60
    Web 40 · Mobile 20 · Partner 0
    Busan · 행 합계 80
    Web 30 · Mobile 0 · Partner 50
    Daejeon · 행 합계 105
    Web 45 · Mobile 25 · Partner 35

    채널별 열 합계: Web 115 · Mobile 45 · Partner 85 · 전체 245

    지역 합계 · 한 행
    row를 고정하고 column을 움직입니다. 60 + 80 + 105 = 245
    채널 합계 · 한 열
    column을 고정하고 row를 움직입니다. 115 + 45 + 85 = 245

    0도 실제 가입 수입니다. 이 보고서는 각 행 길이가 channels.length와 같음을 검사한 직사각형 모델이며, 미집계 여부는 별도 정보가 필요합니다.

    이 예의 조건

    • 화살표는 합계 방향이며 실제 루프 실행 순서를 고정하지 않습니다.

    관계도의 보충 설명

    원 도표의 전체 관계·라벨·조건
    구분내용
    29개 실제 값의 지역 행 합 60/80/105와 채널 열 합 115/45/85, 두 전체 합 245; 합계 방향 선이며 실행 순서가 아님
    3행=row · 지역
    4열=column · 채널
    원 도표의 설명과 주석
    구분내용
    설명9개 실제 값의 지역 행 합 60/80/105와 채널 열 합 115/45/85, 두 전체 합 245; 합계 방향 선이며 실행 순서가 아님
    전체 읽기 정보같은 가입 현황을 행으로 합치면 지역, 열로 합치면 채널이다 RegionalSignupGrid의 지역과 채널별 9개 값, 행 합계 60·80·105, 열 합계 115·45·85와 전체 합계 245의 대응표. 지역별 합계는 row를 고정하고 column을 움직이며, 채널별 합계는 column을 고정하고 row를 움직입니다. 두 집계는 같은 9개 값을 사용합니다. ARRAY · BOUNDARIES 지역은 행, 채널은 열: 같은 표의 두 합계 지역 합계는 한 행을 따라, 채널 합계는 한 열을 따라 계산합니다. 두 방향의 전체 합계는 모두 245입니다. RegionalSignupGrid · 행은 지역, 열은 채널 · 오른쪽은 행 합계, 아래쪽은 열 합계 지역 / 채널 Web Mobile Partner 지역 합계 Seoul 40 20 0 60 Busan 30 0 50 80 Daejeon 45 25 35 105 채널 합계 115 45 85 전체 245 RegionalSignupGrid 3개 지역 × 공통 채널 3개 Seoul · 행 합계 60 Web 40 · Mobile 20 · Partner 0 Busan · 행 합계 80 Web 30 · Mobile 0 · Partner 50 Daejeon · 행 합계 105 Web 45 · Mobile 25 · Partner 35 채널별 열 합계: Web 115 · Mobile 45 · Partner 85 · 전체 245 지역 합계 · 한 행 row를 고정하고 column을 움직입니다. 60 + 80 + 105 = 245 채널 합계 · 한 열 column을 고정하고 row를 움직입니다. 115 + 45 + 85 = 245 0 도 실제 가입 수입니다. 이 보고서는 각 행 길이가 channels.length 와 같음을 검사한 직사각형 모델이며, 미집계 여부는 별도 정보가 필요합니다.
    예의 값과 변화
    관찰·상황값·보충 정보변화·이유
    지역별 합계

    행의 세 채널 값을 합칩니다.

    • Seoul: 40+20+0=60
    • Busan/Daejeon: 30+0+50=80 /45+25+35=105
    • 같은 지역의 채널 값을 하나의 행 합으로 모읍니다.
    채널별 합계

    열의 세 지역 값을 합칩니다.

    • Web: 40+30+45=115
    • Mobile/Partner: 45 /85
    • 같은 입력을 다른 축으로 묶어 채널 합을 만듭니다.
    전체 대조

    행 합과 열 합을 각각 합칩니다.

      • 모든 셀을 한 번씩 더해 두 전체 합이 일치합니다.

      연습 문제

      이번 연습에서는 앞의 RegionalSignupGrid와 다른 입력을 사용합니다. Seoul은 {40, 20}, Busan은 {30, 50, 10}, Daejeon은 {45}로 두고 각 행의 합계를 구한 뒤, 가입자 수가 가장 많은 지역을 출력하세요.

      모든 행이 비어 있는 입력은 없다고 가정하되, 행마다 열 길이가 달라도 동작하게 만드세요.

      해설 보기
      src/BestSignupRegion.java
      public final class BestSignupRegion {
          public static void main(String[] args) {
              String[] regions = {"Seoul", "Busan", "Daejeon"};
              int[][] signupCounts = {
                      {40, 20},
                      {30, 50, 10},
                      {45}
              };
      
              int bestRow = -1;
              int bestTotal = -1;
      
              for (int row = 0; row < signupCounts.length; row++) {
                  int total = 0;
                  for (int value : signupCounts[row]) {
                      total += value;
                  }
                  if (total > bestTotal) {
                      bestTotal = total;
                      bestRow = row;
                  }
              }
      
              System.out.println(regions[bestRow] + "=" + bestTotal);
          }
      }
      Busan=90

      행마다 안쪽 반복문으로 합계를 계산하고, 지금까지 가장 큰 합계와 행 번호를 함께 갱신합니다.

      합계가 0 이상이라는 입력 조건을 사용해 bestTotal을 -1로 시작했습니다.

      같은 합계 계산이 여러 곳에서 반복되기 시작하면 ch3-6에서 메서드로 분리합니다.