본문 바로가기
Language/Java

[Java] 자바가 확장한 객체 지향 (1)

by jaee_ 2021. 9. 27.
본 글은 📚 스프링 입문을 위한 자바 객체지향의 원리와 이해 를 읽고 정리한 내용입니다.

 

1. abstract 키워드 - 주상 메소드와 추상 클래스

추상메소드(Abstract Method)란 간단하게 설명하면 선언부는 있는데 구현부가 없는 메소드를 말한다. 추상 메소드를 하나라도 갖고있는 클래스는 추상클래스로 정의되어야 하며 추상메소드 없이 추상 클래스를 선언할 순 있다. 몸체가 없이 선언만 하는 것이 의미가 있을까? 왜 필요할까? 아래의 코드를 보며 이해해보자.

 

public abstract class 동물 {

    abstract void 울어보세요();
}

class 고양이 extends 동물 {

    @Override
    void 울어보세요() {
        System.out.println("나는 고양이 야옹~ 야옹~");
    }
}

class 쥐 extends 동물 {

    @Override
    void 울어보세요() {
        System.out.println("나는 쥐 찍! 찍!");
    }
}

class 병아리 extends 동물 {

    @Override
    void 울어보세요() {
        System.out.println("나는 병아리 삐약! 삐약!");
    }
}

class 강아지 extends 동물 {

    @Override
    void 울어보세요() {
        System.out.println("나는 강아지 멍! 멍!");
    }
}

class 송아지 extends 동물 {

    @Override
    void 울어보세요() {
        System.out.println("나는 송아지 음메~ 음메~");
    }
}
public class Driver {

    private static Object 동물;

    public static void main(String[] args) {
        동물[] 동물들 = new 동물[5];

        동물들[0] = new 쥐();
        동물들[1] = new 고양이();
        동물들[2] = new 강아지();
        동물들[3] = new 송아지();
        동물들[4] = new 병아리();

        for (int i = 0; i < 동물들.length; i++) {
            동물들[i].울어보세요();
        }

        // 동물 짐승 = new 동물(); // 에러 발생
    }
}

// 결과
나는 쥐 찍! 찍!
나는 고양이 야옹~ 야옹~
나는 강아지 멍! 멍!
나는 송아지 음메~ 음메~
나는 병아리 삐약! 삐약!

위의 예제에서 동물클래스는 추상클래스로 선언되어 있다. 그리고 추상메소드로 울어보세요()가 선언되어 있다. 하위클래스가 동물타입의 참조 변수를 통해 하위 클래스의 인스턴스가 가진 울어보세요() 메소드를 호출하고 있으니 상위 클래스인 동물울어보세요()메소드는 반드시 존재해야 한다. 하지만, 만약 추상메소드가 아닌 형태로 선언되어 있으면 동물 클래스의 울어보세요() 메소드에는 뭐라고 작성해줄까? 동물은 우는 소리가 무엇인지 흉내낼 수 없으므로 애매한 상황에 빠지게 된다. 이와 같은 경우 추상메소드를 사용하게 된다. 추상메소드는 메소드 선언은 있으되 몸체는 없는 형태로 구성이 되어있다.

위의 코드에서 주석 처리 되어있는 동물 짐승 = new 동물(); 부분을 보자. 주석을 해제하면 아래의 사진과 같은 오류메세지를 볼 수 있다.

동물 짐승 = new 동물(); 오류 메세지

"동물 타입은 인스턴스를 만들 수 없다"라고 오류메세지가 뜨는 것을 볼 수 있다. 추상 클래스는 인스턴스, 즉 객체를 만들 수 없는 클래스가 된다.

또한, 추상메소드로 선언된 메소드를 하위 클래스에서 재정의(오버라이딩) 하지 않으면 컴파일 시점에 에러가 발생한다. 만약 고양이 클래스에서 울어보세요() 메소드를 주석처리하면 아래와 같은 오류 메세지를 볼 수 있다.

정리해보면

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

2. 생성자

클래스의 인스턴스, 즉 객체를 만들 때마다 new 키워드를 사용한다.

동물 뽀로로 = new 동물();

 

