매직 넘버와 매직 스트링은 코드 안에서 의미를 명확히 알 수 없는 상수 값(숫자나 문자열)들입니다. 예를 들어:
if (statusCode == 200) { // 매직 넘버
// 성공 처리
}
if (user.role === 'admin') { // 매직 스트링
// 관리자 권한 처리
}
위의 코드에서는 200이나 'admin'이 무엇을 의미하는지 코드만으로는 바로 알 수 없습니다.
예시만 봐도 기억이 없으면 언제 어떻게 썼는지 막막합니다. 여기서 의미를 알려주는 주석까지 없었더라면 더 문제가 심각했을 것입니다.
매직 넘버나 매직 스트링을 사용하면 코드 가독성이 떨어지고 값이 변경될 때 해당 값이 여러 곳에 사용되었다면 일일이 찾아서 수정해야 하는 번거로움이 생깁니다.
언어에 따라 그리고 사람에 따라 이 문제를 해결하는 방법이 다르지만 저는 제 주력 언어인 TypeScript와 Java에서 매직 넘버와 매직 스트링을 처리하는 방법에 대해 설명하려 합니다.
자바에서는 매직 넘버나 매직 스트링을 final 키워드를 사용해 상수로 정의하는 것이 일반적입니다. 상수를 선언하면 코드 가독성이 높아지고 값이 여러 곳에서 사용될 때 유지보수가 쉬워집니다.
public class HttpStatus {
public static final int SUCCESS = 200;
public static final int NOT_FOUND = 404;
}
// 사용
int statusCode = HttpStatus.SUCCESS;
이 방법은 값이 하나의 클래스나 서비스에서만 사용될 때 유용합니다. 상수를 통해 가독성을 높이고 코드 유지보수를 쉽게 만들 수 있습니다. 단일 서비스에서 사용되며 상태나 의미의 확장 없이 단순히 특정 값의 의미를 전달하는 경우 final 변수를 사용하는 것이 적합합니다.
자바에서 상태나 값이 더욱 명확하게 구분되거나 다양한 상태와 관련된 작업이 필요할 때는 enum을 사용하는 것이 좋습니다.
enum은 단순한 상수 이상의 기능을 제공하며 코드 구조를 더 깔끔하고 확장 가능하게 만들어줍니다.
public enum HttpStatus {
SUCCESS(200),
NOT_FOUND(404);
private final int code;
HttpStatus(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}
enum을 사용하면 단순히 값을 담는 것뿐만 아니라 그 값과 관련된 행동이나 메소드를 추가할 수 있습니다. 특히 여러 클래스나 서비스에서 같은 값을 사용하거나 값에 의미와 행동이 연결될 때 적합합니다.
Java에서는 enum이 클래스와 유사하게 처리되며 자바스크립트처럼 런타임에 추가적인 객체 생성이 필요하지 않기 때문에 성능 저하가 없습니다.
TypeScript에서도 자바와 유사한 개념을 적용할 수 있지만, 타입 시스템을 활용한 추가적인 방법도 있습니다.
TypeScript에서는 const 상수를 사용해 매직 넘버와 매직 스트링을 처리하는 것이 가장 기본적인 방법입니다. 자바의 final처럼 값이 변경되지 않도록 상수로 선언하여 가독성을 높입니다.
const SUCCESS_CODE = 200;
const ADMIN_ROLE = 'admin';
if (statusCode === SUCCESS_CODE) {
// 성공 처리
}
if (user.role === ADMIN_ROLE) {
// 관리자 처리
}
const 상수는 단일 파일이나 모듈에서 사용되는 값을 처리하는 데 적합하며, 코드의 중복과 의미 없는 값을 방지할 수 있습니다.
TypeScript의 강력한 기능 중 하나는 리터럴 타입을 사용해 상수를 안전하게 관리하는 것입니다.
리터럴 타입은 코드에서 사용할 수 있는 값을 명시적으로 제한할 수 있으며 이를 통해 매직 넘버와 매직 스트링을 방지할 수 있습니다.
type HttpStatus = 200 | 404;
function handleRequest(status: HttpStatus) {
if (status === 200) {
console.log('Success');
} else if (status === 404) {
console.log('Not Found');
}
}
리터럴 타입은 타입 체크를 강화하여 오류를 방지하고, 값이 제한된 범위 내에서만 사용되도록 강제할 수 있습니다. 이는 코드의 안정성을 높이고 가독성을 유지하는 데 유용합니다.
TypeScript에서도 자바처럼 enum을 사용할 수 있습니다.
📣 그러나 TypeScript의 enum은 런타임에 자바스크립트 객체로 변환되며 이로 인해 성능 문제가 발생할 수 있습니다.
특히 큰 프로젝트나 성능이 중요한 코드에서 enum은 주의해서 사용해야 합니다.
enum의 런타임 오버헤드는 자바스크립트로 컴파일될 때 생깁니다. 예를 들어:
enum HttpStatus {
SUCCESS = 200,
NOT_FOUND = 404,
}
이 코드는 자바스크립트로 변환될 때 다음과 같이 컴파일됩니다:
var HttpStatus;
(function (HttpStatus) {
HttpStatus[HttpStatus["SUCCESS"] = 200] = "SUCCESS";
HttpStatus[HttpStatus["NOT_FOUND"] = 404] = "NOT_FOUND";
})(HttpStatus || (HttpStatus = {}));
위처럼 양방향 매핑이 생성되며 이는 런타임 오버헤드를 일으킬 수 있습니다. 따라서 성능을 고려해야 하는 상황에서는 enum 대신 리터럴 타입이나 유니언 타입을 사용하는 것이 더 나은 선택입니다.
TypeScript에서 enum의 성능 문제를 피하면서 유사한 구조를 사용할 수 있는 방법은 const 객체와 리터럴 타입을 결합하는 것입니다.
const HttpStatus = {
SUCCESS: 200,
NOT_FOUND: 404
} as const;
type HttpStatus = typeof HttpStatus[keyof typeof HttpStatus];
function handleRequest(status: HttpStatus) {
if (status === HttpStatus.SUCCESS) {
console.log('Success');
} else if (status === HttpStatus.NOT_FOUND) {
console.log('Not Found');
}
}
이 방식은 트랜스파일 시 추가적인 자바스크립트 코드가 생성되지 않으며 정적 타입 안전성을 유지할 수 있습니다.
자바나 타입스크립트 모두에서 매직 넘버와 매직 스트링을 없애는 방법은 매우 유사하지만, 상황에 따라 다른 선택이 필요합니다.
저는 둘 이상의 서비스나 로직에서 동일한 값을 사용한다면 enum을 사용하는 것이 가장 좋다고 생각합니다.
이는 코드에서 상태나 의미를 명확히 표현할 수 있고 나중에 상태가 확장되거나 변경될 때도 관리가 쉽기 때문입니다.
다만 TypeScript에서는 성능을 고려해 enum 대신 const 객체와 리터럴 타입을 사용하는 것이 좋은 경우가 많다는 점을 주의해야 합니다.
매직 넘버와 매직 스트링은 단어로만 들으면 큰 문제처럼 느껴지지 않을 수 있습니다.
일이 바쁘고 내 손과 머리를 탄 변수인데 “그냥 숫자나 문자열을 쓰면 되지 않을까?“라는 생각을 할 수 있지만 리팩토링과 유지보수를 고려하면 그 영향은 상당합니다.
제가 이 주제를 다시 정리하게 된 이유도 바로 여기서 출발합니다.
한 번은 외부 API의 결과 값이 코드 형식으로 들어오고 우리는 이 결과를 기준으로 로직을 실행해야 하는 상황을 겪었습니다. 문제는 우리 쪽 코드에서 이러한 값들을 "code.312" 같은 형식으로 직접 비교하여 로직을 실행하고 있었다는 점입니다.
그리고 주석도 없었고 서비스 내에서 이런 코드가 핵심 로직으로 돌아가는 것을 확인했습니다.
이후 이 값을 찾아 DB까지 뒤져보니 해당 코드 값과 함께 메시지가 저장되어 있었지만 그 코드가 의미하는 바를 파악하기 위해선 DB를 다 뒤져야 했습니다. 이 과정에서 가독성이 매우 떨어진다는 문제를 실감하게 되었습니다.
그래서 로직에서 사용하는 코드 값을 enum으로 정리하여 로직을 개선하고 나중에 API에서 추가될 가능성이 있는 값들까지 미리 고려해 리팩토링했습니다. 이 경험을 통해 배운 것은 매직 넘버와 매직 스트링을 방치하면 유지보수뿐만 아니라 전체 프로젝트의 안정성까지도 위협할 수 있다는 점이었습니다.
결국 이 글은 저 자신에게도 “리팩토링을 항상 고려하라”는 메시지로 다시금 상기시키기 위해 작성한 것이기도 합니다.
코드가 한눈에 이해되고, 변경 사항이 명확히 드러나는 구조를 갖추는 것이 프로젝트의 성패를 좌우할 수 있습니다.
그중에서도 매직 넘버와 매직 스트링을 명확하게 처리하는 것은 그 첫 걸음이라고 할 수 있습니다.