생성자

박병욱·2025년 5월 20일

Java

목록 보기
4/38
post-thumbnail

🌱 생성자는 왜 필요할까?

객체를 생성하는 시점에 뭔가 하고 싶다면 생성자를 이용하면 된다. 근데 생성자가 왜 필요한건지 코드로 확인하고 진행하도록 하자.

package construct;

public class MemberInit {
    String name;
    int age;
    int grade;
}
package construct;

public class MethodInitMain1 {
    public static void main(String[] args) {
        MemberInit member1 = new MemberInit();
        member1.name = "사용자1";
        member1.age = 15;
        member1.grade = 90;

        MemberInit member2 = new MemberInit();
        member2.name = "사용자2";
        member2.age = 20;
        member2.grade = 100;

        MemberInit[] members = {member1, member2};

        for (MemberInit member : members) {
            System.out.println("이름:" + member.name + " 나이:" + member.age + " 성적:" + member.grade);
        }
    }
}

/*
이름:사용자1 나이:15 성적:90
이름:사용자2 나이:20 성적:100
*/

이처럼 회원 객체를 생성하고 나면, name, age, grade 같은 멤버 변수에 초기값을 설정해줘야 한다. 이름, 나이, 성적이 아무것도 입력 안 된 회원이 있다면 무섭겠지? 그렇기 때문에 객체를 제대로 사용하기 위해서는 객체를 생성하자마자 초기값들을 설정해야 한다.

 

하지만, 위의 코드에서 초기값을 설정하는 로직을 보면 반복이 일어난다. 메서드를 통해 아래와 같이 반복을 제거하도록 하자.

package construct;

public class MethodInitMain2 {
    public static void main(String[] args) {
        MemberInit member1 = new MemberInit();
        initMember(member1, "사용자1", 15, 90);

        MemberInit member2 = new MemberInit();
        initMember(member2, "사용자2", 20, 100);

        MemberInit[] members = {member1, member2};

        for (MemberInit member : members) {
            System.out.println("이름:" + member.name + " 나이:" + member.age + " 성적:" + member.grade);
        }
    }

    static void initMember(MemberInit member, String name, int age, int grade) {
        member.name = name;
        member.age = age;
        member.grade = grade;
    }
}

반복을 제거하는 것을 성공했지만, initMember() 메서드는 대부분 MemberInit 객체의 멤버 변수를 사용한다. 이런 경우에 속성과 기능을 한 곳에 묶어 두는 것이 더 나은 방법이라고 했다. 쉽게 말해, MemberInit이 자기 자신의 데이터를 변경하는 메서드를 제공하는 것이 좋다는 말이다.


👍 this

이제 이전 코드의 MemberInit 클래스에 initMember() 메서드를 추가하도록 하자.

package construct;

public class MemberInit {
    String name;
    int age;
    int grade;

    void initMember(String name, int age, int grade) {
        this.name = name;
        this.age = age;
        this.grade = grade;
    }
}
package construct;

public class MethodInitMain3 {
    public static void main(String[] args) {
        MemberInit member1 = new MemberInit();
        member1.initMember("사용자1", 15, 90);

        MemberInit member2 = new MemberInit();
        member2.initMember("사용자2", 20, 100);

        MemberInit[] members = {member1, member2};

        for (MemberInit member : members) {
            System.out.println("이름:" + member.name + " 나이:" + member.age + " 성적:" + member.grade);
        }
    }
}

initMember() 메서드는 MemberInit에 초기값 설정 기능을 제공하는 메서드다. 근데 메서드의 매개 변수에 정의한 nameMemberInit의 멤버 변수의 이름이 name으로 동일한 것을 볼 수 있다. 나머지 멤버 변수도 마찬가지다.

 

그럼 이 클래스의 멤버 변수랑 매개 변수를 어떻게 구분해야 할까?

  • 이 경우 멤버 변수보다 매개변수 코드 블록이 더 안쪽에 있기 때문에 매개 변수가 우선순위를 가진다. 따라서 initMember() 메서드 안에서 name이라고 적으면 매개 변수에 접근하게 된다.
  • 멤버 변수에 접근하려면 앞에 this.를 붙여주면 된다. this“인스턴스 자신의 참조값” 을 가리킨다.

