하드코딩 (Hardcoding) 이해하기

코드 리뷰를 받으면서 “이건 하드코딩이니까 수정해주세요”라는 피드백을 받아본 적 있으신가요?

‘하드코딩’은 개발자들 사이에서 자주 사용되는 용어지만, 정확히 무엇이 문제이고 언제는 괜찮은지 명확히 아는 분들은 생각보다 많지 않습니다. 어떤 개발자는 “모든 하드코딩은 나쁘다”고 말하고, 다른 개발자는 “상황에 따라 다르다”고 합니다.

2019년 GitHub의 한 보안 연구에 따르면, 공개 저장소에서 발견된 보안 취약점의 약 20%가 API 키나 비밀번호를 하드코딩한 것이었습니다. 하드코딩은 단순히 “나쁜 습관”을 넘어서 심각한 보안 문제를 야기할 수 있습니다.

이 문서에서는 하드코딩이 정확히 무엇인지, 왜 문제가 되는지, 그리고 언제는 괜찮은지 실전 예제와 함께 알아보겠습니다.

왜 하드코딩을 이해해야 할까요?

1. 유지보수의 악몽

실무에서 가장 큰 문제는 유지보수입니다.

상상해보세요. API 엔드포인트 URL이 20개 파일에 직접 박혀있는 프로젝트를 인수인계 받았습니다. 서버 주소가 변경되면 어떻게 될까요? 20개 파일을 일일이 찾아서 수정해야 합니다. 하나라도 놓치면 버그가 발생하죠.

2018년 Stack Overflow 개발자 설문조사에 따르면, 개발자들이 시간을 가장 많이 쓰는 작업 중 하나가 “코드 유지보수”였습니다. 하드코딩은 이 유지보수 시간을 기하급수적으로 증가시킵니다.

2. 보안 위험

GitHub의 자동 스캔 시스템은 매일 수천 개의 하드코딩된 비밀번호와 API 키를 찾아냅니다.

// ❌ 실제로 GitHub에서 흔히 발견되는 패턴
const API_KEY = 'sk-1234567890abcdef';  // OpenAI API 키
const DB_PASSWORD = 'admin123';          // 데이터베이스 비밀번호

이런 코드가 공개 저장소에 올라가면? 몇 분 안에 봇들이 찾아내서 악용합니다. 2021년에는 하드코딩된 AWS 키로 인해 한 스타트업이 하루 만에 수천 달러의 요금 폭탄을 맞은 사례가 HackerNews에 공유되기도 했습니다.

3. 환경별 설정 불가

현대 애플리케이션은 여러 환경에서 실행됩니다:

  • 개발 환경 (localhost)
  • 테스트 환경 (staging)
  • 운영 환경 (production)

하드코딩된 값은 환경마다 다른 설정을 사용할 수 없게 만듭니다.

먼저, 기초부터 이해하기

하드코딩의 탄생

프로그래밍 초기 시절로 돌아가볼까요? 1950-60년대, 컴퓨터는 매우 제한적이었습니다. 메모리도 부족했고, 외부 설정 파일을 읽는 것조차 비싼 작업이었죠.

그 시절에는 프로그램에 값을 직접 넣는 것이 유일한 방법이었습니다:

C FORTRAN 코드 (1960년대)
      PROGRAM HELLO
      PRINT *, 'HELLO WORLD'
      STOP
      END

이 코드에서 ‘HELLO WORLD’는 하드코딩된 값입니다. 다른 메시지를 출력하려면? 코드를 수정하고 다시 컴파일해야 했죠.

왜 하드코딩이 문제가 되었나?

시간이 지나면서 소프트웨어는 점점 복잡해졌습니다. 한 프로그램에서 같은 값을 여러 곳에서 사용하게 되었죠.

// C 언어 (1970년대)
// 문제: MAX_SIZE가 5곳에 하드코딩됨
int buffer1[100];
int buffer2[100];
int buffer3[100];
// ... 나중에 크기를 200으로 바꾸고 싶다면?
// 모든 100을 찾아서 200으로 수정해야 함

이런 문제를 해결하기 위해 상수(constant)라는 개념이 등장했습니다:

// 개선: 상수로 분리
#define MAX_SIZE 100
int buffer1[MAX_SIZE];
int buffer2[MAX_SIZE];
int buffer3[MAX_SIZE];
// 이제 한 곳만 바꾸면 됨!

하드코딩의 정의

현대적 의미에서 하드코딩(Hardcoding)은:

코드 내부에 데이터나 설정 값을 직접 작성하여, 실행 시점에 변경할 수 없게 만드는 것

핵심은 유연성의 부재입니다. 값이 코드에 “hard”하게 박혀있어서 쉽게 바꿀 수 없다는 뜻이죠.

하드코딩의 유형

하드코딩에도 여러 종류가 있습니다. 어떤 건 심각한 문제고, 어떤 건 괜찮기도 합니다.

1. 매직 넘버 (Magic Numbers)

의미를 알 수 없는 숫자를 직접 사용하는 것입니다.

나쁜 예:

function calculateDiscount(price) {
  return price * 0.15;  // 0.15가 뭐지? 왜 15%?
}

function isAdult(age) {
  return age >= 20;  // 한국 기준? 미국 기준?
}

// 더 심각한 예
if (status === 3) {  // 3이 무슨 상태?
  // ...
}

좋은 예:

const DISCOUNT_RATE = 0.15;  // 회원 할인율 15%

function calculateDiscount(price) {
  return price * DISCOUNT_RATE;
}

const ADULT_AGE = 20;  // 한국 성인 나이

function isAdult(age) {
  return age >= ADULT_AGE;
}

// enum이나 상수 객체 사용
const OrderStatus = {
  PENDING: 1,
  PROCESSING: 2,
  COMPLETED: 3,
  CANCELLED: 4,
};

if (status === OrderStatus.COMPLETED) {
  // 의미가 명확함
}

왜 문제인가?

  • 숫자만 봐서는 의미를 알 수 없음
  • 같은 값을 여러 곳에서 사용하면 일관성 유지 어려움
  • 값이 변경되면 모든 곳을 수정해야 함

2. 하드코딩된 문자열

문자열을 직접 박아넣는 것입니다.

나쁜 예:

// API 엔드포인트를 여러 곳에서 사용
fetch('https://api.example.com/users')
  .then(res => res.json());

// 다른 파일에서도...
fetch('https://api.example.com/posts')
  .then(res => res.json());

// 또 다른 파일에서도...
fetch('https://api.example.com/comments')
  .then(res => res.json());

// 문제: 서버 주소가 바뀌면 모든 파일을 수정해야 함

좋은 예:

// config.js
const API_BASE_URL = process.env.REACT_APP_API_URL || 'https://api.example.com';

export const API = {
  USERS: `${API_BASE_URL}/users`,
  POSTS: `${API_BASE_URL}/posts`,
  COMMENTS: `${API_BASE_URL}/comments`,
};

// 사용
import { API } from './config';

fetch(API.USERS)
  .then(res => res.json());

3. 하드코딩된 경로

파일 경로를 절대 경로로 박아넣는 것입니다.

나쁜 예:

# Windows 개발 환경에서 작성
def load_config():
    with open('C:\\Users\\john\\project\\config.json') as f:
        return json.load(f)

# 문제: 다른 사람의 컴퓨터에서는 작동 안 함!
# Linux/Mac에서도 작동 안 함!

좋은 예:

import os
from pathlib import Path

# 프로젝트 루트 디렉토리 기준 상대 경로
BASE_DIR = Path(__file__).parent.parent
CONFIG_PATH = BASE_DIR / 'config.json'

def load_config():
    with open(CONFIG_PATH) as f:
        return json.load(f)

4. 하드코딩된 비밀 정보

가장 심각한 유형입니다. 절대로 하면 안 됩니다!

매우 나쁜 예:

// ⚠️ 절대 이렇게 하지 마세요!
const config = {
  apiKey: 'sk-1234567890abcdefghijklmnop',
  dbPassword: 'MyP@ssw0rd123',
  jwtSecret: 'super-secret-key',
};

좋은 예:

// .env 파일 (Git에 올리지 않음!)
// API_KEY=sk-1234567890abcdefghijklmnop
// DB_PASSWORD=MyP@ssw0rd123
// JWT_SECRET=super-secret-key

