Java가 확장한 객체지향 - (1)

이 글에서는 자바가 객체 지향을 확장하기 위해 어떠한 방법을 사용했는지 키워드 중심으로 살펴볼 것이다.

이 포스팅은 스프링 입문을 위한 자바 객체 지향의 원리와 이해(저자: 김종민)를 공부하면서 작성한 글이다.


1. abstract 키워드 (추상 메서드와 추상 클래스)


추상 메서드(Abstract method) : 선언부는 있는데 구현부가 없는 메서드를 일컫는다.

추가적으로 추상 메서드(Abstract method)를 하나라도 갖고 있는 클래스는 반드시 추상 클래스(Abstract Class)로 선언해야한다. ( 추상 메서드 없이도 추상 클래스를 선언할 수 있다는 것은 알아두자!! )

추상 클래스와 추상 메서드의 Example

public abstract class Animal {
   abstract void cry();
}

public class Cat extends Animal {
   void cry() {
      System.out.println("나는 고양이! 야옹! 야옹!")
   }
}

public class Driver {
   public static void main(String[] args) {
      Animal tom = new Cat();
      tom.cry();
      
      Anumal savage = new Anumal(); 	// 컴파일 에러 (인스턴스 생성 불가)
   }
}

이러한 추상클래스와 추상 메서드는 다음과 같은 4가지의 문제를 해결해 준다.

  1. 슈퍼 클래스 객체가 생성되는 것을 막아준다.
  2. 서브 클래스에서 오버라이딩 하는 슈퍼 클래스의 메소드를 정의하기 곤란했는데 추상 메소드로써 정의하면 보다 쉽게 정의할 수 있다. ( ex. cat 클래스의 cry() 메소드는 System.out.println(“야옹!!”) 으로 정의할 수 있지만 cat 클래스가 오버라이딩하는 animal 클래스의 cry() 메소드는 정의하기 참 곤란하다. => abstract 메소드를 이용하자)
  3. 슈퍼 클래스의 참조 변수 배열로 서브 클래스 객체들의 메소드를 호출하려면 슈퍼 클래스에서 오버라이딩 되어지는 메서드를 정의해야 하는데 이를 추상메서드로써 쉽게 가능하게 해준다.
  4. 서브 클래스가 슈퍼 클래스의 추상 메서드를 오버라이딩하지 않으면 컴파일 시점에 에러가 발생하기 때문에 런타임 시에 에러를 발생하는 경우보다 효율이 매우 증가한다!!

추상 클래스 및 추상 메서드의 특징

  • 추상 클래스는 인스턴스, 즉 객체를 만들 수 없다. 즉, new를 사용할 수 없다
  • 추상 메서드는 하위 클래스에서 메서드 구현을 강제한다. 오버라이딩 강제.
  • 추상 메서드를 포함하는 클래스는 반드시 추상 클래스여야 한다,

2. 생성자 (new)


생성자(Constructor) : 반환값이 없고 클래스명과 같은 이름을 가진 메서드이다. 객체 생성자 메서드라고도 한다.

  • 생성자가 존재하지 않으면 자바 컴파일러가 알아서 기본 생성자를 자동으로 만들어준다.

  • 필요하다면 개발자가 인자를 갖는 생성자를 만들어 줄 수도 있다. (이러한 경우에는 기본 생성자는 만들어지지 않음)

  • 생성자 예제

public class 동물 {
   public 동물() {} 	// 기본 생성자 (생성자가 없다면 컴파일러가 기본적으로 만들어준다)
   public 동물(Strng name) { ... } // 하나의 인자가있는 생성자
}

3. 클래스 생성 시의 실행 블록, static 블록


객체 생성자가 있다면 클래스 생성자도 있을 거라고 기대해 볼 만하다. 하지만 자바는 그 기대의 절반만 부응해 준다. 절반만 부응한다는 말은 도대체 어떤 말일까?? 아래의 내용에서 다루어보자!!

클래스 생성자는 존재하지 않는다. 하지만 클래스가 스태틱 영역에 배치될 때 실행되는 코드블록이 있다. 바로 static 블록이다. 예제는 다음과 같다.

package staticBlock;

public class 동물 {
   static {
      System.out.println("동물 클래스 레디 온!");
   }
}
package staticBlock;

public class Driver02 {
   public static void main(String[] args){
      동물 뽀로로 = new 동물();
   }
}
// 실행 결과
동물 클래스 레디 !
package staticBlock;

public class Driver02 {
   public static void main(String[] args){
      System.out.println("main 메서드 시작!");
   }
}
// 실행 결과
main 메서드 시작!

두 main 메소드의 실행 결과에 대한 이유를 알 수 있겠는가? 그렇다. 자바는 사용하지 않는 클래스는 Static 영역에 배치조차 하지 않는다. 해당 패키지 또는 클래스가 처음으로 사용될때 Static 영역에 로딩되기 때문이다.

이러한 말을 증명시켜줄 예제 코드를 살펴보자.

package staticBlock;

public class Driver03 {
   public static void main(String[] args){
      System.out.println("main 메서드 시작!");
      동물 뽀로로 = new 동물();
   }
}
// 실행 결과
main 메서드 시작!
동물 클래스 레디 !

동물 클래스의 static 블록보다 main() 메서드의 실행문이 먼저 실행된 것을 볼 수 있다.(해당 클래스가 처음으로 사용 되어야만 Static 영역에 로딩된다.)

