Enum 클래스 (enumeration type, 열거체)

박영준·2023년 1월 24일
0

Java

목록 보기
42/111

1. 정의

과거

class Car {
	static final int CLOVER = 0;
    static final int HEART = 1;
    static final int DIAMOND = 2;
    static final int SPADE = 3;
    
    static final int TWO = 0;
    static final int THREE = 1;
    static final int FOUR = 2;
    
    final int kind;
    final int num;
}

특정 상수값을 사용하기 위해선 모두 상수로 선언해서 사용
→ 개발자의 실수, 가시성↓, 지저분한 변수명 등... 문제 발생

대안
Enum 클래스

class Car {
	enum Kind {			// 열거형 Kind 정의
    	 CLOVER, HEART, DIAMOND, SPADE			// 각각의 값은 '열거 상수(enumeration constant)'
    }
    
    enum Value {			// 열거형 Value 정의
    	TWO, THREE, FOUR
    }
    
    final Kind kind;		// 주의! Kind 타입이다
    final Value value;
}
  • 여러 상수를 선언할 경우, 편리하게 사용할 수 있는 방법

    • 일정 개수의 상수 값을 정의한 다음, 그 외의 값은 허용하지 않는 타입
    • 따로 값을 지정해주지 않아도, 자동적으로 0부터 시작하는 정수값이 할당됨 (0부터, 그 다음은 바로 앞의 상숫값보다 1만큼 증가되며 설정)
  • 모든 enum들은 내부적으로 java.lang.Enum 클래스에 의해 상속된다.
    (클래스는 하나의 부모 클래스에 의해서만 상속되므로, enum은 다른것을 상속할 수가 없다.)

  • 열거 타입은 참조 타입이기 때문에, 열거 타입 변수에 null값을 저장할 수 있음

2. 장점

  1. Enum으로 관리하면, DB접근이 없다.
    → DB로 관리하면, 테이블과 조회쿼리가 복잡해진다.

  2. 런타임환경에서는 카테고리가 변하지 않는다.
    → 컴파일 타임 안전성을 제공

  3. 열거체를 비교할 때, '실제 값' + '타입'까지도 체크

  4. 열거체의 상숫값이 재정의되더라도, 다시 컴파일할 필요 X

  5. 허용 가능한 값들을 제한 가능

  6. (문자열과 달리) IDE 의 적극적인 지원을 받을 수 있다. (동완성, 오타검증, 텍스트 리팩토링 등...)

  7. 리팩토링 시, 변경 범위가 최소화
    → 내용의 수정/추가가 필요하더라도, Enum 코드 외에 수정할 필요 X

3. 명명 규칙

1) 열거 타입 명명 규칙

  1. 첫 글자는 대문자, 나머지는 소문자

  2. 여러 단어로 구성된 이름은? 각 단어의 첫 글자를 대문자로

  3. 소스 파일 이름과 대소문자 모두 일치해야 함

2) 열거 상수 명명 규칙

  1. 대문자

  2. 여러 단어로 구성될 경우? 단어 사이를 언더바( _ )로 연결

  3. 열거 상수는 객체

4. 사용법

예시 1 : 문법

// 문법
enum 열거형이름 { 상수이름1, 상수이름2, ... }

// 예시 : 상수로 정의하는 방법
enum Direction { EAST, SOUTH, WEST, NORTH }

// 예시 : 정의된 상수를 사용하는 방법
class Unit {
	int x;			// 유닛의 위치
    int y;			// 유닛의 위치
    Direction dir;	// 열거형 인스턴스 변수 선언
    
    void init() {
    	dir = Direction.EAST;		// 유닛의 방향을 EAST로 초기화
    }
}
  • 정의된 상수를 사용하는 방법
    • 열거형이름.상수이름

예시 2 : 비교

if (DIR == Direction.EAST) {
	x++;
} else if (dir > Direction.WEST) {			// 에러 :  '>' 사용 X
	...
} else if (dir.compareTo(Direction.WEST) > 0) {
	...
}

예시 3 : 메서드

values() 메소드

static E values()

  • 해당 열거체의 모든 상수를 저장한 배열을 생성하여 반환

  • 자바의 모든 열거체에 컴파일러가 자동으로 추가

    enum Rainbow { RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET }
    
    public class Enum01 {
        public static void main(String[] args) {
            Rainbow[] arr = Rainbow.values();
            for (Rainbow rb : arr) {
                System.out.println(rb);
            }
        }
    }
    // 실행 결과
    RED
    ORANGE
    YELLOW
    GREEN
    BLUE
    INDIGO
    VIOLET

