외부활동/우아한테크코스 [프리코스]

ErrorMessage Enum 관리 및 간소화 시도

softmoca__ 2025. 10. 29. 17:34
목차

프리코스 1주차와 2주차 미션을 진행하고 코드리뷰를 통해 3주차에서는 ErrorMessage Enum 을 도입하게 되었다.

도입 후 확실한 장점을 느끼게 되어 이에 대해 정리해 보고자 한다.

 

1,2주차에서는  ErrorMessage를 상수로 해당 클래스 내부에서 선언해서 사용을 했지만 테스트코드에서 메시지 내용까지는 검증하지 않았다. 각 클래스가 아닌 다른 부분에서 검증을 해야하는 경우 상수를 테스트코드 내부로 옮기거나 직접 타이핑을 했어야 했지만 매번 패키지들을 넘나들며 에러 문구를 확인하고 수정했어야 했다.

또한 한줄 한줄 매번 복사 붙여넣기 혹은 타이핑 하는 과정에 꽤나 피로감이 많게 느껴졌다 .

또한 3주차 미션 요구사항에 아래와 같은 요구 사항이 추가되어 더욱 Enum을 사용해ErrorMessage를 관리하면 더욱 효과적이라 생각을 했다.

'사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.'

 

 

기존 문제점

// InputValidator.java
throw new IllegalArgumentException("[ERROR] 구입 금액은 숫자여야 합니다.");

// Lotto.java
throw new IllegalArgumentException("[ERROR] 로또 번호는 6개여야 합니다.");

// WinningNumbers.java
throw new IllegalArgumentException("[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.");

  • 중복 코드: [ERROR] 접두사를 매번 작성
  • 오타 위험: "구입"을 "구입금"으로 잘못 쓸 수 있음
  • 일관성 부족: "입력해 주세요" vs "입력하세요" 혼용 가능
  • 유지보수 어려움: 메시지 변경 시 모든 파일 수정 필요
  • 찾기 어려움: 에러 메시지 전체 목록 파악 불가

-> 전체적인 개발 효율이 떨어지고 피로감이 상당하다 !

 

 

주요 장점

장점 1: 컴파일 타임 안전성

// ❌ 문자열: 오타가 있어도 컴파일 성공
throw new IllegalArgumentException("[ERROR] 로또 번호는 6게여야 합니다.");  // "6게" 오타

// ✅ Enum: 오타 시 컴파일 에러
throw new IllegalArgumentException(ErrorMessage.INVALID_LOTTO_SZIE.getMessage());  // 컴파일 에러!

- IDE 자동 완성 지원

- 오타 즉시 탐지 가능

개발 편의성 측면의 장점이지만 개인적으로 해당 장점이 가장 크게 체감되었다,

IDE 내부 패키지들을 넘나들고 복사붙여넣기하고, 직접 타이핑을 하지 않아도 되어 개발 편의성이 상당히 올라갔다 !

 장점 2: 중앙 집중식 관리

// 모든 에러 메시지가 한 곳에!
public enum ErrorMessage {
    // 구입 금액 관련 (3개)
    INVALID_PURCHASE_AMOUNT_FORMAT("..."),
    INVALID_PURCHASE_AMOUNT_POSITIVE("..."),
    INVALID_PURCHASE_AMOUNT_UNIT("..."),

    // 로또 번호 관련 (4개)
    INVALID_LOTTO_SIZE("..."),
    // ...
}

-  프로젝트의 모든 에러 메시지를 한눈에 파악

-  중복 메시지 쉽게 발견

-  용어 일관성 유지 (예: "숫자" vs "정수" 통일)

장점 1과 이어지지는 장점으로서 여러 패키지를 넘나들며 일관성을 해치는 원인 자체를 없애준다 !

 

장점 3: 에러 코드 관련 기능 추가  및 유지보수 용이성

 Before: 여러 곳의 파일 수정

// InputValidator.java
"[ERROR] 구입 금액은 숫자여야 합니다."
→ "[ERROR] 구입 금액은 숫자여야 합니다. 다시 입력해 주세요."

// Lotto.java
"[ERROR] 로또 번호는 6개여야 합니다."
→ "[ERROR] 로또 번호는 6개여야 합니다. 다시 입력해 주세요."

 

After: 1개 메서드만 수정

