관리 메뉴

나구리의 개발공부기록

열거형 - ENUM, 문자열과 타입 안정성, 타입 안전 열거형 패턴, 열거형(Enum Type, 주요 메서드, 리펙토링) 본문

인프런 - 실전 자바 로드맵/실전 자바 - 중급 1편

열거형 - ENUM, 문자열과 타입 안정성, 타입 안전 열거형 패턴, 열거형(Enum Type, 주요 메서드, 리펙토링)

소소한나구리 2025. 1. 20. 14:38

출처 : 인프런 - 김영한의 실전 자바 - 중급1편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 문자열과 타입 안정성

1) 문자열과 타입 안정성

(1) 예제 요구사항

  • 고객은 3등급으로 나누고 상품 구매시 등급별로 할인을 적용하고 할인 시 소수점 이하는 버림
  • BASIC -> 10% 할인
  • GOLD -> 20% 할인
  • DIAMOND -> 30% 할인

(2) DiscountService

  • discount() 메서드는 매개변수로 넘어오는 등급에 따라 할인율을 적용하고 회원 등급 외의 다른 값이 입력이 되면 "할인X"가 출력되고 적용되는 할인율은 없음
  • 할인 금액을 구하기 위해 가격 * 할인율 / 100으로 연산하고 그 결과를 반환
  • 예제의 단순화를 위해 회원 등급에 null은 입력되지 않는다고 가정
package enumeration.ex0;

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("할인X");
        }

        return price * discountPercent / 100;
    }
}

 

(3) StringGradeEx0_1

  • DiscountService를 호출하여 할인을 적용해보면 회원 등급에 맞게 할인적용이 잘 되었음
  • 그러나 이렇게 단순히 문자열을 입력하는 방식은 오타가 발생하기 쉽고 유효하지 않는 값이 입력될 수 있음
package enumeration.ex0;

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
*/

 

(3) StringGradeEx0_2

  • 다음 예제에서는 존재하지 않는 등급을 입력하거나 오타가 발생하여 애플리케이션은 동작하지만 원하는 비즈니스로직이 구현되지 않았음
  • 등급에 문자열을 사용하는 방식이기 때문에 타입 안정성이 부족하고 데이터의 일관성이 떨어지는 문제가 발생함
  • 타입 안정성 부족: 문자열은 오타가 발생하기 쉽고 유효하지 않은 값이 입력될 수 있음
  • 데이터 일관성: "GOLD", "gold" 등 다양한 형식으로 문자열을 입력할 수 있기 때문에 일관성이 떨어짐
package enumeration.ex0;

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

        DiscountService discountService = new DiscountService();
        int vip = discountService.discount("VIP", price);     // 존재하지 않는 등급
        System.out.println("VIP 등급의 할인 금액: " + vip);

        int diamondd = discountService.discount("DIAMONDD", price);   // 추가로 더 입력(오타)
        System.out.println("DIAMONDD 등급의 할인 금액: " + diamondd);

        int gold = discountService.discount("gold", price);     // 소문자 입력(오타)
        System.out.println("gold 등급의 할인 금액: " + gold);

    }
}
/* 실행 결과
할인X
VIP 등급의 할인 금액: 0
할인X
DIAMONDD 등급의 할인 금액: 0
할인X
gold 등급의 할인 금액: 0
*/

 

(4) String 사용 시 타입 안정성 부족 문제

  • 값의 제한 부족: String으로 상태가 카테고리를 표현하면 잘못된 문자열을 실수로 입력할 가능성이 있음
  • 컴파일 시 오류 감지 불가: 이러한 잘못된 값은 컴파일 시에 감지되지 않고 런타임에서만 문제가 발견되기 때문에 디버깅이 어려워질 수 있음
  • 이런 문제를 해결하려면 특정 범위로 값을 제한하여 discount()메서드에 전달해야 하는데 String은 어떤 문자열이든 받을 수 있기 때문에 자바 문법 관점에서는 아무런 문제가 없으므로 String 타입을 사용해서는 문제를 해결할 수 없음

2) 문자열 상수를 사용

(1) StringGrade

  • 문제를 해결해보기 위해 상수를 사용할 수 있도록 상수를 정의
  • 미리 정의한 변수명을 사용하기 때문에 문자열을 직접 사용하는 것 보다는 안전함
