[Java] 양방향 참조 Enum 초기화 순서에 따른 문제 (순환 참조)
- 취급 대상 품목(Good)은 STONE, ALCOHOL, COMPUTER, SHIP, SUSHI 5가지 이고, 이 중 일부는 목적지(Destination) SEOUL, 일부는 BUSAN으로 보내야 한다.
- 그리고 목적지에 따라, 해당 목적지로 보내는 품목 리스트를 구할 수 있어야 한다.
- 그러면 아래와 같이 구현 할 수 있는데...
@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);
}
}
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이 먼저 초기화 되느냐에 따라서 초기화 문제가 발생 할 수도, 아닐 수도 있다.
- 최초 접근 시 초기화되므로... JVM 관련
- Good이 초기화 되기 위해서 field인 Destination이 초기화 되어야 하는데 Destination 내부에서 ``java Good.values()``를 사용하고 있다. (순환 참조)
- ``java Good.values()``는 Good의 모든 Enum 상수가 초기화 되어야만 결과 반환이 가능하기 때문에 호출되는 시점에는 정상적으로 결과 반환이 불가능하다. => 호출 즉시 NPE 발생한다.
참고 차 values() 구현을 찾아보려 했으나, 이는 컴파일러가 추가해주는 코드임. (stackoverflow)
어떻게 해결 할 수 있을까?
방법 1. Desitnation(Good), Good(Destination) 양쪽 참조를 모두 관리한다 -- X
@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]
}
}
- 이 방법은 여전히 양방향 참조이기 때문에 초기화 순서에 따라 결과가 제대로 반환 될 수도, 아닐 수도 있다.
- 양방향 참조 문제는 Enum의 생성자에서 서로를 참조하고 있다는 것에서 기인하므로 상대방을 초기화 하는 부분을 static block으로 옮기면 해결 할 수 있다.
방법 1.을 static 사용해 동작하게끔 만든다면
@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
- ``java SEOUL.getGoods()``는 가능하겠지만, ``java STONE.getDestination()``은 구하기 까다로워진다.
- Destination을 돌면서 STONE이 어디에 포함 되어 있는지 확인해야 한다. 게다가 잘못 추가해서 1개 이상의 Destination이 반환 될 수도 있다.
방법 3. getGoods를 Good의 static 메서드로 -- O
@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));
}
}
- ``java Good.getGoodsOf(Destination.SEOUL)`` 형태로 호출. getGoodsOf 기능이 Good에 있는 것이 맞는가?에 대해 의견이 갈릴 수 있지만 나쁘지 않은 방법이다.
방법 4. 미리 static 변수로 초기화 해두지 말고, getGoods() 메서드가 호출 될 때 마다 evaluation하여 결과 반환 -- O
@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를 만들어야 해서 성능에서 약간 불리하다는 점만 빼면 괜찮은 해결책이다. (사실 이 정도 케이스에서 성능으로 인한 문제는 무시해도 될 수준이다.)
'Java Stack > Java' 카테고리의 다른 글
[Java] Jackson 프로퍼티명 snake_case <-> camelCase 변환 (0) | 2021.08.01 |
---|---|
[Java] HmacUtils, Mac이 thread-safe하지 않다? (0) | 2021.03.17 |
[Java] Collection 초기화 (0) | 2020.11.02 |
[Java] Enum to Json / Enum to Object (0) | 2020.04.27 |
[Spring] resources 경로 문제 (0) | 2020.03.18 |