김영한의 실전 자바 - 중급 1편 강의 | 김영한 - 인프런
김영한 | , 국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바 문법을 안다? 이걸로는 안됩니다!전 우아한형제들 기술이사, 누적 수강생 40만 명 돌
www.inflearn.com
비즈니스 요구사항
고객을 3등급으로 나누고, 상품 구매 시 등급별로 할인율을 적용한다. 할인 시 소수점 이하는 버린다.
- `BASIC` → 10% 할인
- `GOLD` → 20% 할인
- `DIAMOND` → 30% 할인
회원 등급과 가격을 입력하면 할인 금액을 계산해주는 클래스를 만들어보자.
문자열과 타입 안전성
`String`
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;
}
}
- `price * discountPercent / 100`을 계산하면 할인 금액을 구할 수 있다.
- 회원 등급 외 다른 값이 입력되면 `할인X`를 출력한다.
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
실행 결과를 보면 각각의 회원 등급에 맞는 할인이 적용된 것을 확인할 수 있다.
그런데 위와 같이 단순히 문자열을 입력하는 방식은 오타가 발생하기 쉽고, 유효하지 않는 값이 입력될 수 있다.
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 등급의 할인 금액: " + vip);
// 소문자 입력
int gold = discountService.discount("gold", price);
System.out.println("gold 등급의 할인 금액: " + gold);
}
}
실행 결과
VIP: 할인X
VIP 등급의 할인 금액: 0
DIAMONDD: 할인X
DIAMONDD 등급의 할인 금액: 0
gold: 할인X
gold 등급의 할인 금액: 0
- 존재하지 않는 VIP라는 등급을 입력했다.
- 오타: DIAMOND 마지막에 D가 하나 추가되었다.
- 소문자 입력: 등급은 모두 대문자인데, 소문자를 입력했다.
string 사용 시 타입 안정성 부족 문제
- 값의 제한 부족: `String`으로 상태나 카테고리를 표현하면, 잘못된 문자열을 실수로 입력할 가능성이 있다. 예를 들어, "Monday", "Tuesday" 등을 나타내는 데 `String`을 사용한다면, 오타("Munday")나 잘못된 값("Funday")이 입력될 위험이 있다.
- 컴파일 시 오류 감지 불가: 이러한 잘못된 값은 컴파일 시에는 감지되지 않고, 런타임에서만 문제가 발견되기 때문에 디버깅이 어려워질 수 있다.
이런 문제를 해결하려면 특정 범위로 값을 제한해야 한다. 예를 들어 `BASIC`, `GOLD`, `DIAMOND`라는 정확한 문자만 `discount()` 메서드에 전달되어야 한다. 하지만 `String`은 어떤 문자열이든 받을 수 있기 때문에 자바 문법 관점에서는 아무런 문제가 없다. 결국 `String` 타입을 사용해서는 문제를 해결할 수 없다.
문자열 상수
상수는 미리 정의한 변수명을 사용할 수 있기 때문에 문자열을 직접 사용하는 것보다는 더 안전하다.
public class StringGrade {
public static final String BASIC = "BASIC";
public static final String GOLD = "GOLD";
public static final String DIAMOND = "DIAMOND";
}
public class DiscountService {
//StringGrade를 참고하세요.
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);
}
}
실행 결과
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
문자열 상수를 사용한 덕분에 전체적으로 코드가 더 명확해졌다. `discount()`에 인자를 전달할 때도 `StringGrade`가 제공하는 문자열 상수를 사용하면 된다. 더 좋은 점은 만약 실수로 상수의 이름을 잘못 입력하면 컴파일 시점에 오류가 발생한다는 점이다.
하지만 문자열 상수를 사용해도, 지금까지 발생한 문제들을 근본적으로 해결할 수는 없다. 왜냐하면 `String` 타입은 어떤 문자열이든 입력할 수 있기 때문이다. 실수로 `StringGrade`에 있는 문자열 상수를 사용하지 않고, 직접 문자열을 사용해도 막을 수 있는 방법이 없다.
public class StringGradeEx1_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 등급의 할인 금액: " + vip);
// 소문자 입력
int gold = discountService.discount("gold", price);
System.out.println("gold 등급의 할인 금액: " + gold);
}
}
그리고 사용해야 하는 문자열 상수가 어디에 있는지 `discount()`를 호출하는 개발자가 알 수 없다. 다음 코드를 보면 분명 `String`은 다 입력할 수 있다고 되어있다.
public int discount(String grade, int price) {}
결국 누간가 주석을 잘 남겨두어서, `StringGrade`에 있는 상수를 사용해달라고 해야 한다. 물론 이렇게 해도 누군가는 주석을 깜박하고 문자열을 직접 입력할 수 있다.
타입 안전 열거형 패턴
Type-Safe-Enum Pattern
지금까지 설명한 문제를 해결하기 위해 나온 결과가 바로 타입 안전 열거형 패턴이다. 여기서 영어인 `enum`은 `enumeration`의 줄임말인데, 어떤 항목을 나열하는 것을 뜻한다. 우리의 경우, `BASIC`, `GOLD`, `DIAMOND`를 나열하는 것이다.
타입 안전 열거형 패턴을 사용하면 나열한 항목만 사용할 수 있는 것이 핵심이다. 나열한 항목이 아닌 것은 사용할 수 없다.
public class ClassGrade {
public static final ClassGrade BASIC = new ClassGrade();
public static final ClassGrade GOLD = new ClassGrade();
public static final ClassGrade DIAMOND = new ClassGrade();
}
- 회원 등급을 다루는 클래스를 만들고, 각각의 회원 등급별로 상수를 선언한다.
- 이때 각각의 상수마다 별도의 인스턴스를 생성하고, 생성한 인스턴스를 대입한다.
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@x001
ref GOLD = enumeration.ex2.ClassGrade@x002
ref DIAMOND = enumeration.ex2.ClassGrade@x003
- 각각의 상수는 모두 `ClassGrade` 타입을 기반으로 인스턴스를 만들었기 때문에 `getClass()`의 결과는 모두 `ClassGrade`이다.
- 각각의 상수는 모두 서로 각각 다른 `ClassGrade` 인스턴스를 참조하기 때문에 참조값이 다르게 출력된다.
`static`이므로 애플리케이션 로딩 시점에 다음과 같이 3개의 `ClassGrade` 인스턴스가 생성되고, 각각의 상수는 같은 `ClassGrade` 타입의 서로 다른 인스턴스의 참조값을 가진다.
- `ClassGrade BASIC`: `x001`
- `ClassGrade GOLD`: `x002`
- `ClassGrade DIAMOND`: `x003`
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;
}
}
- `discount()` 메서드는 매개변수로 `ClassGrade` 클래스를 사용한다.
- 값을 비교할 때는 `classGrade == ClassGrade.BASIC`과 같이 `==` 참조값 비교를 사용하면 된다.
private 생성자
그런데 이 방식은 외부에서 임의로 `ClassGrade` 인스턴스를 생성할 수 있다는 문제가 있다.
public class ClassGradleEx2_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
이 문제를 해결하려면 외부에서 `ClassGrade`를 생성할 수 없도록 막으면 된다. 기본 생성자를 `private`으로 변경하자.
public class ClassGrade {
public static final ClassGrade BASIC = new ClassGrade(); //x001
public static final ClassGrade GOLD = new ClassGrade(); //x002
public static final ClassGrade DIAMOND = new ClassGrade(); //x003
//private 생성자 추가
private ClassGrade() {}
}
- `private` 생성자를 사용해서 외부에서 `ClassGrade`를 임의로 생성하지 못하게 막았다.
- `private` 생성자 덕분에 `ClassGrade`의 인스턴스를 생성하는 것은 `ClassGrade` 클래스 내부에서만 할 수 있다.
- 이제 `ClassGrade` 인스턴스를 사용할 때는 `ClassGrade` 내부에 정의한 상수를 사용해야 한다. 그렇지 않으면 컴파일 오류가 발생한다.
타입 안전 열거형 패턴(Type-Safe Enum Pattern)의 장점
- 타입 안정성 향상: 정해진 객체만 사용할 수 있기 때문에, 잘못된 값을 입력하는 문제를 근본적으로 방지할 수 있다. (사전에 정의된 몇 개의 인스턴스만 생성된다.)
- 데이터 일관성: 정해진 객체만 사용하므로 데이터의 일관성이 보장된다. (잘못된 값이 사용되는 것을 컴파일 시점에 방지할 수 있다.)
타입 안전 열거형 패턴(Type-Safe Enum Pattern)의 단점
이 패턴을 구현하려면 다음과 같이 많은 코드를 작성해야 한다. 그리고 `private` 작성자를 추가하는 등 유의해야 하는 부분도 있다.
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 ClassGrade() {}
}
Enum Type
개념
자바는 타입 안전 열거형 패턴을 매우 편리하게 사용할 수 있는 열거형(Enum Type)을 제공한다.
public enum Grade {
BASIC, GOLD, DIAMOND
}
- 열거형을 정의할 때는 `class` 대신 `enum`을 사용한다. (자동으로 `java.lang.Enum`을 상속 받는다.)
- 원하는 상수의 이름을 나열하면 된다.
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에 새로운 상수를 추가하기만 하면 된다.
열거형을 사용하는 경우, `static import`를 적절하게 사용하면 더 읽기 좋은 코드를 만들 수 있다.
주요 메서드
모든 열거형은 `java.lang.Enum` 클래스를 자동으로 상속 받는다. 따라서 해당 클래스가 제공하는 기능들을 사용할 수 있다.
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); //toString() 오버라이딩 가능
}
}
실행 결과
values = [BASIC, GOLD, DIAMOND]
name=BASIC, ordinal=0
name=GOLD, ordinal=1
name=DIAMOND, ordinal=2
gold = GOLD
`Arrays.toString()`은 배열의 참조값이 아니라 배열 내부의 값을 출력할 때 사용한다.
주요 메서드
- `values()`: 모든 ENUM 상수를 포함하는 배열을 반환한다.
- `valueOf(String name)`: 주어진 이름과 일치하는 ENUM 상수를 반환한다.
- `name()`: ENUM 상수의 이름을 문자열로 반환한다.
- `ordinal()`: ENUM 상수의 선언 순서(0부터 시작)를 반환한다.
- `ordinal()`의 값은 가급적 사용하지 않는 것이 좋다. 왜냐하면 이 값을 사용하다가 중간에 상수를 선언하는 위치가 변경되면 전체 상수의 위치가 변경될 수 있기 때문이다.
- `toString()`: ENUM 상수의 이름을 문자열로 반환한다. `name()` 메서드와 유사하지만, `toString()`은 직접 오버라이딩할 수 있다.
총 정리
- 열거형은 `java.lang.Enum`을 자동(강제)으로 상속 받는다.
- 열거형은 이미 `java.lang.Enum`을 상속받아씩 때문에 추가로 다른 클래스를 상속 받을 수 없다.
- 열거형은 인터페이스를 구현할 수 있다.
- 열거형은 추상 메서드를 선언하고 구현할 수 있다.
- 이 경우 익명 클래스와 같은 방식을 사용한다.
리팩토링
타입 안전 열거형 패턴
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");
}
- 불필요한 `if`문을 제거하자.
- 이 코드에서 할인율(`discountPercent`)은 각각의 회원 등급별로 판단되다. 할인율은 결국 회원 등급에 따라간다. 따라서 회원 등급 클래스가 할인율(`discountPercent`)을 가지고 관리하도록 변경하자.
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;
}
}
- `classGrade`에 할인율(`discountPercent`) 필드와 조회 메서드를 추가했다.
- 생성자를 통해서만 `discountPercent`를 설정하도록 했고, 중간에 이 값이 변하지 않도록 불변으로 설계했다.→상수를 정의할 때 각각의 등급에 따른 할인율(`discountPercent`)이 정해진다.
public class DiscountService {
public int discount(ClassGrade classGrade, int price) {
return price * classGrade.getDiscountPercent() / 100;
}
}
이를 통해서 기존에 있던 `if`문이 완전히 제거되고, 단순한 할인율 계산 로직만 남았다. 단순히 회원 등급안에 있는 `getDiscountPercent()` 메서드를 호출하면 인수로 넘어온 회원 등급의 할인율을 바로 구할 수 있다.
Enum
public enum Grade {
BASIC(10), GOLD(20), DIAMOND(30);
private final int discountPercent;
Grade(int discountPercent) {
this.discountPercent = discountPercent;
}
public int getDiscountPercent() {
return discountPercent;
}
}
- `discountPercent` 필드를 추가하고, 생성자를 통해서 필드에 값을 저장ㅎ나다.
- 열거형은 상수로 지정하는 것 외에 일반적인 방법으로 생성이 불가능하다. 따라서 생상자에 접근제어자를 선언할 수 없게 막혀있다. `private`이라고 생각하면 된다.
- `BASIC(10)`과 같이 상수 마지막에 괄호를 열고 생성자에 맞는 인수를 전달하면 적절한 생성자가 호출된다.
public class DiscountService {
public int discount(Grade grade, int price) {
return price * grade.getDiscountPercent() / 100;
}
}
- 이 코드를 보면 할인율 계산을 위해 `Grade`가 가지고 있는 데이터인 `discountPercent`의 값을 꺼내서 사용한다.
- 결국 `Grade`의 데이터인 `discountPercent`를 할인율 계산에 사용한다.
- 객체지향 관점에서 이렇게 자신의 데이터를 외부에 노출하는 것보다는, `Grade` 클래스가 자신의 할인율을 어떻게 계산하는지 스스로 관리하는 것이 캡슐화 원칙에 더 맞다.
`Grade` 클래스 안으로 `discount()` 메서드를 이동시키자.
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`가 스스로 할인율을 계산하면서 `DiscountService` 클래스가 더는 필요하지 않게 되었다.
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));
}
}
- 각각의 등급별로 자신의 `discount()`를 직접 호출하면 할인율을 구할 수 있다.
출력 부분의 중복을 제거하면 아래와 같다.
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));
}
}
- `grade.name()`을 통해서 ENUM의 상수 이름을 사용할 수 있다.
Enum 목록
public class EnumRefMain3_4 {
public static void main(String[] args) {
int price = 10000;
Grade[] grades = Grade.values();
for (Grade grade : grades) {
printDiscount(grade, price);
}
}
private static void printDiscount(Grade grade, int price) {
System.out.println(grade.name() + " 등급의 할인 금액: " + grade.discount(price));
}
}
- `Grade.values()`을 사용하면 `Grade` 열거형의 모든 상수를 배열로 구할 수 있다.
'Java' 카테고리의 다른 글
[Java/김영한] 중첩 클래스, 내부 클래스 (0) | 2025.05.01 |
---|---|
[Java/김영한] 날짜와 시간 (0) | 2025.04.24 |
[Java/김영한] 래퍼, Class 클래스 (0) | 2025.03.23 |
[Java/김영한] String 클래스 (0) | 2025.03.22 |
[Java/김영한] 불변 객체 (1) | 2025.03.08 |