본문으로 건너뛰기

6.3 생성자 (Constructor)

클래스로부터 객체를 생성할 때, 생성과 동시에 인스턴스 변수들을 원하는 값으로 초기화해주는 특수한 코드 블록을 생성자(Constructor) 라고 합니다.

1. 생성자란?​

생성자는 메서드처럼 생겼지만 몇 가지 독특한 특징을 가집니다.

생성자의 3가지 특징​

  1. 클래스 이름과 완전히 같은 이름 을 가져야 합니다.
  2. 반환 타입이 없으며, void도 쓰지 않습니다.
  3. new 키워드로 객체가 생성될 때 자동으로 딱 한 번 호출됩니다.
public class Person {
String name;
int age;

// 생성자: 클래스명과 동일, 반환타입 없음
Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("Person 객체 생성됨: " + name + ", " + age + "세");
}
}

public class PersonTest {
public static void main(String[] args) {
// new Person(...) 호출 시 생성자가 자동으로 실행됨
Person p1 = new Person("홍길동", 25);
// 출력: Person 객체 생성됨: 홍길동, 25세

Person p2 = new Person("이순신", 40);
// 출력: Person 객체 생성됨: 이순신, 40세
}
}
왜 생성자를 사용하나요?

생성자 없이도 객체 생성 후 필드에 값을 직접 할당할 수 있습니다. 하지만 생성자를 사용하면 객체 생성과 동시에 초기화 가 보장되어, 불완전한 상태의 객체가 생기는 것을 방지할 수 있습니다.

2. 기본 생성자 (Default Constructor)​

매개변수가 없는 생성자를 기본 생성자(Default Constructor) 라고 합니다.

컴파일러가 자동으로 제공하는 조건​

클래스에 생성자가 하나도 선언되지 않은 경우에만, 컴파일러가 아래와 같은 빈 기본 생성자를 자동으로 추가합니다.

// 개발자가 아무 생성자도 작성하지 않은 경우
class Dog {
String name;
}
// 컴파일러가 자동으로 추가: Dog() { }

Dog d = new Dog(); // 정상 동작

기본 생성자가 없어지는 경우​

class Dog {
String name;

// 매개변수 있는 생성자를 선언하면
// 컴파일러는 기본 생성자를 더 이상 자동으로 추가하지 않음!
Dog(String name) {
this.name = name;
}
}

public class DogTest {
public static void main(String[] args) {
// Dog d = new Dog(); // 컴파일 에러! 기본 생성자 없음
Dog d = new Dog("멍멍이"); // 정상
}
}
기본 생성자 주의

매개변수 있는 생성자를 하나라도 선언하면, 기본 생성자가 자동 추가되지 않습니다. 기본 생성자가 필요하다면 명시적으로 직접 선언해야 합니다.

3. 생성자 오버로딩​

생성자도 메서드처럼 오버로딩이 가능합니다. 매개변수의 타입, 개수, 순서가 다른 여러 생성자를 선언할 수 있습니다.

public class Person {
String name;
int age;
String email;

// 생성자 1: 기본 생성자
Person() {
name = "이름 없음";
age = 0;
email = "";
}

// 생성자 2: 이름만
Person(String name) {
this.name = name;
this.age = 0;
this.email = "";
}

// 생성자 3: 이름 + 나이
Person(String name, int age) {
this.name = name;
this.age = age;
this.email = "";
}

// 생성자 4: 모든 필드
Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}

void printInfo() {
System.out.printf("이름: %s, 나이: %d, 이메일: %s%n", name, age, email);
}
}

public class PersonTest {
public static void main(String[] args) {
Person p1 = new Person();
Person p2 = new Person("홍길동");
Person p3 = new Person("이순신", 40);
Person p4 = new Person("장보고", 35, "jbg@korea.com");

p1.printInfo(); // 이름: 이름 없음, 나이: 0, 이메일:
p2.printInfo(); // 이름: 홍길동, 나이: 0, 이메일:
p3.printInfo(); // 이름: 이순신, 나이: 40, 이메일:
p4.printInfo(); // 이름: 장보고, 나이: 35, 이메일: jbg@korea.com
}
}

4. this()를 이용한 생성자 체이닝​

위 예시에서 생성자마다 초기화 코드가 중복됩니다. this()를 사용하면 중복을 제거할 수 있습니다.

