본문으로 건너뛰기 C++ One Definition Rule | '단일 정의 규칙' 가이드

C++ One Definition Rule | '단일 정의 규칙' 가이드

C++ One Definition Rule | '단일 정의 규칙' 가이드

이 글의 핵심

C++ One Definition Rule: "단일 정의 규칙" 가이드. ODR 기본 규칙·ODR 예외.

들어가며

ODR(One Definition Rule)은 C++의 핵심 규칙으로, 변수, 함수, 클래스 등이 전체 프로그램에서 하나의 정의만 가져야 한다는 원칙입니다. ODR을 이해하면 링크 에러를 예방하고 안전한 코드를 작성할 수 있습니다.


1. ODR 기본 규칙

C++는 여러 개의 소스 파일(번역 단위)을 각각 독립적으로 컴파일한 뒤 링커가 이들을 하나의 실행 파일로 합치는 구조입니다. 이 과정에서 같은 이름의 전역 변수나 함수가 서로 다른 파일에 중복으로 정의되어 있으면, 링커는 어느 쪽을 최종 결과에 남겨야 할지 판단할 수 없습니다. ODR은 바로 이런 충돌을 막기 위해 “전체 프로그램에서 각 개체는 정확히 하나의 정의만 가져야 한다”고 규정한 규칙입니다.

변수의 ODR

전역 변수를 헤더가 아닌 여러 .cpp 파일에 각각 int globalVar = 10;처럼 정의해 버리면, 두 번역 단위 모두 같은 이름의 심볼을 만들어내므로 링크 단계에서 “multiple definition” 오류가 발생합니다. 이 문제를 피하려면 헤더에는 extern으로 “이 변수가 어딘가에 존재한다”는 선언만 두고, 실제 메모리를 할당하는 정의는 단 하나의 소스 파일에만 두어야 합니다.

// ❌ ODR 위반
// file1.cpp
int globalVar = 10;

// file2.cpp
int globalVar = 20;  // 중복 정의!

// 링크 에러: multiple definition of 'globalVar'

위 문제를 해결한 아래 구조에서는, 헤더의 extern 선언이 여러 파일에 포함되어도 실제 정의가 아니므로 ODR 위반이 아니며, 정의는 오직 file1.cpp 한 곳에만 존재합니다.

// ✅ 올바른 방법
// header.h
extern int globalVar;  // 선언

// file1.cpp
int globalVar = 10;  // 정의 (한 곳에서만)

// file2.cpp
#include "header.h"  // 선언만 포함

함수의 ODR

함수도 변수와 완전히 동일한 원리로 ODR의 적용을 받습니다. 서로 다른 소스 파일에 같은 이름과 시그니처를 가진 함수를 각각 몸체까지 포함해 정의하면, 링커 입장에서는 어느 구현을 실행 파일에 남겨야 할지 알 수 없어 링크 에러가 발생합니다.

// ❌ ODR 위반
// file1.cpp
void func() {
    std::cout << "file1" << std::endl;
}

// file2.cpp
void func() {
    std::cout << "file2" << std::endl;
}

// 링크 에러: multiple definition of 'func()'

헤더에는 함수의 시그니처만 선언해두고, 실제 구현(정의)은 하나의 소스 파일에만 두면 이 문제가 사라집니다. 여러 파일이 이 헤더를 #include해도 각 파일이 보는 것은 선언일 뿐 정의가 아니기 때문입니다.

// ✅ 올바른 방법
// header.h
void func();  // 선언

// file1.cpp
void func() {
    std::cout << "implementation" << std::endl;
}

2. ODR 예외

헤더 파일에 함수 몸체나 변수 초기값을 직접 적어두고 싶은 경우가 실무에서는 매우 흔합니다. 유틸리티 함수, 템플릿 클래스, 상수 정의 등이 대표적인데, 이런 것들을 헤더에 정의하면서도 여러 번역 단위에 포함될 때 ODR을 위반하지 않도록, C++ 표준은 inline, 템플릿, constexpr 세 가지에 대해 예외 규칙을 두고 있습니다.

inline 함수

