Ash Framework 튜토리얼 따라하기 (3)

https://hexdocs.pm/ash/get-started.html 를 따라가며 Ash 프레임워크를 살펴 본다. 이전 글에 이어서 Ticket 리소스를 업데이트 하는 방법을 알아 본다.

Ticket 을 종료하는 기능이 필요하다. 티켓이 가지고 있는 status 를 :closed 로 바꾸면 된다. 이미 :closed status 인 경우 값을 변경하지 않고 적절한 에러 메시지를 안내한다.

Ticket 리소스에서 다음과 같이 선언을 변경한다.

# lib/helpdesk/support/ticket.ex

actions do
  ...
  update :close do
    accept []

    validate attribute_does_not_equal(:status, :closed) do
      message "Ticket is already closed"
    end

    change set_attribute(:status, :closed)
  end
end

액션의 형태는 update 이고 이름은 :close 이다. 파라미터가 필요하다면 accept 를 줄 수 있다. 이 예제는 티켓을 종료할 때 특별히 전달 받을 데이터가 없으므로 빈 리스트로 지정한다.

validate 는 값의 유효성을 검사하기 위해 사용한다. 그대로 읽어 보자면 :status attribute_does_not_equal :closed -> :status 속성은 :closed 가 아니어야 한다 로 읽을 수 있을 것 같다. 따라오는 do ~ end 는 이 조건에 해당하지 않으면(즉, invalidate 하다면) 처리할 내용이다. attribute_does_not_equal 은 Built-in function 이다. https://hexdocs.pm/ash/validations.html 페이지에 사용할 수 있는 validation built-in function 목록이 있다. custom validation function 도 만들 수 있다.

change 는 값을 변경할 때 사용한다. 예제 코드는 :status attribute 를 :closed 로 변경한다. validate 와 마찬가지로 Built-in function 들이 준비되어 있고 custom change function 도 만들 수 있다.

다음은 실행 결과이다.

% iex -S mix           

iex(1)> ticket = Helpdesk.Support.Ticket |> Ash.Changeset.for_create(:open, %{subject: "My mouse won't click!"}) |> Ash.create!()

%Helpdesk.Support.Ticket{
  id: "054e9700-03e4-4ec4-8b6b-7964a258e775",
  subject: "My mouse won't click!",
  status: :open,
  __meta__: #Ecto.Schema.Metadata<:built, "">
}

iex(2)> ticket = ticket |> Ash.Changeset.for_update(:close) |> Ash.update!()

%Helpdesk.Support.Ticket{
  id: "054e9700-03e4-4ec4-8b6b-7964a258e775",
  subject: "My mouse won't click!",
  status: :closed,
  __meta__: #Ecto.Schema.Metadata<:built, "">
}

iex(3)> ticket |> Ash.Changeset.for_update(:close) |> Ash.update!()

** (Ash.Error.Invalid) 
Invalid Error

* Invalid value provided for status: Ticket is already closed.

Value: :closed

update 액션이기 때문에 Ash.Changeset.for_update(:close) 로 호출했다. 이어지는 함수 호출도 create!() 가 아닌 Ash.update!() 이다. 이미 :closed 로 변경된 ticket 을 다시 :close 액션을 호출하는 경우 에러가 발생하는 것을 확인할 수 있다.

다음 글 에서는 리소스를 조회하는 방법에 대해 알아보겠다.

Ash Framework 튜토리얼 따라하기 (2)

https://hexdocs.pm/ash/get-started.html 를 따라가며 Ash 프레임워크를 살펴 본다. 이전 글에 이어서 Ticket 리소스를 개선한다.

Helpdesk.Support.Ticket |> Ash.Changeset.for_create(:create) |> Ash.create!() 를 통해 Ticket 데이터를 만들 수 는 있었지만 subject 를 입력할 수도 상태를 지정할 수 도 없었다. 보통 create 함수에 처음 세팅할 값을 전달하거나 create 함수 내부에서 기본 값을 설정하는 등 일정 로직이 필요하다.

먼저 Ticket 리소스의 attribute 들에 몇가지 속성을 추가하자.

# lib/helpdesk/support/ticket.ex

attributes do
  ...
  attribute :subject, :string do
    allow_nil? false

    public? true
  end

  attribute :status, :atom do
    constraints [one_of: [:open, :closed]]

    default :open

    allow_nil? false
  end
