프로그래밍에서의 매직 — 개념, 매직 넘버의 위험성 및 대체 방법

저자: IT Sectr 게시일: 2026-07-27 읽는 시간: 10 분

프로그래밍에서 매직은 은유가 아니라, 맥락에서 의미가 명확하지 않고 이해를 위해 외부 지식이 필요한 값(숫자, 문자열, 플래그)을 가리키는 정확한 용어입니다. 가장 흔한 매직의 유형은 매직 넘버입니다. 특정 값을 선택한 이유에 대한 설명 없이 코드에 직접 작성된 숫자 상수입니다. SonarSource 코드 품질 보고서(2025)에 따르면, 모든 정적 분석기 경고의 약 8퍼센트가 설명되지 않은 리터럴과 관련되어 있습니다. 매직 값은 코드를 취약하게 만듭니다. 변경하려면 모든 발생 위치를 찾아야 하고, 새 개발자는 숫자를 건드려도 되는지 아니면 시스템 작동에 중요한지 알 수 없습니다.

핵심 요점

  • 매직 — 코드에 내재된 숫자, 문자열, 플래그로, 그 의미가 독자에게 숨겨져 있습니다.
  • 매직 넘버 — 이름 없는 숫자 리터럴: 86400, 3.14, 0.85, 1024.
  • 매직 문자열 — 상수로 추출하지 않고 하드코딩된 경로, 키, URL.
  • 감지 도구: SonarQube(MagicNumber 규칙), ESLint(no-magic-numbers), Detekt.
  • 해결책: 각 매직 값을 설명적인 이름을 가진 이름 있는 상수로 추출합니다.

프로그래밍에서 매직이란?

매직은 추가 도메인 지식 없이는 의미가 명확하지 않은 소스 코드의 모든 값입니다. 이 용어는 커뮤니티에 확립되어 있습니다. 개발자가 숫자를 보고 그것이 어디서 왔는지 말할 수 없다면 — 그것이 매직입니다.

매직에는 여러 유형이 있습니다: 숫자형(매직 넘버), 문자열형(매직 문자열), 부울형(매직 플래그), 설정형(설정 파일에 있어야 할 하드코딩된 매개변수). 네 가지 유형 모두 하나의 문제를 공유합니다. 요구사항이 변경될 때, 개발자는 값이 사용된 모든 위치를 찾아 수동으로 대체해야 합니다. 단 한 곳이라도 놓치면 버그가 발생합니다.

JetBrains 코드 품질 설문조사(2025)에 따르면, 73퍼센트의 개발자가 매직 넘버를 낮은 코드 품질의 지표로 간주하는 반면, 41퍼센트는 가끔 남겨둔다고 인정합니다. 주된 이유는 서두름입니다. “나중에 상수를 추가하겠지” — 하지만 그 “나중”은 결코 오지 않으며, 한 달 후에도 0.85라는 숫자가 설명 없이 메서드 본문 중간에 남아 있습니다.

핵심 규칙: 0, 1, true, false, 빈 문자열을 제외한 모든 리터럴 값은 이름 있는 상수로 추출해야 합니다. 예외: 카운터 증가(i + 1), 수학적 0(0 확인), 누산기의 초기값. 나머지는 모두 이름을 지정해야 할 대상입니다.

매직 넘버와 그 위험성

매직 넘버는 맥락에서 값이 명확하지 않은 숫자 리터럴입니다. 전형적인 예: 타임아웃 관련 코드의 86400. 개발자는 이 숫자를 보고 하루의 초 수임을 추측해야 합니다. 실수로 84600이라고 쓰면 타임아웃이 18분 일찍 발생하기 때문에 버그를 잡기 어렵습니다.

매직 넘버가 위험한 이유: 첫째, 가독성을 해칩니다. 숫자 1024는 킬로바이트 크기, 페이지 매김 임계값, 또는 최대 항목 수를 의미할 수 있습니다. 맥락이 없으면 — 그저 숫자일 뿐입니다. 둘째, 중복을 만듭니다: 1024가 다섯 곳에서 사용되는 경우, 임계값이 2048로 변경될 때 개발자는 다섯 곳을 모두 찾아 교체해야 합니다. 한 곳이라도 놓치면 시스템이 명시적 오류 없이 잘못 작동합니다.

