본문 바로가기
💊 Java & Kotlin & Spring/- Java & kotlin

다형성을 위한 instanceof 를 Generic 으로 제거하는 방법

by Wonit 2022. 4. 3.

목차

 

  • 서론
  • instanceof 란?
  • 코드에서 instanceof 를 느껴보자
  • Generic 을 이용하여 우아하게 제거하기
  • 그럼에도 존재하는 문제점
  • 하지만?

 

서론

 

자바에서 다형성을 이용한 객체지향적 프로그래밍을 하다보면 종종 특정 객체가 지정된 유형의 인스턴스인지 확인해야 하는 경우가 있다.

그 경우 우리는 여러가지 선택지가 있지만, 오늘은 instanceof 에 대해 집중하고 이야기해보려 한다.

 

다형성을 이용한 프로그래밍을 하다 보면 특정 형으로 변환하기 위해 몇가지 작업을 해야 하는데, 그중 대표적인 것이 바로 instanceof 연산자이다.

 

instanceof 란?

 

앞서 말했듯 Java 에서는 어떤 객체의 특정 type 에 대해서 동일한 type 인지 확인할 수 있는 연산을 instanceof 연산자를 통해서 수행하고 있다.

 

예를 들어서 다음과 같은 상황에서의 코드를 보자

 

User 는 총 3가지의 형태(Guest, Member, Admin)가 존재한다.

 

public interface User {
    // User 인터페이스
}

// 게스트
public class Guest implements User {}

// 멤버, 일반 유저
public class Member implements User {}

// 관리자
public class Admin implements User {}

public class Testing {
    @Test
    void test() {
        User admin = new Admin();

        boolean actual = admin instanceof Admin;

        assertThat(actual).isTrue();
    }
}

 

당연하게도 위의 코드는 성공하게 된다.

 

하지만 일각에서는 이러한 instanceof 의 사용을 나쁜 냄새라고 해석하기도 하며 Anti-Pattern 이라는 의견도 존재한다.

 

“instanceof”, Why And How To Avoid It In Code article

 

왜 이것이 피해야할 대상인지 코드단으로 한 번 알아보자

 

코드에서 instanceof 를 느껴보자

 

코드에서 instanceof 를 느껴보자. 왜 이를 Anti-Pattern 이라고 하고 나쁜 냄새라고 하는지.

 

우선 문제 상황은 이러하다.

 

사용자의 유형에 따라 할 수 있는 행동의 범위가 달라지는데, 이를 확인할 수 있는 어떠한 기능을 만들어보려 한다.

 

위의 기능을 구현한다면 아래와 같이 세부 내용 및 특징과 제약을 정리할 수 있을 것이다.

 

사용자

 

  • 사용자는 3가지의 유형과 각각의 행위가 존재한다.
    • Guest : 방문자
      • 가능한 행위 : 글 읽기
    • Member : 회원
      • 가능한 행위 : 글 읽기, 글 쓰기
    • Admin : 관리자
      • 가능한 행위 : 글 읽기, 글 쓰기, 글 삭제

 

Actuator

 

 

  • Actuator 에 의해서 각각 사용자의 행위를 출력
    • 사용자 유형에 따라 Actuator 를 각기 다르게 구현
      • GuestActuator : 1개의 행위를 출력
      • MemberActuator : 2개의 행위를 출력
      • AdminActuator : 3개의 행위를 출력
    • Composit 패턴을 이용하여 ActuatorContainer 에서 Actuator를 합성하여 소유

 

전체적 설계

 

위의 내용들을 합치면 아래와 같은 설계가 나오게 된다.

 

 

그럼 위의 그림을 토대로 실제 구현을 해보도록 하자

 

User 패키지

 

User 패키지는 User 서로 다른 user 의 구현체들이 존재하는 곳이다.

 

3가지의 User Type 을 봐보자

 

// UserType Enum
public enum UserType {
    GUEST, ADMIN, MEMBER
}

// User Interface
public interface User {
    UserType getType();
    void readArticle();
}

// Guest Class
public class Guest implements User {
    @Override
    public UserType getType() {
        return UserType.GUEST;
    }

    @Override
    public void readArticle() {
        System.out.println("Guest 는 Article 을 읽을 수 있습니다.");
    }
}

// Member Class
public class Member implements User {

    @Override
    public UserType getType() {
        return UserType.MEMBER;
    }

    @Override
    public void readArticle() {
        System.out.println("Member 은 Article 을 읽을 수 있습니다.");
    }

    public void createArticle() {
        System.out.println("Member 은 Article 을 생성할 수 있습니다.");
    }
}

// Admin Class
public class Admin implements User {

    @Override
    public UserType getType() {
        return UserType.ADMIN;
    }

    @Override
    public void readArticle() {
        System.out.println("Admin 은 Article 을 읽을 수 있습니다.");
    }

    public void createArticle() {
        System.out.println("Admin 은 Article 을 생성할 수 있습니다.");
    }

    public void deleteArticle() {
        System.out.println("Admin 은 Article 을 삭제할 수 있습니다.");
    }
}

 

앞서 이야기했던 바와 같이 3가지의 서로 다른 사용자 유형은 서로 다른 권한을 가지고 있고, User 를 생성하는 클라이언트에서는 적절한 상황에 적절한 User 하위 구현체들을 생성해줘야 한다.

 

이제 User 를 사용하는 쪽을 가보자

 

Actuator들

 

Actuator 에서는 User 를 매개변수로 받아서 User 가 할 수 있는 행동들을 취한다.

 