package enumeration.ex1;

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

 

(2) DiscountService - 수정

  • discount()메서드의 if문 조건을 상수와 비교하도록 수정
package enumeration.ex1;

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("할인X");
        }

        return price * discountPercent / 100;
    }
}

 

(3) StringGradeEx1_1

  • discount()메서드를 호출 시 문자열을 직접 입력하는 것이 아니라 StringGrade에 정의된 상수를 인수로 메서드를 호출
  • 실행해보면 정상적으로 모든 할인 금액이 출력되는 것을 확인할 수 있음
  • 문자열 상수를 사용한 덕분에 전체적으로 코드가 명확해졌고 실수로 상수의 이름을 잘못 입력하면 컴파일 시점에 오류가 발생하기 때문에 오류를 쉽고 빠르게 찾을 수 있음
package enumeration.ex1;

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);
    }
}

 

(4) 문제점

  • 그러나 문자열 상수를 사용해도 지금까지 발생한 문제들을 근본적으로 해결할 수는 없음
  • String 타입은 어떤 문자열이든 입력할 수 있으므로 다른 개발자가 discount()메서드를 호출할 때 문자열 상수를 사용하지 않고 직접 문자열을 입력해도 막을 수 있는 방법이 없기 때문에 실제로 상수를 정의했다고해도 메서드는 문자열을 입력할 수 있음
  • 애초에 discount(String grade, int price)메서드의 매개변수 타입이 String이기 때문에 모든 문자열을 입력받도록 설계한 것 자체가 문제의 근본임
  • discount()를 사용하는 개발자는 사용하는 문자열 상수가 어디에있는지 알 수가 없고 주석을 남겨놓는다고해도 못보고 문자열을 입력할 수 있기 때문에 애초에 설계할 때 잘못된 값을 입력하지 못하도록 막아서 설계해야 함

2.타입 안전 열거형 패턴

1) 타입 안전 열거형 패턴 - Type-Safe Enum Pattern

(1) 문제 해결

  • 지금까지 설명한 문제를 해결하기 위해 많은 개발자들이 오랜기간 고민하고 나온 결과가 바로 타입 안전 열거형 패턴임
  • enum은 enumeration의 줄임말인데 번역하면 '열거'라는 뜻으로 어떤 항목을 나열하는 것을 뜻함
  • 여기서 중요한 것은 타입 안전 열거형 패턴을 사용하면 우리가 나열하고자한 항목만 사용할 수 있고 나열하지 않은 항목은 사용할 수 없다는 것이 핵심임
  • String처럼 아무런 문자열이나 다 사용할 수 있는 것이 아니라 제약을 두는 것임

(2) ClassGrade

  • 회원 등급을 다루는 클래스를 만들고 각각의 회원 등급별로 상수를 선언한 뒤 각각의 상수마다 별도의 인스턴스를 생성 및 생성한 인스턴스의 참조값을 대입함
  • 필드를 상수로 선언하기 위해 static으로 메서드 영역에 선언하고 final을 사용하여 참조값을 변경할 수 없도록 작성
package enumeration.ex2;

public class ClassGrade {

    public static final ClassGrade BASIC = new ClassGrade();
    public static final ClassGrade GOLD = new ClassGrade();
    public static final ClassGrade DIAMOND = new ClassGrade();
}

 

(3) ClassRefMain

  • 위에서 선언한 상수를 더 이해하기 쉽게 getClass()로 클래스 정보를 확인해보면 모두 ClassGrade 타입을 기반으로 만들었기 때문에 모두 ClassGrade로 나옴
  • 하지만 각각의 상수는 서로 다른 ClassGrade 인스턴스를 참조하기 때문에 참조값이 모두 다름
  • 상수로 열거한 BASIC, GOLD, DIAMOND는 static 이므로 애플리케이션 로딩 시점에 3개의 ClassGrade 인스턴스가 생성되고 각각의 상수는 ClassGrade 타입의 서로다른 인스턴스의 참조값을 가짐
  • 이제 ClassGrade 타입을 사용할 때는 열거한 상수들만 사용하면 됨
package enumeration.ex2;