valueOf() 메소드

static E valueOf(String name)

  • 전달된 문자열과 일치하는 해당 열거체의 상수를 반환

  • 기본 타입의 값을 문자열로 변환

    참고: 타입

    //예시 1
    enum Rainbow { RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET }
    
    public class Enum02 {
        public static void main(String[] args) {
            Rainbow rb = Rainbow.valueOf("GREEN");
            System.out.println(rb);
        }
    }
    
    /* 실행 결과
    GREEN */
    // 예시 2
    public class StringvalueofExample {
        public static void main(String[] args) {
            String str1 = String. valueof(10);
            String str2 = String.valueof(10.5);
            String str3 = String.valueof(true);
    
            System.out.println(str1);
            System.out.println(str2);
            System.out.println(str3);
        }
    }
    
    /* 실행 결과
    10
    10.5
    true */

    참고: parseInt(), intValue(), valuOf()

ordinal() 메소드

int ordinal()

  • 해당 열거체 상수가 열거체 정의에서 정의된 순서(0부터 시작)를 반환

  • 이때 반환되는 값은 열거체 정의에서 해당 열거체 상수가 정의된 순서 (상숫값 자체가 아님)

    enum Rainbow { RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET }
    
    public class Enum03 {
        public static void main(String[] args) {
            int idx = Rainbow.YELLOW.ordinal();
            System.out.println(idx);
        }
    }
    // 실행 결과
    2

protected void finalize()

해당 Enum 클래스가 final 메소드를 가질 수 없게 됨

String name()

해당 열거체 상수의 이름을 반환

예시 4 : 원하는 값 부여하기

// 예시 1
enum Direction { 
	EAST(1), SOUTH(5), WEST(-1), NORTH(10);

	private final int value;		// 정수를 저장할 인스턴스 변수(필드) 추가
    
    Direction(int value) {			// 생성자 추가
    	this.value = value;
    }
    
    publid int getValue() {			// 외부에서 해당 값을 얻을 수 있도록 함
    	return value;
    }
}

// 예시 2 : 값이 두 개일 경우
enum Direction { 
    EAST(1, ">"), SOUTH(5, "V"), WEST(-1, "<"), NORTH(10, "^");

	private final int value;		// 정수를 저장할 인스턴스 변수(필드) 추가
    private final String symbol;
    
    Direction(int value, String symbol) {			// 생성자 추가
    	this.value = value;
        this.symbol = symbol;
    }
    
    publid int getValue() {			// 외부에서 해당 값을 얻을 수 있도록 함
    	return value;
    }
    
    publid int getSymbol() {			// 외부에서 해당 값을 얻을 수 있도록 함
    	return symbol;
    }
}
  • Enum 클래스의 ordinal() 메서드는 열거형 상수가 정의된 순서를 반환
    그러나, 해당 값을 상수 값으로 그대로 사용하기는 부적절하다.
    • 대안
      • 열거형 상수 이름 옆에 (원하는 값)을 적어 줄 수 있다
      • 추가로, 해당 값을 저장할 인스턴스 변수& 생성자도 추가해야 한다
    • 따라서, 열거형 생성자는 외부에서 호출 X
      • Direction d = new Direction(1); 에러!
      • (암묵적으로) 열거형 생성자는 private 이기 때문

예시 5 : == 제거

// 기존 코드
LoginResult result = LoginResult.FAIL_PASSWORK;

if (result == LoginResult.SUCCESS) {
	...
} else if (result == LoginResult.FAIL_ID) {
	...
} else if (result == LoginResult.FAIL_PASSWORK) {
	...
}

// 열거 타입으로 변형
public enum LoginResult {
	SUCCESS,
	FAIL_ID,
	FAIL_PASSWORD
}

5. 관리 주체를 DB에서 객체로

상품의 카테고리를 관리하려고 한다.

상황 1

문제 상황

DB 로 관리

SELECT item.* FROM 
item INNER JOIN categories ON item.category_id = categories.id
WHERE item.is_active = 1 AND (categories.is_active = 1 AND categories.parent_id IN 
    (SELECT id FROM categories WHERE parent_id IS NULL AND is_active = 1)) OR ( item.is_active = 1 AND categories.parent_id IS NULL AND categories.is_active = 1 )

계층형 구조를 지닌 카테고리의 경우, 매우 복잡해진다.
→ 그 대안으로 Enum 을 사용한다.