end

subject attribute에는 nil 을 허용하지 않는 속성이 추가되었고 public 속성이 이 true 가 되었다. 여기서 public/private 은 REST/GraphQL 등 외부 API로 노출되었을때에 대한 가시성을 의미한다. public 이면 나중에 API 로 접근할 경우 필드로 나타날 것이다. public? 으로 명시하지 않으면 기본적으로 모두 private 이다. 내부적 접근 (예를들어 subject = ticket.subject 같은) 과는 상관이 없다.

status attribute 가 추가되었고 자료형은 atom, 값에 대한 제약조건이 :open, :closed 중 한개로 제약 조건이 붙었다. 기본 값은 :open 이다.

attribute 를 보완했으니 create 액션도 보완할 차례다.

# lib/helpdesk/support/ticket.ex

actions do
  ...
  create :open do
    accept [:subject]
  end
end

create :create 에서 create :open 으로 변경되었다. create 액션의 이름이 :open 로 변경된 것이다. Ash.Changeset.for_create 호출 파라미터도 :open 으로 바뀌어야 한다. accept 은 값을 인자로 전달받을 attribute 를 지정한다. attribute 로 지정되어 있지 않은 이름을 atom 으로 작성하면 거절(컴파일 오류) 된다.

변경된 Ticket 리소스의 형태는 다음과 같다.

defmodule Helpdesk.Support.Ticket do
  use Ash.Resource, domain: Helpdesk.Support

  actions do
    defaults [:read]

    create :open do
      accept [:subject]
    end
  end

  attributes do
    uuid_primary_key :id

    attribute :subject, :string do
      allow_nil? false

      public? false
    end

    attribute :status, :atom do
      constraints one_of: [:open, :closed]

      default :open

      allow_nil? false
    end
  end
end

iex 를 재실행하거나 recompile() 해서 코드를 업데이트 하고 다음과 같이 값을 전달하여 Ticket을 만든다.

iex> recompile()

iex> Helpdesk.Support.Ticket
|> Ash.Changeset.for_create(:open, %{subject: "My mouse won't click!"})
|> Ash.create!()

%Helpdesk.Support.Ticket{
  id: "d29d5348-ff98-4cc2-adbd-bffe29cef64f",
  subject: "My mouse won't click!",
  status: :open,
  __meta__: #Ecto.Schema.Metadata<:built, "">
}

입력 값이 잘못되거나 액션 이름이 잘못되었다면 에러가 발생한다.

iex> ticket = Helpdesk.Support.Ticket|> Ash.Changeset.for_create(:open, %{subject1: "My mouse won't click!"})|> Ash.create!()
** (Ash.Error.Invalid) 
Bread Crumbs:
  > Error returned from: Helpdesk.Support.Ticket.open

Invalid Error

* No such input `subject1` for action Helpdesk.Support.Ticket.open

No such attribute on Helpdesk.Support.Ticket, or argument on Helpdesk.Support.Ticket.open

Did you mean:

* subject


Valid Inputs:

* subject
iex> ticket = Helpdesk.Support.Ticket|> Ash.Changeset.for_create(:open)|> Ash.create!()
** (Ash.Error.Invalid) 
Bread Crumbs:
  > Error returned from: Helpdesk.Support.Ticket.open

Invalid Error

* attribute subject is required

다음 글에서는 레코드의 변경과 검증에 대해 알아 보자.

Ash Framework 튜토리얼 따라하기 (1)

https://hexdocs.pm/ash/get-started.html 을 따라해 보며 Ash 프레임워크의 기본 기능을 배워 본다.

시나리오

간소화된 HelpDesk 시스템을 가정해 보자.

시스템에는 두 개의 리소스가 있다.

  • Helpdesk.Support.Ticket (티켓)
  • Helpdesk.Support.Representative (담당자)

리소스에는 다음과 같은 액션을 할 수 있다.

  • 티켓을 열 수 있다
  • 티켓을 닫을 수 있다
  • 담당자에게 티켓을 배정할 수 있다

Ash 를 사용하여 구현

새 프로젝트 생성

mix 프로젝트가 없다면 새로 만들 수 있다. Ash 만 사용하는 프로젝트도 만들 수 있으나 향후 JSON API 까지 고려해 Phoenix도 같이 설치 한다.

