C++ LNK2019 | "unresolved external symbol" 링커 에러 원인 5가지와 해결법

C++ LNK2019 | "unresolved external symbol" 링커 에러 원인 5가지와 해결법

이 글의 핵심

C++ LNK2019에 대한 실전 가이드입니다.

들어가며: C++ 개발자라면 무조건 겪는 링커 에러

LNK2019: unresolved external symbol 에러는 C++ 개발자라면 누구나 한 번쯤 만나는 링커(linker) 에러입니다. 링커는 컴파일된 오브젝트 파일들을 묶어 실행 파일·라이브러리를 만드는 단계이고, unresolved external symbol은 “선언은 있는데 실제 정의(구현)를 찾을 수 없다”는 뜻입니다. “컴파일은 되는데 링크에서 실패한다”, “정의를 찾을 수 없다”는 메시지가 나오면 이 글에서 다루는 원인 중 하나일 가능성이 높습니다. 컴파일은 성공했는데 링크 단계에서 “이 함수·변수의 실제 구현을 찾을 수 없다”며 빌드가 실패합니다. 에러 메시지만 봐서는 원인을 찾기 어렵고, 프로젝트 구조·빌드 설정·헤더/소스 분리 등 여러 곳을 확인해야 합니다.

이 글에서 다루는 것:

  • LNK2019 에러가 나는지 (컴파일 vs 링크 단계)
  • 5가지 주요 원인과 각각의 해결법
  • Visual Studio, CMake, Makefile 환경별 대응

요구 환경: 예제와 해결법은 Visual Studio(Windows) 및 g++/Clang + CMake(Linux/macOS) 기준입니다. 링커 에러이므로 컴파일러·IDE·빌드 시스템은 본인이 쓰는 환경 그대로 두고, 프로젝트 설정만 맞추면 됩니다.

증상 → 원인 → 해결(점검 순서) 을 한 번에 잡고 가시려면 아래 순서를 권장합니다.

  1. 증상: 빌드 로그에 LNK2019 / unresolved external symbol 이 뜨고, 어느 심볼(함수·변수 이름·장식된 이름)을 찾지 못했는지가 적혀 있습니다.
  2. 원인 방향: 그 심볼의 정의가 있는지, 그 정의가 들어 있는 .cpp가 빌드에 포함되는지, 필요한 .lib/.a를 링크하는지, 이름·네임스페이스·호출 규약이 일치하는지, 템플릿 정의가 헤더에 있는지 중 하나로 좁혀 갑니다.
  3. 해결: 아래 목차의 원인 1~5에 해당하는 조치를 적용한 뒤, Visual Studio·CMake 절차로 링크 입력과 빌드 구성을 다시 확인합니다.

목차

  1. LNK2019가 뭔가요? (컴파일 vs 링크)
  2. 원인 1: 함수 선언만 있고 정의가 없음
  3. 원인 2: 소스 파일(.cpp)을 빌드에 포함 안 함
  4. 원인 3: 라이브러리를 링크하지 않음
  5. 원인 4: 네임스페이스·이름 불일치
  6. 원인 5: 템플릿 정의가 헤더에 없음
  7. Visual Studio에서 확인하는 법
  8. CMake에서 확인하는 법

1. LNK2019가 뭔가요? (컴파일 vs 링크)

컴파일과 링크의 차이

C++ 빌드는 두 단계로 나뉩니다. 이 구분을 이해하면 LNK2019 에러가 나는지 명확해집니다.

1단계: 컴파일 (Compile)
.cpp 파일을 **오브젝트 파일(.obj 또는 .o)**로 변환합니다. 이때 컴파일러는 “이 함수가 어딘가에 정의되어 있겠지”라고 가정하고, 선언(void foo();)만 있어도 통과시킵니다. 즉, 헤더 파일에 선언만 있어도 컴파일은 성공합니다.

