본문으로 건너뛰기 C++ duration | '시간 간격' 가이드

C++ duration | '시간 간격' 가이드

C++ duration | '시간 간격' 가이드

이 글의 핵심

C++ duration의 C++, duration, "시간, 1.

들어가며

std::chrono::duration시간 간격을 표현하는 타입입니다. 타입 안전하며, 단위 변환이 명확하며, 시간 연산을 지원합니다. C 스타일 코드에서는 흔히 시간을 int 하나로 표현하고 그것이 초인지 밀리초인지는 변수 이름이나 주석에만 의존했지만, duration은 단위 정보를 타입 자체에 담아 컴파일 타임에 단위 실수를 잡아낼 수 있게 해줍니다. 예를 들어 sleep(1000)이 1000초인지 1000밀리초인지 헷갈려서 생기는 버그는, std::chrono::milliseconds(1000)처럼 타입으로 명시하면 애초에 발생할 수 없습니다.


실전 경험에서 배운 교훈

duration을 실무에 도입할 때 가장 자주 마주치는 함정은 하위 단위에서 상위 단위로 변환할 때 발생하는 정밀도 손실입니다. duration_cast는 기본적으로 소수점을 버리는(절삭) 방식으로 동작하기 때문에, 1.5초를 duration_cast<seconds>로 변환하면 조용히 1초가 되어버립니다. 이 사실을 모르고 타이머나 재시도 로직에 duration_cast를 사용하면, 의도한 것보다 짧게 대기하거나 통계가 미묘하게 어긋나는 버그로 이어질 수 있습니다. 이 글에서는 이런 변환 규칙을 명확히 하고, C++14 리터럴과 C++17 반올림 함수를 활용해 더 안전하게 시간을 다루는 방법을 정리합니다.

1. duration 기본

기본 사용

std::chrono::duration은 사실 “틱(tick)의 개수”와 “1틱이 몇 초인지”라는 두 정보를 조합한 템플릿 타입입니다. seconds, milliseconds, minutes, hours는 이 템플릿을 자주 쓰는 단위로 미리 특수화해둔 타입 별칭일 뿐이므로, 서로 다른 이름을 가졌어도 근본적으로는 같은 duration 틀 안에서 동작합니다. count() 메서드는 그 duration이 담고 있는 순수한 숫자 값(틱 개수)을 꺼내는 유일한 방법으로, 이 값 자체는 단위 정보를 포함하지 않는다는 점에 유의해야 합니다.

#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono;
    
    // 시간 간격 생성: duration 타입
    // std::chrono::seconds: 초 단위 duration
    seconds sec(10);  // 10초
    // std::chrono::milliseconds: 밀리초 단위 duration
    milliseconds ms(10000);  // 10000밀리초 = 10초
    // std::chrono::minutes: 분 단위 duration
    minutes min(1);  // 1분 = 60초
    // std::chrono::hours: 시간 단위 duration
    hours hr(1);  // 1시간 = 60분 = 3600초
    
    // 값 얻기: count() 메서드로 숫자 값 추출
    std::cout << "초: " << sec.count() << std::endl;  // 10
    std::cout << "밀리초: " << ms.count() << std::endl;  // 10000
    
    return 0;
}

표준 duration 타입

C++11 표준은 나노초부터 시간(hour) 단위까지 여섯 가지 기본 duration 타입을 <chrono> 헤더에 미리 정의해두었습니다. 이들은 모두 내부적으로 정수 카운터와 std::ratio(분수 형태의 단위 비율)의 조합으로 구현되어 있어, 서로 다른 단위끼리도 정확한 비율로 변환할 수 있습니다. 아래 코드는 여섯 가지 타입이 모두 같은 시간(1시간)을 표현할 수 있다는 것을 보여줍니다.

#include <chrono>

// C++11 표준 단위
std::chrono::hours h(1);
std::chrono::minutes m(60);
std::chrono::seconds s(3600);
std::chrono::milliseconds ms(3600000);
std::chrono::microseconds us(3600000000);
std::chrono::nanoseconds ns(3600000000000);