public class ClassRefMain {
    public static void main(String[] args) {
        System.out.println("class BASIC = " + ClassGrade.BASIC.getClass());
        System.out.println("class GOLD = " + ClassGrade.GOLD.getClass());
        System.out.println("class DIAMOND = " + ClassGrade.DIAMOND.getClass());

        System.out.println("ref BASIC = " + ClassGrade.BASIC);
        System.out.println("ref GOLD = " + ClassGrade.GOLD);
        System.out.println("ref DIAMOND = " + ClassGrade.DIAMOND);
    }
}
/* 실행 결과
class BASIC = class enumeration.ex2.ClassGrade
class GOLD = class enumeration.ex2.ClassGrade
class DIAMOND = class enumeration.ex2.ClassGrade
ref BASIC = enumeration.ex2.ClassGrade@3f99bd52
ref GOLD = enumeration.ex2.ClassGrade@4f023edb
ref DIAMOND = enumeration.ex2.ClassGrade@3a71f4dd
*/

 

(4) DiscountService - 수정

  • discount() 메서드를 매개변수로 ClassGrade 클래스를 사용하도록 변경
  • 값을 비교할 때는 매개변수에 넘어오는 인수가 ClassGrade가 가진 상수중에 하나를 사용하기 때문에 열거한 상수의 참조값으로 비교하기 위해 == 비교를 사용하면 됨
package enumeration.ex2;

public class DiscountService {
    public int discount(ClassGrade classGrade, int price) {
        int discountPercent = 0;

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

 

(5) ClassGradeEx2_1

  • discount()를 호출할 때 미리 정의한 ClassGrade의 상수를 전달하면 기존과 할인 금액이 정상적으로 출력되는 것을 확인할 수 있음
  • 다른 타입이나 다른 값을 인수로 입력하려고 하면 컴파일 오류가 발생함
package enumeration.ex2;

public class ClassGradeEx2_1 {
    public static void main(String[] args) {
        int price = 10000;
        
        DiscountService discountService = new DiscountService();

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

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

 

(6) ClassGradeEx2_2

  • 하지만 이 방식은 외부에서 임의로 ClassGrade의 인스턴스를 생성할 수 있다는 문제가 존재함
  • 객체의 상수를 사용해야 하지만 이렇게 객체를 생성할 수 있게 설계하면 이를 설계해서 사용하는 개발자에게는 이 클래스를 생성하면 안된다는 것을 알 수 없으므로 지금처럼 잘못 적용하게 될 수 있기 때문에 애초에 설계를 ClassGrade를 생성할 수 없게 설계해야 함
package enumeration.ex2;

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

        DiscountService discountService = new DiscountService();
        ClassGrade newClassGrade = new ClassGrade();
        int result = discountService.discount(newClassGrade, price);
        System.out.println("newClassGrade 등급의 할인 금액: " + result);
    }
}
/* 실행 결과
할인X
newClassGrade 등급의 할인 금액: 0
*/

 

(6) ClassGrade - 수정

  • 기존 ClassGrade 코드에 private 생성자를 추가하여 외부에서 객체를 생성하지 못하도록 막음
  • private 생성자 덕분에 ClassGrade의 인스턴스를 생성하는 것은 ClassGrade 클래스 내부에서만 할 수 있기 때문에 상수에 인스턴스를 생성한 참조값을 저장하는 것은 그대로 유지할 수 있음
  • 이렇게 하면 기존의 ClassGradeEx2_2에서 new ClassGrade()로 인스턴스를 생성하는 코드에 컴파일 오류가 발생하게 되고 개발자가 실수로 인스턴스를 생성할 수 있는 부분을 원천 차단하게 되고 의도한 값만 입력할 수 밖에 없게됨
package enumeration.ex2;

public class ClassGrade {

    // 타입 안전 열거형 패턴 코드는 동일
    
    // private 생성자 추가
    private ClassGrade() {
    }
}

 

(7) 타입 안전 열거형 패턴(Type-Safe Enum Pattern)의 장점과 단점

  • 타입 안정성 향상: 정해진 객체만 사용할 수 있기 때문에 잘못된 값을 입력하는 문제를 근본적으로 방지할 수 있음
  • 데이터 일관성: 정해진 객체만 사용하므로 데이터의 일관성이 보장됨
  • 클래스는 사전에 정의된 몇 개의 인스턴스만 생성하고 외부에서는 이 인스턴스들만 사용할 수 있도록 제한하여 미리 정의된 값들만 사용하도록 보장함
  • 특정 메서드가 특정 열거형 타입의 값을 요구한다면 오직 그 타입의 인스턴스만 전달할 수 있기 때문에 다른 타입이 들어올 수 없어 매우 안전하며 다른 타입이 들어올 경우 컴파일 오류가 발생하여 빠르게 문제를 파악할 수 있게됨
  • 유일한 단점은 코드를 많이 구현해야한다는 것인데 자바가 이를 편리하게 사용할 수 있는 타입을 제공함

3. 열거형

1) Enum Type

(1) Grade - enum