this.name = name; // 1. 오른쪽의 name은 매개 변수에 접근한다.
this.name = "user"; // 2. name 매개 변수로 받아온 값을 사용한다.
x001.name = "user"; // 3. 자신의 멤버 변수에 "user" 값을 대입한다.

 

this를 제거한다면, 당연하게도 name, age, grade가 매개 변수를 의미하므로 멤버 변수에 접근할 방법이 없어진다. 그렇기 때문에, 매개 변수의 이름과 멤버 변수의 이름이 같은 경우에 this를 사용해서 둘을 명확하게 구분할 필요가 있다.


🖐 this의 생략

하지만 this는 생략할 수 있다. 이 경우에는, 변수를 찾을 때 가까운 지역 변수(매개 변수도 지역 변수)를 먼저 찾고 없다면 그 다음으로 멤버 변수를 찾는다. 이때 멤버 변수도 없다면 오류가 발생한다. 아래 예제를 살펴보자.

package construct;

public class MemberThis {
    String nameField;

    void initMember(String nameParameter) {
        nameField = nameParameter;
    }
}

보다시피 nameField는 앞에 this가 없어도 멤버 변수에 접근할 수 있다. 일련의 과정을 살펴보자면, nameField는 먼저 지역 변수에서 같은 이름이 있는지 스캔한다. 지금 매개 변수의 이름이 nameParameter이므로 멤버 변수로 시선을 옮긴다.

 

해당 코드가 멤버 변수를 사용하고 있다는 것을 쉽게 알아볼 수 있도록 this를 붙여주는 코딩 스타일도 많았지만, IDE가 색깔로 쉽게 구분해주기 때문에 요즘은 권장되지 않는다.


🏁 생성자 - 도입

보통 객체를 생성하면 바로 초기값을 할당해야 하는 경우가 대다수다. 그럼 매번 initMember() 메서드를 만들어줘야 한다는 말인가? 생성자가 있어서 정말 다행이다. 대부분의 객체 지향 언어는 객체를 생성하자마자 즉시 필요한 기능을 편리하게 수행할 수 있도록 “생성자” 라는 기능을 제공한다.

 

아래 코드를 살펴보자.

package construct;

public class MemberConstruct {
	String name;
	int age;
	int grade;

	// 아래 부분이 생성자
	MemberConstruct(String name, int age, int grade) {
		System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade="
+ grade);
				
		this.name = name;
		this.age = age;
		this.grade = grade;
	}
}

보다시피, 생성자는 메서드와 비슷하지만 약간의 차이가 있다.

  • 생성자의 이름은 클래스 이름과 같아야 한다. 따라서 첫 글자도 대문자로 시작한다.
  • 생성자는 반환 타입이 없다.

 

package construct;

public class ConstructMain1 {
	public static void main(String[] args) {
		MemberConstruct member1 = new MemberConstruct("사용자1", 15, 90);
		MemberConstruct member2 = new MemberConstruct("사용자2", 20, 100);
				
		MemberConstruct[] members = {member1, member2};
		for (MemberConstruct s : members) {
			System.out.println("이름:" + s.name + " 나이:" + s.age + " 성적:" +
s.grade);
		}
	}
}

/*
생성자 호출 name=사용자1,age=15,grade=90
생성자 호출 name=사용자2,age=20,grade=100
이름:사용자1 나이:15 성적:90
이름:사용자2 나이:20 성적:100
*/

생성자는 인스턴스를 생성하자마자 호출된다. 호출하는 방법은 위와 같이 new 명령어 다음에 생성자 이름과 매개 변수에 맞춰 인수를 전달하면 끝이다.

 

👍 생성자의 장점

먼저, 중복 호출을 제거해준다. 생성자가 없었을 때는 어떤 작업을 수행하기 위해서는 객체를 만들고 메서드를 직접 다시 호출해야 했다. 하지만, 생성자는 이 과정을 한방에 처리해준다.

