본문 바로가기

2차원 배열과 향상된 for

행과 열이 있는 표를 2차원 배열로 표현하고, 행마다 길이가 다른 배열을 안전하게 순회하며, 향상된 for와 기본 for를 골라 써서 지역별·채널별 가입자 수를 집계합니다.

배열과 인덱스에서 만든 배열은 값을 한 줄로 늘어놓은 목록이었습니다. 서울 지역의 가입 채널별 가입자 수처럼 한 가지 기준으로 나열되는 값이라면 이것으로 충분합니다. 그런데 서울, 부산, 대전의 채널별 가입자 수를 함께 비교하려면 행은 지역, 열은 채널인 표가 필요합니다. 지역마다 배열을 하나씩 만들 수도 있지만, 그러면 지역이 늘 때마다 배열 변수와 반복문을 추가해야 하는 문제가 다시 생깁니다.

자바에서는 이런 표를 2차원 배열로 표현합니다. 다만 자바의 2차원 배열은 칸이 바둑판처럼 이어진 한 덩어리가 아니라, 배열을 원소로 담은 배열입니다. 이 구조를 이해하면 직사각형 표뿐 아니라 행마다 길이가 다른 표도 안전하게 다룰 수 있습니다. 이 글에서는 2차원 배열의 구조와 순회 방법, 인덱스 없이 모든 원소를 읽는 향상된 for를 차례로 배우고, 지역별·채널별 가입 현황을 집계합니다.

2차원 배열 만들고 읽기

2행 3열짜리 표를 만들고 칸마다 값을 넣은 뒤, 표 모양 그대로 출력해 보겠습니다. 코드의 앞부분 여섯 줄은 칸에 값을 하나씩 넣고, 뒷부분의 겹친 for는 그 값을 행 단위로 출력합니다. 첫 행에 20, 30, 40을, 둘째 행에 15, 25, 35를 넣었으니 출력도 이 순서로 두 줄이 되어야 합니다.

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++) {
                if (column > 0) {
                    System.out.print(" ");
                }
                System.out.print(signupCounts[row][column]);
            }
            System.out.println();
        }
    }
}
20 30 40
15 25 35

int[][]는 'int 배열을 담는 배열'이라는 타입이고, new int[2][3]은 행 2개, 열 3개짜리 2차원 배열을 만듭니다. 칸 하나는 signupCounts[행][열]로 씁니다. 1차원 배열과 마찬가지로 두 인덱스 모두 0부터 시작하므로, signupCounts[1][2]는 둘째 행의 셋째 칸, 즉 35입니다.

출력은 반복문 두 개를 겹쳐서 합니다. 바깥 반복은 row로 행을 하나씩 고르고, 안쪽 반복은 column으로 그 행의 칸을 하나씩 읽습니다. 숫자 사이에만 공백이 들어가도록 column > 0일 때만 공백을 먼저 출력하고, 한 행을 다 출력하면 System.out.println()으로 줄을 바꿉니다. 반복 조건에 쓰인 두 길이는 의미가 다릅니다. signupCounts.length는 행의 수 2이고, signupCounts[row].length는 지금 고른 행의 칸 수 3입니다.

바깥 배열에는 행 배열의 참조가 들어 있다

두 길이가 왜 다른지는 2차원 배열의 실제 구조를 보면 알 수 있습니다. new int[2][3]은 숫자 여섯 개를 담는 한 덩어리를 만드는 것이 아닙니다. 칸이 두 개인 바깥 배열을 하나 만들고, 길이 3인 int[] 배열 두 개를 따로 만든 뒤, 바깥 배열의 각 칸에 그 행 배열의 참조를 넣습니다. 그래서 signupCounts[1]은 숫자가 아니라 둘째 행 배열 자체이고, signupCounts[1][2]는 '바깥 배열에서 1번 행 배열을 꺼낸 다음, 그 배열에서 2번 칸을 읽는다'는 두 단계의 접근입니다.

값을 나열해 만드는 배열 초기화도 같은 구조를 만듭니다. 다음 두 선언은 모양이 같은 배열을 만듭니다.

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

바깥 중괄호 안의 {0, 0, 0} 하나하나가 행 배열입니다. 행이 각각 독립된 배열이므로, 행 하나를 통째로 다른 배열로 바꿀 수도 있습니다.