비유: 레스토랑 메뉴판(헤더)에 “스테이크”가 적혀 있으면, 주문은 받을 수 있습니다. 실제로 주방에 스테이크가 있는지는 나중 문제입니다.

2단계: 링크 (Link)
모든 오브젝트 파일과 라이브러리를 모아서 실행 파일을 만듭니다. 이때 링커는 “호출된 모든 함수·변수의 실제 구현”을 찾습니다. 찾지 못하면 LNK2019 에러가 발생합니다.

비유: 주문받은 스테이크를 실제로 만들려고 주방을 뒤졌는데, 재료가 없으면 주문을 완료할 수 없습니다. 이게 링크 실패입니다.

왜 두 단계로 나뉘나요?
대규모 프로젝트에서는 수백~수천 개의 .cpp 파일이 있습니다. 한 파일만 수정해도 전체를 다시 컴파일하면 시간이 오래 걸립니다. 컴파일과 링크를 분리하면, 수정된 파일만 재컴파일하고, 나머지는 기존 오브젝트 파일을 재사용할 수 있어서 증분 빌드(Incremental Build)가 가능합니다.

아래 다이어그램은 소스 파일이 실행 파일이 되기까지의 흐름을 요약합니다. LNK2019는 2단계(링크)에서 “정의를 찾을 수 없음”이 발생할 때 나옵니다.

flowchart LR
  subgraph compile["1단계: 컴파일"]
    A[.cpp + 헤더] --> B[컴파일러]
    B --> C[.obj / .o]
    C --> D["선언만 있어도 OK"]
  end
  subgraph link["2단계: 링크"]
    C --> E[링커]
    E --> F[.exe / 실행파일]
    E -.->|"정의 없음 →"| G[LNK2019 에러]
  end

에러 메시지 예시

error LNK2019: unresolved external symbol "void __cdecl foo(void)" (?foo@@YAXXZ) 
referenced in function _main

해석:

  • foo() 함수가 main에서 호출되었습니다.
  • 하지만 링커가 foo()정의(구현)를 찾지 못했습니다.
  • 컴파일은 성공했지만(선언만 있어도 OK), 링크 단계에서 실패했습니다.

2. 원인 1: 함수 선언만 있고 정의가 없음

문제 상황

헤더 파일(utils.h):

#ifndef UTILS_H
#define UTILS_H

void printMessage();  // 선언만 있음

#endif

main.cpp:

#include "utils.h"

int main() {
    printMessage();  // ❌ LNK2019: unresolved external symbol
    return 0;
}

utils.cpp가 없거나, 있어도 빌드에 포함 안 됨 → 링커가 printMessage()의 구현을 찾지 못합니다.

한 파일로 정상 빌드되는 예(선언과 정의를 같은 파일에 두면 LNK2019가 나지 않음):

// 복사해 붙여넣은 뒤: g++ -std=c++17 -o ok ok.cpp && ./ok  (선언+정의가 같이 있어서 링크 성공)
#include <iostream>
void printMessage();  // 선언
int main() {
    printMessage();
    return 0;
}
void printMessage() {  // 정의
    std::cout << "Hello from utils!\n";
}

실행 결과: Hello from utils! 가 출력됩니다. 위처럼 정의를 같은 파일에 두거나, 별도 utils.cpp를 만들고 빌드에 포함하면 LNK2019가 사라집니다.

왜 이런 일이 생기나요?
초보자가 가장 자주 하는 실수입니다. 헤더 파일에 함수 선언을 추가하고, main.cpp에서 호출했는데, 정작 구현을 작성하는 걸 깜빡합니다. 컴파일러는 “선언이 있으니 어딘가에 구현이 있겠지”라고 믿고 컴파일을 통과시키지만, 링커는 실제 구현을 찾지 못해 에러를 냅니다.

해결법

utils.cpp를 만들고 정의 추가:

#include "utils.h"
#include <iostream>

void printMessage() {
    std::cout << "Hello from utils!\n";
}

빌드에 포함:

Visual Studio:
솔루션 탐색기에서 프로젝트 우클릭 → 추가 → 기존 항목 → utils.cpp 선택. 또는 새 항목 추가로 직접 생성할 수도 있습니다.

CMake:

add_executable(myapp main.cpp utils.cpp)

또는 라이브러리로 분리:

add_library(utils utils.cpp)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE utils)

Makefile:

myapp: main.o utils.o
	g++ -o myapp main.o utils.o

main.o: main.cpp utils.h
	g++ -c main.cpp

utils.o: utils.cpp utils.h
	g++ -c utils.cpp

g++ 직접 빌드 (간단한 프로젝트):

g++ main.cpp utils.cpp -o myapp

3. 원인 2: 소스 파일(.cpp)을 빌드에 포함 안 함

문제 상황

CMakeLists.txt:

add_executable(myapp
    main.cpp
    # utils.cpp를 깜빡함 ❌
)

utils.cppprintMessage() 정의가 있지만, 빌드 대상에 포함되지 않아서 링커가 찾지 못합니다.

왜 이런 일이 생기나요?
프로젝트에 새 .cpp 파일을 추가했는데, 빌드 시스템에 알려 주는 걸 깜빡하는 경우입니다. 파일 탐색기에는 보이지만, CMake나 Visual Studio 프로젝트 설정에는 포함되지 않아서 컴파일 자체가 안 됩니다. 당연히 링커도 찾을 수 없습니다.

확인 방법:
빌드 출력 창을 보면 utils.cpp컴파일되는 메시지가 없습니다. main.cpp만 컴파일되고 바로 링크로 넘어갑니다.

해결법

CMake: 소스 파일을 명시적으로 추가:

add_executable(myapp
    main.cpp
    utils.cpp  # ✅ 추가
)

Visual Studio: 솔루션 탐색기에서 프로젝트 우클릭 → 추가 → 기존 항목 → utils.cpp 선택.

Makefile: 컴파일 명령에 포함:

myapp: main.o utils.o
	g++ -o myapp main.o utils.o

utils.o: utils.cpp utils.h
	g++ -c utils.cpp

4. 원인 3: 라이브러리를 링크하지 않음

문제 상황

외부 라이브러리(예: mylib.lib 또는 libmylib.a)의 함수를 호출했는데, 링크 설정에 해당 라이브러리를 추가하지 않음.

#include <mylib/api.h>

int main() {
    mylib::doSomething();  // ❌ LNK2019
    return 0;
}

왜 이런 일이 생기나요?
헤더 파일(mylib/api.h)을 include하면 선언은 보이므로 컴파일이 됩니다. 하지만 실제 구현은 **별도의 라이브러리 파일(.lib, .a, .so, .dll)**에 있습니다. 이 라이브러리를 링커에게 알려 주지 않으면, 링커는 구현을 찾을 수 없습니다.

실전 예시:

  • Boost.Asio 사용 시 boost_system.lib 링크 필요
  • OpenSSL 사용 시 libssl.lib, libcrypto.lib 링크 필요
  • Windows API 사용 시 ws2_32.lib(소켓), user32.lib(윈도우) 등 링크 필요

해결법

Visual Studio:

  1. 프로젝트 속성 → 링커 → 입력 → 추가 종속성에 mylib.lib 추가.
  2. 링커 → 일반 → 추가 라이브러리 디렉터리에 .lib 파일 경로 추가.

CMake:

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE mylib)

또는 외부 라이브러리:

find_library(MYLIB_LIBRARY NAMES mylib PATHS /usr/local/lib)
target_link_libraries(myapp PRIVATE ${MYLIB_LIBRARY})

g++ 명령줄:

g++ main.cpp -o myapp -L/path/to/lib -lmylib

5. 원인 4: 네임스페이스·이름 불일치

문제 상황

헤더(utils.h):

namespace util {
    void printMessage();
}

소스(utils.cpp):

#include "utils.h"
#include <iostream>

