본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
5장 : 객체 책임과 캡슐화

생성자 오버로딩과 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";
}

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

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


생성자 오버로딩

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

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

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

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

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


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를 결정하는 편의 경로와 실제 검증·대입 경로가 분리됩니다.

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

this(...)는 생성자의 첫 문장이어야 합니다.

다른 문장을 먼저 실행하면 아직 현재 객체의 초기화 경로가 결정되지 않았으므로 컴파일 오류입니다.

한 생성자에서 this(...)와 직접적인 다른 생성자 호출을 함께 할 수도 없습니다.


thisthis()

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()가 존재하는 이유도 유효한 기본 이름과 용량을 정할 수 있기 때문입니다.


연습 문제

이메일과 표시 이름을 필수로, 프로필 공개 여부를 선택으로 받는 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

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

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