주요글: 도커 시작하기
반응형
메소드로 이해하기 쉬운 생성자에 대하여 상세하게 살펴본다.

생성자의 기능과 형태

클래스는 초기화를 위하여 생성자(constructor)라는 특별한 코드 블록을 가질 수 있다. 즉, 생성자는 클래스가 new 표현식에 의해 인스턴스화되어 객체를 생성할 때 객체의 레퍼런스를 생성하기 전에 객체의 초기화를 위해 사용되는 코드의 블록이다.

따라서, 생성자는 자바 클래스의 멤버가 아니며, 멤버가 아니므로 상속되지 않는다. 따라서, 오버라이딩의 대상이 될 수도 없다. 또한, 일반적인 메소드 호출방법으로 호출할 수 없다.

일반적인 생성자의 형태는 다음과 같다.

  public class Example {
     public Example() {
        ...
     }
  }

이러한 생성자들은 다음과 같은 new 표현식을 통하여 객체의 생성과 함께 실행된다.

  Example ex = new Example();

생성자의 특징

생성자는 접근 제한 수식어인 public, protected, private 만을 쓸 수 있으며, abstract, final, native, static, strictfp, synchronized 등의 수식어를 사용할 수 없다. 접근 제한이 생략된 경우 클래스와 마찬가지로 package 접근 제한이 설정된다. 생성자는 접근제어를 통해 클래스의 인스턴스화를 통한 객체 생성을 조절할 수 있지만, 이러한 접근 제어는 생성자가 상속되지 않으므로 상속관계에서는 아무런 의미가 없다. 또한 상속되지 않으므로 abstract 또는 final 수식어를 가질 수 없다. 생성자는 객체의 생성시에만 호출되므로 static일 수 없다. 생성시에 락을 발생하는 것은 의미없으므로 synchronized 될 필요가 없다. native 생성자는 자바가상머신이 생성자를 통한 초기화에 부모 클래스의 초기화와 같은 보이지 않는 코드들을 조절할 수 없기 때문에 허용되지 않는다.

또한 메소드가 아니므로 반환값은 void 조차 허용되지 않으며, 아예 반환값이란 있을 수 없다. 또한, 생성자의 이름은 메소드와는 달리 반드시 클래스 이름하고 같아야 한다.

생성자의 파라미터 리스트는 일반 메소드의 파라미터와 동일한 방법이 적용된다. 없거나 한 개 이상의 파라미터를 가질 수 있으며, 각 파라미터는 타입과 이름을 가진다. 생성자의 시그너쳐는 파라미터 리스트에 의해 정해지며, 같은 시그너쳐(즉, 동일한 파라미터 순서)를 가진 생성자는 선언될 수 없으며, 컴파일 시에 에러가 발생한다.

클래스는 마치 메소드의 오버로딩처럼 시그너쳐가 다른 여러 개의 생성자를 가질 수 있다. 실제로 메소드의 오버로딩과 동일한 개념으로 구현되며, 컴파일 시에 어떤 생성자가 실행될지 결정된다.

또한, 생성자는 throws 리스트를 통해 발생될 예외 리스트를 가질 수 있다. 이것은 일반 메소드와 동일한 개념과 방법으로 사용된다. new 표현식으로 객체를 생성할 때 생성자가 호출되므로, 그때 throws 리스트의 예외가 발생할 수 있게 된다.

super와 this

생성자는 클래스의 생성시에 단 한번만 호출되며, 객체의 초기화를 담당하게 된다. 일반 메소드와는 달리 필요할 때마다 호출될 수 없다. 이러한 생성자는 new 표현식으로 객체를 생성할 때 주어진 파라미터에 따라 일치하는 생성자가 실행된다.

생성자가 여러 개일 때, 다른 생성자를 호출할 수가 있다. 이때 사용되는 특별한 코드가 this 이다. 객체가 자기 자신을 참조할 때 사용하는 this는 생성자를 호출하기 위한 특별한 형태인 this()를 통하여 클래스 내의 다른 생성자를 호출할 수 있게 된다.

