안동민 개발노트

본문 시작

배열 기반 회원가입

용량과 저장 개수를 구분하고 합계·평균·최솟값·최댓값을 계산해 배열 기반 회원가입을 완성합니다.

배열을 저장소로 쓰려면 원소를 넣는 것만으로는 부족합니다.

비어 있는 칸과 실제 회원을 구분하고, 용량을 넘지 않게 추가하며, 저장된 범위만 조회하고 집계해야 합니다.

이 절에서는 상품 관리 문제에서 자주 쓰는 고정 용량 배열 패턴을 회원가입에 적용합니다.

용량 4, size 2의 병렬 배열에서 index 0과 1은 회원이고 index 2와 3은 기본값이 남은 미사용 영역이다. 같은 합계 90의 평균 분모는 용량 4가 아니라 회원 수 2다.

LengthInsteadOfSize의 동일한 입력 상태를 사용합니다. size가 회원 집합의 경계이고 null이나 0이라는 기본값은 회원 존재를 뜻하지 않습니다.

length = 4 · size = 2 사용 [0, 2) / 미사용 [2, 4)

LengthInsteadOfSize · 같은 index의 email과 age가 한 회원
indexemails[index]age[index]회원 여부
0kim@example.com40사용 · 실제 회원
1lee@example.com50사용 · 실제 회원
2 · sizenull0미사용 · size 경계
3null0미사용 · 기본값
index 0 · 사용 · 실제 회원
kim@example.com / age 40
index 1 · 사용 · 실제 회원
lee@example.com / age 50
index 2 · 미사용 · size 경계
null / age 0
index 3 · 미사용 · 기본값
null / age 0
잘못된 분모 · 용량 4
미사용 두 칸까지 출력하면 null=0이 두 줄 나타납니다. 정수 나눗셈 90 / 4 = 22는 회원 평균이 아닙니다.
같은 회원 집합 · 개수 2
index < size만 읽으면 합계는 40 + 50 = 90, 평균은 (double) 90 / 2 = 45.0입니다.

0 ≤ size ≤ length. size == 0은 나눗셈 전에 분기하며 예제의 0.0은 회원 없음의 표시 정책이지 나이 0인 회원이 아닙니다.


length와 size 구분

lab/LengthInsteadOfSize.java
public final class LengthInsteadOfSize {
    public static void main(String[] args) {
        String[] emails = new String[4];
        int[] age = new int[4];
        int size = 0;

        emails[size] = "kim@example.com";
        age[size] = 40;
        size++;

        emails[size] = "lee@example.com";
        age[size] = 50;
        size++;

        int total = 0;
        for (int index = 0; index < age.length; index++) {
            total += age[index];
            System.out.println(emails[index] + "=" + age[index]);
        }
        System.out.println("average=" + total / age.length);
    }
}
잘못된 결과
kim@example.com=40
lee@example.com=50
null=0
null=0
average=22

예외는 발생하지 않지만 결과는 틀렸습니다.

배열의 기본값 null과 0이 존재하지 않는 두 회원처럼 출력되고, 평균의 분모도 용량 4가 되어 45가 아니라 22가 됩니다.

반복 범위와 평균 분모에는 size를 사용해야 합니다.

length는 추가 가능 여부를 판단하는 용량입니다.

if (size >= emails.length) {
    System.out.println("저장 공간이 가득 찼습니다.");
} else {
    emails[size] = "park@example.com";
    age[size] = 35;
    size++;
}

두 병렬 배열에 값을 모두 쓴 다음 마지막에 size를 증가시킵니다.

중간에 검증이 실패했는데 size부터 증가시키면 빈 회원이 생깁니다.


저장 범위 집계

src/SummaryWithSize.java
public final class SummaryWithSize {
    public static void main(String[] args) {
        int[] age = new int[5];
        int size = 3;
        age[0] = 40;
        age[1] = 50;
        age[2] = 30;

        int total = 0;
        for (int index = 0; index < size; index++) {
            total += age[index];
        }

        double average = size == 0 ? 0.0 : (double) total / size;
        System.out.println("total=" + total);
        System.out.println("average=" + average);
    }
}
total=120
average=40.0

정수끼리 나누면 소수 부분이 먼저 버려집니다.

(double) total / size처럼 피연산자 하나를 실수로 변환해야 실수 나눗셈이 됩니다.

size가 0이면 나눗셈 자체가 불가능하므로 평균의 정책을 정해야 합니다.

여기서는 회원 없음의 평균을 0.0으로 표시했지만, 화면에 회원 없음을 출력하거나 계산을 건너뛰는 편이 더 정확할 수도 있습니다.

합계가 커질 수 있는 영역에서는 int total 대신 long total을 고려합니다.

회원 나이 몇 건에서는 int가 충분하지만, 단위를 밀리초로 바꾸거나 장기간 데이터를 합치면 범위를 검토해야 합니다.