// 코드에서는 환경 변수 사용
const config = {
  apiKey: process.env.API_KEY,
  dbPassword: process.env.DB_PASSWORD,
  jwtSecret: process.env.JWT_SECRET,
};

언제 하드코딩이 괜찮을까?

모든 하드코딩이 나쁜 건 아닙니다. 상황에 따라 하드코딩이 더 나은 선택일 수 있습니다.

괜찮은 하드코딩 1: 진짜 상수

절대 변하지 않는 값들입니다.

// ✅ 괜찮은 하드코딩
const DAYS_IN_WEEK = 7;
const HOURS_IN_DAY = 24;
const PI = 3.14159;
const MAX_USERNAME_LENGTH = 20;
const VALID_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

// 왜 괜찮은가?
// - 비즈니스 로직이 아닌 물리적/수학적 상수
// - 변경될 가능성이 거의 없음
// - 코드 가독성을 높임

괜찮은 하드코딩 2: 기본값

사용자가 값을 제공하지 않았을 때의 기본값입니다.

// ✅ 괜찮은 하드코딩
function fetchUsers(page = 1, limit = 10) {
  // page와 limit의 기본값 하드코딩
  return fetch(`/api/users?page=${page}&limit=${limit}`);
}

// React 컴포넌트 기본 props
function Button({ variant = 'primary', size = 'medium', children }) {
  return <button className={`btn-${variant} btn-${size}`}>{children}</button>;
}

괜찮은 하드코딩 3: 프로토타입/학습용 코드

빠른 실험이나 학습이 목적일 때입니다.

// ✅ 프로토타입이므로 괜찮음
// 나중에 리팩토링할 예정
function demo() {
  const mockUsers = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
  ];

  mockUsers.forEach(user => console.log(user.name));
}

하지만 이런 코드가 프로덕션에 들어가면 안 됩니다!

괜찮은 하드코딩 4: 테스트 데이터

단위 테스트에서 사용하는 고정된 테스트 데이터입니다.

// ✅ 테스트이므로 괜찮음
describe('calculateDiscount', () => {
  it('should apply 15% discount for members', () => {
    const originalPrice = 100;
    const expected = 85;

    expect(calculateDiscount(originalPrice)).toBe(expected);
  });
});

실전 문제 해결하기

하드코딩을 어떻게 제거할까요? 실제 상황별로 살펴보겠습니다.

시나리오 1: React 앱의 API 엔드포인트

여러 컴포넌트에서 같은 API 주소를 사용하고 있습니다.

문제 코드:

// components/UserList.jsx
function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetch('https://api.myapp.com/users')
      .then(res => res.json())
      .then(setUsers);
  }, []);

  return <div>{/* ... */}</div>;
}

// components/PostList.jsx
function PostList() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetch('https://api.myapp.com/posts')  // 같은 주소
      .then(res => res.json())
      .then(setPosts);
  }, []);

  return <div>{/* ... */}</div>;
}

해결 방법 1: 환경 변수 + 설정 파일

// .env.development
REACT_APP_API_URL=http://localhost:3000/api

// .env.production
REACT_APP_API_URL=https://api.myapp.com

// src/config/api.js
const API_BASE_URL = process.env.REACT_APP_API_URL;

export const endpoints = {
  users: `${API_BASE_URL}/users`,
  posts: `${API_BASE_URL}/posts`,
  comments: `${API_BASE_URL}/comments`,
};

// 사용
import { endpoints } from '../config/api';

fetch(endpoints.users)
  .then(res => res.json())
  .then(setUsers);

해결 방법 2: Custom Hook

// hooks/useApi.js
const API_BASE_URL = process.env.REACT_APP_API_URL;

export function useApi(endpoint) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(`${API_BASE_URL}${endpoint}`)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [endpoint]);

  return { data, loading, error };
}

// 사용
function UserList() {
  const { data: users, loading } = useApi('/users');

  if (loading) return <div>Loading...</div>;
  return <div>{/* ... */}</div>;
}

시나리오 2: Node.js 백엔드의 데이터베이스 연결

데이터베이스 연결 정보가 코드에 직접 박혀있습니다.

문제 코드:

// db.js
const mysql = require('mysql2');

const connection = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: 'admin123',  // 보안 위험!
  database: 'myapp',
  port: 3306,
});

module.exports = connection;

해결 방법:

// .env (절대 Git에 올리지 말 것!)
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=admin123
DB_NAME=myapp
DB_PORT=3306

// .env.example (Git에 올림, 예시로 제공)
DB_HOST=your_db_host
DB_USER=your_db_user
DB_PASSWORD=your_db_password
DB_NAME=your_db_name
DB_PORT=3306

// db.js
require('dotenv').config();
const mysql = require('mysql2');

const connection = mysql.createConnection({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  port: process.env.DB_PORT,
});

module.exports = connection;

시나리오 3: Python 스크립트의 파일 경로

Windows에서 개발한 코드가 Linux 서버에서 작동하지 않습니다.

문제 코드:

# data_processor.py
def load_data():
    # Windows 절대 경로 하드코딩
    with open('C:\\Users\\john\\project\\data\\input.csv') as f:
        return f.read()

# 문제:
# - 다른 사람의 컴퓨터에서 작동 안 함
# - Linux/Mac에서 작동 안 함
# - 프로젝트 위치가 바뀌면 작동 안 함

해결 방법:

# data_processor.py
import os
from pathlib import Path

# 방법 1: 상대 경로 사용
def load_data():
    # 현재 파일 기준 상대 경로
    current_dir = Path(__file__).parent
    data_path = current_dir / 'data' / 'input.csv'

    with open(data_path) as f:
        return f.read()

# 방법 2: 환경 변수 사용
def load_data_from_env():
    data_dir = os.getenv('DATA_DIR', './data')  # 기본값 제공
    data_path = Path(data_dir) / 'input.csv'

    with open(data_path) as f:
        return f.read()

# 방법 3: 설정 파일 사용
# config.yaml
# data_dir: /path/to/data

import yaml

def load_data_from_config():
    with open('config.yaml') as f:
        config = yaml.safe_load(f)

    data_path = Path(config['data_dir']) / 'input.csv'

    with open(data_path) as f:
        return f.read()

시나리오 4: 다국어 지원

UI 텍스트가 코드에 하드코딩되어 있습니다.

문제 코드:

// LoginForm.jsx
function LoginForm() {
  return (
    <form>
      <h1>로그인</h1>
      <input placeholder="이메일을 입력하세요" />
      <input placeholder="비밀번호를 입력하세요" type="password" />
      <button>로그인</button>
      <p>계정이 없으신가요? <a href="/signup">회원가입</a></p>
    </form>
  );
}

// 문제: 영어, 일본어 등 다른 언어 지원 불가

해결 방법: i18n 라이브러리 사용

// locales/ko.json
{
  "login": {
    "title": "로그인",
    "email": "이메일을 입력하세요",
    "password": "비밀번호를 입력하세요",
    "submit": "로그인",
    "signupPrompt": "계정이 없으신가요?",
    "signup": "회원가입"
  }
}

// locales/en.json
{
  "login": {
    "title": "Login",
    "email": "Enter your email",
    "password": "Enter your password",
    "submit": "Sign In",
    "signupPrompt": "Don't have an account?",
    "signup": "Sign Up"
  }
}

// LoginForm.jsx
import { useTranslation } from 'react-i18next';

function LoginForm() {
  const { t } = useTranslation();

  return (
    <form>
      <h1>{t('login.title')}</h1>
      <input placeholder={t('login.email')} />
      <input placeholder={t('login.password')} type="password" />
      <button>{t('login.submit')}</button>
      <p>
        {t('login.signupPrompt')} <a href="/signup">{t('login.signup')}</a>
      </p>
    </form>
  );
}

하드코딩 대안들

하드코딩을 피하기 위한 다양한 방법들을 살펴볼까요?

1. 환경 변수 (Environment Variables)

가장 일반적이고 권장되는 방법입니다.

# .env
NODE_ENV=production
API_URL=https://api.myapp.com
DB_URL=postgresql://user:pass@localhost/mydb
JWT_SECRET=your-secret-key
MAX_UPLOAD_SIZE=10485760

장점:

  • 코드 변경 없이 설정 변경 가능
  • 환경마다 다른 값 사용 가능
  • 비밀 정보를 코드에서 분리

언제 사용:

  • API 엔드포인트
  • 데이터베이스 연결 정보
  • 비밀 키
  • 환경별로 다른 모든 설정

