윈도우 시간 자동설정 코드, “시간 맞추기”보다 중요한 건 구조다
윈도우에서 시간 자동설정 코드를 만든다고 하면 많은 사람이 먼저 시스템 시간을 직접 바꾸는 함수부터 떠올립니다. 하지만 실제 운영에서는 시간을 직접 덮어쓰는 방식보다, Windows Time 서비스와 표준 도구를 이용해 동기화 흐름을 맞추는 방식이 더 안정적입니다. 마이크로소프트도 w32tm을 Windows Time 서비스 구성·모니터링·문제 해결을 위한 기본 명령줄 도구로 안내하고 있습니다.
핵심은 두 가지입니다. 첫째, 시간대(Time Zone) 를 올바르게 맞추는 것, 둘째, 시간 동기화(Time Sync) 를 수행하는 것입니다. 시간대는 PowerShell의 Set-TimeZone으로 설정할 수 있고, 현재 시간대 조회나 전체 목록 확인은 Get-TimeZone으로 처리할 수 있습니다. 이 cmdlet들은 Windows 플랫폼에서 동작합니다.
예를 들어 한국 표준시로 맞추고 시간 동기화를 다시 수행하는 가장 단순한 PowerShell 흐름은 아래와 같습니다.
# 관리자 권한 PowerShell
Set-TimeZone -Id "Korea Standard Time"
w32tm /resync
이 방식이 깔끔한 이유는 운영체제의 기본 메커니즘을 그대로 활용하기 때문입니다. 특히 Windows Time 서비스는 네트워크 상의 시계를 NTP 기반으로 동기화하며, 인증과 여러 시스템 기능에 중요한 역할을 합니다. 도메인 환경에서는 시간 불일치가 인증 문제로 이어질 수도 있기 때문에, 임의로 시간을 박아 넣는 코드보다 서비스 기반 접근이 훨씬 안전합니다.
실무에서 자동설정 코드를 작성할 때는 보통 “설정 → 확인 → 재동기화 → 예외처리” 순서로 짭니다. 예를 들면 이런 식입니다.
try {
$targetTz = "Korea Standard Time"
$currentTz = (Get-TimeZone).Id if ($currentTz -ne $targetTz) {
Set-TimeZone -Id $targetTz
} w32tm /resync | Out-Null
Write-Output "시간 자동설정 완료"
}
catch {
Write-Error "시간 자동설정 실패: $($_.Exception.Message)"
}
이 코드는 단순하지만 의미가 분명합니다. 먼저 현재 시간대를 읽고, 목표 시간대와 다를 때만 변경합니다. 그다음 w32tm /resync로 동기화를 요청합니다. 다만 이 과정은 관리자 권한이 필요한 경우가 많고, 환경에 따라 w32tm /resync가 실패할 수도 있습니다. 마이크로소프트는 로컬에서 w32tm 사용 시 관리자 그룹 권한이 필요할 수 있다고 안내하고 있고, 동기화 실패 시 “사용 가능한 시간 데이터가 없다” 같은 오류에 대한 별도 문제 해결 문서도 제공합니다.
여기서 중요한 포인트가 하나 더 있습니다. 사용자가 말하는 “시간 자동설정”은 사실 두 의미로 나뉩니다. 하나는 시간 서버와 자동 동기화이고, 다른 하나는 시간대를 자동으로 감지하는 것입니다. 전자는 w32tm과 Windows Time 서비스 영역이고, 후자는 Windows 설정의 “시간대를 자동으로 설정” 토글과 관련이 있습니다. PowerShell에서 시간대를 특정 값으로 설정하는 것은 가능하지만, “자동” 자체를 Set-TimeZone의 값으로 넣는 방식은 아닙니다.
그래서 컬럼의 결론은 분명합니다. 윈도우 시간 자동설정 코드는 “현재 시각을 강제로 바꾸는 코드”가 아니라, 운영체제가 제공하는 시간 서비스와 시간대 설정을 정석대로 호출하는 코드여야 합니다. 그렇게 해야 권한 문제, 동기화 안정성, 도메인 환경 호환성, 유지보수성까지 함께 챙길 수 있습니다. 단순한 몇 줄짜리 스크립트라도, 구조를 제대로 잡으면 운영 품질은 크게 달라집니다.
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <stdio.h>
#include <time.h>
#include <fstream>
#include <string>
#pragma comment(lib, “ws2_32.lib”)
#define NTP_TIMESTAMP_DELTA 2208988800ull
const char* ntp_servers[] = {
“129.6.15.28”,
“132.163.97.1”,
“216.239.35.0”,
“1.1.1.1”
};
void WriteLog(const char* message) {
const size_t MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB
char path[MAX_PATH];
GetModuleFileNameA(NULL, path, MAX_PATH);
*strrchr(path, ‘\\’) = ‘\0’;
strcat_s(path, “\\timesync.log”);
// 파일 크기 확인
DWORD fileSize = 0;
HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile != INVALID_HANDLE_VALUE) {
fileSize = GetFileSize(hFile, NULL);
CloseHandle(hFile);
}
// 파일 열기 모드 결정
std::ofstream log;
if (fileSize > MAX_LOG_SIZE) {
log.open(path, std::ios::out); // 덮어쓰기
}
else {
log.open(path, std::ios::app); // 추가
}
if (log.is_open()) {
SYSTEMTIME st;
GetLocalTime(&st);
log << “[” << st.wYear << “-” << st.wMonth << “-” << st.wDay << ” “
<< st.wHour << “:” << st.wMinute << “:” << st.wSecond << “] “
<< message << “\n”;
log.close();
}
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
const int max_rounds = 5;
const int server_count = sizeof(ntp_servers) / sizeof(ntp_servers[0]);
for (int round = 1; round <= max_rounds; ++round) {
WriteLog((“count ” + std::to_string(round) + ” start”).c_str());
for (int i = 0; i < server_count; ++i) {
const char* server_ip = ntp_servers[i];
WriteLog((“try server: ” + std::string(server_ip)).c_str());
SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (s == INVALID_SOCKET) {
WriteLog(“socket failed”);
continue;
}
int timeout = 5000;
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout));
struct sockaddr_in server = { 0 };
server.sin_family = AF_INET;
server.sin_port = htons(123);
server.sin_addr.s_addr = inet_addr(server_ip);
unsigned char packet[48] = { 0 };
packet[0] = 0x1B;
sendto(s, (char*)packet, 48, 0, (struct sockaddr*)&server, sizeof(server));
int server_len = sizeof(server);
int recv_len = recvfrom(s, (char*)packet, 48, 0, (struct sockaddr*)&server, &server_len);
closesocket(s);
if (recv_len < 0) {
WriteLog((“no response : ” + std::string(server_ip)).c_str());
Sleep(1000);
continue;
}
unsigned long long secs = ((unsigned long long)packet[40] << 24) |
((unsigned long long)packet[41] << 16) |
((unsigned long long)packet[42] << 8) |
((unsigned long long)packet[43]);
time_t timestamp = (time_t)(secs – NTP_TIMESTAMP_DELTA);
struct tm gmt = { 0 };
gmtime_s(&gmt, ×tamp);
SYSTEMTIME st;
st.wYear = gmt.tm_year + 1900;
st.wMonth = gmt.tm_mon + 1;
st.wDay = gmt.tm_mday;
st.wHour = gmt.tm_hour;
st.wMinute = gmt.tm_min;
st.wSecond = gmt.tm_sec;
st.wMilliseconds = 0;
if (SetSystemTime(&st)) {
char success_msg[128];
sprintf_s(success_msg, “time sucessed (%s): %04d-%02d-%02d %02d:%02d:%02d UTC”,
server_ip, st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
WriteLog(success_msg);
WSACleanup();
return 0;
}
else {
DWORD err = GetLastError();
char fail_msg[128];
sprintf_s(fail_msg, “system fail (%s): error code %ld”, server_ip, err);
WriteLog(fail_msg);
}
Sleep(1000);
}
}
WriteLog(“fail”);
WSACleanup();
return 1;
}