본문 바로가기

클래스·참조 종합 실습

입력에서 객체 생성, 배열 저장, 검색과 출력까지 값과 참조의 흐름을 하나의 프로그램으로 연결합니다.

지금까지는 참조의 한 장면을 보기 위해 작은 프로그램을 따로 실행했습니다.

이제 회원 저장소 CLI에 세 개의 회원 정보를 입력하고, 객체 배열에 저장하고, 이메일로 검색하고, 전체 회원을 출력해 봅시다.

이 종합 예제의 목적은 기능을 많이 만드는 데 있지 않습니다.

입력 문자열과 숫자 값, 새 객체의 참조, 배열 칸의 참조가 어느 경계에서 복사되는지를 끝까지 추적하는 데 있습니다.

이번 버전의 Member는 여전히 필드만 가진 데이터 객체입니다.

입력·검증·출력 메서드는 MemberRegistryCli에 있습니다.

다음 객체 지향 장에서는 이 책임 중 무엇을 객체 안으로 옮겨야 하는지 판단합니다.


데이터 타입 설계

Member는 앞 문서와 같은 세 필드를 가집니다.

src/members/ch04/finalv/Member.java
public class Member {
    String email;
    int age;
    boolean active;
}

CLI는 최대 세 개를 입력받는 고정 배열과 실제 회원 수 size를 관리합니다.

Member[] members = new Member[3];
int size = 0;

배열 길이는 용량이고 size는 채운 칸의 수입니다.

회원 생성에 실패했는데 size부터 증가시키면 빈 슬롯이 데이터 범위에 포함됩니다.

따라서 다음 순서를 지킵니다.

  1. 입력을 읽고 검증합니다.
  2. 완성된 Member를 만듭니다.
  3. members[size]에 참조를 저장합니다.
  4. 저장에 성공한 뒤 size를 증가시킵니다.

크기와 저장 순서

다음 코드는 빈 자리를 예약한다는 생각으로 size를 먼저 증가시킵니다.

실패 사례: 크기를 먼저 증가
int index = size;
size++;

Member member = createMember(email, age);
members[index] = member;

createMember가 잘못된 나이 때문에 예외를 던지면 배열 칸은 null인데 size는 이미 증가한 상태가 됩니다.

이후 for (int i = 0; i < size; i++)가 그 슬롯을 정상 회원으로 보고 members[i].email을 읽다가 NPE가 납니다.

오류가 난 입력 지점과 NPE가 난 출력 지점이 멀어져 원인을 찾기도 어렵습니다.

개선 버전은 객체가 성공적으로 만들어진 다음에만 상태를 갱신합니다.

Member member = createMember(email, age);
members[size] = member;
size++;

위 두 짧은 예제의 호출에는 세 번째 인자가 생략되어 있습니다. 이 문서에 정의된 메서드로 실행하려면 두 호출 모두 createMember(email, age, false)를 사용해야 합니다.

한 줄 차이지만 불변 조건이 생깁니다.

0 <= i < size인 모든 배열 칸에는 null이 아닌 Member가 있다.

생성 실패 때 size를 보존하는 갱신 순서

같은 생성 실패 입력에서 size를 먼저 올린 버전은 size=1/null, 생성 후 갱신 버전은 size=0/null이 남습니다. 예외가 앞서 실행한 부수 효과를 취소하지 않는다는 점을 갱신 순서별 결과로 확인할 수 있습니다.

생성 실패 때 size를 보존하는 갱신 순서

생성이 실패하는 같은 조건에서 size를 먼저 늘리면 null 슬롯이 사용 구간에 들어가고, 생성 후에만 저장과 증가를 하면 size와 배열이 그대로입니다.

객체 생성에 성공한 뒤 저장하고 size를 늘립니다

같은 생성 실패를 두 갱신 순서로 비교합니다. size를 먼저 늘리면 null 칸을 회원 범위에 포함하게 됩니다.

실패 시 상태 비교 · 시작은 size = 0, members[0] = null
관찰 지점size를 먼저 증가생성 성공 뒤에만 갱신
생성 호출 전index = size 후 size++: index = 0, size = 1size = 0 그대로
생성에서 예외잘못된 나이로 createMember가 IllegalArgumentException을 던짐같은 예외가 발생함
도달하지 못한 연산members[index] = member 미실행members[size] = member와 size++ 모두 미실행
예외 직후 상태size = 1, members[0] = null. 빈 칸이 사용 구간에 포함됨size = 0, members[0] = null. 사용 구간은 여전히 비어 있음
객체 생성에 성공한 뒤 저장하고 size를 늘립니다 같은size0/null과유효하지않은나이로두독립비교. 증가먼저size1/null오염,생성먼저예외시size0/null유지. 관찰점선은catch후재개보장아님;전체CLI는사전나이검사. A · size 먼저 증가 size = 0 members[0] = null index=0; size++; size는 이미 1 createMember 호출 같은 잘못된 나이 예외로 중단 예외 직후 상태 관찰 size = 1 members[0] = null 빈 칸을 사용 구간에 포함 B · 생성 뒤 갱신 size = 0 members[0] = null createMember 호출 같은 잘못된 나이 예외로 중단 예외 직후 상태 관찰 size = 0 members[0] = null 저장·size++ 미도달 점선: 예외 뒤 상태 관찰 원래 실행은 예외로 중단 CLI는 먼저 나이 범위 검사
객체 생성에 성공한 뒤 저장하고 size를 늘립니다 같은size0/null과유효하지않은나이로두독립비교. 증가먼저size1/null오염,생성먼저예외시size0/null유지. 관찰점선은catch후재개보장아님;전체CLI는사전나이검사. A · size 먼저 증가 size = 0 members[0] = null index=0; size++; size는 이미 1 createMember 호출 같은 잘못된 나이 예외로 중단 예외 직후 상태 관찰 size = 1 members[0] = null 빈 칸을 사용 구간에 포함 B · 생성 뒤 갱신 size = 0 members[0] = null createMember 호출 같은 잘못된 나이 예외로 중단 예외 직후 상태 관찰 size = 0 members[0] = null 저장·size++ 미도달 점선: 예외 뒤 상태 관찰 원래 실행은 예외로 중단 CLI는 먼저 나이 범위 검사
size를 먼저 증가
  1. index = size, size++로 index는 0, size는 1입니다.
  2. 잘못된 나이로 createMember가 IllegalArgumentException을 던집니다.
  3. members[index] = member는 실행되지 않습니다.

