• 기본 전제: Grafana, InfluxDB 및 Loki가 이미 설치되어 있다.
  • 운영체제: Amazon Linux 2

여기서는 편의상 sudo su 커맨드를 실행하여 루트 사용자로 전환한 상태에서의 작업 내용을 보여준다.

(1) Promtail 실행파일 다운받고 실행권한 부여

mkdir /opt/promtail && cd /opt/promtail
curl -O -L "https://github.com/grafana/loki/releases/download/v1.5.0/promtail-linux-amd64.zip"
unzip "promtail-linux-amd64.zip"
chmod a+x "promtail-linux-amd64"

(2) yaml 환경설정 파일 편집

위와 동일하게 /opt/promtail 내에 위치한 상태에서 다음 커맨드를 실행하여 yaml 예제 파일을 다운받는다.

curl https://raw.githubusercontent.com/grafana/loki/master/docs/sources/clients/aws/ec2/promtail-ec2.yaml > ec2-promtail.yaml

그리고 이 파일을 열어서 아래와 같은 내용으로 편집해 준다.

server:
  http_listen_port: 3100
  grpc_listen_port: 0

clients:
  - url: https://<Loki 서버 URL>/loki/api/v1/push

positions:
  filename: /opt/promtail/positions.yaml

scrape_configs:
- job_name: system
  static_configs:
  - targets:
      - localhost
    labels:
      host: '<host name>'
      job: varlogs
      __path__: /var/log/*log

(3) Promtail을 데몬으로 띄우기

1) vim /etc/systemd/system/promtail.service을 실행한 후 아래 내용을 입력하여 서비스 생성

[Unit]
Description=Promtail

[Service]
User=root
WorkingDirectory=/opt/promtail/
ExecStartPre=/bin/sleep 30
ExecStart=/opt/promtail/promtail-linux-amd64 --config.file=./ec2-promtail.yaml
SuccessExitStatus=143
TimeoutStopSec=10
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

2) 데몬을 로딩한 후 시작해 주기

systemctl daemon-reload
systemctl enable promtail.service
systemctl start promtail.service
반응형

환경: Amazon Linux 2

작성 위치: /etc/bashrc

* 현재 접속중인 사용자가 누구냐에 관계 없이 전체 시스템에 적용하기 위해 /etc/bashrc 파일에 적용하기로 결정.

* 운영체제가 우분투인 경우, /etc/bash.bashrc

아래 내용을 맨 마지막에 추가해 준다. 여기서 'username:username'은 루트 계정이 아닌 현재 사용자가 접속중인 계정으로 바꿔준다.

if [ ! -f "/var/log/command_history.log" ]; then
    sudo touch /var/log/command_history.log
    sudo chown username:username /var/log/command_history.log
fi

export PROMPT_COMMAND='echo "$(date "+%Y-%m-%d.%H:%M:%S") $(whoami) $(pwd) $(history 1)" >> /var/log/command_history.log;'

 

추가한 내용을 저장한 후 source /etc/bashrc 커맨드 실행을 해주면, /var/log/command_history 내에 로그가 쌓이는 것을 확인할 수 있다.

반응형

다음과 같은 코드가 있다. 현 시점에서 add_exceptionadd_assert 함수의 로직에 대해서는 고려하지 않는다.

int main() {
    int size = 1000000000;

    // x, y의 입력값은 각각 정상범위 (-2,147,483,648 ~ 2,147,483,647)라고 가정.
    int32_t x = 20000;
    int32_t y = 10000;
    int32_t *result_exception = new int32_t[size];
    int32_t *result_assert = new int32_t[size];

    std::chrono::time_point<std::chrono::system_clock> start, end;
    std::chrono::duration<double> elapsed;
    start = std::chrono::system_clock::now();
    for(int i=0; i<size; ++i) {
        try {
             result_exception[i] = add2_exception(x, y);
        } catch(invalid_argument& ia) {
            cout << ia.what() << endl;
        }
    }
    end = std::chrono::system_clock::now();
    elapsed = end - start;
    std::cout << "using exception: " << elapsed.count() << " seconds" << std::endl;

    std::chrono::time_point<std::chrono::system_clock> start2, end2;
    std::chrono::duration<double> elapsed2;
    start2 = std::chrono::system_clock::now();
    for(int i=0; i<size; ++i) {
        result_assert[i] = add2_assert(x, y);
    }
    end2 = std::chrono::system_clock::now();
    elapsed2 = end2 - start2;
    std::cout << "using assert: " << elapsed2.count() << " seconds" << std::endl;

    // 작업이 다 끝난 후 힙메모리에 할당한 변수를 반드시 지워준다.
    delete[] result_exception;
    delete[] result_assert;
    return 0;
}

이 코드에는 중요한 버그가 하나 있는데, new를 통해 힙메모리 할당을 해주는 부분에서 결함이 있다.

변수를 함수 호출 초기에 선언한 후 실행이 끝나기 직전에 delete를 통해서 메모리 해제를 해주도록 하였는데, 달리 말해서 실행이 도중에 멈추면, 즉 delete를 호출하기 전에 프로그램이 죽는 문제가 발생하면?

프로세스가 죽는 이유로는, 내부적인 문제가 원인이 될 수도 있지만 프로세스 외부의 컨디션 등에 문제가 있어서 프로세스가 죽는 원인이 될 수도 있다. 외부의 문제로 인해 프로세스가 죽는다고 가정해 보면, delete 근처로 가기도 전에 프로세스가 죽을 수도 있고, 그렇게 되면 이 프로그램은 메모리 누수를 발생시킨다. 실제 현업에서도 메모리 누수가 발생하여 주기적으로 메모리를 비워주기 위해 물리적으로 서버를 재부팅 하는 경우가 많다.

이런 문제는 네이키드 포인터를 활용해서는 해결이 어렵기 때문에, 스마트 포인터의 활용이 권장된다.

스마트 포인터는 어떤 원리로 동작하는가?

간단하게 말하자면, 스마트 포인터는 소멸자 안에다가 delete를 삽입한 개념이다. 즉 함수 안에서 생성한 변수가 스택 프레임이 날라가면서 지워질 때, 자동적으로 소멸자 호출을 통해 메모리를 해제해 주는 것이다. 그렇기 때문에 스마트 포인터를 사용할 때는 예상치 못한 문제로 인해 프로세스가 도중에 죽더라도 메모리 해제는 하고 죽게 된다.

요새 운영체제에서는 메모리 누수를 주기적으로 감지해서 해제해 준다는데?

사실, 요새라고 하기엔 이미 10년 가까이 지난 이슈이긴 하다. 어쨌든 이것을 고려했을 때는 굳이 네이키드 포인터를 사용하는 것을 주의할 필요가 없지 않은가?

다음 경우를 보자.

(1) 운영체제에서 주기적으로 메모리 누수를 해제한다고 하는데, 그 주기보다 프로세스가 죽었다가 다시 실행되는 주기가 더 빠른 경우
(2) 메모리 누수를 운영체제에서 바로잡는데 발생하는 별도의 오버헤드 발생

이를 봤을 때 안정적으로 운영하려면 그래도 스마트 포인터를 쓰는게 맞다.

최종: 위의 코드를 그럼 어떻게 고치나?

우선 스마트 포인터를 이용한 동적 할당을 다음과 같이 선언해 줄 수 있다.

unique_ptr<int32_t[]> result_exception(new int32_t[size]);
unique_ptr<int32_t[]> result_assert(new int32_t[size]);

그리고 나서 delete 부분은 필요가 없어졌으므로 지워주면 된다.

반응형

네이키드 포인터 (naked pointer)

우리가 흔히 말하는 *를 이용하여 생성된 그 포인터를 말함 (plain pointer).

#include <memory>

struct X { int a,b,c; };

int main()
{
    X* np = new X;
    delete np;
    return 0;
}

여기서 np 변수가 네이키드 포인터를 말한다. np는 동적으로 할당되어 힙메모리 안에 저장이 되는데, 자원의 활용이 끝나면 delete를 활용하여 객체에 할당된 메모리를 반드시 해제해 줘야만 메모리 누수를 막을 수 있다. 해제하지 않으면 힙메모리가 자원을 계속 들고 있어서 성능상 문제가 발생하기 때문이다.

이렇게 개발자가 직접 메모리 해제에 관여해줘야 하는 네이키드 포인터와 달리, 개발자가 직접 메모리 해제에 관여하지 않아도 되도록 설계된 포인터가 스마트 포인터.

스마트 포인터

C++11부터 등장한 새로운 포인터 선언 방식.

스마트 포인터 사용을 위해서는 위의 코드에서 보이듯 #include <memory> 선언을 해줘야 한다.

[예제 1-1] 스마트 포인터를 활용한 예제 코드

#include <iostream>
#include <memory>

using namespace std;

class Blablabla {

    public:
        Blablabla() {
            cout << "자원 획득" << endl;
        }

        void call() {
            cout << "did you call me?" << endl;
        }

        ~Blablabla() {
            cout << "자원 해제" << endl;
        }
};

int main() {
    unique_ptr<Blablabla> mB1(new Blablabla());
    mB1->call();
    return 0;
}

여기서 unique_ptr이 스마트 포인터인데, 스마트 포인터 종류로는 unique_ptr, shared_ptr, weak_ptr 세가지가 있다. 우선 이 문서에서는 unique_ptr, shared_ptr 둘만 다루도록 하겠다.

(1) unique_ptr

여기서 unique는 유일하다는 의미로 쓰여서 '리소스에 대한 유일한 소유권'을 갖는 포인터라는 뜻. 달리 말해서 다른 객체가 unique_ptr로 선언된 리소스에 대한 소유권을 가질 수 없다. 여기서 말하는 소유권을 가질 수 없다는 것은 "타 객체가 이 객체로의 접근을 할 수 없다"라는 뜻 정도 되는것 같다.

[예제 1-2] unique_ptr로 선언된 리소스에 대하여 다른 객체로부터 접근 시도

unique_ptr<Blablabla> mB2 = mB1;
mB2->call();

위의 예제에서는 mB2 객체를 선언한 후, 이 객체가 mB1에 접근하도록 선언해 주고 있는데, 이를 컴파일 하면 에러가 발생한다.

marshall@marshall-IdeaPad-5-15ITL05:~/learn-cpp$ g++ smart_pointer.cpp 
smart_pointer.cpp: In function ‘int main()’:
smart_pointer.cpp:26:30: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = Blablabla; _Dp = std::default_delete<Blablabla>]’
   26 |  unique_ptr<Blablabla> mB2 = mB1;
      |                              ^~~
In file included from /usr/include/c++/9/memory:80,
                 from smart_pointer.cpp:2:
/usr/include/c++/9/bits/unique_ptr.h:414:7: note: declared here
  414 |       unique_ptr(const unique_ptr&) = delete;
      |       ^~~~~~~~~~

에러 메시지에서 말하길 삭제된 함수를 사용하려 한다고 하는데, 이 메시지의 내용을 좀 더 들여다볼 필요가 있다.

(2) shared_ptr (공유 포인터)

unique_ptr과 다르게 shared_ptr을 이용하면 객체 공유가 가능해진다. 예제 1-1의 메인함수 내용을 다음과 같이 고친다.

shared_ptr<Blablabla> mB1(new Blablabla());
mB1->call();

shared_ptr<Blablabla> mB2 = mB1;
mB2->call();

이를 실행하면 다음과 같이 출력한다.

자원 획득
did you call me?
did you call me?
자원 해제

mB1, mB2가 동일한 객체를 소유하고 있다 보니, 함수 종료 후 소멸자 호출도 하나의 객체에서만 이루어짐으로써 '자원 해제'라는 메시지가 한번만 나오는 것을 확인할 수 있다.

shared_ptr를 쓸 때 유의할 점

공유 포인터의 특징은 앞에서 말했듯 한 객체에 여러 공유 포인터가 접근할 수 있다는 점이다.

ClassA* a = new ClassA();
shared_ptr<A> pA1(a);
shared_ptr<A> pA2(a);

이렇게 선언을 하면 pA1, pA2 둘 다 a를 바라보게 된다. 각 공유 포인터 pA1, pA2는 어떤 다른 공유 포인터가 같은 객체를 공유하고 있는지를 알 수 없다. 다시 말해서 각 공유 포인터는 각자의 고유 레퍼런스 카운트를 갖고 있게 된다. 만일 pA1의 레퍼런스 카운트가 0이 된다면 프로그램에서는 pA1이 가리키고 있는 객체 a를 소멸시켜 버릴텐데, 그러면 pA2가 아직 레퍼런스 카운트가 0이 아닌 상태인데, 여기서 a에 대한 접근을 시도하게 될 경우 참조 에러가 발생한다.

[예제 2] 두 공유 포인터

#include <iostream>
#include <memory>

using namespace std;

class Blablabla {

    public:
        Blablabla() {
            cout << "자원 획득" << endl;
        }

        void call() {
            cout << "did you call me?" << endl;
        }

        ~Blablabla() {
            cout << "자원 해제" << endl;
        }
};

int main() {
    Blablabla *b = new Blablabla();
    shared_ptr<Blablabla> mB1(b);
    shared_ptr<Blablabla> mB2(b);

    cout << mB1.use_count() << endl;
    cout << mB2.use_count() << endl;
    return 0;
}

위 코드 자체는 문제없이 컴파일 된다. use_count 함수는 동일하게 참조되는 객체를 참조하는 공유포인터의 수를 리턴한다. [https://en.cppreference.com/w/cpp/memory/shared_ptr/use_count](여기 참조). 다만 실행해 보면 다음과 같이 정상적으로 실행되지 않는 것을 볼 수 있다.

marshall@marshall-IdeaPad-5-15ITL05:~/learn-cpp$ ./a.out 
자원 획득
1
1
자원 해제
자원 해제
free(): double free detected in tcache 2
Aborted (core dumped)

같은 객체에 대해 메모리 해제를 두번 해주려 하다가 문제가 발생했다는 메시지다.

다음 시간에는 weak_ptr 개념에 대한 소개, shared_ptr 포인터 사용시 발생하는 문제점을 해결하는 방법 등에 대한 내용을 싣겠다.

반응형

개요

Qt GUI 개발을 하면서 지속적으로 등장하게 될 시그널과 슬롯의 정의를 다시 한번 명확하게 짚고 넘어감으로써 앞으로 능숙하게 써먹을 준비 갖추기

시그널은 from, 슬롯은 to의 성격을 가지는 개념.

시그널

  • 특정 이벤트 발생시에 내보내어짐 (emit을 이용하여)

시그널 자체는 public으로 접근 가능한 함수이며 따라서 어디서든 접근이 가능하지만, 이 시그널을 정의한 클래스 및 서브클래스에서만 호출할 것을 권장함.

또한 시그널은 별도의 구현 절차가 없다. 무슨 말이냐면, emit sigNoname(param); 식으로 시그널을 발생시키기만 하면, 그 다음에 할 일은 슬롯으로 넘겨서 슬롯 내부에서 로직을 구현하면 되기 때문이다.

다음은 심플한 QObject 기반 클래스 예제다.

#include <QObject>

class Counter : public QObject
{
    Q_OBJECT

public:
    Counter() { m_value = 0; }

    int value() const { return m_value; }

public slots:
    void setValue(int value);

signals:
    void valueChanged(int newValue);

private:
    int m_value;
};

슬롯

특정 이벤트 발생 후 시그널이 emit 되면, 연결된 슬롯에서 이어서 사전에 정의된 작업을 수행한다.

하나의 시그널에는 다수의 슬롯이 연결될 수 있다.

위에서 언급했듯, 시그널과 달리 슬롯은 시그널과 함께 받은 인자를 가지고 무슨 작업을 해줄지 정의해줘야 한다. 위의 Counter 클래스 예제를 통해 정의된 setValue 슬롯을 내부적으로 다음과 같이 정의할 수 있다.

void Counter::setValue(int value)
{
    if (value != m_value) {
        m_value = value;
        emit valueChanged(value);
    }
}

위의 예제에서는 갱신된 값을 인자로 하여 슬롯 내부에서 다시 한번 시그널을 내보내도록 정의하고 있다.

시그널과 슬롯 연결해 주기

그렇다면 이 둘을 어떻게 연결을 해줘야 실질적으로 우리가 기대하는 동작을 할 수 있나?

QObject에서는 connect 함수를 이용해서 둘을 연결하도록 지원하고 있다. Qt 6버전 기준으로 connect 함수는 다음과 같은 6종류가 지원되고 있다. 보다 자세한 내용은 여기 참조.

  • QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)
  • QMetaObject::Connection QObject::connect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method, Qt::ConnectionType type = Qt::AutoConnection)
  • QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const char *method, Qt::ConnectionType type = Qt::AutoConnection) const
  • template <typename PointerToMemberFunction> QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method, Qt::ConnectionType type = Qt::AutoConnection)
  • template <typename PointerToMemberFunction, typename Functor> QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
  • template <typename PointerToMemberFunction, typename Functor> QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *context, Functor functor, Qt::ConnectionType type = Qt::AutoConnection)

현 단계에서는 이 중에서 맨 위의 함수만 확인하고 넘어간다. 왜냐면 현재 프로젝트에서 connect는 이 방식의 호출로 구현이 되고 있었기 때문이다.

파라미터 순서대로 들여다 보면

1. 어떤 객체에서
2. 어떤 시그널을 호출 했을 때
3. 어떤 객체의
4. 어떤 슬롯에서 이를 받는다
5. 그리고 커넥션 타입은 ㅇㅇㅇ와 같다

와 같이 정의되어 있다. 여기서 중요한 점은, 시그널 및 슬롯을 파라미터로 전달할 때 각각 SIGNALSLOG 매크로를 사용해서 던져야 한다는 점이다.

커넥션 타입에는 여섯 종류가 제공되고 있는데, 이는 시그널과 슬롯이 연결된 방식을 정의한다. 다시 말하면 시그널 발생 시 이를 슬롯에 즉시 던지는지, 혹은 기다렸다가 던지는지 등을 정리하고 있다. 현재 크립토 프로젝트 내에서 쓰이고 있는 Qt::QueuedConnection은 그중 하나인데, 이 타입에 대한 설명은 다음과 같다.

The slot is invoked when control returns to the event loop of the receiver's thread. The slot is executed in the receiver's thread.

정확한 내용 이해가 되지 않아서 내용 해석이 이상할 수 있음. 얼른 이해하고 가다듬기

이 설명에 의하면,

  • 이 연결방식을 채택한 경우, 슬롯은 시그널 호출을 받는 스레드 (receiver thread)의 이벤트 루프로 컨트롤이 넘겨질 때 호출이 되며
  • 슬롯은 리시버 스레드 내에서 실행된다.

현재 크립토 코드 내에서 다음과 같이 호출하고 있음을 확인할 수 있다.

QObject::connect(this, SIGNAL(sigLog1(QString)), this, SLOT(slotLog1(QString)), Qt::QueuedConnection);

그러면 위에 있는 Counter의 시그널과 슬롯은 다음과 같이 연결해 주면 될 것이다. Counter 객체 내에서 이를 호출한다고 가정한다.

QObject::connect(this, SIGNAL(valueChanged(int)), this, SLOT(setValue(int)), Qt::QueuedConnection);
반응형

최초에 주어진 함수는 다음과 같다.

int32_t add2(int32_t x, int32_t y) {
    if(x + y > INT32_MAX) {
        throw std::invalid_argument("Overflow!!");
    } else {
        return (x + y);
    }
}

이에 대한 개선된 함수는 다음과 같다고 한다. 그러나 여전히 문제가 있다.

int32_t add2(int32_t x, int32_t y) {
    if(x + y > INT32_MAX || x + y < INT32_MIN) {
        throw std::invalid_argument("Overflow!!");
    } else {
        return (x + y);
    }
}

위 코드에서 문제가 되는 점

x + y가 int32_t 데이터 타입이 허용하는 범위를 벗어나면 이미 오버플로우가 발생한다. 예를 들어 x가 2,147,483,647, y가 1이면 x + y은 이미 오버플로우가 발생하여 -2,147,483,648이 되어있기 때문에 위의 로직으로는 걸러지지가 않는다.

따라서

  • x, y의 합이 오버플로우가 발생하는 로직을 개선한 함수를 구현하고,
  • 추가적으로 x, y의 차에 대한 함수도 구현하기.

1. 최초에 제출한 코드

#include <iostream>
#include <cstdint>

using namespace std;

int32_t add(int32_t, int32_t);
int32_t sub(int32_t, int32_t);


int main() {
    int32_t x = -2000000000;
    int32_t y = 2000000000;

    try {
        cout << sub(x, y) << endl;
    } catch(invalid_argument& ia) {
        cout << ia.what() << endl;
    }

    return 0;
}

int32_t add(int32_t x, int32_t y) {
    if(x >= 0) {
        if(y > (INT32_MAX - x)) {
            throw invalid_argument("Overflow: 두 수의 합이 허용하는 범위를 넘었습니다.");
        }
    } else {
        if(y < (INT32_MIN - x)) {
            throw invalid_argument("Overflow: 두 수의 합이 허용하는 범위보다 작습니다.");
        }
    }
    return x + y;
}

int32_t sub(int32_t x, int32_t y) {
    if(x >= 0) {
        if(y < (INT32_MAX - x)) {
            throw invalid_argument("Overflow: 두 수의 합이 허용하는 범위를 넘었습니다.");
        }
    } else {
        if(y > (INT32_MIN - x)) {
            throw invalid_argument("Overflow: 두 수의 합이 허용하는 범위보다 작습니다.");
        }
    }
    return x - y;
}

2. 그럼 위의 코드 실행은 정상적으로 동작하는가?

sub2 함수 실행시 문제가 있었음. 기본적으로 다음과 같은 값 갖고 테스트를 해봤을 때

x = 0, y = 2,147,483,646, 기대값 = -2,147,483,646
x = 0, y = 0, 기대값 = 0
x = 2, y = 0, 기대값 = 2

모든 경우에서 발생해서는 안되는 에러가 발생함.

x 값을 입력하세요 (허용범위: -2,147,483,648 ~ 2,147,483,647). 2
y 값을 입력하세요 (허용범위: -2,147,483,648 ~ 2,147,483,647). 0
입력받은 x값: 2
입력받은 y값: 0
x - y = Overflow: 두 수의 합이 허용하는 범위를 넘었습니다.

add2 함수를 구현한 후, 단순히 부호만 뒤집어주면 sub2 함수가 실행이 될 것이라고 생각을 했는데, 이 부분에 대해 아예 잘못 생각하고 테스트도 하지 않고 넘어가서 전혀 엉뚱하게 작동함. 따라서 새로 로직을 다음과 같이 구성함.

(1) 두 변수의 차를 구할 때 오버플로우가 발생할 가능성이 있는 경우

0도 양수로 포함

  1. x가 음수, y가 양수인 경우
  2. x가 양수, y가 음수인 경우

1번 경우에서 x - y를 하는 경우, 음수 오버플로우가 발생하여 연산 값이 양으로 나오게 되고, 2번 경우에서는 양수 오버플로우가 발생하여 음의 경우가 나오게 됩니다. 따라서 이 두 부분을 걸러내야 함.

(2) 위의 코드 자체의 문제점

쓸데없이 if문 안에 if문을 들여서 가독성을 떨어뜨리는 문제

두가지를 고려했을 때 수정한 코드는 다음과 같음.

int32_t sub2(int32_t x, int32_t y) {

    // 두 변수의 차를 계산하여 담을 변수
    int32_t subResult = x - y;

    // x가 음수, y가 양수인 경우에 subResult가 양수인 경우
    if(x < 0 && y > 0 && subResult > 0) {
        throw invalid_argument("negative overflow");
    } 

    // x가 양수, y가 음수인 경우에 subResult가 음수인 경우
    else if(x > 0 && y < 0 && subResult < 0) {
        throw invalid_argument("positive overflow");
    }

    return subResult;
}

한편, add2 함수 또한 가독성 측면에서 문제가 있어서 새로이 로직을 구성하기로 함.

0도 양수로 포함

  1. x가 양수인데 y도 양수인 경우 둘의 합이 음수이면 positive overflow
  2. x가 음수인데 y도 음수인 경우 둘의 합이 양수이면 negative overflow
int32_t add2(int32_t x, int32_t y) {
    // 두 변수의 합을 계산하여 담을 변수
    int32_t addResult = x + y;

    // x 및 y가 양수인데 그 합이 음수인 경우 positive overflow 예외처리
    if(x >= 0 && y >= 0 && addResult < 0) {
        throw invalid_argument("positive overflow");
    }

    // x 및 y가 음수인데 그 합이 양수인 경우 negative overflow 예외처리
    else if(x < 0 && y < 0 && addResult >= 0) {
        throw invalid_argument("negative overflow");
    }

    return addResult;
}

3. 재구성한 코드에 대한 테스트

위에서 재구성한 코드 전체 내용은 다음과 같음.

#include <iostream>
#include <cstdint>

using namespace std;

int32_t add2(int32_t, int32_t);
int32_t sub2(int32_t, int32_t);


int main() {
    // x, y의 입력값은 각각 정상범위 (-2,147,483,648 ~ 2,147,483,647)라고 가정.
    int32_t x;
    int32_t y;

    cout << "x 값을 입력하세요 (허용범위: -2,147,483,648 ~ 2,147,483,647). ";
    cin >> x;

    cout << "y 값을 입력하세요 (허용범위: -2,147,483,648 ~ 2,147,483,647). ";
    cin >> y; 

    try {
        cout << "입력받은 x값: " << x << endl;
        cout << "입력받은 y값: " << y << endl;
         cout << "x + y = " << add2(x, y) << endl;   
        cout << "x - y = " << sub2(x, y) << endl;
    } catch(invalid_argument& ia) {
        cout << ia.what() << endl;
    }

    return 0;
}

int32_t add2(int32_t x, int32_t y) {
    // 두 변수의 합을 계산하여 담을 변수
    int32_t addResult = x + y;

    // x 및 y가 양수인데 그 합이 음수인 경우 positive overflow 예외처리
    if(x >= 0 && y >= 0 && addResult < 0) {
        throw invalid_argument("positive overflow");
    }

    // x 및 y가 음수인데 그 합이 양수인 경우 negative overflow 예외처리
    else if(x < 0 && y < 0 && addResult >= 0) {
        throw invalid_argument("negative overflow");
    }

    return addResult;
}

int32_t sub2(int32_t x, int32_t y) {

    // 두 변수의 차를 계산하여 담을 변수
    int32_t subResult = x - y;

    // x가 음수, y가 양수인 경우에 subResult가 양수인 경우
    if(x < 0 && y >= 0 && subResult > 0) {
        throw invalid_argument("negative overflow");
    } 

    // x가 양수, y가 음수인 경우에 subResult가 음수인 경우
    else if(x >= 0 && y < 0 && subResult < 0) {
        throw invalid_argument("positive overflow");
    }

    return subResult;
}

다음 변수를 입력하여 각각 테스트를 진행한 결과

x y add2 연산 기대값 add2 실제 결과값 sub2 연산 기대값 sub2 실제 결과값
0 0 0 0 0 0
0 1 1 1 -1 -1
1 0 1 1 1 1
1 1 2 2 0 0
2,147,483,647 -2,147,483,648 -1 -1 positive overflow positive overflow
-2,147,483,648 2,147,483,647 -1 -1 negative overflow negative overflow
1 2,147,483,647 positive overflow positive overflow -2,147,483,646 -2,147,483,646
2,147,483,647 1 positive overflow positive overflow 2,147,483,646 2,147,483,616
0 -2,147,483,648 -2,147,483,648 -2,147,483,648 positive overflow positive overflow
-1 -2,147,483,648 negative overflow negative overflow 2,147,483,647 2,147,483,647
2,147,483,647 2,147,483,647 positive overflow positive overflow 0 0
-2,147,483,648 -2,147,483,648 negative overflow negative overflow 0 0

4. 수행 중 배운 점

언더플로우와 오버플로우 개념의 혼동

기존에는 특정 데이터타입의 최대값보다 큰 값이 주어진 것을 오버플로우, 최소값보다 작은 값이 주어진 것을 언더플로우라고 착각하고 있었다.

  • 오버플로우: 특정 데이터타입이 담을 수 있는 범위를 넘어선 값이 주어진 경우
  • 언더플로우: 부동소수점 연산시에 precision 허용 범위보다 더 적은 값을 표현하지 못하는 경우

5. 이어서 수행할 과제

예외처리를 하지 않고 처리하는 방법을 적용하여 코드 개선하기

반응형

다음과 같은 함수가 선언되어 있다.

이 함수 내부의 로직을 보면, 문제점이 하나 보인다.

if (x + y > INT32_MAX) 부분을 보면, 이 부분이 과연 정상적으로 실행될 수 있는 부분인지 먼저 확인해볼 필요가 있다.

예를 들어서 x의 값을 2000000000, y의 값도 마찬가지로 2000000000이라고 해보자. 이 둘을 합하면 어떤 결과가 나올지 다음 코드를 통해 확인해 보자.

#include <iostream>
#include <cstdint>

using namespace std;

int main() {
    int32_t x = 2000000000;
    int32_t y = 2000000000;

    cout << x + y << endl;
    return 0;
}

실행 결과:
-294967296

20억과 20억을 더했는데 전혀 엉뚱한 결과가 나왔다. 왜일까?

int32_t 데이터 타입에서 제공하는 값의 범위는 -2,147,483,648 ~ 2,147,483,647 이다. 여기서 최대값 + 1을 하게 되면 int32_t 타입의 변수 입장에서는 담을 수 있는 범위의 최대치를 넘겨버리게 된다. 이를 정수 오버플로우 (integer overflow)라고 한다.

'끝과 끝은 맞닿아 있다'라는 말처럼, 데이터 타입의 최소값과 최대값은 서로 맞닿아 있다. 그렇기 때문에 오버플로우가 발생한 경우 컴파일러는 증가분에 대한 값을 INT32_MAX의 다음 값인 최소 값으로 넘긴다. 따라서 -2,147,483,648을 리턴하게 된다.

만일 최대값 + 2를 하게 되면 위의 논리에 따라 -2,147,483,647을 리턴하게 된다.

이러한 문제는 역으로 최소값에 1을 빼려고 시도하는 경우에도 마찬가지로 나타난다.

#include <iostream>
#include <cstdint>

using namespace std;

int main() {
    int32_t x = INT32_MIN;
    int32_t y = -1;

    cout << x + y << endl;
    return 0;
}

위의 코드에서 출력되는 x + y의 값은 2,147,483,647가 된다. 최소값인 -2,147,483,648에서 1을 더 빼서 맞닿아 있는 최대값을 리턴하기 때문이다.

주의

이번 문제에 대해 찾아보던 중, 용어를 헷갈려 해서 다음과 같이 정리해 보고자 한다. 난 최대값의 범위를 넘어가서 계산하는 것을 오버플로우, 최소값보다 작은 값을 계산하는 것을 언더플로우라고 생각하고 있었는데 그동안에 용어에 대한 개념을 완전 잘못 알고 있었던 것이다. 두 용어를 다시 한번 더 정리하자면 다음과 같다.

  • 오버플로우: 특정 데이터 타입의 값 범위 밖의 연산이 일어나는 경우 (그게 최소값이던 최대값이던간에 관계없음)
  • 언더플로우: 나무위키에서 정리한 내용이 있어서 다음과 같이 인용해 본다 (https://namu.wiki/w/%EC%96%B8%EB%8D%94%ED%94%8C%EB%A1%9C)
    부동소수점 언더플로(floating point underflow) 또는 산술 언더플로(arithmetic underflow)는 컴퓨터에서 부동소수점으로 표현된 수에서, 지수(E)가 최솟값보다 낮아지는 상황을 뜻한다. 예를 들어 IEEE 754의 단정밀도 실수(single-precision)에서 표현가능한 지수의 값은 −126 ~ +127 이다. 그런데, 2-100 * 2-100 을 계산하면 2-200 이 되어야 하는데, 단정밀도에서는 지수의 값을 -126까지만 처리할 수 있으므로 범위를 벗어나는 값이 된다.

즉 언더플로우는 부동소수점의 정밀도와 관련된 개념으로, 표현이 가능한 지수 범위를 초과한 경우를 일컫는 말이다.

그렇다면 위의 함수를 개선하여 올바른 코드를 짜는 방법은?

그건 다음 시간에 이어서 올리겠다.

반응형

웹소켓은 하나의 TCP 연결에 전이중 통신 채널을 제공하는 통신 프로토콜이다.

여기서 말하는 전이중 통신이라는 단어는 '양방향 통신이 가능하다'는 뜻으로, 웹소켓 커넥션이 맺어진 클라이언트와 서버 간에는 하나의 커넥션을 통해 통신이 가능하다.

이는 HTTP와 가장 큰 차이점을 보이는 부분으로, HTTP 요청의 경우 보낼 때마다 새로 커넥션을 맺고 응답을 받은 후 커넥션이 종료가 되는데 비해 웹소켓의 경우에는 아래 이미지에서 보듯, 최초의 handshake 이후에 한쪽에서 커넥션을 종료시키지 않는 한 커넥션이 유지가 된다.

최초의 커넥션이 성공적으로 맺어졌다는 것을 어떻게 확인할 수 있나? handshake 성공시 101 응답을 받게 되는데, 이 101 코드의 의미는 '프로토콜 전환'이 성공적으로 이루어졌음을 말하는 것이다.

반응형

'Web & Network' 카테고리의 다른 글

NAT (Network Address Translation, 네트워크 주소 변환)  (0) 2018.12.19

여기서 말하는 복사의 대상은 바로 '객체'다.

객체를 복사하는데 있어 객체의 '무엇'을 복사하느냐가 깊은 복사와 얕은 복사를 나누는 기준이 되는데 그 대상은 각각 다음과 같다.

Deep Copy Shallow Copy
객체 자체를 복제 (clone) 하여 새 객체 생성 객체의 주소값만 복사

그러면 코드 상에서는 각각 어떻게 다른 결과물을 만들어 내는지 한번 보기로 한다.

우선, Car라고 하는 클래스를 하나 정의하고, 다음과 같이 구현해 보자.


public class Car {
    private String name;
    private int price;
    private String color;

    public Car() {

    }

    public Car(String name) {
        this.name = name;
    }

    public Car(String name, int price) {
        this.name = name;
        this.price = price;
    }

    public Car(String name, String color) {
        this.name = name;
        this.color = color;
    }

    public Car(String name, int price, String color) {
        this.name = name;
        this.price = price;
        this.color = color;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }
}

그리고 메인 함수에서 이 클래스의 인스턴스를 하나 생성한다.

Car c1 = new Car("Lamborghini", 1000000000, "Yellow");

Shallow copy (얕은 복사)

얕은 복사를 하려면 새 인스턴스를 생성하고, 그 인스턴스의 값으로 위에 생성한 c1 인스턴스를 할당해 주면 된다.

Car c2 = c1;

여기서 c2 인스턴스의 이름을 Lamborghini에서 Maserati로 바꿔 보고, 그 이후 결과값을 한번 조회해 본다.

c2.setName("Maserati");
System.out.println(c1.getName());
System.out.println(c2.getName());

// 두 인스턴스의 해시코드 값도 동일하게 출력되어 나온다. 즉 가리키는 주소값이 같은 객체라는 뜻.
System.out.println(c1.hashCode());
System.out.println(c2.hashCode());

실행해 보니 c2 인스턴스의 이름이 바뀌긴 했는데, c1 인스턴스의 이름까지 바뀌는 현상이 발생했다. 왜일까?

그것은 바로, 맨 위에서 언급한 것처럼 이 경우에는 c1 객체의 주소값을 복사하여 c2 인스턴스를 만들었기 때문이다. 즉 두 인스턴스는 힙메모리 상에서 같은 메모리 주소를 참조하고 있으며, 따라서 그 메모리 주소가 갖는 값이 변경되면 그 영향이 c1, c2 둘에게 다 간다.

그렇다면 복사한 인스턴스의 값이 변경되어도 기존 인스턴스에는 영향이 없게 하려면 어떻게 해야 할까? 여기서 등장하는 개념이 '깊은 복사'다.

Deep copy (깊은 복사)

깊은 복사는 객체를 복사하되 이를 별개의 메모리 공간에 할당하는 것이다. 자바에서는 이를 clone (복제)한다고 표현하는데, 이를 위해서는 깊은 복사의 대상이 clone이 가능하도록 Cloneable 인터페이스를 구현하고 있어야 한다. 따라서 위에서 선언한 Car 클래스를 다음과 같이 수정해 본다.

public class Car implements Cloneable {
    ...

    @Override
    protected Car clone() throws CloneNotSupportedException {
        return (Car) super.clone();
    }
}

새로 추가된 메소드는, Cloneable 인터페이스가 갖고 있는 clone() 메소드를 오버라이딩 한 것으로, Car 타입의 인스턴스를 복제하여 리턴한다는 내용이다. 이 메소드를 호출하려면 다음과 같이 하면 된다.

// deep copy
try {
    Car c3 = c1.clone();
    c3.setName("BMW");

    // c1과 c3의 이름이 이번에는 각각 다르게 출력된다. 즉 한쪽의 변화가 다른 한쪽에 영향을 주지 않는다.
    System.out.println(c1.getName());
    System.out.println(c3.getName());

    // 두 해시코드의 값이 다르게 나온다. 즉 두 인스턴스는 각기 다른 주소값을 갖는다.
    System.out.println(c1.hashCode());
    System.out.println(c3.hashCode());
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}

clone() 메소드가 CloneNotSupportedException을 던지기 때문에, 이 메소드 실행은 try-catch 구문 내에서 한다.

실행해 보면 한쪽 인스턴스의 값 변화가 다른 한쪽 인스턴스의 값에 영향을 주지 않고 있으며, 각 인스턴스가 가리키고 있는 주소값이 다른 것을 알 수 있다.

반응형

데코레이터는 함수를 받아 명령을 추가한 뒤 이를 다시 함수의 형태로 반환하는 함수이다.

대략적인 구조는 다음과 같다.

def outer_function(func):  # 기능을 추가할 함수를 인자로 받고,

    def inner_function(*args, **kwargs):  # 여기서 func 함수에서 받을 인자를 정의.
        return func(*args, **kwargs)

    return inner_func

그럼 데코레이터는 언제 쓰게 될까?

  1. 함수마다 공통적으로 호출되는 기능들을 하나로 묶을 때
  2. 함수의 내부를 수정하지 않고 변화를 주고자 할 때
  3. 특정 함수를 실행하는데 있어서 전처리 혹은 후처리가 필요한 경우

예를 들어, 다음과 같이 함수의 실행 속도를 측정하는 기능을 데코레이터로 만들어서 붙일 수 있다.

import time


def measure_performance(func):
    def wrapper(*args, **kwargs):  
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        elapsed = end - start
        print('Elapsed: {}'.format(elapsed))
        return result  # 데코레이터를 사용하는 함수의 결과값을 리턴
    return wrapper

이 함수를 데코레이터로 이용한 피보나치 계산 함수를 다음과 같이 선언해 본다.

data = {0: 1, 1: 1}


def _fibonacci(n):
    if n in data:
        return data[n]

    data[n] = _fibonacci(n-1) + _fibonacci(n-2)
    return data[n]


@measure_performance
def fibonacci_wrapper(n):
    val = _fibonacci(n)
    return val

그리고, 위에서 선언한 fibonacci_wrapper 함수를 한번 실행해 보자.

answer = fibonacci_wrapper(900)
print(answer)

다음과 같이 실행 결과를 가져오는 것을 확인할 수 있다.

Elapsed: 0.00044655799865722656
88793027306605937532517516910637647045239090036365766884466525589158360259770006891772711976920559280382807770394537471560061517120086971996377683290300054868066659454250625417891167369401

이렇듯 어렵지 않게 데코레이터를 사용할 수 있는데, 데코레이터 사용시에 주의해야 할 점으로는 무엇이 있을까?

다음 시간에서 이를 정리해 보고자 한다.

반응형

'Python' 카테고리의 다른 글

CentOS 7에서 파이썬 3 설치  (0) 2019.04.24
파이썬에서 __init__ 함수는 어떤 역할을 하는가?  (0) 2019.04.19

+ Recent posts