매직 넘버 예시: 전후 비교

kotlin
// before - magic in its pure form
fun calculateTimeout(base: Int): Int {
    return base * 3 + 5000
}

// after - values replaced with constants
private const val RETRY_MULTIPLIER = 3
private const val BASE_TIMEOUT_MS = 5000

fun calculateTimeout(base: Int): Int {
    return base * RETRY_MULTIPLIER + BASE_TIMEOUT_MS
}

세 번째 위험은 테스트 불가능성입니다. 임계값이 리터럴로 하드코딩된 경우, 테스트가 이를 재정의하여 경계 조건을 확인할 수 없습니다. companion object나 설정 파일로 추출된 상수는 코드를 테스트 가능하게 만듭니다. 테스트가 다른 값을 대입하여 경계에서의 시스템 동작을 확인합니다.

습관을 들이세요: 0, 1, 100, 2가 아닌 숫자를 쓸 때마다 — 멈추고 상수로 추출해야 할지 고려하세요. 숫자가 비즈니스 로직(제한, 임계값, 타임아웃, 크기)과 관련된 경우 — 주저하지 말고 추출하세요. 숫자가 수학 상수(pi, e)인 경우 — 표준 라이브러리(Math.PI, Math.E)를 사용하세요.

매직 문자열과 경로

매직 문자열은 상수나 리소스로 추출되지 않고 코드에 내장된 문자열 리터럴입니다. 일반적인 예: 엔드포인트 URL, SharedPreferences 키 이름, Intent Action, 번들 키, 파일 이름, SQL 쿼리.

매직 문자열의 위험성은 컴파일 타임 검사 부재입니다. “user_prefs” 문자열의 오타는 런타임까지 발견되지 않습니다. 문자열이 열 곳에서 사용되고 개발자가 그중 한 곳에서 “user_pref”(s 빠짐)라고 쓰면 — 앱은 충돌하지 않지만 데이터는 저장되지 않습니다. 이러한 버그는 충돌을 일으키지 않기 때문에 몇 달간 프로덕션에 남아 있을 수 있습니다.

Android 프로젝트의 경우, 매직 문자열은 리소스(strings.xml, arrays.xml)나 companion object의 상수로 추출해야 합니다. iOS의 경우 — 문자열 리소스(Localizable.strings)나 enum 상수로. 백엔드의 경우 — 설정 파일(.env, application.properties)로. 어떤 키, URL, 경로도 코드에 문자열 리터럴로 존재해서는 안 됩니다.

swift
// before - magic strings across the class
let prefs = UserDefaults.standard
prefs.set(token, forKey: "auth_token")
prefs.set(userId, forKey: "current_user_id")

// after - strings extracted to enum
enum PrefKeys: String {
    case authToken = "auth_token"
    case currentUserId = "current_user_id"
}

prefs.set(token, forKey: PrefKeys.authToken.rawValue)
prefs.set(userId, forKey: PrefKeys.currentUserId.rawValue)

중복되는 문자열에 특별히 주의하세요. 동일한 키 “user_settings”가 세 파일에 나타나는 경우 — 99퍼센트 확률로 결국 그중 하나에 오타가 생깁니다. enum이나 상수로 추출하면 모든 참조가 동일한 값을 사용함을 보장합니다.

매직 플래그와 부울 매개변수

매직 플래그는 호출 맥락에서 의미가 명확하지 않은 부울 매개변수입니다. 전형적인 안티패턴: 플래그가 정확히 무엇을 활성화 또는 비활성화하는지 설명 없이 메서드에 true나 false를 전달하는 것입니다.

예시: userDao.fetch(includeDeleted = false). 개발자는 false를 보고 “삭제된 항목 포함 안 함”인지 “활성 항목 포함 안 함”인지 알 수 없습니다. 한 달 후, false가 true로 바뀌고 출력에 삭제된 레코드가 나타나기 시작합니다. 버그는 프로덕션에서만 발견됩니다.