  • 자바는 타입 안전 열거형 패턴을 매우 편리하게 사용할 수 있는 열거형(Enum Type)을 제공함
  • 자바의 enum은 타입 안정성을 제공하고 코드의 가독성을 높이며 예상 가능한 값들의 집합을 표현하는 데 사용됨
  • 열거형을 정의할 때는 class 대신에 enum을 사용하고 원하는 상수의 이름을 나열하기만 하면 되기 때문에 직접 ClassGrade를 구현할 때와 비교가 되지 않을 정도로 편리함
  • 지금 작성한 코드는 앞서 직접 만들었던 ClassGrade과 거의 같은 구조로 되어있으므로 상수 하나하나가 각각의 인스턴스임
  • 열거형도 클래스이며 자동으로 java.lang.Enum을 상속 받고 외부에서 임의로 생성할 수 없음
package enumeration.ex3;

public enum Grade {
    BASIC, GOLD, DIAMOND
}

 

(2) EnumRefMain

  • 실행 결과를 보면 상수들이 열거형으로 선언한 타입인 Grade 타입을 사용하는 것과 각각 다른 인스턴스인 것을 확인할 수 있음
  • 열거형은 toString()을 상수값을 출력하도록 재정의 하기 때문에 참조값을 직접 확인할 수 없으므로 refValue()메서드를 만들어서 참조값을 확인함
  • enum은 열거형을 제공하기 위해 제약이 추가된 클래스라고 생각하면 됨
package enumeration.ex3;

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

        System.out.println("ref BASIC = " + refValue(Grade.BASIC));
        System.out.println("ref GOLD = " + refValue(Grade.GOLD));
        System.out.println("ref DIAMOND = " + refValue(Grade.DIAMOND));
    }

    private static String refValue(Object grade) {
        return Integer.toHexString(System.identityHashCode(grade));
    }
}
/* 실행 결과
class BASIC = class enumeration.ex3.Grade
class GOLD = class enumeration.ex3.Grade
class DIAMOND = class enumeration.ex3.Grade
ref BASIC = 3f99bd52
ref GOLD = 4f023edb
ref DIAMOND = 3a71f4dd
*/

 

(3) 열거형을 사용하여 코드 수정

  • 열거형의 사용법은 앞서 타입 안전 열거형 패턴을 직접 구현한 코드와 똑같은 것을 알 수 있음
  • 참고로 열거형은 switch문에 사용할 수 있는 장점도 있음
  • 열거형은 내부에서 상수로 지정하는 것 외에 직접 생성이 불가능하므로 생성할 경우 컴파일 오류가 발생함
package enumeration.ex3;

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;
    }
}


package enumeration.ex3;

public class EnumEx3_1 {
    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);
    }
}

 

(4) 열거형(ENUM)의 장점 정리

  • 타입 안정성: 열거형은 사전에 정의된 상수들로만 구성되므로 유효하지 않은 값이 입력될 가능성이 없으며 이런 경우에는 컴파일 오류가 발생함
  • 간결성 및 일관성: 열거형을 사용하면 코드가 더 간결하고 명확해지며 데이터의 일관성이 보장됨
  • 확장성: 새로운 회원 등급을 추가하고 싶을 때 ENUM에 새로운 상수를 추가하기만 하면 됨
  • 열거형을 사용하는 경우 static import를 적절하게 사용하면 더 읽기 좋은 코드를 만들 수 있음

2) 주요 메서드

(1) EnumMethodMain

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

