JAVA

[Java] 문자열과 타입 안전성

songsua 2025. 2. 8. 19:40

자바에서 제공하는 열거형(Enum Type)에 대해서 정리할 것이다.

정리하기 전에 열거형이 생겨난 이유를 알아볼려고한다.

 

예시 상황

Basic --> 10% 할인, Gold --> 20%할인, VIP --> 30% 할인

예) 골드회원이 10000원을 구매하면 할인 대상 금액은 2000원이다.

회원 등급과 가격을 입력하면 할인 금액을 계산해주는 클래스를 만들어보자.

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("VIP")) {
            discountpercent = 30;
        } else {
            System.out.println("할인가능한 계급이 아닙니다.");
        }
        return discountpercent * price / 100;
    }
}
package enumeration.ex0;

public class StirngGradeEx0_1 {
    public static void main(String[] args) {
        int price = 10000;
        DiscountService discountService = new DiscountService();
        int data1 = discountService.discount("VIP", price);
        int data2 = discountService.discount("BASIC", price);
        int data3 = discountService.discount("Gold", price);

        System.out.println("VIP 등급의 할인 금액" + data1);
        System.out.println("Basic등급의 할인 금액" + data2);



    }
}

 

만약 "VIP" 같은 단어를 오타를 발생하면(대소문 구분, 오타 등등) 컴파일 시 오류 감지 불가능하다. 런타임에서만 문제가 발견되기 때문에 디버깅이 어려워질 수 있다. 그래서 대안으로 문자열 상수를 사용해본다. 
상수는 미리 정의한 변수명을 사용할 수 있기 때문에 문자열을 직접 사용하는 것 보다는 더 안전하다.


메서드를 이용하여, 보다 안전하게 해보았다.

package enumeration.ex0.ex1;

public class StringGrade {
    public static final String BASIC = "BASIC";
    public static final String GOLD = "GOLD";
    public static final String DIAMOND = "DIAMOND";
}
package enumeration.ex0.ex1;

public class DiscountSevice {
    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("할인가능한 계급이 아닙니다.");
        }
        return discountpercent * price / 100;
    }
}

grade.equals 함수를 사용해서 StringGrade안에 있는 문자열 상수를 사용하여 이름을 잘못 입력하면 컴파일 시점에서 오류를 감지할 수 있다.

package enumeration.ex0.ex1;

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

        DiscountSevice discountService = new DiscountSevice();
        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);

        //오타를 내도 컴파일 오류가 발생하지 않음
        int dia = discountService.discount("dia", price);
        System.out.println("오타"+ dia);
    }
}


하지만, 문자열 상수를 사용해도 지금까지 발생한 원인을 해결할 수 없다.

왜냐하면, String 타입은 어떤 문자열이든 입력할 수 있기 때문이다. 어떤 개발자가 실수로 StringGrade 에 있는 문자열 상수를 사용하지 않고 위에서 처럼 존재하지 않은 "dia" 같이 문자열을 사용해도 막을 수 있는 방법은 없다.

 

애초에 DiscountService에서 String grade을 받을 수 없게 설정을 해야한다.

package enumeration.ex0.ex1;

public class DiscountSevice {
    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("할인가능한 계급이 아닙니다.");
        }
        return discountpercent * price / 100;
    }
}

 

타입 안전 열거형 패턴 

먼저 회원 등급을 다루는 클래스를 만들고, 각각의 회원 등급별로 상수를 선언한다.

이때 각각의 상수마다 별도의 인스턴스를 생성하고, 생성한 인스턴스를 대입한다.

static 을 사용해서 상수를 메서드 영역에 선언한다.

final을 사용해서 참조값을 변경할 수 없게 한다.

package enumeration.ex0.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();
}

package enumeration.ex0.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.ex0.ex2.ClassGrade
class GOLD = class enumeration.ex0.ex2.ClassGrade
class DIAMOND = class enumeration.ex0.ex2.ClassGrade

ref Basic = enumeration.ex0.ex2.ClassGrade@3feba861
ref GOLD = enumeration.ex0.ex2.ClassGrade@5b480cf9
ref DIAMOND = enumeration.ex0.ex2.ClassGrade@6f496d9f

