Enum아 Enum아, 너만 말고 너의 값도 유니크해 다오 Java

Enum아 Enum아, 너만 말고 너의 값도 유니크해 다오 Java

Enum의 하위 필드의 유니크 필터

🇰🇷 Kor: 지금 보는 중!
🗺 Eng: Not yet!
🇯🇵 Jap: まだです。


🪖 "enum은 개인주의야."

자바에서 enum은 열거 타입이라고 해서, 여러 선택지를 나열한 특별한 클래스 유형이죠. 그래서 기본적으로 서로 구분되는 유니크 상태입니다. 서로 구분되는 상태나 역할 등을 표현하는 데에 좋습니다.

public enum AccountStatus {
    PENDING,
    ACTIVE,
    PROTECTED,
    SUSPENDED,
    SLEPT,
    REMOVED
}

약간 더 구체적인 작성 양식은 이렇죠. 아마 이 글을 보시는 분들은 보통 아실 것 같습니다!

public enum 클래스_식별자 { // PascalCase
    열거상수(생성자가 없다면 소괄호 생략), // UPPER_CASE
    열거상수(생성자가 없다면 소괄호 생략),
    ...
    세미콜론이 나올 때까지 콤마로 구분하여 열거상수 나열
    ... ;

    // 세미콜론 이후로 일반 클래스처럼 필드 등을 작성합니다.

    // 생성자는 외부에서 사용할 수 없습니다.
}

필드를 준 예시는 이렇겠죠. (@RequiredArgsConstructor는 생성자를 만들어 주는 롬복 애노테이션)

@RequiredArgsConstructor
public enum AccountStatus {
    PENDING(false),
    ACTIVE(true),
    PROTECTED(true),
    SUSPENDED(true),
    SLEPT(true),
    REMOVED(false);

    private final Boolean canSignIn;

    public Boolean canSignIn() {
        return canSignIn;
    }
}

일반 클래스와 다르게 생성자가 static statements 블록보다 장유유서합니다.

자바의 클래스에는 클래스 로드 타임에 동작하는 정적 블록이 있습니다.

// 이렇게 생겼고요, 아는 분들은 다 아는데 모르는 분들은 말 안 해주면 모르는 그런 겁니다.
static {
    // 여기에 실행문 쓰면 클래스가 처음 쓰일 때 딱 한 번 실행됩니다.
    System.out.println("클래스가 메모리에 올라갈 때 실행되는 공간이오.");
}

클래스 로드 타임에 동작하니까, 동작 시점은 이렇게 되는 게 당연했어요:

클래스 "에헴, 메모리에 오르겠노라."
정적 블록 "(나는 이때 수행되어야 하는, 클래스 소유의 실행문이오.) 두둥!"

enum도 클래스니까 정적 블록이 있는데, enum 클래스는 나열된 상수가 없으면 완성될 수 없기 때문에, 열거 상수를 만드는 각 '생성자'가 'static statements block'보다 먼저 실행됩니다.

클래스(enum) "에헴, 메모리에 오르겠노라."
열거상수들 "나도 나도 나 돋 나도나도도도 나도. 우리 없이 너네가 뭐 돼?" (생성자)
정적 블록 "선배님들 먼저 오르시지 말입니다..."

그래서 정적 블록이 실행될 때 이미 열거 상수는 생성되어 있어요. 물론 필드도요. 그래서 정적 블록에서 필드를 확인할 수 있다는 특징이 있습니다.

필드에 대한 필터는 static 블록에서 작성하면 됩니다:

static {
    for (var item : values()) {
        // 각 열거 항목(열거 상수)의 필드를 확인할 수 있습니다.
        System.out.println("선배님들 딱 대시지 말입니다: " + item.field);
    }
}

이제 유니크 필터를 간단히 씌워 봅시다.


Enum의 하위 필드의 유니크 필터

static 블럭을 사용한다는 점이 특징일 뿐, 그 내부에서 구현은 거창한 것 없이 그냥 유니크하게 관리하면 됩니다. 간단하게는 Set 타입을 사용해 중복을 체크할 수 있겠죠.

자바가 원래 클래스로드 타임은 동시성을 보장하니까 Concurrent 계열(ex: ConcurrentSkipListSet<...>)을 안 써도 됩니다.

public enum Bits {
    MaskA(1),
    MaskB(1 << 1),
    MaskC(1 << 2),
    MaskD(1 << 3),
    MaskE(1 << 4),
    MaskF(1 << 5);
    MaskG(1 << 6);

    // Here
    static {
        Set<Integer> valueSet = new HashSet<>();

        // values()에서 모든 열거 상수를 가져옵니다.
        for (var enumItem : values()) {
            // Set에서는 add 결과가 boolean이죠. (값이 잘 추가되면 true)
            if (!valueSet.add(enumItem.value)) {
                // 예시에서는 assert 키워드보단 Error를 던지는 걸 택했습니다.
                throw new Error("중복되는 값이 없어야 합니다.");
            }
        }
    }// static

    private final int value;

    Sample(int value) {
        this.value = value;
    }

    public int value() {
        return value;
    }
}

Set 타입 데이터에 중복된 값이 add 되면 add 결과가 false로 나오면서, 이를 반전한 위 조건식(!valueSet.add(...))은 true가 됩니다.

즉, 중복된 값을 넣는 경우 에러를 throw합니다. 이를 통해 중복 여부를 코드로 검증할 수 있습니다. 이 과정이 런타임에 생략되기를 원한다면 함수로 만들어 assert 함수() : "중복되는 값이 없어야 합니다."; 등으로 바꿀 수도 있죠.