// 모두 1시간을 표현

2. 시간 변환

암시적 변환 (상위 → 하위)

큰 단위를 작은 단위로 바꾸는 변환은 정보 손실이 전혀 없기 때문에(10초는 정확히 10000밀리초와 같으므로) 컴파일러가 자동으로 처리해줍니다. seconds에서 milliseconds로의 대입이 별도의 캐스팅 함수 없이 그냥 대입문만으로 이루어지는 것도 이 때문입니다. 다만 정수 오버플로우 가능성은 여전히 존재하므로, 매우 긴 시간을 매우 작은 단위(나노초 등)로 변환할 때는 정수 타입의 표현 범위를 염두에 두어야 합니다.

#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono;
    
    seconds s(10);  // 10초
    
    // ✅ 상위 → 하위 (암시적 변환 가능)
    // 큰 단위 → 작은 단위: 정보 손실 없음
    // seconds → milliseconds: 10초 = 10000밀리초
    milliseconds ms = s;  // 10000ms (자동 변환)
    // seconds → microseconds: 10초 = 10000000마이크로초
    microseconds us = s;  // 10000000us (자동 변환)
    
    std::cout << "밀리초: " << ms.count() << std::endl;  // 10000
    std::cout << "마이크로초: " << us.count() << std::endl;  // 10000000
    
    return 0;
}

명시적 변환 (하위 → 상위)

반대로 작은 단위를 큰 단위로 바꾸는 변환은 정보를 잃을 수 있습니다(1500밀리초는 1.5초인데, seconds는 정수만 표현할 수 있어 소수부를 담을 수 없습니다). 표준 라이브러리는 이런 손실 가능성이 있는 변환을 암시적으로 허용하지 않고, 반드시 duration_cast를 명시적으로 호출하도록 강제해 개발자가 “이 변환에서 정밀도를 잃을 수 있다”는 사실을 의식하도록 만듭니다. duration_cast의 기본 동작은 절삭(truncation)이므로, 1500ms는 1s로 변환되며 나머지 500ms는 그냥 버려집니다.

#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono;
    
    milliseconds ms(1500);  // 1500밀리초 = 1.5초
    
    // ❌ 하위 → 상위 (암시적 불가)
    // 작은 단위 → 큰 단위: 정보 손실 가능 (소수점 버림)
    // seconds s = ms;  // 컴파일 에러: 명시적 변환 필요
    
    // ✅ duration_cast (명시적 변환)
    // duration_cast<seconds>: 밀리초를 초로 변환
    // 1500ms → 1s (500ms 버림, 절삭)
    seconds s = duration_cast<seconds>(ms);  // 1s (절삭)
    
    std::cout << "초: " << s.count() << std::endl;  // 1
    
    return 0;
}

C++17 반올림 함수

duration_cast의 절삭 동작이 항상 원하는 결과는 아닙니다. C++17은 이를 보완하기 위해 round(반올림), ceil(올림), floor(내림) 세 가지 함수를 추가로 제공합니다. 예를 들어 사용자에게 “약 몇 초 남았는지” 보여주는 UI라면 round가 더 자연스러운 결과를 주고, 타임아웃 계산처럼 “최소한 이만큼은 기다려야 한다”는 보장이 필요하다면 값을 줄여서는 안 되므로 ceil이 적합합니다. floorduration_cast와 동일한 절삭 동작을 하지만 이름으로 의도를 더 명확히 드러낼 수 있습니다.

