# HWP 복붙하면 왜 ?가 되나요 — MSSQL VARCHAR vs NVARCHAR 완전 정복

> HWP에서 복사한 특수문자가 MSSQL에 저장하면 ?로 깨지는 이유를 분석합니다. VARCHAR와 NVARCHAR의 차이, 코드 페이지의 개념, N 접두사 사용법, 다른 DB와의 비교까지 한 번에 정리합니다.

**Published:** 2026-04-20 | **Updated:** 2026-04-20

---


## 이런 경험, 한 번쯤 있지 않으셨나요?

오후 3시. 어김없이 전화가 걸려옵니다.

> "홈페이지 공지사항 올렸는데요, 글자가 이상하게 나와요. 물음표가 막 떠요."

담당자분은 분명히 "별다른거 없이 멀쩡하게 글을 썼어요." 라고 합니다. 하지만 경험치가 있는 전산 담당자는 알죠. 몇마디 나눠보면 한글 문서(HWP)에서 내용을 **그대로 복사해서** 붙여넣었다고 합니다. 미리보기에서도 멀쩡했고요. 그런데 저장하고 나서 보니 이런 화면이 펼쳐집니다.

```
교육과정 안내 ? 신청 방법
수업료 ?15만 원 (분할납부 가능)
```

{{< img src="images/contents/broken-char-example.png" alt="웹페이지에서 특수문자가 ?로 표시되는 실제 화면 — VARCHAR 컬럼에 HWP 특수문자를 저장했을 때 나타나는 문제" caption="캡처이미지: codeslog" >}}

분명히 중앙점 특수문자가 들어가 있었는데, 어딘가에서 `?`로 바뀌어버렸습니다.

DB를 열어보면...

```sql
SELECT content FROM notices WHERE id = 123456;
-- 결과: 교육과정 안내 ? 신청 방법
```

역시 DB에 이미 `?`로 들어가 있었습니다. 문제는 저장 시점이었어요.

HWP 파일은 결백합니다. 네트워크도 결백합니다. 브라우저도 결백합니다.
**용의자는 처음부터 `VARCHAR` 컬럼이었습니다.**

이 글에서는 이 문제가 **왜 생기는지**, 그리고 **어떻게 해결하는지** 차근차근 설명해보겠습니다.

---

## 어떤 문자가 문제였나?

HWP에서 자주 사용하는 중앙점·따옴표·쌍따옴표 계열 문자들을 실제로 테스트해봤습니다. 컬럼 타입이 `VARCHAR`인 경우, 아래처럼 결과가 달라집니다.

### 중앙점 계열

| 문자 | 유니코드 | 이름 | VARCHAR 저장 결과 |
|------|----------|------|:-----------------:|
| `・` | U+30FB | 가타카나 중점 | ❌ `?` |
| `•` | U+2022 | 불릿 | ❌ `?` |
| `·` | U+00B7 | 중간점 (Middle Dot) | ✅ 정상 |
| `˙` | U+02D9 | 위 점 (Dot Above) | ✅ 정상 |

### 홑따옴표 계열

| 문자 | 유니코드 | 이름 | VARCHAR 저장 결과 |
|------|----------|------|:-----------------:|
| `'` | U+2018 | 왼쪽 작은따옴표 | ✅ 정상 |
| `'` | U+2019 | 오른쪽 작은따옴표 | ✅ 정상 |
| `′` | U+2032 | 프라임 | ✅ 정상 |
| `´` | U+00B4 | 악센트 (Acute Accent) | ✅ 정상 |

### 쌍따옴표 계열

| 문자 | 유니코드 | 이름 | VARCHAR 저장 결과 |
|------|----------|------|:-----------------:|
| `"` | U+201C | 왼쪽 큰따옴표 | ✅ 정상 |
| `"` | U+201D | 오른쪽 큰따옴표 | ✅ 정상 |
| `″` | U+2033 | 더블 프라임 | ✅ 정상 |
| `˝` | U+02DD | 더블 악센트 | ✅ 정상 |

`・`(가타카나 중점)과 `•`(불릿)만 `?` 처리된 것이죠.