자료형 선택도 요구사항의 일부입니다.


최솟값과 최댓값 초기화

최솟값을 0으로 초기화하면 모든 회원 나이가 양수일 때 최솟값이 계속 0으로 남습니다.

0은 저장된 회원이 아니라 개발자가 만든 가짜 값입니다.

src/MemberExtremes.java
public final class MemberExtremes {
    public static void main(String[] args) {
        String[] emails = {"kim@example.com", "lee@example.com", "park@example.com", null};
        int[] age = {40, 55, 25, 0};
        int size = 3;

        if (size == 0) {
            System.out.println("회원 없음");
            return;
        }

        int minIndex = 0;
        int maxIndex = 0;
        for (int index = 1; index < size; index++) {
            if (age[index] < age[minIndex]) {
                minIndex = index;
            }
            if (age[index] > age[maxIndex]) {
                maxIndex = index;
            }
        }

        System.out.println("min=" + emails[minIndex] + ":" + age[minIndex]);
        System.out.println("max=" + emails[maxIndex] + ":" + age[maxIndex]);
    }
}
min=park@example.com:25
max=lee@example.com:55

값만 기억하지 않고 인덱스를 기억하면 같은 위치의 email도 함께 찾을 수 있습니다.

같은 최댓값이 여러 개일 때 >는 먼저 나온 회원을 유지하고 >=는 나중 회원 정보로 교체합니다.

어느 쪽이 맞는지는 동점 정책에 따라 정합니다.

빈 배열과 회원이 0개인 배열은 다릅니다.

new int[5]는 원소 다섯 개가 있지만 size가 0이면 저장된 회원은 없습니다.

최솟값·최댓값 계산 전에 size를 검사해야 index 0 접근도 피할 수 있습니다.


회원 관리 패턴

상품 관리 예제는 보통 상품명과 가격을 병렬 배열에 넣고, 추가와 목록 출력을 반복합니다.

회원가입도 이메일과 나이를 병렬 배열로 관리하므로 구조가 같습니다.

상품 관리회원가입공통 불변식
상품명 배열이메일 배열같은 index의 문자열이 이름 역할
가격 배열회원 나이 배열같은 index의 숫자가 속성 역할
상품 수회원 수0 <= size <= length
재고 초과 방지회원 용량 초과 방지추가 전에 capacity 검사

도메인 단어는 다르지만 저장 알고리즘은 재사용할 수 있습니다.

다만 상품 가격에는 0이 허용될 수 있고 회원 나이에는 양수만 허용한다는 식으로 검증 규칙은 도메인에 맞게 달라집니다.


배열 기반 명령 흐름

다음 예제는 입력을 문자열로 고정해 추가, 목록, 요약 흐름을 재현합니다.

실제 콘솔로 바꾸려면 new Scanner(System.in)만 사용하면 됩니다.

src/ArrayMemberRegistry.java
import java.util.Scanner;

public final class ArrayMemberRegistry {
    public static void main(String[] args) {
        Scanner scanner = new Scanner("add\nkim@example.com\n40\nadd\nlee@example.com\n50\nlist\nsummary\nquit\n");
        String[] emails = new String[3];
        int[] age = new int[3];
        int size = 0;
        boolean running = true;

        while (running && scanner.hasNextLine()) {
            String command = scanner.nextLine().trim();

            switch (command) {
                case "add" -> {
                    if (size >= emails.length) {
                        System.out.println("full");
                        break;
                    }

                    String email = scanner.nextLine().trim();
                    int value = Integer.parseInt(scanner.nextLine().trim());
                    if (email.isEmpty() || value < 14 || value > 120) {
                        System.out.println("invalid");
                        break;
                    }

                    emails[size] = email;
                    age[size] = value;
                    size++;
                    System.out.println("added");
                }
                case "list" -> {
                    for (int index = 0; index < size; index++) {
                        System.out.println((index + 1) + ". " + emails[index] + "=" + age[index]);
                    }
                }
                case "summary" -> {
                    int total = 0;
                    for (int index = 0; index < size; index++) {
                        total += age[index];
                    }
                    double average = size == 0 ? 0.0 : (double) total / size;
                    System.out.println("total=" + total + ", average=" + average);
                }
                case "quit" -> running = false;
                default -> System.out.println("unknown");
            }
        }
    }
}
added
added
1. kim@example.com=40
2. lee@example.com=50
total=90, average=45.0

이 프로그램이 지켜야 할 핵심 불변식은 네 가지입니다.

  1. size는 0보다 작지 않고 배열 길이보다 크지 않습니다.
  2. 0부터 size - 1까지는 두 배열 모두 유효한 값이 있습니다.
  3. size부터 length - 1까지는 사용하지 않는 영역입니다.
  4. 검증을 통과한 회원만 저장하고 그때만 size를 증가시킵니다.