#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono;
    
    milliseconds ms(1500);  // 1500밀리초 = 1.5초
    
    // duration_cast: 절삭 (소수점 버림)
    // 1.5초 → 1초 (0.5초 버림)
    auto s1 = duration_cast<seconds>(ms);
    std::cout << "절삭: " << s1.count() << "s" << std::endl;  // 1s
    
    // round: 반올림 (C++17)
    // 1.5초 → 2초 (0.5 이상이므로 올림)
    auto s2 = round<seconds>(ms);
    std::cout << "반올림: " << s2.count() << "s" << std::endl;  // 2s
    
    // ceil: 올림 (C++17)
    // 1.5초 → 2초 (무조건 올림)
    auto s3 = ceil<seconds>(ms);
    std::cout << "올림: " << s3.count() << "s" << std::endl;  // 2s
    
    // floor: 내림 (C++17)
    // 1.5초 → 1초 (무조건 내림, duration_cast와 동일)
    auto s4 = floor<seconds>(ms);
    std::cout << "내림: " << s4.count() << "s" << std::endl;  // 1s
    
    return 0;
}

3. 시간 연산

산술 연산

duration은 마치 숫자 타입처럼 사칙연산과 나머지 연산을 그대로 지원합니다. 이는 내부적으로 각 연산자가 오버로드되어 있어, 두 duration을 더하거나 정수를 곱하는 것이 일반 정수 연산만큼이나 자연스럽게 표현되도록 설계되었기 때문입니다. 서로 다른 단위의 duration끼리 연산할 때는(예: seconds + milliseconds) 표준이 정한 규칙에 따라 자동으로 더 정밀한 단위로 맞춰져 계산되므로, 개발자가 수동으로 단위를 맞출 필요가 없습니다.

#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono;
    
    seconds s1(10);
    seconds s2(5);
    
    // 덧셈
    auto sum = s1 + s2;
    std::cout << "합: " << sum.count() << "s" << std::endl;  // 15s
    
    // 뺄셈
    auto diff = s1 - s2;
    std::cout << "차: " << diff.count() << "s" << std::endl;  // 5s
    
    // 곱셈
    auto mul = s1 * 2;
    std::cout << "곱: " << mul.count() << "s" << std::endl;  // 20s
    
    // 나눗셈
    auto div = s1 / 2;
    std::cout << "나눔: " << div.count() << "s" << std::endl;  // 5s
    
    // 나머지
    auto mod = s1 % s2;
    std::cout << "나머지: " << mod.count() << "s" << std::endl;  // 0s
    
    return 0;
}

비교 연산

duration 사이의 비교 연산자(<, >, == 등)도 단위가 다른 값끼리 자연스럽게 비교할 수 있도록 설계되어 있습니다. 아래 예제의 s1 == ms처럼 secondsmilliseconds를 직접 비교해도, 내부적으로 두 값을 공통의(더 정밀한) 단위로 변환한 뒤 실제 시간 길이를 기준으로 비교하기 때문에 10초와 10000밀리초는 올바르게 같다고 판정됩니다. 이 덕분에 서로 다른 곳에서 각기 다른 단위로 만들어진 duration 값들도 안전하게 서로 비교할 수 있습니다.

#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono;
    
    seconds s1(10);
    seconds s2(5);
    milliseconds ms(10000);
    
    std::cout << std::boolalpha;
    std::cout << "s1 > s2: " << (s1 > s2) << std::endl;  // true
    std::cout << "s1 == ms: " << (s1 == ms) << std::endl;  // true (단위 다름)
    
    return 0;
}

4. duration 리터럴 (C++14)

리터럴 사용

std::chrono::seconds(10)처럼 매번 타입 이름을 완전히 써서 duration을 만드는 것은 코드가 길어지고 가독성이 떨어집니다. C++14는 std::chrono_literals 네임스페이스에 10s, 500ms처럼 숫자 뒤에 단위를 붙이는 사용자 정의 리터럴을 도입해 이 문제를 해결했습니다. 이 리터럴들은 일반 산술 연산자와도 자연스럽게 조합되므로, 아래 예제처럼 1h + 30min + 45s + 500ms같이 여러 단위를 섞어 하나의 총 시간을 표현하는 코드를 매우 직관적으로 작성할 수 있습니다.