그러나, this()를 통한 생성자의 호출은 오직 생성자에서만 가능하고, 생성자의 맨 첫 줄에서 단 한번만 호출이 가능하다. this()는 파라미터가 없는 생성자를 호출하는 형태이고, int 형의 파라미터를 두개 가지고 있는 생성자를 호출하려면 this(12,10)처럼 적합한 파라미터 리스트로 호출한다.

동일한 개념과 방법으로 부모 클래스의 생성자를 호출하는 방법은 super() 이다. super는 상속에서 부모 객체를 참조할 수 있도록 해준다. 그러나 super의 특수한 형태인 super()는 부모 클래스의 생성자를 호출할 수 있도록 해준다. 단, this()와 마찬가지로 생성자는 메소드가 아니므로, super() 역시 생성자 내에서만 호출 가능하며, 생성자의 맨 첫 줄에서 단 한번만 사용가능하다. super(10,20) 처럼 생성자의 시그너쳐에 따라 파라미터를 적용할 수 있다.

컴파일러에 의해 자동으로 추가되는 코드

생성자는 객체의 생성과 관련하여 초기화를 위한 부분을 담당한다. 따라서, 객체 초기화에 관련하여, 생략될 경우 자동으로 추가되는 코드가 있을 수 있다.

첫번째는, 클래스에 생성자가 없을 경우, 기본 생성자가 자동으로 추가된다. 클래스에 생성자가 하나라도 있을 경우 자동으로 추가되는 생성자는 없다. 기본 생성자는 다음과 같은 형태를 지닌다.

  public class Example {
     Example() {
        super();
     }
  }

기본 생성자는 파라미터가 없고, 생성자의 내용은 부모 클래스의 생성자를 호출하는 super() 코드만 포함되어 있다. 또한, 예외 발생을 위한 throws 리스트가 없다. 기본 생성자의 접근 제한은 클래스의 접근 제한을 따르게 된다. 위의 예에서 기본 생성자는 public의 접근 제한을 가진다.

두번째는, 생성자의 첫 줄에 this(…) 또는 super(…) 라는 코드가 없을 경우, super() 라는 코드가 부모 클래스의 생성자를 호출하도록 자동으로 추가된다. 자동으로 추가되는 코드는 파라미터가 없는 super() 형태의 코드이므로 부모 클래스의 생성자 중에 파라미터가 없는 생성자를 호출한다.

모든 생성자는 오직 맨 첫 줄에 this(…) 또는 super(…) 코드가 반드시 들어가게 되며, 명시적으로 this(…)를 통해 클래스 내의 다른 생성자를 호출하거나 super(…)를 통해 부모 클래스의 생성자를 호출하게 된다. 명시적으로 this(…) 또는 super(…) 코드가 생략되면 파라미터가 없는 super() 코드가 컴파일 시에 자동으로 추가된다.

상속에 관하여

클래스의 상속은 메소드와 변수, 상수, inner class, inner interface 등을 포함한 모든 클래스 멤버를 상속하지만 멤버가 아닌 생성자는 상속되지 않는다. 상속되지 않으므로 생성자는 오버라이딩의 개념도 존재하지 않는다.

생성자는 메소드가 아니라, 클래스가 객체로 인스턴스화 될 때 초기화를 담당하기 위한 코드 블록이다. 따라서 비록 상속관계에서도 부모 클래스와 자식 클래스의 초기화는 상속될 수 없고 각 클래스의 고유한 부분으로 남을 수 밖에 없다.

다음과 같은 경우를 생각해 보자.

  public class A {
     public A(int x) {
     }
  }

클래스 A는 아무런 문제없이 컴파일될 것이다. 그럼 클래스 A를 상속하는 클래스 B를 생각해보자.

  public class B extends A {
  }

물론, 이 클래스도 아무런 멤버도 포함하지 않은 정상적인 클래스이다. 그러나 이 클래스는 컴파일 시에 에러가 발생한다. 에러의 위치는 생성자 호출에서 발생한다. 그러면, 다음과 같이 고쳐본다.

  public class B extends A {
     public B() {
     }
  }

