본문 바로가기
개발 관련됨/개발 이슈를 해결함

Java 에 Enum Circular Dependency 이라는 말을 들어봤나요?

by simplify-len 2023. 12. 16.

문제

어느 날과 다르지 않는 기능 개발 중에 이상한 상황을 마주쳤습니다. 

코드부터 살펴보겠습니다.

enum DisplayType {

    DISPLAY_1("1", ServiceType.SERVICE_A),
    DISPLAY_2("2", ServiceType.SERVICE_A),
    DISPLAY_3("3", ServiceType.SERVICE_B),
    DISPLAY_4("4", ServiceType.SERVICE_B);

    private final String name;
    private final ServiceType serviceType;

    DisplayType(String name, ServiceType serviceType) {
        this.name = name;
        this.serviceType = serviceType;
    }

    public String getName() {
        return name;
    }

    public ServiceType getServiceType() {
        return serviceType;
    }
}
enum ServiceType {

    SERVICE_A("SERVICE_A", Lists.list(DisplayType.DISPLAY_1, DisplayType.DISPLAY_2)),
    SERVICE_B("SERVICE_B", Lists.list(DisplayType.DISPLAY_3, DisplayType.DISPLAY_4));

    private final String name;
    private final List<DisplayType> displayTypes;

    ServiceType(String name, List<DisplayType> displayTypes) {
        this.name = name;
        this.displayTypes = displayTypes;
    }

    public String getName() {
        return name;
    }

    public List<DisplayType> getDisplayTypes() {
        return displayTypes;
    }
}

다음과 같은 테스트 코드가 있습니다.

@Test
void alwaysTrueTest() {
    assertThat(ServiceType.SERVICE_A.getDisplayTypes()).contains(DisplayType.DISPLAY_1);
}

 

 

위 테스트 코드는 통과 될까요?

당연히 통과가 되는거 아니야? 라고 생각하실 수 있는데, 그렇지 않습니다. 이 뒤에 어떤 코드가 있냐에 따라 통과될 수도 있고, 아닐 수도 있습니다. 바로 다음 코드를 볼까요?

@Test
void circle() {
    assertThat(DisplayType.DISPLAY_1.getServiceType().getDisplayTypes())
            .contains(DisplayType.DISPLAY_1, DisplayType.DISPLAY_2);
}

위의 코드는 어떤가요? 통과가 될 것 같은가요?

displayTypes()가 Null을 반환하고, 이 코드는 실패합니다.

circle 테스트 코드와 alwayTrueTest 테스트 코드를 함께 실행하면 실패하고, alwayTrueTest 코드만 실행하면 성공합니다.

왜 이런일이 일어날까요?

컴파일 과정에서 ClassLoader 가 Enum 클래스를 불러오는 과정에 의존하고 있는 다른 Enum 클래스를 호출할 경우 발생합니다.

순서대로 살펴보면 다음과 같습니다. DisplayType

1. DisplayType Enum 클래스를 활용하여 DisplayType.DISPLAY_1.getServiceType().getDisplayTypes() 호출한다면, DisplayType 이 ClassLoader 에 의해 Load 되어집니다.
2. 이때, ClassLoader 는 DisplayType Enum 을 로드하려고하면, ServiceType Enum이 사용되는 것을 확인한 다음 ServiceType 를 로드하려고 시도할 것입니다.
3. ServiceType 초기화 중에 DisplayType도 사용되므로, DisplayType도 초기화해야 하는데, DisplayType 초기화가 진행 중이기 때문에 ServiceType 생성자의 인수가 Null이 됩니다. 
4. 반면에 ServiceType 가 로드되면 DisplayType의 생성자 인수는  null이 아닌 ServiceType값이 됩니다.

2개의 Enum 이 누가 먼저 호출되냐에 따라 Null 값이 나올 수 있습니다.

그럼 어떻게 해결을 해야 될까요?

이미 사용된 프로젝트 코드에도 적용되어 있는 것을 확인했는데, static block 을 활용합니다.

enum ServiceType {

    SERVICE_A("SERVICE_A", Lists.list(DisplayType.DISPLAY_1, DisplayType.DISPLAY_2)),
    SERVICE_B("SERVICE_B", Lists.list(DisplayType.DISPLAY_3, DisplayType.DISPLAY_4));

    private final String name;
    private final List<DisplayType> displayTypes;

    private final static HashMap<String, List<DisplayType>> MAP;
    static {
        HashMap<String, List<DisplayType>> map = new HashMap<>();
        map.put("SERVICE_A", Lists.list(DisplayType.DISPLAY_1, DisplayType.DISPLAY_2));
        map.put("SERVICE_B", Lists.list(DisplayType.DISPLAY_3, DisplayType.DISPLAY_4));

        MAP = map;
    }

    static public List<DisplayType> get(String name) {
        return MAP.get(name);
    }
    ...
}

get을 통해 꺼내오는 방식으로 변경하면, 다음과 같은 테스트 코드로 변경됩니다.

@Test
void circle2() {
    assertThat(ServiceType.get("SERVICE_A"))
            .contains(DisplayType.DISPLAY_1, DisplayType.DISPLAY_2);
}

이 테스트 코드는 통과되게 됩니다. static block 은 인스턴스가 모두 뜨고 난 이후에 실행되기 때문에 의존성의 문제를 회피할 수 있습니다.

결론

하지만 Enum Circular Dependency 가장 큰 문제점은 null 이 반환되는 것을 컴파일 시점에 발견되지 않는다는 것이므로, 이는 가급적 사용하기 않거나, 만약 사용한다면 지연 로딩이 될 수 있도록 static, funcation interface 을 사용하거나 의존성의 방향이 한곳으로만 노출될 수 있도록 해야 합니다.

댓글