JAVA의 가장 기본이 되는 내용

Enum

cwchoiit 2024. 4. 3. 15:48
728x90
반응형
SMALL

자바는 열거형(Enum Type)을 제공하는데, 이 열거형을 제대로 이해하기 위해서 열거형이 생겨난 이유를 알아보자.

다음과 같은 요구사항이 들어왔다고 생각해보자.

 

등급별 할인율 적용

  • BASIC 등급 - 10%
  • GOLD 등급 - 20%
  • DIAMOND 등급 - 30%

이 요구사항을 처리하기 위해 다음과 같은 코드를 작성했다.

 public class DiscountService {
     public int discount(String grade, int price) {
        int discountPercent = 0;
        if (grade.equals("BASIC")) {
            discountPercent = 10;
        } else if (grade.equals("GOLD")) {
            discountPercent = 20;
        } else if (grade.equals("DIAMOND")) {
            discountPercent = 30;
        } else {
        System.out.println(grade + ": 할인X");
        }
        return price * discountPercent / 100;
    }
}

 

그래서 파라미터로 등급과 금액을 받아서 등급으로 들어온 문자열이 BASIC, GOLD, DIAMOND 중 어느것이냐에 따라 할인율을 적용한 후 할인 금액을 반환한다.

 

그리고 이 메서드를 사용하는 코드를 다음과 같이 작성했다.

public class StringGradeEx0_1 {
    public static void main(String[] args) {
        int price = 10000;
        
        DiscountService discountService = new DiscountService();
        int basic = discountService.discount("BASIC", price);
        int gold = discountService.discount("GOLD", price);
        int diamond = discountService.discount("DIAMOND", price);
		
        System.out.println("BASIC 등급의 할인 금액: " + basic);
        System.out.println("GOLD 등급의 할인 금액: " + gold);
        System.out.println("DIAMOND 등급의 할인 금액: " + diamond);
	}
}

 

실행결과:

BASIC 등급의 할인 금액: 1000 
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000

 

문제없이 잘 동작하는 것 같지만, 이 코드는 아주 많은 잠재적 버그를 내포하고 있다.

우선, 등급을 넣는 파라미터에 잘못된 값이 들어가도 문자열이기 때문에 어떠한 컴파일 에러도 발생하지 않는다.

 

예를 들어, 다음 코드같이 작성해도 컴파일 단계에서 에러가 발생하지 않는다.

int vip = discountService.discount("VIP", price);
System.out.println("VIP 등급의 할인 금액: " + vip);

 

VIP라는 등급은 현재 없다. 그럼에도 불구하고 등급 파라미터에 넣을 수 있다.

또는 오타가 발생할 수도 있다. "GOLD"를 "GOLDD"라고 작성해도 아무런 문제가 발생하지 않는다.

또는 소문자로 입력을 하면 등급을 찾지 못한다. 

 

즉, 문자열을 사용하는 이 방식은 다음과 같은 문제가 있다.

  • 타입 안정성 부족: 어떠한 문자열이 들어가도 입력이 가능하게 되어있다.
  • 데이터 일관성: GOLD를 gold라고 작성하면 골드 등급임에도 할인 적용을 받지 못한다.

 

그래서 이러한 문제를 해결해보기 위해 다음과 같이 나름의 고민끝에 코드가 작성됐다. 등급을 클래스 내 상수로 만들어 두고 이 상수를 사용해보는 것이다.

public class StringGrade {
     public static final String BASIC = "BASIC";
     public static final String GOLD = "GOLD";
     public static final String DIAMOND = "DIAMOND";
}

 

 public class DiscountService {
     public int discount(String grade, int price) {
         int discountPercent = 0;
         if (grade.equals(StringGrade.BASIC)) {
             discountPercent = 10;
         } else if (grade.equals(StringGrade.GOLD)) {
             discountPercent = 20;
         } else if (grade.equals(StringGrade.DIAMOND)) {
             discountPercent = 30;
        } else {
        System.out.println(grade + ": 할인X");
        }
         return price * discountPercent / 100;
     }
}

 