이 두 문자의 공통점은 뭘까요? 서버의 **코드 페이지**에 포함되지 않은 문자라는 점입니다.

---

## 문제의 핵심: 코드 페이지와 유니코드

### 코드 페이지(Code Page)란?

컴퓨터가 문자를 저장할 때는 결국 숫자(바이트)로 변환해야 합니다. 이 **"어떤 문자를 어떤 숫자로 바꾼다"는 약속표**가 바로 코드 페이지입니다.

한국어 환경에서 많이 쓰이는 코드 페이지는 **CP949** (EUC-KR 확장)입니다. `가` → `0xB0A1`, `나` → `0xB3AA` 이런 식으로 매핑됩니다.

문제는, 이 약속표에 **등록되지 않은 문자**는 저장할 수가 없다는 점입니다. SQL Server는 그냥 `?`로 대체해버립니다.

- `・` (U+30FB, 가타카나 중점) → CP949에 없음 → `?`
- `•` (U+2022, 불릿) → CP949에 없음 → `?`
- `·` (U+00B7, 중간점) → KS X 1001 문자 집합에 포함 → 정상 저장

### 유니코드(Unicode)란?

유니코드는 세상의 거의 모든 문자를 하나의 표준 번호 체계로 정리한 것입니다. `가` = U+AC00, `A` = U+0041, `・` = U+30FB... 이런 식으로요.

MSSQL의 `NVARCHAR`는 이 유니코드를 **UTF-16LE** 방식으로 저장합니다. 코드 페이지에 의존하지 않기 때문에, HWP에서 복사한 어떤 특수문자든 그대로 저장할 수 있습니다.

{{< img src="images/contents/code-page-mapping.png" alt="코드 페이지(CP949)와 유니코드 비교 개념도 — CP949에 없는 문자(• ・)는 ?로 대체되고, NVARCHAR의 유니코드는 모든 문자를 저장합니다" caption="이미지: Nanobanana AI로 생성" >}}

---

## VARCHAR vs NVARCHAR, 무엇이 다른가?

| 구분 | `VARCHAR` | `NVARCHAR` |
|------|-----------|------------|
| 저장 방식 | 서버 코드 페이지 (CP949 등) | 유니코드 (UTF-16LE) |
| 한 글자 용량 | 영문 1바이트, 한글 2바이트 | 모든 문자 2바이트 (서로게이트 4바이트) |
| 저장 용량 예시 | `VARCHAR(100)` = 최대 100바이트 | `NVARCHAR(100)` = 최대 200바이트 |
| 코드 페이지 밖 문자 | `?`로 손상 | 정상 저장 |
| 권장 상황 | ASCII 위주 데이터 (영문 로그 등) | 한글·특수문자·다국어 포함 데이터 |

### 용량에 대한 오해

`NVARCHAR`는 글자당 2바이트를 사용하므로, 같은 길이라면 저장 공간이 2배라고 볼 수 있습니다. 그래서 레거시 시스템에서는 "용량 낭비"를 이유로 `VARCHAR`를 고집하는 경우가 있어요.

하지만 현대의 저장 비용을 생각하면, 사용자 입력 데이터에서 이 차이는 거의 의미가 없습니다.

`VARCHAR`로 아낀 바이트 vs `NVARCHAR`로 막은 야근 — 어느 쪽이 더 중요할까요.

{{< img src="images/contents/varchar-nvarchar-comparison.png" alt="VARCHAR와 NVARCHAR 비교 — VARCHAR는 코드 페이지 기반 저장으로 특수문자 손상 위험, NVARCHAR는 유니코드로 모든 문자 안전 저장" caption="이미지: Nanobanana AI로 생성" >}}

---

## 해결 방법

### 1단계: 컬럼 타입을 NVARCHAR로 변경

```sql
ALTER TABLE notices
ALTER COLUMN content NVARCHAR(4000);
```

> **주의**: 이미 `?`로 저장된 데이터는 컬럼을 바꿔도 복구되지 않습니다. 원본 데이터를 다시 입력해야 합니다.

