언제까지 비주얼스튜디오만 쓸거야?

얼마전 넷플릭스에서 쌈 마이웨이를 정주행했다. 김지원의 소주 원샷송이 아직도 여운이 남았는지 “언제까지 어깨춤을 추게할거야” 비슷한 제목을 뽑았다. 비주얼스튜디오는 논쟁의 여지없이 개발에 있어 기본적인 도구이지만 윈도우가 아닌 환경에서는 사용할 수 없다. 반면에 닷넷코어는 실행환경이자 개발환경(SDK)으로써 윈도우, 리눅스, 맥에서 동작한다. 이 글에서는 개발자의 운영체제에 영향받지 않는 크로스플랫폼 개발환경을 구축하는 것에 대해 알아보겠다.

Visual Studio Code + Extensions

맥 또는 리눅스상에서 윈도우의 비주얼스튜디오를 대체할 몇가지 개발툴이 있다.

dev-tools

JetBrains의 Rider는 비주얼스튜디오보다 여러 면에서 앞서가는 IDE다. 개인적으로 리팩토링, 테스트 커버리지 측정 기능때문에 사용하는데 이것 말고도 성능 프로파일 기능등 고급 기능들이 많이 있다. 상용제품이라서 일년 단위로 사용료를 지불하는데 20만원 가량된다. 개인 라이센스라고 해도 회사 개발업무에 사용할 수 있는데 회사에서 라이센스 비용을 지원받지 않는다는 조건이 있다. 회사에서 팀 단위로 구입하는 다른 라이센스 모델이 있기 때문이다.

맥을 사용한다면 Visual Studore for Mac을 고려할 것이다. 아쉽게도 Visual Studore for Mac은 자마린 스튜디오가 그 이름만 바꾸고 아주 기본적인 개발 기능만 추가한 것이라, 윈도우의 비주얼스튜디오 기능에 훨씬 못미치는 제품이다.

OmniSharp는 ATOM, Subline Text 같은 텍스트 편집기에 익숙한 개발자에게 좋은 옵션으로 보인다. VS Code를 사용하는 경우에도 C# for Visual Studio Code 라는 이름으로 익스텐션을 제공한다.

윈도우가 아닌 다른 운영체제에서의 현실적인 대안은 Visual Studio Code 이다. 무료이고 익스텐션 생태계가 풍부해서 마이크로소프트가 놀랄 정도로 시장에 잘 받아들여진 제품이다. 꾸준히 업데이트되고 익스텐션 또한 지속적으로 증가하고 있다.

VS Code Download에서 다운로드하여 설치한 후, 닷넷 개발에 꼭 필요한 익스텐션을 설치해보자.

| Extension | 설명 | | C# for Visual Studio Code (powered by OmniSharp)
ms-vscode.csharp | C# 편집시 문법 하이라이트, 인텔리센스, 정의 보기, 사용된 곳 찾기와 디버깅을 지원한다. | | C# Extensions
jchannon.csharpextensions | C# 클래스, 인터페이스 추가 / 생성자로부터 필드 및 속성 추가 / 속성들을 정의한 후 생성자를 통한 초기화. 코딩시 가장 많이 사용하는 리팩토링을 지원한다. | | REST Client
humao.rest-client | HTTP 요청을 보내고 응답을 확인할 수 있다. | | ILSpy .NET Decompiler | MSIL 어셈블리를 디컴파일한다. 예를 들어, int 에서 F12를 눌러 닷넷 타입 System.Int32 가 어떻게 정의되어 있는지 볼 수 있다. |

API 프로젝트 만들기

VS Code는 텍스트 에디터다. 솔루션, 프로젝트 생성 등 IDE에서 당연시했던 기능들이 없다. 대신, SDK가 제공하는 명령어를 실행해야 하는데 사실 비주얼스튜디오도 SDK가 제공하는 기능을 UI를 통해 실행하고 있다. 커맨드 명령에 익숙해져야 하고 터미널 창을 사용하는 시간이 늘어날테지만 더욱 개발자스러워지고 있다고 생각하자.

새로운 닷넷코어 API 프로젝트를 만들 것이다. VS Code를 실행하고 열기 메뉴에서 적당한 폴더로 이동한 후, 새로운 폴더를 원하는 이름으로 생성한 후 연다 (폴더명이 프로젝트 이름으로 사용된다). 보기 메뉴에서 터미널을 선택하여 (자주 사용하기 때문에 메뉴 옆에 있는 단축키를 기억) 터미널 창으로 들어가자. 아래 명령을 통해 새로 만들 수 있는 프로젝트 템플릿을 살펴보자.