결과: size = 1, members[0] = null. 빈 칸이 사용 구간에 포함됩니다.

생성 성공 뒤에만 갱신
  1. size = 0에서 바로 생성 함수를 호출합니다.
  2. 같은 예외가 발생합니다.
  3. 배열 저장과 size++ 모두 실행되지 않습니다.

결과: size = 0, members[0] = null. 사용 구간은 여전히 비어 있습니다.

실제 정의에 맞는 호출은 createMember(email, age, false)입니다. 두 순서를 비교할 때는 같은 유효하지 않은 나이와 나머지 입력을 사용합니다.

이는 생성 실패를 따로 유발한 비교입니다. 전체 CLI는 readMemberAge에서 범위를 먼저 검사합니다. 예외 직후 상태는 진단용 catch에서 관찰할 수 있지만, 원래 조각이 예외 뒤에도 계속 실행한다는 뜻은 아닙니다.

이 예의 조건

  • 두 독립 버전은 같은 잘못된 나이 입력에서 시작한다.
  • 예외 뒤 상태는 관찰 설명이며 원 실행은 예외로 중단한다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2같은 size 0/null과 유효하지 않은 나이로 두 버전을 독립적으로 비교한다. 먼저 증가시키면 size 1/null로 오염되고, 먼저 생성하면 예외 시 size 0/null을 유지한다. 관찰 점선은 catch 후 재개를 보장하지 않으며 전체 CLI는 나이를 사전에 검사한다.
원 도표의 설명과 주석
구분내용
설명같은 size 0/null과 유효하지 않은 나이로 두 버전을 독립적으로 비교한다. 먼저 증가시키면 size 1/null로 오염되고, 먼저 생성하면 예외 시 size 0/null을 유지한다. 관찰 점선은 catch 후 재개를 보장하지 않으며 전체 CLI는 나이를 사전에 검사한다.
전체 읽기 정보생성 실패 때 size를 보존하는 갱신 순서 생성 실패 때 size를 보존하는 갱신 순서 생성이 실패하는 같은 조건에서 size를 먼저 늘리면 null 슬롯이 사용 구간에 들어가고, 생성 후에만 저장과 증가를 하면 size와 배열이 그대로입니다. 객체 생성에 성공한 뒤 저장하고 size를 늘립니다 같은 생성 실패를 두 갱신 순서로 비교합니다. size를 먼저 늘리면 null 칸을 회원 범위에 포함하게 됩니다. 실패 시 상태 비교 · 시작은 size = 0, members[0] = null 관찰 지점 size를 먼저 증가 생성 성공 뒤에만 갱신 생성 호출 전 index = size 후 size++ : index = 0 , size = 1 size = 0 그대로 생성에서 예외 잘못된 나이로 createMember 가 IllegalArgumentException 을 던짐 같은 예외가 발생함 도달하지 못한 연산 members[index] = member 미실행 members[size] = member 와 size++ 모두 미실행 예외 직후 상태 size = 1 , members[0] = null . 빈 칸이 사용 구간에 포함됨 size = 0 , members[0] = null . 사용 구간은 여전히 비어 있음 size를 먼저 증가 index = size , size++ 로 index는 0, size는 1입니다. 잘못된 나이로 createMember 가 IllegalArgumentException 을 던집니다. members[index] = member 는 실행되지 않습니다. 결과: size = 1 , members[0] = null . 빈 칸이 사용 구간에 포함됩니다. 생성 성공 뒤에만 갱신 size = 0 에서 바로 생성 함수를 호출합니다. 같은 예외가 발생합니다. 배열 저장과 size++ 모두 실행되지 않습니다. 결과: size = 0 , members[0] = null . 사용 구간은 여전히 비어 있습니다. 실제 정의에 맞는 호출은 createMember(email, age, false) 입니다. 두 순서를 비교할 때는 같은 유효하지 않은 나이와 나머지 입력을 사용합니다. 이는 생성 실패를 따로 유발한 비교입니다. 전체 CLI는 readMemberAge 에서 범위를 먼저 검사합니다. 예외 직후 상태는 진단용 catch에서 관찰할 수 있지만, 원래 조각이 예외 뒤에도 계속 실행한다는 뜻은 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
동일 시작

빈 첫 칸과 size 0을 둡니다.

  • 두 버전 size: 0
  • members[0]: null
  • 변경 순서만 다르게 비교한다.
생성 전 차이

A는 size를 먼저 늘리고 B는 생성부터 호출합니다.

  • A size: 1
  • B size: 0
  • 생성 입력: 같은 잘못된 나이
  • A의 부수 효과는 아직 생성 성공으로 보호되지 않았다.
생성 예외

두 생성이 실패해 저장에 도달하지 않습니다.

  • A 결과: size 1/null
  • B 결과: size 0/null
  • 예외는 앞서 실행한 size 증가를 되돌리지 않는다.

생성 실패 때 size를 보존하는 갱신 순서

같은 생성 실패 입력에서 size를 먼저 올린 버전은 size=1/null, 생성 후 갱신 버전은 size=0/null이 남습니다. 예외가 앞서 실행한 부수 효과를 취소하지 않는다는 점을 갱신 순서별 결과로 확인할 수 있습니다.

생성 실패 때 size를 보존하는 갱신 순서

생성이 실패하는 같은 조건에서 size를 먼저 늘리면 null 슬롯이 사용 구간에 들어가고, 생성 후에만 저장과 증가를 하면 size와 배열이 그대로입니다.

객체 생성에 성공한 뒤 저장하고 size를 늘립니다

같은 생성 실패를 두 갱신 순서로 비교합니다. size를 먼저 늘리면 null 칸을 회원 범위에 포함하게 됩니다.