// ❌ 네임스페이스 없이 정의
void printMessage() {
    std::cout << "Hello\n";
}

main.cpp:

#include "utils.h"

int main() {
    util::printMessage();  // ❌ LNK2019: util::printMessage를 찾을 수 없음
    return 0;
}

원인: 헤더는 util::printMessage로 선언했지만, 소스는 전역 네임스페이스printMessage를 정의했습니다. 링커 입장에서는 다른 함수입니다.

왜 다른 함수인가요?
C++에서 네임스페이스는 함수 이름의 일부입니다. util::printMessage::printMessage(전역)는 완전히 다른 심볼(symbol)입니다. 마치 [email protected][email protected]이 다른 이메일 주소인 것과 같습니다.

Name Mangling:
C++ 컴파일러는 함수 이름을 맹글링(mangling)해서 오버로딩·네임스페이스를 구분합니다. 예를 들어:

  • util::printMessage()?printMessage@util@@YAXXZ
  • ::printMessage()?printMessage@@YAXXZ

링커는 이 맹글링된 이름으로 함수를 찾으므로, 네임스페이스가 다르면 완전히 다른 함수로 인식합니다.

해결법

소스에도 네임스페이스 추가:

#include "utils.h"
#include <iostream>

namespace util {
    void printMessage() {  // ✅ 네임스페이스 일치
        std::cout << "Hello\n";
    }
}

또는:

#include "utils.h"
#include <iostream>

void util::printMessage() {  // ✅ 스코프 지정
    std::cout << "Hello\n";
}

6. 원인 5: 템플릿 정의가 헤더에 없음

문제 상황

템플릿 함수·클래스는 정의가 헤더에 있어야 합니다. .cpp에 정의를 두면, 다른 파일에서 인스턴스화할 때 링커가 찾지 못합니다.

헤더(math.h):

template <typename T>
T add(T a, T b);  // 선언만

소스(math.cpp):

#include "math.h"

template <typename T>
T add(T a, T b) {  // ❌ 정의가 .cpp에
    return a + b;
}

main.cpp:

#include "math.h"

int main() {
    int result = add(3, 5);  // ❌ LNK2019
    return 0;
}

원인: 템플릿은 사용 시점에 인스턴스화됩니다. main.cpp를 컴파일할 때 add<int>가 필요한데, math.cpp에 정의가 있어도 main.cpp는 그걸 볼 수 없습니다.

템플릿의 특수성:
일반 함수는 컴파일 타임에 한 번만 코드가 생성됩니다. 하지만 템플릿은 사용하는 타입마다 별도의 코드가 생성됩니다(add<int>, add<double> 등). 이 코드 생성은 템플릿을 사용하는 파일을 컴파일할 때 일어나므로, 그 시점에 정의를 볼 수 있어야 합니다.

구체적인 과정:

  1. main.cpp 컴파일 시 add(3, 5) 발견 → add<int> 코드 생성 필요
  2. math.h에는 선언만 있음 → 정의를 찾을 수 없어서 코드 생성 실패
  3. 컴파일러는 “나중에 링커가 찾겠지”라고 외부 심볼로 표시
  4. 링커가 add<int>를 찾지만, math.cppmain.cpp별도로 컴파일되었고, add<int>를 사용하지 않았으므로 코드가 생성되지 않음
  5. 결과: LNK2019 에러

해결법 1: 정의를 헤더로 이동

math.h:

#ifndef MATH_H
#define MATH_H

template <typename T>
T add(T a, T b) {  // ✅ 헤더에 정의
    return a + b;
}

#endif

해결법 2: 명시적 인스턴스화 (Explicit Instantiation)

math.cpp에서 사용할 타입을 미리 인스턴스화:

#include "math.h"

template <typename T>
T add(T a, T b) {
    return a + b;
}

// ✅ 명시적 인스턴스화
template int add<int>(int, int);
template double add<double>(double, double);