// 메서드를 직접 다시 호출
MemberInit member = new MemberInit();
member.initMember("사용자1", 15, 90);

// 생성자로 한방에 처리
MemberConstruct member = new MemberConstruct("사용자1", 15, 90);

또, 제약이라는 관점에서 장점이 있다. 만약 메서드를 호출하는 코드에서 실수로 메서드를 호출하지 않으면 어떻게 될까? 문법적으로는 아무 문제가 없기 때문에 프로그램은 정상적으로 컴파일되고 돌아가지만, 메서드를 빼먹은 객체는 유령 객체가 되고 만다. 이런 유령 객체가 시스템 내부에 돌아다니고 있으면 큰 문제가 발생할 수도 있다.

이러한 관점에서 생성자 로직에서는 무언가를 빼먹으면 컴파일 오류가 발생한다. 내가 실수를 해도 생성자가 곧바로 꾸짖어준다. 이처럼 객체를 생성할 때 직접 정의한 생성자가 있다면, 반드시 호출해야 하는 것이다. 참고로 생성자를 메서드 오버로딩처럼 여러 개를 정의할 수도 있는데, 이럴 때는 하나만 호출하면 된다.


🦴 기본 생성자

생성자를 만들지 않았는데, 생성자를 호출한 적이 있었다. 아래 코드를 통해 확인해보자.

public class MemberInit {
	String name;
	int age;
	int grade;
}
package construct;

public class MethodInitMain1 {
    public static void main(String[] args) {
        MemberInit member1 = new MemberInit();
        member1.name = "사용자1";
        member1.age = 15;
        member1.grade = 90;

        MemberInit member2 = new MemberInit();
        member2.name = "사용자2";
        member2.age = 20;
        member2.grade = 100;

        MemberInit[] members = {member1, member2};

        for (MemberInit member : members) {
            System.out.println("이름:" + member.name + " 나이:" + member.age + " 성적:" + member.grade);
        }
    }
}

위의 new MemberInit() 부분을 보면, 매개 변수가 없는 생성자가 필요한 것처럼 보인다. 그런 생성자를 “기본 생성자” 라고 하며, 만약 자바 컴파일러는 클래스에 생성자가 하나도 없다면, 매개 변수 및 작동하는 코드가 없는 기본 생성자를 자동으로 만들어준다. 생성자가 하나라도 있으면 자바는 기본 생성자를 만들지 않는다.

 

예제를 통해 더 알아보자.

package construct;

public class MemberDefault {
    String name;

    public MemberDefault() {} // 자바가 자동으로 기본 생성자를 만든다.
}
package construct;

public class MemberDefaultMain {
    public static void main(String[] args) {
        MemberDefault memberDefault = new MemberDefault();
    }
}

MemberDefault 클래스에 생성자가 하나도 없다면, 위의 코드처럼 보이지는 않지만 기본 생성자를 만들어주는 것이다.

package construct;

public class MemberDefault {
    String name;

    MemberDefault() {
        System.out.println("생성자 호출");
    }
}

위와 같이 기본 생성자를 직접 정의해도 문제 없다.

 

🤔 자바는 왜 기본 생성자를 만들어줄까?

만약 기본 생성자를 만들어주지 않는다면 생성자 기능이 필요하지 않은 경우에도 모든 클래스에 개발자가 직접 기본 생성자를 정의해야 할 것이다. 대부분은 아니지만, 이렇게 생성자 기능을 사용하지 않는 상황도 있기 때문에 이런 편의 기능을 제공하는 것이다.

정리하자면, 생성자는 반드시 호출되어야 하고, 만약 없으면 기본 생성자가 제공된다. 생성자가 하나라도 있다면 기본 생성자는 제공되지 않고, 개발자가 정의한 생성자를 직접 호출해야 한다.


👣 생성자 - 오버로딩과 this()

생성자도 메서드 오버로딩처럼 매개 변수만 다르게 해서 여러 생성자를 제공할 수 있다. 아래 코드를 보자.

package construct;

public class MemberConstruct {
    String name;
    int age;
    int grade;

    // 추가
    MemberConstruct(String name, int age) {
        this.name = name;
        this.age = age;
        this.grade = 50;
    }