3개의 UserType 이 존재하므로 3개의 Actuator 를 만들어보자.

 

public interface UserActuator {
    void describeActions(User user);
}

public class GuestActuator implements UserActuator {
    @Override
    public void describeActions(User user) {

        if (!(user instanceof Guest)) {
            throw new IllegalArgumentException("Guest 가 아닙니다.");
        }

        user.readArticle();
    }
}

public class MemberActuator implements UserActuator {

    @Override
    public void describeActions(User user) {
        if (!(user instanceof Member)) {
            throw new IllegalArgumentException("Member 가 아닙니다.");
        }

        Member member = new Member();

        member.readArticle();
        member.createArticle();
    }
}

public class AdminActuator implements UserActuator {
    @Override
    public void describeActions(User user) {
        if (!(user instanceof Admin)) {
            throw new IllegalArgumentException("Admin 이 아닙니다.");
        }
        Admin admin = (Admin) user;

        admin.readArticle();
        admin.createArticle();
        admin.deleteArticle();
    }
}

 

  • GuestActuator
  • MemberActuator
  • AdminActuator

 

총 3가지의 Actuator 로 User 를 매개변수로 받아서 해당 User 객체가 진짜 Actuator 가 원하는 타입인지 먼저 확인하는 코드가 존재한다.

 

이 과정에서 코드의 냄새가 나기 시작한다.

 

특정 Actuator 입장에서는 왜 계속해서 User 가 올바른지 확인해야 할까?

 

Actuator는 애초에 User를 받을 때 타입에 맞는 User 만 받으면 되지 않을까?

 

일단 문제점이라고 생각되는 부분을 찾았으니 마지막 남은 ActuatorContainer 를 구현해주자.

 

public class ActuatorContainer implements UserActuator {
    private final Map<UserType, UserActuator> map;

    public ActuatorContainer() {
        this.map = new HashMap<>();

        map.put(UserType.GUEST, new GuestActuator());
        map.put(UserType.MEMBER, new MemberActuator());
        map.put(UserType.ADMIN, new AdminActuator());
    }


    @Override
    public void describeActions(User user) {
        UserType type = user.getType();
        UserActuator userActuator = map.get(type);

        userActuator.describeActions(user);
    }
}

ActuatorContainer 는 합성을 이용해서 구현하였다.

 

앞서 지적했던 문제를 한 번 해결해보자

 

Generic 을 이용하여 우아하게 제거하기

 

앞에서 지적한 문제가 뭐였는가?

 

다시 정확히 정의하자면 이것이다.

 

Actuator 는 컴파일 시점에 올바르지 않은 타입의 User 가 들어오는 것을 제한한다

 

이를 위해서 Generic 을 이용할 수 있다.

 

UserActuator 에서 제네릭 사용하기

// Actuator
public interface UserActuator <T extends User> {
    void describeActions(T user);
}

 

우선 Actuator 에서 User 타입만 받는다는 것을 명확히 하기 위해 Generic Expression 을 이용한다.

 

User 를 상속한 T 만 받도록 하자

 

각각의 Actuator 구현체에 타입 지정하기

 

그리고 각각의 Actuator 에서 구현체의 타입을 제너릭으로 지정해주자.

 

// Guest Actuator
public class GuestActuator implements UserActuator<Guest> {
    @Override
    public void describeActions(Guest user) {
        user.readArticle();
    }
}

// Member Actuator
public class MemberActuator implements UserActuator<Member> {

    @Override
    public void describeActions(Member user) {
        user.readArticle();
        user.createArticle();
    }
}

// Admin Actuator
public class AdminActuator implements UserActuator<Admin> {
    @Override
    public void describeActions(Admin user) {
        user.readArticle();
        user.createArticle();
        user.deleteArticle();
    }
}

 

그럼 어떤가?

 

앞서 보았던 if/else 구문을 깔끔하게 없앨 수 있다.

 

그럼에도 존재하는 문제점

 

하지만 위의 방식에는 한가지 큰 문제점이 존재한다.

 

바로, Raw use of parameterized class 이다.

 

코드로 봐보자

 

public class ActuatorContainer implements UserActuator {
    private final Map<UserType, UserActuator> map;

    public ActuatorContainer() {
        this.map = new HashMap<>();

        map.put(UserType.GUEST, new GuestActuator());
        map.put(UserType.MEMBER, new MemberActuator());
        map.put(UserType.ADMIN, new AdminActuator());
    }


    @Override
    public void describeActions(User user) {
        UserType type = user.getType();
        UserActuator userActuator = map.get(type);

        userActuator.describeActions(user);
    }
}

 

위 코드에서 보면 2번째 라인에서 map 으로 container 를 생성할 때, Raw Type 의 UserActuator 가 들어가게 되는 것이다.

 

이 말은 Generic Expression 을 사용한 클래스를 호출할 때, 명확한 타입을 지정해주지 않아서 발생하는 문제이다.

 

그럼 타입을 지정해주면 되지 않느냐? 라고 할 수 있지만 타입을 지정해버리는 순간 특정 구현에 결정되어 버린다.

 

즉, UserActuator 의 제너릭을 UserType.MEMBER 로 지정하는 순간 map.put(UserType.GUEST, new GuestActuator()) 에서 컴파일 에러가 발생하게 된다.

 

끝으로

 

아직 마지막 문제를 해결하지 못하고 이와 비슷한 상황에서는 Raw Type 을 사용해버리고 있다.

 

추후에 이와 관련하여 더 자료를 찾아보고 고민해본 뒤 글을 완성해보겠다.

 

... TBD

댓글0