inline 키워드는 원래 의미였던 “컴파일러에게 인라이닝을 제안한다”는 역할보다, 현대 C++에서는 “이 함수는 여러 번역 단위에 정의되어도 ODR 위반이 아니다”라는 링커 차원의 허가 표시로 더 자주 쓰입니다. 대신 표준은 조건을 하나 겁니다. 모든 번역 단위에 있는 정의가 토큰 단위로 완전히 동일해야 한다는 것이며, 이 조건이 깨지면 컴파일러가 항상 잡아내지 못하는 정의되지 않은 동작으로 이어질 수 있습니다.

// ✅ inline 함수는 여러 번 정의 가능
// utils.h
inline int add(int a, int b) {
    return a + b;
}

// file1.cpp
#include "utils.h"  // add 정의 포함

// file2.cpp
#include "utils.h"  // add 정의 포함 (OK, 동일한 정의)

핵심: inline 함수는 모든 정의가 동일하면 여러 번역 단위에 정의될 수 있습니다.

템플릿

템플릿은 실제 타입이 주어지기 전까지는 완전한 코드가 아니라 “코드를 찍어내는 틀”에 가깝기 때문에, 각 번역 단위가 서로 다른 타입으로 이 틀을 인스턴스화해도 문제가 되지 않습니다. 그래서 템플릿 클래스나 템플릿 함수는 관례상 헤더 파일에 정의를 통째로 넣어두며, 아래처럼 여러 소스 파일이 같은 헤더를 포함해 동일한 타입(Stack<int>)으로 인스턴스화하더라도 컴파일러가 중복 인스턴스를 하나로 병합해 주므로 ODR 위반이 발생하지 않습니다.

// ✅ 템플릿은 헤더에 정의
// stack.h
template<typename T>
class Stack {
public:
    void push(const T& value) {
        data.push_back(value);
    }
    
    T pop() {
        T value = data.back();
        data.pop_back();
        return value;
    }
    
private:
    std::vector<T> data;
};

// file1.cpp
#include "stack.h"
Stack<int> intStack;  // int 버전 인스턴스화

// file2.cpp
#include "stack.h"
Stack<int> intStack2;  // int 버전 인스턴스화 (OK)

constexpr 변수

C++17 이전에는 constexpr 전역 변수를 헤더에 두면 각 번역 단위마다 내부 링크(internal linkage)를 갖는 별도의 복사본이 만들어졌습니다. 이는 ODR 위반은 피할 수 있지만, 같은 값을 가리키는 변수가 여러 개 존재해 주소를 비교하면 서로 다르게 나오는 등 미묘한 문제를 낳을 수 있었습니다. C++17부터 inline constexpr을 함께 쓰면 모든 번역 단위가 진짜로 동일한 하나의 변수를 공유하게 되어, 앞서 살펴본 inline 함수와 같은 방식으로 이 문제를 해결합니다.

// C++17 이전
// header.h
constexpr int MAX = 100;  // 각 번역 단위마다 복사

// C++17 이후
// header.h
inline constexpr int MAX = 100;  // 하나의 정의

3. 실전 예제

이론을 이해했다면, 이제 실제 프로젝트에서 자주 마주치는 세 가지 구조 — 전역 설정 값 관리, 클래스 헤더 분리, static 멤버 변수 — 에 ODR 규칙을 어떻게 적용하는지 살펴보겠습니다.

예제 1: 전역 변수 관리

애플리케이션 전역에서 공유하는 설정 값(최대 연결 수, 앱 이름 등)은 여러 소스 파일에서 참조해야 하는 경우가 많아 ODR을 특히 신경 써야 하는 영역입니다. 아래 config.h는 일반 변수는 extern으로 선언만 해두고, C++17부터는 inline 변수로 헤더에서 바로 정의까지 끝내는 두 가지 방식을 함께 보여줍니다.

// config.h
#ifndef CONFIG_H
#define CONFIG_H

// ❌ 헤더에 정의 (ODR 위반)
// int maxConnections = 100;

// ✅ 헤더에 선언
extern int maxConnections;
extern const char* appName;

// ✅ C++17 inline 변수
inline int maxRetries = 3;

#endif

extern으로 선언된 변수들은 반드시 어딘가에 실제 정의가 있어야 링크에 성공합니다. 아래 config.cpp가 바로 그 유일한 정의가 위치하는 곳입니다.