해결법

Enum 으로 관리

public enum Category {

    ROOT("카테고리", null),
    
        FASHION("패션의류/잡화", ROOT),
            FASHION_MEN("남성", FASHION),
                MEN_T_SHIRT("티셔츠", FASHION_MEN), 
                MEN_SWEATSHIRT_HOOD("스웻셔츠/후드", FASHION_MEN),
                MEN_SHIRT("셔츠", FASHION_MEN),
                MEN_SUIT("정장", FASHION_MEN),
                MEN_PANTS("바지", FASHION_MEN),
            FASHION_WOMEN("여성", FASHION),
                WOMEN_T_SHIRT("티셔츠", FASHION_WOMEN),
                WOMEN_BLOUSE("블라우스/셔츠", FASHION_WOMEN),
                WOMEN_SWEATSHIRT_HOOD("스웻셔츠/후드", FASHION_WOMEN),
                WOMEN_SUIT("정장", FASHION_WOMEN),
                WOMEN_ONE_PIECE("원피스", FASHION_WOMEN),
                WOMEN_SKIRT("치마", FASHION_WOMEN),
                WOMEN_PANTS("바지", FASHION_WOMEN),
            UNISEX("남녀공용", FASHION),
                UNISEX_T_SHIRT("티셔츠", UNISEX),
                UNISEX_PANTS("바지", UNISEX),
            BAG_ACC("가방/잡화", FASHION),
                BAG("가방", BAG_ACC),
                    BACKPACK("백팩", BAG),
                    CROSS_BAG("크로스백", BAG),
                    SHOULDER_BAG("숄더백", BAG),
                    MINI_BAG("미니백", BAG),
                    ECO_BAG("캔버스/에코백", BAG),
                WALLET_BELT("지갑/벨트", BAG_ACC),
                    WALLET("지갑", WALLET_BELT),
                    BELT("벨트", WALLET_BELT),
            SHOES("신발", FASHION),
                SNEAKERS("운동화/스니커즈", SHOES),
                FLAT_SHOES("단화/플랫", SHOES),
                HEEL("힐", SHOES),
                BOOTS("워커/부츠", SHOES),
                SLIPPER("슬리퍼", SHOES),
            FASHION_CHILDREN("아동", FASHION),
                GIRL_FASHION("여아", FASHION_CHILDREN),
                BOY_FASHION("남아", FASHION_CHILDREN),
                
        FOOD("식품", ROOT),
            INSTANT("가공/즉석식품", FOOD),
            BEVERAGE("생수/음료", FOOD),
            FRESH("신선식품", FOOD),
            MEAT_EGG("축산/계란", FOOD),
            RICE("쌀/잡곡", FOOD),
            
        DIGITAL("가전/디지털", ROOT),
            VIDEO("TV/영상가전", DIGITAL),
                TV("TV", VIDEO),
                PROJECTOR("프로젝터/스크린", VIDEO),
            COMPUTER("컴퓨터/게임/SW", DIGITAL);
    
    // 카테고리 이름
    private final String title;
  
    // 부모 카테고리
    private final Category parentCategory;
    
    // 자식카테고리
    private final List<Category> childCategories;

    Category(String title, Category parentCategory) {
        this.childCategories = new ArrayList<>();
        this.title = title;
        this.parentCategory = parentCategory;
        if(Objects.nonNull(parentCategory)) {
        	parentCategory.childCategories.add(this);
        }
    }

    public String getTitle() {
        return title;
    }
    
    // 부모카테고리 Getter
    public Optional<Category> getParentCategory() {
        return Optional.ofNullable(parentCategory);
    }

    // 자식카테고리 Getter
    public List<Category> getChildCategories() {
        return Collections.unmodifiableList(childCategories);
    }

    // 마지막 카테고리(상품추가 가능)인지 반환
    public boolean isLeafCategory() {
        return childCategories.isEmpty();
    }

    // 마지막 카테고리(상품추가 가능)들 반환
    public List<Category> getLeafCategories() {
        return Arrays.stream(Category.values())
                .filter(category -> category.isLeafCategoryOf(this))
                .collect(Collectors.toList());
    }
    
    private boolean isLeafCategoryOf(Category category) {
        return this.isLeafCategory() && category.contains(this);
    }

    private boolean contains(Category category) {
        if(this.equals(category)) return true;

        return Objects.nonNull(category.parentCategory) && 
        				this.contains(category.parentCategory);
    }
}