public enum ErrorMessage {
    INVALID_LOTTO_SIZE("L001", "로또 번호는 6개여야 합니다."),
    INVALID_BONUS_NUMBER_DUPLICATION("B001", "보너스 번호는 당첨 번호와 중복될 수 없습니다.");

    private final String code;
    private final String message;

    public String getFullMessage() {
        return String.format("[ERROR:%s] %s", code, message);
    }
        public String getMessage() {
        return ERROR_PREFIX + message + " 다시 입력해 주세요.";  // 한 줄만 수정!
    }
}

 메시지 변경을 해야할 시 아주 간단하게 휴먼 에러가능성을 줄이며 변경을 할 수 있다.

 

장점 4: 테스트 작성 용이

// ❌ Before: 문자열 하드코딩
@Test
void 로또_번호_6개_초과시_예외() {
    assertThatThrownBy(() -> new Lotto(List.of(1,2,3,4,5,6,7)))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessage("[ERROR] 로또 번호는 6개여야 합니다.");  // 오타 위험!
}

// ✅ After: Enum 사용
@Test
void 로또_번호_6개_초과시_예외() {
    assertThatThrownBy(() -> new Lotto(List.of(1,2,3,4,5,6,7)))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessage(ErrorMessage.INVALID_LOTTO_SIZE.getMessage());  // 안전!
}

 

장점 5: 문서화 효과

public enum ErrorMessage {
    // === 구입 금액 검증 에러 ===
    /** 구입 금액이 숫자가 아닐 때 */
    INVALID_PURCHASE_AMOUNT_FORMAT("구입 금액은 숫자여야 합니다."),

    /** 구입 금액이 0 이하일 때 */
    INVALID_PURCHASE_AMOUNT_POSITIVE("구입 금액은 0보다 커야 합니다."),

    // === 로또 번호 검증 에러 ===
    /** 로또 번호가 6개가 아닐 때 */
    INVALID_LOTTO_SIZE("로또 번호는 6개여야 합니다."),

    // ...
}

- 단어 상수만 봐도 어떤 기능과 어떤 제한이 있는지 실제 코드 내에서 살아있는 문서로서의 역할을 한다 

 

 

 

 

간소화 방법

ErrorMessage Enum 도입 후 한 가지 아쉬운 점이 있었다.

throw new IllegalArgumentException(ErrorMessage.INVALID_PURCHASE_AMOUNT_POSITIVE.getMessage());

코드가 너무 길어서 가독성이 떨어진다는 점이었다.

이를 개선하기 위해 여러 방법을 고민해보았다.

1. Static Import 사용

import static lotto.exception.ErrorMessage.*;

public class InputValidator {
    public static int validatePurchaseAmount(String input) {
        if (amount <= 0) {
            throw new IllegalArgumentException(
                INVALID_PURCHASE_AMOUNT_POSITIVE.getMessage());
        }
    }
}

코드가 간결해지고, Java 표준 관례를 따른다, 하지만 어느 클래스의 상수인지 바로 보지이 않는 약간의 단점이 있지만 IDE가 자동 완성을 지원해 주니, 해당 단점은 거의 체감이 안되었다.

2. toString() 오버라이드로 더 간소화

public enum ErrorMessage {
    // ...
    
    @Override
    public String toString() {
        return getMessage();
    }
}

// 사용
throw new IllegalArgumentException(INVALID_PURCHASE_AMOUNT_POSITIVE);

 

getMessage() 호출까지 제거되어 마치 문자열 상수처럼 직관적으로 사용 가능하며 내가 가장 원하던 방향이었다.

 

내가 생각한 동작은 아래와 같았다.

// IllegalArgumentException 생성자
public IllegalArgumentException(String message)

// Java가 자동으로 수행하는 변환
throw new IllegalArgumentException(INVALID_PURCHASE_AMOUNT_POSITIVE);
→ Java가 Enum을 String으로 변환 필요
→ toString() 메서드 자동 호출
→ getMessage() 반환
→ "[ERROR] 구입 금액은 0보다 커야 합니다." 전달

Implicit conversion of enum constant to String"
Wrap with 'String.valueOf()' to make the conversion explicit

하지만  IntelliJ와 Java는 명시성(explicitness) 을 중요하게 여긴다.

