안동민 개발노트

본문 시작

생성자 오버로딩과 this 호출

자동 기본 생성자의 생성 조건을 확인하고 생성자 오버로딩과 this() 위임으로 초기화 경로를 하나로 모읍니다.

하나의 클래스가 간단한 생성과 상세 생성을 모두 지원할 수 있습니다.

생성자를 여러 개 선언하는 생성자 오버로딩은 편의 경로를 제공하지만, 각 생성자가 필드를 따로 초기화하면 검증과 기본값이 갈라질 수 있습니다.

this(...)로 한 생성자가 다른 생성자에 위임해 최종 초기화를 하나로 모읍니다.


기본 생성자의 생성 조건

lab/NoDefaultConstructor.java
public final class NoDefaultConstructor {
    public static void main(String[] args) {
        Member member = new Member();
        System.out.println(member);
    }

    private static final class Member {
        private final String email;

        Member(String email) {
            this.email = email;
        }
    }
}
컴파일 실패 관찰
error: constructor Member in class Member cannot be applied to given types

클래스에 생성자를 하나도 작성하지 않았을 때만 컴파일러가 매개변수 없는 기본 생성자를 제공합니다.

Member(String)을 개발자가 선언한 순간 자동 기본 생성자는 제공되지 않습니다.

매개변수 없는 생성도 필요하면 직접 선언해야 합니다.

Member() {
    this.email = "untitled";
}

기본 생성자는 “언제나 존재하는 생성자”가 아니라 개발자 생성자가 없을 때만 제공되는 편의 기능입니다.

필수 값이 있는 객체에 무의미한 기본값을 넣으려고 기본 생성자를 억지로 추가하지 않습니다.

기본 생성자 제공 조건과 직접 선언의 차이

개발자 생성자가 없을 때만 컴파일러가 기본 생성자를 제공한다. NoDefaultConstructor의 Member(String)에는 자동 Member()가 없으며, 직접 선언한 Member()는 자동 제공과 다르다.

접근 가능하고 나머지 클래스 선언이 유효한 경우의 생성자 서명을 비교합니다.

생성자 선언과 자동 제공 여부
개발자 선언컴파일러가 자동 제공인수 없는 생성
생성자 없음매개변수 없는 기본 생성자new Member()에 대응하는 서명이 있습니다.
한 인수만없음

Member(String email)은 직접 선언한 생성자입니다.

NoDefaultConstructor의 new Member()는 컴파일 오류입니다.
인수 없는 생성자도 직접 선언없음

Member()의 본문은 개발자가 작성합니다.

직접 선언한 Member()를 호출합니다. 자동 기본 생성자가 아닙니다.
개발자 생성자 없음
컴파일러가 매개변수 없는 기본 생성자를 제공합니다. new Member()에 대응하는 서명이 있습니다.
한 인수 생성자만 직접 선언
Member(String email)이 있으면 자동 기본 생성자는 없습니다. NoDefaultConstructor의 new Member()는 컴파일 오류입니다.
인수 없는 생성자도 직접 선언
Member()는 개발자가 작성한 본문을 실행합니다. 컴파일러가 자동 제공한 기본 생성자가 아닙니다.

서명 제공 조건과 업무상 유효성은 별개입니다. 인수 없는 생성을 열기 위해 필수 이메일을 무효한 값으로 숨길 필요는 없습니다.


생성자 오버로딩

src/OverloadedConstructors.java
public final class OverloadedConstructors {
    public static void main(String[] args) {
        Member basic = new Member("kim@example.com", 40);
        Member detailed = new Member("lee@example.com", 50, true);

        System.out.println(basic.describe());
        System.out.println(detailed.describe());
    }

    private static final class Member {
        private String email;
        private int age;
        private boolean active;

        Member(String email, int age) {
            this.email = email;
            this.age = age;
            this.active = false;
        }

        Member(String email, int age, boolean active) {
            this.email = email;
            this.age = age;
            this.active = active;
        }

        String describe() {
            return email + "=" + age + ":" + active;
        }
    }
}
kim@example.com=40:false
lee@example.com=50:true

매개변수 개수와 타입 목록이 달라 어떤 생성자를 호출할지 결정됩니다.

반환 타입은 없고 이름은 모두 클래스 이름이므로 일반 메서드 오버로딩과 마찬가지로 매개변수 목록이 구분 기준입니다.

이 코드는 동작하지만 email과 age 대입이 두 생성자에 복제됐습니다.

범위 검증을 추가할 때 한 생성자만 수정하면 다른 경로로 잘못된 객체가 만들어질 수 있습니다.


this 생성자 위임

src/DelegatingConstructors.java
public final class DelegatingConstructors {
    public static void main(String[] args) {
        Member basic = new Member("kim@example.com", 40);
        Member detailed = new Member("lee@example.com", 50, true);

        System.out.println(basic.describe());
        System.out.println(detailed.describe());
    }

    private static final class Member {
        private final String email;
        private final int age;
        private final boolean active;

        Member(String email, int age) {
            this(email, age, false);
        }

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

        String describe() {
            return email + "=" + age + ":" + active;
        }
    }
}
kim@example.com=40:false
lee@example.com=50:true

