업 캐스팅 (Upcasting)
업 캐스팅을 요약해보자면, 서브 클래스(하위) 타입의 인스턴스를 슈퍼 클래스(상위) 타입으로의 캐스팅(형 변환)을 의미합니다. 말이 좀 어려운데, 코드로 보면 어떤 상황인지 쉽게 이해가 가실 겁니다.
class Animal {
...
}
class Tiger extends Animal {
...
}
public class Example {
public static void main(String[] args) {
Animal animalTiger = new Tiger(); // Upcasting
}
}
이런 상황입니다. 이 경우에는 Animal이 슈퍼 클래스이고 Tiger가 서브 클래스가 되겠죠.
자, 이제 main 함수 안에 업 캐스팅이 일어나고 있는 코드를 보겠습니다.
Animal animalTiger = new Tiger(); // Upcasting
먼저 코드부터 해석해볼까요?
① Tiger 생성자를 통해 ② Tiger 타입의 인스턴스가 만들어지고, ③ Animal 타입으로 형 변환이 일어나게 됩니다.
여기서 주목해야 할 점은 바로 ③입니다. 이 지점이 바로 업 캐스팅이 되고 있는 부분이죠.
이런 식으로 업 캐스팅이 된다면, 분명 우리는 Tiger 인스턴스를 만들었지만 Tiger 클래스만의 멤버에는 접근할 수 없게 됩니다. Animal 클래스의 멤버에만 접근할 수 있게 되죠. 하지만 오버라이딩 된 함수는 오버라이딩 된 상태로 유지됩니다. 아래 코드를 보면서 이해해보죠.
class Animal {
public int age;
public void move() {
System.out.println("어떤 동물이 움직이나요?");
}
}
class Tiger extends Animal {
public String color;
@Override
public void move() {
System.out.println("호랑이가 움직입니다!!!");
}
}
public class Example {
public static void main(String[] args) {
Animal animal = new Animal();
animal.age = 12;
animal.move(); // 어떤 동물이 움직이나요?
Tiger tiger = new Tiger();
tiger.age = 100;
tiger.color = "Yellow";
tiger.move(); // 호랑이가 움직입니다!!!
Animal animalTiger = new Tiger();
animalTiger.age = 5;
// animalTiger.color = "White"; <-- 컴파일 에러
animalTiger.move(); // 호랑이가 움직입니다!!!
}
}
main 함수 안에 animalTiger 부분을 보면 이해가 되실 겁니다. Tiger 형태로 인스턴스가 만들어졌지만 Animal로 형 변환이 일어나게 되면서 color 필드에는 더 이상 접근할 수 없게 됩니다. 다만, Animal 타입임에도 불구하고 move() 메서드는 오버라이드 된 상태로 유지됐죠.
이를 일반화 시켜보면 이렇습니다.
서브 클래스 타입을 슈퍼 클래스 타입으로 업 캐스팅 했을 때, 더 이상 서브 클래스의 멤버에는 접근할 수 없고 슈퍼 클래스의 멤버에만 접근할 수 있다. 단, 오버라이드 됐던 메서드는 오버라이드 된 상태로 유지된다.
이제 업 캐스팅은 언제 성립될 수 있는지를 봐보겠습니다.
일단 업 캐스팅이 성립되기 위해서는, 우항의 정보가 좌항이 요구하는 정보를 모두 포함하고 있어야 합니다.
이때 클래스의 상속 관계 혹은 인터페이스와 구현체의 관계는 이러한 조건을 자연스럽게 만족하고 있습니다. 때문에 이러한 경우에서 업 캐스팅은 자연스럽게 성립이 될 수 있습니다.
이제 위 코드의 경우에서 그 이유를 구체적으로 봐보죠.
Tiger 클래스는 Animal 클래스를 상속받았기 때문에, Animal 클래스의 모든 멤버에 접근할 수 있음은 물론, 추가적으로 Tiger 클래스에만 있는 멤버에도 접근이 가능합니다. 즉, Tiger 클래스에서 접근 가능한 멤버의 수는 Animal 클래스에서 접근 가능한 멤버의 수보다 크거나 같습니다. 따라서, Tiger 타입은 Animal 타입에서 필요한 멤버를 모두 만족하므로 이러한 형태의 형 변환이 가능하게 됩니다.
자, 이제 중요한 주제입니다. 업 캐스팅을 하다보면 이런 의문이 들 수 있습니다.
왜 업 캐스팅을 하는가?
하위 타입의 인스턴스를 생성했다면 그냥 해당 하위 타입을 참조하는 변수에 넣어 사용하는 것이 더 합리적이라는 생각이 들 수 있습니다. 굳이 상위 타입으로 업 캐스팅을 하면 사용할 수 있는 멤버의 수를 스스로 제한하는 꼴이 돼 버리니까요.
업 캐스팅이라는 개념만 놓고 보면 굉장히 간단하고 쉽지만, 왜 사용하는가에 대한 답변은 그리 간단하지 않습니다. 이에 대한 해답을 얻기 위해서는 좋은 객체 지향 디자인이란 무엇인지, 그리고 이를 위한 디자인 원칙은 무엇인지를 생각해봐야 하기 때문이죠.
잘 알려진 객체 지향 디자인 원칙 중 OCP(Open-Closed Principle, 이하 OCP)라는 디자인 원칙이 있습니다. 이 글의 메인 주제는 아니기 때문에 자세히 다루지는 않겠지만, 요약하자면 확장에는 열려있어야 하고 변경에는 닫혀있어야 한다 라는 내용의 디자인 원칙입니다. 이런 OCP 원칙을 지키기 위해서는 추상화(Abstraction) 그리고 다형성(Polymorphism)을 이해하고 활용할 줄 알아야 하는데, 업 캐스팅은 이를 코드로 구현하기 위한 하나의 방법이라고 할 수 있습니다.
정말 간단한 상황을 하나 만들어서 봐보죠.
카페에 다양한 음료들이 있다고 했을 때, 그 음료들의 가격을 모두 출력해서 보여주는 서비스가 필요하다고 가정해봅시다.
그렇다면 일단 음료들을 먼저 구현해봐야겠죠. 코드는 아래와 같습니다.
class Beverage {
public void printCost() {
System.out.println("음료의 가격을 보여줍니다.");
};
}
class Coffee extends Beverage{
@Override
public void printCost() {
System.out.println("커피의 가격은 1500원입니다.");
}
}
class OrangeJuice extends Beverage{
@Override
public void printCost() {
System.out.println("오렌지 쥬스의 가격은 2500원입니다.");
}
}
class Water extends Beverage{
@Override
public void printCost() {
System.out.println("물은 무료입니다!");
}
}
Beverage라는 음료 슈퍼 클래스와 이를 상속받는 각각의 음료 서브 클래스들입니다.
이제 음료는 구현했으니, 각 음료들의 가격을 모두 보여주는 서비스 코드를 만들어봅시다.
이때, 업 캐스팅을 이용한 방식과 이용하지 않은 방식으로 나누어 구현해보겠습니다.
class BeverageServiceWithUpcasting {
// 업 캐스팅을 이용한 서비스 클래스
Beverage[] beverages;
BeverageServiceWithUpcasting(Beverage[] beverages) {
this.beverages = beverages;
}
public void printBeveragesCost() {
for (Beverage beverage : beverages) {
beverage.printCost();
}
}
}
class BeverageServiceWithoutUpcasting {
// 업 캐스팅을 이용하지 않은 서비스 클래스
Coffee coffee = new Coffee();
OrangeJuice orangeJuice = new OrangeJuice();
Water water = new Water();
public void printBeveragesCost() {
coffee.printCost();
orangeJuice.printCost();
water.printCost();
}
}
어떤 차이가 있는지 보이시나요?
업 캐스팅을 이용한 서비스 클래스부터 봐보죠.
이 클래스 안에 있는 각각의 음료들은 Beverage라는 공통의 타입을 가집니다. 따라서 향상된 for문을 통해 반복하여 함수를 호출해주기만 하면 끝입니다.
업 캐스팅을 이용하지 않은 서비스 클래스는 어떨까요?
여기서의 음료들은 공통의 인터페이스를 가지지 않기 때문에 for문을 이용해 반복할 수 없습니다. 따라서 각 음료 객체를 직접 써서 함수를 호출하고 있습니다.
그렇다면 만약 카페에 새로운 음료 메뉴가 출시됐다고 해봅시다. 이 경우에는 어떻게 될까요?
업 캐스팅을 이용한 클래스에서는 기존의 코드를 고치지 않아도 됩니다. 단지 beverages배열을 넘겨줄 때 새로운 메뉴를 포함한 배열을 넘겨주기만 하면 되죠. 즉, 확장에는 열려있고 변경에는 닫혀있습니다.
하지만 업 캐스팅을 이용하지 않은 클래스에서는 기존의 코드를 변경해야 합니다. 확장이 일어날 때마다 말이죠.
이처럼 좋은 객체 지향 디자인을 하는 데에는 업 캐스팅이 자연스럽게 따라오게 됩니다.
적절한 수준의 추상화, 그리고 추상화된 슈퍼 클래스로 업 캐스팅하는 서브 클래스들. 이를 통해 슈퍼 클래스 타입은 마치 각각의 서브 클래스 타입처럼 행동할 수 있습니다. 바로 다형성이죠.
이처럼 업 캐스팅은 자연스럽게 OCP라는 객체 지향 디자인 원칙의 중요 메커니즘을 구현하고 있습니다. 물론, 업 캐스팅만 사용한다고 절대로 OCP 원칙을 만족할 순 없습니다. 다만, OCP 원칙을 만족하기 위해서는 업 캐스팅이 사용된다는 것입니다.
<참고>
이 글에서는 업 캐스팅에 대해서만 다루었지만, 반대로 다운 캐스팅이라는 것도 존재합니다.
이미 업 캐스팅이 된 참조 변수에 대해서 다시 원 상태의 참조 타입으로 되돌리는 것을 의미하는데, 보통 다운 캐스팅은 instanceof 연산자와 짝을 이루어 사용되는 경우가 많습니다. 하지만 많은 경우에 대해서 OCP 원칙에 반하는 형태이므로 좋은 객체 지향 디자인이라고 보기는 어렵습니다.
'개발 > Java' 카테고리의 다른 글
Repository 계층, 도메인과 영속성 엔티티 사이의 간극 메꾸기 (0) | 2023.05.21 |
---|---|
반환형이 void인 메서드, 어떻게 테스트할까? (with 행위 검증) (6) | 2023.05.07 |
public VS private + getter (0) | 2023.04.02 |
취약한 기반 클래스 문제 (2) | 2023.03.26 |
JDBC, DB 접근을 위한 자바 표준 인터페이스 (0) | 2023.03.12 |
댓글