상황 2

문제 상황

  • 코드명("배달비", "가상계좌" 등...)만 or 코드번호(01, 02 등...)가 if문으로 구현되었을 때,
    이것들만 봐서는 해당 데이터가 무엇을 나타내는지 정확히 알 수 없다.
    → 따라서, 서버코드에서 실행되는 코드는 확인하고, 이를 DB 에서 조회해야한다.
    → 문서가 업데이트 됐는지 확신할 수 없기에, DB 에서 또다시 조회해야한다.
    → 이런 방시은 항상 '테이블 조회 쿼리'가 실행되었어야만 한다.

  • 카테고리 코드를 기반으로 한 서비스 로직 추가 시, 위치의 모호성
    → 해당 코드에 따라 수행 할 기능이 있을 경우, 메소드의 위치는 Service 또는 Utility Class ?

  • 카테고리 코드의 추가/수정은 그리 자주 일어나는 일이 아니므로, 굳이 테이블로 카테고리를 관리한다는 것은 비효율적

해결법

Enum 인스턴스 생성을 위한 interface 생성(클래스 선언)

public interface EnumMapperType {
	String getCode();		// 해당 Enum의 이름을 조회하는 변수 → DB 의 컬럼값으로 사용
    String getTitle();		// 해당 Enum의 설명을 조회하는 변수
}

모든 Enum 데이터들이 공통으로 갖는 데이터

interface 를 구현(implements)

@RequiredArgsConstructor
poublic enum FeeType implements EnumMapperType {

	// PERCENT, MONEY 는 code 에 해당 → 아래의 getCode() 통해 조회 가능
    // 정율, 정액은 title 에 해당
	PERCENT("정율"),
    MONEY("정액");
    
    @Getter
    private final Stirng title;		// Enum 의 변수들은 불변이므로, final 키워드를 붙여줄 수 있다
    
    FeeType(String title) {
    	this.title = title;
    }    
     
    @Override
    public String getCode() {
    	return name();
    }
    
    @Override
    public String getTitle() {
    	return title();
    }
}

interface 를 인자로 받아서, 인스턴스를 생성

public class EnumMapperValue {
	private String code;
    private String title;
    
    // interface 를 인자로 받음
    public EnumMapperValue (EnumMapperType enumMapperType) {
    	code = enumMapperType.getCode();
        title = enumMapperType.getTitle();
	}
    
    public String getCode() {
    	return code;
    }
    
    public String getTitle();
    	return title;
    }

	@Override
    public Stirng toString() {
    	return "{" + "code='" + code + '\'' + ", title='" + title + '\'' + '}'; 
    }
}

EnumMapperValue 는 EnumMapperType을 구현한(implements) 구현체에 대해 실제 값을 갖는다.

Enum 을 Value 클래스로 변환한 후, 전달

@GetMapping("/no-bean-categories")
pbulci List<EnumMapperValue> getNoBeanCategories() {
	return Arrays.stream(FeeType.values())
    		.map(EnumMapperValue::new)
            .collect(Collectors.toList());
}

JSON 으로 출력된 결과

[
  {
   "code": "PERCENT",
    "titel": "정율"
  },
  {
    "code": "MONEY",
    "titel": "정액"
  }
]  

상황 2

문제 상황

CodeVO 테이블 + 조회

@Entity
@Table("codeVO")
@Getter
@NoArgsConstructor
public class CodeVO extends CommonVO implements Serializable {

    private String codeSn;					// 코드일련번호
    
    @Setter
    private String lvl1;					// 레벨1코드
    
    @Setter
    private String lvl2;					// 레벨2코드
    private String codeName;				// 코드한글명
    private String codeDesc;				// 코드설명
    
    @Setter
    private String selType;					// 조회유형

    @Builder
    public CodeVO(String lvl1, String selType){
        this.lvl1 = lvl1;
        this.selType = selType;
    }
}

상수와 관련된 코드들을 별도의 테이블로 만들어서 관리한다.

BoardVO 테이블 + 조회

public class BoardVO extends CodeVO implements Serializable {
    private String title;					// 제목
    private String contents;				// 내용
    private String status;					// 상태
}

BoardController 로 조회하기

@RestController
@RequestMapping("/board")
@Log4j2
public class BoardController {

    @Resource(name = "boardService")
    private BoardService boardService;

