클래스·참조 종합 실습
입력에서 객체 생성, 배열 저장, 검색과 출력까지 값과 참조의 흐름을 하나의 프로그램으로 연결합니다.
지금까지는 참조의 한 장면을 보기 위해 작은 프로그램을 따로 실행했습니다.
이제 회원 저장소 CLI에 세 개의 회원 정보를 입력하고, 객체 배열에 저장하고, 이메일로 검색하고, 전체 회원을 출력해 봅시다.
이 종합 예제의 목적은 기능을 많이 만드는 데 있지 않습니다.
입력 문자열과 숫자 값, 새 객체의 참조, 배열 칸의 참조가 어느 경계에서 복사되는지를 끝까지 추적하는 데 있습니다.
이번 버전의 Member는 여전히 필드만 가진 데이터 객체입니다.
입력·검증·출력 메서드는 MemberRegistryCli에 있습니다.
다음 객체 지향 장에서는 이 책임 중 무엇을 객체 안으로 옮겨야 하는지 판단합니다.
데이터 타입 설계
Member는 앞 문서와 같은 세 필드를 가집니다.
public class Member {
String email;
int age;
boolean active;
}CLI는 최대 세 개를 입력받는 고정 배열과 실제 회원 수 size를 관리합니다.
Member[] members = new Member[3];
int size = 0;배열 길이는 용량이고 size는 채운 칸의 수입니다.
회원 생성에 실패했는데 size부터 증가시키면 빈 슬롯이 데이터 범위에 포함됩니다.
따라서 다음 순서를 지킵니다.
- 입력을 읽고 검증합니다.
- 완성된
Member를 만듭니다. members[size]에 참조를 저장합니다.- 저장에 성공한 뒤
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를 먼저 늘리면 null 슬롯이 사용 구간에 들어가고, 생성 후에만 저장과 증가를 하면 size와 배열이 그대로입니다.
객체 생성에 성공한 뒤 저장하고 size를 늘립니다
같은 생성 실패를 두 갱신 순서로 비교합니다. size를 먼저 늘리면 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에서 관찰할 수 있지만, 원래 조각이 예외 뒤에도 계속 실행한다는 뜻은 아닙니다.
이 예의 조건
- 두 독립 버전은 같은 잘못된 나이 입력에서 시작한다.
- 예외 뒤 상태는 관찰 설명이며 원 실행은 예외로 중단한다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 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을 둡니다. |
|
|
| 생성 전 차이 A는 size를 먼저 늘리고 B는 생성부터 호출합니다. |
|
|
| 생성 예외 두 생성이 실패해 저장에 도달하지 않습니다. |
|
|
생성 실패 때 size를 보존하는 갱신 순서
같은 생성 실패 입력에서 size를 먼저 올린 버전은 size=1/null, 생성 후 갱신 버전은 size=0/null이 남습니다. 예외가 앞서 실행한 부수 효과를 취소하지 않는다는 점을 갱신 순서별 결과로 확인할 수 있습니다.
생성이 실패하는 같은 조건에서 size를 먼저 늘리면 null 슬롯이 사용 구간에 들어가고, 생성 후에만 저장과 증가를 하면 size와 배열이 그대로입니다.
객체 생성에 성공한 뒤 저장하고 size를 늘립니다
같은 생성 실패를 두 갱신 순서로 비교합니다. size를 먼저 늘리면 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에서 관찰할 수 있지만, 원래 조각이 예외 뒤에도 계속 실행한다는 뜻은 아닙니다.
이 예의 조건
- 두 독립 버전은 같은 잘못된 나이 입력에서 시작한다.
- 예외 뒤 상태는 관찰 설명이며 원 실행은 예외로 중단한다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 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을 둡니다. |
|
|
| 생성 전 차이 A는 size를 먼저 늘리고 B는 생성부터 호출합니다. |
|
|
| 생성 예외 두 생성이 실패해 저장에 도달하지 않습니다. |
|
|
입력 변환 경계
Scanner.nextInt() 뒤 nextLine()이 빈 줄을 읽는 문제를 피하기 위해 이번 버전은 모든 입력을 nextLine()으로 받고 숫자만 Integer.parseInt로 변환합니다.
System.out.print("회원 나이(세): ");
String rawAge = scanner.nextLine();
int age = Integer.parseInt(rawAge);숫자가 아닌 입력은 NumberFormatException을 일으킵니다.
대화형 프로그램이 종료되지 않도록 입력 메서드에서 처리하고 다시 묻습니다.
변환된 int가 회원 나이 정책인 14–120 범위인지도 별도로 검사해야 합니다.
“숫자로 변환 가능함”과 “유효한 회원 나이임”은 다른 계약입니다.
전체 프로그램 구성
다음 두 파일은 같은 디렉터리에 둡니다.
이 실습은 사용자가 끝까지 줄을 입력하는 대화형 실행을 전제로 합니다. 세 회원의 이메일·나이를 채우기 전에 입력 스트림이 끝나면 nextLine()이 실패하며, 아직 EOF 복구는 구현하지 않았습니다. 이메일 검사는 앞뒤 공백을 제거한 뒤 비어 있는지만 확인합니다. 일반적인 이메일 주소의 전체 형식을 검증하는 코드는 아닙니다. 검색은 이 프로그램의 선택에 따라 대소문자를 무시하고 첫 일치 회원을 반환합니다.
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의 실제 순서를 보여 줄 수 있습니다.
길이 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 복구는 구현되어 있지 않습니다.
이 예의 조건
- 배열 길이 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를 호출합니다. |
|
|
| 생성·저장 createMember(email,age,false)의 참조를 members[size]에 둡니다. |
|
|
| 반복 끝 검색 세 회원을 저장한 size 3에서 printMembers와 email 검색을 수행합니다. |
|
|
MemberRegistryCli의 입력 반복과 검색 진입
용량 조건을 확인하고 입력값 확정·Member 생성·배열 저장·size 증가를 반복해 세 회원 후 출력과 검색을 합니다. 현재 슬롯과 읽기/확정 단계를 고정하여 CLI의 실제 순서를 보여 줄 수 있습니다.
길이 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 복구는 구현되어 있지 않습니다.
이 예의 조건
- 배열 길이 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를 호출합니다. |
|
|
| 생성·저장 createMember(email,age,false)의 참조를 members[size]에 둡니다. |
|
|
| 반복 끝 검색 세 회원을 저장한 size 3에서 printMembers와 email 검색을 수행합니다. |
|
|
객체 참조와 변경 전파
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]에서도 변경된 같은 필드를 읽습니다.
- 배열 칸 · 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은 가드에서 걸러져 필드를 바꾸지 않습니다.
이 예의 조건
- 세 회원 저장 뒤 [email protected] 성공 검색 경로입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 검색 결과와 배열 칸이 가리키는 같은 회원 검색 결과와 배열 칸이 가리키는 같은 회원 본문 예제의 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가 검색에서 같은 참조를 받습니다. |
|
|
| 활성 변경 found.active=true가 공유 객체를 바꿉니다. |
|
|
검색 결과와 배열 칸이 가리키는 같은 회원
검색이 배열 칸의 같은 Member 참조를 반환하여 found.active=true가 저장소에서도 보입니다. 검색 전후와 필드 false→true 전환을 구분할 가치가 있습니다.
본문 예제의 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은 가드에서 걸러져 필드를 바꾸지 않습니다.
이 예의 조건
- 세 회원 저장 뒤 [email protected] 성공 검색 경로입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 검색 결과와 배열 칸이 가리키는 같은 회원 검색 결과와 배열 칸이 가리키는 같은 회원 본문 예제의 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가 검색에서 같은 참조를 받습니다. |
|
|
| 활성 변경 found.active=true가 공유 객체를 바꿉니다. |
|
|
지금은 필드가 외부에 열려 있어 어느 메서드든 age = -1 같은 잘못된 상태를 만들 수 있습니다.
다음 장에서 접근 제어와 객체 메서드를 도입하면 “활성 처리”와 “나이 변경”의 규칙을 객체가 지키도록 개선할 수 있습니다.
연습 문제
종합 프로그램에 다음 기능을 추가하세요. 배열과 사용 구간의 회원은 null이 아니며, size는 0 이상 용량 이하라는 전제를 유지합니다. 독립적으로 확인할 데이터는 저장 순서대로 김 회원 35세, 이 회원 40세, 박 회원 40세입니다. 먼저 누구를 반환해야 하는지 예측하고, size=0인 별도 호출 결과도 적어 보세요.
findOldest(Member[] members, int size)메서드를 작성합니다.- 회원이 하나 이상이라는 전제를 두지 말고
size == 0이면null을 반환합니다. - 가장 높은 나이가 같은 회원이 여러 명이면 먼저 저장된 객체를 반환합니다.
- 반환된 객체의 나이를 바꾸면 배열에서도 변경이 보이는지 확인합니다.
- 독립적인 결과가 필요하다면 어떤 복사가 추가되어야 하는지 설명합니다.
해설 보기
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을 배울 때 같은 회원 저장소를 다시 열어 각 도구가 어느 경계를 개선하는지 비교합니다.
클래스와 참조를 배우는 목적은 문법을 늘리는 것이 아니라 데이터가 함께 움직이는 경계와 객체에 도달하는 경로를 코드에서 읽는 것입니다.
이제 회원 저장소는 한 회원을 객체로 표현하지만 행동과 유효성은 아직 바깥에 있습니다.
다음 객체 지향 장에서는 절차를 객체의 책임으로 옮겨 이 허술한 경계를 단단하게 만듭니다.