실패 시 상태 비교 · 시작은 size = 0, members[0] = null
관찰 지점size를 먼저 증가생성 성공 뒤에만 갱신
생성 호출 전index = size 후 size++: index = 0, size = 1size = 0 그대로
생성에서 예외잘못된 나이로 createMember가 IllegalArgumentException을 던짐같은 예외가 발생함
도달하지 못한 연산members[index] = member 미실행members[size] = member와 size++ 모두 미실행
예외 직후 상태size = 1, members[0] = null. 빈 칸이 사용 구간에 포함됨size = 0, members[0] = null. 사용 구간은 여전히 비어 있음
객체 생성에 성공한 뒤 저장하고 size를 늘립니다 같은size0/null과유효하지않은나이로두독립비교. 증가먼저size1/null오염,생성먼저예외시size0/null유지. 관찰점선은catch후재개보장아님;전체CLI는사전나이검사. A · size 먼저 증가 size = 0 members[0] = null index=0; size++; size는 이미 1 createMember 호출 같은 잘못된 나이 예외로 중단 예외 직후 상태 관찰 size = 1 members[0] = null 빈 칸을 사용 구간에 포함 B · 생성 뒤 갱신 size = 0 members[0] = null createMember 호출 같은 잘못된 나이 예외로 중단 예외 직후 상태 관찰 size = 0 members[0] = null 저장·size++ 미도달 점선: 예외 뒤 상태 관찰 원래 실행은 예외로 중단 CLI는 먼저 나이 범위 검사
객체 생성에 성공한 뒤 저장하고 size를 늘립니다 같은size0/null과유효하지않은나이로두독립비교. 증가먼저size1/null오염,생성먼저예외시size0/null유지. 관찰점선은catch후재개보장아님;전체CLI는사전나이검사. A · size 먼저 증가 size = 0 members[0] = null index=0; size++; size는 이미 1 createMember 호출 같은 잘못된 나이 예외로 중단 예외 직후 상태 관찰 size = 1 members[0] = null 빈 칸을 사용 구간에 포함 B · 생성 뒤 갱신 size = 0 members[0] = null createMember 호출 같은 잘못된 나이 예외로 중단 예외 직후 상태 관찰 size = 0 members[0] = null 저장·size++ 미도달 점선: 예외 뒤 상태 관찰 원래 실행은 예외로 중단 CLI는 먼저 나이 범위 검사
size를 먼저 증가
  1. index = size, size++로 index는 0, size는 1입니다.
  2. 잘못된 나이로 createMember가 IllegalArgumentException을 던집니다.
  3. members[index] = member는 실행되지 않습니다.

결과: size = 1, members[0] = null. 빈 칸이 사용 구간에 포함됩니다.

생성 성공 뒤에만 갱신
  1. size = 0에서 바로 생성 함수를 호출합니다.
  2. 같은 예외가 발생합니다.
  3. 배열 저장과 size++ 모두 실행되지 않습니다.

결과: size = 0, members[0] = null. 사용 구간은 여전히 비어 있습니다.

실제 정의에 맞는 호출은 createMember(email, age, false)입니다. 두 순서를 비교할 때는 같은 유효하지 않은 나이와 나머지 입력을 사용합니다.

이는 생성 실패를 따로 유발한 비교입니다. 전체 CLI는 readMemberAge에서 범위를 먼저 검사합니다. 예외 직후 상태는 진단용 catch에서 관찰할 수 있지만, 원래 조각이 예외 뒤에도 계속 실행한다는 뜻은 아닙니다.

이 예의 조건

  • 두 독립 버전은 같은 잘못된 나이 입력에서 시작한다.
  • 예외 뒤 상태는 관찰 설명이며 원 실행은 예외로 중단한다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2같은 size 0/null과 유효하지 않은 나이로 두 버전을 독립적으로 비교한다. 먼저 증가시키면 size 1/null로 오염되고, 먼저 생성하면 예외 시 size 0/null을 유지한다. 관찰 점선은 catch 후 재개를 보장하지 않으며 전체 CLI는 나이를 사전에 검사한다.
원 도표의 설명과 주석
구분내용
설명같은 size 0/null과 유효하지 않은 나이로 두 버전을 독립적으로 비교한다. 먼저 증가시키면 size 1/null로 오염되고, 먼저 생성하면 예외 시 size 0/null을 유지한다. 관찰 점선은 catch 후 재개를 보장하지 않으며 전체 CLI는 나이를 사전에 검사한다.
전체 읽기 정보생성 실패 때 size를 보존하는 갱신 순서 생성 실패 때 size를 보존하는 갱신 순서 생성이 실패하는 같은 조건에서 size를 먼저 늘리면 null 슬롯이 사용 구간에 들어가고, 생성 후에만 저장과 증가를 하면 size와 배열이 그대로입니다. 객체 생성에 성공한 뒤 저장하고 size를 늘립니다 같은 생성 실패를 두 갱신 순서로 비교합니다. size를 먼저 늘리면 null 칸을 회원 범위에 포함하게 됩니다. 실패 시 상태 비교 · 시작은 size = 0, members[0] = null 관찰 지점 size를 먼저 증가 생성 성공 뒤에만 갱신 생성 호출 전 index = size 후 size++ : index = 0 , size = 1 size = 0 그대로 생성에서 예외 잘못된 나이로 createMember 가 IllegalArgumentException 을 던짐 같은 예외가 발생함 도달하지 못한 연산 members[index] = member 미실행 members[size] = member 와 size++ 모두 미실행 예외 직후 상태 size = 1 , members[0] = null . 빈 칸이 사용 구간에 포함됨 size = 0 , members[0] = null . 사용 구간은 여전히 비어 있음 size를 먼저 증가 index = size , size++ 로 index는 0, size는 1입니다. 잘못된 나이로 createMember 가 IllegalArgumentException 을 던집니다. members[index] = member 는 실행되지 않습니다. 결과: size = 1 , members[0] = null . 빈 칸이 사용 구간에 포함됩니다. 생성 성공 뒤에만 갱신 size = 0 에서 바로 생성 함수를 호출합니다. 같은 예외가 발생합니다. 배열 저장과 size++ 모두 실행되지 않습니다. 결과: size = 0 , members[0] = null . 사용 구간은 여전히 비어 있습니다. 실제 정의에 맞는 호출은 createMember(email, age, false) 입니다. 두 순서를 비교할 때는 같은 유효하지 않은 나이와 나머지 입력을 사용합니다. 이는 생성 실패를 따로 유발한 비교입니다. 전체 CLI는 readMemberAge 에서 범위를 먼저 검사합니다. 예외 직후 상태는 진단용 catch에서 관찰할 수 있지만, 원래 조각이 예외 뒤에도 계속 실행한다는 뜻은 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
동일 시작