new 클래스명()을 자세히 보면 열고 닫는 소괄호를 볼 수 있다. 이전에 사용했던 열고 닫는 소괄호는 메소드를 의미하는 것이었다. 클래스명에 소괄호는 무엇을 뜻하는 것일까? 바로 객체 생성자 메소드를 의미한다. 반환값이 없고 클래스명과 같은 이름을 가진 메소드를 객체를 생성하는 메소드라고 해서 갹채 생성자 메소드라 한다. 줄여서 생성자라고 불리기도 한다. 자바에서는 알아서 인자가 없는 기본 생성자를 자동으로 만들어주기 때문에 따로 만들어주지 않은 경우가 더 많을 것이다. 아래의 예제를 보며 살펴보자.

 

// 새로 만든 동물 클래스
public class 동물 {

}

// 실행 클래스
public class Driver {

    public static void main(String[] args) {
        동물 뽀로로 = new 동물();
    }
}

아무것도 없는 동물 클래스와 동물 인스턴스를 만드는 것을 실험한 예제이다. 동물 클래스에는 아무런 메소드도 없는 것처럼 보이지만 Driver클래스에서 new 동물() 이라는 메소드를 사용할 수 있다. 아무런 인자도 갖고있지 않은 기본 생성자 메소드가 존재하는 것이다. 자바 컴파일러가 알아서 아래의 자바 코드처럼 만들어준다.

public class 동물 {
    public 동물(){} // 컴파일 시 컴파일러가 알아서 추가해 줌
}

필요하다면 인자를 갖는 생성자를 더 만들 수도 있다.

public class 동물 {
    public 동물(){}

    public 동물(String name){
        System.out.println(name);
    }
}


// 실행 클래스
public class Driver01 {

    public static void main(String[] args) {
        동물 뽀로로 = new 동물();
        동물 앵무새 = new 동물("앵무새");
    }
}

// 결과
앵무새

하지만 주의해야할 것이 있다. 개발자가 아무런 코드를 추가하지 않으면 자바는 인자가 없는 기본 생성자를 자동으로 만들어주지만, 인자가 있는 생성자를 하나라도 만든다면 자바는 기본 생성자를 자동으로 만들어주지 않는다. 그러니 만약 인자가 있는 생성자를 만든 후 동물 뽀로로 = new 동물(); 처럼 인자가 없는 객체를 생성하고 싶다면 인자가 없는 생성자도 직접 만들어줘야 한다.

생성자는 개발자가 필요한만큼 오버로딩해서 만들 수 있다. 그리고 생성자로 줄여서 부르지만 정확하게 표현하자면 객체 생성자 메소드임을 잊지말자.

생성자


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

객체 생성자가 있다면 클래스 생성자도 있을 거라고 기대해 볼 만 하다. 자바는 그 기대의 절반만 부응해 준다. 클래스 생성자는 따로 존재하지 않는다. 그러나 클래스가 스태틱 영역에 배치될 때 실행되는 코드 블록이 있다. 바로 static 블록이다. 예제로 살펴보자.

// 사람 클래스
public class 사람 {
    static{
        System.out.println("사람 클래스 static block");
    }
}


// 실행 클래스
public class Driver {

    public static void main(String[] args) {
        사람 홍길동 = new 사람();
    }
}


//결과
사람 클래스 static block

실행 한 결과 사람 클래스에 static 블록안에 작성한 메세지가 찍히는 것을 볼 수 있다. 그리고 static 블록에서 사용할 수 있는 속성과 메소드는 당연히 static 멤버 뿐이다. 왜 그런지 궁금하다면 T 메모리가 생성되는 것을 그려보면 알 수 있다. 객체 멤버는 클래스가 static영역에 자리 잡을 후에 객체 생성자를 통해 힙에 생성이 된다. 클래스의 static 블록이 실행되고 있을 때는 해당 클래스의 객체는 하나도 존재하지 않기 때문에 static 블록에서는 객체 멤버에 접근할 수 없는 것이다.

여기서 궁금한 점이 그럼 실행클래스인 Driver클레스main 메소드안에 출력 구문보다 늦게 사람 클래스를 객체를 생성해도 main 메소드 출력 구문보다 빨리 출력될까? 코드로 확인해보자.

public class Driver {

    public static void main(String[] args) {
        System.out.println("main() 메소드 실행");
        사람 홍길동 = new 사람();
    }
}

// 결과
main() 메소드 실행
사람 클래스 static block

결과를 보면 main 메소드내에 출력구문이 먼저 실행되는 것을 알 수 있다. 다음 코드를 보자.

public class Driver {