// config.cpp
#include "config.h"

// 정의 (한 곳에서만)
int maxConnections = 100;
const char* appName = "MyApp";

이제 config.h를 포함하는 다른 파일에서는 extern 변수와 inline 변수를 구분할 필요 없이 동일하게 사용할 수 있습니다.

// main.cpp
#include "config.h"
#include <iostream>

int main() {
    std::cout << "Max connections: " << maxConnections << std::endl;
    std::cout << "Max retries: " << maxRetries << std::endl;
    return 0;
}

예제 2: 클래스 정의

클래스나 구조체의 정의 자체는 ODR의 특별한 예외 조항(정의가 동일하기만 하면 여러 번역 단위에 나타나도 된다는 규칙) 덕분에 헤더에 그대로 두어도 안전합니다. 다만 이는 “선언만 여러 번 허용”이라는 변수·함수의 예외와는 다른 규칙으로, 멤버 함수 몸체까지 헤더 안에서 완성해도 되는 이유가 바로 여기에 있습니다.

// widget.h
#ifndef WIDGET_H
#define WIDGET_H

class Widget {
public:
    Widget();
    void use();
    
private:
    int data;
};

#endif

클래스의 선언 자체는 헤더에 있지만, 각 멤버 함수의 구현(정의)은 여전히 일반 함수와 동일한 ODR 규칙을 따르므로 소스 파일 하나에만 두어야 합니다.

// widget.cpp
#include "widget.h"
#include <iostream>

Widget::Widget() : data(0) {
    std::cout << "Widget 생성" << std::endl;
}

void Widget::use() {
    std::cout << "Widget 사용" << std::endl;
}

여러 소스 파일이 같은 widget.h를 포함해도, 클래스 정의가 완전히 동일한 헤더에서 왔기 때문에 문제없이 각각 Widget 인스턴스를 만들 수 있습니다.

// file1.cpp
#include "widget.h"
Widget w1;  // OK

// file2.cpp
#include "widget.h"
Widget w2;  // OK (클래스 정의는 동일하면 OK)

예제 3: Static 멤버 변수

클래스 안에서 static int count;처럼 선언한 정적 멤버 변수는 클래스 선언과 달리 “선언”일 뿐이며, 실제로 메모리를 갖는 정의는 별도로 클래스 밖에서 한 번 더 해줘야 한다는 점이 초보자들이 자주 놓치는 부분입니다. C++17부터는 inline static을 사용하면 이 번거로운 클래스 외부 정의 없이도 헤더 안에서 선언과 정의를 한 번에 끝낼 수 있습니다.

// counter.h
#ifndef COUNTER_H
#define COUNTER_H

class Counter {
public:
    static int count;  // 선언
    
    Counter() {
        ++count;
    }
};

// ❌ C++17 이전: 헤더에 정의 불가
// int Counter::count = 0;

// ✅ C++17: inline static
class Counter2 {
public:
    inline static int count = 0;  // 정의 가능
    
    Counter2() {
        ++count;
    }
};

#endif
// counter.cpp
#include "counter.h"

// C++17 이전: 소스 파일에 정의
int Counter::count = 0;

4. 자주 발생하는 문제

ODR 규칙 자체는 간단하지만, 실무에서는 아래 네 가지 상황에서 특히 자주 위반이 발생합니다. 링크 에러로 바로 드러나는 경우도 있지만, 컴파일도 링크도 성공한 채로 조용히 정의되지 않은 동작을 일으키는 더 위험한 경우도 있습니다.

문제 1: 헤더에 함수 정의

가장 흔한 초보자 실수는 헤더 파일에 inline 없이 함수 몸체를 직접 작성해두는 것입니다. 이 헤더가 두 개 이상의 .cpp 파일에 포함되면, 각 번역 단위마다 동일한 함수의 완전한 정의가 생겨나 링크 단계에서 반드시 충돌합니다.

// ❌ 비인라인 함수 정의 (ODR 위반)
// utils.h
void func() {
    std::cout << "Hello" << std::endl;
}

// file1.cpp, file2.cpp 모두 인클루드 시 링크 에러!