#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono_literals;
    
    // 리터럴로 duration 생성
    auto d1 = 10s;      // seconds
    auto d2 = 500ms;    // milliseconds
    auto d3 = 100us;    // microseconds
    auto d4 = 100ns;    // nanoseconds
    auto d5 = 1min;     // minutes
    auto d6 = 2h;       // hours
    
    // 조합
    auto total = 1h + 30min + 45s + 500ms;
    
    auto total_ms = std::chrono::duration_cast<std::chrono::milliseconds>(total);
    std::cout << "총 시간: " << total_ms.count() << "ms" << std::endl;  // 5445500ms
    
    return 0;
}

5. 자주 발생하는 문제

문제 1: 정밀도 손실

duration_cast가 항상 절삭한다는 사실을 잊으면, 통계 집계나 사용자에게 보여줄 경과 시간 계산에서 미묘하게 값이 낮게 나오는 버그가 생깁니다. 예를 들어 1.5초가 계속 1초로 절삭되어 누적되면 실제 경과 시간과 표시되는 값 사이에 눈에 띄는 오차가 쌓일 수 있습니다. 반올림이 더 자연스러운 상황이라면 C++17의 round를 사용해 이런 체계적인 편향을 피해야 합니다.

#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono;
    
    milliseconds ms(1500);
    
    // ❌ duration_cast: 절삭 (500ms 손실)
    seconds s1 = duration_cast<seconds>(ms);
    std::cout << "절삭: " << s1.count() << "s" << std::endl;  // 1s
    
    // ✅ round: 반올림
    seconds s2 = round<seconds>(ms);
    std::cout << "반올림: " << s2.count() << "s" << std::endl;  // 2s
    
    return 0;
}

문제 2: 타입 불일치

duration을 매개변수로 받는 함수에 단순 정수를 넘기려고 하면 컴파일 에러가 발생합니다. 이는 버그가 아니라 오히려 duration이 제공하는 타입 안전성의 핵심입니다. 정수 10이 초를 의미하는지 밀리초를 의미하는지 함수 시그니처만으로는 알 수 없기 때문에, 표준 라이브러리는 이런 암묵적 변환을 아예 허용하지 않고 std::chrono::seconds(10)이나 C++14 리터럴(10s)처럼 단위를 명시하도록 강제합니다.

#include <chrono>
#include <iostream>

void waitFor(std::chrono::seconds s) {
    std::cout << s.count() << "초 대기" << std::endl;
}

int main() {
    // ❌ 타입 불일치
    // waitFor(10);  // 컴파일 에러
    
    // ✅ 명시적 생성
    waitFor(std::chrono::seconds(10));
    
    // ✅ 리터럴 (C++14)
    using namespace std::chrono_literals;
    waitFor(10s);
    
    return 0;
}

문제 3: 음수 duration

duration의 내부 표현이 부호 있는 정수(대부분의 표준 타입은 std::int64_t 기반)이기 때문에, 뺄셈 결과가 음수가 되는 것을 막을 방법이 없습니다. 두 시점 사이의 차이를 계산할 때 어느 쪽이 더 나중인지 확신할 수 없다면 결과가 음수일 가능성을 항상 고려해야 하며, “경과 시간”처럼 항상 양수여야 하는 값을 다룬다면 아래처럼 부호를 명시적으로 처리하는 코드가 필요합니다.

#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono;
    
    seconds s1(5);
    seconds s2(10);
    
    // 음수 duration
    auto diff = s1 - s2;  // -5s
    
    std::cout << "차이: " << diff.count() << "s" << std::endl;  // -5
    
    // 절대값
    auto abs_diff = (diff < seconds::zero()) ? -diff : diff;
    std::cout << "절대값: " << abs_diff.count() << "s" << std::endl;  // 5
    
    return 0;
}

문제 4: 0 duration

seconds(0)처럼 직접 값을 넣어 0을 표현할 수도 있지만, 표준은 zero()라는 정적 멤버 함수를 제공해 “이 duration 타입의 0 값”을 더 명확하게 표현할 수 있게 해줍니다. zero(), min(), max()는 duration 타입의 특수값을 다룰 때 매직 넘버(0, 하드코딩된 최댓값 등)를 직접 쓰는 대신 사용하는 관용구로, 초기화되지 않은 타이머 상태를 표시하거나 값의 유효 범위를 검사할 때 유용합니다.