빈 첫 칸과 size 0을 둡니다.

  • 두 버전 size: 0
  • members[0]: null
  • 변경 순서만 다르게 비교한다.
생성 전 차이

A는 size를 먼저 늘리고 B는 생성부터 호출합니다.

  • A size: 1
  • B size: 0
  • 생성 입력: 같은 잘못된 나이
  • A의 부수 효과는 아직 생성 성공으로 보호되지 않았다.
생성 예외

두 생성이 실패해 저장에 도달하지 않습니다.

  • A 결과: size 1/null
  • B 결과: size 0/null
  • 예외는 앞서 실행한 size 증가를 되돌리지 않는다.

입력 변환 경계

Scanner.nextInt() 뒤 nextLine()이 빈 줄을 읽는 문제를 피하기 위해 이번 버전은 모든 입력을 nextLine()으로 받고 숫자만 Integer.parseInt로 변환합니다.

입력 경계
System.out.print("회원 나이(세): ");
String rawAge = scanner.nextLine();
int age = Integer.parseInt(rawAge);

숫자가 아닌 입력은 NumberFormatException을 일으킵니다.

대화형 프로그램이 종료되지 않도록 입력 메서드에서 처리하고 다시 묻습니다.

변환된 int가 회원 나이 정책인 14–120 범위인지도 별도로 검사해야 합니다.

“숫자로 변환 가능함”과 “유효한 회원 나이임”은 다른 계약입니다.


전체 프로그램 구성

다음 두 파일은 같은 디렉터리에 둡니다.

이 실습은 사용자가 끝까지 줄을 입력하는 대화형 실행을 전제로 합니다. 세 회원의 이메일·나이를 채우기 전에 입력 스트림이 끝나면 nextLine()이 실패하며, 아직 EOF 복구는 구현하지 않았습니다. 이메일 검사는 앞뒤 공백을 제거한 뒤 비어 있는지만 확인합니다. 일반적인 이메일 주소의 전체 형식을 검증하는 코드는 아닙니다. 검색은 이 프로그램의 선택에 따라 대소문자를 무시하고 첫 일치 회원을 반환합니다.

src/members/ch04/finalv/MemberRegistryCli.java
import java.util.Scanner;

public class MemberRegistryCli {
    private static final int CAPACITY = 3;

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        Member[] members = new Member[CAPACITY];
        int size = 0;

        while (size < members.length) {
            System.out.println("\n[" + (size + 1) + "/" + members.length + "]");
            String email = readEmail(scanner);
            int age = readMemberAge(scanner);

            Member member = createMember(email, age, false);
            members[size] = member;
            size++;
        }

        printMembers(members, size);

        System.out.print("\n찾을 이메일: ");
        String keyword = scanner.nextLine().trim();
        Member found = findByEmail(members, size, keyword);
        printSearchResult(found);
    }

    static String readEmail(Scanner scanner) {
        while (true) {
            System.out.print("이메일: ");
            String email = scanner.nextLine().trim();
            if (!email.isEmpty()) {
                return email;
            }
            System.out.println("이메일은 비워 둘 수 없습니다.");
        }
    }

    static int readMemberAge(Scanner scanner) {
        while (true) {
            System.out.print("회원 나이(세): ");
            String input = scanner.nextLine().trim();
            try {
                int age = Integer.parseInt(input);
                if (age >= 14 && age <= 120) {
                    return age;
                }
                System.out.println("회원 나이는 14세 이상 120세 이하여야 합니다.");
            } catch (NumberFormatException exception) {
                System.out.println("정수를 입력해 주세요.");
            }
        }
    }

    static Member createMember(
        String email,
        int age,
        boolean active
    ) {
        if (email == null || email.isBlank() || age < 14 || age > 120) {
            throw new IllegalArgumentException("invalid member");
        }
        Member member = new Member();
        member.email = email;
        member.age = age;
        member.active = active;
        return member;
    }

    static void printMembers(Member[] members, int size) {
        System.out.println("\n== 회원 저장소 ==");
        for (int i = 0; i < size; i++) {
            Member member = members[i];
            System.out.printf(
                "%d. %s / %d세 / %s%n",
                i + 1,
                member.email,
                member.age,
                member.active ? "활성" : "비활성"
            );
        }
    }

    static Member findByEmail(
        Member[] members,
        int size,
        String email
    ) {
        for (int i = 0; i < size; i++) {
            Member member = members[i];
            if (member.email.equalsIgnoreCase(email)) {
                return member;
            }
        }
        return null;
    }

    static void printSearchResult(Member found) {
        if (found == null) {
            System.out.println("일치하는 회원이 없습니다.");
            return;
        }
        System.out.println(
            "검색 결과: " + found.email + " / " + found.age + "세"
        );
    }
}
컴파일과 실행
javac -d out src/members/ch04/finalv/Member.java src/members/ch04/finalv/MemberRegistryCli.java
java -cp out MemberRegistryCli
실행 예
[1/3]
이메일: [email protected]
회원 나이(세): 35

[2/3]
이메일: [email protected]
회원 나이(세): 삼십
정수를 입력해 주세요.
회원 나이(세): 30

[3/3]
이메일: [email protected]
회원 나이(세): 20

== 회원 저장소 ==
1. [email protected] / 35세 / 비활성
2. [email protected] / 30세 / 비활성
3. [email protected] / 20세 / 비활성

찾을 이메일: [email protected]
검색 결과: [email protected] / 30세

이 프로그램에는 지금까지 배운 값 복사가 모두 들어 있습니다.

  • email과 keyword에는 문자열 참조가 저장됩니다.
  • age와 size에는 정수 값이 저장됩니다.
  • createMember는 새 객체 참조를 반환합니다.
  • members[size] = member는 그 참조를 배열 칸에 복사합니다.
  • printMembers와 findByEmail의 매개변수에는 배열 참조와 숫자 size가 각각 복사됩니다.
  • findByEmail은 찾은 객체의 참조 또는 null을 반환합니다.

MemberRegistryCli의 입력 반복과 검색 진입

용량 조건을 확인하고 입력값 확정·Member 생성·배열 저장·size 증가를 반복해 세 회원 후 출력과 검색을 합니다. 현재 슬롯과 읽기/확정 단계를 고정하여 CLI의 실제 순서를 보여 줄 수 있습니다.

