Java

[Java] instanceof 키워드 보단 다형성을 이용하자!

Lea Hwang 2023. 12. 4. 18:28

이번 포스팅에서는 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 DoganimalDog 클래스의 인스턴스인지를 확인합니다. 위의 예시에서 animalDog 클래스로 생성되었으므로 해당 조건은 참이 되어 "이것은 개다!"가 출력됩니다. (다형성)

그럼 언제 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/