Post

(Java) 양방향 참조 Enum 초기화 순서에 따른 문제 (순환 참조)

  • 취급 대상 품목(Good) 은 STONE, ALCOHOL, COMPUTER, SHIP, SUSHI 5가지 이고, 이 중 일부는 목적지(Destination) SEOUL, 일부는 BUSAN으로 보내야 한다.
  • 그리고 목적지에 따라, 해당 목적지 로 보내는 품목 리스트를 구할 수 있어야 한다.
  • 그러면 아래와 같이 구현 할 수 있는데…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Getter
@RequiredArgsConstructor
enum Destination {
    SEOUL,
    BUSAN;
    private static final Map<Destination, List<Good>> goodsByDestination =
        Arrays.stream(values()).collect(Collectors.toMap(
            e -> e,
            e -> Arrays.stream(Good.values())
                .filter(v -> v.getDestination() == e)
                .collect(Collectors.toList())
        ));
    public List<Good> getGoods() {
        return goodsByDestination.get(this);
    }
}
@Getter
@RequiredArgsConstructor
enum Good {
    STONE(Destination.SEOUL),
    ALCOHOL(Destination.SEOUL),
    COMPUTER(Destination.SEOUL),
    SHIP(Destination.BUSAN),
    SUSHI(Destination.BUSAN);
    private final Destination destination;
}
public class SimpleTest {
    @Test
    public void testBidirectionalEnum_order1() {
        System.out.println(Destination.BUSAN);
        System.out.println(Good.ALCOHOL);
        /* 정상 실행 */
    }
    @Test
    public void testBidirectionalEnum_order2() {
        System.out.println(Good.ALCOHOL);  // <-- ExceptionInInitializerError
        System.out.println(Destination.BUSAN);
    }
}
1
2
3
4
5
6
7
8
9
10
java.lang.ExceptionInInitializerError
    at Good.<clinit>(SimpleTest.java:33)
    at SimpleTest.testBidirectionalEnum_order2(SimpleTest.java:52) <22 internal lines>
Caused by: java.lang.NullPointerException
    at Good.values(SimpleTest.java:30)
    at Destination.lambda$static$2(SimpleTest.java:20)
    at java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1321) <6 internal lines>
    at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
    at Destination.<clinit>(SimpleTest.java:18)
    ... 24 more
  • 이런 방식으로 구현하면 어떤 Enum이 먼저 초기화 되느냐에 따라서 초기화 문제가 발생 할 수도, 아닐 수도 있다.
  • Good이 초기화 되기 위해서 field인 Destination이 초기화 되어야 하는데 Destination 내부에서 Good.values()를 사용하고 있다. (순환 참조 )
  • Good.values()는 Good의 모든 Enum 상수가 초기화 되어야만 결과 반환이 가능 하기 때문에 호출되는 시점에는 정상적으로 결과 반환이 불가능하다. => 호출 즉시 NPE 발생한다.

참고 차 values() 구현을 찾아보려 했으나, 이는 컴파일러가 추가해주는 코드임. ( stackoverflow )

어떻게 해결 할 수 있을까?

방법 1. Desitnation(Good), Good(Destination) 양쪽 참조를 모두 관리한다 – X

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Getter
@RequiredArgsConstructor
enum Good {
    STONE(Destination.SEOUL),
    ALCOHOL(Destination.SEOUL),
    COMPUTER(Destination.SEOUL),
    SHIP(Destination.BUSAN),
    SUSHI(Destination.BUSAN);
    private final Destination destination;
}
public class SimpleTest {
    @Test
    public void testBidirectionalEnum_order1() {
        System.out.println(Destination.BUSAN);
        System.out.println(Good.ALCOHOL);
        System.out.println(Destination.BUSAN.getGoods()); // <-- [SHIP, SUSHI]
    }
    @Test
    public void testBidirectionalEnum_order2() {
        System.out.println(Good.ALCOHOL);
        System.out.println(Destination.BUSAN);
        System.out.println(Destination.BUSAN.getGoods()); // <-- [null, null]
    }
}
방법 1.을 static 사용해 동작하게끔 만든다면
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Getter
@RequiredArgsConstructor
enum Destination {
    SEOUL,
    BUSAN;
    private List<Good> goods;
    static {
        SEOUL.goods = Arrays.asList(Good.STONE, Good.ALCOHOL, Good.COMPUTER);
        BUSAN.goods = Arrays.asList(Good.SHIP, Good.SUSHI);
    }
}
@Getter
@RequiredArgsConstructor
enum Good {
    STONE,
    ALCOHOL,
    COMPUTER,
    SHIP,
    SUSHI;
    private Destination destination;
    static {
        STONE.destination = Destination.SEOUL;
        ALCOHOL.destination = Destination.SEOUL;
        COMPUTER.destination = Destination.SEOUL;
        SHIP.destination = Destination.BUSAN;
        SUSHI.destination = Destination.BUSAN;
    }
}
  • 정상적으로 동작은 한다. 그러나 이 방법은 관리 포인트가 둘로 늘어나서 비효율적이다. (Good이 추가 될 때, Destination 에도 신경써서 추가해주지 않으면 누락이나 불일치가 발생한다. 추가 하지 않았을 때 어떠한 경고나 컴파일 에러도 발생하지 않는다.)
  • 게다가 상당히 tricky하다. 코드를 읽는 사람으로 하여금 “왜 이렇게 했을까?” 라는 질문을 유발한다.