# install the archive
mix archive.install hex phx_new
mix archive.install hex igniter_new

# use the `--with` flag to generate the project with phx.new and add Ash
mix igniter.new helpdesk --install ash,ash_phoenix --with phx.new && cd helpdesk

igniter 는 Elixir 로 만들어진 코드 생성기 이다. 지금 처럼 프레임워크 초반 세팅이나 버전 업그레이드를 할 때 사용할 수 있다.

첫 번째 도메인

먼저 도메인 폴더와 파일을 만든다.

mkdir -p lib/helpdesk/support && touch $_/ticket.ex && touch lib/helpdesk/support.ex

다음과 같은 폴더 구조일 것이다. lib/helpdesk 내부에 support 도메인이 만들어졌다.

lib/helpdesk/support.ex 파일에 다음 코드를 작성한다.

# lib/helpdesk/support.ex

defmodule Helpdesk.Support do
  use Ash.Domain

  resources do
    resource Helpdesk.Support.Ticket
  end
end

Helpdesk.Support 는 Ash.Domain 이고 Helpdesk.Support.Ticket 이라는 Resource 를 가지는 것으로 선언한다.

defmodule Helpdesk.Support.Ticket do
  # 이것은 이 모듈을 리소스로 만듭니다
  use Ash.Resource, domain: Helpdesk.Support

  actions do
    # :read 액션의 기본 구현을 사용합니다
    defaults [:read]
    # 그리고 나중에 커스터마이징할 create 액션
    create :create
  end

  # 속성들은 리소스에 존재하는 간단한 데이터 조각들입니다
  attributes do
    # `:id`라는 자동 생성되는 UUID 기본 키를 추가합니다
    uuid_primary_key :id
    # `:subject`라는 문자열 타입 속성을 추가합니다
    attribute :subject, :string
  end
end

Helpdesk.Support.Ticket 은 Ash.Resource 이고 Helpdesk.Support 도메인에 속한다. 이 리소스를 다루는 몇가지 action 들을 선언할 수 있고 속성 데이터도 정의할 수 있다.

실행

% iex -S mix

iex(1)> Helpdesk.Support.Ticket|> Ash.Changeset.for_create(:create)|> Ash.create!()

Helpdesk.Support.Ticket 리소스를 한 개 만들어 내기 위해 먼저 Changeset 을 만들고 Changeset 을 가지고 Ash.create! 함수를 통해 레코드를 만든다. Ecto.Changeset의 역할을 Ash 에선 Ash.Changeset이 담당하는 것 같다.

warning: Domain Helpdesk.Support is not present in

    config :helpdesk, ash_domains: [].


To resolve this warning, do one of the following.

1. Add the domain to your configured domain modules. The following snippet can be used.

    config :helpdesk, ash_domains: [Helpdesk.Support]

2. Add the option `validate_config_inclusion?: false` to `use Ash.Domain`

3. Configure all domains not to warn, with `config :ash, :validate_domain_config_inclusion?, false`

warning 발생해도 결과는 얻어지지만 warning 을 없애기 위해 다음 코드를 config.exs 에 추가한다.

# in config/config.exs
import Config

config :helpdesk, :ash_domains, [Helpdesk.Support] # 추가
% iex -S mix

iex(1)> Helpdesk.Support.Ticket|> Ash.Changeset.for_create(:create)|> Ash.create!()

%Helpdesk.Support.Ticket{
  id: "7d3d0639-5201-4eb3-9532-a7c837c98eaa",
  subject: nil,
  __meta__: #Ecto.Schema.Metadata<:built, "">
}

도메인과 리소스를 정의하고 몇가지 선언만 추가해도 기본 뼈대가 되는 기능을 얻을 수 있다.

다음 글에서 계속…

여러 SDK 버전을 선택할 수 있는 mise

개발 환경을 갖출 때 필수 과정이 언어의 SDK 를 설치하는 것이다. 보통 환경 구축 시점의 최신 버전을 설치하거나 프로젝트에서 필요한 버전을 선택해 설치한다. 이 때 시스템에 각기 다른 버전의 SDK 를 충돌 없이 변경해 가며 사용하고 싶을 수 있다. 이럴 때 mise 같은 도구를 사용한다.

설치

# 직접 설치
curl https://mise.run | sh

# brew 로 설치
brew install mise