public class EnumMethodMain {
    public static void main(String[] args) {

        // 모든 ENUM 반환
        Grade[] values = Grade.values();
        System.out.println("values = " + Arrays.toString(values));

        // 이름과 순서 반환
        for (Grade value : values) {
            System.out.println("name = " + value.name() + ", ordinal = " + value.ordinal());
        }

        // String -> ENUM, 잘못된 문자면 IllegalArgumentException 발생
        String input = "GOLD";
        Grade gold = Grade.valueOf(input);
        System.out.println("gold = " + gold);

    }
}
/* 실행 결과
values = [BASIC, GOLD, DIAMOND]
name = BASIC, ordinal = 0
name = GOLD, ordinal = 1
name = DIAMOND, ordinal = 2
gold = GOLD
*/

 

** 주의

  • ordinal()은 가급적 사용하지 않는 것이 좋은데 이 값을 사용하다가 중간에 상수를 선언하는 위치가 변경되면 전체 상수의 위치가 모두 변경될 수 있기 때문임
  • BASIC, GOLD, DIAMOND의 순서로 ENUM을 만들고 ordinal()값을 데이터베이스나 파일에 저장하고 있다가 중간에 SILVER가 추가되면 데이터베이스나 파일에 있는 값은 그대로 1로 유지되지만 애플리케이션에서는 GOLD는 2가되고 SILVER는 1이 되어버림
  • 즉, ordinal()의 값을 사용하면 기존의 GOLD 회원이 갑자기 SILVER가 되는 큰 버그가 발생할 수 있게 됨

(2) 열거형 정리

  • java.lang.Enum을 자동(강제)으로 상속 받았기 때문에 추가로 다른 클래스를 상속 받을 수 없음
  • 열거형은 인터페이스를 구현할 수 있음
  • 열거형에 추상 메서드를 선언하고 구현할 수 있으며 이 경우 익명 클래스와 같은 방식을 사용하는데, 익명 클래스는 뒤에서 다룸

4. 열거형 리펙토링

1) ex2(열거형 패턴 구현) - 리펙토링

(1) 리펙토링 포인트

  • 지금은 열거형이 익숙하지 않으니 클래스를 사용하여 열거형 패턴을 구현했던 ex2의 코드를 먼저 리펙토링
  • discount()의 너무 많은 if문을 제거하고, 할인율은 각각의 회원 등급별로 판단하므로 ClassGrade가 할인율을 가지고 관리하도록 변경

(2) ClassGrade - 수정

  • discountPercent필드와 조회 메서드를 추가
  • 생성자를 private으로 하여 외부에서 생성하지 못하도록하고 이 생성자를 통해서 내부에서 할인율을 설정되며 할인율이 중간에 값이 변하지 않도록 불변으로 설계
  • 즉 상수를 정의할 때 각각의 등급에 따른 할인율이 정해짐
package enumeration.ref1;

public class ClassGrade {
    
    public static final ClassGrade BASIC = new ClassGrade(10);
    public static final ClassGrade GOLD = new ClassGrade(20);
    public static final ClassGrade DIAMOND = new ClassGrade(30);

    private final int discountPercent;

    private ClassGrade(int discountPercent) {
        this.discountPercent = discountPercent;
    }

    public int getDiscountPercent() {
        return discountPercent;
    }
}

 

(3) DiscountService - 수정

  • 기존에 if문을 통해서 회원의 등급을 찾고 등급별로 할인율의 값을 지정하던 코드를 모두 지우고 단순히 할인율 계산 로직만 남게 됨
package enumeration.ref1;

public class DiscountService {
    public int discount(ClassGrade classGrade, int price) {
        return price * classGrade.getDiscountPercent() / 100;
    }
}

 

(3) ClassGradeRefMain1

  • ClassGradeEx2_1의 코드와 클래스 이름만 다르고 구현 코드가 똑같으므로 생략
  • 프로그램을 실행해보면 할인 금액이 정상적으로 출력되는 것을 확인할 수 있음

2) 열거형 Grade 리펙토링

