본문 바로가기
개발/자바, 코틀린

[Java] instanceOf 연산자

by 카펀 2022. 2. 21.

업무 중 마주한 내용에 대해 추가적으로 알아보고자 공부하고 작성하였습니다.

 

Java 코드를 보던 중 아래와 같은 경우를 보게 되었습니다 (예시입니다).

 

Queue<Integer> q = new LinkedList<>();
if (q instanceof LinkedList) {
    System.out.println("true");
}

 

instanceof 연산자입니다.

단순히 뜻을 유추해 보자면 q가 LinkedList의 instance인지 true/false 결과를 리턴하는 연산자로 보입니다.

 

실제로 그런 역할을 합니다.

무슨 말이냐 하면,

 

object instanceof type

 

위와 같은 코드가 있을 때, object가 type을 상속받는 클라스라면 true를 리턴하고, 그렇지 않다면 false를 리턴합니다.

위 코드의 경우 queue는 LinkedList를 이용하여 구현되었기 때문에, 위 코드가 true가 됩니다.

 

Queue<Integer> q = new LinkedList<>();
if (q instanceof TreeSet) {
    System.out.println("true");
}
else System.out.println("false");

 

위와 같은 경우에는 어떻게 될까요?

queue는 TreeSet을 상속받지 않았기 때문에 false를 출력하는 것으로 예상할 수 있습니다.

 

마찬가지로 출력되네요.

 

모든 객체는 Object 클라스를 상속받기 때문에, 객체 a에 대해 a instanceof Object는 항상 참이 됩니다.

 

Queue<Integer> q = new LinkedList<>();
if (q instanceof Object) {
    System.out.println("true");
}
else System.out.println("false");

 

 

a가 null일 경우, 아무것도 상속 받지 않았기 때문에 (정확히는 객체가 아니기 때문에) 항상 거짓이 됩니다.

 

Queue<Integer> q = null;
if (q instanceof Object) {
    System.out.println("true");
}
else System.out.println("false");

 

이렇듯 instanceof 연산자는 a가 Object를 상속 받은 클라스인지 알아볼 수 있는 연산자입니다.

타입 비교 연산자 (type comparison operator)라고도 불리는데, 인스턴스를 타입으로 비교하기 떄문입니다.

 

사용을 지양해야 하는 이유

instanceof 연산자는 기능적으로는 아무런 문제가 없지만, instanceof를 사용하는 코딩 습관은 좋지 못하다는 이야기가 있습니다.

물론 필연적으로 사용해야 할 때도 있지만, 좋은 코드 설계는 instanceof 사용을 피하는 편이라고 합니다.

 

class Animal {}
//Fish, Bird, Kangaroo가 각각 Animal을 상속받음
class Fish extends Animal {
  void swim(){
    System.out.println(“Swim”);
  }
}
class Bird extends Animal {
  void fly(){
    System.out.println(“Fly”);
  }
}
class Kangaroo extends Animal {
  void jump(){
    System.out.println(“Jump”);
  }
}

//나쁜 사용 예
public final class BadUseOfInstanceOf {
  public static void main(String[] args){
     makeItMove(new Fish());
     makeItMove(new Bird());
     makeItMove(new Kangaroo());
  }

  public static void makeItMove(Animal animal){
    //파라미터 animal이 Fish를 상속받은 경우
    if (animal instanceof Fish){
      Fish fish = (Fish)animal;
      fish.swim();
    }
    //파라미터 animal이 Bird를 상속받은 경우
    else if (animal instanceof Bird){
      Bird bird = (Bird)animal;
      bird.fly();
    }
    //파라미터 animal이 Kangaroo 상속받은 경우
    else if (animal instanceof Kangaroo){
      Kangaroo kangaroo = (Kangaroo)animal;
      kangaroo.jump();
    }
  }
}

 

위처럼 구현하는 경우에는 Fish, Bird, Kangaroo에 대해 각각 다른 액션을 취하고 싶은 경우일 것입니다.

하지만 단순히 Kangaroo 객체라는 것만으로, 어떤 액션을 취하기에 명확한 경우는 실제로는 잘 없습니다.

즉 추상화가 잘못되었다고 할 수 있습니다.

 

또, 코드를 보면, 각 객체가 무엇이고, 어떤 결과를 리턴해야 하는지 외부의 객체가 불필요하게 알 수 있게 됩니다.

이를 "캡슐화가 깨졌다" 라고 표현합니다.

 

또, 유지보수 문제 역시 있습니다.

만약 Animal을 상속받는 새로운 클라스가 추가된다면, BadUseOfInstanceOf 내부에도 마찬가지로 행동을 명시해 주어야 합니다. 이를 까먹을 수도 있고, 코드가 길어지면 쉽게 찾을 수 없을 것입니다.

 

아래와 같이 다형성을 적극 활용하여 코드를 작성한다면 좋을 것입니다.

 

class Animal {

  void move(){
    System.out.println(“Move”);
  }
}

class Fish extends Animal {

  @Override void move(){
    System.out.println(“Swim”);
  }
}

class Bird extends Animal {

  @Override void move(){
    System.out.println(“Fly”);
  }
}

class Kangaroo extends Animal {

  @Override void move(){
    System.out.println(“Jump”);
  }
}

 

public final class ProperUseOfInstanceOf {

  public static void main(String[] args){

     makeItMove(new Fish());
     makeItMove(new Bird());
     makeItMove(new Kangaroo());
  }



  /**

  * 이렇게 구현하는 경우, maktItMove는 Fish/Bird/Kangaroo 여부를 

  * 확인하지 않고, 그저 Animal의 move를 호출합니다.

  * Fish, Bird, Kangaroo는 각각 다른 버전의 move를

  * 가지고 있고, 각자의 move를 실행하게 됩니다.

  */

  public static void makeItMove(Animal animal){

    animal.move();
  }

}

 

내용 참고

댓글