dotnet new --help

모든 형태의 프로젝트가 열거된 것을 확인할 수 있다. 빈 솔루션을 만들고 프로젝트를 추가할 수도 있다. 여기서는 dotnet new webapi 명령으로 API 프로젝트를 만들고 단일 프로젝트만 사용한다. 명령을 실행하면 현재 폴더명을 사용해서 프로젝트 파일이 만들어진다. 아래에 이어지는 명령으로 빌드와 실행까지 해보자.

dotnet new webapi dotnet build dotnet run

브라우저를 열고 https://localhost:5001/weatherforecast 처럼 주소를 입력하면 API가 리턴하는 결과를 확인할 수 있다.

다시 VS Code 터미널창에서 Ctrl + C 를 눌러 웹서비스를 중단한다. 비주얼스튜디오 처럼 F5 키로 디버깅을 시작할 수 있고 Ctrl + F5 키로 디버깅없이 시작할 수도 있다.

개발용 로컬 데이터베이스

현재 API는 Weatherforcast 컨트롤러는 샘플로 갖고 있는데 정적으로 선언된 데이터를 사용하고 있다. 이 데이터를 로컬 데이터베이스에서 가져다 보여주도록 바꿔보자. 가장 먼저 필요한건 데이터베이스 설치다.

SQL Server Express LocalDB 같은 럭셔리 옵션은 윈도우에서만 가능하다. 이런 제약사항에 얽매이기보다 도커를 사용해서 원하는 데이터베이스를 운영하는 것도 개발자의 큰 즐거움이다. 여기서는 오픈소스 데이터베이스인 PostgreSQL을 사용한다.

자신의 운영체제에 맞는 도커 데스크탑을 설치한다. 그리고, 도커 허브에서 database 키워드로 검색해 보자. 대부분의 데이터베이스가 이미지로 존재한다는 것을 확인할 수 있다. 누겟 패키지를 다운로드해서 사용하는 것 만큼이나 도커 이미지를 다운로드해서 사용하는 것도 쉽다.

docker-hub-databases

검색 결과에서 Postgres를 선택하면 제품에 대한 개략적인 내용과 사용하는 방법을 확인할 수 있다. 도커 데스크탑을 설치했다면 아래 한줄 명령으로 Postgres 데이터베이스를 실행할 수 있다.

docker run --name postgres-docker -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres

위 명령을 통해,

  • 도커 허브에서 postgres 이미지를 다운로드한다 (로컬에 캐시된 이미지가 있으면 재사용).
  • 도커 컨테이너의 이름(--name)을 postgres-docker라고 지정한다.
  • 컨네이너 외부와 내부의 포트(-p)를 5432로 매핑한다.
  • 환경변수(-e)로서 데이터베이스 접속에 필요한 비밀번호를 지정한다.
  • 도커 컨테이너를 백그라운드(-d)에서 실행하고 컨테이너 ID를 출력한다.

docker run 명령은 이미지를 지정해서 컨테이너를 생성하고 실행까지 하는 함축적인 명령이다. 위 명령어는 두번 실행할 수 없다. 한번 더 실행하면 다음과 같은 에러를 만나게 될 것이다.

You have to remove (or rename) that container to be able to reuse that name.

컨테이너가 생성되어 이미 존재하는데 같은 이름으로 다시 만들려고 하기 때문에 생기는 문제다. 컨테이너가 생성된 이후에는 docker container <command> 명령어로 제어한다.

아래는 기본적으로 자주 쓰게될 명령어들이다.

docker ps # 컨테이너 리스트 보기 docker ps -a # 비활성화된 컨테이너까지 모두(all) 보기 docker container stop <container id or name> # 컨테이너 실행중지 docker container start <container id or name> # 컨테이너 실행 docker container rm <container id or name> # 컨테이너 삭제

Postgres 데이터베이스가 실행되었다고 했지만 그 실체는 아직 보지 못했다. 아래 명령문을 통해 postgres-docker 컨테이너에 접속해서 터미널과 같은 bash 애플리케이션을 실행할 수 있다.

docker exec -it postgres-docker bash

Bash 모드에서 psql -U postres 를 타입하면 사용자 postgres로 데이터베이스에 접속할 수 있다.

root@5d320df909e3:/# psql -U postgres psql (12.2 (Debian 12.2-2.pgdg100+1)) Type "help" for help. postgres=#