Static 블록에서 사용할 수 있는 속성과 메서드는 당연히 static 멤버 뿐이다. (인스턴스 멤버에는 접근할 방법이 없다.)

이번에는 클래스의 인스턴스를 만드는 작업이 아닌 클래스의 정적 속성에 접근하는 경우에 대해서 예제를 통해 알아보자.

package staticBlock;

public class Driver05 {
   public static void main(String[] args) {
      System.out.println("main 메서드 시작!");
      System.out.println(Animal.age);
   }
}

class Animal {
   static int age = 0;
   
   static {
      System.out.println("Animal class ready on!");
   }
}
/* 실행 결과
main 메서드 시작!
Animal class ready on!
0
*/

위 예제를 통해서 클래스 생성시의 실행 블록과 static 블록에 대해서 정리해 보면.

  • 클래스 정보는 해당 클래스가 코드에서 맨 처음 사용될 때 메모리의 Static 영역에 로딩된다. 이때 단 한번 해당 클래스의 Static 블록이 실행된다.
  • 클래스가 맨 처음 사용될 때의 경우는 다음과 같다.
    1. 클래스의 정적 속성을 사용할 때
    2. 클래스의 정적 메서드를 사용할 때
    3. 클래스의 인스턴스를 최초로 생성할 때

그렇다면 왜 프로그램이 실행될 때 바로 클래스들의 정보를 메모리의 Static 영역에 로딩하지 않고 해당 클래스가 처음 사용될 때 로딩할까?

이러한 이유는 스태틱 영역도 메모리이기 때문이다. 메모리는 최대한 늦게 사용을 시작하고 최대한 빨리 반환하는 것이 정석이다. 물론 자바는 클래스가 Static 영역에 한번 올라가면 프로그램이 종료되기 전까지는 해당 메모리를 반환할 수 없지만 그럼에도 최대한 늦게 로딩함으로써 메모리 사용을 최대한 늦추기 위해서이다.

static 블록은 거의 사용할 일이 없지만 그래도 알아두면 한번은 요긴하게 써먹을 수 있다.

사실 static 블록과 유사하게 클래스의 인스턴스를 위한 인스턴스 블록도 존재한다. 아무런 표시없이 {} 블록을 사용하게 되면 인스턴스가 생성될 때마다 {} 블록이 실행된다. {} 블록은 객체 생성자가 실행되기 전에 먼저 실행된다.

객체는 주로 생성자를 통해 초기화하기 때문에 사실 {} 블록을 거의 사용할 일은 없지만 종종 사용한다.


4. final 키워드


final은 마지막, 최종이라는 의미를 가진 단어다. final 키워드가 나타날 수 있는 곳은 객체 지향 언어의 세가지 구성 요소인 클래스, 변수, 메소드이다.

fianl과 클래스

클래스에 final이 붙었다면 어떤 의미가 될까? 예제를 통해 알아보자.

package finalClass;

public final class 고양이 { }
package finalClass;

// 컴파일 에러 : the type 길고양이 cannot subclass the final class 고양이
public class 길고양이 extends 고양이 {}

상속을 허락하지 않겠다는 의미이다. 즉, 다음과 같은 서브 클래스를 만들면 컴파일 에러가 발생한다.

final과 변수

변수에 final이 붙었다면 그 의미는 뭘까? 예제를 통해 알아보자.

package finalVariable;

public class Cat {
   final static int STATIC_CONSTANT_1 = 1;
   final static int STATIC_CONSTANT_2;

   final int INSTANCE_CONSTANT_1 = 1;
   final int INSTANCE_CONSTANT_2;

   static {
      STATIC_CONSTANT_2 = 2;
      
      //STATIC_CONSTANT_2 = 4; 컴파일 에러 (상수(final)는 한 번 초기화되면 값을 변경할 수 없다.)
   }

   Cat() {
      INSTANCE_CONSTANT_2 = 2;
      
      //INSTANCE_CONSTANT_2 = 4; 컴파일 에러 (상수(final)는 한 번 초기화되면 값을 변경할 수 없다.)

      final int LOCAL_CONSTANT_1 = 1;
      final int LOCAL_CONSTANT_2;

      LOCAL_CONSTANT_2 = 2;
   }
}

위의 코드에서 볼 수 있는 것 처럼 변수에 final이 붙으면 변경 불가능한 상수가 된다. 정적 상수는 선언 시에, 또는 정적 생성자에 해당하는 static 블록 내부에서 초기화가 가능하다. 객체 상수 역시 선언 시에, 또는 객체 생성자 또는 인스턴스 블록에서 초기화할 수 있다. 지역 상수 역시 선언 시에, 또는 최초 한 번만 초기화가 가능하다.

다른 언어에서는 읽기 전용인 상수에 대해서 final 키워드 대신 const 키워드를 사용하기도 하는데 자바에서는 이런 혼동을 피하기 위해 const를 키워드로 등록해두고 쓰지 못하게 하고 있다.

final과 메서드

메서드가 final이면 재정의, 즉 오버라이딩을 금지하게 된다.

다음의 예제를 통해서 살펴보자.

package finalMethod;

public class 동물 {
   final void 숨쉬다() {
      System.out.println("호흡 중");
   }
}

class 포유류 extends ehdanf {
   
   // 컴파일 에러 : Cannot override the final method from 동물
   void 숨쉬다() {
      System.out.println("호흡 중");
   }
}