public class StringGradeEx1_1 {
     public static void main(String[] args) {
     	int price = 10000;
     	
        DiscountService discountService = new DiscountService();
     	int basic = discountService.discount(StringGrade.BASIC, price);
     	int gold = discountService.discount(StringGrade.GOLD, price);
     	int diamond = discountService.discount(StringGrade.DIAMOND, price);
        
        System.out.println("BASIC 등급의 할인 금액: " + basic);
        System.out.println("GOLD 등급의 할인 금액: " + gold);
        System.out.println("DIAMOND 등급의 할인 금액: " + diamond);
	} 
}

 

이렇게 사용을 하니, 오타를 방지할 수 있다. 예를 들면 StringGrade.GOLDD라는 값을 넣으면 컴파일 에러가 발생할 것이다. 왜냐하면 StringGrade 클래스에는 GOLDD라는 필드는 없기 때문이다. 근데 이 또한 문제가 있다. 어떤 문제냐면 여전히 받는 파라미터는 문자열 타입이라는 것이다. 그래서 다음 코드가 여전히 문제가 된다는 것.

int vip = discountService.discount("VIP", price); 
System.out.println("VIP 등급의 할인 금액: " + vip);

 

이러한 문제를 해결하기 위해 Enum이 탄생했다. 

 

열거형 - Enum Type

enum은 enumeration의 줄임말로 열거라는 뜻을 가지고 있다. 어떤 항목들을 나열한다는 의미이다. 다음 코드를 보자.

public enum Grade {
     BASIC, GOLD, DIAMOND
}

 

열거형을 정의할 땐 class 대신 enum이라는 키워드를 사용한다.

이 코드를 클래스로 표현하면 다음 코드와 거의 같다.

public class Grade extends Enum {
 	public static final Grade BASIC = new Grade();
 	public static final Grade GOLD = new Grade();
 	public static final Grade DIAMOND = new Grade();
    
    //private 생성자 추가
 	private Grade() {}
}
  • 열거형(Enum)도 클래스다.
  • 열거형은 자동으로 java.lang.Enum을 상속받는다.
  • 열거형은 외부에서 임의로 생성할 수 없다. (private constructor)

실제로 Enum으로 선언한 BASIC, GOLD, DIAMOND는 각 참조값이 따로 존재하고 모두 Grade라는 클래스의 인스턴스다.

코드로 확인해보자.

public class Main {
    public static void main(String[] args) {
        System.out.println(Grade.BASIC.getClass());
        System.out.println(Grade.GOLD.getClass());
        System.out.println(Grade.DIAMOND.getClass());

        System.out.println(Grade.BASIC);
        System.out.println(Grade.GOLD);
        System.out.println(Grade.DIAMOND);
    }
}

실행결과:

class enums.Grade
class enums.Grade
class enums.Grade
BASIC
GOLD
DIAMOND

 

실행결과를 봤더니 전부 Grade라는 클래스 소속이고, 참조값을 확인해보기 위해 찍은 자기 자신이 문자 그대로가 나왔다. 이 이유는 열거형에서는 toString()을 알아서 본인이 찍히도록 오버라이딩되어 있기 때문이다. 그래서 이 각 열거값들의 참조값을 알아보기 위해 다음과 같이 코드를 수정했다.

public class Main {
    public static void main(String[] args) {
        System.out.println(Grade.BASIC.getClass());
        System.out.println(Grade.GOLD.getClass());
        System.out.println(Grade.DIAMOND.getClass());


        System.out.println(Integer.toHexString(System.identityHashCode(Grade.BASIC)));
        System.out.println(Integer.toHexString(System.identityHashCode(Grade.GOLD)));
        System.out.println(Integer.toHexString(System.identityHashCode(Grade.DIAMOND)));
    }
}