명령줄이 postgres=# 로 바뀌면서 데이터베이스에 접속했다. CREATE TABLE 같은 문장을 직접 실행할 수 있지만 접속 테스트는 여기까지 하고 \q를 입력하여 빠져나온다. exit 타입하여 bash 터미널도 빠져 나온다.

테이블을 관리하거나 데이터를 보기에 터미널은 불편하다. 대부분의 데이터베이스에 접속할 수 있는 DBeaver를 설치해 보자. 다음에 열거한 접속정보를 사용하여 Postgres에 연결할 수 있다.

postgres-docker

설치 후, 다음 정보를 이용해서 데이터베이스에 연결한다.

  • Host : localhost
  • Port : 5432
  • Username : postgres
  • Password : postgres (docker run 명령에서 지정한 비밀번호)

그림과 같이 접속에 성공했다. 기본 데이터베이스로 postgres 라는 데이터베이스가 보인다. 이왕이면 프로젝트에 맞게 데이터베이스를 생성하고 테스트용 데이터까지 포함된 이미지라면 더욱 좋을 것이다.

도커 이미지 만들기

이전 연습을 통해 postgres라는 기존의 도커 이미지를 사용하여 컨테이너를 올리고 데이터베이스를 운영하는 것이 명령어 한줄로 된다는 사실을 알았다. 그 단순한 과정을 강조하기 위해 실무에 필요한 요소들을 다루지 않았다. 이제 좀더 쓸모있는 데이터베이스가 되기 위해 몇가지를 추가하여 이미지를 만들 것이다.

VS Code에서 열기 메뉴를 통해 새로운 폴더를 만들어 연다. init.sql 이름의 파일을 추가하고 다음의 내용으로 저장한다.

create table status (description varchar(255)); insert into status values('Freezing'); insert into status values('Bracing'); insert into status values('Chilly'); insert into status values('Cool'); insert into status values('Mild'); insert into status values('Warm'); insert into status values('Balmy'); insert into status values('Hot'); insert into status values('Sweltering'); insert into status values('Scorching');

데이터베이스에 새 테이블을 만들고 데이터를 입력하는 seeding 작업이다.

위 파일과 같은 위치에 Dockerfile라는 이름으로 확장자 없이 파일을 추가한 후, 아래와 같이 저장한다.

FROM postgres ENV POSTGRES_PASSWORD mysecretpassword ENV POSTGRES_DB weather COPY init.sql /docker-entrypoint-initdb.d/

Dockerfile 은 도커 이미지를 만들기 위한 설명서라고 할 수 있으며 다음과 같이 해석된다.

  • postgres 이미지를 사용하는데
  • mysecretpassword를 비밀번호로 지정하고
  • weather 라는 데이터베이스를 생성한다.
  • 그리고, init.sql 파일을 도커 이미지 내의 /docker-entrypoint-initdb.d/ 폴더로 복사한다. 이곳에 위치한 파일은 컨테이너 시작시 자동으로 실행된다.

단 네 줄로 도커 이미지에 필요한 내용을 설명하고 있다. VS Code의 터미널 창에서 다음 명령으로 도커 이미지를 생성한다.

docker build -t postgres-weather .

아래 스냅샷에서는 이미지가 스텝별로 생성되는 과정을 보여준다. 또한, docker images -a 명령으로 이미지가 생성되었는지 재확인하고 있다.

docker-image-created

도커 컨테이너 만들기

로컬 머신에 이미지가 존재하기 때문에 이를 이용해서 컨테이너를 만드는건 아주 쉬운 일이다.

docker run --name postgres-docker -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres # 수정하면, docker run --name postgres-weather -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres-weather

최초로 사용했던 위 명령어를 응용하면 될 것이다. 컨테이너 이름(postgres-docker)이 겹치지 않게 새롭게 지정하고, 소스 이미지 이름 postgrespostgres-weather로 바꾸면 될 것이다.

여기서는 한걸은 더 나아가 앞서 작성한 이미지를 도커 허브에 게시하고 팀원들이 사용할 수 있는 방법을 설명할 것이다.

도커 허브에 이미지 게시

jakecompany-dockerhub

누겟 패키지를 다운로드하는 패키지 리포지토리가 있는 것처럼 도커 이미지를 등록하고 다운로드 할 수 있는 장소가 도커 허브다.