    @GetMapping(value = "/list")
    public ResponseEntity<List<BoardVO>> list() {
    	BoardVO boardVO = new BoardVO();
        boardVO.setLvl1("001");
        boardVO.setLvl2("002");
        List<BoardVO> boardList = boardService.findAll(boardVO);
        return new ResponseEntity.ok(boardList);
    }
}
  • 게시물 조회 시, 게시물의 Code Lvl1 이 어떤 값인지 계속 확인이 필요
  • 만약 Code 테이블이 잘못되었을 경우, 확인이 어렵다

해결법

모든 Enum 데이터들이 공통으로 갖는 데이터(인터페이스)

public interface EnumMapperType {	
    String getCode();		// 해당 Enum의 이름을 조회하는 변수	
    String getTitle();		// 해당 Enum의 설명을 조회하는 변수
}

interface 를 구현

@RequiredArgsConstructor
public enum Status implements EnumMapperType {

	// PROCEEDING, COMPLETE 는 code 에 해당
    // "진행중", "진행완료" 는 title 에 해당
    PROCEEDING("진행중"),
    COMPLETE("진행완료");

	// Enum의 변수들은 불변이기 때문에 final 키워드를 붙여줄 수 있다
    @Getter
    private final String title;

	// getCode()를 통해 위의 code 를 조회할 수 있다
    @Override
    public String getCode() {
        return name();
    }
}

Enum 종류들을 관리하기 위한 EnumMapperFactory

@Getter
@AllArgsConstructor
public class EnumMapperFactory {
	// 다양한 종류의 Enum을 생성 및 관리하는 factory
    private Map<String, List<EnumMapperValue>> factory;

    // 새로운 Enum 종류를 추가하는 함수
    public void put(String key, Class<? extends EnumMapperType> e) {
        factory.put(key, toEnumValues(e));
    }

    // 특정 Enum의 항목들을 조회하는 함수
    public List<EnumMapperValue> get(String key) {
        return factory.get(key);
    }

    // Enum의 내용들을 List로 바꾸어주는 함수
    	// toEnumValues 함수 :  Enum 타입을 Enum의 Code와 Title을 변수로 갖는 EnumMapperValue 로 변환시켜줌
    private List<EnumMapperValue> toEnumValues(Class<? extends EnumMapperType> e) {
        return Arrays.stream(e.getEnumConstants()).map(EnumMapperValue::new)
                .collect(Collectors.toList());
    }
}
  • Status 외에 Category 등... 다른 Enum 항목들을 쉽게 관리하기 위한 클래스
  • interface를 구현하는 Status는 {PROCEEDING, "진행중"}, {COMPLETE, "진행완료"}를 갖는데,
    EnumMapperFactory 는 이러한 Enum의 항목들을 순차적으로 접근하여,
    code와 title을 변수로 갖는 EnumMapperType의 객체로 새로 생성하여 List로 모은다.

EnumMapperValue

@Getter
public class EnumMapperValue {
    private String code;
    private String title;

    public EnumMapperValue(EnumMapperType enumMapperType) {
        code = enumMapperType.getCode();
        title = enumMapperType.getTitle();
    }
}

EnumMapperType을 implements한 구현체에 대해 실제 값을 갖는다.

Enum Status 를 EnumMapperFactory 에 등록

@Configuration
public class EnumMapper {

    @Bean
    public EnumMapperFactory createEnumMapperFactory() {
        EnumMapperFactory enumMapperFactory = new EnumMapperFactory(new LinkedHashMap<>());
        enumMapperFactory.put("Status", Status.class);
        return enumMapperFactory;
    }
}

BoardVO 테이블 + 조회

public class BoardVO extends CodeVO implements Serializable {

	// 수정 전
    private String title;					// 제목
    private String contents;				// 내용
    private String status;					// 상태
    
    // 수정 후
    private String title;					// 제목
    private String contents;				// 내용
   
    @Column(nullable = false, length = 10)
    @Enumerated(EnumType.STRING)	// Enum의 Code가 DB에 저장되도록 설정
    private Status status;					// 상태
}

DB 에 insert 했을 때
→ Query 속도↑ 가동성↑

프론트 쪽으로 상태를 전달해주려면?

@ReqruiedArgsConstructor
@RestController
public class EnumsController {

    private final EnumMapperFactory enumMapperFactory;

    @GetMapping("/status")
    public ResponseEntity status(){
        return ResponseEntity.ok(enumMapperFactory.get("Status"));
    }
}

6. 데이터들 간의 연관관계

문제 상황

