코드스테이츠/section1

코드스테이츠 2주차 - 객체지향 프로그래밍 기초(생성자와 내부 클래스)

강예은 2023. 3. 1. 14:44

생성자(Constructor)

생성자는 말 그대로 객체를 생성하는 역할을 하는 클래스의 구성 요소로서, 인스턴스가 생성될 때 호출되는 인스턴스 초기화 메서드라 정리할 수 있습니다.

앞서 우리가 new 키워드를 사용하여 객체를 생성할 때에 호출되는 것이 사실 바로 이 생성자입니다. 그럼 이제 좀 더 자세하게 생성자에 대해서 알아보도록 하겠습니다.

 

종종 생성자라는 이름에서 생성자가 인스턴스를 생성하는 역할을 한다는 오해가 발생하는데, 이것은 사실이 아닙니다. 인스턴스 생성을 담당하는 것은 new 키워드이며, 생성자는 인스턴스 변수들을 초기화하는 데 사용되는 특수한 메서드라 할 수 있습니다.

 

생성자는 메서드와 비슷한 구조를 가지고 있지만 크게 두 가지 부분에서 큰 차이를 가집니다.

첫 번째는 생성자의 이름은 반드시 클래스의 이름과 같아야 합니다. 만약 클래스 이름과 생성자의 이름이 다르다면 그 메서드는 더이상 생성자로서의 기능을 수행할 수 없습니다.

두 번째로, 생성자는 리턴 타입이 없습니다. 하지만 메서드에서 리턴 값이 없을 때 표시하는 void 키워드를 사용하지 않습니다. 그 이유는 무언가를 ‘리턴하지 않는다’를 의미하는 void와는 다르게 생성자는 아예 리턴 타입 자체가 존재하지 않기 때문입니다.

 

이 두 가지 특징을 기억하면서 아래 예시를 한번 살펴봅시다.

클래스명(매개변수) { // 생성자 기본 구조
	...생략...
}

 

먼저 클래스명과 같은 이름의 생성자명을 작성해주고 리턴 타입이 없기 때문에 리턴 타입에는 아무것도 적지 않습니다. 매개변수는 있을 수도 있고 없을 수도 있습니다.

한 가지 기억해야하는 사실은 생성자도 앞서 학습했던 오버로딩이 가능하므로 한 클래스 내에 여러 개의 생성자가 존재할 수 있다는 점입니다.

public class ConstructorExample {
    public static void main(String[] args) {
        Constructor constructor1 = new Constructor();
        Constructor constructor2 = new Constructor("Hello World");
        Constructor constructor3 = new Constructor(5,10);
    }
}

class Constructor {
    Constructor() { // (1) 생성자 오버로딩
        System.out.println("1번 생성자");
    }

    Constructor(String str) { // (2) 
        System.out.println("2번 생성자");
    }

    Constructor(int a, int b) { // (3) 
        System.out.println("3번 생성자");
    }
}

위의 예시에서 확인할 수 있듯이, 오버로딩을 활용하여 같은 이름을 가진 생성자 여러 개를 만들 수 있습니다.

여기서, 생성자의 모양에 따라서 객체를 생성하는 방법이 결정됩니다. 예를 들면, (2)번 생성자를 호출하기 위해서는 객체 생성 시에 문자열을 전달해주어야 하고, (3)번 생성자를 위해서는 두 개의 int형 매개변수를 전달해주어야 합니다.

 

기본 생성자 vs 매개변수가 있는 생성자

기본 생성자(Default Constructor)

지금까지 우리는 생성자의 존재를 모르고 그저 new 키워드를 사용하여 생성자를 호출하여 객체를 만들었지만, 사실 모든 클래스에는 반드시 하나 이상의 생성자가 존재해야 합니다.