src/RowSwap.java
public final class RowSwap {
    public static void main(String[] args) {
        int[][] created = new int[2][3];
        created[1] = new int[] {7, 8};

        System.out.println(java.util.Arrays.deepToString(created));
        System.out.println("row0=" + created[0].length + ", row1=" + created[1].length);
    }
}
[[0, 0, 0], [7, 8]]
row0=3, row1=2

created[1] = new int[] {7, 8};는 바깥 배열의 1번 칸이 길이 2인 새 배열을 가리키게 합니다. 그 결과 첫 행은 길이 3, 둘째 행은 길이 2가 되어 더 이상 직사각형이 아닙니다. Arrays.deepToString은 2차원 배열의 내용을 행별로 보여 주는 메서드입니다. 이번에는 import 문을 쓰는 대신 java.util.Arrays.deepToString처럼 패키지 이름까지 붙여 불렀는데, 이렇게 전체 이름을 쓰면 import 없이도 클래스를 사용할 수 있습니다. 1차원 배열에 쓰던 Arrays.toString에 2차원 배열을 넘기면 각 행이 배열이라서 [[I@659e0bfd, [I@2a139a55]처럼 행 배열의 표시만 나오므로, 2차원 배열에는 deepToString을 씁니다.

행마다 길이가 다른 배열

행마다 길이가 다른 2차원 배열을 가변 길이 배열(jagged array)이라고 부릅니다. 지역마다 운영하는 가입 채널 수가 다르거나, 공연장의 좌석 줄마다 좌석 수가 다른 경우처럼, 실제 데이터에는 이런 모양이 흔합니다. 초기화할 때 행마다 다른 개수의 값을 나열하면 바로 만들 수 있습니다.

이런 배열을 순회할 때 흔히 저지르는 실수가 있습니다. 다음 코드는 모든 행의 칸 수가 첫 행과 같다고 가정합니다.

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을 씁니다. 첫 행은 실제로 칸이 세 개라서 20, 30, 40이 정상적으로 출력됩니다. 둘째 행으로 넘어가 column이 0일 때 15를 출력하지만, column이 1이 되는 순간 칸이 하나뿐인 둘째 행에서 1번 칸을 읽으려다 예외가 발생합니다. 메시지의 Index 1 out of bounds for length 1이 바로 이 상황을 말해 줍니다.

고치는 방법은 안쪽 반복의 조건에 지금 고른 행의 길이를 쓰는 것입니다.

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

이제 row가 0일 때는 3칸, 1일 때는 1칸, 2일 때는 2칸만 읽으므로, 여섯 개의 값을 빠짐없이 한 번씩 출력하고 정상적으로 끝납니다. 가변 길이 배열 자체는 잘못이 아닙니다. 문제는 실제 구조를 확인하지 않고 모든 행의 길이가 같다고 가정한 데 있습니다. 행의 길이가 모두 같은 직사각형 배열에서도 signupCounts[row].length를 쓰는 습관을 들이면, 나중에 데이터 모양이 바뀌어도 반복문을 고칠 필요가 없습니다.

바깥 배열의 세 참조가 길이 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

더 깊이: 행 배열을 나중에 만드는 방법

int[][] seats = new int[3][];처럼 바깥 배열의 길이만 정하고 안쪽 길이를 비워 두면, 행 배열은 만들어지지 않고 바깥 배열만 생깁니다. 바깥 배열의 원소 타입은 int[]라는 참조 타입이므로, 세 칸 모두 기본값 null입니다. 이 상태에서 seats[0][0]을 읽으면 가리키는 행 배열이 없어서 NullPointerException이 발생합니다. 그래서 seats[0] = new int[4];처럼 행마다 원하는 길이의 배열을 만들어 넣은 뒤에 사용해야 합니다. 행마다 길이를 계산해서 정해야 할 때 쓰는 방법입니다.

이 구조 때문에 2차원 배열을 복사할 때도 주의가 필요합니다. 2차원 배열 grid를 Arrays.copyOf(grid, grid.length)로 복사하면 바깥 배열만 새로 만들어지고, 그 안에는 원본과 같은 행 배열의 참조가 그대로 담깁니다. 따라서 복사본의 행을 통해 원소를 바꾸면 원본에서도 바뀐 값이 보입니다. 원소까지 독립된 복사본이 필요하면 행마다 Arrays.copyOf를 따로 호출해야 합니다.

인덱스가 필요 없을 때 쓰는 향상된 for

합계를 구하는 일처럼 모든 원소를 한 번씩 읽기만 하고 몇 번째 원소인지는 필요 없다면, 인덱스 변수 없이 원소를 차례로 꺼내 주는 향상된 for(enhanced for)를 쓸 수 있습니다. for (원소타입 변수 : 배열) 형태로 쓰며, 배열의 원소가 하나씩 차례로 변수에 담겨 반복 본문이 실행됩니다. 흔히 for-each 문이라고도 부릅니다.

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

바깥 반복은 signupCounts의 원소, 즉 행 배열을 하나씩 꺼내 region에 담습니다. 그래서 region의 타입은 int[]입니다. 안쪽 반복은 그 행의 원소인 int 값을 하나씩 꺼내 value에 담습니다. 행의 길이는 향상된 for가 알아서 처리하므로, 앞 절의 가변 길이 배열도 범위 오류 없이 20, 30, 40, 15, 25, 35를 모두 더해 165를 구합니다. 인덱스를 직접 다루지 않으니 첫 행의 길이를 잘못 쓰는 실수도, <=로 한 칸 더 읽는 실수도 생길 수 없습니다.

향상된 for로는 원소를 바꿀 수 없다

향상된 for의 변수 value에는 배열 원소의 값이 복사되어 담깁니다. 그래서 반복 본문에서 value = 0;을 실행해도 바뀌는 것은 복사본인 value뿐이고, 배열의 원소는 그대로입니다. 원소의 값을 바꾸려면 인덱스로 칸을 직접 가리켜야 합니다. 다음 코드는 모든 칸에 5를 더합니다.

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

바깥 반복의 region은 사정이 조금 다릅니다. region에 복사되는 것은 행 배열의 참조이므로, region[0] = 0;처럼 region을 통해 원소를 바꾸면 실제 행 배열이 바뀝니다. 앞 글에서 본 배열 변수의 대입과 같은 원리입니다. 반면 region = new int[0];처럼 region 변수 자체에 새 배열을 대입하면 region만 다른 배열을 가리킬 뿐, signupCounts에는 아무 영향이 없습니다.

두 반복문 중 무엇을 쓸지는 작업에 무엇이 필요한지로 정합니다. 몇 번째 칸인지 출력해야 하거나, 칸의 값을 바꿔야 하거나, 같은 인덱스로 다른 배열을 함께 읽어야 한다면 기본 for를 씁니다. 모든 값을 읽어 합계나 개수 같은 하나의 결과로 모으기만 한다면 향상된 for가 의도를 더 잘 드러내고 실수할 여지도 적습니다.

행 반복을 바깥 반복으로 묶기

2차원 배열의 바깥 반복이 왜 필요한지는, 바깥 반복 없이 행마다 반복문을 따로 쓴 코드와 비교하면 분명해집니다. 다음 코드는 앞의 signupCounts를 반복문 두 개로 출력합니다. 두 반복문에서 서로 다른 글자가 어디인지 찾아보세요.

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]);
}