두 인수 생성자는 직접 필드를 대입하지 않고 세 인수 생성자를 호출합니다.

기본값 false를 결정하는 편의 경로와 실제 검증·대입 경로가 분리됩니다.

모든 생성은 결국 세 인수 생성자를 지나므로 불변식이 한곳에 있습니다.

두 Member 생성 경로가 한 검증 구현으로 모이는 관계

DelegatingConstructors에서 두 인수 경로는 active=false를 보충해 같은 객체의 세 인수 생성자에 위임한다. 상세 경로는 직접 그 생성자를 호출하고 email과 age만 검증한다.

DelegatingConstructors의 생성 경로입니다. this(...)는 같은 객체의 다른 생성자에 위임하며 새 객체를 하나 더 만들지 않습니다.

기본값 보충 경로와 상세 경로가 공유하는 Member 생성자 basic의 두 인수 Member 생성자는 active false를 보충해 세 인수 생성자에 위임한다. detailed는 active true로 세 인수 생성자를 직접 호출한다. 각 생성은 자기 객체에서 이메일과 나이를 검사하고 세 필드를 대입한다. this(...) 위임 직접 호출 두 인수 경로 · basic "kim@example.com", 40 Member(String email, int age) this(email, age, false); 세 인수 경로 · detailed "lee@example.com", 50, true 호출자가 active를 지정 Member(String email, int age, boolean active) 검사: email은 null·공백 아님, age는 14~120 통과 후 this.email, this.age, this.active에 대입 실패하면 IllegalArgumentException → 정상 참조 반환 없음 각 호출은 별도 객체를 생성하며 구현만 공유
두 인수 경로 · basic
"kim@example.com", 40을 받는 생성자가 this(email, age, false)로 같은 객체의 세 인수 생성자에 위임합니다.
세 인수 경로 · detailed
"lee@example.com", 50, true로 세 인수 생성자를 직접 호출합니다.
두 경로가 공유하는 검증·대입 구현

email은 null·공백이 아니고 age는 14~120이어야 합니다. 통과하면 this.email, this.age, this.active에 대입합니다.

실패하면 IllegalArgumentException이 발생하고 정상 참조를 반환하지 않습니다. 각 호출은 별도 객체를 생성하며 구현만 공유합니다.

결과는 basic: kim@example.com=40:false, detailed: lee@example.com=50:true입니다. active는 전달한 불리언 값을 저장하며 별도 검증 조건은 없습니다.

앞의 OverloadedConstructors는 두 생성자에 필드 대입이 복제되어 있고 검증 코드는 없습니다. 여기서는 대입과 추가한 검증을 세 인수 생성자 한곳에 모았습니다.

이 예제는 this(...)를 첫 문장에 두어 다른 생성자에 먼저 위임합니다.

Java 25에서는 유연한 생성자 본문을 통해 this(...) 앞에도 매개변수 검증 같은 문장을 둘 수 있습니다. 다만 초기 생성 문맥의 제한을 지켜야 하므로 현재 객체의 필드를 읽거나 인스턴스 메서드를 호출할 수는 없습니다.

한 생성자 본문에는 명시적인 this(...) 또는 super(...) 호출을 하나만 둘 수 있습니다.


this와 this()

this.email = email;

this.는 현재 객체의 필드나 메서드에 접근합니다.

this(email, age, false);

this()는 같은 클래스의 다른 생성자를 호출합니다.

둘 다 현재 객체와 관련 있지만 하나는 멤버 접근, 다른 하나는 생성자 위임입니다.

생성자 밖에서 this()를 호출할 수 없습니다.

위임 고리는 순환하면 안 됩니다.

Member(String email) {
    this(email, 30);
}

Member(String email, int age) {
    // this(email); // 서로 호출하면 recursive constructor invocation 오류
}

편의 생성자는 더 많은 정보를 받는 중심 생성자로 한 방향으로 모이게 설계합니다.


기본값의 업무 의미

Member의 active=false는 아직 활성화되지 않았다는 명확한 상태라 기본값으로 사용할 수 있습니다.

반면 email=""이나 age=0은 유효하지 않은 값이라 기본 생성자에 넣기 부적절합니다.

기본 생성자가 유용한 경우도 있습니다.

  • 모든 필드 기본값이 유효한 상태를 이룰 때
  • 프레임워크가 매개변수 없는 생성자를 요구하고 이후 안전한 바인딩 절차가 있을 때
  • 빈 컨테이너처럼 내용이 없어도 완전한 객체일 때

단지 호출을 짧게 만들려고 필수 정보를 숨기면 오류가 실행 시점으로 늦춰집니다.

생성자 서명은 객체 생성 계약을 읽는 문서입니다.


MemberRegistry 생성 정책

src/MemberRegistryConstructorPolicy.java
public final class MemberRegistryConstructorPolicy {
    public static void main(String[] args) {
        MemberRegistry defaultRegistry = new MemberRegistry();
        MemberRegistry namedRegistry = new MemberRegistry("work", 2);

        defaultRegistry.add("kim@example.com", 40);
        namedRegistry.add("lee@example.com", 50);

        System.out.println(defaultRegistry.summary());
        System.out.println(namedRegistry.summary());
    }