Class grade로 만들어진 것을 알 수 있다. 그리고 참조값이 각각 다른 것을 알 수 있다.
각각의 상수는 모두 서로 각각 다른 인스턴스를 참조하기 때문에 참조값이 다르게 출력된다.

Static 을 사용하여 ClassGrade 인스턴스가 각각 3개로 생성되고, 각각의 상수는 ClassGrade 타입의 서로 다은 인스턴스의 참조값을 가진다.

 

package enumeration.ex0.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("할인 할 수 없습니다");
        }
        return discountPercent * price / 100;
    }
}

discount() 메서드는 매개변수로 ClassGrade 클래스를 사용한다.

값을 비교할 때는 classGrade == ClassGrade.Basic 와 같이 == 참조값 비교를 사용하면 된다.

package enumeration.ex0.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(ClassGrade.BASIC + " 할인 금액: " + basic);
        System.out.println(ClassGrade.GOLD + " 할인 금액: " + gold);
        System.out.println(ClassGrade.DIAMOND + " 할인 금액: " + diamond);


    }
}

 

하지만, 해당 코드의 한계점이 있는데,

package enumeration.ex0.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);
    }
}
할인 할 수 없습니다
newClassGrade의 등급0

Process finished with exit code 0

이 문제를 해결할려면 외무에서 ClassGrade 을 생성할 수 없도록 막으면 된다.

기본 생성자를 private 로 변경한다.

package enumeration.ex0.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();
    
    //외부에서 생성하지 못하도록 private 생성자 추가하여 막아둔다
    private ClassGrade() {}
}

 

 

이로 인해 타입 안정성과 제한된 인스턴스 생성을 할 수 있다.

하지만 단점이 있는데, 이를 구현할려면 다음과 같이 많은 코드를 작성해야한다.
그래서 열거형 - enum type을 사용한다.

 

열거형 - Enum Type

위에서 상수로 정의한 BASIC, GOLD, DIAMOND 만 사용할 수 있다는 뜻이다.

자바의 enum은 안전성을 제공하고, 코드의 가독성을 높이며, 예상 가능한 값들의 집합을 표현하는 데 사용한다.

 

package enumeration.ex0.ex3;

public enum Grade { 
    BASIC, GOLD, DIAMOND
}

 

Class대신 enum을 사용하여 위와 같이만 하면 된다.

package enumeration.ex0.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();
    
    //외부에서 생성하지 못하도록 private 생성자 추가하여 막아둔다
    private ClassGrade() {}
}

 

와는 확실하게 코드가 확 줄어든다.

열거형은 외부에서 임의로 생성할 수 없다.

package enumeration.ex0.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));  //enum의 경우 Tostring을 오버라이딩한다. 그래서 참조값을 볼려면 메서드를 생성해줘야함
        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.ex0.ex3.Grade
Class GOLD = class enumeration.ex0.ex3.Grade
Class DIAMOND = class enumeration.ex0.ex3.Grade
ref BASIC = 3feba861
ref GOLD = 5b480cf9
ref DIAMOND= 6f496d9f

 Class는 같은 Grade 타입을 사용하는 것을 알 수 있으며, 인스턴스 참조값은 서로 다른 것을 알 수 있다.

열거형은 toString() 을 재정의하기 때문에 참조값을 직접 확인할 수 없다. 참조값을 구하기 위해서는 refValue()를 만들어야한다.

  • System.identityHashCode(grade) : 자바가 관리하는 객체의 참조값을 숫자로 반환한다.
  • Integer.toHexString(): 숫자를 16진수로 변환, 우리가 일반적으로 확인하는 참조값은 16진수

열거형도 클래스이다. 열거형을 제공하기 위해 제약이 추가된 클래스라고 생각하면 된다.
열거형의 장점 : 1. 타입 안정성 2. 간결성 및 일광성 3. 확장성

열거형 주요 메서드

1. 모든 ENUM의 반환

package enumeration.ex0.ex3;

import java.lang.reflect.Array;
import java.util.Arrays;