사실 지금까지 생성자를 따로 만들지 않아도 정상적으로 인스턴스를 만들 수 있었던 이유는 만약 생성자가 클래스 안에 포함되어 있지 않은 경우에는 자바 컴파일러가 기본 생성자를 자동으로 추가해줬기 때문입니다.

이 기본 생성자는 앞선 챕터에서도 우리가 봤듯이 매개변수가 없는 생성자를 의미합니다.

 

클래스명(){} //기본 생성자

DefaultConst(){} // 예시) DefaultConst 클래스의 기본 생성자

 

위의 예시에서 보실 수 있는 것처럼 컴파일러가 자동으로 추가해주는 기본 생성자에는 매개변수도 없고 바디에 아무런 내용이 없습니다. 그렇다면 만약에 생성자가 이미 추가되어있는 경우는 어떻게 될까요?

이 경우에는 기본생성자가 아니라 이미 추가되어 있는 생성자를 기본으로 사용하게 됩니다.

 

매개변수가 있는 생성자

그럼 매개변수가 있는 생성자의 경우도 간략하게 한번 살펴보겠습니다. 매개변수가 있는 생성자는 메서드처럼 매개변수를 통해 호출 시에 해당 값을 받아 인스턴스를 초기화하는 데 사용됩니다.

고유한 특성을 가진 인스턴스를 계속 만들어야하는 경우 인스턴스마다 각기 다른 값을 가지고 초기화할 수 있어서 매우 유용합니다. 간단한 예시를 통해서 확인해보겠습니다.


public class ConstructorExample {
    public static void main(String[] args) {
        Car c = new Car("Model X", "빨간색", 250);
        System.out.println("제 차는 " + c.getModelName() + "이고, 컬러는 " +  c.getColor() + "입니다.");
    }
}

class Car {
    private String modelName;
    private String color;
    private int maxSpeed;

    public Car(String modelName, String color, int maxSpeed) {
        this.modelName = modelName;
        this.color = color;
        this.maxSpeed = maxSpeed;
    }

    public String getModelName() {
        return modelName;
    }

    public String getColor() {
        return color;
    }
}

//Output
제 차는 Model X이고, 컬러는 빨간색입니다.

위의 예시를 보시면 Car 인스턴스를 생성 시 매개변수가 있는 생성자를 사용하게되면 인스턴스를 만든 후에 인스턴스의 필드값을 일일이 설정해줄 필요없이 생성과 동시에 원하는 값으로 설정해줄 수 있어서 굉장히 편리합니다.

또한, 보시는 것처럼 생성자의 모양에 따라서 객체를 생성하는 방법도 달라지게 됩니다. 앞의 기본 생성자의 경우에는 매개변수가 없었기 때문에 원래 우리가 객체를 생성하던 방식으로 new 키워드와 생성자를 호출하면 되었지만, 매개변수가 있는 경우에는 그 개수와 타입에 알맞게 생성자를 호출해주어야 합니다.

이렇듯 생성자의 특징을 잘 이해하면 보다 간결하고 직관적인 코드 작성이 가능합니다.

 

위의 예제에서 Car 클래스를 한번 살펴볼까요?

인스턴스 변수로 modelName, color, 그리고 maxSpeed가 선언되어 있는데, 동시에 생성자의 매개변수로 modelName, color, maxSpeed가 정의되어 있습니다.

이런 경우, 인스턴스 변수와 매개변수를 이름만으로는 구분하기가 어려워지는 문제가 발생하게 되는데, 이를 구분해주기 위한 용도로 주로 사용되는 방법이 바로 this 키워드라 할 수 있습니다.

만약에 위의 코드에서, this.modelName = modelName 대신 modelName = modelName 라고 작성하면 둘 다 지역변수로 간주되게 됩니다.

 

this 키워드에 대해 좀 더 구체적으로 설명해보면, 모든 메서드에는 자신이 포함된 클래스의 객체를 가리키는 this라는 참조변수가 있는데, 일반적인 경우에는 컴파일러가 this.를 추가해주기 때문에 생략하는 경우가 많습니다.