### 2단계: 쿼리에 N 접두사 추가

이 부분을 빠뜨리는 경우가 많습니다. **컬럼이 `NVARCHAR`여도, 리터럴 문자열에 `N`을 붙이지 않으면 여전히 코드 페이지로 처리됩니다.**

```sql
-- ❌ 여전히 ? 가 될 수 있음
INSERT INTO notices (content) VALUES ('교육과정 안내 ・ 신청 방법');

-- ✅ 올바른 방법
INSERT INTO notices (content) VALUES (N'교육과정 안내 ・ 신청 방법');

-- UPDATE도 마찬가지
UPDATE notices SET content = N'수업료 ・15만 원' WHERE id = 42;
```

`N'...'` 표기는 SQL Server에게 "이 문자열을 유니코드로 처리하세요"라고 명시적으로 알려주는 것입니다.

### 애플리케이션 레이어에서의 처리

직접 SQL을 작성하는 경우 외에도, ADO.NET 등을 사용할 때 파라미터 타입을 확인해야 합니다.

```csharp
// ❌ SqlDbType.VarChar 로 바인딩하면 손상 발생 가능
cmd.Parameters.Add("@content", SqlDbType.VarChar).Value = content;

// ✅ NVarChar 로 명시
cmd.Parameters.Add("@content", SqlDbType.NVarChar).Value = content;
```

대부분의 현대 ORM(Entity Framework 등)은 `string` 타입을 자동으로 `NVARCHAR`로 매핑하지만, 레거시 코드나 직접 ADO.NET을 사용하는 경우에는 반드시 확인이 필요합니다.

---

## DB를 바꾸기 어렵다면: 서버사이드 문자 치환

컬럼 타입을 `NVARCHAR`로 바꾸는 것이 가장 확실한 해결책이지만, 언제나 그게 가능한 건 아닙니다. 레거시 시스템이거나, 써드파티 솔루션을 사용 중이거나, 배포 일정상 DB 변경이 어렵다면 **저장 전에 서버사이드에서 문제 문자를 안전한 대체 문자로 바꾸는** 방법이 현실적인 대안이 될 수 있습니다.

일종의 **통역사**를 두는 방식입니다. HWP에서 `・`를 들고 오면, DB에 전달하기 전에 먼저 `·`로 바꿔서 건네주는 거죠.

### 치환 원칙

- 가능하면 **시각적으로 유사한 문자**로 대체 (예: `・` → `·`)
- 완전히 같은 문자가 없으면 **의미에 가장 가까운 문자**로 교체 (예: `—` → `-`)

### C# 예제

```csharp
private static readonly Dictionary<char, string> HwpCharReplacements = new()
{
    { '\u30FB', "·" },  // 가타카나 중점 → 중간점(U+00B7)
    { '\u2022', "·" },  // 불릿 → 중간점
    { '\u2027', "·" },  // Hyphenation Point → 중간점
    { '\u2014', "-" },  // Em 대시 → 하이픈
    { '\u2013', "-" },  // En 대시 → 하이픈
    { '\u2010', "-" },  // Hyphen (U+2010) → 하이픈
    { '\u2015', "-" },  // Horizontal Bar → 하이픈
    { '\u2192', "->" }, // 오른쪽 화살표
    { '\u21D2', "=>" }, // 이중 화살표
    { '\u3000', " " },  // 전각 공백 → 반각 공백
    { '\uFF01', "!" },  // 전각 느낌표
    { '\uFF08', "(" },  // 전각 왼쪽 괄호
    { '\uFF09', ")" },  // 전각 오른쪽 괄호
    { '\uFF1A', ":" },  // 전각 콜론
};

public static string ReplaceUnsafeChars(string input)
{
    if (string.IsNullOrEmpty(input)) return input;

    var sb = new StringBuilder(input.Length);
    foreach (char c in input)
    {
        if (HwpCharReplacements.TryGetValue(c, out var replacement))
            sb.Append(replacement);
        else
            sb.Append(c);
    }
    return sb.ToString();
}
```

