⚠️운영 환경에서 Java enum의 상태 변경으로 인한 문제 발생
Java에서 enum을 사용하여 미리 메시지를 설정해두고, 해당 메시지가 동적으로 변경되는 로직이 있었다. 그러나 운영 환경에서 간헐적으로 이상한 메시지가 내려가는 문제가 발생하였다.
👌문제찾기
Issue에 사용자가 이상한 오류 메시지를 받는 문제가 있었다.
간헐적으로 비정상적인 Validation 오류 메시지가 사용자에게 반환되었다.
ex) 비밀번호를 입력해주세요 -> 잘못된 아이디 입니다
처음에는 해당 메시지가 어디서 오는지 찾고 있었지만, 외부 API에서도 코드에서도 해당 메시지를 발견할 수 없었다. 하지만 DB에 특정 오류 상황에 해당 메시지가 저장되는걸 발견했지만, 해당 API에서 메시지가 변경되는걸 확인할 수 없었다.
그렇다면 원인은 뭐였을까?
enum Message {
GREETING("안녕하세요!");
private String text;
Message(String s) {
}
public void setText(String text) {
this.text = text;
}
public String getText() {
return text;
}
}
위와 같이 enum에 setter 메서드를 추가하여 메시지를 변경할 수 있도록 설계가 되어 있었다.
특정 요청에서 Message.GREETING.setText("안녕하세요, 사용자님!");와 같이 메시지를 변경하면
다른 API에서 동일한 enum 인스턴스를 참조하기 때문에 변경된 메시지를 보게 된다.
💣 enum 테스트
package com.mntdev.chatgpt;
public class EnumStateTest {
enum Message {
VALIDATION_ERROR("비밀번호를 입력해주세요");
private String text;
Message(String text) {
this.text = text;
}
public void setText(String text) {
this.text = text;
}
public String getText() {
return text;
}
}
public static void main(String[] args) {
// 메시지를 변경하는 스레드 (Writer)
Thread writerThread = new Thread(() -> {
try {
// 3초 간격으로 메시지 변경
for (int i = 1; i <= 3; i++) {
String newMessage = "잘못된 아이디입니다. 변경 횟수: " + i;
System.out.println("[Writer] 메시지 변경: " + newMessage);
Message.VALIDATION_ERROR.setText(newMessage);
Thread.sleep(3000); // 3초 대기
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("[Writer] 스레드 인터럽트됨");
}
});
// 메시지를 읽는 스레드 (Reader)
Thread readerThread = new Thread(() -> {
try {
// 1초 간격으로 메시지 읽기
for (int i = 1; i <= 10; i++) {
String currentMessage = Message.VALIDATION_ERROR.getText();
System.out.println("[Reader] 현재 메시지: " + currentMessage);
Thread.sleep(1000); // 1초 대기
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("[Reader] 스레드 인터럽트됨");
}
});
// 두 스레드 시작
writerThread.start();
readerThread.start();
// 메인 스레드에서 두 스레드가 종료될 때까지 기다림
try {
writerThread.join();
readerThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("[Main] 스레드 인터럽트됨");
}
}
}
✅ 문제해결
문제의 근본 원인은 enum의 인스턴스가 애플리케이션 전체에서 단 하나만 존재하는 싱글턴(Singleton) 패턴으로 동작하기 때문이다. 따라서 enum에 setter를 추가하여 상태를 변경할 경우, 이는 모든 사용자가 공유하는 상태를 변경하게 되어 위와 같은 문제가 발생한다.
📈운영 환경에서의 영향
enum에 setter를 사용하여 상태를 변경한 결과, 운영 환경에서는 다음과 같은 문제가 발생하였다:
- 상태 공유로 인한 데이터 불일치: 한 사용자의 요청으로 enum 상태가 변경되면, 다른 사용자의 요청에서도 동일한 변경된 상태를 참조하게 되어 데이터 불일치가 발생하였다.
- 멀티스레드 환경에서의 레이스 컨디션: 여러 스레드가 동시에 enum의 상태를 변경하려고 할 때, 적절한 동기화가 이루어지지 않아 레이스 컨디션 문제가 발생할 수 있었다.
- 디버깅 어려움: enum의 상태가 동적으로 변경되면서 버그를 추적하고 재현하는 과정이 복잡해졌다.
🔧권장 사항
Java enum은 본질적으로 불변(immutable) 객체으로 설계하는 것이 바람직하다. enum에 setter를 추가하여 상태를 변경하는 것은 여러 가지 문제를 초래할 수 있으므로, 다음과 같은 권장 사항을 따르는 것이 좋다:
- 불변성 유지: enum의 필드를 final로 선언하고, setter 메서드를 추가하지 않는다.
- 상태 관리 분리: 동적인 상태를 enum으로 관리해야 하는지 검토해야 한다.
- 스레드 안전성 보장: 정말 enum을 통해 상태 변경이 필요하다면, 동기화 기법을 적용하여 스레드 안전성을 확보한다.
2023.09.14 - [Study/이펙티브 자바] - [Effective Java 3E] 변경 가능성을 최소화하라
📚참고 자료
💡결론
Java enum은 본질적으로 불변 객체으로 설계되어야 하며, setter 메서드를 통해 상태를 변경하는 것은 여러 가지 심각한 문제를 초래할 수 있다. 특히, 멀티스레드 환경에서 상태가 공유되면서 데이터 불일치 및 레이스 컨디션 문제가 발생할 수 있다. 따라서 enum의 불변성을 유지하고, 동적인 상태 관리는 별도의 방법을 통해 안전하게 처리하는 것이 바람직하다. 단순하게 Enum을 케이스별로 추가하는 방법이 훨씬 더 나을것이다. 정말 동적인 상태 관리가 필요하다면 enum이 최선인지 검토해야 한다.
Tip: enum을 설계할 때는 불변성을 유지하고, 필요한 경우 상태 관리는 별도의 클래스를 통해 안전하게 관리해야 한다.