6.3 생성자 (Constructor)
클래스로부터 객체를 생성할 때, 생성과 동시에 인스턴스 변수들을 원하는 값으로 초기화해주는 특수한 코드 블록을 생성자(Constructor) 라고 합니다.
1. 생성자란?
생성자는 메서드처럼 생겼지만 몇 가지 독특한 특징을 가집니다.
생성자의 3가지 특징
- 클래스 이름과 완전히 같은 이름 을 가져야 합니다.
- 반환 타입이 없으며,
void도 쓰지 않습니다. 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()의 규칙
- 반드시 생성자의 첫 번째 줄 에만 작성할 수 있습니다.
- 메서드가 아닌 생성자 내부 에서만 사용 가능합니다.
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 필드 + 생성자로만 초기화 |
| 빌더 패턴 | 매개변수 많을 때 가독성 있게 객체 생성 |