this()의 규칙​

  1. 반드시 생성자의 첫 번째 줄 에만 작성할 수 있습니다.
  2. 메서드가 아닌 생성자 내부 에서만 사용 가능합니다.
public class Person {
String name;
int age;
String email;

// 기본 생성자: 가장 많은 매개변수의 생성자에게 위임
Person() {
this("이름 없음", 0, ""); // 생성자 체이닝!
}

Person(String name) {
this(name, 0, ""); // 생성자 체이닝!
}

Person(String name, int age) {
this(name, age, ""); // 생성자 체이닝!
}

// 핵심 생성자: 실제 초기화 로직이 집중된 곳
Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
}
생성자 체이닝의 장점

초기화 코드를 가장 많은 매개변수를 가진 생성자 하나에 집중시키면, 나중에 초기화 로직이 변경될 때 그 생성자만 수정하면 됩니다. 코드 중복이 줄고 유지보수성이 높아집니다.

5. 복사 생성자 (Copy Constructor)​

기존 객체와 동일한 상태를 갖는 새 객체를 만들 때 사용합니다.

public class Point {
int x;
int y;

// 일반 생성자
Point(int x, int y) {
this.x = x;
this.y = y;
}

// 복사 생성자: 기존 Point 객체를 받아 동일한 값으로 초기화
Point(Point other) {
this.x = other.x;
this.y = other.y;
}

@Override
public String toString() {
return "Point(" + x + ", " + y + ")";
}
}

public class CopyConstructorTest {
public static void main(String[] args) {
Point original = new Point(3, 7);
Point copy = new Point(original); // 복사 생성자 사용

System.out.println("원본: " + original); // Point(3, 7)
System.out.println("복사: " + copy); // Point(3, 7)

// 복사본 변경이 원본에 영향을 주지 않음 (독립적인 객체)
copy.x = 100;
System.out.println("변경 후 원본: " + original); // Point(3, 7)
System.out.println("변경 후 복사: " + copy); // Point(100, 7)
}
}

6. 불변 객체 (Immutable Object)​

생성 후 상태를 변경할 수 없는 객체입니다. private final 필드와 생성자만 사용합니다.

public final class ImmutablePoint {
// final 필드: 한 번 설정되면 변경 불가
private final int x;
private final int y;

// 생성자에서만 초기화 가능
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}

// Getter만 있고 Setter는 없음
public int getX() { return x; }
public int getY() { return y; }

// 새 위치로 이동하려면 새 객체를 반환
public ImmutablePoint move(int dx, int dy) {
return new ImmutablePoint(x + dx, y + dy);
}

@Override
public String toString() {
return "ImmutablePoint(" + x + ", " + y + ")";
}
}

public class ImmutableTest {
public static void main(String[] args) {
ImmutablePoint p1 = new ImmutablePoint(3, 5);
System.out.println("원본: " + p1); // ImmutablePoint(3, 5)

// p1.x = 10; // 컴파일 에러! final 필드는 변경 불가

// 이동하면 새 객체가 만들어짐 (원본은 그대로)
ImmutablePoint p2 = p1.move(2, 3);
System.out.println("이동 후 원본: " + p1); // ImmutablePoint(3, 5)
System.out.println("이동 후 p2: " + p2); // ImmutablePoint(5, 8)
}
}
불변 객체의 장점
  • 스레드 안전(Thread-Safe): 상태가 변하지 않아 동시 접근이 안전합니다.
  • 예측 가능: 어디서든 값이 동일하여 디버깅이 쉽습니다.
  • Java에서의 활용: String, Integer, LocalDate 등이 불변 클래스입니다.

7. 빌더 패턴(Builder Pattern) 소개​

매개변수가 많은 생성자는 읽기 어렵습니다. 빌더 패턴은 이를 해결합니다.