실행결과:

class enums.Grade
class enums.Grade
class enums.Grade
30f39991
452b3a41
4a574795

 

결과를 확인했더니 서로 다른 참조값을 가지는 것을 알 수 있다. 그래서 이 열거형으로 위에서 겪었던 문제를 해결해보자. 

등급을 받는 파라미터의 타입을 Grade라는 열거형으로 변경하여 해당 열거형에 존재하지 않는 값 자체를 받지 못하게 설정한다.

public class DiscountService {
    public int discount(Grade grade, int price) {
        int discountPercent = 0;

        if (grade == Grade.BASIC) {
            discountPercent = 10;
        } else if (grade == Grade.GOLD) {
            discountPercent = 20;
        } else if (grade == Grade.DIAMOND) {
            discountPercent = 30;
        } else {
            System.out.println("할인X");
        }
        return price * discountPercent / 100;
    }
}

 

사용하는 코드에서는 이 Grade라는 타입을 받는 파라미터를 입력하는 부분에서 Grade에 속한 값 외에 어떤 값도 넣지 못한다. 

Grade라는 열거형 클래스의 인스턴스도 만들지 못한다. 생성자가 외부에서 접근하지 못하도록 private으로 선언되어 있기 때문이다.

public class Main {
    public static void main(String[] args) {
        int price = 10000;
        
        DiscountService discountService = new DiscountService();
        int basic = discountService.discount(Grade.BASIC, price);
        int gold = discountService.discount(Grade.GOLD, price);
        int diamond = discountService.discount(Grade.DIAMOND, price);
        
        System.out.println("BASIC 등급의 할인 금액: " + basic);
        System.out.println("GOLD 등급의 할인 금액: " + gold); 
        System.out.println("DIAMOND 등급의 할인 금액: " + diamond);
    }
}

 

열거형으로 인한 다음 두 가지 장점이 생겼다

  • 타입 안정성 향상: 열거형으로 사전에 정의된 상수들만으로 구성되어 유효하지 않은 값이 입력될 가능성은 없다. 이럴 경우 컴파일 에러가 발생한다.
  • 간결성 및 일관성: 열거형을 사용하면 코드가 더 간결해지고 명확해진다.
  • 확장성: 새로운 회원등급을 추가하고 싶을 때 열거형에 상수하나만 추가해주면 된다.

위 코드를 조금 더 간단하게 바꿀 수도 있다. static import를 하면 조금 더 간결해진다. 실무에서 많이 사용하는 방식이다. 

import static enums.Grade.*;

public class Main {
    public static void main(String[] args) {
        int price = 10000;

        DiscountService discountService = new DiscountService();
        int basic = discountService.discount(BASIC, price);
        int gold = discountService.discount(GOLD, price);
        int diamond = discountService.discount(DIAMOND, price);

        System.out.println("BASIC 등급의 할인 금액: " + basic);
        System.out.println("GOLD 등급의 할인 금액: " + gold);
        System.out.println("DIAMOND 등급의 할인 금액: " + diamond);
    }
}

 

열거형의 주요 메서드

열거형도 클래스다. 클래스라서 메서드가 있다. 대표적인 메서드들은 다음과 같다.

  • values(): 모든 ENUM 상수를 포함하는 배열을 반환한다.
  • valueOf(String name): 주어진 이름과 일치하는 ENUM 상수를 반환한다.
  • name(): ENUM 상수의 이름을 문자열로 반환한다.
  • ordinal(): ENUM 상수의 선언 순서(0부터 시작)를 반환한다. 
  • toString(): ENUM 상수의 이름을 문자열로 반환한다. name() 메서드와 유사하지만 toString()은 직접 오버라이딩할 수 있다.

근데, ordinal()은 가급적 사용하면 안된다. 왜냐하면 ENUM을 만들고 추후에 추가적으로 새로운 상수를 중간에 추가하고 싶어질 때가 있을 수 있다. 다음과 같이 말이다.