(1) Grade - 수정

  • discountPercent 필드를 추가하고 생성자를 통해서 필드에 값을 저장
  • 열거형은 상수로 지정하는 것 외에 일반적인 방법으로 생성이 불가능하므로 생성자에 접근 제어자를 선언할 수 없게 막혀있음(private 이라고 생각하면 됨)
  • BASIC(10)처럼 상수 마지막에 괄호를 열고 생성자에 맞는 인수를 전달하면 적절한 생성자가 호출됨
  • 열거형도 클래스이므로 메서드를 추가할 수 있기 때문에 값을 조회하기 위해 getDiscountPercent()메서드를 추가함
  • 바로 위에서 ClassGrade를 수정한 것과 비교하면 생성자에 인수를 입력하는 방법만 조금 다르고 나머지는 동일함
package enumeration.ref2;

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

    private final int discountPercent;

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

    public int getDiscountPercent() {
        return discountPercent;
    }
}

 

(2) DiscountService - 수정

  • 마찬가지로 if문이 제거되고 단순히 할인율 계산 로직만 남았음
package enumeration.ref2;

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

 

(3) EnumRefMain2

  • EnumEx3_1의 코드와 클래스 이름만 다르고 구현 코드가 똑같으므로 생략
  • 마찬가지로 프로그램을 실행해보면 정상적으로 할인 금액이 출력됨

3) 할인율 계산 리펙토링

(1) 객체지향 관점 리펙토링

  • 위에서 리펙토링된 DiscountService 코드를 보면 단순히 할인율을 계산하는 로직만 남아있지만 할인율 계산을 위해 Grade가 가지고 있는 데이터인 discountPercent의 값을 꺼내서 사용하고 있음
  • 결국 Grade의 데이터인 discountPercent를 할인율 계산에 사용하고 있음
  • 객체지향 관점에서 이렇게 자신의 데이터를 외부에 노출하는 것 보다는 Grade 클래스가 자신의 할인율을 어떻게 계산하는지 스스로 관리하는 것이 캡슐화 원칙에 더 알맞음

(2) Grade - 수정

  • Grade 내부에 discount()메서드를 만들어 할인율을 바탕으로 할인금액을 Grade 스스로 계산하도록 변경
package enumeration.ref3;

public enum Grade {
    
    // 기존 코드 동일 생략

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

}

 

(3) DiscountService - 수정

  • 할인율 계산 자체를 Grade에서 하기 때문에 이제 DiscountServie에서는 discount()메서드를 호출하여 할인금액만 반환하면 됨
package enumeration.ref3;

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

 

(4) EnumRefMain3_1

  • EnumRefMain2와 클래스 이름만 변경되고 동일한 코드이므로 코드는 생략
  • 프로그램을 실행하면 정상적으로 원하는 할인금액이 출력됨

(5) EnumRefMain3_2 - DiscountService 제거

  • Grade가 스스로 할인율을 계산하면서 사실상 DiscountService는 값만 반환하는 로직만 존재할 뿐이므로 사실상 필요가 없음
  • DiscountService를 완전히 제거하고 Grade에서 직접 할인금액을 계산하는 discount()를 직접 불러오도록 변경할 수 있음
  • 실행해보면 정상적으로 등급별 할인율이 출력됨
package enumeration.ref3;

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

        System.out.println("BASIC 등급의 할인 금액: " + Grade.BASIC.discount(price));
        System.out.println("GOLD 등급의 할인 금액: " + Grade.GOLD.discount(price));
        System.out.println("DIAMOND 등급의 할인 금액: " + Grade.DIAMOND.discount(price));
    }
}

 

(6) EnumRefMain3_3 - 출력 반복 제거

  • 출력문에서 등급 부분만 다르고 나머지는 모두 동일하므로 등급과, 가격을 매개변수로 하는 printDiscount()메서드를 생성
  • name()메서드로 ENUM상수의 이름을 꺼내수 있으므로 이를 활용하여 출력문을 완성
  • 해당 메서드에 등급과 가격만 인수로 입력해주면 등급에 따른 할인금액이 출력됨
package enumeration.ref3;

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

        printDiscount(Grade.BASIC, price);
        printDiscount(Grade.GOLD, price);
        printDiscount(Grade.DIAMOND, price);
    }

    private static void printDiscount(Grade grade, int price) {
        System.out.println(grade.name() + " 등급의 할인 금액: " + grade.discount(price));
    }
}

 