    MemberConstruct(String name, int age, int grade) {
        System.out.println("생성자 호출 name=" + name + ", age=" + age + ", grade=" + grade);
        this.name = name;
        this.age = age;
        this.grade = grade;
    }
}

위와 같이 MemberConstruct에 생성자를 추가할 수 있다. name, age만 받는 생성자와 name, age, grade 모두 받는 생성자, 총 2개가 있는 것이다.

 

package construct;

public class ConstructMain2 {
    public static void main(String[] args) {
        MemberConstruct member1 = new MemberConstruct("사용자1", 15, 90);
        MemberConstruct member2 = new MemberConstruct("사용자2", 20);  // grade 빠짐

        MemberConstruct[] members = {member1, member2};
        for (MemberConstruct s : members) {
            System.out.println("이름:" + s.name + " 나이:" + s.age + " 성적:" +
                    s.grade);
        }
    }
}

/*
생성자 호출 name=사용자1, age=15, grade=90
이름:사용자1 나이:15 성적:90
이름:사용자2 나이:20 성적:50
*/

사용자2의 생성자는 보다시피 grade를 받지 않고, 50으로 초기화 해준다. 이런 식으로 생성자를 오버로딩해서 원하는 생성자를 호출할 수 있다. 성적 입력이 꼭 필요한 경우에, grade가 있는 생성자를 호출하면 되고, 그렇지 않은 경우에는 grade가 없는 생성자를 호출하면 되는 것이다.


👀 this()

근데 2개의 MemberConstruct를 보면 중복되는 부분이 보인다.

	MemberConstruct(String name, int age) {
        this.name = name;
        this.age = age;
        this.grade = 50;
    }

    MemberConstruct(String name, int age, int grade) {
        System.out.println("생성자 호출 name=" + name + ", age=" + age + ", grade=" + grade);
        this.name = name;
        this.age = age;
        this.grade = grade;
    }

 

중복을 어떻게 제거할 수 있을까? 아래 코드를 살펴보자.

	MemberConstruct(String name, int age) {
		this(name, age, 50);
    }

    MemberConstruct(String name, int age, int grade) {
        System.out.println("생성자 호출 name=" + name + ", age=" + age + ", grade=" + grade);
        this.name = name;
        this.age = age;
        this.grade = grade;
    }

this()라는 기능을 사용하면 생성자 내부에서 자신의 생성자를 호출할 수 있다. this는 인스턴스 자신의 참조값을 가리키므로 자신의 생성자를 호출한다고 생각하면 된다.

첫 번째 생성자 MemberConstruct(String name, int age) 내부에서 두 번째 생성자를 호출하는 것이다. 이처럼 this()를 통해 생성자 내부에서 다른 생성자를 호출하는 방식으로 중복을 제거할 수 있다. this()는 생성자 코드의 첫 줄에만 작성할 수 있다는 점도 유의하자.


📖 문제 - Book과 생성자

package construct;

public class BookMain {
    public static void main(String[] args) {
        Book book1 = new Book();
        book1.displayInfo();
        
        Book book2 = new Book("Hello Java", "Seo");
        book2.displayInfo();
        
        Book book3 = new Book("JPA 프로그래밍", "kim", 700);
        book3.displayInfo();
    }
}

위의 코드가 잘 작동하도록 Book 클래스를 만들면 된다. 클래스의 생성자 코드에 중복이 없도록 설계하자.

 

// 내가 푼 풀이
package construct;

public class Book {
    String title;
    String author;
    int page;

    Book() {
        this("", "", 0);
    }

    Book(String title, String author) {
        this(title, author, 0);
    }

    Book(String title, String author, int page) {
        this.title = title;
        this.author = author;
        this.page = page;
    }

    void displayInfo() {
        System.out.println("제목:" + title + ", 저자:" + author + ", 페이지:" + page);
    }
}

이처럼 생성자는 객체 생성 직후 객체를 초기화 하기 위한 특별한 메서드라고 생각하면 된다.

profile
도메인을 이해하는 백엔드 개발자(feat. OOP)

0개의 댓글