두 반복문은 행 번호 0과 1만 다르고 나머지가 똑같습니다. 이런 코드는 행이 하나 늘어날 때마다 반복문을 하나씩 더 붙여야 하고, 붙이다가 행 번호를 잘못 고치는 실수도 생기기 쉽습니다. 바뀌는 부분인 행 번호를 바깥 반복의 변수 row로 바꾸면 앞 절의 중첩 반복이 되고, 행이 몇 개든 코드는 그대로입니다.

이렇게 프로그램이 내는 결과는 그대로 두고 코드의 구조만 고치는 일을 리팩터링(refactoring)이라고 합니다. 리팩터링 전후에는 작은 입력으로 출력과 합계를 비교해 보는 것이 좋습니다. 결과가 같아야 구조만 바뀌었다고 말할 수 있고, 다르다면 값을 빠뜨렸거나 두 번 센 곳이 생긴 것입니다.

지역별·채널별 가입 현황 집계하기

이제 같은 표에서 두 가지 합계를 구해 보겠습니다. 행은 지역(서울, 부산, 대전), 열은 가입 채널(웹, 모바일, 제휴)입니다. 이렇게 정해 두면 같은 열은 언제나 같은 채널을 뜻합니다. 지역 이름과 채널 이름은 별도의 배열에 두고, 같은 인덱스로 표의 행과 열에 짝지어 씁니다. 표의 축이 무엇인지 코드 가까이에 적어 두면 숫자만 보고도 의미를 잃지 않습니다.

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

