이번 포스팅은 [Baeldung] 사이트에서 자바 타입 캐스팅에 관한 글을 번역한 내용입니다. 오역과 의역이 있으니 읽으실 때 참고 부탁드립니다. (https://www.baeldung.com/java-type-casting)
1. Overview
자바의 타입은 원시형(Primitives)과 참조형(References) 이렇게 2가지 종류로 구성되어 있다. 우리는 원시형 타입에 대해서는 다른 글에서 다루었고, 이번 글에서는 참조형 캐스팅을 다룰 예정이다.
2. 원시형(Primitives) vs 참조형(References)
[원시 타입 변환(Primitive conversions]과 [참조형 변수 캐스팅(Reference variable casting]은 어떻게 보면 비슷할지 모르겠지만, 두 개의 개념은 다르다. 원시형 타입 변환 개념을 이해하기 위해 아래 코드를 살펴보자. 값을 가지고 있는 원시 타입 변수에 다른 타입으로 캐스팅을 하게 되면 원래 존재하였던 값이 변하게 된다.
public class TypeTest {
@Test
@DisplayName("원시형 타입 변환")
void primitive_casting_value_check() {
double myDouble = 1.1; // primitive (double)
int myInt = (int) myDouble; // primitive type conversion (double -> int)
System.out.println("double: " + myDouble + " | " + "int: " + myInt);
assertNotEquals(myDouble, myInt); // Not Equal OK
}
}
조금 더 자세하게 살펴보면, [myInt] 변수에는 이전의 선언된 "1.1"의 값이 아닌 "1"로 저장된다. 그렇다면 참조형 변수들은 어떻까? 결론적으로 말하면 참조형 변수들은 다른 양상을 띤다. 참조 변수는 객체만 참조하고 있지 객체 자체를 가지고 있지 않다. 따라서 참조 변수를 캐스팅해도 참조하는 객체에는 직접적인 변화를 줄 수 없고, 다만 객체의 레이블을 통해 확장/축소할 수 있다. (여기서 레이블이랑 부모/자식 간의 이동으로 보는 게 쉽다. ex. 상속관계) 이러한 확장/축소를 Upcasting과 Downcasting이라 하는데, Upcasting은 사용 가능한 메서드 및 속성을 축소하는 것이고, Downcasting은 반대로 확장하는 것이다.
참조(Reference)는 여러 가지 타입의 객체를 조종하는 리모컨과 같다. 객체의 실제 값에 접근하기 위해서는 객체 참조를 통해 간접적으로 접근할 수 있다. 참고로 실제 값을 가리키는 참조 값은 JVM Heap 메모리에 저장된다. 따라서 우리가 Downcasting을 하게 될 때, 우리는 타입을 변경할 수 있지만, 값을 변경할 수 없게 된다.
3. Upcasting
자식 클래스가 부모 클래스로 캐스팅되는 것을 의미한다. 일반적으로 Upcasting은 컴파일러에 의해 수행된다.
Upcasting은 상속과 밀접하게 연관되어 있다(상속은 Java의 핵심 기능). 구체적인 타입을 참고하기 위해 참조 변수를 사용하게 되면 암묵적 Upcasting이 발생한다. Upcasting을 살펴보기 위해 Animal 클래스를 정의해 보자.
public class Animal {
public void eat() {
System.out.println("음식을 먹는다.");
}
}
이제 Animal 클래스를 확장해 보자.
public class Cat extends Animal {
public void eat() {
System.out.println("고양이가 음식을 먹는다.");
}
public void meow() {
System.out.println("고양이가 야옹한다.");
}
}
이제 객체를 생성한 후 Cat 클래스를 Cat 타입의 참조 변수에 할당해 보자.
Cat cat = new Cat();
추가로 cat 참조 변수를 다시 Animal 타입의 참조 변수로 할당하자.
Animal animal = cat;
위에 할당 과정에서 암묵적 Upcasting이 발생하게 된다. 실제 명시를 하게 되면 아래와 같다.
animal = (Animal) cat;
상속관계를 명시적으로 캐스팅할 필요는 없다. 컴파일러는 "cat"이 "Animal"인 줄 알고 에러를 표시하지 않는다. 이러한 참조 방식은 선언만 미리 되어 있다면 어떠한 타입도 참조가 가능하다.
Upcating을 사용하여 Cat 인스턴스는 Animal 타입으로 캐스팅되었다. 다만 기존 Cat 타입에서 사용할 수 있는 메서드가 제한된다 (Animal 클래스의 메서드만 사용). 하지만 역시 객체 자체가 변경되지는 않는다.
public class Client {
public static void main(String[] args) {
Cat cat = new Cat();
Animal animal = cat;
// Cat Object
System.out.println("Cat Object: " + cat);
cat.eat();
cat.meow();
// Animal Object
System.out.println("Cat -> Animal Upcasting: " + animal);
animal.eat();
animal.meow(); // Cannot resolve method
}
}
[Cat Object]: me.minikuma.javatest.Cat@15aeb7ab
[Cat -> Animal Upcasting]: me.minikuma.javatest.Cat@15aeb7ab
실제 코드를 보면 참조하는 객체의 주소는 변하지 않았지만, Upcasting 된 객체(animal)에서는 meow() 메서드를 사용할 수 없다. 어떻게 하면 meow() 메서드를 사용할 수 있을까? Downcasting을 통해 가능하다. 사실 이렇게 설명한 Upcasting이 있기 때문에 우리는 "다형성"의 이점을 이용할 수 있게 된다.
3.1 다형성
다른 하위 클래스인 Dog 클래스를 만들어 보자.
public class Dog extends Animal {
public void eat() {
System.out.println("강아지가 음식을 먹는다.");
}
}
추가로 모든 고양이와 강아지를 먹일 수 있는 AnimalFeeder 클래스와 feed()라는 메서드를 하나 만들자.
public class AnimalFeeder {
public void feed(List<Animal> animals) {
animals.forEach(animal -> {
animal.eat();
});
}
}
feed() 메서드는 Cat 혹은 Dog와 같은 특정 타입과 상관없이 수행된다. 여기서 Upcasting은 Animal 리스트를 등록할 때 발생한다.
public class Client {
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
animals.add(new Cat());
animals.add(new Dog());
AnimalFeeder feeder = new AnimalFeeder();
feeder.feed(animals);
}
}
Dog와 Cat는 암묵적으로 Animal로 캐스팅이 발생한다. Cat도 Animal에 속해 있고, Dog도 Animal에 속해 있게 되는데, 이것을 "다형성"이라 한다. 결국 동물이라는 객체가 고양이 객체도 될 수 있고, 강아지도 객체도 될 수 있음을 의미한다.
이것을 확장해 보면, 모든 자바의 객체도 Object 객체를 부모로 가지고 있기 때문에 "모든 자바 객체도 다형성을 가지고 있다"라고 볼 수 있다. 따라서 아래와 같이 Animal 객체를 Object 객체에 할당을 해도 자바 컴파일러는 에러를 발생시키지 않는다.
Object object = new Animal();
우리가 생성하는 모든 자바 객체에는 이미 Object와 관련된 메서드가 존재한다. (예) toString() -> 부모인 Object 속성을 자식이 사용할 수 있다. 이러한 Upcasting은 인터페이스에서도 흔하게 발생한다. 아래 예제를 살펴보자.
public interface Mew {
public void meow();
}
public class Cat extends Animal implements Mew {
public void eat() {
System.out.println("고양이가 음식을 먹는다.");
}
public void meow() {
System.out.println("고양이가 야옹한다.");
}
}
public class Client {
public static void main(String[] args) {
Mew mew = new Cat(); // Mew 객체로 Upcasting
mew.meow();
}
}
Cat 객체는 Mew이고, 캐스팅은 암묵적으로 발생한다. 결국 Cat 객체는 Mew, Animal, Object, Cat 객체를 참조할 수 있도록 지정할 수 있다.
3.2 오버 라이딩
위에 예제를 보면, Animal 타입 변수에서 eat() 메서드가 호출되지만 실제 객체(Cat or Dog)에서 호출된 메서드가 동작하게 된다. 즉, 부모 객체의 속성을 자식 객체에서 재 정의할 수 있다.
public void feed(List<Animal> animals) {
animals.forEach(animal -> {
animal.eat();
});
}
각 클래스에서 동작 여부를 확인하기 위해 출력을 하게 되면 아래와 같은 결과를 얻을 수 있다.
고양이가 음식을 먹는다.
강아지가 음식을 먹는다.
요약하면,
- 객체가 변수와 동일한 타입이거나 하위 타입인 경우 참조 변수는 해당 객체를 참조할 수 있다.
- Upcasting은 암묵적으로 발생한다.
- 모든 자바 객체는 다형성을 가지고 있고, Upcasting으로 인해 슈퍼 타입의 객체로 취급될 수 있다.
4. Downcasting
Animal 클래스의 변수를 사용하여 Cat 클래스에 사용할 수 있는 메서드를 호출하려면 어떻게 해야 할까? 슈퍼 클래스에서 서브 클래스로 캐스팅을 통해 가능하며, 이를 Downcasting이라 부른다.
위에 Upcasting 예제에서 우리는 animal 변수가 Cat 인스턴스를 참조하고 있기 때문에 Cat 클래스에 존재하는 meow() 메서드를 사용할 수 있다고 생각했지만 실제는 컴파일러에서 에러가 발생하였다. 이를 해결하기 위해서는 animal 변수가 그 하위 클래스인 Cat 클래스로 Downcasting 되어야 한다.
((Cat) animal).meow();
내부 괄호와 그 안에 포함된 타입을 캐스트 연산자라고 한다. 코드를 컴파일하기 위해서는 외부 괄호를 추가해야 한다. 이전 예제였던 AnimalFeeder 클래스에 아래와 같이 meow() 메서드를 추가해 보자.
public class AnimalFeeder {
public void feed(List<Animal> animals) {
animals.forEach(animal -> {
animal.eat();
if (animal instanceof Cat) {
((Cat) animal).meow(); // Downcasting
}
});
}
}
//Result
고양이가 음식을 먹는다. -> Cat 클래스의 eat() 메서드
야옹한다. -> Cat 클래스의 meow() 메서드
강아지가 음식을 먹는다. -> Dog 클래스의 eat() 메서드
이제 animal 참조 변수에서도 Cat 클래스에서 사용하는 모든 메서드(eat, meow)에 접근할 수 있게 되었다. 실제로 Cat 인스턴스 객체만 Downcasting 하기 위해 instanceof 연산자를 사용하였다.
4.1 instanceof 연산자
우리는 Downcasting 하기 전에 instanceof 연산자를 사용하여 객체가 특정 타입에 속하는지 확인한다.
4.2 ClassCastException
만약 instanceof 연산자로 객체 타입을 확인하지 않으면 어떻게 될까? 컴파일에서는 에러가 발생하지 않지만 런타임에 예외가 발생하게 된다. 이를 증명하기 위해 위에 코드에서 instanceof 연산자를 제거해 보자. 이 코드는 문제없이 컴파일된다. 하지만 실행하면 에러가 발생한다. (java.lang.ClassCastException) Downcasting 할 객체가 실제 객체의 타입과 일치하지 않는 경우에 발생한다.
그렇다면 아예 관련 없는 타입을 Downcasting 하면 어떻게 될까?
Animal animal;
String s = (String) animal;
해당 코드를 컴파일하게 되면 컴파일 에러가 발생한다. 결국 컴파일을 하기 위해서는 두 타입이 동일한 상속 트리에 있어야 한다. (Animal 객체와 String 객체는 동일 상속 트리에 미 존재)
요약을 해 보면 아래와 같다.
- 하위 클래스에 특정 멤버에 접근하기 위해서는 Downcasting이 필요하다.
- Downcasting은 캐스트 연산자를 사용해야 한다.
- 객체를 안전하게 Downcasting하기 위해서는, "instanceof" 연산자가 필요하다.
- 만약 우리가 Downcasting한 타입에 해당하는 실제 객체가 없다면, 런타임 예외(ClassCastException)가 발생한다.
5. Cast() 메서드
클래스의 cast() 메서드를 사용하여 객체를 Downcasting 하는 방법이 있다.
public void whenDownToCatWithCastMethod_thenMeowIsCalled() {
Animal animal = new Cat();
if (Cat.class.isInstance(animal)) {
Cat cat = Cat.class.cast(animal);
cat.meow();
}
}
위에 예제에서 cast() 및 isInstance() 메서드는 cast 및 instanceof 연산자 대신 사용하였다. 일반적으로 사용할 때에는 cast()와 instanceof() 메서드는 제네릭 타입을 사용한다.
제네릭 동작 확인을 위해 feed() 메서드를 가지는 AnimalFeederGeneric <T> 클래스를 하나 만들자. feed() 메서드는 animal 타입을 가지고 있지만, 파라미터에 의해 Dog 혹은 Cat이 될 수 있다. 파라미터는 생성자로 주입받는다.
public class AnimalFeederGeneric<T> {
private Class<T> type;
public AnimalFeederGeneric(Class<T> type) {
this.type = type;
}
public List<T> feed(List<Animal> animals) {
List<T> list = new ArrayList<>();
animals.forEach(animal -> {
if (type.isInstance(animal)) {
T objAsType = type.cast(animal);
list.add(objAsType);
}
});
return list;
}
}
feed() 메서드는 유입된 "T"의 인스턴스와 각각의 animal 인스턴스를 확인하고 반환한다. 제네릭의 구체적인 타입은 생성자의 파라미터로 주입한다. 아래 테스트를 보면 제네릭 타입인 T는 생성자 주입을 통해 구체적인 Cat 타입으로 생성이 된다.
@Test
@DisplayName("Generic Type Down casting")
public void whenParameterCat_thenOnlyCatsFed() {
List<Animal> animals = new ArrayList<>();
animals.add(new Cat()); // index 0
animals.add(new Dog()); // index 1
AnimalFeederGeneric<Cat> catFeeder = new AnimalFeederGeneric<Cat>(Cat.class);
List<Cat> fedAnimals = catFeeder.feed(animals);
assertEquals(1, fedAnimals.size());
assertTrue(fedAnimals.get(0) instanceof Cat); // index-0에 Cat 이 존재한는가?
}
6. Conclusion
해당 포스팅을 통해 아래 3가지에 해당하는 질문에 답을 하였다.
- Upcasting과 Downcasting이 무엇(What) 인지?
- Upcasting과 Downcasting을 어떻게 사용(How to use)하는지?
- 다형성에 어떠한 이점(Take of advantage)을 주는지?
'dev. > Java' 카테고리의 다른 글
자바 맵을 사용할 때 자주하는 9가지 질문 (1) | 2019.12.09 |
---|