현재는 모든 로직이 main 안에 있어 길고, 같은 집계가 다른 메뉴에 추가되면 중복될 수 있습니다.

다음 절에서 추가·출력·집계를 메서드로 분리해 변경 이유가 다른 코드를 떼어 냅니다.


용량 초과 처리

검사 없이 네 번째 원소를 넣으면 즉시 실패합니다.

lab/CapacityOverflow.java
public final class CapacityOverflow {
    public static void main(String[] args) {
        String[] emails = new String[2];
        int size = 0;

        emails[size++] = "kim@example.com";
        emails[size++] = "lee@example.com";
        emails[size++] = "park@example.com";
    }
}
실패 관찰
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2

예외 메시지는 문제가 생긴 인덱스 2와 배열 길이 2를 함께 보여 줍니다.

그러나 사용자 입력 때문에 프로그램이 예외로 종료되게 두기보다, 추가 전에 공간을 확인해 full이라는 도메인 결과로 바꾸는 편이 낫습니다.

ArrayMemberRegistry의 add 분기. 공간과 값을 검사한 뒤에만 emails와 age를 순차 기록하고 마지막으로 size를 늘린다. full과 invalid는 쓰기 없이 switch를 빠져나가 다음 명령 조건으로 돌아간다.

전제는 같은 용량의 두 배열, 유효한 size, 완전한 정수 입력입니다. 두 배열의 쓰기는 순차 실행이며 트랜잭션이나 예외 롤백 보장을 뜻하지 않습니다.

ArrayMemberRegistry의 add 명령 · 같은 배열 용량 · 0 ≤ size ≤ capacity · 완전한 정수 입력 전제

회원 추가의 검사와 size 확정 공간 없음과 값 오류는 기존 상태를 보존한 채 다음 명령 조건으로 복귀한다. 성공만 emails[size], age[size], size++를 순서대로 실행한다. 거짓 참 거짓 참 size < capacity ? 쓰기 전에 남은 공간 검사 full · 공간 없음 기존 size·배열 불변 break → switch 끝 email·나이 읽기 trim · parseInt 완전한 정수 입력 전제 입력값이 유효한가? email ≠ 빈 문자열 14 ≤ age ≤ 120 invalid · 값 오류 기존 size·배열 불변 break → switch 끝 이메일 먼저 기록 emails[size] = email 같은 index에 나이 기록 age[size] = value 마지막에 size++ 사용 영역 한 칸 확장 · added 출력 다음 command while 조건 running && hasNextLine() 다음 명령을 읽거나 입력 종료
  1. 공간 검사

    size < capacity가 거짓이면 full을 출력합니다. 값을 읽거나 배열에 쓰지 않고 switch를 빠져나갑니다.

  2. 읽기와 값 검사

    email을 trim하고 나이 문자열을 정수로 변환합니다. email이 비었거나 나이가 14–120 밖이면 invalid를 출력하고 switch를 빠져나갑니다.

  3. 같은 index에 순차 기록

    emails[size] = email 다음에 age[size] = value를 실행합니다. 두 검사를 통과한 경우에만 도달합니다.

  4. 성공 뒤 size 확정

    두 쓰기가 끝난 뒤 size++하고 added를 출력합니다. full과 invalid는 기존 size 및 저장 영역을 바꾸지 않습니다.

  5. 다음 명령 조건으로

    여기서 break는 switch만 끝냅니다. 성공과 거절 모두 바깥 while (running && scanner.hasNextLine())의 다음 조건으로 돌아갑니다.

  • 검사와 순차 실행
  • 가드 거절 · 상태 보존 복귀
  • 성공한 size 갱신

숫자 파싱 실패와 입력 조기 종료의 복구는 이 흐름에 없습니다. 두 쓰기는 원자적 동시 작업이나 예외 롤백이 아닌 순차 실행입니다.


연습 문제

이메일과 나이 배열, size가 주어졌을 때 30세 이상인 회원의 개수, 합계, 평균을 출력하세요.

조건을 만족하는 회원이 하나도 없으면 평균 대신 none을 출력합니다.

해설 보기
src/QualifiedSummary.java
public final class QualifiedSummary {
    public static void main(String[] args) {
        int[] age = {20, 45, 30, 60, 0};
        int size = 4;
        int count = 0;
        int total = 0;

        for (int index = 0; index < size; index++) {
            if (age[index] >= 30) {
                count++;
                total += age[index];
            }
        }

        System.out.println("count=" + count);
        System.out.println("total=" + total);
        if (count == 0) {
            System.out.println("average=none");
        } else {
            System.out.println("average=" + (double) total / count);
        }
    }
}
count=3
total=135
average=45.0

평균의 분모는 전체 size가 아니라 조건을 통과한 count입니다.

필터링과 집계를 함께 할 때 어떤 집합의 평균인지 끝까지 일치시켜야 합니다.