[JAVA] 자바 객체의 타입 변환

    728x90

    안녕하세요

    로로봉 입니다 : )

    오늘은 자바에서 객체의 타입 변환에 대해 알아보겠습니다.

    기본 자료형을 사용할 때 다른 프로그램 언어와 마찬가지로 자바 프로그램은 등호(=)를 중심으로 항상 왼쪽과 오른쪽의 자료형이 일치해야 합니다.

    만약 자료형이 서로 다를 경우에는 컴파일러가 자동으로 타입을 변환해주거나 개발자가 직접 타입을 변환해 주어야 합니다.

    객체에서도 이러한 타입 변환이 일어납니다. 이것을 업캐스팅, 다운캐스팅이라고 합니다.


    1) 객체의 업캐스팅과 다운캐스팅

    기본 자료형에서 업캐스팅은 범위가 좁은 쪽에서 넓은 쪽으로 캐스팅하는 것을 말하고, 다운 캐스팅은 그 반대를 말합니다.

    객체에서는 자식 클래스에서 부모 클래스 쪽으로 변환되는 것이 업캐스팅이고, 그 반대가 다운 캐스팅입니다.

    객체는 항상 업캐스팅할 수 있으므로 명시적으로 적어주지 않아도 컴파일러가 대신 넣어주게 되어있습니다.

    하지만 다운 캐스팅은 개발자가 직접 명시적으로 넣어주어야 합니다.

    객체의 다운캐스팅에서 한가지 주의해야 할 점은 기본 자료형에서 다운캐스팅 할 때는 넓은 범위의 값이 좁은 범위로 바뀌기 때문에 오차가 발생하긴 하지만 문법적으로는 가능했습니다.

    하지만 객체는 명시적으로 적어 준다고 해도 다운캐스팅 자체가 안 될 때가 있습니다.

    잘못된 다운캐스팅을 수행하면 ClassCastException이라는 예외가 발생하고 프로그램이 종료됩니다.

    예를 들어 알아보겠습니다.

    class A {}
    class B extends A{}
    class C extends B{}

    상속 관계는 A < B < C로 상속을 하고 있고, 업캐스팅일 때는 아래와 같습니다.

    B b1 = new B();
    A a1 = (A)b1;	// (A)를 쓰지 않아도 컴파일러가 자동으로 추가
    
    C c2 = new C();
    B b2 = (B)c2;	// (B)를 쓰지 않아도 컴파일러가 자동으로 추가
    A a2 = (A)c2;	// (A)를 쓰지 않아도 컴파일러가 자동으로 추가

    객체를 B 생성자로 생성하면 B를 기준으로 부모 클래스 방향이 업캐스팅이므로 A로는 캐스팅할 수 있습니다.

    만일 객체를 C 생성자로 생성하면 A와 B로 모두 캐스팅 할 수 있게 됩니다.

    업캐스팅이므로 생략하더라도 컴파일러가 자동으로 추가하여 컴파일 하게 됩니다.

    다운 캐스팅일 때를 알아보겠습니다.

    A a1 = new A();
    B b1 = (B)a1;		// 예외 발생
    
    A a2 = new B();
    B b2 = (B)a2;		// 가능
    C c2 = (C)a2;		// 예외 발생

    A 생성자로 만든 A 타입은 B 타입으로 다운캐스팅할 수 없습니다.

    문법적으로 오류가 발생하지 않지만 실행 이후 실제 캐스팅 과정에서 ClassCastException 예외가 발생합니다.

    실제 객체가 A타입으로 만들어져 있기 때문입니다. 반면 B 생성자로 만든 A타입은 B타입으로의 다운 캐스팅을 할 수 있습니다.

    객체 자체가 B타입으로 만들어져 있기 때문입니다. 같은 이유로 B 생성자로 만든 A타입을 C타입으로 다운캐스팅할 수 없습니다.

    다운캐스팅의 경우 무슨 타입으로 선언되어 있는지는 보다는 어떤 생성자로 생성됐는지가 중요합니다.

    실제 생성된 객체의 부모 클래스 방향에 있는 모든 클래스 타입으로 항상 캐스팅할 수 있습니다.


    2) 다운캐스팅 메모리 구조

    다운캐스팅을 메모리 구조와 함께 그 과정을 알아보도록 하겠습니다.

    메모리에서의 동작만 잘 이해하면 캐스팅의 가능 여부나 선언된 타입에 따른 차이점까지 한 번에 파악할 수 있습니다.

    위에서 예를 들었던 A < B < C 의 상속 관계로 생각해 보겠습니다.

    먼저 A a = new B()를 살펴보면 실제 객치는 B 생성자로 만들어졌다는 것을 알 수 있습니다.

    상속에서 알아봤듯이 자식 클래스의 생성자를 호출하면 부모 클래스의 객체를 먼저 자동으로 생성한다고 배웠습니다.

    A 객체가 먼저 메모리에 만들어지고 B 객체가 완성될 것 입니다. 아래 그림과 같이 B 객체 속에 A 객체를 포함하고 있는 것이 됩니다.

    [ 그림 1 : 다운캐스팅 과정 메모리 구조 ]

    이 객체를 A 타입의 참조 변수(a)로 가리키고 있습니다. 실제 참조 변수는 힙 메모리의 B 객체 안에 있는 A객체를 가리키게 됩니다.

    선언된 타입이 의미하는 바는 실제 객체에서 자신이 선언된 타입의 객체를 가리키게 되는 것입니다.

    이제 B b = (B)a와 같이 A 타입의 참조변수(a)를 B 타입으로 캐스팅해 저장하고자 합니다.

    a는 A객체를 가리키지만 (B)a는 B 객체를 가리켜야 되는 것입니다. 힙 메모리에는 이미 B 객체가 있으므로 B 타입을 가리키는 것이 문제가 없습니다.

    반면 C c = (C)a 와 같이 C 타입으로 캐스팅을 하게되면 참조변수 c가 C 타입을 가리켜야하는데, 힙 메모리에는 C타입 객체가 만들어진 적이 없기 때문에 다운 캐스팅을 할 수 없게됩니다.

    이와 같은 이유로 캐스팅의 가능 여부를 확인하기 위해 실제 어떤 생성자로 만들어졌는지가 중요하게 됩니다.


    3) 선언 타입에 따른 차이점

    다운캐스팅을 메모리 구조상에서 이해하셨다면 선언 타입에 따른 차이점은 어렵지 않습니다.

    다음과 같이 2개의 클래스가 있다고 가정해보겠습니다.

    [ 그림 2 : 선언 타입에 따른 차이 예시 ]

    클래스 A는 필드 m과 메서드 abc()가 있고, 이 클래스를 상속한 클래스 B는 필드 n과 메서드 bcd()를 추가로 정의했습니다.

    즉, 클래스 B에서는 m, n, abc(), bcd()를 사용할 수 있습니다.

    이제 동일하게 B()생성자로 객체를 생성하고, 이를 B타입과 A타입으로 각각 선언했을 때의 차이를 알아보겠습니다.

    먼저 B b = new B()일 때는 아래와 같습니다.

    B b = new B();
    System.out.println(b.m);	// 가능
    System.out.println(b.n);	// 가능
    b.abc();			// 가능
    b.bcd();			// 가능

    [ 그림 3 : B b = new B()로 객체를 생성했을 때 메모리 구조 ]

    B() 생성자로 생성했으므로 힙 메모리에는 A 객체를 감싸고 있는 B 객체가 만들어질 것입니다.

    A 객체의 내부에는 m과 abc()가 있고, B 객체에는 추가로 n과 bcd()가 있습니다. 결국 B 객체의 내부에 m, n, abc(), bcd()가 있는 형태입니다.

    참조 변수가 B타입으로 선언되어 있으므로 참조 변수 b는 B객체를 가리키게 되고, 이때 참조 변수를 이용해 2개의 필드와 2개의 메서드를 모두 사용할 수 있습니다.

    다음으로는 A a = new B() 일 때를 확인해보겠습니다.

    A a = new B();
    System.out.println(a.m);	// 가능
    System.out.println(a.n);	// 오류
    a.abc();			// 가능
    a.bcd();			// 오류

    [ 그림 4 : A a = new B()로 객체를 생성했을 때 메모리 구조 ]

    B() 생성자로 객체를 생성한 것은 동일하므로 힙 메모리에 생성되는 객체의 모양은 동일한 것입니다.

    하지만 참조 변수가 A타입으로 선언되어 있으므로 실제로 힙 메모리에 B 객체가 있더라도 참조 변수 a는 A 객체만을 가리키게 됩니다.

    그래서 참조변수 a는 m과 abc()만 사용할 수 있습니다.


    4) 캐스팅 가능 여부를 확인하는 instanceof 키워드

    캐스팅할 수 있는지를 확인하려면 실제 객체를 어떤 생성자로 만들었는지와 클래스 사이의 상속 관계를 알아야 합니다.

    하지만 다른 사람이 만든 클래스를 사용할 때는 이런 정보를 하나하나 확인하는 것이 번거로운 일이 될 수 있습니다.

    심지어 자신이 만든 클래스나 객체도 프로젝트 규모가 커지거나 소스 코드가 길어지면 일일이 생성 객체의 타입을 확인하기가 쉽지 않습니다.

    이를 위해 자바에서는 캐스팅 가능 여부를 Boolean 타입으로 확인할 수 있는 문법 요소를 제공하고 있습니다.

    바로 instanceof 를 사용하면 됩니다.

    참조 변수 instanceof 타입

    참조 변수가 해당 타입으로 캐스팅할 수 있을 때 True, 그렇지 않을 때 False를 리턴합니다.

    여기서 instanceof 뒤의 타입은 참조 변수가 표현될 수 있는 모든 다형적 타입을 의미합니다.

    예를 들어서 A < B < C 의 상속 구조에서 C c = nes C()와 같이 생성했을 때 생성된 객체는 C 타입으로 선언되어 있지만, 다형적 표현 방법에 따라 A 및 B 타입으로도 선언될 수 있습니다.

    따라서 아래와 같이 했을 경우 모두 true의 결과값을 리턴 받을 수 있습니다.

    C c = new C();
    System.out.println(c instanceof A);		// true
    System.out.println(c instanceof B);		// ture
    System.out.println(c instanceof C);		// true

    이렇게 instanceof 키워드를 사용하면 상속 관계나 객체를 만든 생성자를 직접 확인하지 않아도 캐스팅 가능 여부를 확인할 수 있습니다.

    따라서 잘못된 캐스팅에 따른 실행 예외(ClassCastException)로 프로그램이 종료되는 것을 방지하기 위해 일반적으로 다운캐스팅을 수행할 때 instanceof를 이용해 캐스팅 가능 여부를 확인하고 가능할 때만 캐스팅을 처리합니다.

    A ab = new B();
    if (ab instanceof B) {		// true
       B b = (B)ab;			// 캐스팅 구문 실행
    }
    
    A aa = new A();
    if (aa instanceof B) {		// false
       B b = (B)aa;			// 캐스팅 구문 실행되지 않음
    }

     

    공감 ♥ + 구독 부탁드립니다 : )

    728x90
    반응형

    댓글