그 외 환경의 설치는 https://mise.jdx.dev/installing-mise.html 를 참고한다.

활성화

설치를 마쳤으면 mise exec 명령어를 통해 프로그램을 실행할 수 도 있지만 쉘에 자동으로 mise 를 활성화 하는 것이 더 편하다. 쉘이 뜰 때마다 activate 를 하도록 쉘 시동 스크립트에 활성화 명령어를 추가한다. 사용하는 쉘 별로 입력해야 할 명령어와 파일이 다르다.

# 직접 설치했다면
echo 'eval "$(~/.local/bin/mise activate zsh)"' >> ~/.zshrc

# brew 로 설치 했다면, 그리고 bash 쉘을 사용한다면
echo 'eval "$(mise activate bash)"' >> ~/.bashrc

실행

다음과 같은 시나리오를 예를 들어 보자.

  • elixir_latest: 현재 개발 환경. elixir 최신 버전. (현 시점 1.18.4)
  • elixir_beta: 앞으로 출시될 버전의 베타 버전. (현 시점 1.19.0-rc.0)

첫번째 개발 환경 부터 만든다.

% mkdir elixir_latest
% cd elixir_latest
% mise use erlang elixir

mise 2025.8.12 by @jdx – install ✓ installed                                    mise ~/elixir_latest/mise.toml tools: erlang@28.0.2, elixir@1.18.4-otp-28

% elixir -v

Erlang/OTP 28 [erts-16.0.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]

Elixir 1.18.4 (compiled with Erlang/OTP 28)

폴더를 만들고 mise use 명령어를 이용해 필요한 SDK 를 선택한다. 이 때 버전을 명시하지 않으면 latest 버전이 선택된다.

% cat mise.toml 

[tools]
elixir = "latest"
erlang = "latest"

mise use 명령어를 실행하면 mise.toml 파일이 만들어 지는데 이 파일이 mise 문맥을 결정하는 것으로 보인다.

또 다른 개발 환경을 만들자.

% mkdir elixir_beta
% cd elixir_beta
% mise use erlang elixir@1.19.0-rc.0

mise 2025.8.12 by @jdx – install ✓ installed                                    mise ~/elixir_beta/mise.toml tools: erlang@28.0.2, elixir@1.19.0-rc.0

% elixir -v

Erlang/OTP 28 [erts-16.0.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]

Elixir 1.19.0-rc.0 (2a9a4f2) (compiled with Erlang/OTP 26)

% cat mise.toml 

[tools]
elixir = "1.19.0-rc.0"
erlang = "latest"

이 폴더에서 실행되는 elixir 버전은 1.19.0-rc.0 버전이고 mise.toml 에 버전이 명시되어 있음을 확인할 수 있다.


mise 를 사용하면 개발 환경 및 SDK 버전 관리를 쉽게 할 수 있을 것 같다. 본문의 예는 매우 기초적인 사례지만 https://mise.jdx.dev/ 홈페이지를 살펴보면 개발 환경 별 환경변수 관리, lockfile 을 사용한 버전 고정 등 개발 환경 버저닝을 위한 다양한 기능을 제공하고 있으니 한번 살펴보면 좋을 것 같다.

다른 모듈의 기능을 흡수(?) 하는 use

Elixir로 만들어진 프레임워크를 가지고 개발을 할 때 다음과 같이 use 구문을 자주 보게 된다.

defmodule MyAppWeb.DemoLive do
  use Phoenix.LiveView

  ...
end

구문이 익숙하지 않더라도 뭔가 모듈에 해당 프레임워크 기능을 추가? 사용? 한다는 느낌이 든다. use 키워드가 어떤 기능을 하는지 알아 보자.

Elixir School 에서는 use 키워드를 다음과 같이 설명하고 있다.

use 매크로로 현재 모듈의 정의를 다른 모듈이 수정할 수 있게 합니다. 코드에서 use를 호출하면 실제로 제공된 모듈에 의해 정의된 __using__/1 콜백을 호출합니다. __using__/1 매크로의 결과는 모듈 정의의 일부가 됩니다.

사용하는 모듈 입장에서는 use 구문을 통해 외부 모듈이 만들어 놓은 코드(기능)를 내 모듈로 가져오는 효과가 있다. Elixir School 의 예제 코드를 살펴 보자.