MemberRegistryCli의 입력 반복과 검색 진입

길이 3의 배열에서 size가 3보다 작은 동안 입력, 생성, 저장, 증가를 반복하고 세 회원을 채운 뒤 전체 출력과 이메일 검색으로 진행합니다.

세 회원을 저장한 뒤 출력과 검색으로 진행합니다

size < 3 동안 입력 확정→객체 생성→저장→size 증가를 반복합니다. 세 회원을 채우면 전체 출력과 이메일 검색을 실행합니다.

MemberRegistryCli의 입력 반복과 검색 진입size는 0에서 시작합니다. size가 3보다 작으면 입력 메서드, 객체 생성, 배열 저장과 size 증가를 거쳐 반복 조건으로 돌아갑니다. 3이면 전체 출력과 이메일 검색 후 종료합니다. 예 아니요 다음 회원 시작 · 배열 생성length = 3 · size = 0 size < 3 ?반복 조건 입력값 확정readEmail / readMemberAge거절 시 메서드 내부 재입력 새 Member 생성createMember(email, age, false)반환 참조를 member에 보관 저장 후 개수 증가members[size] = membersize++ 세 회원 입력 완료printMembers검색 후 결과 출력 종료
세 회원을 저장한 뒤 출력과 검색으로 진행합니다 size0/length3시작,참이면입력→생성→저장후size++→조건복귀,거짓size3이면출력/검색/결과→종료.재입력메서드내부/14..120/EOF없음조건보존. 배열 생성 · 시작 length=3 · size=0 size < 3 ? 반복 조건 예 입력값 확정 readEmail readMemberAge 거절 시 내부 재입력 새 Member 생성 createMember (email,age,false) 반환 참조를 보관 저장 후 개수 증가 members[size] = member; size++ 아니요 출력·검색·결과 printMembers findByEmail printSearchResult 종료 EOF 복구 구현은 없음
  1. 길이 3인 Member[]를 만들고 size = 0으로 시작합니다.
  2. size < 3이면 readEmail과 readMemberAge로 입력값을 확정합니다. 거절된 값은 각 메서드 내부에서 다시 묻습니다.
  3. createMember(email, age, false)의 새 객체 참조를 member에 보관합니다.
  4. members[size] = member로 먼저 저장한 뒤 size++합니다. 반복 조건으로 돌아갑니다.
  5. 세 회원을 채워 조건이 거짓이면 printMembers로 전체 출력하고, 이메일 검색과 결과 출력을 한 뒤 종료합니다.

검색은 findByEmail의 참조 또는 null 반환을 printSearchResult가 처리합니다. 입력 나이는 정수 14~120, 이메일은 trim 후 비어 있지 않은지만 검사합니다.

이 흐름은 본문의 전체 CLI가 정상 입력 스트림으로 진행하는 경로입니다. 조기 종료 메뉴나 EOF 복구는 구현되어 있지 않습니다.

이 예의 조건

  • 배열 길이 3, size 0에서 시작하며 거절 입력은 메서드 내부에서 재입력합니다.
관계도의 보충 설명
원 도표의 설명과 주석
구분내용
전체 읽기 정보MemberRegistryCli의 입력 반복과 검색 진입 MemberRegistryCli의 입력 반복과 검색 진입 길이 3의 배열에서 size가 3보다 작은 동안 입력, 생성, 저장, 증가를 반복하고 세 회원을 채운 뒤 전체 출력과 이메일 검색으로 진행합니다. 세 회원을 저장한 뒤 출력과 검색으로 진행합니다 size < 3 동안 입력 확정→객체 생성→저장→size 증가를 반복합니다. 세 회원을 채우면 전체 출력과 이메일 검색을 실행합니다. 길이 3인 Member[] 를 만들고 size = 0 으로 시작합니다. size < 3 이면 readEmail 과 readMemberAge 로 입력값을 확정합니다. 거절된 값은 각 메서드 내부에서 다시 묻습니다. createMember(email, age, false) 의 새 객체 참조를 member 에 보관합니다. members[size] = member 로 먼저 저장한 뒤 size++ 합니다. 반복 조건으로 돌아갑니다. 세 회원을 채워 조건이 거짓이면 printMembers 로 전체 출력하고, 이메일 검색과 결과 출력을 한 뒤 종료합니다. 검색은 findByEmail 의 참조 또는 null 반환을 printSearchResult 가 처리합니다. 입력 나이는 정수 14~120, 이메일은 trim 후 비어 있지 않은지만 검사합니다. 이 흐름은 본문의 전체 CLI가 정상 입력 스트림으로 진행하는 경로입니다. 조기 종료 메뉴나 EOF 복구는 구현되어 있지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
반복 진입

size<3에서 readEmail과 readMemberAge를 호출합니다.

  • 시작: size 0, length 3
  • 입력: 확정값 반환까지 재입력
  • 유효 입력이 돌아와야 객체 생성 단계로 갑니다.
생성·저장

createMember(email,age,false)의 참조를 members[size]에 둡니다.

  • 생성: 새 Member
  • 다음: size++
  • 저장 뒤 증가시켜 다음 빈 칸으로 넘어갑니다.
반복 끝 검색

세 회원을 저장한 size 3에서 printMembers와 email 검색을 수행합니다.

  • 조건: 3<3 false
  • 후속: 검색 결과 출력 후 종료
  • 입력 반복이 끝나야 전체 출력과 검색에 진입합니다.

MemberRegistryCli의 입력 반복과 검색 진입

용량 조건을 확인하고 입력값 확정·Member 생성·배열 저장·size 증가를 반복해 세 회원 후 출력과 검색을 합니다. 현재 슬롯과 읽기/확정 단계를 고정하여 CLI의 실제 순서를 보여 줄 수 있습니다.

MemberRegistryCli의 입력 반복과 검색 진입

길이 3의 배열에서 size가 3보다 작은 동안 입력, 생성, 저장, 증가를 반복하고 세 회원을 채운 뒤 전체 출력과 이메일 검색으로 진행합니다.

세 회원을 저장한 뒤 출력과 검색으로 진행합니다