(7) EnumRefMain3_4 - 메서드 호출 변경

  • ENUM에 새로운 등급이 추가되더라고 main() 코드의 변경이 없이 모든 등급의 할인이 출력되도록 변경
  • values()를 통해 Enum의 모든 상수를 배열로 반환할 수 있으므로 반복문을 통해 메서드를 호출하면 됨
  • Grade에 새로운 등급과 생성자에 할인율을 지정해보면 main()코드없이 할인금액이 반영되는 것을 확인할 수 있음
package enumeration.ref3;

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

        for (Grade grade : Grade.values()) {
            printDiscount(grade, price);
        }
    }

    private static void printDiscount(Grade grade, int price) {
        System.out.println(grade.name() + " 등급의 할인 금액: " + grade.discount(price));
    }
}
/* 실행 결과
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
MASTER 등급의 할인 금액: 4000
CHALLENGER 등급의 할인 금액: 5000
*/

5. 문제와 풀이

1) 인증 등급 만들기

(1) 문제 설명

  • 회원의 인증 등급을 AuthGrade라는 이름의 열거형으로 생성
  • 인증 등급은 3가지이며 인증 등급에 따른 레벨과 설명을 가짐
  • 레벨과 설명을 getXxx()메서드로 조회할 수 있어야 함
더보기

GUEST(손님)

  • level=1
  • description=손님

LOGIN(로그인 회원)

  • level=2
  • description=로그인 회원

ADMIN(관리자)

  • level=3
  • description=관리자

 

(2) 정답

더보기
package enumeration.test.ex1;

public enum AuthGrade {
    GUEST(1, "손님"),
    LOGIN(2, "로그인 회원"),
    ADMIN(3, "관리자");

    private final int level;
    private final String description;

    AuthGrade(int level, String description) {
        this.level = level;
        this.description = description;
    }

    public int getLevel() {
        return level;
    }

    public String getDescription() {
        return description;
    }
}

2) 인증 등급 열거형 조회하기

(1) 문제 설명

  • 앞서 만든 AuthGrade을 활용하여 AuthGradeMain1이라는 클래스를 만들고 다음 결과가 출력되도록 코드를 작성
더보기

grade=GUEST, level=1, 설명=손님

grade=LOGIN, level=2, 설명=로그인 회원

grade=ADMIN, level=3, 설명=관리자

 

(2) 정답

더보기
package enumeration.test.ex1;

public class AuthGradeMain1 {
    public static void main(String[] args) {

        AuthGrade[] authGrades = AuthGrade.values();
        for (AuthGrade authGrade : authGrades) {
            System.out.println("grade=" + authGrade.name() +
                                ", level=" + authGrade.getLevel() +
                                ", 설명=" + authGrade.getDescription());
        }
    }
}

3) 인증 등급 열거형 활용

(1) 문제 설명

  • AuthGradeMain2 클래스에 코드를 작성
  • 인증 등급을 입력 받은 후 AuthGrade 열거형으로 변환
  • 인증 등급에 따라 접근할 수 있는 화면이 다르며 각각의 등급에 따라서 출력되는 메뉴 목록이 달라짐
    - GUEST는 메인 화면만 접근, ADMIN 등급은 모든 화면에 접근
  • 출력 결과를 참고하여 코드들 완성
더보기

GUEST 입력 예

당신의 등급을 입력하세요[GUEST, LOGIN, ADMIN]: GUEST

당신의 등급은 손님입니다.

==메뉴 목록==

- 메인 화면

 

LOGIN 입력 예

당신의 등급을 입력하세요[GUEST, LOGIN, ADMIN]: LOGIN

당신의 등급은 로그인 회원입니다.

==메뉴 목록==

- 메인 화면

- 이메일 관리 화면

 

ADMIN 입력 예

당신의 등급을 입력하세요[GUEST, LOGIN, ADMIN]: ADMIN

당신의 등급은 관리자입니다.

==메뉴 목록==

- 메인 화면

- 이메일 관리 화면

- 관리자 화면

 

잘못된 값이 입력 되는 경우 - 에러 발생

당신의 등급을 입력하세요[GUEST, LOGIN, ADMIN]: x

Exception in thread "main" java.lang.IllegalArgumentException: No enum constant

enumeration.test.AuthGrade.X

at java.base/java.lang.Enum.valueOf(Enum.java:293)

at enumeration.test.AuthGrade.valueOf(AuthGrade.java:3)