defmodule Hello do
  defmacro __using__(_opts) do
    quote do
      def hello(name), do: "Hi, #{name}"
    end
  end
end

어떤 외부 모듈이 미리 만들어 매크로로 hello 라는 기능(함수)을 정의해 놨다.

defmodule Example do
  use Hello
end

Example 모듈에서 Hello 모듈이 만들어 놓은 기능을 사용할 수 있도록 use 구문을 사용했다.

iex> Example.hello("Sean")
"Hi, Sean"

Example 모듈에서 마치 본인 모듈의 기능인 것 처럼 함수를 호출할 수 있다.


use 키워드를 살펴보면서 자바/스프링에서 어노테이션이 자연스럽게 떠올랐다. 자바/스프링 개발 경험이 있다면 데이터 클래스에 @Data 를 붙이거나 컨트롤러에 @RestController 를 붙이는 예를 생각하면 이해가 쉬울 것 같다. 개발자가 프레임워크를 쓰는 이유는 “이미 잘 만들어진 걸 가져다 쓰자”인데, Elixir 에서는 use 키워드가 그걸 가능하게 해 준다.

Spring Boot 3.4 에서 추가되는 구조화된 로깅

최근 안내된 Spring Boot 3.4 버전에서 새로 추가되는 구조화된 로깅 지원에 대한 소개글 입니다. 기본적으로 Spring Blog의 소개 글(https://spring.io/blog/2024/08/23/structured-logging-in-spring-boot-3-4)의 흐름을 따르고 있고 중간 중간 저의 의견 및 실행 결과 등을 추가했습니다.

Spring Boot 3.4에서는 구조화된 로깅(Structured logging)을 기본적으로 지원합니다. Elastic Common Schema (ECS)와 Logstash 형식을 지원하며, 사용자 정의 형식으로 확장하는 것도 가능합니다. 먼저 구조화된 로깅에 대해 알아봅시다.


구조화된 로깅이란?

로그 출력이 잘 정의된 형식으로 작성되는 기법입니다. 이 형식은 기계가 읽기 쉽도록 작성되며, ELK 스택 같은 로그 관리 시스템에서 효율적으로 처리될 수 있습니다. 대표적으로 JSON 형식이 많이 사용됩니다.

기본적인 설정 방법

Spring 3.4 버전 에서는 다음과 같은 설정이 가능합니다.

logging.structured.format.console=ecs

프로그램을 시작하면 다음과 같이 JSON 으로 로그가 포맷팅 됩니다.

{"@timestamp":"2024-07-30T08:41:10.561295200Z","log.level":"INFO","process.pid":67455,"process.thread.name":"main","service.name":"structured-logging-demo","log.logger":"com.example.structured_logging_demo.StructuredLoggingDemoApplication","message":"Started StructuredLoggingDemoApplication in 0.329 seconds (process running for 0.486)","ecs.version":"8.11"}

설정 전, 후 로그 출력 비교

  • 설정 전
  • 설정 후

파일로 로깅

콘솔 화면은 사람이 읽을 수 있는 기존 구조로 설정하고, 기계가 읽을 수 있는 구조의 로깅은 다른 특정 파일에 작성하고 싶을 수 있습니다. 다음과 같은 설정으로 파일에 구조화된 로깅을 설정할 수 있습니다.

logging.structured.format.file=ecs 
logging.file.name=log.json

추가 필드를 추가

구조화된 로깅의 강력한 기능 중 하나는 개발자가 구조화된 방식으로 로그 이벤트에 정보를 추가할 수 있다는 것입니다. 예를 들어 모든 로그 이벤트에 사용자 아이디를 추가한 다음 나중에 해당 아이디를 필터링하여 특정 사용자가 무엇을 했는지 확인할 수 있습니다.

Elastic Common Schema와 Logstash 모두 JSON에 매핑된 진단 컨텍스트(Mapped Diagnostic Context)의 콘텐츠를 포함합니다. 실제로 작동하는지 확인하기 위해 자체 로그 메시지를 생성해 보겠습니다:

@Component 
class MyLogger implements CommandLineRunner { 
    private static final Logger LOGGER = LoggerFactory.getLogger(MyLogger.class); 

    @Override public void run(String... args) { 
        MDC.put("userId", "1"); 
        LOGGER.info("Hello structured logging!"); 
        MDC.remove("userId"); 
    } 
}

로그 메시지를 로깅하기 전에 이 코드는 MDC에 “userId”도 설정합니다. Spring Boot는 JSON에 “userId”를 자동으로 포함합니다:

{ ... ,"message":"Hello structured logging!","userId":"1" ... }

또한 우아한 로깅 API를 사용하여 MDC에 의존하지 않고도 추가 필드를 추가할 수 있습니다:

@Component 
class MyLogger implements CommandLineRunner { 
    private static final Logger LOGGER = LoggerFactory.getLogger(MyLogger.class); 

    @Override public void run(String... args) { 
        LOGGER.atInfo().setMessage("Hello structured logging!").addKeyValue("userId", "1").log();
    } 
}

Elastic Common Schema는 기본적으로 많은 필드 이름을 정의하고 있습니다. Spring Boot 에서는 서비스 이름, 서비스 버전, 서비스 환경 및 노드 이름에 대한 기본 지원 기능을 갖추고 있습니다. 이러한 필드에 대한 값을 설정하려면 application.properties에서 다음을 사용할 수 있습니다:

logging.structured.ecs.service.name=MyService
logging.structured.ecs.service.version=1
logging.structured.ecs.service.environment=Production
logging.structured.ecs.service.node-name=Primary

JSON 출력에서 이제 service.name, service.version, service.environment, service.node.name와 같은 필드가 나타납니다. 이를 통해 로깅 시스템에서 노드 이름, 서비스 버전 등을 기준으로 필터링할 수 있게 됩니다. 이 네가지 필드 이외의 다른 Elastic Common Schema 필드에 대한 값은 properties 에서 설정할 수 없습니다. 다른 필드에 대한 값을 설정하려면 후에 서술할 맞춤형 로그 형식을 통해 추가해야 할 것 같습니다.

로깅 예시

{"@timestamp":"2024-08-29T11:30:41.325556Z","service.name":"MyService","service.version":"1","service.environment":"Production","service.node.name":"Primary","log.logger":"com.example.structuredlogging.StructuredLoggingApplication",...,"ecs.version":"8.11"}

맞춤형 로그 형식

각자의 프로젝트의 맞춤화된 로그 포맷을 위해 다음과 같은 단계가 필요합니다.

  1. StructuredLogFormatter 인터페이스의 커스텀 구현을 작성하세요
class MyStructuredLoggingFormatter implements StructuredLogFormatter<ILoggingEvent> { 

    @Override public String format(ILoggingEvent event) { 
        return "time=" + event.getTimeStamp() + " level=" + event.getLevel() + " message=" + event.getMessage() + "\n"; 
    } 
}
  1. application.properties에서 해당 커스텀 구현을 참조하세요
logging.structured.format.console=com.example.structuredlogging.MyStructuredLoggingFormatter

Spring Boot 3.4에 새로 추가되는 JsonWriter 를 사용해 JSON 으로 출력할 수도 있습니다.

class MyStructuredLoggingFormatter implements StructuredLogFormatter<ILoggingEvent> {  
    private final JsonWriter<ILoggingEvent> writer = JsonWriter.<ILoggingEvent>of((members) -> {  
       members.add("time", (event) -> event.getInstant());  
       members.add("level", (event) -> event.getLevel());  
       members.add("thread", (event) -> event.getThreadName());  
       members.add("message", (event) -> event.getFormattedMessage());  
       members.add("application").usingMembers((application) -> {  
          application.add("name", "StructuredLoggingDemo");  
          application.add("version", "1.0.0-SNAPSHOT");  
       });  
       members.add("node").usingMembers((node) -> {  
          node.add("hostname", "node-1");  
          node.add("ip", "10.0.0.7");  
       });  
    }).withNewLineAtEnd();  

    @Override  
    public String format(ILoggingEvent event) {  
       return this.writer.writeToString(event);  
    }  
}

마무리

Spring Boot 3.4 에서 강화된 구조화된 로깅 지원 기능은 매우 편리하게 로그에 다양한 정보를 추가할 수 있도록 도와줍니다. 로그를 남길때 특정 로그가 작성되는 시점의 맥락을 로그에 담았으면 좋겠다는 생각을 많이 하는데 이럴 때 유용하게 사용할 수 있을 것 같습니다. 특히 코드로 맞춤형 로그 구조를 직접 만들 수 있는 기능은 개인정보 마스킹등의 로그 요구사항도 비교적 손쉽게 구현이 가능해 질 것 같아서 기대가 됩니다.

추가 자료 링크

Spring Boot 프로젝트에서 초간단 docker compose 사용하기

Spring Boot를 사용해서 서비스를 만들 때 데이터베이스 같은 외부 서비스 들을 보통 한 개 이상 사용하게 되는데요, 이 때 docker compose를 사용하면 개발 과정에서 외부 서비스 들을 쉽게 관리할 수 있습니다.

docker-compose.yml 파일에 사용할 외부 서비스들을 정의하고 프로젝트 시작 전에 “docker-compose up” 프로젝트 종료 후 “docker-compose down” 명령어를 실행하는 방식이었죠.

Spring Boot 3.1 버전 부터는 docker compose 지원이 강화되어 프로젝트의 docker compose 파일을 감지하고 프로젝트가 기동 될 때 up, 중지 될 때 down 명령을 자동으로 실행해 줍니다.

바로 살펴봅시다!


필요 사항

  • Spring Boot 3.1 이상
  • docker-compose 2.2.0 이상

사용 방법

services:
database:
image: 'postgres'
ports:
– '5432'
environment:
– 'POSTGRES_USER=admin'
– 'POSTGRES_DB=mydatabase'
– 'POSTGRES_PASSWORD=password'

위와 같은 docker-compose.yml 이 프로젝트에 있다고 가정합니다.

developmentOnly 'org.springframework.boot:spring-boot-docker-compose'

build.gradle 파일에 spring-boot-docker-compose 라이브러리를 추가합니다. 개발환경에만 활성화되도록 developmentOnly 를 적용시킵니다.

끝!

직접 실행해서 결과를 확인해보겠습니다.

실행 결과

Spring Boot 실행 로그 중 3번째 라인에서 “Using Docker Compose file …” 을 확인 할 수 있습니다. 이후 Container 를 띄우고 Healthy 상태까지 기다린 이후에 프로젝트가 시작 되는 것을 확인 할 수 있네요!

정말 간단하죠? 심지어 데이터베이스 연결 정보를 애플리케이션 프로퍼티에 지정할 필요도 없습니다. spring-boot-docker-compose 에서 docker-compose.yml 을 읽어서 연결 정보가 필요하다면 ConnectionDetails 을 자동으로 만들어 주기 때문입니다. (자동으로 연결해주는 서비스 목록)

추가 설정

만약 커넥션 주소에 옵션 값을 추가하려고 한다면 docker compose yml 파일에 다음과 같이 추가 할 수 있습니다.

services:
postgres:
image: 'postgres:15.3'
environment:
– 'POSTGRES_USER=myuser'
– 'POSTGRES_PASSWORD=secret'
– 'POSTGRES_DB=mydb'
ports:
– '5432:5432'
labels:
org.springframework.boot.jdbc.parameters: 'ssl=true&sslmode=require'

마치며

Spring Boot 3.1 부터 강화된 docker compose 지원은 로컬 개발 시 정말 손쉽게 docker compose 를 다룰 수 있도록 도와줍니다. 그동안 외부 서비스들을 별도로 띄워서 사용했다면 docker compose 를 한번 사용해 보시는 것도 좋을 것 같고 이미 docker compose를 사용하고 있었다면 Spring Boot 버전을 3.1 이상으로 올리는 것도 고려해 볼 만 할 것 같습니다. 이미 boot 버전이 3.1 이상이라면 이 기능은 안 쓸 이유가 없습니다!

참고

처음 배우는 엘릭서 프로그래밍 5장 연습문제 풀이

처음 배우는 엘릭서 프로그래밍 책에 포함된 연습문제에 대한 풀이를 공유합니다.

문제 5-2, 5-3 이고 조건문을 사용하지 않고 FizzBuzz를 구현하는 방법입니다.

# 5-2 인자 세 개를 받는 함수
fizz_buzz = fn
0, 0, _ -> "FizzBuzz"
0, _, _ -> "Fizz"
_, 0, _ -> "Buzz"
_, _, a -> a
end
# 5-3 FizzBuzz 함수
fizzbuzz = fn
n -> fizz_buzz.(rem(n, 3), rem(n, 5), n)
end
IO.puts fizzbuzz.(10)
IO.puts fizzbuzz.(11)
IO.puts fizzbuzz.(12)
IO.puts fizzbuzz.(13)
IO.puts fizzbuzz.(14)
IO.puts fizzbuzz.(15)
IO.puts fizzbuzz.(16)
view raw 5-FizzBuzz.exs hosted with ❤ by GitHub

처음 배우는 엘릭서 프로그래밍 2장 연습문제 풀이

처음 배우는 엘릭서 프로그래밍 책에 포함된 연습문제에 대한 풀이를 공유합니다.

# 처음 배우는 엘릭서 프로그래밍 연습문제
# 2-1 다음 중 매칭에 성공하는 식은 무엇일까?
a = [1, 2, 3] # 성공
a = 4 # 성공
4 = a # 성공. 단, a 가 4로 할당되어 있다면
[a, b] = [1, 2, 3] # 실패
a = [[1, 2, 3]] # 성공
[a] = [[1, 2, 3]] # 성공
[[a]] = [[1, 2, 3]] # 실패
# 2-2 다음 중 매칭에 성공하는 식은 무엇인가?
[a, b, a] = [1, 2, 3] # 실패
[a, b, a] = [1, 1, 2] # 실패
[a, b, a] = [1, 2, 1] # 성공. a = 1, b = 2
# 2-3 변수 a에 값 2가 바인딩 되어 있다면 다음 중 매칭에 성공하는 식은 무엇인가?
[a, b, a] = [1, 2, 3] # 실패
[a, b, a] = [1, 1, 2] # 실패
a = 1 # 1로 새로 바인딩 됨
^a = 2 # 성공
^a = 1 # 실패
^a = 2 - a # 실패
view raw 2.exs hosted with ❤ by GitHub

Phoenix Up and Running

Phoenix 는 Elixir 를 위한 웹 개발 프레임워크이다. 피닉스 가이드 문서를 따라서 간단하게 피닉스 웹 서버를 띄운다.

설치

Elixir 설치
brew install elixir

다른 플랫폼의 경우 https://elixir-lang.org/install.html 참조

Hex 설치

Elixir 설치가 처음이면 Hex 설치가 필요하다. Hex 는 Erlang 에코시스템의 패키지 매니저이다.

mix local.hex

mix 는 Elixir 의 빌드 도구이다.

Mix is a build tool that ships with Elixir that provides tasks for creating, compiling, testing your application, managing its dependencies and much more

Phoenix 생성기 설치

Phoenix 프로젝트를 생성하기 위해 다음의 명령 실행. Phoenix 생성기를 설치한다.

mix archive.install hex phx_new

프로젝트 생성

hello 라는 이름의 Phoenix 프로젝트를 만든다.

mix phx.new hello

데이터베이스를 사용하지 않을 것이라면 –no-ecto 옵션을 추가한다. Ecto 는 Elixir 를 위한 데이터베이스 래퍼와 쿼리 생성기이다.

Fetch and instll dependencies? 를 Y 로 넘어가면 다음과 같은 결과를 얻을 수 있다.

Fetch and install dependencies? [Yn] Y

* running mix deps.get
* running mix deps.compile

We are almost there! The following steps are missing:

&nbsp; &nbsp; $ cd hello

Then configure your database in config/dev.exs and run:

&nbsp; &nbsp; $ mix ecto.create

Start your Phoenix app with:

&nbsp; &nbsp; $ mix phx.server

You can also run your app inside IEx (Interactive Elixir) as:

&nbsp; &nbsp; $ iex -S mix phx.server

프로젝트 폴더로 이동 한 다음 ecto 설정을 실행한다.

cd hello
mix ecto.create

ecto 명령어 실행 중 에러가 발생하면 다음 문서를 참고한다.

postgresql 을 처음 설치했다면 role 이 없어서 오류가 발생한다. psql 에서 다음 명령어를 실행한다.

=# CREATE ROLE postgres LOGIN CREATEDB;

실행

모두 완료가 되었다면 Phoenix 서버를 실행한다.

mix phx.server
[info] Running HelloWeb.Endpoint with cowboy 2.9.0 at 127.0.0.1:4000 (http)
[info] Access HelloWeb.Endpoint at http://localhost:4000

위와 같은 로그를 확인했다면 127.0.0.1:4000 에서 첫 화면을 확인.

완료!