해결책은 부울 플래그를 enum이나 sealed 클래스로 대체하는 것입니다. Boolean 매개변수 대신 UserFilter.includeDeleted나 UserFilter.activeOnly를 사용하세요. 이렇게 하면 코드가 의도를 문서화하고, IDE가 자동 완성 중에 사용 가능한 옵션을 제안합니다.

부울 플래그가 여러 계층을 통해 전달되는 경우 — 이는 추상화가 잘못되었다는 또 다른 신호입니다. 세 단계의 호출을 통해 플래그를 끌고 다니는 대신, 필터 선택을 최상위 수준에서 결정하고 준비된 설정으로 전달해야 할지 고려하세요. 코드에 부울 플래그가 적을수록 — 매직이 줄어듭니다.

규칙을 채택하세요: 이름 있는 인수를 지원하는 언어에서는 이름 있는 인수 없이 부울 매개변수를 메서드에 전달하지 마세요. Kotlin과 Swift에서는 이 요구사항이 자동으로 충족됩니다. Java에서는 true/false 대신 Builder나 enum 상수를 사용하세요.

매직 감지 도구

매직 값 검색은 예상치 못한 위치에서 리터럴을 감지하도록 설정된 정적 분석기에 의해 자동화됩니다. 각 언어는 사용자 정의 가능한 예외와 함께 자체 도구를 제공합니다.

도구언어규칙
SonarQubeJava, Kotlin, Swift, Python, JSMagicNumber, HardcodedString
ESLintJavaScript, TypeScriptno-magic-numbers, no-hardcoded-strings
DetektKotlinMagicNumber, ComplexCondition
SwiftLintSwiftmagic_number(옵트인)
PMDJava, Apex, PLSQLMagicNumber(구성 가능한 허용 목록)
PhpStorm 검사PHPNumericLiteralWithContext(내장 검사)

예외 구성이 중요합니다 — 이를 설정하지 않으면 분석기가 모든 증가(-1, +1)와 수학적 0에 경고를 표시합니다. SonarQube의 허용 숫자 목록: 0, 1, -1, 2(두 배용), 100(백분율), 60과 24(시간). 다른 모든 값에 대해서는 — public static final(Java) 또는 const val(Kotlin) 수정자가 있는 이름 있는 상수를 요구하세요.

CI 수준 분석의 경우, 매직을 경고로 확인하지만 빌드를 차단하지 않는 단계를 추가하세요. 첫 번째 실행에서는 레거시 코드에 수백 개의 경고가 표시됩니다. 점진적으로, 티켓별로, 코드를 상수로 마이그레이션하고 품질 임계값을 높이세요. 매직 넘버의 수가 10 미만이 되면 — 규칙을 빌드 오류로 활성화하세요.

리팩토링: 매직을 상수로 대체

매직 리팩토링은 가장 안전한 작업 중 하나입니다. 리터럴을 상수로 대체해도 코드 동작은 변경되지 않습니다. 그럼에도 불구하고, 숨겨진 의존성을 놓치지 않도록 접근 방식은 체계적이어야 합니다(예: 동일한 매직 넘버가 관련 없는 맥락에서 사용되지만 우연히 값이 같은 경우).

단계별 프로세스: 매직 값의 모든 발생 위치를 찾고, 각각의 맥락을 이해하며, 다른 상수로 분할하고(값이 일치하더라도 — 맥락이 다르므로 상수 이름도 달라야 함), 리터럴을 상수로 대체하고, 테스트를 통해 검증합니다. 2단계의 실수가 가장 일반적입니다. 두 가지 다른 개념(밀리초 단위 타임아웃과 바이트 단위 임계값)이 수치적으로 일치할 수 있지만(예: 5000), 의미적으로는 다른 양이며 하나의 상수로 통합할 수 없습니다.

java
// before - same number in different contexts
public class Config {
    public void setupCache() {
        cache.setMaxSize(5000); // 5 MB
    }
    public void setupTimeout() {
        client.setReadTimeout(5000); // 5 seconds
    }
}