예를 들면, 현재 Car 클래스의 modelName이라는 인스턴스 필드를 클래스 내부에 출력하고자 한다면 원래는 System.out.println(this.modelName) 이런 방식으로 작성해주어야 합니다.

 

결론적으로 this는 인스턴스 자신을 가리키며, 우리가 참조변수를 통해 인스턴스의 멤버에 접근할 수 있는 것처럼 this를 통해서 인스턴스 자신의 변수에 접근할 수 있는 것입니다.

그리고 위의 예시에서 봤던 것처럼, this 키워드는 주로 인스턴스의 필드명과 지역변수를 구분하기 위한 용도로 사용됩니다.

애초에 이름이 서로 달랐다면 구분이 필요없었겠지만, 자바 프로그래밍에서 많은 경우 메서드의 지역 변수명이 필드명과 동일하게 구성되어있기 때문에 이 형식에 꼭 익숙해지기를 권장합니다.

this vs this()

this()

앞서 메서드의 호출 내용에서 같은 클래스 안에 메서드들끼리 서로 호출할 수 있었던 것처럼 생성자도 상호 호출이 가능합니다. 그리고 이를 위해 사용하는 것이 바로 this() 메서드입니다.

한마디로 this() 메서드는 자신이 속한 클래스에서 다른 생성자를 호출하는 경우에 사용합니다. 예를 들면 만약 클래스명이 Car라는 Car 클래스의 생성자를 호출하는 것은 Car()가 아니라 this()이고, 그 효과는 Car() 생성자를 호출하는 것과 동일합니다.


this() 메서드를 사용하기 위해서는 크게 두 가지의 문법요소를 충족시켜야 합니다.

  • 첫 째, this() 메서드는 반드시 생성자의 내부에서만 사용할 수 있습니다.
  • 둘 째, this() 메서드는 반드시 생성자의 첫 줄에 위치해야 합니다.
public class Test {
    public static void main(String[] args) {
        Example example = new Example();
        Example example2 = new Example(5);
    }
}

class Example  {
    public Example() {
        System.out.println("Example의 기본 생성자 호출!");
    };

    public Example(int x) {
        this();
        System.out.println("Example의 두 번째 생성자 호출!");
    }
}

//Output
Example의 기본 생성자 호출!
Example의 기본 생성자 호출!
Example의 두 번째 생성자 호출!

 

전반적인 실행 흐름을 위주로 한번 살펴봅시다. Example 클래스는 두 개의 생성자를 가지고 있습니다. 하나는 매개변수가 필요하지 않은 기본 생성자이고, 다른 하나는 int 타입의 매개변수를 받고 있는 생성자입니다.

그리고 두 번째 생성자 내부의 첫 번째 줄에 this() 메서드가 포함되어 있습니다.

 

이제 Example 클래스를 기반으로 만들어지는 인스턴스를 생성하면, 첫 번째 생성자가 호출되고 그 결과로 Example의 기본 생성자 호출! 이라는 문구가 출력됩니다.

다음으로 두 번째 생성자를 사용하여 객체를 만드는 과정에서 생성자가 호출되면 먼저 this() 메서드가 출력되어 다시 첫 번째 기본생성자가 호출되고, 그 다음으로 Example의 두 번째 생성자 호출! 이라는 문구가 출력됩니다.

내부 클래스

 

내부 클래스(Inner Class)는 클래스 내에 선언된 클래스로, 외부 클래스와 내부 클래스가 서로 연관되어 있을 때 사용합니다.

내부 클래스를 사용하면 외부 클래스의 멤버들에 쉽게 접근 할 수 있고, 코드의 복잡성을 줄일 수 있습니다.

또한 외부적으로 불필요한 데이터를 감출 수 있어 뒤에서 학습하게 될 객체지향의 중요한 핵심 원칙인 캡슐화(encapsulation)를 달성하는 데 유용합니다.