    public static void main(String[] args) {
        System.out.println("main() 메소드 실행");
        사람 홍길동 = new 사람();
        사람 청길동 = new 사람();
    }
}

// 결과
main() 메소드 실행
사람 클래스 static block

사람 클래스의 인스턴스를 여러 개 만들어도 사람 클래스의 static 블록은 단 한 번만 실행되는 것을 알 수 있다.

이번에는 클래스의 인스턴스를 만드는 작업이 아닌 정적 속성에 접근하는 코드를 만들어보자.

// 사람2 클래스
public class 사람2 {
    static int age = 27;

    static{
        System.out.println("사람 클래스 static block");
    }
}

// 실행 클래스
public class Driver2 {

    public static void main(String[] args) {
        System.out.println("main() 메소드 실행");
        System.out.println(사람.age);
    }
}

// 결과
main() 메소드 실행
사람 클래스 static block
27

결과를 보면 static 블록이 실행되는 것을 볼 수 있다. 정리하자면 래스 정보는 해당 클래스가 맨 처음 사용될 때 T메모리에 스태틱 영역에 로딩되며 한 번만 클래스의 static 블록이 실행된다. 여기서 클래스가 제일 처음 사용될 때는 다음 세가지 경우 중 하나다.

  • 클래스의 정적 속성을 사용할 때
  • 클래스의 정적 메소드를 사용할 때
  • 클래스의 인스턴스를 최초로 만들 때

static도 메모리이기 때문에 최대한 늦게 사용하고 최대한 빨리 반환하는 것이 좋다. 그렇기 때문에 프로그램 실행 시 바로 스태틱 영역에 로딩되는 것이 아니라 클래스가 처음 사용될 때 스태틱 영역에 로딩되는 것이다.

 


4. final 키워드

final은 마지막, 최종이라는 사전적인 의미를 가진다. final키워드를 사용할 수 있는 곳은 딱 세 군데다. 바로 클래스, 변수, 메소드이다. (사실 객체 지향 언어의 구성요소는 이 세가지 뿐이다.)

 

4.1 final과 클래스

클래스에 final이 붙으면 어떤 의미일까? 상속을 허락하지 않겠다는 뜻이다. 따라서 아래의 아래의 코드를 실행할 수 없다.

// final이 붙은 고양이 클래스
public final class 고양이 { }

// 위의 고양이 클래스를 상속받고 싶은 길고양이 클래스
class 길고양이 extends 고양이{ }

코드를 작성하면 아래와 같은 오류메세지가 뜨는 것을 볼 수 있다. 

 

4.2 final과 변수

변수에 final이 붙으면 어떤 의미일까? 바로 변경 불가능한 상수가 된다. 아래의 코드를 보자.

public class finalVariable {

    final static int 정적상수1 = 1;
    final static int 정적상수2;

    final int 객체상수1 = 1;
    final int 객체상수2;

    static {
        정적상수2 = 2;

        // 상수는 한 번 초기화되면 값을 변경할 수 없다.
        // 정적상수2 = 3;
    }

    finalVariable() {
        객체상수2 = 2;

        // 상수는 한 번 초기화되면 값을 변경할 수 없다.
        // 객체상수2 = 3;

        final int 지역상수1 = 1;
        final int 지역상수2;

        지역상수2 = 2;

    }

}

정적 상수선언 시에, 또는 정적 생성자에 해당하는 static 블록 내부에서 초기화가 가능하다.

객체 상수 역시 선언 시에, 또는 객체 생성자 또는 인스턴스 블록에서 초기화할 수 있다.

지역 상수 역시 선언 시에, 또는 최초 한번만 초기화가 가능하다. 

 

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

 

4.2 final과 메소드

메소드가 final이라면 최종이니 재정의, 즉 오버라이딩을 금지하게 된다. 따라서 아래의 코드 작성이 불가능해진다. 

public class 동물 {

    final void 숨쉬다() {
        System.out.println("숨쉬는 중");
    }
}

class 포유류 extends 동물{
    // 에러 발생 : cannot override '숨쉬다()' in 'study.week08.finalPackage.동물'; overridden method is final
    void 숨쉬다() {
        System.out.println("숨쉬는 중");
    }
}

글이 길어질 것 같아 나머지 내용은 자바가 확장한 객체 지향 (2) 에서 계속 정리하도록 하겠다. 

 


참고

- 스프링 입문을 위한 자바 객체 지향의 원리와 이해

댓글