도커 허브에 계정을 만들면 공개 리포지토리를 생성할 수 있다 (비공개 리포지토리는 유료다). 개인 계정에 이미지를 게시하는 것도 괜찮지만 팀에 공유할 것이니 Organization을 추가하자. Organization 이름은 고유해야하고 개인 계정에 추가하는데 제약이 없다. 당신이 회사를 대표해서 도커 허브에 orginization을 등록하는 첫번째 기회를 놓치지 말자.

여기서는 jakecompany라는 organization을 추가했고 이 것은 별도의 네임스페이스로 인식된다. 도커 허브를 사용하는 퀵가이드에 내용이 잘 정리돼있다.

이제 조직을 위한 공간(네임스페이스)이 준비되었으니 그 하위에 이미지를 배포할 리포지토리를 생성한다. 도커 허브의 첫 화면으로 돌아가서 네임스페이스를 방금 생성한 조직으로 변경한 후 Create Repository 버튼을 클릭한다.

dockerhub-namespace

postgres-weather라는 이름의 리포지토리를 생성한다. 이 과정은 생략할 수도 있는데 이미지를 게시할 때, 이미지 이름에 해당하는 리포지토리를 자동으로 생성해 주기 때문이다. 따라서, 이미지 이름을 jakecompany/pstgres-weather로 하면 그만이다.

postgres-weather-jakecompany

이미지를 업로드할 리포지토리가 준비되었으니 VS Code 터미널로 돌아가 아래 명령을 실행하자.

docker tag postgres-weather jakecompany/postgres-weather:v1 docker push jakecompany/postgres-weather:v1

jakecompany-repository

기존 이미지의 이름을 도커 허브의 네임스페이스(조직명)에 맞추고 버전으로 태깅했다.

그리고, docker push 명령으로 이미지를 게시한다. 도커 허브의 화면을 새로고침하면 업로드된 이미지를 볼 수 있다.

이미지를 찾을 수 없다면 네임스페이스를 올바르게 선택했는지 확인하자.

도커 컨테이너 운영

이제 팀원들에게 알릴 준비가 됐다. 먼저 도커 데스크탑을 설치하라고 안내한 후, 다음 명령어를 사용하게 한다.

docker run -d --name weather-dev -p 5555:5432 -v weather-dev-volume:/var/lib/postgresql/data jakecompany/postgres-weather:v1

위 명령을 통해,

  • 도커 허브에서 jakecompany/postgres-weather:v1 이미지를 다운로드한다.
  • 도커 컨테이너의 이름을 weather-dev 라고 지정하고,
  • 외부 포트를 5555로 설정했다. 데이터베이스 연결시 외부 포트를 사용해야 한다.
  • 마지막으로, 볼륨 파라미터(-v)를 사용해서 데이터베이스의 변경 내용을 잃지 않게 설정한다.

볼륨은 컨테이너와 별개로 호스트 머신에 설정하는 공간이다. 컨테이너는 무상태(stateless)로 유지하기 때문에 변경된 내용을 저장하려면 컨테이너 외부공간 즉, 볼륨을 사용한다. 컨테이너를 실수로 삭제하더라도 원본 이미지와 볼륨으로 이전 컨테이너를 복구할 수 있다.

도커 데스크탑 설치와 한줄 명령으로 팀원들은 새로운 데이터베이스를 사용할 수 있게 되었다. DBeaver로 연결해서 status 테이블에 seeding 데이터가 잘 입력되었는지 확인하자.

dbeaber

호스트 머신을 재시작하면 컨테이너는 자동으로 실행되지 않는다. 컨테이너는 이미 생성되었으므로 찾아서 실행시켜 줄 필요가 있다.

docker container start weather-dev 명령을 사용할 수 있지만 도커 데스크탑을 사용하면 편리하다. 태스크 바에 있는 도커 아이콘을 클릭해서 Dashboard 메뉴를 선택하면 아래와 같은 화면을 볼 수 있다. 컨테이너 정지, 시작, 삭제 및 CLI 모드로 접속할 수 있다.

docker-dashboard

API 수정

이제 데이터베이스가 준비되었으니 API의 Weather 컨트롤러를 수정해서 데이터베이스에 있는 내용을 출력하도록 해보자.

VS Code의 파일 편집 기능과 터미널 기능으로 도커관련 작업이 끝났다. 이제, API 프로젝트 폴더로 이동해야 한다. 만약, 두 개 이상의 폴더를 동시에 열고 싶다면 VS Code의 워크스페이스를 활용하자.