이 문제는 두 가지 방법으로 해결할 수 있습니다. 함수가 짧고 여러 헤더 사용자가 공유해야 한다면 inline을 붙여 ODR 예외 규칙을 활용하고, 함수가 크거나 헤더에 구현 세부사항을 노출하고 싶지 않다면 선언만 헤더에 남기고 정의는 소스 파일로 옮기는 전통적인 방식을 택합니다.

// ✅ 해결책 1: inline 추가
// utils.h
inline void func() {
    std::cout << "Hello" << std::endl;
}

// ✅ 해결책 2: 소스 파일로 이동
// utils.h
void func();

// utils.cpp
void func() {
    std::cout << "Hello" << std::endl;
}

문제 2: 다른 정의

같은 이름의 타입이 서로 다른 번역 단위에서 서로 다른 내용으로 정의되면, 이는 링크 에러조차 발생시키지 않고 조용히 컴파일과 링크에 성공하는 가장 위험한 형태의 ODR 위반입니다. 아래 예제처럼 Data 구조체가 한 파일에서는 int 멤버로, 다른 파일에서는 double 멤버로 정의되면, 두 번역 단위가 서로 다른 메모리 레이아웃을 가정하고 코드를 생성하기 때문에 실제 실행 시점에 메모리를 잘못 해석하는 정의되지 않은 동작이 발생합니다.

// ❌ 다른 정의 (정의되지 않은 동작)
// file1.cpp
struct Data {
    int x;
};

void process(Data d) {
    std::cout << d.x << std::endl;
}

// file2.cpp
struct Data {
    double x;  // 다른 정의!
};

Data d{3.14};
process(d);  // 정의되지 않은 동작 (UB)

해결책: 헤더 파일에 클래스를 정의하여 모든 번역 단위에서 동일한 정의를 사용하세요.

문제 3: 익명 네임스페이스

익명 네임스페이스 안에 선언된 이름은 각 번역 단위마다 서로 다른 내부 링크(internal linkage) 심볼로 취급되므로, 이를 헤더에 넣고 여러 소스 파일에 포함시키면 그 헤더를 포함하는 파일마다 완전히 독립적인 별개의 변수가 생겨 버립니다. 값이 하나뿐이라고 착각하고 여러 파일에서 공유하려 했다면, 실제로는 파일마다 별도의 복사본이 생겨 값이 서로 동기화되지 않는 예상치 못한 버그로 이어집니다.

// ❌ 헤더에 익명 네임스페이스
// header.h
namespace {
    int value = 10;  // 각 번역 단위마다 다른 value
}

// ✅ 명명된 네임스페이스
// header.h
namespace MyLib {
    extern int value;
}

// source.cpp
namespace MyLib {
    int value = 10;
}

문제 4: constexpr 변수

앞서 ODR 예외 절에서 다룬 내용과 이어지는 문제로, C++17 이전 표준을 대상으로 하는 코드베이스라면 헤더의 constexpr 전역 변수가 여전히 번역 단위마다 별도의 복사본을 만든다는 점을 기억해야 합니다. 대부분의 경우 값 자체는 동일하므로 눈에 보이는 버그로 이어지지는 않지만, 그 변수의 주소를 취해 포인터로 비교하는 코드가 있다면 예상과 다른 결과를 얻을 수 있습니다.

// C++17 이전
// header.h
constexpr int MAX = 100;  // 각 번역 단위마다 복사 (비효율)

// C++17 이후
// header.h
inline constexpr int MAX = 100;  // 하나의 정의 (효율적)

5. ODR 준수 패턴

지금까지 다룬 규칙과 예외들을 실무에서 재사용 가능한 세 가지 패턴으로 정리하면, 헤더-소스 분리, inline 함수, 템플릿 클래스로 나눌 수 있습니다. 각 패턴은 서로 다른 상황에 맞춰 선택하면 됩니다.

패턴 1: 헤더-소스 분리

가장 전통적이고 안전한 패턴은 헤더에는 함수와 변수의 선언만 남기고, 실제 구현은 대응하는 소스 파일에 몰아넣는 것입니다. 이 구조는 헤더를 포함하는 파일이 늘어나도 컴파일 시간이 크게 늘지 않고, 구현 세부사항을 헤더 사용자에게 노출하지 않는다는 장점도 있어 라이브러리 설계의 기본 골격으로 널리 쓰입니다.