이렇게 하면 math.cpp 컴파일 시 add<int>, add<double>의 코드가 생성되어, 링커가 찾을 수 있습니다. 하지만 사용할 타입을 미리 알아야 하므로, 보통은 헤더에 정의하는 것이 일반적입니다.


7. Visual Studio에서 확인하는 법

출력 창 확인

빌드 실패 시 출력 창(Output)에서 전체 에러 메시지를 확인합니다.

error LNK2019: unresolved external symbol "void __cdecl foo(void)" (?foo@@YAXXZ) 
referenced in function _main
  • referenced in function _main: main에서 호출했다는 뜻.
  • ?foo@@YAXXZ: 맹글링된 이름(name mangling). 실제 함수명은 foo().

프로젝트 속성 확인

링커 → 입력 → 추가 종속성:

  • 필요한 .lib 파일이 모두 나열되어 있는지 확인.
  • 예: ws2_32.lib, mylib.lib 등.

링커 → 일반 → 추가 라이브러리 디렉터리:

  • .lib 파일이 있는 경로가 포함되어 있는지 확인.

C/C++ → 일반 → 추가 포함 디렉터리:

  • 헤더 파일 경로가 맞는지 확인 (컴파일 단계용).

8. CMake에서 확인하는 법

add_executable(myapp main.cpp utils.cpp)

# ✅ 라이브러리 링크
target_link_libraries(myapp PRIVATE mylib)

외부 라이브러리 찾기

find_package(Boost REQUIRED COMPONENTS system)
target_link_libraries(myapp PRIVATE Boost::system)

또는:

find_library(MYLIB_LIBRARY NAMES mylib PATHS /usr/local/lib)
if(NOT MYLIB_LIBRARY)
    message(FATAL_ERROR "mylib not found")
endif()
target_link_libraries(myapp PRIVATE ${MYLIB_LIBRARY})

빌드 로그 확인

cmake --build . --verbose

--verbose로 실제 링크 명령을 보면, 어떤 라이브러리가 링크되는지 확인할 수 있습니다.


정리: LNK2019 해결 체크리스트

원인확인 사항해결법
선언만 있고 정의 없음함수·변수가 어디에도 구현되지 않음.cpp에 정의 추가
소스 파일 미포함.cpp가 빌드 대상에 없음CMake/VS 프로젝트에 소스 추가
라이브러리 미링크.lib/.a 파일을 링크 안 함target_link_libraries 또는 VS 링커 설정
네임스페이스 불일치선언과 정의의 네임스페이스가 다름네임스페이스 맞추기
템플릿 정의 분리템플릿 정의가 .cpp에만 있음정의를 헤더로 이동 또는 명시적 인스턴스화

실전 디버깅 팁

1. 에러 메시지에서 함수 이름 찾기
맹글링된 이름(?foo@@YAXXZ)은 읽기 어렵지만, 앞부분에 실제 함수명이 보입니다. Visual Studio는 보통 "void __cdecl foo(void)"처럼 친절하게 보여 줍니다.

: 에러 메시지를 복사해서 텍스트 에디터에 붙여넣고, 함수 이름 부분만 찾으세요. 여러 개의 LNK2019 에러가 나면, 가장 먼저 나온 에러부터 해결하세요. 첫 번째 에러를 고치면 나머지가 연쇄적으로 해결되는 경우가 많습니다.

2. “referenced in function” 확인
어느 함수에서 호출했는지 알려 줍니다. 해당 파일을 열어서 include한 헤더호출한 함수를 확인하세요.

예시:

referenced in function _main

main() 함수에서 호출했다는 뜻입니다. main.cpp를 열어서 해당 함수 호출 부분을 찾으세요.

3. 대소문자·철자 확인
C++는 대소문자를 구분합니다. printMessagePrintMessage다른 함수입니다.

실전 예시:

// 헤더
void printMessage();

// 소스
void PrintMessage() { }  // ❌ 대문자 P

// main
printMessage();  // ❌ LNK2019: 소문자 p를 찾을 수 없음

