Error/트러블슈팅 / / 2024. 12. 10. 20:16

[JAVA] ENUM에 setter를 쓰면 생기는 일 (feat. 싱글턴)

⚠️운영 환경에서 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를 사용하여 상태를 변경한 결과, 운영 환경에서는 다음과 같은 문제가 발생하였다:

  1. 상태 공유로 인한 데이터 불일치: 한 사용자의 요청으로 enum 상태가 변경되면, 다른 사용자의 요청에서도 동일한 변경된 상태를 참조하게 되어 데이터 불일치가 발생하였다.
  2. 멀티스레드 환경에서의 레이스 컨디션: 여러 스레드가 동시에 enum의 상태를 변경하려고 할 때, 적절한 동기화가 이루어지지 않아 레이스 컨디션 문제가 발생할 수 있었다.
  3. 디버깅 어려움: enum의 상태가 동적으로 변경되면서 버그를 추적하고 재현하는 과정이 복잡해졌다.

 

🔧권장 사항

Java enum은 본질적으로 불변(immutable) 객체으로 설계하는 것이 바람직하다. enum에 setter를 추가하여 상태를 변경하는 것은 여러 가지 문제를 초래할 수 있으므로, 다음과 같은 권장 사항을 따르는 것이 좋다:

  1. 불변성 유지: enum의 필드를 final로 선언하고, setter 메서드를 추가하지 않는다.
  2. 상태 관리 분리: 동적인 상태를 enum으로 관리해야 하는지 검토해야 한다.
  3. 스레드 안전성 보장: 정말 enum을 통해 상태 변경이 필요하다면, 동기화 기법을 적용하여 스레드 안전성을 확보한다.

2023.09.14 - [Study/이펙티브 자바] - [Effective Java 3E] 변경 가능성을 최소화하라

 

[Effective Java 3E] 변경 가능성을 최소화하라

💥 개요 불변클래스란 인스턴스 내부 값을 수정할 수 없는 클래스다. 불변 인스턴스에 간직된 정보는 생성된 순간부터 파괴되는 순간까지 절대 달라지지 않습니다. 자바에서 String, 기본 타입의

mntdev.tistory.com

 

📚참고 자료

 

💡결론

Java enum은 본질적으로 불변 객체으로 설계되어야 하며, setter 메서드를 통해 상태를 변경하는 것은 여러 가지 심각한 문제를 초래할 수 있다. 특히, 멀티스레드 환경에서 상태가 공유되면서 데이터 불일치 및 레이스 컨디션 문제가 발생할 수 있다. 따라서 enum의 불변성을 유지하고, 동적인 상태 관리는 별도의 방법을 통해 안전하게 처리하는 것이 바람직하다. 단순하게 Enum을 케이스별로 추가하는 방법이 훨씬 더 나을것이다. 정말 동적인 상태 관리가 필요하다면 enum이 최선인지 검토해야 한다.

Tip: enum을 설계할 때는 불변성을 유지하고, 필요한 경우 상태 관리는 별도의 클래스를 통해 안전하게 관리해야 한다.
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유