enum, 같은 데이터의 다른 표현

Alex·2024년 3월 17일
0

자바 공부

목록 보기
5/8
post-custom-banner

체스 미션을 enum을 공부하는 시간을 가졌다.

public enum Pieces {

    BLACKPAWN("black", "P"),

    WHITEPAWN("white", "p"),
    NONE("NONE", ".");
    
    //enum의 인스턴스들에 매핑해둔 데이터와 enum간에 연관성을 표현하려고 했다.
    }

enum에 데이터를 맵핑해두면 여러 데이터간의 연관성을 확실하게 보여줄 수 있다는 점을 이번에 배우게 됐다.

우아한형제들의 기술블로그를 보면서 영감을 얻는 것이다.

(출처=https://techblog.woowahan.com/2527/)



위 사진을 보면, "Y가 1이고 true다" "N이 0이고 false다" "기타가 Point고 COUPON이다"라는 것을 쉽게 알 수 있다.

보통 상수를 쓰는 이유는 매직넘버를 없애기 위해다.

즉, 코드를 보는 사람이 숫자 0을 보고 이게 무슨 말이야? 라는 일이 없도록 하기 위해서 상수를 쓰는 것이다.

TOSS라는 문자열만 보고 이게 뭐야? 이게 어떤 결제 종류인데? 라는 의문이 생기지 않도록 "현금"에 맵핑을 해둘 수 있다.

나도 BlackPawn에 데이터를 맵핑해두면 되겠다! 라는 생각을 했다.
그래서, black이라는 색과 "P"라는 데이터를 맵핑했다.

그런데, 머지를 하기 위햐서 pr을 올렸더니 호눅스로부터

"Piece 라는 enum 타입에 black이라는 다른 정보가 포함되어 있습니다. 클래스의 원래 의미와 맞지 않다고 생각합니다"

"저 방식은 같은 데이터의 다른 표현이구요. 지금은 검정과 폰은 직접적인 연관이 없죠. 잘못된 응용이라고 생각해요"

라는 피드백을 받았다. 피드백을 받고서 다시 우아한형제들의 글을 읽어보니, 내가 의도를 잘못 이해했다는 게 느껴졌다.

enum에 맵핑해둔 것들은 "동일한 것(equals관계)"이거나, "추상적인 개념의 구체적인 사례들"이었다.

쉽게 생각하면, 클래스에 인스턴스를 맵핑해둔 것이다.

blackPawn이라는 인스턴스에 속성값인(black)을 설정하는 것은 어울리지 않는다.

그래서 color는 따로 enum으로 빼주었다.

public enum Color {

    WHITE, BLACK, NOCOLOR
}

이번 기회에 enum을 자세하게 공부하기 위해서 여러 자료를 정리해본다.

상태와 행위를 한 곳에서 관리

우아한형제들의 글을 여기서 다시 요약한다(공부하는 차원에서 정리한다)


블로그 글을 보면, 이 코드는 케이스마다 다른 계산식을 적용한다.

예를 들어 DB에 저장된 code의 값이 "CALC_A"일 경우엔 값 그대로, "CALC_B"일 경우엔 10 한 값을, "CALC_C"일 경우엔 3을 계산하여 전달해야만 합니다.

다만, 글쓴이는 "코드는 코드대로 조회하고 계산은 별도의 클래스&메소드를 통해 진행해야함을 알 수 있습니다."라고 이야기한다.

별도로 페이 코드를 조회한 뒤, calulate라는 메서드를 호출하는 두 단계에 걸친 작업이 필요하다는 것이다.

예전에 '단위 테스트'라는 책을 읽었을 때, 캡슐화하려면 일련의 동작이 하나의 메서드 안에서 동작하는 것을 보장해야 한다고 배웠다.

즉, A라는 행위 뒤에 B라는 행위가 무조건 와야한다면,

public doSomething(){
A();
B();
}

이런 식으로 묶어줘야 한다는 것이다.

그렇지 않으면, A를 호출하고 B를 호출하지 않을 수 있고 그 반대의 경우도 될 수 있다.

인퍼페이스 사용방식을 잘 모르거나, 헷갈리면 저지를 수 있는 실수다.

우아한형제들의 기술 블로그에서 언급한 내용이 이 내용과 완전히 일치하지는 않겠지만, enum으로 코드를 한 곳에서 관리하고 캡슐화하면 좋을 것이다.

LegacyCalculator의 메소드와 code는 서로 관계가 있음을 코드로 표현할 수가 없기 때문입니다.뽑아낸 Code에 따라 지정된 메소드에서만 계산되길 원하는데, 현재 상태로는 강제할 수 있는 수단이 없습니다.
지금은 문자열 인자를 받고, long 타입을 리턴하는 모든 메소드를 사용할 수 있는 상태라서 히스토리를 모르는 분(저와 같은^^;)들은 실수할 확률이 높습니다.

public enum CacluatorType {

    CALC_A(value->value),
    CALC_B(value ->value*3),
    CALC_C(value->value*10),
    CALC_D(value->0L);

    private Function<Long, Long> expression;

    CacluatorType(Function<Long, Long> expression) {
        this.expression = expression;
    }

    public long caclulate(long value) {
        return expression.apply(value);
    }
}

    public static void main(String[] args) {

        long value = 20L;
        long result = CacluatorType.CALC_B.caclulate(value);
        System.out.println(result);
    }
    //처음에는 위처럼 코드를 짜봤는데, 이게 왜 코드를 조회하고 계산하는 걸 합쳤다는거지? 하고 이해가 잘 안됐다. 글을 다시 보니
    
     public static void main(String[] args) {

        CacluatorType code = selectCode();
        long value = 20L;
        long result = code.caclulate(value);
        System.out.println(result);
    }
    
    //이렇게 짜는 게 맞을듯하다. 그니까, selectCode자체가 CaclualtorType타입의 코드를 반환하도록 돼어있어서, 거기에 맵핑되는 enum 인스턴스를 활용해 계산까지 같이한다는 것이다.
    

위처럼, 계산식을 맵핑해서 써주면 좋을듯하다.

우아한 형제들의 글에서는 분기점에 대한 이야기도 나온다.

(코드를 각색했다)
    public String cacluateAge(String ageCode){

        if("20".equals(ageCode)||"21".equals(ageCode)||"22".equals(ageCode)){
            return "Agroup";
        } else if("30".equals(ageCode)||"31".equals(ageCode)||"32".equals(ageCode)){
            return "Bgroup";
        } else if("40".equals(ageCode)||"41".equals(ageCode)||"42".equals(ageCode)){
            return "Cgroup";
        } else {

            return "Dgroup";
        }
    }
    
    
       public void print(String group){
        if(group.equals("Agroup")){
            printA();
        } else if(group.equals("Bgroup")){
            pringB();
        } else if(group.equals("Cgroup")){
            printC();
        } else {
            printD();

        } 
        
        //그룹별로 다른 메서드를 쓰는 코드
    
    
    

위와같은 코드는, 분기를 정해주는 방식을 계속 중복해서 만들어야 한다는 불편함이 있다. 당연히 관리가 어렵다. 또한, 입력값으로 String이라면 다 들어갈 수 있어서 입력값을 제한하기도 어렵다.

//우아한 형제들의 기술블로그 글을 보면서 코드를 각색했다.
public enum AgeGroup {

    AGROUP("20대", Arrays.asList("20", "21", "22", "23", "24")),
    BGROUP("30대",Arrays.asList("30", "31", "32", "33", "34")),
    CGROUP("30대",Arrays.asList("40", "41", "42", "43", "44")),
    EMPTY("없음", Collections.emptyList());

    String group;
    List<String> ageList;


    AgeGroup(String group, List<String> ageList) {
        this.group = group;
        this.ageList = ageList;
    }

    static AgeGroup findByAge(String age){
        return Arrays.stream(AgeGroup.values())
                .filter(i->i.hasAge(age)).findAny().orElse(EMPTY);
    }

    boolean hasAge(String age){
        return ageList.stream()
                .anyMatch(i->i.equals(age));

    }
    
        public static void main(String[] args) {
        String ageOfPeople = "21"; //어떤 메서드를 통해서 AGROUP이라는 반환값이 나왔다고 가정하자.
        AgeGroup byAge = AgeGroup.findByAge(ageOfPeople);
    }
}

위처럼 하면 if else 를 범벅으로 쓰지 않고도 분기를 정할 수 있다.

참고로, 예전에 어떤 개발자분이 "enum을 보수적으로 보는 사람이 있다. enum에 왜 메서드를 넣는지를 이해하지 못한다. enum을 단순히 상수의 집합으로 보기 때문이다."라고 말했던 적이 있다.

나도 그래서 위처럼 어떤 메서드를 갖는 것이 조심스러웠는데, 최근 호눅스가 "enum도 클래스다. 메서드를 가질 수 있다."라고 말했던 게 기억이 난다. 클래스의 역할이 상태값을 갖는거과 그 상태값을 조작하는 행동을 갖는거라면, enum에도 이처럼 메서드들이 들어가는 게 맞다고 생각한다!

최종 완성본 같은 느낌이다. 결제수단도 정해진 값만 들어올 수 있도록 enum으로 만들어준 것이다!

이 글을 읽고 enum에 대해서 정말 많은 것을 배울 수 있었다. 시간이 된다면, 원본 글을 꼭 읽어볼 것을 강추한다.

(추가)

향로님 블로그에 있는 enum 정리글은 여기서 다시 요약하면서 공부한다
(출처=https://jojoldu.tistory.com/137)


이 부분에서 인상 깊은 구절이 있어서 적어본다.

이런 경우가 제가 생각하기엔 전형적인 데이터와 로직이 분리된 사례라고 생각합니다.
매출타입별 연산식에 대한 책임은 누가 갖고있어야 할까요?
서비스코드일까요?
각 매출타입이 갖고있어야 하지 않을까요?
A 타입은 a식으로 계산해야하고,
B 타입은 b식으로 계산해야한다라는건
A와 B가 책임져야하는 부분이 아닐까요?

어느 코드에서든 특정 금액에 대해 타입별 계산금이 어떻게 되는지는 이제 그 타입에 직접 물어보면 되는 것입니다.
(향로님 블로그 내용)

이말을 듣고 보니, 계산타입이 연산식에 대한 책임을 져야 한다는 것이 수긍된다.

*여기서는 DB에 대한 내용이 있었는데 사실 내가 아직 DB를 사용할 줄 몰라서 내용이 크게 와닿지는 않았다. 나중에 플젝을 해보면서 다시 참고해보자!

Enum에 대한 추가 정리

우선, 공식 문서에 따르면 enum의 정의는 다음과 같다.

An enum type is a special data type that enables for a variable to be a set of predefined constants.
Because they are constants, the names of an enum type's fields are in uppercase letters.

어렵게 표현했지만, 미리 지정한 상수들을 담는 데이터 타입이란 뜻으로 보인다. 상수처럼 대문자로 쓰는 것이 권장된다.

You should use enum types any time you need to represent a fixed set of constants. That includes natural enum types such as the planets in our solar system and data sets where you know all possible values at compile time—for example, the choices on a menu, command line flags, and so on.

예전에 호눅스가 "enum은 표현하려고 하는 데이터가 정해진 수, 제한적일 때만 사용하는 것이 좋다."라고 말했는데 그 말인 거 같다. 사실 이 부분은 왜 제한된 데이터에 사용하면 좋은건지..잘 이해가 안 되긴 한다. 더 공부해야할 부분!

enum의 이점은 Baeluding의 아티클을 참고했다.

Constants defined this way make the code more readable, allow for compile-time checking, document the list of accepted values upfront, and avoid unexpected behavior due to invalid values being passed in.

enum은 코드의 가독성을 높여주고, 컴파일 타임체크(선언된 enum 인스턴스가 아니면 컴파일 에러가 뜬다)를 해준다.

 public Blank(Color color, String logo, Pieces pieces) {
        this.color = color;
        this.logo = logo;
        this.pieces = pieces;
    }
    
    
   
public enum Color {

    WHITE, BLACK, NOCOLOR
    
}


public enum Pieces {

    PAWN("p", 1.0),
    KNIGHT("n", 2.5),
    ROOK("r", 5.0),
    BISHOP("b", 3.0),
    QUEEN("q", 9.0),
    KING("k", 0.0),
    BLANK(".", 0.0);
    
   }//근데 이 부분에 점수가 들어가도 괜찮을지 아직 잘 모르겠다. 같은 데이터의 다른 타입이라는 게 명확하게 와닿지가 않는다... 더 공부해야할 부분이다.


    public static void main(String[] args) {
        new Blank("blue", "logo","KING")
    }
//우선 BLUE와 KING은 각각 데이터타입이 미리 선언해둔 Color와 Pieces라는 enum이 아닌 String이다. 데이터타입이 다르기 떄문에 컴파일 에러가 뜬다. 

또한, 생성자에 인자를 넣을 떄는 enum을 쓰더라도 미리 선언해둔 WHITE, BLUE, NOCOLOR 혹은 PAWN, KNIGHT, ROOK, BISHOP, QUEEN, KING, BLANK만 쓸 수 있따. 다른 데이터를 넣을 수 없어서 잘못된 데이터가 들어가지 못하도록 방어할 수있다. 

다만, enum값이 들어갈 자리에 null값도 들어갈 수 있어서 null체크는 해주는 게 좋다고 들었다.

Since enum types ensure that only one instance of the constants exist in the JVM, we can safely use the “==” operator to compare two variables, like we did in the above example. Furthermore, the “==” operator provides compile-time and run-time safety.

enum은 ==연산자를 써도 괜찮다고 한다.

        System.out.println(Color.WHITE == Color.WHITE);//true
        System.out.println(Color.WHITE == Color.BLACK);//false

        System.out.println(Color.WHITE.equals(Color.WHITE));//true
        System.out.println(Color.WHITE.equals(Color.BLACK));//false

실제로 ==연산자를 써도 잘 작동했다.

Baeluding 아티클을 보니, equals와 ==의 차이가 있는 거 같다.

public class Pizza {
    private PizzaStatus status;
    public enum PizzaStatus {
        ORDERED,
        READY,
        DELIVERED;
    }

    public boolean isDeliverable() {
        if (getStatus() == PizzaStatus.READY) {
            return true;
        }
        return false;
    }


    public PizzaStatus getStatus() {
        return status;
    }

    public void setStatus(PizzaStatus status) {
        this.status = status;
    }
}
    public static void main(String[] args) {

        Pizza testPz = new Pizza();

        if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED){
            System.out.println("피자가 배달됐습니다.");
        } else{
            System.out.println("피자가 배달되지 않았습니다.");
        }

    }

위 코드를 실행하면 "피자가 배달되지 않았습니다."가 출력되고, NullPointerExcetpion이 발생하지 않는다.

 public static void main(String[] args) {

        Pizza testPz = new Pizza();

        if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED)){
            System.out.println("피자가 배달됐습니다.");
        } else{
            System.out.println("피자가 배달되지 않았습니다.");
        }

    }