4. extern “C” 확인
C 라이브러리를 C++에서 쓸 때는 extern "C"로 감싸야 합니다. 안 그러면 name mangling 때문에 링커가 못 찾습니다.

extern "C" {
    #include <c_library.h>
}

왜 필요한가요?
C는 name mangling을 하지 않습니다. 함수 이름이 그대로 심볼이 됩니다. 하지만 C++는 오버로딩을 지원하므로, 함수 이름에 매개변수 타입 정보를 붙여서 맹글링합니다. extern "C"는 “이 함수는 C 방식으로 링크하라”고 컴파일러에게 알려 줍니다.

5. 32비트 vs 64비트
프로젝트는 x64인데 라이브러리는 x86(32비트)이면 링크 실패합니다. 플랫폼을 맞추세요.

확인 방법 (Visual Studio):

  • 프로젝트 속성 → 일반 → 플랫폼 확인 (x64 또는 Win32)
  • 라이브러리 파일 경로에서 x64/ 또는 x86/ 폴더 확인

6. 라이브러리 빌드 설정 일치
Debug 빌드인데 Release 라이브러리를 링크하거나, /MT(정적 런타임)인데 /MD(동적 런타임) 라이브러리를 링크하면 실패할 수 있습니다. 빌드 설정을 일치시키세요.

한 줄 요약: LNK2019는 선언은 있는데 정의(구현)를 링커가 찾지 못할 때 나오므로, 빌드 대상·링크 옵션·플랫폼 일치를 확인하면 해결됩니다. 다음으로 Segfault 디버깅이나 CMake 링크 에러를 읽어보면 좋습니다.


자주 묻는 질문 (FAQ)

Q. LNK2019와 LNK1120의 차이는?

A: LNK2019는 개별 심볼을 찾지 못한 에러이고, LNK1120은 “해결되지 않은 외부 참조가 N개 있다”는 요약 에러입니다. LNK2019를 모두 해결하면 LNK1120도 사라집니다.

Q. “unresolved external symbol main”이 나오면?

A: main 함수가 없거나, 잘못된 프로젝트 타입입니다. 콘솔 앱인데 Windows 앱으로 설정했거나, 라이브러리 프로젝트인데 실행 파일로 빌드하려 했을 수 있습니다.

Q. 같은 코드인데 Debug는 되고 Release는 안 되면?

A: 라이브러리 파일이 Debug용과 Release용이 따로 있는 경우입니다. Debug 빌드에는 mylib_d.lib를, Release 빌드에는 mylib.lib를 링크해야 합니다.

Q. 헤더 파일만 있는 라이브러리는 링크가 필요 없나요?

A: 네. 헤더 온리(Header-only) 라이브러리는 모든 구현이 헤더에 있으므로, #include만 하면 됩니다. 예: Eigen, nlohmann/json, Boost의 일부.


관련 글

  • CMake 입문: CMake로 멀티파일 프로젝트 빌드하기
  • CMake 치트시트: 라이브러리 링크 템플릿
  • C++ 템플릿 기초: 템플릿 정의를 헤더에 두는 이유

LNK2019는 “컴파일은 되는데 링크가 안 된다”는 신호입니다. 위 5가지 원인 중 하나를 체크하면 대부분 해결됩니다. 에러 메시지에서 함수 이름어디서 호출했는지를 확인한 뒤, 해당 함수의 정의가 빌드에 포함되어 있는지, 라이브러리가 링크되어 있는지를 순서대로 점검하세요.

검색 시 참고 키워드: LNK2019, unresolved external symbol, C++ 링커 에러, 함수 정의 없음, target_link_libraries, Visual Studio 링크 에러


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

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

  • Visual Studio C++ 빌드 느림 | “10분 걸리던 빌드” PCH·/MP로 2분 만들기
  • CMake 입문 | 수십 개 파일 컴파일할 때 필요한 빌드 자동화 (CMakeLists.txt 기초)