// math.h
#ifndef MATH_H
#define MATH_H

// 선언만
int add(int a, int b);
int multiply(int a, int b);

extern int globalCounter;

#endif

헤더에서 선언한 함수와 extern 변수는 이 소스 파일 하나에서만 실제로 정의됩니다.

// math.cpp
#include "math.h"

// 정의
int add(int a, int b) {
    return a + b;
}

int multiply(int a, int b) {
    return a * b;
}

int globalCounter = 0;

패턴 2: inline 함수

한두 줄짜리 짧은 유틸리티 함수까지 매번 헤더-소스로 나누면 오히려 코드가 여러 파일에 흩어져 관리하기 번거로워집니다. 이런 경우 헤더 온리(header-only) 방식으로 inline 함수를 바로 정의해두면, 별도의 소스 파일 없이도 여러 프로젝트에 헤더 하나만 복사해서 재사용할 수 있는 편리함을 얻을 수 있습니다.

// utils.h
#ifndef UTILS_H
#define UTILS_H

#include <iostream>

// inline 함수는 헤더에 정의 가능
inline void log(const std::string& msg) {
    std::cout << "[LOG] " << msg << std::endl;
}

inline int square(int x) {
    return x * x;
}

#endif

패턴 3: 템플릿 클래스

제네릭 컨테이너나 알고리즘처럼 다양한 타입에 대응해야 하는 코드는 애초에 컴파일 시점에 타입이 확정되어야 하므로, 소스 파일로 분리하는 것 자체가 거의 불가능합니다. 이런 코드는 자연스럽게 헤더 온리 형태로 작성하게 되며, 앞서 살펴본 대로 템플릿이 ODR 예외 대상이기 때문에 여러 파일에서 같은 헤더를 포함해도 안전합니다.

// container.h
#ifndef CONTAINER_H
#define CONTAINER_H

#include <vector>

template<typename T>
class Container {
public:
    void add(const T& item) {
        items.push_back(item);
    }
    
    size_t size() const {
        return items.size();
    }
    
private:
    std::vector<T> items;
};

#endif

6. 실전 예제: 라이브러리 설계

지금까지 배운 세 가지 패턴 — 헤더-소스 분리, inline 함수, inline static 멤버 — 은 실제로는 하나의 클래스 안에서 함께 섞여 쓰이는 경우가 더 많습니다. 아래 싱글턴 Logger 클래스는 무거운 초기화 로직을 담은 함수는 소스 파일로 분리하고, 짧은 편의 함수는 헤더에 inline으로 남겨두는 실무적인 절충안을 보여줍니다.

// logger.h
#ifndef LOGGER_H
#define LOGGER_H

#include <string>
#include <iostream>

class Logger {
public:
    // 선언
    static Logger& getInstance();
    void log(const std::string& message);
    
    // inline 함수 (헤더에 정의 가능)
    inline void debug(const std::string& msg) {
        log("[DEBUG] " + msg);
    }
    
private:
    Logger() = default;
    
    // C++17 inline static
    inline static int logCount = 0;
};

#endif

헤더에서 선언만 해둔 getInstance()log()는 아래 소스 파일에서 한 번만 정의됩니다. getInstance() 내부의 지역 static 변수는 C++11부터 스레드 안전하게 초기화되는 것이 표준으로 보장되어, 별도의 동기화 코드 없이도 안전한 싱글턴을 구현할 수 있습니다.

// logger.cpp
#include "logger.h"

Logger& Logger::getInstance() {
    static Logger instance;
    return instance;
}

void Logger::log(const std::string& message) {
    std::cout << message << std::endl;
    ++logCount;
}

사용하는 쪽에서는 헤더에 정의된 inline 함수와 소스 파일에 정의된 함수를 구분할 필요 없이 동일한 방식으로 호출할 수 있습니다.

// main.cpp
#include "logger.h"

int main() {
    Logger& logger = Logger::getInstance();
    logger.log("Hello");
    logger.debug("Debug message");
    
    return 0;
}

7. ODR 위반 탐지