public class EnumMethodMain {
    public static void main(String[] args) {
        //모든 enum 반환
        Grade[] values = Grade.values();
        System.out.println("Enum Method" + values);

        //array.tosTring 으로 배열의 값을 출력하기
        System.out.println("Enum Method" + Arrays.toString(values));
        for (Grade grade : values) {
            System.out.println("name" + grade.name() + ", grade: " + grade.ordinal());
        }

    }
}
Enum Method[Lenumeration.ex0.ex3.Grade;@6acbcfc0
Enum Method[BASIC, GOLD, DIAMOND]
nameBASIC, grade: 0
nameGOLD, grade: 1
nameDIAMOND, grade: 2

Process finished with exit code 0

 

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

좀 더 enum 를 리팩토링 해보기

package enumeration.ex0.ref1;

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

    private final int discountPercent;


    //생성자 생성
    Grade(int discountPercent) {
        this.discountPercent = discountPercent;
    }
    
    public int getDiscountPercent() {
        return discountPercent;
    }
}

enum안에 생성자를 생성해서 바로 할인률을 적용해줄 수 있다.

열거형은 상수로 지정하는 것 외에 일반적인 방법으로는 생성이 불가능하다. 따라서 생성자에 접근제어자를 선언할 수 없게 막혀있기 때문에 private 라고 생각하면 된다.

값을 조회하기 위해 getDiscountPercent() 메서드를 추가했다. 열거형도 메서드를 생성할 수 있다.

package enumeration.ex0.ref1;

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

에서 인라인 단축키를 사용해서 한줄로

package enumeration.ex0.ref1;

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

Main 클래스도 수정하면

package enumeration.ex0.ref1;
public class ClassGradeEx4_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);
        System.out.println("할인 금액: " + gold);
        System.out.println(" 할인 금액: " + diamond);


    }
}


이제,, DiscountService 메서드를 수정하고 싶은데,

package enumeration.ex0.ref1;

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

이 코드를 보면 할인율을 계산하기 위해서 Grade 가 가지고 있는 데이터인 discountPercent 의 값을 꺼내서 사용한다.
꺼내서 사용하는 것은 뭔가 객체지향스럽지 않다.

결국 Grade의 데이터인 discountPercent 를 할인율 계산에 사용한다.

package enumeration.ex0.ref1;

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

    private final int discountPercent;


    //생성자 생성
    Grade(int discountPercent) {
        this.discountPercent = discountPercent;
    }
    
    public int getDiscountPercent() {
        return discountPercent;
    }
}

객체지향 관점에서 이렇게 자신의 데이터를 외부에 노출하는 것보다는 Grade 클래스가 자신의 할인율을 어떻게 계산하는지 스스로 관리하는 것이 캡슐화의 원칙이다.

Grade 클래스안으로 discount() 메서드를 이동시키자.

package enumeration.ex0.ref1;

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

Grade 안에 discount 메서드도 추가하였다.

package enumeration.ex0.ref2;

public class DiscountService {
    public int discount(Grade grade, int price) {
        //Grade에 있는 discountPercent값만 가져오면 된다.
        return grade.getDiscountPercent();
    }
}

Main 

package enumeration.ex0.ref2;
public class ClassGradeEx5_1 {
    public static void main(String[] args) {
        int price = 10000;
        System.out.println("기본 등급의 할인 금액" + Grade.BASIC.discount(price));
        System.out.println("골드 등급의 할인 금액" + Grade.GOLD.discount(price));
        System.out.println("다이아 등급의 할인 금액" + Grade.DIAMOND.discount(price));
    }
}

 

중복을 제거해보자.

package enumeration.ex0.ref2;
public class ClassGradeEx5_2 {
    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.discount(price));
    }
}


이후에 새로운 등급이 추가되더라도 main() 코드의 변경없이 모든 등급의 할인율을 출력해보도록 하자

package enumeration.ex0.ref2;
public class ClassGradeEx5_3 {
    public static void main(String[] args) {
        int price = 10000;
        //ctrl+comd+v
        Grade[] grades = Grade.values();
        //iter 단축키 사용
        for (Grade grade : grades) {
            printDiscount(grade, price);
        }
        
        
//        printDiscount(Grade.BASIC, price);
//        printDiscount(Grade.GOLD, price);
//        printDiscount(Grade.DIAMOND, price);
    }

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