2. 설정 파일 (Configuration Files)

복잡한 설정이나 구조화된 데이터에 적합합니다.

# config.yaml
server:
  port: 3000
  cors:
    origins:
      - http://localhost:3000
      - https://myapp.com

database:
  host: localhost
  port: 5432
  pool:
    min: 2
    max: 10

logging:
  level: info
  format: json

장점:

  • 복잡한 설정 구조화 가능
  • 주석으로 설명 추가 가능
  • 버전 관리 가능

언제 사용:

  • 복잡한 애플리케이션 설정
  • 여러 값의 조합
  • 팀과 공유할 기본 설정

3. 데이터베이스

런타임에 변경 가능한 설정입니다.

// 관리자 페이지에서 설정 변경 가능
const settings = await db.settings.findOne({ key: 'maintenance_mode' });

if (settings.value === true) {
  return res.status(503).send('Under maintenance');
}

장점:

  • 앱 재시작 없이 설정 변경
  • 사용자별/조직별 설정 가능
  • 변경 이력 추적 가능

언제 사용:

  • 기능 플래그 (Feature Flags)
  • 사용자별 설정
  • 자주 변경되는 비즈니스 규칙

4. 커맨드 라인 인자

스크립트나 CLI 도구에 적합합니다.

# script.py
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--input', required=True, help='Input file path')
parser.add_argument('--output', required=True, help='Output file path')
parser.add_argument('--verbose', action='store_true', help='Verbose output')

args = parser.parse_args()

process_file(args.input, args.output, verbose=args.verbose)
# 사용
python script.py --input data.csv --output result.json --verbose

언제 사용:

  • CLI 도구
  • 배치 스크립트
  • 일회성 작업

흔한 실수와 해결 방법

하드코딩을 제거하려다가 오히려 더 복잡해지는 실수들이 있습니다. 이런 함정들을 미리 알아두면 시행착오를 줄일 수 있습니다.

실수 1: 과도한 추상화

모든 것을 설정으로 빼려다가 오히려 복잡해집니다.

나쁜 예:

// config.js - 너무 많은 것을 설정으로 뺌
const config = {
  buttonColor: process.env.BUTTON_COLOR || 'blue',
  fontSize: process.env.FONT_SIZE || '14px',
  marginTop: process.env.MARGIN_TOP || '10px',
  marginBottom: process.env.MARGIN_BOTTOM || '10px',
  paddingLeft: process.env.PADDING_LEFT || '5px',
  // ... 100개의 설정
};

// 문제: CSS가 환경 변수로 가득 차있음. 이건 과도함!

좋은 예:

// 정말 필요한 것만 설정으로
const config = {
  apiUrl: process.env.API_URL,
  theme: process.env.THEME || 'light',  // 'light' or 'dark'
};

// 나머지는 테마 파일에서 관리
const themes = {
  light: {
    buttonColor: 'blue',
    fontSize: '14px',
    // ...
  },
  dark: {
    buttonColor: 'navy',
    fontSize: '14px',
    // ...
  },
};

원칙: 환경마다 달라야 하는 것만 설정으로 빼세요.

실수 2: 비밀 정보를 코드에 남기기

환경 변수로 옮겼지만 기본값에 비밀 정보를 넣습니다.

나쁜 예:

const API_KEY = process.env.API_KEY || 'sk-real-production-key-1234';
// 문제: 기본값에 진짜 키를 넣어버림!

좋은 예:

const API_KEY = process.env.API_KEY;

if (!API_KEY) {
  throw new Error('API_KEY environment variable is required');
}
// 환경 변수가 없으면 명확한 에러 발생

실수 3: .env 파일을 Git에 올리기

.env 파일에는 실제 비밀 정보가 들어있습니다. 절대 Git에 올리면 안 됩니다!

올바른 방법:

# .gitignore
.env
.env.local
.env.*.local

# Git에 올릴 파일
.env.example  # 예시만 제공
# .env.example (Git에 올림)
API_URL=your_api_url_here
DB_PASSWORD=your_db_password_here
JWT_SECRET=your_jwt_secret_here

# .env (Git에 올리지 않음)
API_URL=https://api.myapp.com
DB_PASSWORD=actual_password_123
JWT_SECRET=actual_secret_key_xyz