#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono;
    
    // 0 duration
    seconds s = seconds::zero();
    milliseconds ms = milliseconds::zero();
    
    std::cout << std::boolalpha;
    std::cout << "s == 0: " << (s == seconds::zero()) << std::endl;  // true
    
    return 0;
}

6. 실전 예제: 타이머 유틸리티

Timer 클래스는 지금까지 다룬 duration 개념을 실제 경과 시간 측정에 적용한 예입니다. steady_clock을 사용하는 이유는(대신 system_clock을 쓰지 않는 이유는) 시스템 시계가 NTP 동기화나 사용자 조작으로 갑자기 앞뒤로 튈 수 있는 반면, steady_clock은 오직 단조 증가만 보장하기 때문에 경과 시간 측정처럼 “항상 앞으로만 흐르는 시간”이 필요한 용도에 적합합니다. elapsedMs는 현재 시각에서 시작 시각을 뺀 결과(두 time_point의 차이는 duration이 됩니다)를 duration_cast로 밀리초 단위로 변환해 반환합니다.

#include <chrono>
#include <thread>
#include <iostream>

class Timer {
    using Clock = std::chrono::steady_clock;
    using TimePoint = std::chrono::time_point<Clock>;
    
    TimePoint start;
    
public:
    Timer() : start(Clock::now()) {}
    
    // 경과 시간 (밀리초)
    long long elapsedMs() const {
        auto now = Clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - start);
        return duration.count();
    }
    
    // 경과 시간 (마이크로초)
    long long elapsedUs() const {
        auto now = Clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(now - start);
        return duration.count();
    }
    
    // 재시작
    void reset() {
        start = Clock::now();
    }
};

int main() {
    Timer timer;
    
    // 작업 1
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::cout << "작업 1 완료: " << timer.elapsedMs() << "ms" << std::endl;
    
    // 작업 2
    std::this_thread::sleep_for(std::chrono::milliseconds(200));
    std::cout << "작업 2 완료: " << timer.elapsedMs() << "ms" << std::endl;
    
    // 재시작
    timer.reset();
    
    // 작업 3
    std::this_thread::sleep_for(std::chrono::milliseconds(150));
    std::cout << "작업 3 완료: " << timer.elapsedMs() << "ms" << std::endl;
    
    return 0;
}

출력:

작업 1 완료: 100ms
작업 2 완료: 300ms
작업 3 완료: 150ms

정리

핵심 요약

  1. duration: 시간 간격 표현
  2. count(): 값 얻기
  3. 상위→하위: 암시적 변환
  4. 하위→상위: duration_cast 필요
  5. 연산: +, -, *, /, % 지원
  6. 리터럴: 10s, 500ms (C++14)

duration 변환 규칙

변환방법예시
상위 → 하위암시적milliseconds ms = seconds(10);
하위 → 상위duration_castseconds s = duration_cast<seconds>(ms);
반올림round (C++17)seconds s = round<seconds>(ms);
올림ceil (C++17)seconds s = ceil<seconds>(ms);
내림floor (C++17)seconds s = floor<seconds>(ms);

실전 팁

사용 원칙:

  • 상위→하위는 암시적
  • 하위→상위는 duration_cast
  • 반올림은 round (C++17)
  • 리터럴 사용 (10s, 500ms)

성능:

  • duration_cast는 컴파일 타임
  • 연산은 정수 연산과 동일
  • 타입 안전으로 버그 방지
  • 리터럴로 가독성 향상

주의사항:

  • duration_cast는 절삭 (정밀도 손실)
  • 음수 duration 가능
  • zero(), min(), max() 특수 값
  • 단위 불일치 주의

다음 단계


관련 글


자주 묻는 질문 (FAQ)

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

A. Everything about C++ duration : from basic concepts to practical applications.

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

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

Q. 더 깊이 공부하려면?

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


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

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


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

C++, duration, chrono, time, C++11 등으로 검색하시면 이 글이 도움이 됩니다.