    private static final class MemberRegistry {
        private static final int DEFAULT_CAPACITY = 10;
        private final String name;
        private final Member[] members;
        private int size;

        MemberRegistry() {
            this("default", DEFAULT_CAPACITY);
        }

        MemberRegistry(int capacity) {
            this("default", capacity);
        }

        MemberRegistry(String name, int capacity) {
            if (name == null || name.isBlank() || capacity <= 0) {
                throw new IllegalArgumentException("invalid registry configuration");
            }
            this.name = name;
            this.members = new Member[capacity];
        }

        void add(String email, int age) {
            if (size == members.length) {
                throw new IllegalStateException("full");
            }
            members[size++] = new Member(email, age);
        }

        String summary() {
            return name + ":count=" + size + ", capacity=" + members.length;
        }
    }

    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;
        }
    }
}
default:count=1, capacity=10
work:count=1, capacity=2

세 생성 경로는 이름과 용량을 받는 중심 생성자로 모입니다.

기본 정책은 한 상수로 표현되고 검증은 한 번만 수행됩니다.

MemberRegistry()가 존재하는 이유도 유효한 기본 이름과 용량을 정할 수 있기 때문입니다.

MemberRegistry의 세 생성 경로와 유효한 기본 정책

MemberRegistryConstructorPolicy는 default 이름과 용량 10을 기본값으로 쓰고 세 생성 경로가 이름과 양수 용량 검사, 배열 생성 구현을 공유한다. main은 기본 경로와 work 용량 2 경로만 호출한다.

MemberRegistryConstructorPolicy의 세 서명입니다. DEFAULT_CAPACITY = 10이며 생성 직후에는 모두 size = 0입니다.

생성 경로별 기본값 보충과 main 호출 여부
생성 경로중심 생성자에 전달되는 이름과 용량본문 main 관찰
인수 없음MemberRegistry()

this("default", DEFAULT_CAPACITY)

이름 "default", 용량 10

회원 1명 추가 후

default:count=1, capacity=10

용량만MemberRegistry(int capacity)

this("default", capacity)

이름만 기본값, 용량은 호출자가 지정

선언되어 있지만 이 main에서는 호출하지 않습니다.
이름과 용량MemberRegistry(String name, int capacity)

호출자가 두 값을 모두 지정합니다.

"work", 2로 생성하고 회원 1명 추가 후

work:count=1, capacity=2

인수 없음

MemberRegistry()는 this("default", DEFAULT_CAPACITY)로 위임해 이름 "default", 용량 10을 사용합니다.

main에서 회원 1명 추가 후 default:count=1, capacity=10을 출력합니다.

용량만
MemberRegistry(int capacity)는 this("default", capacity)로 위임합니다. 이름만 기본값이고 용량은 호출자가 지정합니다. 이 main에서는 호출하지 않습니다.
이름과 용량

MemberRegistry(String name, int capacity)는 두 값을 직접 받는 중심 생성자입니다.

main에서는 "work", 2로 생성하고 회원 1명 추가 후 work:count=1, capacity=2를 출력합니다.

중심 생성자는 이름이 null·공백이거나 용량이 0 이하이면 IllegalArgumentException을 던집니다. 통과한 경우에만 이름을 대입하고 new Member[capacity]를 생성합니다.

이는 저장소 설정의 검증입니다. 이 파일의 Member 생성자는 이메일·나이 검증 없이 값을 대입하므로, 앞의 검증된 Member 예제와 혼합하지 않습니다.


연습 문제

이메일과 표시 이름을 필수로, 프로필 공개 여부를 선택으로 받는 MemberProfile을 만드세요.

두 인수 생성자는 프로필 비공개를 기본값으로 세 인수 생성자에 위임합니다.

이메일과 표시 이름은 공백일 수 없습니다.

해설 보기
src/MemberProfileConstructorExercise.java
public final class MemberProfileConstructorExercise {
    public static void main(String[] args) {
        MemberProfile privateProfile = new MemberProfile("kim@example.com", "김회원");
        MemberProfile publicProfile = new MemberProfile("lee@example.com", "이회원", true);

        System.out.println(privateProfile.describe());
        System.out.println(publicProfile.describe());
    }

    private static final class MemberProfile {
        private final String email;
        private final String displayName;
        private final boolean publicProfile;

        MemberProfile(String email, String displayName) {
            this(email, displayName, false);
        }

        MemberProfile(String email, String displayName, boolean publicProfile) {
            if (email == null || email.isBlank() || displayName == null || displayName.isBlank()) {
                throw new IllegalArgumentException("profile");
            }
            this.email = email;
            this.displayName = displayName;
            this.publicProfile = publicProfile;
        }

        String describe() {
            return email + "/" + displayName + "/" + publicProfile;
        }
    }
}
kim@example.com/김회원/false
lee@example.com/이회원/true

편의 생성자에도 검증을 복사하지 않았습니다.

어느 경로로 생성해도 중심 생성자가 같은 불변식을 적용합니다.