origin 테이블에 있는 내용을 table1 테이블, table2 테이블에 등록

public class LegacyCase {
	public String toTable1Value (String originValue) {
    	if ("Y".equals(originValue)) {
        	return "1"
        } else {
        	return "0";
        }
    }
    
    public boolean toTable2Value (String originValue) {
    	if ("Y".equals(originValue)) {
        	return true;
        } else {
        	return false;
        }
    }    
}

여기서 문제가 발생한다.

  • "Y", "1", true 는 모두 같은 의미
    → Y 가 "1" 이 될 수도 있고, true 가 될 수도 있다.
    → 이를 확인하기 위해서는 항상 위에서 선언된 클래스와 메소드를 찾아야만 힌다.

  • 불필요한 코드량 多
    → (Y, N 외에) R, S 등...의 추가 값이 필요한 경우, if문을 포함한 메소드 단위로 코드가 증가된다.
    → 동일한 타입의 값이 추가되는것에 비해, 너무 많은 반복성 코드가 발생

해결법

위의 코드를 Enum 으로 구현

public enum TableStatus {
	Y ("1", true),		// "Y", "1", true 를 한 묶음으로
    N ("0", false);		// "N", "0", false 를 한 묶음으로
        
    private String table1Value;
    private boolean table2Value;
    
    TableStatus (String table1Value, boolean table2Value) {
    	this.table1Value = table1Value;
        this.table2Value = table2Value;
    }
    
    public String getTable1Value() {	// @Getter 을 사용하면, 더 깔끔한 코드가 된다.
    	return table1Value;
    }
    
    public boolean isTable2Vaule() {
    	return table2Value;
    }
}
  • 여기서 추가 타입이 필요한 경우, Enum 상수와 get메소드만 추가하면 된다.

TableStatus 의 데이터를 전달받는 곳

@Test
public void origin테이블에서_조회한_데이터를_다른_2테이블에_등록한다() throws Exception {

	TableStaus origin = selectFromOriginTable();    
    
    String table1Value = origin.getTable1Value();
    boolean table2Value = origin.getTable2Value();
    
    assertThat(origin, is(TableStaus.Y));
    assertThat(table1Value, is("1"));
    assertThat(table2Value, is(true));
}
  • TableStatus 라는 Enum 타입을 전달받아서, 그에 맞춘 table1, table2값을 바로 얻을 수 있다.

7. 한 곳에서 상태와 행위를 관리

문제 상황

계산식 설정

public class LegacyCalculator {
	public static long calculate (String code, long originValue) {
    	if ("CALC_A".equals(code)) {		// DB 에 저장된 code의 값이 "CALC_A"일 경우, 값 그대로 전달
        	return originValue;
        } else if ("CALC_B".equals(code)) {		// DB 에 저장된 code의 값이 "CALC_B"일 경우, * 10 한 값을 전달
        	return originValue * 10;
        } else if ("CALC_C".equals(code)) {		// DB 에 저장된 code의 값이 "CALC_C"일 경우, * 3 한 값을 전달
        	return originValue * 3;
        } else {
        	return 0;
    }
}

메소드 호출하기

@Test
public void 코드에_따라_서로다른_계산하기_기존방식() throws Exception {

	// 코드는 코드대로 조회
	String code = selectCode();
    
    // 계산은 메소드를 통해 코드와는 별개로 진행
    logn originValue = 10000L;
    long result = LegacyCalculator.calculate(code, originValue);
    
    assertThat(result, is(10000L));
}
  • LegacyCalculator 의 메소드인 calculate() 와 code 가 서로 관계있다는 것을 표현 할 수 없다.

  • 계산 메소드를 만들어놓은 것을 잊어버리고, 동일한 기능의 메소드를 중복 생성하는 경우 발생

해결법

위의 코드를 Enum 으로 구현

public enum CalculatorType {
	CALC_A (value -> value),
    CALC_B (value -> value * 10),
    CALC_C (value -> value * 3),
    CALC_ETC (value -> 0L),
    
    private Function<Long, Long> expression;
    
    CalculatorType(Function<Long, Long> expression) {
    	this.expression = expression;
    }
        