Enum을 String으로 암묵적으로 변환하는 것은 개발자의 의도가 명확하지 않다고 판단하여 해당 흐름을 막는것이 문제였다..!

 

결국은 아래처럼 사용을 해야했다.

 

  • String.valueOf(INVALID_PURCHASE_AMOUNT_POSITIVE) 
  • INVALID_PURCHASE_AMOUNT_POSITIVE.toString() 
  • IntelliJ 설정 조작

 

하지만 결국은 메서드명만 달라졌을 뿐 코드가 길어지는건 변함이 없었다..!

 

3. toException() 헬퍼 메서드 추가

public enum ErrorMessage {
    // ...
    
    public IllegalArgumentException toException() {
        return new IllegalArgumentException(getMessage());
    }
}

// 사용
throw INVALID_PURCHASE_AMOUNT_POSITIVE.toException();

2번과 비슷하게 상당히 짧고 깔끔하게 상요을 할수 있다 ! 거기다 의미까지 상당히 명확하다고 생각을 했다.

그래서 Static import와 함께 사용하면 딱 맞겠다 싶었다 !

 

하지만 문득 너무 간소화에만 집중을 해서 기본적인 원칙을 무시하지 않았나 싶은 걱정이 들었다.

 

1) 단일 책임 원칙(SRP) 위반 아닌가?

// ErrorMessage의 원래 책임
public enum ErrorMessage {
    // 1. 에러 메시지 상수 정의 ✅
    // 2. 메시지 포맷팅 (접두사 추가) ✅
    
    // 3. 예외 객체 생성 ❓ <- 이게 ErrorMessage의 책임인가?
}

ErrorMessage는 "에러 메시지를 관리하는 것" 이 핵심 책임이다.

하지만 toException() 메서드를 추가하면 "예외 객체를 생성하는 책임" 까지 가지게 된다..!

 

2) 과도한 설계로 혼란이 더 커지는건 아닌가?
 toException() 메서드가 진짜 필요한가?

 단순히 코드 몇 자 줄이려고 메서드를 추가하는 것은 아닌가?

실무에서 나중에 다른 개발자가 봤을 때 "왜 이런 메서드가?"라고 생각하지 않을까?

 

3) 만약 나중에 다른 예외 타입이 필요하다면?

 
IllegalArgumentException 뿐만 아니라 다른 예외 타입을 처리해야한다면? 그에 맞는 메서드를 또 추가?
-> 결국은 간소화를 위해 오히려 복잡해지고 범용성이 떨어질것 같다 !
 
너무 이번 3주차 미션 한정 해결법이라고 깨달았다.
즉,  실용성 vs 원칙 사이의 트레이드오프를 고려해야했다 !
 
 

최종 선택: Static Import 만 사용

여러 방법을 고민한 끝에 Static Import만 사용하기로 결정했다.

  • toException() 메서드는 실용적이지만, ErrorMessage가 예외 생성이라는 추가 책임을 가지게 되어 과도한 설계라고 판단
  • Static Import만으로도 충분히 코드가 간결해지고 의미가 명확함
  • Java의 표준 관례를 따르는 방법
  • 단순하고 직관적이며, 추가적인 메서드 없이도 목적을 달성

 

간소화 라는 목적 하나만 보고 달려가며 때로는 가장 단순한 해법이 최선의 선택이 될 수 있다는 생각을 했다 !

ErrorMessage Enum의 핵심 목적인 '에러 메시지의 중앙 집중 관리'와 '타입 안정성'을 해치지 않으면서도, 불필요한 복잡도를 추가하지 않는 선에서 타협점을 찾은 것이다 !

 

이번 간소화를 시도하며 느꼈던 것은 역시나 '트레이드 오프' 와 '잘못된 몰입 방향성' 이었다..!

많은 시도와 시간을 투자했지만 더더 조금 더 좋은 방법이 없나..하는 너무 과한 욕심과 단 하나의 목적만을 위해 다른 원칙들을 고려하지 못한 나 자신을 다시금 볼 수 있었다 !

 

하지만 이 또한 너무 좋은 경험이라 생각한다.

역시 이론만 이해한 뒤 그저 사용하는것 보다 이런저런 시도와 삽질을 하며 몸소 부딪히며 더욱 많은 관점을 다시 생각해 보며 시야를 키울 수 있는것 같다 !