절차 중심 설계 비용
평행 배열과 절차 중심 메서드의 결합을 관찰하고 데이터 묶음과 메서드 추출만으로 해결되지 않는 변경 비용을 찾습니다.
ch4에서는 이메일과 나이를 Member 객체 하나로 묶었습니다.
그러나 프로그램의 모든 행동이 main과 static 메서드에 남아 있으면, 객체는 값을 운반할 뿐 규칙은 여전히 바깥에 흩어집니다.
절차 지향이 잘못이라는 뜻은 아닙니다.
작은 순차 작업에는 명확하지만, 같은 상태를 바꾸는 절차가 늘어날 때 변경 비용이 커집니다.
생성자는 new가 객체를 만들 때 처음 실행되는 초기화 코드입니다.
클래스 이름과 같고 반환 타입을 적지 않습니다.
다음 정상 예제는 객체가 만들어지는 순간 이메일과 나이를 함께 받습니다.
class Member {
String email;
int age;
Member(String email, int age) {
this.email = email;
this.age = age;
}
}
Member member = new Member("choi@example.com", 40);this.email은 현재 만들고 있는 객체의 필드이고 오른쪽 email은 생성자가 받은 값입니다.
ch5-3에서 생성 순서와 this를 자세히 추적합니다.
병렬 배열 삭제 오류
public final class ParallelDeleteBug {
public static void main(String[] args) {
String[] emails = {"kim@example.com", "park@example.com", "lee@example.com"};
int[] age = {40, 30, 50};
int size = 3;
int target = 1;
for (int index = target; index < size - 1; index++) {
emails[index] = emails[index + 1];
}
size--;
for (int index = 0; index < size; index++) {
System.out.println(emails[index] + "=" + age[index]);
}
}
}kim@example.com=40
lee@example.com=30이메일 배열만 앞으로 당겨 lee@example.com이 원래 park@example.com의 30세와 결합했습니다.
두 배열의 같은 인덱스가 한 회원이라는 규칙을 모든 추가·삭제·정렬 절차가 기억해야 합니다.
컴파일러는 두 값의 관계를 알지 못하므로 정상 실행된 틀린 결과가 나옵니다.
데이터를 객체로 묶으면 함께 이동합니다.
Member[] members = {
new Member("kim@example.com", 40),
new Member("park@example.com", 30),
new Member("lee@example.com", 50)
};이제 배열 원소 하나가 이메일과 나이를 함께 가리킵니다.
하지만 삭제 범위, size 감소, 합계 계산 규칙은 여전히 외부 절차가 맡습니다.
데이터 묶음
public final class ProceduralMemberArray {
public static void main(String[] args) {
Member[] members = new Member[3];
int size = 0;
members[size++] = new Member("kim@example.com", 40);
members[size++] = new Member("lee@example.com", 50);
printMembers(members, size);
System.out.println("total=" + total(members, size));
}
private static void printMembers(Member[] members, int size) {
for (int index = 0; index < size; index++) {
Member member = members[index];
System.out.println(member.email + "=" + member.age);
}
}
private static int total(Member[] members, int size) {
int result = 0;
for (int index = 0; index < size; index++) {
result += members[index].age;
}
return result;
}
private static final class Member {
String email;
int age;
Member(String email, int age) {
this.email = email;
this.age = age;
}
}
}kim@example.com=40
lee@example.com=50
total=90이메일과 나이의 평행 관계는 Member 안으로 들어갔습니다.
정렬할 때도 객체 참조 하나만 이동하면 됩니다.
이것이 데이터 묶음의 효과입니다.
반면 member.age = -10처럼 외부에서 필드를 직접 바꿀 수 있고, 출력과 합계 규칙은 각각 static 메서드가 객체 내부를 해석합니다.
메서드 추출
main 안에 추가·목록·합계를 모두 쓰면 실행 순서는 한눈에 보이지만 규칙의 경계는 흐립니다.
다음처럼 메서드를 추출하면 main은 시나리오를, 각 메서드는 세부 작업을 보여 줍니다.
public final class ExtractedMemberProcedures {
public static void main(String[] args) {
Member[] members = new Member[3];
int size = 0;
size = add(members, size, "kim@example.com", 40);
size = add(members, size, "", 30);
size = add(members, size, "lee@example.com", 50);
print(members, size);
printSummary(members, size);
}
private static int add(Member[] members, int size, String email, int age) {
if (size >= members.length) {
System.out.println("full");
return size;
}
if (email == null || email.isBlank() || age < 14 || age > 120) {
System.out.println("invalid");
return size;
}
members[size] = new Member(email, age);
return size + 1;
}
private static void print(Member[] members, int size) {
for (int index = 0; index < size; index++) {
System.out.println(members[index].email + "=" + members[index].age);
}
}
private static void printSummary(Member[] members, int size) {
int total = 0;
for (int index = 0; index < size; index++) {
total += members[index].age;
}
System.out.println("count=" + size + ", total=" + total);
}
private static final class Member {
String email;
int age;
Member(String email, int age) {
this.email = email;
this.age = age;
}
}
}invalid
kim@example.com=40
lee@example.com=50
count=2, total=90검증 실패는 기존 size를 반환해 상태를 보존합니다.
메서드 추출 덕분에 중복은 줄었지만 add(members, size, ...)처럼 상태를 구성하는 배열과 size를 호출할 때마다 함께 전달합니다.
printSummary도 Member의 필드를 직접 읽습니다.
데이터와 그 데이터를 다루는 행동이 서로 떨어져 있다는 신호입니다.
변경 요구와 결합도
다음 요구가 들어왔다고 가정합니다.
회원 나이는 14세부터 120세까지만 허용한다
추가 경로가 하나라면 add만 바꾸면 됩니다.
그러나 파일 불러오기나 수정 기능이 member.age를 직접 대입하면 각 경로에 검증을 복사해야 합니다.
필드를 공개한 상태에서는 “모든 Member는 유효하다”는 보장을 할 수 없습니다.
연령대별 회원 수와 전체 회원 수를 추가한다
집계 메서드는 늘어나지만 모두 배열과 size를 전달받아 동일한 순회 경계를 확인합니다.
누군가 length까지 순회하면 null을 만납니다.
저장소가 자신의 유효 범위를 알려 주지 못하고 호출자가 size를 기억합니다.
최대 회원 수를 3에서 10으로 바꾼다
배열 생성 한 곳만 바꾸면 되는 듯하지만, 다른 메서드가 숫자 3을 하드코딩했다면 함께 수정해야 합니다.
members.length를 사용하면 용량 규칙을 저장소에 더 가깝게 둘 수 있습니다.
변경 비용은 수정한 줄 수만이 아닙니다.
규칙을 지키기 위해 찾아야 하는 위치 수, 잘못된 중간 상태를 만들 수 있는 공개 경로 수, 호출자가 알아야 하는 상태 조각 수가 모두 비용입니다.
절차와 객체 비교
절차 중심 코드는 작업 순서가 중심입니다.
입력을 읽는다 → 검증한다 → 배열에 넣는다 → size를 늘린다 → 출력한다객체 중심 코드는 책임을 가진 대상 사이의 메시지가 중심입니다.
memberRegistry.add(email, age)
memberRegistry.printMembers()
memberRegistry.totalAge()둘 중 하나가 언제나 우월하지 않습니다.
단순 변환처럼 상태가 없고 입력에서 출력으로 끝나는 기능은 절차나 정적 유틸리티가 자연스럽습니다.
반면 회원 저장소처럼 상태와 불변식이 있고 여러 행동이 같은 상태를 바꾸면 객체가 경계를 세우기 좋습니다.
객체 설계 질문
현재 Member는 “값 묶음”, add와 total은 “외부 절차”입니다.
다음 절에서는 질문을 바꿉니다.
- 한 회원이 자신의 유효성을 책임질 수 있는가?
- 회원 저장소가 배열과
size를 함께 소유할 수 있는가? - 호출자가 내부 인덱스 대신
add라는 공개 행동만 알게 할 수 있는가? - 출력 형식과 핵심 계산 중 무엇을 객체 안에 둘 것인가?
객체 지향은 클래스를 많이 만드는 문법이 아니라 상태와 행동의 책임을 배치하는 방식입니다.
연습 문제
Member[]와 size를 받아 총 회원 나이와 가장 나이가 많은 회원의 이메일을 출력하는 printReport를 작성하세요.
size가 0이면 report=none만 출력합니다.
실제 첫 회원으로 최댓값을 초기화하세요.
해설 보기
public final class ProceduralReport {
public static void main(String[] args) {
Member[] members = {
new Member("kim@example.com", 40),
new Member("lee@example.com", 55)
};
printReport(members, 2);
}
private static void printReport(Member[] members, int size) {
if (size == 0) {
System.out.println("report=none");
return;
}
int total = 0;
int maxIndex = 0;
for (int index = 0; index < size; index++) {
total += members[index].age;
if (members[index].age > members[maxIndex].age) {
maxIndex = index;
}
}
System.out.println("total=" + total);
System.out.println("oldest=" + members[maxIndex].email);
}
private static final class Member {
private final String email;
private final int age;
private Member(String email, int age) {
this.email = email;
this.age = age;
}
}
}total=95
oldest=lee@example.com절차를 한 메서드로 모았지만 배열과 size의 유효성은 호출자의 계약입니다.
다음 설계에서는 저장소 객체가 이 범위를 직접 소유하게 됩니다.