저장 전에 호출하면 됩니다:

```csharp
string sanitized = ReplaceUnsafeChars(userInput);
cmd.Parameters.Add("@content", SqlDbType.VarChar).Value = sanitized;
```

CP949에서 처리 불가능한 문자가 있는지 사전에 확인하고 싶다면 이런 방법도 있습니다.

```csharp
public static bool IsVarCharSafe(string input)
{
    var cp949 = Encoding.GetEncoding(949);
    byte[] bytes = cp949.GetBytes(input);
    return cp949.GetString(bytes) == input;
}
```

인코딩·디코딩을 거친 문자열이 원본과 다르면, 중간에 `?`로 손상되는 문자가 있다는 뜻입니다.

### Python 예제

Python에서 `pyodbc`나 `pymssql`로 MSSQL을 사용하는 경우:

```python
CHAR_REPLACEMENTS = {
    '\u30FB': '·',  # 가타카나 중점
    '\u2022': '·',  # 불릿
    '\u2027': '·',  # Hyphenation Point
    '\u2014': '-',  # Em 대시
    '\u2013': '-',  # En 대시
    '\u2010': '-',  # Hyphen
    '\u2015': '-',  # Horizontal Bar
    '\u2192': '->',
    '\u21D2': '=>',
    '\u3000': ' ',  # 전각 공백
    '\uFF01': '!',
    '\uFF08': '(',
    '\uFF09': ')',
    '\uFF1A': ':',
}

def replace_unsafe_chars(text: str) -> str:
    return ''.join(CHAR_REPLACEMENTS.get(c, c) for c in text)
```

> **한계**: 치환 목록에 없는 새로운 특수문자가 유입되면 여전히 `?` 손상이 발생할 수 있습니다. 완전한 해결책은 아니에요. DB 마이그레이션이 가능해지는 시점에 `NVARCHAR`로 전환하는 것을 권장합니다.

---

## 다른 데이터베이스는 어떤가요?

MSSQL만 이런 문제가 있는 건 아닙니다. 각 DB마다 접근 방식이 다릅니다.

| DB | VARCHAR 동작 | 권장 설정 |
|----|--------------|-----------|
| **MSSQL** | 서버 코드 페이지 기준 저장. 코드 페이지 외 문자는 `?` 손상 | `NVARCHAR` + `N` 접두사 |
| **MySQL** | 컬럼/테이블/DB별 문자셋 지정 가능. 기본값 `latin1`이면 동일 문제 발생 | `CHARACTER SET utf8mb4` 설정 |
| **PostgreSQL** | DB 생성 시 인코딩 `UTF8` 설정 시 `VARCHAR`도 유니코드 저장. 기본값이 UTF-8이라 실무에서 거의 문제 없음 | `ENCODING 'UTF8'` (대부분 기본값) |
| **Oracle** | `VARCHAR2`는 DB 캐릭터셋 기준. `NVARCHAR2`는 유니코드 전용 | `NVARCHAR2` 또는 DB를 `AL32UTF8`로 설정 |
| **SQLite** | 내부적으로 모든 텍스트를 UTF-8로 저장. `TEXT` 타입이면 유니코드 문제 거의 없음 | 기본값으로 충분 |

**PostgreSQL이나 SQLite처럼 UTF-8을 기본으로 사용하는 DB였다면 처음부터 이 문제를 겪지 않았을 수도 있습니다.** MSSQL은 오래된 코드 페이지 기반 설계의 영향이 지금도 남아 있어, 특히 한국어 환경에서는 주의가 필요합니다.

{{< img src="images/contents/multiple-databases.jpg" alt="DB는 저마다 문자 인코딩 접근 방식이 다릅니다" caption="사진: <a href='https://unsplash.com/ko/%EC%82%AC%EC%A7%84/%EC%84%9C%EB%B2%84%EC%8B%A4%EC%97%90-%EC%9E%88%EB%8A%94-%EC%84%9C%EB%B2%84-%EB%9E%99-2JJ3wBHu4_0?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText'>Unsplash</a>의 <a href='https://unsplash.com/ko/@kevinache?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText'>Kevin Ache</a>" >}}

