일반적으로 lib는 C++에서, dll은 양쪽 모두에서 사용할 수 있는 라이브러리 산출물로 알려져 있습니다.
그동안 이를 당연하게 여기고 별 의문 없이 관습적으로, 사용하던 대로 사용하곤 했습니다.
이 글에선 lib와 dll이 무엇이고 어떤점이 다르길래 왜 C#에선 lib를 사용할 수 없는지 알아보고자 합니다.
.lib와 .dll 파일
.dll 파일과 .lib 파일은 모두 C++ 프로그래밍에서 사용되는 파일 형식입니다. 하지만 그들의 역할과 구성은 다릅니다.
.dll 파일은 실행 중에 다른 프로그램에 의해 호출되는 코드와 데이터의 모음입니다. 이는 동적 라이브러리라고 부르며 프로그램이 실행될 때에만 필요한 함수 및 데이터를 메모리에 로드합니다. 여러 프로그램에서 공유하여 사용할 수 있으며, 코드 및 데이터의 재사용을 용이하게 합니다. dll은 주로 라이브러리나 플러그인 형태로 사용되며, 함수와 데이터를 외부에 공개하기 위한 인터페이스 역할을 합니다.
.lib 파일은 프로그램이 컴파일될 때 링커(Linker)에 의해 사용되는 라이브러리 파일입니다. 정적 라이브러리라고 부르며 해당 파일에 포함된 코드와 데이터가 컴파일 시에 프로그램에 직접 포함됩니다. 프로그램을 실행할 때 .lib 파일이 필요하지 않으며, 해당 파일에 포함된 코드 및 데이터가 이미 실행 파일에 포함되어 있습니다. 주로 컴파일러에 의해 생성된 객체 파일(Object File)을 묶어 놓은 형태로 사용되며, 함수 및 데이터의 정의를 포함합니다.
주요한 차이점은 .dll 파일이 실행 중에 필요한 코드와 데이터를 제공하는 동적 라이브러리인 반면, .lib 파일은 컴파일 시에 링크되는 정적 라이브러리입니다. 또한 .dll 파일은 외부 프로그램에서 호출되고 공유될 수 있지만, .lib 파일은 컴파일 시에 해당 프로그램에 포함되어 실행된다는 점 입니다.
그래서 왜 C#에서는 lib 파일을 사용할 수 없을까?
.lib 파일은 C++에서 사용되는 라이브러리 파일로, 컴파일러에 의해 링크되어 프로그램에 포함됩니다. 이 파일은 주로 함수와 기호의 정의를 포함하고 있을 뿐 .lib 파일 자체는 실행 코드를 포함하고 있지 않습니다.
C++에서는 컴파일 시에 .lib 파일이 필요하며, 링크 과정에서 이 파일이 사용됩니다. 그러나 C#은 .NET Framework 또는 .NET Core와 같은 가상 머신 위에서 실행되는 고수준 언어로, 직접적으로 .lib 파일을 사용하는 것은 불가능합니다.
대신에, C#에서는 Platform Invocation Services(P/Invoke)를 통해 외부 DLL에 있는 함수들을 호출할 수 있습니다. 따라서 .lib 파일을 사용하는 대신 해당 .lib 파일로부터 생성된 DLL 파일을 사용하여 P/Invoke를 통해 함수를 호출해야 합니다.
길지만 결국 근본적으로는 언어 및 런타임 환경이 서로 다르기 때문입니다.
C# 코드는 CLR(Common Language Runtime)이라고 불리는 가상 머신에서 실행됩니다. 이는 메모리 관리, 예외 처리 및 스레드 관리와 같은 기능을 제공합니다. 반면에 C++ 코드는 운영체제의 네이티브 코드로 컴파일되어 실행됩니다.
따라서 C#에서는 .lib 파일에 포함된 네이티브 코드를 직접적으로 이해하고 호출할 수 없습니다. 대신에, C#에서는 P/Invoke를 사용하여 외부 DLL 파일에 포함된 함수들을 호출할 수 있습니다. 이렇게 함으로써 C#은 외부 C++ 코드와 상호작용할 수 있습니다.
일반적으로 객체 지향 프로그래밍 중 클래스에 코드를 구현하다 보면 어? 코드가 너무 많은데? 하는 순간이 찾아옵니다. 보통은 별개의 클래스로 나누어 작업을 진행합니다. 하지만 partial class를 사용하면 하나의 클래스를 여러 파일로 나누어 작업을 진행할 수 있습니다.
작업을 별도의 클래스로 분리하는 경우:
별도의 클래스로 분리하는 경우 단일 책임 원칙 (Single Responsibility Principle)을 지킬 수 있다는 장점이 있습니다.
클래스는 하나의 책임만 가져야 한다는 원칙에 따라, 작업을 별도의 클래스로 분리하면 각 클래스는 더 작은 하나의 책임만 지도록 할 수 있습니다..
또한 모듈화를 통한 재사용성 향상을 기대할 수 있습니다.
작업을 별도의 클래스로 분리하면 해당 작업을 다른 부분에서 재사용하기 용이합니다.
이로 인해 테스트 용이성이 향상됩니다.
분리된 클래스는 독립적으로 테스트하기 쉽기 때문에 단위 테스트가 용이합니다.
하지만 클래스 수 증가로 인한 복잡도 증가가 발생할 수 있습니다.
여러 작업을 별도의 클래스로 나누면 클래스 수가 증가하게 됩니다. 클래스가 많아지면 전체적인 코드의 이해와 유지보수가 어려워질 수 있습니다.
partial 클래스로 나누어 구현하는 경우:
partial 클래스를 사용하면 단일 클래스 내에서 논리적 구분이 가능해집니다.
partial 클래스를 사용하면 논리적으로 관련된 코드를 동일한 클래스 내에서 나누어 구현할 수 있습니다. 이로써 클래스의 응집성(cohesion)을 높일 수 있다는 장점을 갖게 됩니다.
또한 모듈화 시켜 코드를 작성함과 동시에 컴파일 시 통합되기 때문에 코드의 가독성을 높일 수 있습니다.
partial 클래스를 컴파일 시에 하나의 클래스로 통합되기 때문에 코드를 나누어 작성하더라도 최종적으로는 하나의 클래스로 구현됩니다. 따라서 코드의 가독성을 높일 수 있습니다.
결론
만약 프로젝트에 대한 미리 정의된 코딩 가이드가 있다면 그것을 따르는 것이 좋습니다.
일반적으로는 단일 책임 원칙을 준수하고 모듈화와 재사용성을 높일 수 있는 방향으로 작업을 분리하는 것이 좋습니다.
하지만 프로젝트의 구조나 특정한 상황에 따라서는 partial 클래스를 사용하여 클래스의 일부를 나누는 것이 효과적일 수 있습니다.
Small. Simple. Secure. Alpine Linux is a security-oriented, lightweight Linux distribution based on musl libc and busybox. - 가볍고 단순하며 안전합니다. 알파인 리눅스는 musl libc와 busybox를 기반으로 한 보안지향의 경량 리눅스 배포판입니다.
2. 가장 큰 이점은 컨테이너의 용량이 작아진다는 것입니다.
세탁과 같은 상황에서 줄어든다는 것은 나쁜 상황이겠지만 Docker의 세계에서는 Docker 이미지가 더 작아진다는 의미이므로 기대할만한 상황이 됩니다.
Alpine 3.6의 Dockerized 버전의 무게는 3.98MB입니다.
비교를 위해 Alpine을 다른 인기 Linux 배포판과 비교해 보자면 그 크기는 다음 표와 같습니다:
DISTRIBUTION
VERSION
SIZE
Debian
Jessie
123MB
CentOS
7
193MB
Fedora
25
231MB
Ubuntu
16.04
118MB
Alpine
3.6
3.98MB
크기 차이 좀 보세요. 알파인은 데비안보다 약 30배 작습니다.
Docker Hub는 수많은 풀을 처리합니다. API를 조사해 보면 이 글이 작성되는 시점(2017년도 기준)에 Debian에 35,555,107개의 pull이, Alpine은 135,136,475개의 pull이 있었음을 알 수 있습니다.
이러한 pull로 인해 모든 바이트가 전송될까요? 아마도 그렇지 않을 것입니다. 그러나 당신이 한 예상은 제가 한 것처럼 좋을 것입니다. 최소한 상황을 보는 관점은 되어줍니다.
S3를 통해 Debian과 Alpine을 최대 3,500만 번 전송하는 데 드는 예상 비용:
DISTRIBUTION
GIGS TRANSFERRED
S3 TRANSFER COST
Debian
4,373,278
$416,310.72
Alpine
141,509
$17,832.06
S3의 가격 계산기 비용으로 Debian과 Alpine을 최대 3,500만 번 전송하는 데 거의 $400,000 USD의 차이가 발생하게 됩니다..
아마도 여러분이 그 정도의 규모로 운영하고 있지 않을 것이라는 건 압니다. 그러나 모든 규모 수준에서 클라우드의 전송 비용에 있어서는 실질적인 절감 효과가 있습니다.
이미지 크기를 100MB 이상 줄일 수 있다는 것은 큰 일입니다.
많은 패키지가 설치된 실제 웹 애플리케이션에서 Alpine을 사용하면 최종 이미지 크기가 2배 또는 3배 정도 절약되므로 마이크로 벤치마크에만 유용한 것은 아닙니다. 최대 100MB 절약은 이미지에 무엇이 내장되어 있는지에 관계없이 고정되어 있는 이점입니다.
3. 알파인은 빠릅니다.
더 작은 Docker 이미지를 처리할 때 비용만이 유일한 장점은 아닙니다.
Docker 이미지를 다운로드하고 CURL을 설치하고 싶다고 가정해 보겠습니다. Debian과 Alpine의 경우 시간이 얼마나 걸릴까요?
여기서는 2가지 테스트를 수행하겠습니다.
첫 번째 테스트는 30MB 가정용 케이블 연결을 사용하여 기본 Docker 이미지를 시스템에 다운로드하고 CURL을 설치할 것이며 시스템에 이미지가 다운로드되어있지 않은 환경입니다.
두 번째 테스트는 CURL을 설치하기 전에 시스템에 이미 기본 Docker 이미지가 있다는 점을 제외하면 위와 동일합니다.
테스트 1의 결과:
## Debian time docker run --rm debian sh -c "apt-get update && apt-get install curl" real 0m 27.928s user 0m 0.019s sys 0m 0.077s
Debian을 모두 다운로드하고 apt-get 업데이트를 실행한 다음 CURL을 설치하는 데 실제 시간은 약 28초입니다.
## Alpine time docker run --rm alpine sh -c "apk update && apk add curl" real 0m 5.698s user 0m 0.008s sys 0m 0.037s
반면에 Alpine을 사용하면 약 5배 더 빠르게 완료되었습니다. 28초와 5초를 기다리는 것을 비교하는 건 가벼운 게 아닙니다. 상당한 시간입니다.
프로그래밍 테스트가 30초 또는 5초 안에 완료될 때까지 기다리는 것이 얼마나 짜증 나는 일인지 생각해 보십시오.
테스트 2의 결과:
## Debian time docker run --rm debian sh -c "apt-get update && apt-get install curl" real 0m 9.170s user 0m 0.000s sys 0m 0.031s
분명히 30MB 케이블이 병목 현상을 일으키고 있어 apt-get update를 실행하고 컬을 설치하는 데에는 9초 이상이 걸렸습니다.
##Alpine
time docker run --rm alpine sh -c "apk update && apk add curl" real 0m 3.040s user 0m 0.017s sys 0m 0.008s
반면 알파인은 단 3초 만에 완료했습니다. 약 3배 정도 개선된 셈입니다.
새 Docker 이미지를 새로운 서버로 다운로드할 때 Alpine에서 초기 풀링 속도가 상당히 빨라질 것으로 예상할 수 있습니다. 네트워크 속도가 느릴수록 그 차이는 더 커집니다.
자동 확장 기능이 있고 많은 서버를 가동하는 경우라면 이는 매우 큰 문제입니다. 이는 서버가 더 빠른 속도로 트래픽을 수용할 준비가 되었음을 의미합니다.
서버를 많이 가동하지 않으면 속도 이점이 크게 떨어지지만 데이터 전송 및 저장 비용은 여전히 100MB 이상 절약됩니다.
4. 알파인은 안전합니다.
크기가 훨씬 작은 또 다른 장점은 공격을 받는 표면적이 훨씬 적다는 것입니다.
시스템에 패키지와 라이브러리가 많지 않으면 잘못될 수 있는 일이 거의 없습니다.
몇 년 전에 "ShellShock"이라는 이름으로 공격해 해커가 피해를 입은 경우 컴퓨터에 대한 제어권을 얻을 수 있는 불쾌한 Bash 공격이 있었습니다.
알파인에는 Bash는 기본적으로 설치되지 않기 때문에 해당 공격에서 무사했습니다..
또한 대부분의 배포판은 기본적으로 수많은 서비스를 실행합니다. 최신 Debian 또는 Ubuntu 시스템의 ps aux 출력 길이는 1마일이나 됩니다. 이는 Docker가 아닌 설정에서는 합리적일 수 있지만 Dockerized 애플리케이션에는 기본적으로 시작되는 대부분의 작업이 필요하지 않을 가능성이 있습니다.
알파인은 훨씬 다른 접근 방식을 취합니다. 기본적으로 너무 많이 시작하지 않으며 필요한 것만 시작하기를 기대합니다. 이는 Dockerized 애플리케이션에 적합합니다.
작업에 가장 적합한 도구를 사용하세요
알파인은 보안 전반에 대해서도 강력한 입장을 취했습니다. 개발팀은 특정 패키지를 보다 안전한 버전으로 교체하는 것을 두려워하지 않습니다. 예를 들어 OpenSSL을 LibreSSL로 대체했습니다.
위에 링크된 페이지에서 이 인용문이 정말 마음에 듭니다.
While OpenSSL is trying to fix the broken code, libressl has simply removed it.
- OpenSSL이 손상된 코드를 수정하려고 시도하는 동안 libressl은 이를 제거했습니다.
제 생각엔 이것이 바로 알파인의 특징이라고 생각합니다. 작고 안전한 Linux 배포판이 되겠다는 약속을 실제로 실천하고 있습니다. 기본 Docker 이미지로 사용될 때 Docker와 함께 사용하기에 완벽한 콤보입니다.
위 페이지에서 DCMTK 3.6.7 - Source Code and Documentation (2022-04-28)에 해당하는 소스 코드를 다운로드합니다.
이후 동일 페이지 아래에 있는 DCMTK 3.6.7 - Support Libraries for Windows에서 필요한 라이브러리를 다운로드합니다.
이 글에선 VisualStudio 17 2022에서 "MD" 옵션으로 DCMTK를 빌드할 예정이므로 다음 파일을 다운로드합니다: "Pre-compiled libraries for Visual Studio 2022 (MSVC 17.0), 64 bit, with icu4c, with "MD" option"
2. CMake Configure & Generate
편하게 CMake gui를 열고 소스코드와 빌드폴더를 지정합니다.
이후 Configure 버튼을 클릭해 VisualStudio 17 2022와 x64를 선택한 뒤 구성을 진행합니다.
구성이 완료된 뒤 위와 같이 Advanced 체크박스를 클릭해 모든 옵션을 확인합니다.
다음과 같이 구성을 "MT"에서 "MD"로 변경합니다.
이와 함께 "DCMTK_OVERWRITE_WIN32_COMPILER_FLAGS"를 체크해제 합니다. 해당 옵션이 체크되어 있으면 기껏 바꾼 MD옵션이 무시됩니다.
이제 라이브러리를 연결합니다. 다음과 같이 필요한 라이브러리를 선택해 줍니다.
선택한 라이브러리가 위치한 경로를 지정합니다.
해당 경로는 앞서 받은 라이브러리를 압축 해제한 경로를 지정해 주면 됩니다.
저는 사진과 같이 dcmtk 소스 폴더 내에 external 폴더를 만든 뒤 그 안에 압축을 해제해 두었습니다.
여기까지 완료되면 이제 Generate 버튼을 클릭해 VisualStudio 17 2022를 위한 sln 파일을 생성합니다.
3. Solution builld.
CMake에서 Generate가 완료된 뒤 빌드 폴더로 이동하면 sln 파일이 생성된 걸 확인할 수 있습니다.
RabbitMQ에서 VirtualHost(vhost)란 하나의 RabbitMQ 브로커에서 여러 개의 메시지 도메인을 사용할 수 있게 해 주는 논리적 그룹을 의미합니다. 각각의 vhost는 Queue, Exchange, Binding, 권한에 있어서 완전히 분리된 메시징 환경을 가집니다.
vhost는 다른 애플리케이션 유저 또는 부서사이의 메지싱 트래픽을 독립화하고 분리하는 기능을 제공해 줍니다. 기본적으로 RabbitMQ는 브로커 최상단에 위치해 있는 “/”라는 이름의 하나의 vhost를 생성합니다. 하지만 추가적으로 vhost를 메시징 애플리케이션의 요구사항을 충족시키기 위해 필요에 따라 생성할 수 있습니다.
각각의 vhost는 유저나 애플리케이션이 queue로부터 메시지를 소비하거나 교환하거나 발행하는 등과 같은 특정 작업을 수행할 수 있는가를 결정하는 권한들을 갖습니다. 이는 vhost 안에서 메시징 자원에 대한 정교한 컨트롤을 가능하게 합니다.
정리하자면 vhost는 하나의 브로커에서 여러 메시징 도메인을 가능하게 하는 논리적 컨테이너이며 이들 간의 분리와 메시징 트래픽을 컨트롤을 관리 유지 합니다.
EF Core에서 Postgres 사용 시 발생하는 Cannot write DateTime with Kind=UTC to PostgreSQL type 'timestamp without time zone' 에러를 해결하는 방법에 대해 알아봅니다.
현상
public class SomeClass
{
[Column("created_on")]
public DateTime? created_on { get; set; }
}
위와 같이 nullable DateTime 값을 수정하고자 할 때 Cannot write DateTime with Kind=UTC to PostgreSQL type 'timestamp without time zone' 오류가 발생.
수정
직접 setter를 구현해 명시적으로 DateTime.Kind를 설정합니다.
public class SomeClass{
[Column("created_on")]
private DateTime? _created_on
public DateTime? created_on
{
get
{
return this._created_on;
}
set
{
this._created_on = DateTime.SpecifyKind(value, DateTimeKind.Utc);
}
}
}
컨테이너와 PostgreSQL을 함께 사용하는 경우 일반적으로 컨테이너별로 별도의 스키마를 사용하는 것보다 컨테이너별로 데이터베이스를 분리하는 것이 좋습니다.
각 컨테이너가 이상적으로 자체 데이터베이스 인스턴스를 포함하여 자체 독립 환경을 가져야 하기 때문입니다. 각 컨테이너에 대해 별도의 데이터베이스를 사용하면 데이터가 각 컨테이너 내에서 격리되고 안전하며 한 컨테이너의 데이터베이스에 대한 변경 사항이 다른 데이터베이스에 영향을 미치지 않도록 할 수 있습니다.
반면에 단일 데이터베이스 내에서 각 컨테이너에 대해 별도의 스키마를 사용하는 경우 두 컨테이너가 동일한 스키마에 액세스하거나 수정하려고 하면 데이터 유출 또는 충돌의 위험이 있습니다. 이로 인해 데이터 일관성 문제와 잠재적인 보안 취약성이 발생할 수 있습니다.
물론 밀접하게 관련되어 있고 데이터를 공유해야 하는 소수의 컨테이너가 있는 경우와 같이 별도의 스키마를 사용하는 것이 적합한 경우가 있을 수 있습니다. 이러한 경우 권한이 없는 사용자가 실수로 데이터를 유출하거나 수정하지 않도록 권한 및 액세스 제어를 신중하게 관리하는 것이 중요합니다.