[Java] 문자열과 타입 안전성
자바에서 제공하는 열거형(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));
}
}