지역별 합계는 행 하나를 고정하고 그 행의 칸을 모두 더합니다. 바깥 반복이 row를 고르고, 안쪽의 향상된 for가 그 행의 값을 차례로 더해 regionTotal을 만듭니다. 서울은 40 + 20 + 0 = 60, 부산은 30 + 0 + 50 = 80, 대전은 45 + 25 + 35 = 105입니다. 행마다 구한 합계를 grandTotal에도 더해 두므로, 반복이 끝나면 전체 합계가 됩니다.

채널별 합계는 반대로 열 하나를 고정하고 모든 행의 같은 열을 더합니다. 그래서 바깥 반복이 column이고 안쪽 반복이 row입니다. 웹은 40 + 30 + 45 = 115, 모바일은 20 + 0 + 25 = 45, 제휴는 0 + 50 + 35 = 85입니다. 한 열의 값은 하나의 배열에 모여 있지 않고 세 행 배열의 같은 칸에 흩어져 있으므로, 행을 하나씩 고른 뒤 그 행의 column번 칸을 읽어야 합니다. 열 번호가 꼭 필요하기 때문에 바깥 반복은 기본 for로 씁니다. 같은 표라도 무엇을 묻느냐에 따라 순회 방향이 달라지는 것입니다.

마지막 줄 all=245는 행 합계를 모두 더한 값입니다. 채널 합계 115 + 45 + 85도 245로 같습니다. 두 방향 모두 아홉 칸을 한 번씩 더했기 때문이며, 두 값을 비교하면 집계에서 칸을 빠뜨리거나 두 번 세지 않았는지 확인할 수 있습니다.

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