class Outer { // 외부 클래스
	
	class Inner {
		// 인스턴스 내부 클래스	
	}
	
	static class StaticInner {
		// 정적 내부 클래스
	}

	void run() {
		class LocalInner {
		// 지역 내부 클래스
		}
	}
} 
 

위의 코드 예제는 외부 클래스와 그 안에 포함될 수 있는 세 가지의 내부 클래스의 종류를 보여주고 있습니다.

사실 클래스의 선언과 객체의 생성을 동시에 수행하는 일회용 내부 클래스인 익명 내부 클래스도 있지만, 우선순위가 낮기 때문에 편의상 이번 콘텐츠에서는 생략하도록 하겠습니다.

위의 코드 예제로 다시 돌아가서, 세 가지의 내부 클래스의 종류는 각각 인스턴스 내부 클래스, 정적 내부 클래스, 그리고 지역 내부 클래스로 구분할 수 있습니다. 기본적으로 내부 클래스는 외부 클래스 내에 선언된다는 점을 제외하면 일반 클래스와 차이점이 없습니다. 단지 외부 클래스와 내부 클래스가 서로 연관되어 있을 때 사용의 편의성을 고려하여 만들어진 문법 요소입니다.

앞서 언급한 세 가지 내부 클래스는 변수가 선언 위치에 따라 인스턴스 변수, 클래스 변수, 그리고 지역 변수로 구분되는 것과 유사하게 그 위치를 중심으로 구분될 수 있고, 그 유효범위(scope)와 특성이 변수의 그것과 매우 유사하다고 할 수 있습니다.

이제 아래의 표를 통해 선언 위치에 따른 이너 클래스의 구분에 대해서 잠시 살펴보도록 합시다.

 

종 류 선언 위치 사용 가능한 변수
인스턴스 내부 클래스(instance inner class) 외부 클래스의 멤버변수 선언위치에 선언(멤버 내부 클래스) 외부 인스턴스 변수, 외부 전역 변수
정적 내부 클래스(static inner class) 외부 클래스의 멤버변수 선언위치에 선언(멤버 내부 클래스) 외부 전역 변수
지역 내부 클래스(local inner class) 외부 클래스의 메서드나 초기화블럭 안에 선언 외부 인스턴스 변수, 외부 전역 변수
익명 내부 클래스(anonymous inner class) 클래스의 선언과 객체의 생성을 동시에 하는 일회용 익명 클래스 외부 인스턴스 변수, 외부 전역 변수

멤버 내부 클래스

앞서 학습한 변수와 유사하게, 우리는 인스턴스 내부 클래스와 정적 내부클래스를 하나로 묶어 멤버 내부 클래스라 통칭합니다.

인스턴스 내부 클래스

인스턴스 내부 클래스는 객체 내부에 멤버의 형태로 존재하며, 외부 클래스의 모든 접근 지정자의 멤버에 접근할 수 있습니다. 아래의 코드 예제를 통해 한번 확인해봅시다.

class Outer { //외부 클래스
    private int num = 1; //외부 클래스 인스턴스 변수
    private static int sNum = 2; // 외부 클래스 정적 변수

    private InClass inClass; // 내부 클래스 자료형 변수 선언

    public Outer() {
        inClass = new InClass(); //외부 클래스 생성자
    }

    class InClass { //인스턴스 내부 클래스
        int inNum = 10; //내부 클래스의 인스턴스 변수

        void Test() {
            System.out.println("Outer num = " + num + "(외부 클래스의 인스턴스 변수)");
            System.out.println("Outer sNum = " + sNum + "(외부 클래스의 정적 변수)");
        }
    }

    public void testClass() {
        inClass.Test();
    }
}

public class Main {
    public static void main(String[] args) {
        Outer outer = new Outer();
        System.out.println("외부 클래스 사용하여 내부 클래스 기능 호출");
        outer.testClass(); // 내부 클래스 기능 호출
    }
}