팀원들을 위한 README 작성:

## 환경 설정

1. `.env.example`을 복사하여 `.env` 파일 생성
2. 실제 값으로 변경

실수 4: 타입 체크 누락

환경 변수는 항상 문자열입니다!

문제 코드:

const MAX_ITEMS = process.env.MAX_ITEMS || 10;

// 문제: MAX_ITEMS는 "10" (문자열)
// 비교할 때 버그 발생 가능
if (items.length > MAX_ITEMS) {  // "5" > "10" 는 true!
  // ...
}

해결:

const MAX_ITEMS = parseInt(process.env.MAX_ITEMS, 10) || 10;
// 또는
const MAX_ITEMS = Number(process.env.MAX_ITEMS) || 10;

// boolean의 경우
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
// ❌ 나쁨: Boolean(process.env.IS_PROD)  // "false"도 true로 변환됨!

실수 5: 설정 검증 안 함

잘못된 설정으로 앱이 시작되는 것을 방지해야 합니다.

좋은 예:

// config/validate.js
function validateConfig() {
  const required = ['API_URL', 'DB_URL', 'JWT_SECRET'];

  for (const key of required) {
    if (!process.env[key]) {
      throw new Error(`Missing required environment variable: ${key}`);
    }
  }

  // URL 형식 검증
  try {
    new URL(process.env.API_URL);
  } catch (error) {
    throw new Error('API_URL must be a valid URL');
  }

  // 숫자 범위 검증
  const port = parseInt(process.env.PORT, 10);
  if (port < 1 || port > 65535) {
    throw new Error('PORT must be between 1 and 65535');
  }
}

// 앱 시작 시 검증
validateConfig();

보안 체크리스트

하드코딩과 관련된 보안 문제를 방지하기 위한 체크리스트입니다.

✅ Git에 올리기 전

  • .env 파일이 .gitignore에 있는가?
  • 코드에 API 키, 비밀번호가 없는가?
  • .env.example만 Git에 포함되는가?
  • 커밋 전에 git diff로 검토했는가?

✅ 코드 리뷰 시

  • 하드코딩된 URL이 없는가?
  • 매직 넘버에 설명이 있는가?
  • 환경별로 다른 값을 사용하는가?
  • 타입 변환이 올바른가?

✅ 배포 전

  • 모든 환경 변수가 설정되었는가?
  • 설정 검증이 통과하는가?
  • Production 환경 변수가 올바른가?
  • 비밀 정보가 안전하게 관리되는가?

도구 활용

1. git-secrets

AWS 키 등 비밀 정보가 커밋되는 것을 방지합니다.

# 설치
brew install git-secrets  # macOS

# 설정
git secrets --install
git secrets --register-aws

2. ESLint 규칙

// .eslintrc.js
module.exports = {
  rules: {
    'no-magic-numbers': ['warn', {
      ignore: [0, 1, -1],
      ignoreArrayIndexes: true,
    }],
  },
};

3. GitHub Secret Scanning

Public 저장소는 자동으로 스캔됩니다. Private 저장소는 설정에서 활성화하세요.

실전 리팩토링 예제

레거시 코드에서 하드코딩을 제거하는 전체 과정을 살펴보겠습니다.

Before: 하드코딩 투성이 코드

// app.js (레거시 코드)
const express = require('express');
const mysql = require('mysql2');

const app = express();

// 데이터베이스 연결
const db = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: 'password123',
  database: 'myapp',
});

// API 라우트
app.get('/api/users', (req, res) => {
  // 페이지당 10개 고정
  const limit = 10;
  const page = req.query.page || 1;
  const offset = (page - 1) * limit;

  db.query(
    'SELECT * FROM users LIMIT ? OFFSET ?',
    [limit, offset],
    (err, results) => {
      if (err) throw err;
      res.json(results);
    }
  );
});

// 할인 계산
app.post('/api/calculate-discount', (req, res) => {
  const { price, userType } = req.body;

  let discount = 0;
  if (userType === 1) {  // 일반 회원
    discount = price * 0.05;
  } else if (userType === 2) {  // VIP 회원
    discount = price * 0.15;
  } else if (userType === 3) {  // VVIP 회원
    discount = price * 0.25;
  }

  res.json({ discount, final: price - discount });
});

