[Java] instanceof 키워드 보단 다형성을 이용하자!
이번 포스팅에서는 instanceof 키워드란 무엇인지 그리고 해당 키워드 사용을 지양해야 하는 이유에 대해 알아보도록 하겠습니다.
핵심 정리
instanceof 키워드를 자주 사용하는 것은 캡슐화, 단일 책임 원칙, 개방-폐쇄 원칙을 위배할 수 있기 때문에 사용을 지양하고, 대신 다형성을 이용하는 것을 권장합니다.
instanceof 란?
object instanceof type은 객체가 특정 타입의 인스턴스인지를 확인하기 위한 연산자입니다. 이 연산자를 사용하면 런타임에 객체의 타입을 확인할 수 있습니다. 만약 어떤 타입에 대한 instanceof연산의 결과가 true라면 검사한 타입으로 형변환이 가능하다는 것을 뜻합니다.
class Animal {}
class Dog extends Animal {}
public class InstanceOfTest {
public static void main(String[] args) {
Animal animal = new Dog();
if (animal instanceof Dog) {
System.out.println("It's a Dog!");
} else {
System.out.println("It's not a Dog!");
}
}
}
// [ 출력 ]
// It's a Dog!
animal instanceof Dog는 animal이 Dog 클래스의 인스턴스인지를 확인합니다. 위의 예시에서 animal은 Dog 클래스로 생성되었으므로 해당 조건은 참이 되어 "이것은 개다!"가 출력됩니다. (다형성)
그럼 언제 instanceof가 true/false를 리턴할까?
instanceof 결과로 true를 반환하는 예시
①특정 클래스의 인스턴스이거나 ②해당 클래스를 상속하거나 ③해당 인터페이스를 구현하면 true를 반환합니다.
1. 클래스의 인스턴스 일 경우
class Animal {}
class Dog extends Animal {}
public class Main {
public static void main(String[] args) {
Animal animal = new Dog();
if (animal instanceof Animal) {
System.out.println("It's an Animal!");
}
if (animal instanceof Dog) {
System.out.println("It's a Dog!");
}
}
}
2. 해당 클래스를 상속한 경우
class Animal {}
class Dog extends Animal {}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
if (dog instanceof Animal) {
System.out.println("It's an Animal!");
}
}
}
3. 해당 인터페이스 구현한 경우
interface Eater {}
class Animal implements Eater {}
public class Main {
public static void main(String[] args) {
Animal animal = new Animal();
if (animal instanceof Eater) {
System.out.println("It can eat!");
}
}
}
instanceof 결과로 false를 반환하는 예시
객체가 ①특정 클래스의 인스턴스가 아니거나 ②해당 클래스를 상속하지 않거나 ③해당 인터페이스를 구현하지 않으면 instanceof 연산자는 false를 반환합니다.
1. 클래스의 인스턴스가 아닌 경우
class Animal {}
class Dog {}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
if (dog instanceof Animal) {
System.out.println("It's an Animal!");
} else {
System.out.println("It's not an Animal.");
}
}
}
2. 상속관계가 아닌 경우
class Animal {}
class Cat {}
public class Main {
public static void main(String[] args) {
Cat cat = new Cat();
if (cat instanceof Animal) {
System.out.println("It's an Animal!");
} else {
System.out.println("It's not an Animal.");
}
}
}
3. 해당 인터페이스를 구현하지 않은 경우
interface Eater {}
class Plant {}
public class Main {
public static void main(String[] args) {
Plant plant = new Plant();
if (plant instanceof Eater) {
System.out.println("It can eat!");
} else {
System.out.println("It can't eat.");
}
}
}
instanceof 사용 이유
instanceof를 사용하면 프로그램 실행 중에 객체의 실제 타입을 확인할 수 있습니다. 특정 작업을 수행하기 전에 올바른 타입의 객체를 다루고 있는지 확인하는 데 유용합니다.
class Animal {
void makeSound() {
System.out.println("동물은 제각각 울음소리가 다릅니다.");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("멍멍!");
}
}
public class InstanceOfTest {
static void makeSound(Animal animal) {
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.makeSound();
} else {
animal.makeSound();
}
}
public static void main(String[] args) {
Animal animal = new Dog();
makeSound(animal);
}
}
makeSound 메서드가 호출될 때 매개변수로 Animal클래스 또는 그 자손 클래스의 인스턴스를 넘겨받겠지만 메서드 내에서는 정확히 어떤 인스턴스인지 알 길이 없습니다. 이때 instanceof연산자를 이용해서 참조변수 animal가 가리키고 있는 인스턴스의 타입을 체크하고 적절히 형변환한 다음에 작업을 해야 합니다.
그럼에도 instanceof 사용을 지양해야 한다. 왜일까?
코드 작성을 할 때 instanceof 키워드 사용을 지양해야합니다. 그 대신 다형성을 사용하는 것을 권장하는데요.
코드 예시를 보면서 이유에 대해 알아보도록하겠습니다.
package com.example.instanceOf;
import org.springframework.util.StopWatch;
class Animal {
void makeSound() {
System.out.println("동물은 제각각 울음소리가 다릅니다.");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog 멍멍!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Cat 미야야야옹!");
}
}
class Bird extends Animal {
@Override
void makeSound() {
System.out.println("Bird 짹짹!");
}
}
public class InstanceOfExample {
public static void main(String[] args) {
Animal myDog = new Dog();
Animal myCat = new Cat();
Animal myBird = new Bird();
checkAnimalType(myDog);
checkAnimalType(myCat);
checkAnimalType(myBird);
}
static void checkAnimalType(Animal animal) {
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.makeSound();
} else if (animal instanceof Cat) {
Cat cat = (Cat) animal;
cat.makeSound();
} else if (animal instanceof Bird) {
Bird bird = (Bird) animal;
bird.makeSound();
} else {
System.out.println("확인되지 않은 동물타입입니다.");
}
}
}
코드를 보면 불필요한 정보들을 노출하고 있고 인스턴스를 생성할 때마다 조건문이 추가됨과 동시에 메서드 코드를 수정해야 함을 알 수 있습니다. 이를 객체지향프로그램 특징/원칙에 대입해 보자면 3가지 원칙에 위배됩니다.
캡슐화 위배
객체지향 프로그래밍에서 캡슐화란 클래스 안에 속성과 기능을 하나의 캡슐로 만들어 데이터를 외부로부터 보호하는 것을 말합니다. 하지만 instanceof를 사용하면 각 객체가 무엇인지, 어떤 행동을 반환해야 하는지 외부의 객체에 노출되게 됩니다. 캡슐화를 위반하면 객체의 상태에 직접 접근이 가능해지므로, 외부에서 무분별한 수정이 가능해집니다. 이는 보안 상의 문제로 이어질 수 있습니다.
단일책임원칙(SRP) 위배
checkAnimalType 메서드는 다양한 동물 타입에 대한 처리를 모두 담당하고 있습니다. SRP는 클래스나 메서드가 단일의 책임만을 가져야 한다는 원칙인데, 이 메서드는 여러 동물 타입에 대한 처리와 출력까지 모두 수행하고 있습니다.
개방-폐쇄원칙(OCP) 위배
새로운 동물 타입이 추가될 때마다 checkAnimalType 메서드를 수정해야 합니다. OCP는 확장에는 열려있고 변경에는 닫혀 있어야 한다는 원칙인데, 이 메서드는 새로운 동물 타입을 처리하기 위해 수정이 필요하므로 OCP를 위배하고 있습니다.
그럼 어떤 걸 사용해야 할까? 다형성!
다형성을 이용하여 구현하면 if분기문이 많이 필요 없기에 코드가 간결해집니다. 또한 상속 관계가 바뀌는 경우에도 바뀐 타입의 메서드만 수정해 주면 되므로 유지보수가 용이합니다.
package com.example.instanceOf;
import org.springframework.util.StopWatch;
class Animal {
void makeSound() {
System.out.println("동물은 제각각 울음소리가 다릅니다.");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog 멍멍!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Cat 미야야야옹!");
}
}
class Bird extends Animal {
@Override
void makeSound() {
System.out.println("Bird 짹짹!");
}
}
public class polymorphismExample {
public static void main(String[] args) {
checkAnimalType(new Dog());
checkAnimalType(new Cat());
checkAnimalType(new Bird());
}
static void checkAnimalType(Animal animal){
animal.makeSound();
}
}
checkAnimalType는 Dog/Cat/Bird 여부를 확인하지 않고 Animal의 makeSound()를 호출합니다.
마지막으로 성능 체크
instanceof를 사용할 때와 다형성을 사용할 때의 성능에도 차이가 있을지 확인해 보겠습니다.
Case 1. instanceof
package com.example.instanceOf;
import org.springframework.util.StopWatch;
class Animal {
void makeSound() {
System.out.println("동물은 제각각 울음소리가 다릅니다.");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog 멍멍!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Cat 미야야야옹!");
}
}
class Bird extends Animal {
@Override
void makeSound() {
System.out.println("Bird 짹짹!");
}
}
public class InstanceOfExample {
public static void main(String[] args) {
// StopWatch 객체 생성
StopWatch stopWatch = new StopWatch();
// 측정 시작
stopWatch.start();
for (int i = 0; i < 1_000_000; i++) {
Animal myDog = new Dog();
Animal myCat = new Cat();
Animal myBird = new Bird();
checkAnimalType(myDog);
checkAnimalType(myCat);
checkAnimalType(myBird);
}
// 측정 종료
stopWatch.stop();
// 결과 출력
System.out.println("totalTimeSeconds : " + stopWatch.getTotalTimeSeconds() + "초");
}
static void checkAnimalType(Animal animal) {
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.makeSound();
} else if (animal instanceof Cat) {
Cat cat = (Cat) animal;
cat.makeSound();
} else if (animal instanceof Bird) {
Bird bird = (Bird) animal;
bird.makeSound();
} else {
System.out.println("확인되지 않은 동물타입입니다.");
}
}
}
Case 2. 다형성
package com.example.instanceOf;
import org.springframework.util.StopWatch;
class Animal {
void makeSound() {
System.out.println("동물은 제각각 울음소리가 다릅니다.");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog 멍멍!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Cat 미야야야옹!");
}
}
class Bird extends Animal {
@Override
void makeSound() {
System.out.println("Bird 짹짹!");
}
}
public class polymorphismExample {
public static void main(String[] args) {
// StopWatch 객체 생성
StopWatch stopWatch = new StopWatch();
// 측정 시작
stopWatch.start();
for (int i = 0; i < 100000; i++) {
checkAnimalType(new Dog());
checkAnimalType(new Cat());
checkAnimalType(new Bird());
}
// 측정 종료
stopWatch.stop();
// 결과 출력
System.out.println("totalTimeSeconds : " + stopWatch.getTotalTimeSeconds() + "초");
}
static void checkAnimalType(Animal animal){
animal.makeSound();
}
}
100,000개의 객체 생성시 성능을 비교해 보았을 때, instanaceof는 2.5425808초, 다형성은 2.430432101초가 소요됨을 확인했습니다.
instanceof 연산자를 사용하면 타입 체크 작업이 추가되므로 상대적으로 더 많은 시간이 소요됨을 알 수 있었습니다.
참고
자바의 정석
https://tecoble.techcourse.co.kr/post/2021-04-26-instanceof/