    public long  calculate (lone value) {
    	return expression.apply(value);
    }    
}
  • Code 가 본인만의 계산식을 갖도록 만든다.

Enum 을 Entity 클래스에 선언할 경우

@Column
@Enumerated(EnumType.STRING)	// Enum 의 name 인 'CALC_A', 'CALC_B', 'CALC_C', 'CALC_ETC' 이 저장된다
private CalculatorType calculatorType;

메소드 호출하기

@Test
public void 코드에_따라_서로다른_계산하기_변경된방식() throws Exception {

	CalculatorType code = selectType();
    
    // 계산을 코드에게 직접 요청
    logn originValue = 10000L;
    long result = code.calculate(originValue);
    
    assertThat(result, is(10000L));
}
  • 값(상태)과 메소드(행위)가 어떤 관계가 있는지에 대해 더이상 다른 곳을 찾을 필요가 없게 됐다.

8. 데이터 그룹관리

문제 상황

결제 종류 & 결제 수단

public class LegacyPayGroup {
	public static String getPayGroup(String PayCode) {
    	if ("ACCOUNT TRANSFER".equals(payCode) || "REMITTANCE".equals(payCode) || "ONE_SITE_PAYMENT".equals(payCode) || "TOSS".equals(payCode)) {
        	return "CASH";
            
        } else if ("PAYCO".equals(payCode) || "CARD".equals(payCode) || "KAKAO_PAY".equals(payCode) || "BAEMIN_PAY".equals(payCode)) {
        	return "CARD";
        
        } else if ("POINT".equals(payCode) || "COUPON".equals(payCode)) {
        	return "ETC";
        
        } else {
        	return "EMPTY";
    }
}

메소드 구현

// PayGroup 을 기준으로 수행되는 메소드가 추가될 때마다, 이런 코드는 계속 늘어나게 된다.

// PayGroup 메소드 1
public void pushByPayGroup(final String payGroupCode) {
	if ("CASH".equals(payGroupCode)) {
    	pushCashMethod();
    } else if ("CARD".equals(payGroupCode)) {
    	pushCardMethod();
    } else if ("ETC".equals(payGroupCode)) {
    	pushEtcMethod();
    } else {
    	throw new RuntimeException("payGroupCode가 없습니다.");
    }
}    

// PayGroup 메소드 2
public void printByPayGroup(final String payGroupCode) { 
    if ("CASH".equals(payGroupCode)) {
    	doCashMethod();
    } else if ("CARD".equals(payGroupCode)) {
    	doCardMethod();
    } else if ("ETC".equals(payGroupCode)) {
    	doEtcMethod();
    } else {
    	throw new RuntimeException("payGroupCode가 없습니다.");
    }
}
  • 결제가 이루어지면, 해당 '결제' 가 어떤 결제 종류/수단인지 확인 할 수 있어야 한다.
    → 결제 종류가 결제 수단을 포함하는 관계를 보여야하나, 위의 코드에서 사용된 메소드만으로는 표현이 어렵다.

  • 결제의 결과를 주고받을 때, 결제의 종류로 지정된 값만 받도록 검증 코드가 필요해진다.

  • 그룹별 기능 추가가 어렵다.
    (결제 종류에 따라, 추가 기능이 필요할 경우...)

해결법

결제 종류를 Enum 으로 구현

public enum PayGroup {

	// PayGroup 의 Enum 상수들
	CASH("현급", Arrays.asList("ACCOUNT_TRANSFER", "REMITTANCE", "ONE_SITE_PAYMENT", "TOSS")),
    CARD("카드", Arrays.asList("PAYCO", "CARD", "KAKAO_PAY", "BAEMIN_PAY")),
    ETC("기타", Arrays.asList("POINT", "COUPON")),
    EMPTY("없음", Collections.EMPTY_LIST);
    
    private String title;
    private List<String> payList;
    
    PayGroup(String title, List<String> payList) {
    	this.title = title;
        this.payList = payList;
    }
    
    // 확인하기 1
    public static PayGroup findByPayCode(String code) {
    	return Arrays.stream(PayGroup.values())			// PayGroup 의 Enum 상수들을 순회하며
        		.filter(payGroup -> payGroup.hasPayCode(code))		// payCode 를 갖고 있는 게 있는지 확인
                .findAny()
                .orElse(EMPTY);
    }
    
    // 확인하기 2
    public boolean hasPayCode(String code) {
    	return payList.stream()
        		.anyMatch(pay -> pay.equals(code));
    }
    