// 출력값

외부 클래스 사용하여 내부 클래스 기능 호출
Outer num = 1(외부 클래스의 인스턴스 변수)
Outer sNum = 2(외부 클래스의 정적 변수)

위의 코드 예제를 정리해보면, 인스턴스 내부 클래스는 외부 클래스의 내부에 위치해 있으며 뒤에서 학습하게 될 private 접근 제어자(해당 클래스 안에서만 접근 가능한 멤버에 사용)를 사용하고 있음에도 내부에서 외부 클래스의 인스턴스 변수와 정적 변수에 각각 접근하여 해당 값을 사용하고 있습니다.

또 한가지 유의할 사항은 인스턴스 내부 클래스는 반드시 외부 클래스를 생성한 이후에 사용해야 한다는 점입니다. 따라서 클래스의 생성과 상관없이 사용할 수 있는 정적 변수와 정적 메서드는 인스턴스 내부 클래스에서 선언할 수 없습니다.

 

정적 내부 클래스

직전의 내용을 통해 우리는 내부 클래스가 기본적으로 외부 클래스의 존재에 의존하고 있다는 사실을 알 수 있었습니다. 만약 내부 클래스가 외부 클래스의 존재와 무관하게 정적 변수를 사용할 수 있게 하려면 어떻게 해야할까요?

이 경우에 사용할 수 있는 것이 바로 정적 내부 클래스입니다. 정적 내부 클래스는 인스턴스 내부 클래스와 동일하게 클래스의 멤버 변수 위치에 정의하지만, static 키워드를 사용한다는 점에서 차이가 있다고 할 수 있습니다. 뭔가 좀 익숙하게 들리지 않나요?

class Outer { // 외부 클래스
    private int num = 3; // 외부 클래스의 인스턴스 변수
    private static int sNum = 4;

    void getPrint() {
        System.out.println("인스턴스 메서드");
    }

    static void getPrintStatic() {
        System.out.println("스태틱 메서드");
    }

    static class StaticInClass { // 정적 내부 클래스
        void test() {
            System.out.println("Outer sNum = " +sNum + "(외부 클래스의 정적 변수)");
            getPrintStatic();
            // num 과 getPrint() 는 정적 멤버가 아니라 사용 불가.
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Outer.StaticInClass a = new Outer.StaticInClass(); //정적 이너 클래스의 객체 생성
        a.test();
    }
}

//출력값
Outer sNum = 4(외부 클래스의 정적 변수)
스태틱 메서드

 

지역 내부 클래스

 

지역 내부 클래스는 클래스의 멤버가 아닌 메서드 내에서 정의되는 클래스입니다.

지역 내부 클래스도 지역 변수와 유사하게 메서드 내부에서만 사용가능하기 때문에 일반적으로 메서드 안에서 선언 후에 바로 객체를 생성해서 사용합니다.

아래 코드를 통해 좀 더 이해해보도록 합시다. 마찬가지로 직접 입력하면서 코드의 흐름을 파악할 수 있도록 시도해보세요.

class Outer { //외부 클래스
    int num = 5;
    void test() {
        int num2 = 6;
        class LocalInClass { //지역 내부 클래스
            void getPrint() {
                System.out.println(num);
                System.out.println(num2);
            }
        }
        LocalInClass localInClass = new LocalInClass();
        localInClass.getPrint();
    }
}
public class Main {
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.test();
    }
}

//출력값
5
6


위의 코드 예제를 보면 지역 내부 클래스 LocalInClass가 메서드 안에서 선언되고 생성된 후에 정의된 메서드를 호출하여 외부 클래스의 변수들을 출력하고 있는 것을 확인할 수 있습니다.

앞서 언급한 것처럼, 내부 클래스는 기본적으로 개발자의 편의를 위해 서로 연관있는 클래스들을 연결시켜 준 것에 지나지 않습니다.