public enum Grade {
    BASIC, SILVER, GOLD, DIAMOND
}

그럼 기존에는 BASIC(0), GOLD(1), DIAMOND(2)였던게 BASIC(0), SILVER(1), GOLD(2), DIAMOND(3)이 되어버린다. 기존에 ordinal()을 사용해서 코드를 작성했다면 끔찍한 일이 벌어질 것이다.

 

 

열거형 - 리팩토링

기존에 서비스 코드를 다시 한번 보자.

public class DiscountService {
    public int discount(Grade grade, int price) {
        int discountPercent = 0;

        if (grade == Grade.BASIC) {
            discountPercent = 10;
        } else if (grade == Grade.GOLD) {
            discountPercent = 20;
        } else if (grade == Grade.DIAMOND) {
            discountPercent = 30;
        } else {
            System.out.println("할인X");
        }
        return price * discountPercent / 100;
    }
}

이 코드를 보면 등급에 따라 할인율을 적용하고 있다. 그럼 결국 할인율이란 것은 등급에 극도로 의존하고 있다. 그럼 할인율을 계산하는 코드는 사실 ENUM 클래스 내부에 있어도 될 것 같다. 그게 바로 객체 지향이니까. 그리고 등급에 따라 할인율이 고정적으로 정해져 있기 때문에 아예 등급을 선언할 때부터 해당 등급의 할인율을 필드로 가지고 있으면 더 좋을 것 같다. 다시 한번 말하지만 ENUM도 클래스다.

 

그래서 이 코드를

public enum Grade {
    BASIC, GOLD, DIAMOND
}

 

다음과 같이 변경했다. (참고로 상수외에 다른게 있으면 저렇게 상수가 끝나는 지점에 세미콜론(;)이 있어야 한다.)

public enum Grade {
    BASIC(10),
    GOLD(20),
    DIAMOND(30);

    private final int discountPercent;

    Grade(int discountPercent) {
        this.discountPercent = discountPercent;
    }

    public int getDiscountPercent() {
        return discountPercent;
    }

    public int discount(int price) {
        return price * discountPercent / 100;
    }
}

 

각 등급별 discountPercent 필드에 10, 20, 30을 지정해준다. 그리고 이 클래스 안에서 할인가격을 구하는 기능(메서드)을 만들면 된다.

그럼 DiscountService가 이렇게 간단해진다.

public class DiscountService {
    public int discount(Grade grade, int price) {
        return grade.discount(price);
    }
}

 

그럼 결국 DiscountService가 하는것은 위임밖에 없다. 이 말은 이 DiscountService 자체가 없어도 된다는 뜻이다. 날려버리고 메인 메서드를 이렇게 만들어보자. 너무나 깔끔하다.

public class Main {
    public static void main(String[] args) {
        int price = 10000;

        int basic = BASIC.discount(price);
        int gold = GOLD.discount(price);
        int diamond = DIAMOND.discount(price);

        System.out.println("BASIC 등급의 할인 금액: " + basic);
        System.out.println("GOLD 등급의 할인 금액: " + gold);
        System.out.println("DIAMOND 등급의 할인 금액: " + diamond);
    }
}

 

 

정리

열거형을 사용해서 다음과 같은 문제를 해결할 수 있다.

  • 타입 안정성 문제
  • 데이터의 일관성

열거형의 특징은 다음과 같다.

  • 열거형도 클래스이다.
  • 클래스란 것은 필드기능이 존재할 수 있다는 뜻이다.
  • 외부에서 생성하지 못한다 (생성자가 private)

 

728x90
반응형
LIST

'JAVA의 가장 기본이 되는 내용' 카테고리의 다른 글

Stream API  (2) 2024.04.07
날짜와 시간  (0) 2024.04.04
Class 클래스  (0) 2024.04.03
Wrapper Class  (0) 2024.04.02
Method Chaining  (0) 2024.04.02