ODR 위반 중 일부(중복 정의)는 링커가 즉시 잡아주지만, 서로 다른 정의가 섞여 정의되지 않은 동작을 일으키는 경우는 컴파일과 링크 모두 조용히 성공하기 때문에 훨씬 찾기 어렵습니다. 아래 세 가지 도구를 조합하면 이런 문제를 좀 더 체계적으로 찾아낼 수 있습니다.

컴파일러 경고

-Wall -Wextra만으로는 ODR 위반 대부분을 잡아내지 못하지만, 헤더에 비인라인 함수 정의처럼 흔한 실수의 일부는 경고로 표시해 줍니다. Clang의 -Weverything은 훨씬 공격적으로 검사하므로, 실제 문제 유무와 무관하게 나오는 스타일성 경고까지 감안하고 사용해야 합니다.

# GCC
g++ -Wall -Wextra -Werror file1.cpp file2.cpp

# Clang
clang++ -Weverything file1.cpp file2.cpp

링커 에러

가장 흔한 형태의 ODR 위반(같은 이름의 완전한 정의가 중복된 경우)은 링커가 명확한 오류 메시지로 알려줍니다. 아래처럼 “multiple definition of”라는 문구와 함께 어느 오브젝트 파일에 처음 정의가 있고 어느 파일이 중복인지까지 알려주므로, 이 메시지만으로도 문제가 되는 파일을 바로 특정할 수 있습니다.

# 링크 에러 예시
# 실행 예제
/usr/bin/ld: file2.o: in function `func()':
file2.cpp:(.text+0x0): multiple definition of `func()'; 
file1.o:file1.cpp:(.text+0x0): first defined here

nm 도구

nm은 오브젝트 파일이나 라이브러리 안에 어떤 심볼이 들어있는지 직접 확인할 수 있는 로우레벨 도구입니다. 링크 에러가 나기 전에 미리 어떤 함수나 변수가 여러 오브젝트 파일에 동시에 정의되어 있는지 찾아보고 싶을 때, nm *.o | grep " T "처럼 텍스트(코드) 심볼만 걸러 정렬해보면 중복된 이름을 눈으로 바로 확인할 수 있습니다.

# 심볼 확인
nm file1.o
nm file2.o

# 중복 정의 찾기
nm *.o | grep " T " | sort

정리

ODR은 여러 파일로 나뉜 C++ 프로그램이 하나의 일관된 실행 파일로 합쳐질 수 있게 하는 근본적인 규칙입니다. 이 규칙과 inline·템플릿·constexpr이라는 세 가지 예외를 정확히 이해하면, 헤더와 소스 파일을 어떻게 나눠야 할지에 대한 판단이 훨씬 명확해집니다.

핵심 요약

  1. ODR: 하나의 정의만 허용
  2. 변수: 전체 프로그램에서 하나
  3. 함수: 전체 프로그램에서 하나
  4. 클래스: 각 번역 단위에서 동일한 정의
  5. 예외: inline, 템플릿, constexpr
  6. C++17: inline 변수, inline static 멤버

ODR 준수 가이드

항목헤더 파일소스 파일
변수 선언extern int var;int var = 0;
함수 선언void func();void func() {}
클래스 정의class C {};-
inline 함수inline void f() {}-
템플릿template<T> class C {};-
inline 변수 (C++17)inline int var = 0;-

실전 팁

헤더 파일:

  • 선언만 포함 (정의는 소스 파일)
  • inline 함수는 헤더에 정의 가능
  • 템플릿은 헤더에 정의 필수
  • C++17 inline 변수 활용

링크 에러 방지:

  • 헤더 가드 사용 (#ifndef 또는 #pragma once)
  • 비인라인 함수는 소스 파일에 정의
  • extern으로 변수 선언/정의 분리
  • static 함수는 내부 링크 (각 파일마다 별도)

디버깅:

  • 링크 에러 메시지 확인 (multiple definition)
  • nm 도구로 심볼 중복 확인
  • 컴파일러 경고 활성화

다음 단계


관련 글


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Everything about C++ One Definition Rule : from basic concepts to practical applications.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, ODR, one-definition-rule, linking, 정의규칙 등으로 검색하시면 이 글이 도움이 됩니다.