at enumeration.test.AuthGradeMain2.main(AuthGradeMain2.java:12)

 

** 참고

  • Enum.valueOf()를 사용할 때 잘못된 값이 입력되면 IllegalArgumentException예외가 발생하는데 예외를 잡아서 복구하는 방법은 이후 예외처리에서 학습함

 

(2) 정답

더보기

소문자로 입력하여도 정상적으로 동작하도록 설정

package enumeration.test.ex1;

import java.util.Scanner;

public class AuthGradeMain2 {
    public static void main(String[] args) {

        Scanner sc = new Scanner(System.in);
        System.out.print("당신의 등급을 입력하세요[GUEST, LOGIN, ADMIN]: ");

        String grade = sc.nextLine().toUpperCase();
        AuthGrade authGrade = AuthGrade.valueOf(grade);

        System.out.println("당신의 등급은 " + authGrade.getDescription() + "입니다.");
        printMenu(authGrade);
        
    }

    private static void printMenu(AuthGrade authGrade) {
        System.out.println("==메뉴 목록==");
        if (authGrade.getLevel() > 0) {
            System.out.println("- 메인 화면");
        }
        if (authGrade.getLevel() > 1) {
            System.out.println("- 이메일 관리 화면");
        }
        if (authGrade.getLevel() > 2) {
            System.out.println("- 관리자 화면");
        }
    }
}

4) HTTP 상태 코드 정의

(1) 문제 설명

  • enumeration.test.http 패키지를 사용하고 HttpStatus 열거형을 생성
  • HTTP 상태 코드 정의
더보기

(1) OK

  • code: 200
  • message: "OK"

(2) BAD_REQUEST

  • code: 400
  • message: "Bad Request"

(3) NOT_FOUND

  • code: 404
  • message: "Not Found"

(4) INTERNAL_SERVER_ERROR

  • code: 500
  • message: "Internal Server Error"

** 참고

  • HTTP 상태코드는 200 ~ 299 사이의 숫자를 성공으로 인정함
  • HttpStatusMain 코드와 실행 결과를 참고하여 HttpStatus 열거형을 완성
더보기

HttpStatus

package enumeration.test.http;

public enum HttpStatus {
    // 코드 작성
}

 

HttpStatusMain

package enumeration.test.http;

import java.util.Scanner;

public class HttpStatusMain {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("HTTP CODE: ");
        int httpCodeInput = scanner.nextInt();

        HttpStatus status = HttpStatus.findByCode(httpCodeInput);
        if (status == null) {
            System.out.println("정의되지 않은 코드");

        } else {
            System.out.println(status.getCode() + " " + status.getMessage());
            System.out.println("isSuccess = " + status.isSuccess());

        }
    }
}

 

실행 결과 - 200

HTTP CODE: 200

200 OK

isSuccess = true

 

실행 결과 - 400

HTTP CODE: 400

400 Bad Request

isSuccess = false

 

실행 결과 - 404

HTTP CODE: 404

404 Not Found

isSuccess = false

 

실행 결과 - 500

HTTP CODE: 500

500 Internal Server Error

isSuccess = false

 

(2) 정답

package enumeration.test.http;

public enum HttpStatus {

    OK(200, "OK"),
    BAD_REQUEST(400, "Bad Request"),
    NOT_FOUND(404, "Not Found"),
    INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
    ;

    private final int code;
    private final String message;

    HttpStatus(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    public boolean isSuccess() {
        return code >= 200 && code <= 299;
    }

    public static HttpStatus findByCode(int code) {
        for (HttpStatus status : values()) {
            if (status.getCode() == code) {
                return status;
            }
        }
        return null;
    }
}

 

5) 정리

(1) 문제를 원천 차단

  • '조심해야한다' 라는것은 누구나 개발을 할 때 조심하지만 사람이기 때문에 실수할 수 있기 때문에 위험할 수 있음
  • 제약을 두어 실무에서 개발할 때 문제를 원천적으로 차단하게 설계하는 것이 훨신 더 좋은 개발 방법이며 좋은 프로그램을 설계하는 것임
  • 이런 좋은 제약을 두는 대표적인 케이스가 ENUM이며 반복되는 문제나 실수등을 이런 좋은 장치들을 잘 활용하는 것이 중요함