문제
어느 날과 다르지 않는 기능 개발 중에 이상한 상황을 마주쳤습니다.
코드부터 살펴보겠습니다.
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 을 사용하거나 의존성의 방향이 한곳으로만 노출될 수 있도록 해야 합니다.
'개발 관련됨 > 개발 이슈를 해결함' 카테고리의 다른 글
Java Regex 정규표현식 사용시 java.lang.StackOverflowError 가 발생하는걸까 (0) | 2023.02.25 |
---|---|
비관적 잠금, 낙관적 잠금 그런 동시성 이슈 해결하기 (0) | 2023.01.11 |
ConnectionAcquireTimeoutError [SequelizeConnectionAcquireTimeoutError] 문제 해결하기 (0) | 2022.09.11 |
Message Relay 를 PollingPublisher 방식으로 구현하기 (2) | 2022.06.19 |
정규표현식에서 알지 못했던 capture group 과 non- capture group (0) | 2022.06.06 |
댓글