size < 3 동안 입력 확정→객체 생성→저장→size 증가를 반복합니다. 세 회원을 채우면 전체 출력과 이메일 검색을 실행합니다.

MemberRegistryCli의 입력 반복과 검색 진입size는 0에서 시작합니다. size가 3보다 작으면 입력 메서드, 객체 생성, 배열 저장과 size 증가를 거쳐 반복 조건으로 돌아갑니다. 3이면 전체 출력과 이메일 검색 후 종료합니다. 예 아니요 다음 회원 시작 · 배열 생성length = 3 · size = 0 size < 3 ?반복 조건 입력값 확정readEmail / readMemberAge거절 시 메서드 내부 재입력 새 Member 생성createMember(email, age, false)반환 참조를 member에 보관 저장 후 개수 증가members[size] = membersize++ 세 회원 입력 완료printMembers검색 후 결과 출력 종료
세 회원을 저장한 뒤 출력과 검색으로 진행합니다 size0/length3시작,참이면입력→생성→저장후size++→조건복귀,거짓size3이면출력/검색/결과→종료.재입력메서드내부/14..120/EOF없음조건보존. 배열 생성 · 시작 length=3 · size=0 size < 3 ? 반복 조건 예 입력값 확정 readEmail readMemberAge 거절 시 내부 재입력 새 Member 생성 createMember (email,age,false) 반환 참조를 보관 저장 후 개수 증가 members[size] = member; size++ 아니요 출력·검색·결과 printMembers findByEmail printSearchResult 종료 EOF 복구 구현은 없음
  1. 길이 3인 Member[]를 만들고 size = 0으로 시작합니다.
  2. size < 3이면 readEmail과 readMemberAge로 입력값을 확정합니다. 거절된 값은 각 메서드 내부에서 다시 묻습니다.
  3. createMember(email, age, false)의 새 객체 참조를 member에 보관합니다.
  4. members[size] = member로 먼저 저장한 뒤 size++합니다. 반복 조건으로 돌아갑니다.
  5. 세 회원을 채워 조건이 거짓이면 printMembers로 전체 출력하고, 이메일 검색과 결과 출력을 한 뒤 종료합니다.

검색은 findByEmail의 참조 또는 null 반환을 printSearchResult가 처리합니다. 입력 나이는 정수 14~120, 이메일은 trim 후 비어 있지 않은지만 검사합니다.

이 흐름은 본문의 전체 CLI가 정상 입력 스트림으로 진행하는 경로입니다. 조기 종료 메뉴나 EOF 복구는 구현되어 있지 않습니다.

이 예의 조건

  • 배열 길이 3, size 0에서 시작하며 거절 입력은 메서드 내부에서 재입력합니다.

관계도의 보충 설명

원 도표의 설명과 주석
구분내용
전체 읽기 정보MemberRegistryCli의 입력 반복과 검색 진입 MemberRegistryCli의 입력 반복과 검색 진입 길이 3의 배열에서 size가 3보다 작은 동안 입력, 생성, 저장, 증가를 반복하고 세 회원을 채운 뒤 전체 출력과 이메일 검색으로 진행합니다. 세 회원을 저장한 뒤 출력과 검색으로 진행합니다 size < 3 동안 입력 확정→객체 생성→저장→size 증가를 반복합니다. 세 회원을 채우면 전체 출력과 이메일 검색을 실행합니다. 길이 3인 Member[] 를 만들고 size = 0 으로 시작합니다. size < 3 이면 readEmail 과 readMemberAge 로 입력값을 확정합니다. 거절된 값은 각 메서드 내부에서 다시 묻습니다. createMember(email, age, false) 의 새 객체 참조를 member 에 보관합니다. members[size] = member 로 먼저 저장한 뒤 size++ 합니다. 반복 조건으로 돌아갑니다. 세 회원을 채워 조건이 거짓이면 printMembers 로 전체 출력하고, 이메일 검색과 결과 출력을 한 뒤 종료합니다. 검색은 findByEmail 의 참조 또는 null 반환을 printSearchResult 가 처리합니다. 입력 나이는 정수 14~120, 이메일은 trim 후 비어 있지 않은지만 검사합니다. 이 흐름은 본문의 전체 CLI가 정상 입력 스트림으로 진행하는 경로입니다. 조기 종료 메뉴나 EOF 복구는 구현되어 있지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
반복 진입

size<3에서 readEmail과 readMemberAge를 호출합니다.

  • 시작: size 0, length 3
  • 입력: 확정값 반환까지 재입력
  • 유효 입력이 돌아와야 객체 생성 단계로 갑니다.
생성·저장

createMember(email,age,false)의 참조를 members[size]에 둡니다.

  • 생성: 새 Member
  • 다음: size++
  • 저장 뒤 증가시켜 다음 빈 칸으로 넘어갑니다.
반복 끝 검색

세 회원을 저장한 size 3에서 printMembers와 email 검색을 수행합니다.

  • 조건: 3<3 false
  • 후속: 검색 결과 출력 후 종료
  • 입력 반복이 끝나야 전체 출력과 검색에 진입합니다.

객체 참조와 변경 전파

findByEmail이 반환하는 것은 복제된 회원이 아니라 배열이 가리키는 객체의 참조입니다.

따라서 검색 결과를 통해 필드를 바꾸면 전체 출력에서도 변경이 보입니다.

검색 결과로 활성 처리
Member found = findByEmail(members, size, "[email protected]");
if (found != null) {
    found.active = true;
}
printMembers(members, size);

이 동작이 의도라면 검색 메서드는 편리합니다.

검색 결과를 수정본 초안으로 사용하려던 것이라면 공유 참조가 버그가 됩니다.

반환 타입이 같아도 호출자가 그 참조로 무엇을 할지에 따라 복사 정책이 달라집니다.

검색 결과와 배열 칸이 가리키는 같은 회원

검색이 배열 칸의 같은 Member 참조를 반환하여 found.active=true가 저장소에서도 보입니다. 검색 전후와 필드 false→true 전환을 구분할 가치가 있습니다.

검색 결과와 배열 칸이 가리키는 같은 회원

본문 예제의 kim 회원을 검색하면 found와 members[0]이 같은 객체를 가리키므로 found.active를 true로 바꾸면 배열을 통해 읽어도 true입니다.

검색 결과는 배열과 같은 회원을 참조합니다