같은 지역×채널 값에서 행별 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
    • 같은 입력을 다른 축으로 묶어 채널 합을 만듭니다.
    전체 대조

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

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

      집계 전에 두 가지를 확인하는 이유도 짚어 보겠습니다. 첫 번째 if는 지역 이름 수와 행 수가 같은지, 반복 안의 if는 각 행의 칸 수가 채널 수와 같은지 확인합니다. 채널별 합계는 모든 행에 모든 열이 있다는 가정 위에서 signupCounts[row][column]을 읽기 때문입니다. 어떤 행이 짧다면 열 방향 반복에서 ArrayIndexOutOfBoundsException이 나고, 어떤 행이 길다면 예외 없이 남는 칸이 채널 합계에서만 빠집니다. 예를 들어 부산 행에 값 10이 하나 더 있으면, 행 합계로 구한 all은 255가 되지만 채널 합계를 더하면 245가 되어 두 전체 합계가 어긋납니다. 이 보고서는 직사각형 표를 전제로 하므로, 그 전제가 깨졌다면 집계를 시작하기 전에 알리는 편이 낫습니다.

      표의 0도 짚어 둘 필요가 있습니다. 서울의 제휴 채널 0은 '그 채널로 가입한 회원이 없었다'는 실제 집계값입니다. 하지만 배열의 기본값도 0이므로, 아직 집계하지 않은 칸과 실제로 0명인 칸을 배열만 보고는 구분할 수 없습니다. 둘을 구분해야 한다면 칸마다 집계 여부를 기록하는 boolean[][] 배열을 따로 두어야 합니다.

      이런 2차원 배열 모델은 지역과 채널처럼 행과 열의 수가 정해진 보고서에 잘 맞습니다. 반면 회원을 한 명씩 계속 추가하는 서비스라면 표보다는 회원 목록이 필요하고, 그런 데이터에는 뒤에서 배울 객체와 컬렉션이 더 잘 맞습니다. 배열의 차원은 데이터의 모양을 먼저 정한 뒤에 고르는 것입니다.

      정리

      • 자바의 2차원 배열은 행 배열의 참조를 담은 바깥 배열입니다. 배열.length는 행의 수이고, 배열[row].length는 그 행의 칸 수입니다.
      • 행마다 길이가 다를 수 있으므로, 안쪽 반복의 조건에는 첫 행의 길이가 아니라 현재 행의 길이 배열[row].length를 씁니다.
      • 향상된 for는 인덱스 없이 원소를 차례로 꺼내 줍니다. 원소의 값이 변수에 복사되므로, 값을 읽어 모으는 작업에 쓰고 원소를 바꿀 때는 인덱스를 씁니다.
      • 행 방향 집계는 행을 고정하고 열을 움직이며, 열 방향 집계는 열을 고정하고 행을 움직입니다. 두 방향의 전체 합계를 비교하면 집계 실수를 찾을 수 있습니다.

      연습 문제

      1. 앞의 RegionalSignupGrid와 다른 입력을 씁니다. Seoul은 {40, 20}, Busan은 {30, 50, 10}, Daejeon은 {45}로 두고 각 행의 합계를 구한 뒤, 가입자 수가 가장 많은 지역을 출력하세요. 행마다 칸 수가 달라도 동작해야 하며, 비어 있는 행은 없다고 가정합니다.
      2. EnhancedForMemberGrid의 안쪽 반복 본문에 value = 0;을 추가한 뒤 반복이 끝나고 signupCounts[0][0]을 출력하면 무엇이 나올까요? 그 이유도 설명하세요.
      3. RegionalSignupGrid의 지역별 합계를 구하는 안쪽 반복은 향상된 for로 썼지만, 채널별 합계를 구하는 안쪽 반복은 기본 for로 썼습니다. 채널별 합계의 안쪽 반복도 향상된 for로 바꿀 수 있을까요? 바꿀 수 있다면 코드를 쓰고, 그래도 인덱스가 필요한 부분이 있다면 무엇인지 설명하세요.
      해설 보기
      1. 행마다 합계를 구하면서, 지금까지 가장 큰 합계와 그 행 번호를 함께 기억합니다.
      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

      행 합계는 서울 60, 부산 90, 대전 45입니다. 안쪽 반복을 향상된 for로 썼기 때문에 행마다 칸 수가 달라도 범위 오류가 생기지 않습니다. 가입자 수는 0 이상이므로 bestTotal을 -1로 시작하면, 첫 행의 합계가 0이더라도 반드시 첫 비교에서 갱신됩니다. 같은 합계를 구하는 코드가 여러 곳에서 반복되기 시작하면, 메서드와 매개변수에서 배우는 메서드로 따로 떼어 낼 수 있습니다.

      1. 20이 출력됩니다. 향상된 for의 value에는 원소의 값이 복사되어 담기므로, value = 0;은 복사본만 바꾸고 배열의 원소는 그대로 둡니다. 원소를 0으로 바꾸려면 signupCounts[row][column] = 0;처럼 인덱스로 칸을 직접 가리켜야 합니다.

      2. 바꿀 수 있습니다. 안쪽 반복이 하나씩 고르는 대상은 행이므로, 향상된 for로 행 배열을 하나씩 꺼낸 뒤 그 행의 column번 칸을 더하면 됩니다.

      for (int column = 0; column < channels.length; column++) {
          int channelTotal = 0;
          for (int[] region : signupCounts) {
              channelTotal += region[column];
          }
          System.out.println(channels[column] + "=" + channelTotal);
      }

      바꾼 뒤에도 출력은 일곱 줄 모두 같습니다. 다만 바깥 반복의 column은 여전히 필요합니다. 향상된 for가 꺼내 주는 것은 배열의 원소, 여기서는 행 배열이고, 한 채널의 값은 세 행 배열의 같은 칸에 흩어져 있어서 '한 열의 값들'을 담은 배열이 따로 없기 때문입니다. 출력에 채널 이름 channels[column]을 함께 써야 하는 것도 열 번호가 필요한 이유입니다.

      다음 글

      배열로 회원 목록 관리하기에서는 앞 글에서 만든 고정 크기 저장소에 회원을 추가하고, 저장된 회원만으로 합계, 평균, 최솟값, 최댓값을 구해 배열 기반 회원가입을 완성합니다.