역시 컴파일 시에 같은 에러가 발생한다. 이유는 간단한다. 클래스 B의 생성자를 생략한 경우에는 파라미터가 없는 생성자를 추가하므로 위의 두개의 클래스 B는 동일하다. 정확히는 생성자에 super() 코드가 자동으로 추가되므로 실제로는 다음과 같은 코드가 된다.

  public class B extends A {
     public B() {
        super();
     }
  }

여기에서 문제가 발생한다. 클래스 A는 생성자가 한 개 존재하므로 자동으로 파라미터가 없는 생성자가 추가되지 않는다. 그러나 클래스 B의 super() 코드는 부모인 클래스 A의 생성자 중에서 파라미터가 없는 생성자를 호출하는 코드이다. 클래스 A에 그런 생성자가 없으므로 컴파일 시에 에러가 발생하게 된다.

이런 경우 클래스 A에 파라미터가 없는 생성자를 추가하거나, 파라미터가 없는 super() 코드가 자동으로 추가되지 않도록 클래스 B의 생성자에 명시적으로 super(1) 과 같은 식으로 int 타입의 파라미터가 한 개 존재하는 생성자를 호출하도록 코드를 추가한다.

  public class B extends A {
     public B() {
        super(1);
     }
  }

위의 예제는 컴파일 시에 아무런 문제도 발생하지 않는다. 다만, 부모 클래스인 클래스 A의 생성자가 받아들이는 파라미터의 값을 위의 예에서는 1을 무조건 전달하지만, 실제로는 적합한 값이 전달되도록 해야 한다.

생성자의 호출 순서

생성자의 호출 순서는 클래스의 상속과 관련하여 중요한 의미를 가진다. 만약 클래스 B가 클래스 A를 상속하고 클래스 A는 Object 클래스를 상속한다면 new B() 코드로 실행되는 생성자의 호출 순서는 다음과 같다.

  1. 클래스 B의 생성자 호출
  2. 클래스 B 생성자 첫줄에서 super() 코드를 실행
  3. 클래스 A의 생성자 호출
  4. 클래스 A 생성자 첫줄에서 super() 코드를 실행
  5. 클래스 Ojbect의 생성자 호출
  6. 클래스 A 생성자의 두번째 줄 이후 부분을 실행
  7. 클래스 B 생성자의 두번째 줄 이후 부분을 실행
생성자 호출의 순서는 B->A->Object 이지만, 실제 생성자 내부 코드들은 Object->A->B의 순서로 실행된다. 그것은 생성자의 맨 첫 줄의 코드가 super() 이기 때문이다.

Object 클래스는 유일하게 생성자 첫 줄에 super() 코드가 포함되어 있지 않은 클래스이다. Object 클래스는 상속 트리에서 언제나 유일한 최상위 클래스이기 때문이다.

결국 모든 클래스는 인스턴스화되어 객체로 생성될 때, 상속 트리상에 존재하는 모든 클래스의 생성자를 호출하게 되며, Object 클래스의 생성자까지 거슬러 올라가게 된다. 이것은 상속 트리 상에 존재하는 모든 객체의 초기화를 수행하기 위해서다.

결론

생성자는 자바 클래스의 멤버가 아니므로 상속과 같은 멤버가 가지는 특성을 가지고 있지 않다. 생성자는 클래스의 인스턴스인 객체를 생성할 때 초기화를 담당하는 코드 블록이며, 클래스의 상속관계에서 초기화를 수행할 수 있도록 부모 클래스의 생성자를 호출하는 특징이 있다.

그런 특징에 따라, 생성자의 특수성과 일반 메소드 멤버와의 차이점을 알아보고, 생성자의 호출 순서와 super() 및 this(), 그리고, 컴파일 시에 자동으로 추가되는 코드에 대하여 알아보았다.

관련링크:

본 글의 저작권은 이동훈에 있으며 저작권자의 허락없이 온라인/오프라인으로 본 글을 유보/복사하는 것을 금합니다.

+ Recent posts