생성자와 this
초기화가 누락된 객체의 실패를 재현하고 생성자와 this로 유효한 Member의 시작 상태를 보장합니다.
객체가 생성된 뒤 필드를 하나씩 대입하면 “만들어졌지만 아직 쓸 수 없는” 시간이 생깁니다.
다른 메서드가 그 중간 상태를 먼저 사용하거나 한 필드 대입을 빼먹을 수 있습니다.
생성자는 객체 생성과 필수 초기화를 하나의 연산으로 묶어 처음부터 유효한 상태를 만들게 합니다.
초기화 누락
public final class MissingInitialization {
public static void main(String[] args) {
Member member = new Member();
member.age = 40;
System.out.println(member.email.toUpperCase());
}
private static final class Member {
String email;
int age;
}
}Exception in thread "main" java.lang.NullPointerExceptionnew Member()는 필드를 기본값으로 초기화합니다.
참조형 email은 null, int age는 0입니다.
호출자가 age만 대입했으므로 email의 메서드를 호출할 때 실패했습니다.
초기화 누락 예제에서 필드 값과 실패 시점
new의 email=null/age 0에서 age만 40으로 바꾸고 email 값 읽기 성공 뒤 toUpperCase 호출에서 실패합니다. 초기화 누락과 역참조 실패의 다른 시점을 보여 줄 수 있습니다.
MissingInitialization에서 email은 null로 남고 age만 40으로 바뀐다. null 값 읽기는 가능하지만 그 값으로 toUpperCase를 호출하면 예외가 발생한다.
정상 생성 뒤에도 누락된 email은 null로 남는다
age만 대입한 객체에서 이메일을 읽는 것과 메서드를 호출하는 것을 구분합니다.
MissingInitialization의 같은 객체를 실행 순서대로 추적합니다.
| 실행 시점 | 객체 필드 | 관찰 |
|---|---|---|
| 생성 직후 | email = null
| new Member()가 정상 참조를 반환합니다. 필드는 언어 기본값입니다. |
| 나이 대입 후 | email = null
| member.age = 40은 이메일을 초기화하지 않습니다. |
| 이메일 사용 | email = null
| member.email 읽기는 성공합니다. 이어지는 toUpperCase() 호출에서 NullPointerException이 발생합니다. |
- 생성 직후
new Member()는 정상 참조를 반환합니다. 필드는email = null,age = 0입니다. - 나이 대입 후
member.age = 40이후에도email = null이며,age = 40만 달라집니다. - 이메일 사용
member.email읽기는 성공하지만, 그 값으로toUpperCase()를 호출하면NullPointerException이 발생합니다. 필드 값은 그대로입니다.
이 예제의 기본 생성자는 이메일 필수 조건을 검사하지 않습니다. 정상 생성과 업무상 유효한 초기 상태는 같은 뜻이 아닙니다.
이 예의 조건
- email 초기화가 누락된 원 순서입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | new Member의 기본 null 0→age 40, email null 유지→email 필드 읽기 결과 null·age 40 유지→null.toUpperCase에서 NPE가 발생한다. 필드 읽기와 메서드 호출을 구분하며 예외 뒤에도 null 40을 유지한다. |
| 11 | 필드: null |
| 12 | 40 |
| 구분 | 내용 |
|---|---|
| 설명 | new Member의 기본 null 0→age 40, email null 유지→email 필드 읽기 결과 null·age 40 유지→null.toUpperCase에서 NPE가 발생한다. 필드 읽기와 메서드 호출을 구분하며 예외 뒤에도 null 40을 유지한다. |
| 전체 읽기 정보 | 초기화 누락 예제에서 필드 값과 실패 시점 초기화 누락 예제에서 필드 값과 실패 시점 MissingInitialization에서 email은 null로 남고 age만 40으로 바뀐다. null 값 읽기는 가능하지만 그 값으로 toUpperCase를 호출하면 예외가 발생한다. 정상 생성 뒤에도 누락된 email은 null로 남는다 age만 대입한 객체에서 이메일을 읽는 것과 메서드를 호출하는 것을 구분합니다. MissingInitialization 의 같은 객체를 실행 순서대로 추적합니다. 실행 문장별 email과 age 상태 및 결과 실행 시점 객체 필드 관찰 생성 직후 email = null age = 0 new Member() 가 정상 참조를 반환합니다. 필드는 언어 기본값입니다. 나이 대입 후 email = null age = 40 member.age = 40 은 이메일을 초기화하지 않습니다. 이메일 사용 email = null age = 40 member.email 읽기는 성공합니다. 이어지는 toUpperCase() 호출에서 NullPointerException 이 발생합니다. 생성 직후 new Member() 는 정상 참조를 반환합니다. 필드는 email = null , age = 0 입니다. 나이 대입 후 member.age = 40 이후에도 email = null 이며, age = 40 만 달라집니다. 이메일 사용 member.email 읽기는 성공하지만, 그 값으로 toUpperCase() 를 호출하면 NullPointerException 이 발생합니다. 필드 값은 그대로입니다. 이 예제의 기본 생성자는 이메일 필수 조건을 검사하지 않습니다. 정상 생성과 업무상 유효한 초기 상태는 같은 뜻이 아닙니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 기본 필드 new Member 뒤 email null, age 0입니다. |
|
|
| 일부 대입과 읽기 age 40만 대입하고 email 필드를 읽습니다. |
|
|
| 메서드 실패 null.toUpperCase를 호출해 NPE입니다. |
|
|
초기화 누락 예제에서 필드 값과 실패 시점
new의 email=null/age 0에서 age만 40으로 바꾸고 email 값 읽기 성공 뒤 toUpperCase 호출에서 실패합니다. 초기화 누락과 역참조 실패의 다른 시점을 보여 줄 수 있습니다.
MissingInitialization에서 email은 null로 남고 age만 40으로 바뀐다. null 값 읽기는 가능하지만 그 값으로 toUpperCase를 호출하면 예외가 발생한다.
정상 생성 뒤에도 누락된 email은 null로 남는다
age만 대입한 객체에서 이메일을 읽는 것과 메서드를 호출하는 것을 구분합니다.
MissingInitialization의 같은 객체를 실행 순서대로 추적합니다.
| 실행 시점 | 객체 필드 | 관찰 |
|---|---|---|
| 생성 직후 | email = null
| new Member()가 정상 참조를 반환합니다. 필드는 언어 기본값입니다. |
| 나이 대입 후 | email = null
| member.age = 40은 이메일을 초기화하지 않습니다. |
| 이메일 사용 | email = null
| member.email 읽기는 성공합니다. 이어지는 toUpperCase() 호출에서 NullPointerException이 발생합니다. |
- 생성 직후
new Member()는 정상 참조를 반환합니다. 필드는email = null,age = 0입니다. - 나이 대입 후
member.age = 40이후에도email = null이며,age = 40만 달라집니다. - 이메일 사용
member.email읽기는 성공하지만, 그 값으로toUpperCase()를 호출하면NullPointerException이 발생합니다. 필드 값은 그대로입니다.
이 예제의 기본 생성자는 이메일 필수 조건을 검사하지 않습니다. 정상 생성과 업무상 유효한 초기 상태는 같은 뜻이 아닙니다.
이 예의 조건
- email 초기화가 누락된 원 순서입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | new Member의 기본 null 0→age 40, email null 유지→email 필드 읽기 결과 null·age 40 유지→null.toUpperCase에서 NPE가 발생한다. 필드 읽기와 메서드 호출을 구분하며 예외 뒤에도 null 40을 유지한다. |
| 11 | 필드: null |
| 12 | 40 |
| 구분 | 내용 |
|---|---|
| 설명 | new Member의 기본 null 0→age 40, email null 유지→email 필드 읽기 결과 null·age 40 유지→null.toUpperCase에서 NPE가 발생한다. 필드 읽기와 메서드 호출을 구분하며 예외 뒤에도 null 40을 유지한다. |
| 전체 읽기 정보 | 초기화 누락 예제에서 필드 값과 실패 시점 초기화 누락 예제에서 필드 값과 실패 시점 MissingInitialization에서 email은 null로 남고 age만 40으로 바뀐다. null 값 읽기는 가능하지만 그 값으로 toUpperCase를 호출하면 예외가 발생한다. 정상 생성 뒤에도 누락된 email은 null로 남는다 age만 대입한 객체에서 이메일을 읽는 것과 메서드를 호출하는 것을 구분합니다. MissingInitialization 의 같은 객체를 실행 순서대로 추적합니다. 실행 문장별 email과 age 상태 및 결과 실행 시점 객체 필드 관찰 생성 직후 email = null age = 0 new Member() 가 정상 참조를 반환합니다. 필드는 언어 기본값입니다. 나이 대입 후 email = null age = 40 member.age = 40 은 이메일을 초기화하지 않습니다. 이메일 사용 email = null age = 40 member.email 읽기는 성공합니다. 이어지는 toUpperCase() 호출에서 NullPointerException 이 발생합니다. 생성 직후 new Member() 는 정상 참조를 반환합니다. 필드는 email = null , age = 0 입니다. 나이 대입 후 member.age = 40 이후에도 email = null 이며, age = 40 만 달라집니다. 이메일 사용 member.email 읽기는 성공하지만, 그 값으로 toUpperCase() 를 호출하면 NullPointerException 이 발생합니다. 필드 값은 그대로입니다. 이 예제의 기본 생성자는 이메일 필수 조건을 검사하지 않습니다. 정상 생성과 업무상 유효한 초기 상태는 같은 뜻이 아닙니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 기본 필드 new Member 뒤 email null, age 0입니다. |
|
|
| 일부 대입과 읽기 age 40만 대입하고 email 필드를 읽습니다. |
|
|
| 메서드 실패 null.toUpperCase를 호출해 NPE입니다. |
|
|
클래스를 보는 것만으로는 어떤 필드를 반드시 채워야 하는지 강제되지 않습니다.
생성자 없이 다음 절차를 반복하면 순서와 누락을 모든 호출자가 책임집니다.
Member member = new Member();
member.email = email;
member.age = age;필수 값이 늘어날수록 기존 생성 위치를 모두 찾아 수정해야 합니다.
생성자 매개변수로 필수 값을 요구하면 컴파일러가 누락된 호출을 찾습니다.
생성자 선언
public final class ConstructorMember {
public static void main(String[] args) {
Member member = new Member("[email protected]", 40);
System.out.println(member.describe());
}
private static final class Member {
private String email;
private int age;
Member(String email, int age) {
this.email = email;
this.age = age;
}
String describe() {
return email + "=" + age;
}
}
}Member(String email, int age)는 메서드처럼 보이지만 반환 타입이 없고 클래스 이름과 같습니다.
new Member("[email protected]", 40)는 메모리 할당, 필드 기본값 설정, 생성자 본문 실행을 거쳐 참조를 반환합니다.
개발자가 생성자를 직접 일반 메서드처럼 호출할 수는 없습니다.
이미 생성한 객체의 생성자를 일반 메서드처럼 다시 호출할 수는 없습니다. 뒤 절의 this(...) 위임은 같은 객체를 만드는 과정에서 다른 생성자 본문을 이어 실행하는 것이며, 기존 객체를 재초기화하는 호출과 구분합니다.
이후 같은 검증을 거쳐 값을 바꾸려면 별도의 행동 메서드가 필요합니다.
생성자를 다시 호출해 기존 객체를 재초기화하는 방식은 없습니다.
this 필드 참조
public final class MissingThisAssignment {
public static void main(String[] args) {
Member member = new Member("[email protected]", 50);
System.out.println(member.email + "=" + member.age);
}
private static final class Member {
String email;
int age;
Member(String email, int age) {
email = email;
age = age;
}
}
}null=0이름을 찾을 때 가장 가까운 지역 범위의 매개변수가 우선합니다.
email = email은 매개변수 값을 같은 매개변수에 다시 대입하고, 필드는 건드리지 않습니다.
this.email은 현재 생성 중인 객체의 필드를 명시합니다.
Member(String email, int age) {
this.email = email;
this.age = age;
}왼쪽은 객체 필드, 오른쪽은 생성자 매개변수입니다.
매개변수 이름을 inputEmail으로 바꾸면 this 없이도 구분되지만, 필드와 매개변수가 같은 개념이라면 같은 이름과 this가 대응을 분명하게 보여 줍니다.
this는 “현재 인스턴스의 참조”입니다.
인스턴스 메서드에서 this.describe()처럼 메서드를 호출하거나 다른 객체에 현재 인스턴스를 전달할 수도 있습니다.
다만 명확하지 않은 곳마다 this를 붙이는 것이 목표가 아니라 지역 변수와 필드의 경계를 드러내는 데 사용합니다.
매개변수 자기 대입과 this 필드 대입의 결과
매개변수 자기 대입은 필드 기본값을 유지하고 this 필드 대입은 전달값을 저장합니다. 서로 다른 생성자 구현의 대입 전후를 같은 역할로 비교할 수 있습니다.
MissingThisAssignment의 [email protected]과 50을 기준으로 매개변수만 다시 대입하는 코드와 본문의 this 교정 코드를 비교한다.
this로 매개변수와 현재 객체의 필드를 구분한다
자기 대입은 필드를 바꾸지 않아 null과 0이 그대로 남습니다.
MissingThisAssignment에 전달한 "[email protected]", 50을 기준으로 원본과 본문의 교정 코드를 비교합니다.
| 구현 | 대입 대상 | 생성 후 객체 필드 |
|---|---|---|
| 원본 | email = email;
오른쪽도 왼쪽도 같은 매개변수입니다. | email = null
출력: |
| 교정 | this.email = email;
매개변수 값을 현재 객체의 필드에 저장합니다. | email = "[email protected]"
같은 입력의 결과: |
- 원본: 매개변수에 자기 대입
email = email;과age = age;는 매개변수만 다시 대입합니다.객체 필드는
email = null,age = 0으로 남아null=0을 출력합니다.- 교정: 현재 객체 필드에 대입
this.email = email;과this.age = age;는 매개변수 값을 필드에 저장합니다.같은 입력이면 필드는
email = "[email protected]",age = 50이고 결과는[email protected]=50입니다.
this는 현재 인스턴스의 참조입니다. 필드 대입을 구분할 뿐, 입력값 검증을 자동으로 추가하지는 않습니다.
이 예의 조건
- 서로 다른 생성자 구현에 동일 Lee 50을 전달합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 동일한 Lee 50 입력에 대한 서로 다른 구현이다. email=email/age=age는 매개변수의 자기 대입이어서 필드는 null 0이다. this.email/this.age는 받은 값을 현재 인스턴스 필드에 대입하여 Lee 50이 된다. this 자체가 유효성 검사는 아니다. |
| 16 | this.email |
| 17 | this.age |
| 구분 | 내용 |
|---|---|
| 설명 | 동일한 Lee 50 입력에 대한 서로 다른 구현이다. email=email/age=age는 매개변수의 자기 대입이어서 필드는 null 0이다. this.email/this.age는 받은 값을 현재 인스턴스 필드에 대입하여 Lee 50이 된다. this 자체가 유효성 검사는 아니다. |
| 전체 읽기 정보 | 매개변수 자기 대입과 this 필드 대입의 결과 매개변수 자기 대입과 this 필드 대입의 결과 MissingThisAssignment의 [email protected]과 50을 기준으로 매개변수만 다시 대입하는 코드와 본문의 this 교정 코드를 비교한다. this로 매개변수와 현재 객체의 필드를 구분한다 자기 대입은 필드를 바꾸지 않아 null과 0이 그대로 남습니다. MissingThisAssignment 에 전달한 "[email protected]" , 50 을 기준으로 원본과 본문의 교정 코드를 비교합니다. 대입문이 바꾸는 저장 공간과 생성 후 필드 값 구현 대입 대상 생성 후 객체 필드 원본 email = email; age = age; 오른쪽도 왼쪽도 같은 매개변수입니다. email = null age = 0 출력: null=0 교정 this.email = email; this.age = age; 매개변수 값을 현재 객체의 필드에 저장합니다. email = "[email protected]" age = 50 같은 입력의 결과: [email protected]=50 원본: 매개변수에 자기 대입 email = email; 과 age = age; 는 매개변수만 다시 대입합니다. 객체 필드는 email = null , age = 0 으로 남아 null=0 을 출력합니다. 교정: 현재 객체 필드에 대입 this.email = email; 과 this.age = age; 는 매개변수 값을 필드에 저장합니다. 같은 입력이면 필드는 email = "[email protected]" , age = 50 이고 결과는 [email protected]=50 입니다. this 는 현재 인스턴스의 참조입니다. 필드 대입을 구분할 뿐, 입력값 검증을 자동으로 추가하지는 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 매개변수 수신 email과 age 매개변수가 Lee 50을 받습니다. |
|
|
| 대입 대상 비교 자기 대입은 매개변수, this 대입은 현재 필드를 대상으로 합니다. |
|
|
| 생성 결과 잘못된 구현은 null 0, 필드 지정은 Lee 50입니다. |
|
|
매개변수 자기 대입과 this 필드 대입의 결과
매개변수 자기 대입은 필드 기본값을 유지하고 this 필드 대입은 전달값을 저장합니다. 서로 다른 생성자 구현의 대입 전후를 같은 역할로 비교할 수 있습니다.
MissingThisAssignment의 [email protected]과 50을 기준으로 매개변수만 다시 대입하는 코드와 본문의 this 교정 코드를 비교한다.
this로 매개변수와 현재 객체의 필드를 구분한다
자기 대입은 필드를 바꾸지 않아 null과 0이 그대로 남습니다.
MissingThisAssignment에 전달한 "[email protected]", 50을 기준으로 원본과 본문의 교정 코드를 비교합니다.
| 구현 | 대입 대상 | 생성 후 객체 필드 |
|---|---|---|
| 원본 | email = email;
오른쪽도 왼쪽도 같은 매개변수입니다. | email = null
출력: |
| 교정 | this.email = email;
매개변수 값을 현재 객체의 필드에 저장합니다. | email = "[email protected]"
같은 입력의 결과: |
- 원본: 매개변수에 자기 대입
email = email;과age = age;는 매개변수만 다시 대입합니다.객체 필드는
email = null,age = 0으로 남아null=0을 출력합니다.- 교정: 현재 객체 필드에 대입
this.email = email;과this.age = age;는 매개변수 값을 필드에 저장합니다.같은 입력이면 필드는
email = "[email protected]",age = 50이고 결과는[email protected]=50입니다.
this는 현재 인스턴스의 참조입니다. 필드 대입을 구분할 뿐, 입력값 검증을 자동으로 추가하지는 않습니다.
이 예의 조건
- 서로 다른 생성자 구현에 동일 Lee 50을 전달합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 동일한 Lee 50 입력에 대한 서로 다른 구현이다. email=email/age=age는 매개변수의 자기 대입이어서 필드는 null 0이다. this.email/this.age는 받은 값을 현재 인스턴스 필드에 대입하여 Lee 50이 된다. this 자체가 유효성 검사는 아니다. |
| 16 | this.email |
| 17 | this.age |
| 구분 | 내용 |
|---|---|
| 설명 | 동일한 Lee 50 입력에 대한 서로 다른 구현이다. email=email/age=age는 매개변수의 자기 대입이어서 필드는 null 0이다. this.email/this.age는 받은 값을 현재 인스턴스 필드에 대입하여 Lee 50이 된다. this 자체가 유효성 검사는 아니다. |
| 전체 읽기 정보 | 매개변수 자기 대입과 this 필드 대입의 결과 매개변수 자기 대입과 this 필드 대입의 결과 MissingThisAssignment의 [email protected]과 50을 기준으로 매개변수만 다시 대입하는 코드와 본문의 this 교정 코드를 비교한다. this로 매개변수와 현재 객체의 필드를 구분한다 자기 대입은 필드를 바꾸지 않아 null과 0이 그대로 남습니다. MissingThisAssignment 에 전달한 "[email protected]" , 50 을 기준으로 원본과 본문의 교정 코드를 비교합니다. 대입문이 바꾸는 저장 공간과 생성 후 필드 값 구현 대입 대상 생성 후 객체 필드 원본 email = email; age = age; 오른쪽도 왼쪽도 같은 매개변수입니다. email = null age = 0 출력: null=0 교정 this.email = email; this.age = age; 매개변수 값을 현재 객체의 필드에 저장합니다. email = "[email protected]" age = 50 같은 입력의 결과: [email protected]=50 원본: 매개변수에 자기 대입 email = email; 과 age = age; 는 매개변수만 다시 대입합니다. 객체 필드는 email = null , age = 0 으로 남아 null=0 을 출력합니다. 교정: 현재 객체 필드에 대입 this.email = email; 과 this.age = age; 는 매개변수 값을 필드에 저장합니다. 같은 입력이면 필드는 email = "[email protected]" , age = 50 이고 결과는 [email protected]=50 입니다. this 는 현재 인스턴스의 참조입니다. 필드 대입을 구분할 뿐, 입력값 검증을 자동으로 추가하지는 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 매개변수 수신 email과 age 매개변수가 Lee 50을 받습니다. |
|
|
| 대입 대상 비교 자기 대입은 매개변수, this 대입은 현재 필드를 대상으로 합니다. |
|
|
| 생성 결과 잘못된 구현은 null 0, 필드 지정은 Lee 50입니다. |
|
|
생성자 불변식
public final class ValidatedConstructor {
public static void main(String[] args) {
printCreation("[email protected]", 40);
printCreation("", 30);
printCreation("[email protected]", 130);
}
private static void printCreation(String email, int age) {
try {
Member member = new Member(email, age);
System.out.println("created=" + member.describe());
} catch (IllegalArgumentException exception) {
System.out.println("rejected=" + exception.getMessage());
}
}
private static final class Member {
private final String email;
private final int age;
Member(String email, int age) {
if (email == null || email.isBlank()) {
throw new IllegalArgumentException("email");
}
if (age < 14 || age > 120) {
throw new IllegalArgumentException("age");
}
this.email = email;
this.age = age;
}
String describe() {
return email + "=" + age;
}
}
}[email protected]=40
rejected=email
rejected=age검증을 필드 대입보다 먼저 수행해 실패한 객체가 외부로 전달되지 않게 합니다.
생성자가 예외를 던지면 new 표현식이 정상 참조를 반환하지 않습니다.
생성된 모든 Member가 이메일과 나이 범위를 만족한다는 가정을 다른 메서드가 사용할 수 있습니다.
CLI에서 사용자 입력 오류를 예외로 끝내면 안 되므로 입력 경계에서 예외를 잡아 rejected로 바꿉니다.
생성자는 클래스 규칙을 지키고, CLI는 사용자와 대화를 계속하는 책임을 맡습니다.
검증 생성자의 첫 실패와 정상 참조 반환 경계
이메일과 나이 검사를 통과한 뒤 필드를 대입하고 new가 정상 참조를 반환합니다. 실패한 생성에서 외부 정상 참조가 나오지 않는 실행 경계를 나눌 수 있습니다.
ValidatedConstructor의 Member는 이메일 검사 뒤 나이 검사를 수행하고 둘 다 통과해야 필드에 대입한다. 예외가 나면 new는 정상 참조를 반환하지 않으며 printCreation이 rejected 메시지로 바꾼다.
검사를 모두 통과해야 필드 대입과 정상 반환에 도달한다
이메일 검사에서 실패하면 나이 검사도 건너뛰고 호출자가 rejected로 알립니다.
ValidatedConstructor의 Member 생성자입니다. 앞의 단순 대입 예제와 달리 검증 코드가 있습니다.
- 이메일 검사
email == null || email.isBlank()이면IllegalArgumentException("email")로 종료합니다. 나이 검사는 실행하지 않습니다. - 나이 검사
이메일이 통과한 뒤
age < 14 || age > 120이면IllegalArgumentException("age")로 종료합니다. - 필드 대입과 정상 반환
두 검사를 통과해야
this.email과this.age에 대입하고,new가 정상 참조를 반환합니다.
어느 검사에서든 예외가 나면 필드 대입에 도달하지 않고, new도 정상 참조를 반환하지 않습니다. 호출자 printCreation이 예외를 잡아 rejected=email 또는 rejected=age를 출력합니다.
이 불변식은 검증을 직접 구현한 이 예제의 보장입니다. 생성자라는 문법만으로 입력값이 검증되지는 않습니다.
이 예의 조건
- 원 main의 세 독립 호출 중 [email protected],40의 정상 생성 하나만 추적합니다.
- 별도 빈 이메일 30과 [email protected],130의 실패 요청은 원 전체 표에 보존하며 정상 호출의 후속이 아닙니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 검증 생성자의 첫 실패와 정상 참조 반환 경계 검증 생성자의 첫 실패와 정상 참조 반환 경계 ValidatedConstructor의 Member는 이메일 검사 뒤 나이 검사를 수행하고 둘 다 통과해야 필드에 대입한다. 예외가 나면 new는 정상 참조를 반환하지 않으며 printCreation이 rejected 메시지로 바꾼다. 검사를 모두 통과해야 필드 대입과 정상 반환에 도달한다 이메일 검사에서 실패하면 나이 검사도 건너뛰고 호출자가 rejected로 알립니다. ValidatedConstructor 의 Member 생성자입니다. 앞의 단순 대입 예제와 달리 검증 코드가 있습니다. 이메일 검사 email == null || email.isBlank() 이면 IllegalArgumentException("email") 로 종료합니다. 나이 검사는 실행하지 않습니다. 나이 검사 이메일이 통과한 뒤 age < 14 || age > 120 이면 IllegalArgumentException("age") 로 종료합니다. 필드 대입과 정상 반환 두 검사를 통과해야 this.email 과 this.age 에 대입하고, new 가 정상 참조를 반환합니다. 어느 검사에서든 예외가 나면 필드 대입에 도달하지 않고, new 도 정상 참조를 반환하지 않습니다. 호출자 printCreation 이 예외를 잡아 rejected=email 또는 rejected=age 를 출력합니다. 이 불변식은 검증을 직접 구현한 이 예제의 보장입니다. 생성자라는 문법만으로 입력값이 검증되지는 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| Kim의 입력 검증 [email protected]은 null·공백이 아니고 나이 40은 14–120 범위입니다. |
|
|
| 현재 객체 필드 대입 두 검사 통과 후 this.email과 this.age에 입력값을 기록합니다. |
|
|
| new 정상 참조 반환 생성자가 정상 완료한 뒤 new가 Kim 40 객체의 참조를 반환합니다. |
|
|
검증 생성자의 첫 실패와 정상 참조 반환 경계
이메일과 나이 검사를 통과한 뒤 필드를 대입하고 new가 정상 참조를 반환합니다. 실패한 생성에서 외부 정상 참조가 나오지 않는 실행 경계를 나눌 수 있습니다.
ValidatedConstructor의 Member는 이메일 검사 뒤 나이 검사를 수행하고 둘 다 통과해야 필드에 대입한다. 예외가 나면 new는 정상 참조를 반환하지 않으며 printCreation이 rejected 메시지로 바꾼다.
검사를 모두 통과해야 필드 대입과 정상 반환에 도달한다
이메일 검사에서 실패하면 나이 검사도 건너뛰고 호출자가 rejected로 알립니다.
ValidatedConstructor의 Member 생성자입니다. 앞의 단순 대입 예제와 달리 검증 코드가 있습니다.
- 이메일 검사
email == null || email.isBlank()이면IllegalArgumentException("email")로 종료합니다. 나이 검사는 실행하지 않습니다. - 나이 검사
이메일이 통과한 뒤
age < 14 || age > 120이면IllegalArgumentException("age")로 종료합니다. - 필드 대입과 정상 반환
두 검사를 통과해야
this.email과this.age에 대입하고,new가 정상 참조를 반환합니다.
어느 검사에서든 예외가 나면 필드 대입에 도달하지 않고, new도 정상 참조를 반환하지 않습니다. 호출자 printCreation이 예외를 잡아 rejected=email 또는 rejected=age를 출력합니다.
이 불변식은 검증을 직접 구현한 이 예제의 보장입니다. 생성자라는 문법만으로 입력값이 검증되지는 않습니다.
이 예의 조건
- 원 main의 세 독립 호출 중 [email protected],40의 정상 생성 하나만 추적합니다.
- 별도 빈 이메일 30과 [email protected],130의 실패 요청은 원 전체 표에 보존하며 정상 호출의 후속이 아닙니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 검증 생성자의 첫 실패와 정상 참조 반환 경계 검증 생성자의 첫 실패와 정상 참조 반환 경계 ValidatedConstructor의 Member는 이메일 검사 뒤 나이 검사를 수행하고 둘 다 통과해야 필드에 대입한다. 예외가 나면 new는 정상 참조를 반환하지 않으며 printCreation이 rejected 메시지로 바꾼다. 검사를 모두 통과해야 필드 대입과 정상 반환에 도달한다 이메일 검사에서 실패하면 나이 검사도 건너뛰고 호출자가 rejected로 알립니다. ValidatedConstructor 의 Member 생성자입니다. 앞의 단순 대입 예제와 달리 검증 코드가 있습니다. 이메일 검사 email == null || email.isBlank() 이면 IllegalArgumentException("email") 로 종료합니다. 나이 검사는 실행하지 않습니다. 나이 검사 이메일이 통과한 뒤 age < 14 || age > 120 이면 IllegalArgumentException("age") 로 종료합니다. 필드 대입과 정상 반환 두 검사를 통과해야 this.email 과 this.age 에 대입하고, new 가 정상 참조를 반환합니다. 어느 검사에서든 예외가 나면 필드 대입에 도달하지 않고, new 도 정상 참조를 반환하지 않습니다. 호출자 printCreation 이 예외를 잡아 rejected=email 또는 rejected=age 를 출력합니다. 이 불변식은 검증을 직접 구현한 이 예제의 보장입니다. 생성자라는 문법만으로 입력값이 검증되지는 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| Kim의 입력 검증 [email protected]은 null·공백이 아니고 나이 40은 14–120 범위입니다. |
|
|
| 현재 객체 필드 대입 두 검사 통과 후 this.email과 this.age에 입력값을 기록합니다. |
|
|
| new 정상 참조 반환 생성자가 정상 완료한 뒤 new가 Kim 40 객체의 참조를 반환합니다. |
|
|
생성 책임 분리
MemberRegistry의 add가 원시 값으로 Member를 생성할 수 있습니다.
public final class ConstructingMemberRegistry {
public static void main(String[] args) {
MemberRegistry registry = new MemberRegistry(2);
System.out.println(registry.add("[email protected]", 40));
System.out.println(registry.add("[email protected]", 50));
System.out.println(registry.add("[email protected]", 30));
System.out.println(registry.summary());
}
private static final class MemberRegistry {
private final Member[] members;
private int size;
MemberRegistry(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException("capacity");
}
this.members = new Member[capacity];
}
boolean add(String email, int age) {
if (size == members.length) {
return false;
}
try {
members[size] = new Member(email, age);
size++;
return true;
} catch (IllegalArgumentException exception) {
return false;
}
}
String summary() {
int total = 0;
for (int index = 0; index < size; index++) {
total += members[index].age();
}
return "count=" + size + ", total=" + total;
}
}
private static final class Member {
private final String email;
private final int age;
Member(String email, int age) {
if (email == null || email.isBlank() || age < 14 || age > 120) {
throw new IllegalArgumentException("invalid member");
}
this.email = email;
this.age = age;
}
int age() {
return age;
}
}
}true
true
false
count=2, total=90세 번째 요청은 용량 검사에서 거절돼 생성자도 호출하지 않습니다.
성공한 생성 뒤에만 size를 증가시킵니다.
Member 생성 규칙과 MemberRegistry 용량 규칙이 각각 자기 상태 가까이에 있습니다.
회원 생성 성공 뒤 배열 저장과 size 증가
Kim과 Lee는 생성 성공 후 저장·size 증가하고 Park은 용량 검사에서 생성 전에 거절됩니다. 생성 예외도 저장 확정 전에 잡아 상태를 보존하는 실제 add 게이트입니다.
ConstructingMemberRegistry의 용량 2에서 첫 두 add는 생성 후 저장하고 size를 늘린다. 세 번째 add는 가득 찬 검사를 먼저 만나 Member 생성도 하지 않는다.
생성이 성공한 뒤 저장하고 회원 수를 늘린다
용량 2의 세 번째 요청은 생성자에 도달하기 전에 거절됩니다.
ConstructingMemberRegistry의 main: 용량 2, 처음 size = 0인 같은 저장소입니다.
| 요청 | 생성·저장 | 결과와 size |
|---|---|---|
| 첫 번째 | "[email protected]", 40검증 생성 성공 후 | true
|
| 두 번째 | "[email protected]", 50검증 생성 성공 후 | true
|
| 세 번째 | "[email protected]", 30
| false
|
- 첫 번째: kim, 40
"[email protected]", 40의 생성 성공 후members[0]에 저장합니다.size: 0 → 1, 반환값은true입니다. - 두 번째: lee, 50
"[email protected]", 50의 생성 성공 후members[1]에 저장합니다.size: 1 → 2, 반환값은true입니다. - 세 번째: park, 30
"[email protected]", 30은 용량 검사에서 거절됩니다.Member생성자를 호출하지 않으며size: 2 → 2, 반환값은false입니다.
최종 요약은 count=2, total=90입니다. 저장 공간이 있어도 생성자가 IllegalArgumentException을 던지면 add가 false를 반환하며 배열 칸과 size를 바꾸지 않습니다.
이 예제는 생성·저장을 끝낸 다음 별도 문장으로 size++를 실행합니다. members[size++] = new Member(...)와 실행 순서가 다릅니다.
이 예의 조건
- 원 main의 용량 2 같은 저장소에 Kim 40, Lee 50, Park 30을 순차 요청하는 경로만 추적합니다.
- 빈 공간이 있는 별도 저장소의 생성자 예외 조건은 독립 분기이며 원 전체 표에 보존합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 같은 capacity 2 저장소에서 Kim 40은 0→1 true, Lee 50은 1→2 true를 반환하고 Park 30은 capacity full로 2를 유지하며 false를 반환하고 생성하지 않는다. 빈 공간이 있어도 생성자의 IAA를 catch하면 false를 반환하며 배열/size를 유지한다. 생성에 성공해 저장한 후 별도로 size++를 실행한다. |
| 구분 | 내용 |
|---|---|
| 설명 | 같은 capacity 2 저장소에서 Kim 40은 0→1 true, Lee 50은 1→2 true를 반환하고 Park 30은 capacity full로 2를 유지하며 false를 반환하고 생성하지 않는다. 빈 공간이 있어도 생성자의 IAA를 catch하면 false를 반환하며 배열/size를 유지한다. 생성에 성공해 저장한 후 별도로 size++를 실행한다. |
| 전체 읽기 정보 | 회원 생성 성공 뒤 배열 저장과 size 증가 회원 생성 성공 뒤 배열 저장과 size 증가 ConstructingMemberRegistry의 용량 2에서 첫 두 add는 생성 후 저장하고 size를 늘린다. 세 번째 add는 가득 찬 검사를 먼저 만나 Member 생성도 하지 않는다. 생성이 성공한 뒤 저장하고 회원 수를 늘린다 용량 2의 세 번째 요청은 생성자에 도달하기 전에 거절됩니다. ConstructingMemberRegistry 의 main : 용량 2 , 처음 size = 0 인 같은 저장소입니다. 세 add 요청의 생성 여부, 저장 위치, 회원 수 변화 요청 생성·저장 결과와 size 첫 번째 "[email protected]", 40 검증 생성 성공 후 members[0] 에 저장합니다. true 0 → 1 두 번째 "[email protected]", 50 검증 생성 성공 후 members[1] 에 저장합니다. true 1 → 2 세 번째 "[email protected]", 30 size == members.length 이므로 Member 생성자를 호출하지 않습니다. false 2 → 2 첫 번째: kim, 40 "[email protected]", 40 의 생성 성공 후 members[0] 에 저장합니다. size: 0 → 1 , 반환값은 true 입니다. 두 번째: lee, 50 "[email protected]", 50 의 생성 성공 후 members[1] 에 저장합니다. size: 1 → 2 , 반환값은 true 입니다. 세 번째: park, 30 "[email protected]", 30 은 용량 검사에서 거절됩니다. Member 생성자를 호출하지 않으며 size: 2 → 2 , 반환값은 false 입니다. 최종 요약은 count=2, total=90 입니다. 저장 공간이 있어도 생성자가 IllegalArgumentException 을 던지면 add 가 false 를 반환하며 배열 칸과 size 를 바꾸지 않습니다. 이 예제는 생성·저장을 끝낸 다음 별도 문장으로 size++ 를 실행합니다. members[size++] = new Member(...) 와 실행 순서가 다릅니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 두 요청 성공 Kim 40과 Lee 50의 검증 생성 참조를 각각 저장한 뒤 size를 늘립니다. |
|
|
| Park의 용량 거절 같은 저장소에 Park 30을 요청하면 가득 찬 용량 검사에서 생성 전에 끝납니다. |
|
|
| 원 main의 집계 두 성공 회원을 가진 같은 저장소의 count와 total을 읽습니다. |
|
|
회원 생성 성공 뒤 배열 저장과 size 증가
Kim과 Lee는 생성 성공 후 저장·size 증가하고 Park은 용량 검사에서 생성 전에 거절됩니다. 생성 예외도 저장 확정 전에 잡아 상태를 보존하는 실제 add 게이트입니다.
ConstructingMemberRegistry의 용량 2에서 첫 두 add는 생성 후 저장하고 size를 늘린다. 세 번째 add는 가득 찬 검사를 먼저 만나 Member 생성도 하지 않는다.
생성이 성공한 뒤 저장하고 회원 수를 늘린다
용량 2의 세 번째 요청은 생성자에 도달하기 전에 거절됩니다.
ConstructingMemberRegistry의 main: 용량 2, 처음 size = 0인 같은 저장소입니다.
| 요청 | 생성·저장 | 결과와 size |
|---|---|---|
| 첫 번째 | "[email protected]", 40검증 생성 성공 후 | true
|
| 두 번째 | "[email protected]", 50검증 생성 성공 후 | true
|
| 세 번째 | "[email protected]", 30
| false
|
- 첫 번째: kim, 40
"[email protected]", 40의 생성 성공 후members[0]에 저장합니다.size: 0 → 1, 반환값은true입니다. - 두 번째: lee, 50
"[email protected]", 50의 생성 성공 후members[1]에 저장합니다.size: 1 → 2, 반환값은true입니다. - 세 번째: park, 30
"[email protected]", 30은 용량 검사에서 거절됩니다.Member생성자를 호출하지 않으며size: 2 → 2, 반환값은false입니다.
최종 요약은 count=2, total=90입니다. 저장 공간이 있어도 생성자가 IllegalArgumentException을 던지면 add가 false를 반환하며 배열 칸과 size를 바꾸지 않습니다.
이 예제는 생성·저장을 끝낸 다음 별도 문장으로 size++를 실행합니다. members[size++] = new Member(...)와 실행 순서가 다릅니다.
이 예의 조건
- 원 main의 용량 2 같은 저장소에 Kim 40, Lee 50, Park 30을 순차 요청하는 경로만 추적합니다.
- 빈 공간이 있는 별도 저장소의 생성자 예외 조건은 독립 분기이며 원 전체 표에 보존합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 같은 capacity 2 저장소에서 Kim 40은 0→1 true, Lee 50은 1→2 true를 반환하고 Park 30은 capacity full로 2를 유지하며 false를 반환하고 생성하지 않는다. 빈 공간이 있어도 생성자의 IAA를 catch하면 false를 반환하며 배열/size를 유지한다. 생성에 성공해 저장한 후 별도로 size++를 실행한다. |
| 구분 | 내용 |
|---|---|
| 설명 | 같은 capacity 2 저장소에서 Kim 40은 0→1 true, Lee 50은 1→2 true를 반환하고 Park 30은 capacity full로 2를 유지하며 false를 반환하고 생성하지 않는다. 빈 공간이 있어도 생성자의 IAA를 catch하면 false를 반환하며 배열/size를 유지한다. 생성에 성공해 저장한 후 별도로 size++를 실행한다. |
| 전체 읽기 정보 | 회원 생성 성공 뒤 배열 저장과 size 증가 회원 생성 성공 뒤 배열 저장과 size 증가 ConstructingMemberRegistry의 용량 2에서 첫 두 add는 생성 후 저장하고 size를 늘린다. 세 번째 add는 가득 찬 검사를 먼저 만나 Member 생성도 하지 않는다. 생성이 성공한 뒤 저장하고 회원 수를 늘린다 용량 2의 세 번째 요청은 생성자에 도달하기 전에 거절됩니다. ConstructingMemberRegistry 의 main : 용량 2 , 처음 size = 0 인 같은 저장소입니다. 세 add 요청의 생성 여부, 저장 위치, 회원 수 변화 요청 생성·저장 결과와 size 첫 번째 "[email protected]", 40 검증 생성 성공 후 members[0] 에 저장합니다. true 0 → 1 두 번째 "[email protected]", 50 검증 생성 성공 후 members[1] 에 저장합니다. true 1 → 2 세 번째 "[email protected]", 30 size == members.length 이므로 Member 생성자를 호출하지 않습니다. false 2 → 2 첫 번째: kim, 40 "[email protected]", 40 의 생성 성공 후 members[0] 에 저장합니다. size: 0 → 1 , 반환값은 true 입니다. 두 번째: lee, 50 "[email protected]", 50 의 생성 성공 후 members[1] 에 저장합니다. size: 1 → 2 , 반환값은 true 입니다. 세 번째: park, 30 "[email protected]", 30 은 용량 검사에서 거절됩니다. Member 생성자를 호출하지 않으며 size: 2 → 2 , 반환값은 false 입니다. 최종 요약은 count=2, total=90 입니다. 저장 공간이 있어도 생성자가 IllegalArgumentException 을 던지면 add 가 false 를 반환하며 배열 칸과 size 를 바꾸지 않습니다. 이 예제는 생성·저장을 끝낸 다음 별도 문장으로 size++ 를 실행합니다. members[size++] = new Member(...) 와 실행 순서가 다릅니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 두 요청 성공 Kim 40과 Lee 50의 검증 생성 참조를 각각 저장한 뒤 size를 늘립니다. |
|
|
| Park의 용량 거절 같은 저장소에 Park 30을 요청하면 가득 찬 용량 검사에서 생성 전에 끝납니다. |
|
|
| 원 main의 집계 두 성공 회원을 가진 같은 저장소의 count와 total을 읽습니다. |
|
|
연습 문제
SignupQuota에 캠페인 이름과 목표 가입자 수를 필수로 받는 생성자를 추가하세요.
이름은 공백일 수 없고 목표 가입자 수는 1~1,000명이어야 합니다. 독립 경계로 정상 이름과 목표 1·1,000, 목표 0·1,001, 그리고 null 이름을 각각 확인하세요. 이름과 목표가 모두 잘못됐을 때 어느 검사가 먼저 실패해야 할지도 판단합니다.
this로 필드와 매개변수를 구분하고 describe()가 daily=120을 반환하게 하세요.
해설 보기
public final class SignupQuotaConstructor {
public static void main(String[] args) {
SignupQuota goal = new SignupQuota("daily", 120);
System.out.println(goal.describe());
}
private static final class SignupQuota {
private final String name;
private final int targetCount;
SignupQuota(String name, int targetCount) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name");
}
if (targetCount < 1 || targetCount > 1_000) {
throw new IllegalArgumentException("targetCount");
}
this.name = name;
this.targetCount = targetCount;
}
String describe() {
return name + "=" + targetCount;
}
}
}daily=120필수 값 두 개가 생성자 서명에 나타나므로 인수를 빼먹은 호출은 컴파일되지 않습니다.
생성 완료 뒤에는 두 필드가 항상 유효합니다. 1과 1,000은 범위에 포함되고 0과 1,001은 거절됩니다. 이름이 null이면 name == null이 참이어서 isBlank()를 호출하지 않습니다. 이름과 목표가 모두 잘못됐다면 먼저 있는 이름 검사에서 name 예외가 나며 필드 대입에 도달하지 않습니다.