위 코드는 NullPointerException이 발생했다. NullPointerException을 피할 때 이 방식을 사용하면 좋을 듯하다.

Baeluding의 아티클에서는 컴파일타입 안정성을 제공한다는 말도 있다.

  public enum PizzaColor{
        GREEN, BLUE
    }
    

    public static void main(String[] args) {

        Pizza testPz = new Pizza();

        testPz.setStatus(PizzaStatus.ORDERED);

        if(testPz.getStatus()==PizzaColor.BLUE){//여기서 데이터타입이 다르다는 컴파일 에러가 떴다.   
        
            System.out.println("피자가 배달됐습니다.");
        } else{
            System.out.println("피자가 배달되지 않았습니다.");
        }

    }
    
    
 public static void main(String[] args) {

        Pizza testPz = new Pizza();

        testPz.setStatus(PizzaStatus.ORDERED);

        if(testPz.getStatus().equals(PizzaColor.BLUE)){
            System.out.println("피자가 배달됐습니다.");
        } else{
            System.out.println("피자가 배달되지 않았습니다.");
        }

    }


위 코드를 실제로 실행해보면 다른 데이터타입이 비교된 것에 대해서 어떤 예외가 뜨지는 않았다. 여러모로, ==을 쓰는 게 더 이점을 주는 거 같다.

Singleton

Enums provide a quick and easy way of implementing singletons. In addition, since the enum class implements the Serializable interface under the hood, the class is guaranteed to be a singleton by the JVM. This is unlike the conventional implementation, where we have to ensure that no new instances are created during deserialization.

예전에 백기선 강사님의 이펙티브 자바 강의에서, 싱글턴을 만들더라도 직렬화 역직렬화과정에서 인스턴스가 하나 더 생긴다는 말을 들었다. 이러한 일이 발생하지 않게 하려면 Enum을 쓰면 된다고 배웠는데 그 말이다.

*https://javarevisited.blogspot.com/2011/08/enum-in-java-example-tutorial.html#axzz8UhnV1tAG

이 글도 추후에 읽어서 정리하자

profile
답을 찾기 위해서 노력하는 사람
post-custom-banner

0개의 댓글