kim 회원 검색의 성공 경로입니다. found.active를 바꾸면 members[0]에서도 변경된 같은 필드를 읽습니다.

검색 결과와 배열 칸이 가리키는 같은 회원본문 실행 예의 세 회원을 저장한 뒤 [email protected]을 검색한 성공 경로입니다. members[0]과 found의 참조는 같은 kim 회원에 도달하며 found.active = true는 그 객체의 필드를 바꿉니다. 참조 참조 members[0]배열 칸에 저장된 참조 found검색이 반환한 참조를 보관 같은 Member 인스턴스 email: [email protected] · age: 35 active: false → true
검색 결과는 배열과 같은 회원을 참조합니다 실제kim/35객체를members0/found동시에참조. 성공nonnull에서activefalse→true가공유객체필드에보이고검색복제없음. members[0] 배열 칸의 참조 found 검색 반환 참조 같은 Member 한 개 [email protected] age = 35 active: false → true found != null인 성공 경로 실선: 현재 보관한 참조 검색은 객체를 복제하지 않음
배열 칸 · members[0]
본문 실행 예의 kim 회원 객체를 참조합니다.
검색 결과 · found
findByEmail(members, size, "[email protected]")이 반환한 참조를 보관합니다. 배열 칸과 같은 객체를 가리킵니다.
공유된 Member 한 개
email = [email protected], age = 35입니다. found.active = true는 이 객체의 active를 false에서 true로 바꾸므로 members[0].active로 읽어도 true입니다.

본문 실행 예의 세 회원을 저장한 뒤, findByEmail(members, size, "[email protected]")과 if (found != null) found.active = true;를 수행한 성공 경로입니다.

검색은 새 Member를 복제하지 않습니다. printMembers도 배열의 같은 참조를 따라 읽어 변경된 활성 상태를 출력합니다. 검색 실패의 null은 가드에서 걸러져 필드를 바꾸지 않습니다.

이 예의 조건

관계도의 보충 설명
원 도표의 설명과 주석
구분내용
전체 읽기 정보검색 결과와 배열 칸이 가리키는 같은 회원 검색 결과와 배열 칸이 가리키는 같은 회원 본문 예제의 kim 회원을 검색하면 found와 members[0]이 같은 객체를 가리키므로 found.active를 true로 바꾸면 배열을 통해 읽어도 true입니다. 검색 결과는 배열과 같은 회원을 참조합니다 kim 회원 검색의 성공 경로입니다. found.active를 바꾸면 members[0]에서도 변경된 같은 필드를 읽습니다. 배열 칸 · members[0] 본문 실행 예의 kim 회원 객체를 참조합니다. 검색 결과 · found findByEmail(members, size, "[email protected]") 이 반환한 참조를 보관합니다. 배열 칸과 같은 객체를 가리킵니다. 공유된 Member 한 개 email = [email protected] , age = 35 입니다. found.active = true 는 이 객체의 active를 false에서 true로 바꾸므로 members[0].active 로 읽어도 true입니다. 본문 실행 예의 세 회원을 저장한 뒤, findByEmail(members, size, "[email protected]") 과 if (found != null) found.active = true; 를 수행한 성공 경로입니다. 검색은 새 Member를 복제하지 않습니다. printMembers 도 배열의 같은 참조를 따라 읽어 변경된 활성 상태를 출력합니다. 검색 실패의 null 은 가드에서 걸러져 필드를 바꾸지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
저장된 회원

members[0]이 Kim 35, false 객체를 참조합니다.

  • 배열 칸은 객체 자체가 아닌 참조값을 보관합니다.
검색 반환

found가 검색에서 같은 참조를 받습니다.

  • members[0]/found: 같은 Member
  • 복사 객체: 없음
  • 검색 반환값은 새 회원 사본이 아닙니다.
활성 변경

found.active=true가 공유 객체를 바꿉니다.

  • active: false→true
  • 배열에서 읽기: true
  • 검색 결과 경로의 쓰기와 배열 경로의 읽기가 같은 필드에 닿습니다.

검색 결과와 배열 칸이 가리키는 같은 회원

검색이 배열 칸의 같은 Member 참조를 반환하여 found.active=true가 저장소에서도 보입니다. 검색 전후와 필드 false→true 전환을 구분할 가치가 있습니다.

검색 결과와 배열 칸이 가리키는 같은 회원

본문 예제의 kim 회원을 검색하면 found와 members[0]이 같은 객체를 가리키므로 found.active를 true로 바꾸면 배열을 통해 읽어도 true입니다.

검색 결과는 배열과 같은 회원을 참조합니다

kim 회원 검색의 성공 경로입니다. found.active를 바꾸면 members[0]에서도 변경된 같은 필드를 읽습니다.

검색 결과와 배열 칸이 가리키는 같은 회원본문 실행 예의 세 회원을 저장한 뒤 [email protected]을 검색한 성공 경로입니다. members[0]과 found의 참조는 같은 kim 회원에 도달하며 found.active = true는 그 객체의 필드를 바꿉니다. 참조 참조 members[0]배열 칸에 저장된 참조 found검색이 반환한 참조를 보관 같은 Member 인스턴스 email: [email protected] · age: 35 active: false → true
검색 결과는 배열과 같은 회원을 참조합니다 실제kim/35객체를members0/found동시에참조. 성공nonnull에서activefalse→true가공유객체필드에보이고검색복제없음. members[0] 배열 칸의 참조 found 검색 반환 참조 같은 Member 한 개 [email protected] age = 35 active: false → true found != null인 성공 경로 실선: 현재 보관한 참조 검색은 객체를 복제하지 않음
배열 칸 · members[0]
본문 실행 예의 kim 회원 객체를 참조합니다.
검색 결과 · found
findByEmail(members, size, "[email protected]")이 반환한 참조를 보관합니다. 배열 칸과 같은 객체를 가리킵니다.
공유된 Member 한 개
email = [email protected], age = 35입니다. found.active = true는 이 객체의 active를 false에서 true로 바꾸므로 members[0].active로 읽어도 true입니다.

본문 실행 예의 세 회원을 저장한 뒤, findByEmail(members, size, "[email protected]")과 if (found != null) found.active = true;를 수행한 성공 경로입니다.