// after - different constants for different contexts
public class Config {
    private static final int CACHE_MAX_SIZE_MB = 5;
    private static final int READ_TIMEOUT_SECONDS = 5;

    public void setupCache() {
        cache.setMaxSize(CACHE_MAX_SIZE_MB * 1024 * 1024);
    }
    public void setupTimeout() {
        client.setReadTimeout(
            READ_TIMEOUT_SECONDS * 1000
        );
    }
}

새 코드의 경우, 규칙은 간단합니다: 0, 1, -1, true, false, null, 빈 문자열을 제외한 모든 리터럴은 상수로 추출됩니다. 예외: 수학 상수(항상 표준 라이브러리 사용), 테스트 데이터(리터럴은 테스트에 남을 수 있지만 설명적인 변수 이름 사용), 증가를 위한 경계 값(루프에서 i + 1은 괜찮음).

자주 묻는 질문

100이 100퍼센트를 의미한다면 매직 넘버인가요?

네, 100도 맥락 없이 사용되면 매직 넘버입니다. 100 대신 MAX_PERCENTPROBABILITY_SCALE을 쓰세요. 예외: 100이 맥락에서 명백히 백분율인 경우(예: 백분율 계산 공식에서)지만, 이 경우에도 상수가 가독성을 향상시킵니다.

테스트의 숫자는 어떻게 처리해야 하나요?

테스트에서도 이름 있는 변수를 사용하는 것이 좋습니다. assertEquals(42, result) 대신 val expected = 42; assertEquals(expected, result)를 쓰세요. 예외: 경계 값 테스트(0, null, 빈 문자열) — 테스트 맥락에서 읽을 수 있으므로 리터럴로 남겨둘 수 있습니다.

숫자를 Android 리소스로 추출해야 하나요?

네, UI 관련 숫자(크기, 여백, 애니메이션 지속 시간)는 리소스(dimens.xml, integers.xml)에 있어야 합니다. 비즈니스 상수(타임아웃, 제한)는 — companion object나 설정 파일에. 주요 기준: 로직 변경 없이 숫자를 바꿀 수 있다면 — 그것은 리소스입니다.

레거시 프로젝트에서 매직 넘버를 찾으려면?

MagicNumber 규칙으로 SonarQube나 no-magic-numbers로 ESLint를 실행하세요. 보고서를 받아 사용 빈도로 정렬하고 세 군데 이상 나타나는 숫자부터 시작하세요. 그것들이 상수로 추출할 가장 유력한 후보입니다.

코드의 모든 숫자를 상수로 추출해야 하나요?

아니요. 허용되는 리터럴: 0, 1, -1(증가/감소, 빈 값 확인), true, false, null, 빈 문자열. 나머지는 모두 이름 지정이 필요합니다. 숫자 0이 빈 값 확인이 아닌 다른 용도로 사용된다면(예: 0이 루트 카테고리 ID인 경우), 0도 상수여야 합니다: ROOT_CATEGORY_ID = 0.

요약

  • 매직 — 설명 없는 리터럴: 숫자, 문자열, 플래그로, 의미가 코드 독자에게 숨겨져 있습니다.
  • 매직 넘버 — 이름 없는 숫자 상수(86400, 1024, 0.85, 5000)로, 이해에 도메인 지식이 필요합니다.
  • 매직 문자열 — 컴파일러에 보이지 않고 런타임 버그를 일으키는 하드코딩된 키, URL, 경로.
  • 매직 플래그 — 값이 명확하지 않은 부울 매개변수(메서드 호출의 true/false).
  • 도구: SonarQube, ESLint, Detekt, SwiftLint, PMD — 모두 MagicNumber 규칙을 지원합니다.
  • 해결책: 각 리터럴(0, ±1, true, false, null, "" 제외)을 설명적인 이름을 가진 이름 있는 상수로 추출합니다.
  • 다른 맥락 — 다른 상수: 타임아웃으로서의 5000과 캐시 크기로서의 5000은 다른 개체입니다.

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기