나만의 작은 도서관

[TIL][C++] 250812 MMO 서버 개발 78일차: T-SQL이란?, C++에서 T-SQL을 적을때 조심해야 할 점, C++에서 BindCol은 SQL 실행 결과를 바인드한다, T-SQL에 대해 이것저것(SCOPE_IDENTITY(), SET NOCOUNT ON) 본문

Today I Learn

[TIL][C++] 250812 MMO 서버 개발 78일차: T-SQL이란?, C++에서 T-SQL을 적을때 조심해야 할 점, C++에서 BindCol은 SQL 실행 결과를 바인드한다, T-SQL에 대해 이것저것(SCOPE_IDENTITY(), SET NOCOUNT ON)

pledge24 2025. 8. 13. 01:37
주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다. 

T-SQL이란?

  • T-SQL은 Transact-SQL의 약자로, SQL Server와 Azure SQL Database에서 사용하는 SQL의 확장판이다. 기본적인 SQL 표준 문법에 더해, Microsoft가 독자적으로 추가한 기능들이 포함되어 있다.
  • 간단하게 “MS가 원하는 기능을 추가한 SQL → T-SQL “이라고 보면 된다.
  • 당연하게도, T-SQL 전용 문법은 SQL Server에서만 동작한다.

 

T-SQL의 주요 특징

  • 표준 SQL + 확장된 문법
    • 표준 SQL 문법(SELECT, INSERT, UPDATE, DELETE, CREATE TABLE 등)을 모두 지원.
    • 확장 문법
      • 변수 사용 (DECLARE, SET)
      • 제어 흐름문 (IF... ELSE, WHILE, BEGIN... END)
      • 저장 프로시저(Stored Procedure)와 트리거(Trigger)
      • 시스템 함수 (GETDATE(), ISNULL() 등)
      • 에러 처리 (TRY... CATCH)
  • 절차적 프로그래밍 가능
    • T-SQL은 SQL임에도 불구하고 프로그래밍 로직을 넣을 수 있다. 즉, 프로그래밍 언어에서 발견할 수 있는 반복문(ex. for문), 조건문(ex. if), 변수 연산 등을 할 수 있다.

 

T-SQL에서 변수를 선언 + 조건문을 사용하는 예제

DECLARE @UserCount INT;

SELECT @UserCount = COUNT(*)
FROM Users
WHERE Active = 1;

IF @UserCount > 100
    PRINT '활성 유저가 100명을 초과했습니다.';
ELSE
    PRINT '활성 유저가 100명 이하입니다.';

 

 

C++에서 T-SQL을 적을 때 조심해야 할 점

  • C++에서 SQLExecDirect 함수를 이용해서 쿼리를 넘겨주게 되는데, 이때 넘겨주는 쿼리가 길어지면 가독성을 위해 여러 줄에 걸쳐 작성하게 된다. 여러 줄에 작성하는 법은 각 줄 뒤에 ‘\’를 붙여주기만 하면 된다.