---

## 실무에서 체크할 것들

### 기존 데이터베이스 점검

```sql
-- VARCHAR 컬럼 중 ? 가 포함된 데이터 확인
SELECT id, content
FROM notices
WHERE content LIKE '%?%';

-- 사용자 입력 컬럼이 VARCHAR로 잡혀있는지 일괄 확인
SELECT
    TABLE_NAME,
    COLUMN_NAME,
    DATA_TYPE,
    CHARACTER_MAXIMUM_LENGTH
FROM INFORMATION_SCHEMA.COLUMNS
WHERE DATA_TYPE IN ('varchar', 'char', 'text')
  AND TABLE_SCHEMA = 'dbo'
ORDER BY TABLE_NAME, COLUMN_NAME;
```

### 신규 테이블 설계 원칙

한국어 데이터를 다루는 MSSQL 환경이라면, 사용자 입력이 들어가는 컬럼은 **처음부터 `NVARCHAR`로 설계**하는 것을 권장합니다.

```sql
CREATE TABLE notices (
    id       INT            IDENTITY(1,1) PRIMARY KEY,
    title    NVARCHAR(500)  NOT NULL,
    content  NVARCHAR(MAX)  NOT NULL,  -- MAX = 약 2GB
    reg_date DATETIME       DEFAULT GETDATE()
);
```

`NVARCHAR(MAX)`는 최대 약 2GB까지 저장할 수 있으며, 게시판 본문처럼 길이 제한이 불확실한 경우에 적합합니다.

{{< img src="images/contents/data-protection.jpg" alt="데이터 손상은 조용히 일어납니다. 자물쇠로 자산을 미리 보호하듯 처음부터 제대로 설계하는 것이 데이터를 지키는 최선입니다." caption="사진: <a href='https://unsplash.com/ko/%EC%82%AC%EC%A7%84/%EC%A1%B0%ED%95%A9-%EC%9E%90%EB%AC%BC%EC%87%A0%EB%8A%94-%EC%BB%B4%ED%93%A8%ED%84%B0-%ED%82%A4%EB%B3%B4%EB%93%9C-%EC%9C%84%EC%97%90-%EB%86%93%EC%97%AC-%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4-WUJmdr8pNwk?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText'>Unsplash</a>의 <a href='https://unsplash.com/ko/@sasun1990?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText'>Sasun Bughdaryan</a>" >}}

---

## 정리

HWP에서 복사·붙여넣기한 텍스트가 MSSQL에서 `?`로 깨지는 이유, 이제 명확해지셨나요?

1. `VARCHAR`는 **서버 코드 페이지(CP949 등)** 기준으로 저장됩니다.
2. HWP의 일부 특수문자는 해당 코드 페이지에 **포함되지 않아** `?`로 손상됩니다.
3. **`NVARCHAR`로 컬럼 타입을 변경**하고, **쿼리 리터럴에 `N` 접두사**를 붙이면 해결됩니다.
4. PostgreSQL, SQLite 등은 기본이 UTF-8이라 이 문제가 드물지만, MSSQL은 특히 주의가 필요합니다.

신규 시스템을 설계한다면 한글이 들어가는 모든 컬럼을 `NVARCHAR`로 하는 것을 기본값으로 삼는 것이 좋습니다. 나중에 데이터가 깨진 뒤 복구하는 것보다, 처음부터 제대로 설정하는 편이 훨씬 낫거든요.

> 데이터 손상은 조용히 일어납니다. 발견했을 때는 이미 늦은 경우가 많아요.

---

## 참고 자료

- [KS X 1001 — Wikipedia](https://en.wikipedia.org/wiki/KS_X_1001): KS X 1001 표준에 포함된 문자 목록. `′`(U+2032), `˙`(U+02D9) 등이 CP949에 포함되는 근거를 확인할 수 있습니다.
- [CP949 to Unicode Mapping — Unicode Consortium](https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT): Windows CP949(코드 페이지 949) 공식 유니코드 매핑 테이블