방법 2. 반대 쪽 Enum인 Destination에서만 SEOUL(Good.STONE, Good.ALCOHOL, Good.COMPUTER) 형태로 관리한다 – X

  • SEOUL.getGoods()는 가능하겠지만, STONE.getDestination()은 구하기 까다로워진다.
  • Destination을 돌면서 STONE이 어디에 포함 되어 있는지 확인해야 한다. 게다가 잘못 추가해서 1개 이상의 Destination이 반환 될 수도 있다.

방법 3. getGoods를 Good의 static 메서드로 – O

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Getter
@RequiredArgsConstructor
enum Destination {
    SEOUL,
    BUSAN;
}
@Getter
@RequiredArgsConstructor
enum Good {
    STONE(Destination.SEOUL),
    ALCOHOL(Destination.SEOUL),
    COMPUTER(Destination.SEOUL),
    SHIP(Destination.BUSAN),
    SUSHI(Destination.BUSAN);
    private final Destination destination;
    private static final Map<Destination, List<Good>> goodsByDestination =
        Arrays.stream(Destination.values()).collect(Collectors.toMap(
            e -> e,
            e -> Arrays.stream(Good.values())
                .filter(v -> v.getDestination() == e)
                .collect(Collectors.toList())
        ));
    public static List<Good> getGoodsOf(Destination destination) {
        return goodsByDestination.get(destination);
    }
}
public class SimpleTest {
    @Test
    public void testBidirectionalEnum_order1() {
        System.out.println(Destination.BUSAN);
        System.out.println(Good.ALCOHOL);
        System.out.println(Good.getGoodsOf(Destination.BUSAN));
    }
}
  • Good.getGoodsOf(Destination.SEOUL) 형태로 호출. getGoodsOf 기능이 Good에 있는 것이 맞는가?에 대해 의견이 갈릴 수 있지만 나쁘지 않은 방법이다.

방법 4. 미리 static 변수로 초기화 해두지 말고, getGoods() 메서드가 호출 될 때 마다 evaluation하여 결과 반환 – O

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Getter
@RequiredArgsConstructor
enum Destination {
    SEOUL,
    BUSAN;
    public List<Good> getGoods() {
        return Arrays.stream(Good.values())
            .filter(g -> g.getDestination() == this)
            .collect(Collectors.toList());
    }
}
@Getter
@RequiredArgsConstructor
enum Good {
    STONE(Destination.SEOUL),
    ALCOHOL(Destination.SEOUL),
    COMPUTER(Destination.SEOUL),
    SHIP(Destination.BUSAN),
    SUSHI(Destination.BUSAN);
    private final Destination destination;
}
  • 위에서 언급했듯이 양방향 참조 문제는 Enum의 생성자에서 서로를 참조하고 있다는 것에서 기인하므로 생성 시점 이후 호출 할 때 마다 eval 하면 문제가 없다.
  • 호출 될 때 마다 반복문 돌며 List를 만들어야 해서 성능에서 약간 불리하다는 점만 빼면 괜찮은 해결책이다. (사실 이 정도 케이스에서 성능으로 인한 문제는 무시해도 될 수준이다.)
This post is licensed under CC BY 4.0 by the author.