    public String getTitle() {
    	return title;
        }
}

메소드 호출하기

@Test
public void PayGroup에게_직접_결제종류_물어보기_문자열() throws Exception {
	String payCode = selectPayCode();
    PayGroup payGroup = PayGroup.findByPayCode(payCode);
    
    assertThat(payGroup.name(), is("BAEMIN_PAY));
    assertThat(payGroup.getTitle(), is("배민페이));
}

결제 수단을 Enum 으로 구현

public enum PayGroupAdvanced {

	// PayGroup 의 Enum 상수들
	CASH("현급", Arrays.asList("ACCOUNT_TRANSFER", "REMITTANCE", "ONE_SITE_PAYMENT", "TOSS")),
    CARD("카드", Arrays.asList("PAYCO", "CARD", "KAKAO_PAY", "BAEMIN_PAY")),
    ETC("기타", Arrays.asList("POINT", "COUPON")),
    EMPTY("없음", Collections.EMPTY_LIST);
    
    private String title;
    private List<String> payList;
    
    PayGroupAdvanced(String title, List<String> payList) {
    	this.title = title;
        this.payList = payList;
    }
    
    // 확인하기 1
    public static PayGroupAdvanced findByPayType(String code) {
    	return Arrays.stream(PayGroupAdvanced.values())			// PayGroup 의 Enum 상수들을 순회하며
        		.filter(payGroup -> payGroup.hasPayCode(code))		// payCode 를 갖고 있는 게 있는지 확인
                .findAny()
                .orElse(EMPTY);
    }
    
    // 확인하기 2
    public boolean hasPayCode(PayType payType) {
    	return payList.stream()
        		.anyMatch(pay -> pay == payType);
    }
    
    public String getTitle() {
    	return title;
        }
}
public enum PayType {
	ACCOUNT_TRANSFER("계좌이체"),
    REMITTANCE("무통장입금"),
    ONE_SITE_PAYMENT("현장결제"),
    TOSS("토스"),
    PAYCO("페이코"),
    CARD("신용카드"),
    KAKAO_PAY("카카오페이"),
    BAEMIN_PAY("배민페이"),
    POINT("포인트"),
    COUPON("쿠폰"),
    EMPTY("없음");
    
    private Stirng title;
    
    PayType(Stirng title) {
    	this.title = title;
        
    public Stirng getTitle() {
    	return title;
    }
}

메소드 호출하기

@Test
public void PayGroup에게_직접_결제종류_물어보기_PayType() throws Exception {
	PayType payType = selectPayType();
    PayGroupAdvanced payGroupAdvanced = PayGroupAdvanced.findByPayType(payType);
    
    assertThat(payGroupAdvanced.name(), is("BAEMIN_PAY));
    assertThat(payGroupAdvanced.getTitle(), is("배민페이));
}

9. Canlendar 클래스 & get() 메소드

예시 1

int year = now.get(Canlendar.YEAR);            // 연
int month = now.get(Canlendar.MONTH);          // 월(1~12)
int day = now.get(Canlendar.DAY_OF_MONTH);     // 일
int week = now.get(Canlendar.DAY_OF_WEEK);     // 요일(1~7)
int hour = now.get(Canlendar.HOUR);            // 시간
int minute = now.get(Canlendar.MINUTE);        // 분
int second = now.get(Canlendar.SECOND);        // 초

예시 2

public static void main(String[] args) {
	Week today = null;		// 열거 타입 변수를 선언

	Canlendar cal = Calendar.getInstance();		// 오늘의 요일을 구한다.
	int week = cal.get(Calendar.DAY_OF_WEEK);		// 일요일~토요일(일주일)

	// 열거 타입 변수 today에 해당 열거 상수(요일)를 대입
	switch(week) {
		case 1:
			today = Week.SUNDAY;
			break;
		case 2:
			today = Week.MONDAY;
			break;
		.
		.
		.
		case 7:
			today = Week.SATURDAY;
			break;
	}

	System.out.println("오늘 요일: " + today);

	// switch 문에서 얻은 요일을 바탕으로, if 문 실행
	if (today == Week.SUNDAY) {
		System.out.println("일요일에는 축구를 합니다.");
	} else {
		System.out.println("열심히 자바 공부합니다.");
	}
}
  • 열거 타입은 참조 타입이기 때문에, 열거 타입 변수에 null값을 저장할 수 있음

참고: Java Enum 활용기
참고: Enum 클래스
참고: Java Enum 1편 : Enum 기본적인 사용
참고: Java enum의 사용
참고: Java Enum 2편 : 여러가지 활용법
참고: 상품 카테고리 Enum으로 관리
참고: [Java] Enum 사용법 - 고급(Enum 응용하기)

profile
개발자로 거듭나기!

0개의 댓글