DBBind<PARAMS, COLS> dbBind(*dbConn, L"\\
    SELECT character_id\\
    FROM [dbo].[Characters]\\
    WHERE character_name = N'유저1';\\
");
  • 문제는 ‘\’를 사용해서 여러 줄에 걸쳐 쿼리를 작성하면 가끔 파싱이 잘못되어서 오류가 발생한다는 것이다. 겪었던 문제의 쿼리문은 아래와 같다.
BEGIN TRANSACTION;\\
\\
DECLARE @existing_character_id BIGINT;\\
DECLARE @character_id BIGINT;\\
...

// 오류: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Incorrect syntax near 'BEGIN'.
  • 위 쿼리문을 전송하니 오류가 발생했는데, 그 이유를 알아보니 BEGIN TRANSACTION을 T-SQL 블록 안에서 바로 쓰고, 그 뒤에 DECLARE 문을 바로 붙였기 때문이라고 한다.

 

왜 이게 문제가 될까?

//이 형태는 SQL Server에서 허용되지만, 일부 환경(특히 ODBC)에서는 파서가 ; 뒤 공백이 없으면 구문 오류를 뱉을 수 있습니다.
BEGIN TRANSACTION;DECLARE @existing_character_id BIGINT;DECLARE ...
  • 전달되는 쿼리 문자열 안의 ‘\’ 줄 바꿈 처리 때문에 SQL Server가 예상치 못한 곳에서 토큰을 만나 파싱을 잘못한 것! 그래서 T-SQL을 잘못된 방식으로 파싱 하게 되며 “Incorrect syntax near ‘BEGIN’”오류를 뱉는 것이다.
    • 참고로 위 쿼리문을 ‘\’가 빼고 T-SQL 자체에서 실행하면 문제없이 돌아간다.

 

해결 방법 두 가지: ‘\n’을 넣거나 멀티라인 리터럴로 바꿔 작성한다!

 

첫 번째 해결 방법: ‘\n’ 넣기

  • 줄 바꿈 들어가지 않아 문제가 되는거라면 줄 바꿈 문자를 넣어주면 된다. 그러므로 ‘\’ 앞에 ’\n’을 넣어 명시적으로 줄 바꿈을 넣어준다.
L"BEGIN TRANSACTION; \\n\\
DECLARE @existing_character_id BIGINT;\\n\\
..."

 

추가로 조심해야 할 점: 세미콜론 누락

  • 세미콜론이 누락되었다고 SQL문이 반드시 실행이 안되지는 않기에 가끔 누락하고는 하는데, 이 누락이 문제가 되는 경우도 있다. 문장 단위의 SQL문을 작성했다면, 반드시 뒤에 세미콜론을 붙이자.

 

두 번째 해결 방법: 아예 쿼리를 멀티라인 리터럴로 작성하기(C++17~)

  • Raw literal을 지원하는 R 접두사를 사용하면 이스케이프 문자를 처리하지 않게 되면서 여러 줄을 하나의 문자열로 판단하여 처리할 수 있게 된다. 즉, 소스 파일에 적힌 그대로 문자열이 유지되기 때문에 따로 ‘\’나 ‘\n’를 넣지 않아도 된다.
// R 접두사 사용시 ()로 묶어줘야한다.
// SQL이 양쪽에 붙었는데 이건 delimiter로, SQL 문자열이라 적은건 아니고 그냥 사용자가 임의로 적는 거다. 안적어도 된다.
// 적어두면 내부에 ()가 있을때 충돌을 예방하는 효과도 있다.
LR"SQL(BEGIN TRANSACTION;
DECLARE @existing_character_id BIGINT;
...
)SQL"
  • 개인적으론 SQL문 수정할 때마다 ‘\’ 정리가 너무 귀찮아서 “멀티 라인 리터럴”을 사용하는 두 번째 방법이 더 좋은 듯하다. C++ 이 C++17보다 낮은 버전이 아니라면 굳이 첫 번째 방법을 사용할 이유는 없어 보인다.

 

C++에서 BindCol은 SQL 실행 결과를 바인드 한다.

  • 대충 bind 하겠지 하고 넘어간 부분이었는데, 이것저것 T-SQL을 사용하다 보니 알게 되어서 정리해 본다.
  • 결과적으로 BindCol은 “SELECT문을 통해 나온 실행 결과를 바인드 한다”라고 생각하면 편하다. 물론 다른 문법을 통해서도 바인드를 할 수 있지만, 대부분의 경우 Bind는 SELECT문에 의해서 결정된다. 예를 들어,
SELECT -1 AS character_id;
  • 이 SQL문과 같이 -1을 SELECT문으로 반환하게 했다면, 해당 -1이라는 값이 Bind 된다.

 

T-SQL에 대해 이것저것(SCOPE_IDENTITY(), SET NOCOUNT ON)

  • SCOPE_IDENTITY(): 현재 세션과 범위 내에서 마지막으로 삽입된 IDENTITY 값을 반환한다. 즉, 동일한 스코프 내에서 생성된 자동 증가 값(IDENTITY)을 정확하게 가져올 때 사용한다.
    • 값은 IDENTITY로 설정한 열이 있는 행을 삽입할 때 가져올 수 있다.
  • SET NOCOUNT ON; : NOCOUN는 말 그대로 카운트를 하지 않을 것인지 설정하는 옵션인데, 해당 옵션을 ON으로 설정하면 쿼리 수행결과로 “X개의 행이 영향을 받음”과 같은 메시지 반환이 나타나지 않게 된다.
    • 메시지 반환이 되지 않으면서 출력 부하를 없앨 수 있으며, 해당 메시지로 인해 ODBC 커서 흐름을 방해받는 것을 막을 수도 있다.