File 메뉴에서 Save Workspace As...를 선택하여 원하는 이름으로 저장한다. 다시, File 메뉴를 선택하고 Add Folder to Workspace...를 선택하여 API 프로젝트 폴더를 추가한다. 이제 두 개의 폴더를 동시에 볼 수 있게 되었다. 터미널에서 명령을 수행할 때 작업 폴더를 항상 확인하는 습관이 도움이 될것이다. 아예 새로운 터미널 창을 추가해서 폴더별로 터미널 창을 운영하는 것도 좋은 방법이다.

Entity Framework Core 설정

데이터베이스에 접근하기 위해 EF Core를 사용할 것이다. 대부분의 데이터베이스를 지원하는데 그에 맞는 프로바이더를 설치해야 한다. API 프로젝트에 해당하는 터미널에서 다음 명령으로 PostgreSQL 데이터베이스 프로바이더를 추가한다.

dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL

API 프로젝트 루트에 Model 폴더를 만들고 WeatherDbContext.cs 파일을 추가하고 아래와 같이 입력한다.

public class WeatherDbContext : DbContext { public WeatherDbContext(DbContextOptions<WeatherDbContext> options) : base(options) { } public DbSet<Status> Status { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { // IEntityTypeConfiguration<T>를 구현하는 클래스를 모두 구성한다 modelBuilder.ApplyConfigurationsFromAssembly(typeof(WeatherDbContext).Assembly); } } public class Status { public string Description { get; set; } } public class StatusEntityConfiguration : IEntityTypeConfiguration<Status> { public void Configure(EntityTypeBuilder<Status> builder) { builder.ToTable("status"); // case sensitive builder.HasNoKey(); // PK 가 없기 때문 builder.Property(p => p.Description).HasColumnName("description"); // case sensitive } }

편의상 필요한 모든 코드를 한 파일로 작성했다. 날씨 상태를 갖고 있는 Status 모델을 정의하고 EF Core가 인식할 수 있도록 설정한다. 도커 이미지를 생성하면서 init.sql 파일에서 테이블 이름과 컬럼명을 Camel Case로 한건 실수다. 데이터베이스에서 대소문자를 구별하기 때문에 모델의 naming convention과 다른 결과를 초래했다.

컨트롤러 수정

먼저 Startup.cs 파일에 데이터베이스 컨텍스트를 추가한다.

public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddDbContext<WeatherDbContext>(options => options.UseNpgsql("Host=localhost;Port=5555;Database=weather;Username=postgres;Password=mysecretpassword")); }

그리고, 컨트롤러는 다음과 같이 정리했다.

[ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { private readonly WeatherDbContext context; public WeatherForecastController(WeatherDbContext context) { this.context = context; } [HttpGet] public IEnumerable<WeatherForecast> Get() { var rng = new Random(); return context.Status.Select(s => new WeatherForecast{ Date = DateTime.Now.AddDays(rng.Next(0, 5)), TemperatureC = rng.Next(-20, 55), Summary = s.Description }).ToArray(); } }

WeatherForecast API를 호출하면 다음과 같은 결과를 볼 수 있다.

weatherforecast-response

VS Code를 사용하면서 몇가지 익스텐션이 제공하는 기능을 사용했다. 문제가 있거나 리팩토링 제안이 있는 곳에 노란전구가 표시된다. 필요한 네임스페이스를 추가하여 문제를 해결하거나, 생성자의 파라미터로부터 필드를 초기화하는 리팩토링 기능이 그 예다.

Visual Studio에서 사용하던 단축키를 시도해 보면 재밌는 기능을 찾아낼 수 있다. 예를 들어, Ctrl + Shift + B 로 빌드하려고 하면 커맨트 팔레트에서 build 태스크를 추천해 준다.

마무리

VS Code의 익스텐션을 활용하는 모습을 많이 담지 못했지만, 비주얼스튜디오에서 쓰던 방식을 시도해 보면서 새 툴에 대해 하나하나 알아가는 재미를 느꼈으면 한다. 원하는 기능은 추가 익스텐션을 설치해서 해결할 수 있으리라 믿는다.

이번에 이직하면서 많이 느낀거지만 Containerisation 기술을 많이 요구하는 추세다. 도커는 개념도 어렵지 않고 사용법도 쉬워서 적은 시간을 투자하더라도 제법 큰 효과를 얻을 수 있으니 자신의 경력에 추가하기를 바란다.

마지막으로 맥에서 닷넷코어를 개발하는 분들에게, 그리고 맥북을 사용하고 싶었지만 망설였던 분들에게 이 글이 도움이 되기를 바란다.

참고자료