// 서버 시작
app.listen(3000, () => {
  console.log('Server running on port 3000');
});

문제점:

  1. 데이터베이스 비밀번호 하드코딩
  2. 매직 넘버 (10, 3000)
  3. 매직 숫자 (userType 1, 2, 3)
  4. 할인율 하드코딩 (0.05, 0.15, 0.25)

After: 리팩토링 완료

// .env
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=password123
DB_NAME=myapp
PORT=3000
ITEMS_PER_PAGE=10

// config/constants.js
const UserType = {
  REGULAR: 1,
  VIP: 2,
  VVIP: 3,
};

const DiscountRate = {
  [UserType.REGULAR]: 0.05,   // 5%
  [UserType.VIP]: 0.15,        // 15%
  [UserType.VVIP]: 0.25,       // 25%
};

module.exports = { UserType, DiscountRate };

// config/database.js
require('dotenv').config();
const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  waitForConnections: true,
  connectionLimit: 10,
});

module.exports = pool;

// app.js (리팩토링 후)
require('dotenv').config();
const express = require('express');
const db = require('./config/database');
const { UserType, DiscountRate } = require('./config/constants');

const app = express();
app.use(express.json());

// 환경 변수 검증
if (!process.env.PORT || !process.env.DB_HOST) {
  console.error('Missing required environment variables');
  process.exit(1);
}

const PORT = parseInt(process.env.PORT, 10);
const ITEMS_PER_PAGE = parseInt(process.env.ITEMS_PER_PAGE, 10);

// API 라우트
app.get('/api/users', async (req, res) => {
  try {
    const page = parseInt(req.query.page, 10) || 1;
    const offset = (page - 1) * ITEMS_PER_PAGE;

    const [rows] = await db.query(
      'SELECT * FROM users LIMIT ? OFFSET ?',
      [ITEMS_PER_PAGE, offset]
    );

    res.json(rows);
  } catch (error) {
    console.error('Error fetching users:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// 할인 계산
app.post('/api/calculate-discount', (req, res) => {
  const { price, userType } = req.body;

  // 유효성 검증
  if (!price || !userType) {
    return res.status(400).json({ error: 'Missing required fields' });
  }

  const discountRate = DiscountRate[userType];

  if (discountRate === undefined) {
    return res.status(400).json({ error: 'Invalid user type' });
  }

  const discount = price * discountRate;
  const final = price - discount;

  res.json({
    discount,
    final,
    discountRate: `${discountRate * 100}%`,
  });
});

// 서버 시작
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

개선 사항:

  1. ✅ 환경 변수로 비밀 정보 분리
  2. ✅ 상수를 별도 파일로 분리
  3. ✅ 매직 넘버 제거
  4. ✅ 의미 있는 이름 사용 (UserType.VIP)
  5. ✅ 에러 처리 추가
  6. ✅ 입력 검증 추가

참고 자료

공식 가이드

도구

학습 자료

마치며

하드코딩은 단순해 보이지만 소프트웨어의 품질과 보안에 큰 영향을 미칩니다.

핵심 요약:

  1. 하드코딩이란: 코드에 값을 직접 박아넣어 유연성을 잃는 것
  2. 주요 문제: 유지보수 어려움, 보안 위험, 환경별 설정 불가
  3. 대안: 환경 변수, 설정 파일, 상수 분리
  4. 괜찮은 경우: 진짜 상수, 기본값, 테스트 데이터

하드코딩을 무조건 나쁘다고만 생각할 필요는 없습니다. 중요한 건 언제 괜찮고 언제 문제인지 판단하는 능력입니다.

처음에는 “이것도 하드코딩인가? 저것도 하드코딩인가?” 헷갈릴 수 있습니다. 하지만 괜찮아요. 코드 리뷰를 받고, 리팩토링을 경험하면서 점점 감이 생길 겁니다.

이제 여러분의 코드를 한번 살펴보세요. 하드코딩된 부분이 보이시나요? 그중에서 환경 변수로 분리하면 좋을 것들이 있다면, 지금 바로 리팩토링해보세요. 첫 번째 .env 파일을 만들고 설정을 분리하는 순간, “아, 이래서 하드코딩을 피해야 하는구나”를 체감하게 될 거예요!

댓글