검색은 새 Member를 복제하지 않습니다. printMembers도 배열의 같은 참조를 따라 읽어 변경된 활성 상태를 출력합니다. 검색 실패의 null은 가드에서 걸러져 필드를 바꾸지 않습니다.

이 예의 조건

관계도의 보충 설명

원 도표의 설명과 주석
구분내용
전체 읽기 정보검색 결과와 배열 칸이 가리키는 같은 회원 검색 결과와 배열 칸이 가리키는 같은 회원 본문 예제의 kim 회원을 검색하면 found와 members[0]이 같은 객체를 가리키므로 found.active를 true로 바꾸면 배열을 통해 읽어도 true입니다. 검색 결과는 배열과 같은 회원을 참조합니다 kim 회원 검색의 성공 경로입니다. found.active를 바꾸면 members[0]에서도 변경된 같은 필드를 읽습니다. 배열 칸 · members[0] 본문 실행 예의 kim 회원 객체를 참조합니다. 검색 결과 · found findByEmail(members, size, "[email protected]") 이 반환한 참조를 보관합니다. 배열 칸과 같은 객체를 가리킵니다. 공유된 Member 한 개 email = [email protected] , age = 35 입니다. found.active = true 는 이 객체의 active를 false에서 true로 바꾸므로 members[0].active 로 읽어도 true입니다. 본문 실행 예의 세 회원을 저장한 뒤, findByEmail(members, size, "[email protected]") 과 if (found != null) found.active = true; 를 수행한 성공 경로입니다. 검색은 새 Member를 복제하지 않습니다. printMembers 도 배열의 같은 참조를 따라 읽어 변경된 활성 상태를 출력합니다. 검색 실패의 null 은 가드에서 걸러져 필드를 바꾸지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
저장된 회원

members[0]이 Kim 35, false 객체를 참조합니다.

  • 배열 칸은 객체 자체가 아닌 참조값을 보관합니다.
검색 반환

found가 검색에서 같은 참조를 받습니다.

  • members[0]/found: 같은 Member
  • 복사 객체: 없음
  • 검색 반환값은 새 회원 사본이 아닙니다.
활성 변경

found.active=true가 공유 객체를 바꿉니다.

  • active: false→true
  • 배열에서 읽기: true
  • 검색 결과 경로의 쓰기와 배열 경로의 읽기가 같은 필드에 닿습니다.

지금은 필드가 외부에 열려 있어 어느 메서드든 age = -1 같은 잘못된 상태를 만들 수 있습니다.

다음 장에서 접근 제어와 객체 메서드를 도입하면 “활성 처리”와 “나이 변경”의 규칙을 객체가 지키도록 개선할 수 있습니다.


연습 문제

종합 프로그램에 다음 기능을 추가하세요. 배열과 사용 구간의 회원은 null이 아니며, size는 0 이상 용량 이하라는 전제를 유지합니다. 독립적으로 확인할 데이터는 저장 순서대로 김 회원 35세, 이 회원 40세, 박 회원 40세입니다. 먼저 누구를 반환해야 하는지 예측하고, size=0인 별도 호출 결과도 적어 보세요.

  1. findOldest(Member[] members, int size) 메서드를 작성합니다.
  2. 회원이 하나 이상이라는 전제를 두지 말고 size == 0이면 null을 반환합니다.
  3. 가장 높은 나이가 같은 회원이 여러 명이면 먼저 저장된 객체를 반환합니다.
  4. 반환된 객체의 나이를 바꾸면 배열에서도 변경이 보이는지 확인합니다.
  5. 독립적인 결과가 필요하다면 어떤 복사가 추가되어야 하는지 설명합니다.
해설 보기
src/members/ch04/exercise/OldestMember.java
static Member findOldest(Member[] members, int size) {
    if (size < 0 || size > members.length) {
        throw new IllegalArgumentException("잘못된 회원 수: " + size);
    }
    if (size == 0) {
        return null;
    }

    Member oldest = members[0];
    for (int i = 1; i < size; i++) {
        Member candidate = members[i];
        if (candidate.age > oldest.age) {
            oldest = candidate;
        }
    }
    return oldest;
}
호출 예
Member oldest = findOldest(members, size);
if (oldest == null) {
    System.out.println("회원이 없습니다.");
} else {
    System.out.println("최고 연령 회원=" + oldest.email);
    oldest.age += 5;
}

비교 연산을 >로 두었기 때문에 같은 나이가 나오면 기존 oldest를 유지하고 먼저 저장된 회원이 선택됩니다. 위 연습 데이터에서는 김 회원 35로 시작해 이 회원 40에서 후보를 바꾸고, 박 회원 40은 더 크지 않아 이 회원을 유지합니다. size=0이면 배열의 0번 칸을 읽기 전에 null을 반환합니다.

반환값은 배열 칸과 같은 객체를 가리키므로 oldest.age += 5는 배열에서 읽는 값도 바꿉니다.

독립적인 결과가 필요하면 새 Member를 만들고 세 필드를 복사한 뒤 그 참조를 반환해야 합니다.


다음 단계의 설계 과제

프로그램은 실행되지만 설계상 불편이 남아 있습니다.

  • 필드 초기화 순서와 유효성 검사를 createMember 호출자가 지켜야 합니다.
  • 모든 필드가 같은 패키지의 코드에 열려 있어 잘못된 값을 직접 대입할 수 있습니다.
  • 활성 처리와 출력 형식이 객체 밖의 여러 메서드에 흩어질 수 있습니다.
  • 고정 배열은 용량이 차면 더 이상 추가할 수 없습니다.
  • 검색 실패를 null로 표현해 호출자가 검사를 잊을 수 있습니다.

이 문제들은 한 번에 해결하지 않습니다.

생성자, 캡슐화, 컬렉션, Optional을 배울 때 같은 회원 저장소를 다시 열어 각 도구가 어느 경계를 개선하는지 비교합니다.


클래스와 참조를 배우는 목적은 문법을 늘리는 것이 아니라 데이터가 함께 움직이는 경계와 객체에 도달하는 경로를 코드에서 읽는 것입니다.

이제 회원 저장소는 한 회원을 객체로 표현하지만 행동과 유효성은 아직 바깥에 있습니다.

다음 객체 지향 장에서는 절차를 객체의 책임으로 옮겨 이 허술한 경계를 단단하게 만듭니다.