public class Computer {
// 필수 필드
private final String cpu;
private final String ram;
// 선택 필드
private final String storage;
private final String gpu;
private final boolean wifi;

// private 생성자: 빌더를 통해서만 생성 가능
private Computer(Builder builder) {
this.cpu = builder.cpu;
this.ram = builder.ram;
this.storage = builder.storage;
this.gpu = builder.gpu;
this.wifi = builder.wifi;
}

// 내부 빌더 클래스
public static class Builder {
private final String cpu; // 필수
private final String ram; // 필수
private String storage = "256GB SSD"; // 기본값
private String gpu = "내장 그래픽"; // 기본값
private boolean wifi = true; // 기본값

// 필수 값은 빌더 생성자에서
public Builder(String cpu, String ram) {
this.cpu = cpu;
this.ram = ram;
}

// 선택 값은 메서드 체이닝으로
public Builder storage(String storage) {
this.storage = storage;
return this;
}

public Builder gpu(String gpu) {
this.gpu = gpu;
return this;
}

public Builder wifi(boolean wifi) {
this.wifi = wifi;
return this;
}

public Computer build() {
return new Computer(this);
}
}

@Override
public String toString() {
return String.format("Computer{cpu='%s', ram='%s', storage='%s', gpu='%s', wifi=%b}",
cpu, ram, storage, gpu, wifi);
}
}

public class BuilderTest {
public static void main(String[] args) {
// 기본 구성
Computer basic = new Computer.Builder("Intel i5", "8GB")
.build();

// 고사양 구성
Computer gaming = new Computer.Builder("Intel i9", "32GB")
.storage("2TB NVMe SSD")
.gpu("RTX 4090")
.wifi(true)
.build();

System.out.println(basic);
System.out.println(gaming);
}
}

출력:

Computer{cpu='Intel i5', ram='8GB', storage='256GB SSD', gpu='내장 그래픽', wifi=true}
Computer{cpu='Intel i9', ram='32GB', storage='2TB NVMe SSD', gpu='RTX 4090', wifi=true}

8. 실전 예제: Person 클래스 완전 구현​

public class Person {
private String name;
private int age;
private String email;
private String phone;

// 기본 생성자
public Person() {
this("홍길동", 0, "", "");
}

// 이름만
public Person(String name) {
this(name, 0, "", "");
}

// 이름 + 나이
public Person(String name, int age) {
this(name, age, "", "");
}

// 이름 + 나이 + 이메일
public Person(String name, int age, String email) {
this(name, age, email, "");
}

// 핵심 생성자
public Person(String name, int age, String email, String phone) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("이름은 필수입니다.");
}
if (age < 0 || age > 150) {
throw new IllegalArgumentException("나이가 올바르지 않습니다: " + age);
}
this.name = name;
this.age = age;
this.email = email;
this.phone = phone;
}

// 복사 생성자
public Person(Person other) {
this(other.name, other.age, other.email, other.phone);
}

// Getter
public String getName() { return name; }
public int getAge() { return age; }
public String getEmail() { return email; }
public String getPhone() { return phone; }

@Override
public String toString() {
return String.format("Person{name='%s', age=%d, email='%s', phone='%s'}",
name, age, email, phone);
}
}

public class PersonTest {
public static void main(String[] args) {
Person p1 = new Person();
Person p2 = new Person("이순신");
Person p3 = new Person("장보고", 35);
Person p4 = new Person("세종대왕", 50, "sejong@joseon.kr", "010-1234-5678");
Person p5 = new Person(p4); // 복사

System.out.println(p1);
System.out.println(p2);
System.out.println(p3);
System.out.println(p4);
System.out.println(p5);

// 유효성 검사 테스트
try {
Person invalid = new Person("", -5); // 예외 발생
} catch (IllegalArgumentException e) {
System.out.println("오류: " + e.getMessage());
}
}
}

출력:

Person{name='홍길동', age=0, email='', phone=''}
Person{name='이순신', age=0, email='', phone=''}
Person{name='장보고', age=35, email='', phone=''}
Person{name='세종대왕', age=50, email='sejong@joseon.kr', phone='010-1234-5678'}
Person{name='세종대왕', age=50, email='sejong@joseon.kr', phone='010-1234-5678'}
오류: 이름은 필수입니다.

요약​

개념핵심 내용
생성자클래스명 동일, 반환타입 없음, new 시 자동 호출
기본 생성자생성자 없을 때 컴파일러가 자동 추가
생성자 오버로딩매개변수가 다른 여러 생성자 선언
this()같은 클래스의 다른 생성자 호출, 반드시 첫 줄
복사 생성자기존 객체와 동일한 상태의 새 객체 생성
불변 객체private final 필드 + 생성자로만 초기화
빌더 패턴매개변수 많을 때 가독성 있게 객체 생성