<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Tim</title>
    <link>https://majortim.tistory.com/</link>
    <description>개발자 Tim의 블로그</description>
    <language>ko</language>
    <pubDate>Sun, 2 Aug 2026 13:32:21 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Tim(팀)</managingEditor>
    <image>
      <title>Tim</title>
      <url>https://tistory1.daumcdn.net/tistory/3888495/attach/e176e66546a8455a8aed6f9e38f4ac8e</url>
      <link>https://majortim.tistory.com</link>
    </image>
    <item>
      <title>Oracle DATE 타입 컬럼 비교시 성능 저하 현상</title>
      <link>https://majortim.tistory.com/11</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 운영 DB에서 사용한 조회 쿼리 때문에 특정 시간에 DB 서버 CPU 사용률이 급격하게 올라간 일이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 얘기하면 DATE 타입 컬럼 값을 비교할 때 썼던 Literal이 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정일시 이후의 데이터를 조회하고 싶어서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DATE컬럼 &amp;gt;= TIMESTAMP '2023-08-08 12:00'&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 조건절을 넣었는데 이 부분이 문제가 됐던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 DATE컬럼 &amp;gt;= SYSDATE - 1/24 이런 식으로 비교하라고 안내를 받았는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 좋은 방법이 없을까..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더미 데이터를 생성해서 재현해봐야겠다.&lt;/p&gt;</description>
      <category>SQL</category>
      <author>Tim(팀)</author>
      <guid isPermaLink="true">https://majortim.tistory.com/11</guid>
      <comments>https://majortim.tistory.com/11#entry11comment</comments>
      <pubDate>Wed, 9 Aug 2023 01:19:37 +0900</pubDate>
    </item>
    <item>
      <title>Jenkins 설치, Agent 구성, Github 연동 문제 해결</title>
      <link>https://majortim.tistory.com/10</link>
      <description>&lt;h1&gt;Jenkins 설치, Agent 구성, Github 연동 문제 해결&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle Cloud(OS는 Oracle Linux)에서 Jenkins 설치, 구성 중 생긴 문제들과 문제 해결 과정을 기록한다. 설치한 Jenkins 버전은 2.387.3 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 sudo yum install jenkins 명령어로 Jenkins를 설치한다. 설치가 완료되고 sudo service jenkins start 명령어로 Jenkins 서비스를 시작하려고 하니 프로세스는 실행되는데 정상적인 상태가 아닌 거 같다. 실행 결과에 Failed 라는 문구와 함께 Job for jenkins.service failed because a timeout was exceeded 라는 메시지가 뜬다. journalctl -xe 명령어로 자세한 내용을 보려고 해도 timeout 부분 외에 의미 있는 내용을 찾지 못했다. 다른 네트워크 문제가 있는 게 아닌가 한참을 찾아 헤매다 알아낸 결론은 정말 단순 타임아웃 문제가 맞았다. /lib/systemd/system/jenkins.service 설정에서 TimeoutStartSec 설정의 숫자를 높이는 걸로 해결됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 해서 설치는 잘 끝났고 내가 원하는 작업들을 수행해줄 Agent를 설치한다. Agent는 Controller에 연결해서 실행시키는 방법과 SSH로 실행시키는 방법이 있다. 이전에 Docker와 SSH를 이용해 실행시키는 방법을 이용했는데 과정이 복잡해서 이번엔 Controller에 연결해서 실행시키는 방법을 해보기로 했다. 이 방법은 TCP를 이용하는 방법과 웹소켓을 이용하는 방법 중에서 하나를 택해야 한다. Node 설정에서 Use Websocket을 체크하지 않으면 TCP로 이용하게 되는데, 기본 설정에서 TCP가 Disable 되어있으면 에러 문구가 표시된다. Jenkins 관리 &amp;gt; Configure Global Security 설정에서 Agents의 TCP port for inbound agents 부분을 Random이나 Fixed로 바꾸면 된다. 지금 같은 경우는 클라우드 환경이기 때문에 Fixed로 설정했다. Node의 나머지 설정은 기본으로 두고 설정을 완료하면 Agent 실행 방법이 다음과 같이 나온다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;curl -sO &amp;lt;https://젠킨스호스트/jnlpJars/agent.jar&amp;gt;
java -jar agent.jar -jnlpUrl &amp;lt;https://호스트/manage/computer/permanent/jenkins-agent.jnlp&amp;gt; -secret 보안키 -workDir &quot;/작업경로&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나와있는대로 실행을 해보니 타임아웃 문제가 여기서도 발생했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;INFO: Connecting to &amp;lt;젠킨스호스트:포트&amp;gt; (retrying:2)
java.io.IOException: Failed to connect to &amp;lt;호스트명:포트&amp;gt;
        at org.jenkinsci.remoting.engine.JnlpAgentEndpoint.open(JnlpAgentEndpoin
t.java:243)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;firewall-cmd 로 내부 방화벽은 포트 개방을 했기 때문에 내부에서 차단하는 건 아니었다. Cloud에 등록된 보안그룹 규칙이 문제였는데, Agent 실행에 사용할 포트 관련 규칙을 등록할 때 소스 포트는 범위를 한정하지 않고 대상 포트만 범위를 한정해야 정상적으로 동작한다. 규칙 등록 후 다시 실행하니 정상적으로 연결되고 Node 상태창에서도 &amp;lsquo;Agent is connected.&amp;rsquo; 라는 메시지가 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Agent까지 설치 했으니 이젠 새로운 프로젝트를 생성한다. &lt;a href=&quot;https://xn--9t4ba423bv7jrtctuh/view/all/newJob&quot;&gt;https://젠킨스호스트/view/all/newJob&lt;/a&gt; 으로 들어가거나 Dashboard에서 새로운 Item 버튼을 클릭 후 새로운 project를 생성한다. Github Project 정보를 적고 소스 코드 관리 부분에서 Git Repository 정보를 입력한다. Credential을 등록한 게 없으면 Credentials 부분에 Add 버튼을 눌러서 그 창에서 바로 등록하면 된다. Credential Kind는 Secret File이나 Secret Key가 아니라 Username with password를 선택한다. username에는 Github 사용자 ID, 비밀번호는 GIthub의 &lt;a href=&quot;https://github.com/settings/tokens&quot;&gt;Personal Access Token&lt;/a&gt;을 입력하면 된다. Token의 scope는 보통 repo와 admin:repo_hook이 있으면 되는데 사용 환경에 따라 더 필요할 수도 있다. 생성한 Credential을 선택하면 Github 관련 설정은 끝난다. 이 때 Jenkins가 설치된 시스템에 Git이 설치가 안 돼있으면 에러가 난다. sudo yum install git으로 설치하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 Github 연동까지 완료된다. 추가 설정을 안 해서 수동 빌드만 가능하다. 프로젝트 페이지에서 지금 빌드를 누르면 Github에서 Repository의 최신 소스를 Jenkins 작업 폴더에 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 하는데 오래도 걸렸다. 배포 자동화 좀 해본다고 도메인 사고 거의 1년은 걸린 거 같다. 그 사이에 도메인 만료 시점이 다가와서 도메인 갱신도 했다. 스스로 너무 게을렀음을 느낀다. 첫 빌드를 성공했지만 빌드가 애플리케이션 빌드도 아니고 그냥 Repository에서 소스 가져온 거에 불과하다. 다음은 webhook 설정과 애플리케이션 빌드 설정을 해봐야겠다.&lt;/p&gt;</description>
      <category>CI&amp;frasl;CD</category>
      <author>Tim(팀)</author>
      <guid isPermaLink="true">https://majortim.tistory.com/10</guid>
      <comments>https://majortim.tistory.com/10#entry10comment</comments>
      <pubDate>Sun, 21 May 2023 21:47:40 +0900</pubDate>
    </item>
    <item>
      <title>BytesEncryptor와 TextEncryptor</title>
      <link>https://majortim.tistory.com/9</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security의 Crypto 모듈은 대칭키 암호화와 키 생성, 패스워드 인코딩 기능을 제공합니다. 그 중 대칭키 암호화에 관한 인터페이스인 &lt;code&gt;BytesEncryptor&lt;/code&gt;와 &lt;code&gt;TextEncryptor&lt;/code&gt;를 소개합니다.&lt;/p&gt;
&lt;h1&gt;BytesEncryptor&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BytesEncryptor&lt;/code&gt;를 사용하면 인코딩한 결과물의 형식이 byte[]가 됩니다. &lt;code&gt;org.springframework.security.crypto.encrypt.Encryptors&lt;/code&gt; 클래스에는 &lt;code&gt;BytesEncryptor&lt;/code&gt;구현체의 인스턴스를 생성하는 두 가지 메서드가 정의되어 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;static BytesEncryptor stronger(java.lang.CharSequence password, java.lang.CharSequence salt)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;static BytesEncryptor standard(java.lang.CharSequence password, java.lang.CharSequence salt)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 메서드의 차이는 &lt;a href=&quot;https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#cipher-algorithm-modes&quot;&gt;싸이퍼 알고리즘의 모드&lt;/a&gt;입니다. stronger 메서드는 GCM을 사용하며 standard는 CBC를 사용합니다. 인터넷에서 쉽게 찾을 수 있는 예제는 대부분 CBC를 사용하지만 스프링에서 권장하는 방식은 GCM을 사용하는 stronger 메서드입니다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;BytesEncryptor encryptor = Encryptors.stronger(&quot;This is password!&quot;, &quot;0123456f&quot;);
String rawText = &quot;This is text!&quot;;
byte[] cipherBytes = encryptor.encrypt(rawText.getBytes(StandardCharsets.UTF_8));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BytesEncryptor&lt;/code&gt;를 생성하는 팩토리 메서드입니다. 앞에 들어가는 password 패러미터는 암호키를 생성하기 위해 필요한 암호입니다. 뒤에는 salt인데 hex(16진수) 형식으로 인코딩해야 합니다. 만약 위에서 입력한 0123456f가 0123456g로 바뀌면 범위를 넘어가는 문자인 g 때문에 실행할 때 에러가 발생합니다. password와 salt를 올바르게 입력하면 이를 이용해 비밀키를 생성합니다.&lt;/p&gt;
&lt;h1&gt;TextEncryptor&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;TextEncryptor&lt;/code&gt;를 사용하면 인코딩한 결과물의 형식이 hex로 인코딩된 스트링이 됩니다. &lt;code&gt;Encryptors&lt;/code&gt; 클래스에는 &lt;code&gt;TextEncryptor&lt;/code&gt;구현체의 인스턴스를 생성하는 네 가지 메서드가 정의되어 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;static TextEncryptor delux​(java.lang.CharSequence password, java.lang.CharSequence salt)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;static TextEncryptor noOpText()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;del&gt;&lt;code&gt;static TextEncryptor queryableText​(java.lang.CharSequence password, java.lang.CharSequence salt)&lt;/code&gt;&lt;/del&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;static TextEncryptor text(java.lang.CharSequence password, java.lang.CharSequence salt)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;queryTableText(...) 메서드 같은 경우는 초기화 벡터(이하 iv)가 항상 일정하기 때문에(0 값이 16 바이트로 채워진) 동일한 문자열을 인코딩할 땐 몇 번을 수행해도 인코딩 결과가 똑같습니다. 안정성 문제로 deprecated 처리됐으니 사용을 자제하는 것이 좋습니다. noOpText() 메서드로 생성한 인스턴스는 암호화 자체를 하지 않기 때문에 활용도가 높지 않습니다. 남는 건 &lt;code&gt;BytesEncryptor&lt;/code&gt;인터페이스처럼 두 가지 메서드밖에 없습니다. delux(...) 메서드와 text(...) 메서드인데 delux는 stronger 메서드를, text는 standard 메서드를 내부적으로 호출합니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;TextEncryptor encryptor = Encryptors.delux(&quot;This is password!&quot;, &quot;0123456f&quot;);
String rawText = &quot;This is text!&quot;;
String cipherText = encryptor.encrypt(rawText);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;delux 메서드의 사용 예시입니다. 암호화 메서드의 패러미터 타입과 반환 값 타입이 byte[]가 아닌 String입니다.&lt;/p&gt;
&lt;h1&gt;BytesEncryptor의 구현&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 &lt;code&gt;BytesEncryptor&lt;/code&gt;를 어떤 식으로 구현했는지 &lt;b&gt;&lt;code&gt;Encryptors.stronger(java.lang.CharSequence,java.lang.CharSequence)&lt;/code&gt;&lt;/b&gt; 메서드의 소스를 분석 후 암호화 메서드와 복호화 메서드를 단순화시켜서 보여드리겠습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;String password = &quot;test1234&quot;;
String salt = &quot;01234567&quot;;

BytesKeyGenerator ivGenerator = KeyGenerators.secureRandom(16);
SecretKey secretKey = new SecretKeySpec(
        SecretKeyFactory.getInstance(&quot;PBKDF2WithHmacSHA1&quot;)
                .generateSecret(new PBEKeySpec(password.toCharArray(), Hex.decode(salt), 1024, 256))
                .getEncoded(), &quot;AES&quot;);

Cipher encryptor = Cipher.getInstance(AesBytesEncryptor.CipherAlgorithm.GCM.toString());
Cipher decryptor = Cipher.getInstance(AesBytesEncryptor.CipherAlgorithm.GCM.toString());

String rawText = &quot;테스트 문자열&quot;;

byte[] iv = ivGenerator.generateKey();
encryptor.init(Cipher.ENCRYPT_MODE, secretKey, AesBytesEncryptor.CipherAlgorithm.GCM.getParameterSpec(iv));
byte[] encrypted = EncodingUtils.concatenate(iv, encryptor.doFinal(rawText.getBytes(StandardCharsets.UTF_8)));

decryptor.init(Cipher.DECRYPT_MODE, secretKey, AesBytesEncryptor.CipherAlgorithm.GCM.getParameterSpec(iv));
iv = EncodingUtils.subArray(encrypted, 0, ivGenerator.getKeyLength());
byte[] decrypted = decryptor.doFinal(EncodingUtils.subArray(encrypted, iv.length, encrypted.length));

Assert.assertEquals(rawText, new String(decrypted, StandardCharsets.UTF_8));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 소스는 &lt;code&gt;AesBytesEncryptor&lt;/code&gt;, &lt;code&gt;EncodingUtils&lt;/code&gt;, &lt;code&gt;CipherUtils&lt;/code&gt; 등의 클래스를 활용하며, 암호화와 복호화 부분에 synchronized 블럭이 들어가지만 모두 생략하고 한 소스에서 파악할 수 있도록 했습니다. &lt;code&gt;Encryptors&lt;/code&gt; 의 팩토리 메서드는 password, salt만 패러미터로 이용합니다. iv는 내부적으로 생성하고 API 사용자에게 노출하지 않습니다. iv 프로퍼티에 접근할 수 있는 메서드가 없는데 복호화는 어떻게 할까요? 암호화 메서드 호출시 위의 소스처럼 iv 값이 암호화 값 앞에 붙어서 반환되기 때문에 문제가 없습니다. iv 길이도 16바이트로 정해져있기 때문에 복호화 할 때도 암호화된 값에서 iv만큼 잘라서 iv로 이용하고 나머지 부분을 복호화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 BytesEncryptor와 TextEncryptor에 대해서 알아봤습니다. Spring 기반 프로젝트에서 AES-256을 이용한 암호화를 구현해야 한다면 직접 구현하기보단 &lt;code&gt;BytesEncryptor&lt;/code&gt;또는 &lt;code&gt;TextEncryptor&lt;/code&gt; 인터페이스를 써보는 건 어떨까요?&lt;/p&gt;
&lt;h1&gt;참조&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/features/integrations/cryptography.html&quot;&gt;Spring Security Crypto Module&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/crypto/encrypt/Encryptors.html&quot;&gt;org.springframework.security.crypto.encrypt.Encryptors&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring Framework</category>
      <category>Security</category>
      <category>Spring</category>
      <author>Tim(팀)</author>
      <guid isPermaLink="true">https://majortim.tistory.com/9</guid>
      <comments>https://majortim.tistory.com/9#entry9comment</comments>
      <pubDate>Sun, 1 May 2022 23:55:15 +0900</pubDate>
    </item>
    <item>
      <title>JavaMailSender로 메일 보내기</title>
      <link>https://majortim.tistory.com/8</link>
      <description>&lt;h1&gt;JavaMailSender로 메일 보내기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 유용한 메일 전송 API를 제공합니다. org.springframework.mail 패키지 아래에 있는 API 중 JavaMailSender를 이용해 메일 전송하는 방법을 소개합니다. 우선 다음과 같은 의존성이 포함되어 있어야 합니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-context-support&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;5.3.19&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;com.sun.mail&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;jakarta.mail&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;1.6.7&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 5 버전대에선 Jakarta Mail의 네임스페이스가 javax.mail 이기 때문에 Jakarta Mail은 2 버전대가 아니라 1.6 버전대를 이용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 프로퍼티 파일입니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;mail.smtp.host=smtp.naver.com
mail.smtp.port=465
mail.transport.protocol=smtp&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하는 메일 서비스의 설정에 따라 포트가 달라질 수 있습니다. 보통 465 또는 587 포트를 이용합니다. 네이버 메일 서비스를 사용하는 경우 SMTP 사용 설정을 해야합니다. &lt;a href=&quot;https://help.naver.com/support/service/main.help?serviceNo=2342&amp;amp;categoryNo=12648&quot;&gt;참조&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Configuration 클래스에 JavaMailSender 빈을 다음과 같이 정의합니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package org.example.mail.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;

import java.util.Properties;

@Configuration
@PropertySource(&quot;classpath:mail.properties&quot;)
public class AppConfig {
    @Value(&quot;${mail.transport.protocol}&quot;)
    private String protocol;

    @Value(&quot;${mail.smtp.host}&quot;)
    private String host;
    @Value(&quot;${mail.smtp.port}&quot;)
    private int port;
    @Value(&quot;${mail.username}&quot;)
    private String username;
    @Value(&quot;${mail.password}&quot;)
    private String password;

    @Bean
    public JavaMailSender javaMailSender() {
        JavaMailSenderImpl sender = new JavaMailSenderImpl();

        sender.setProtocol(protocol);
        sender.setHost(host);
        sender.setPort(port);
        sender.setUsername(username);
        sender.setPassword(password);

        Properties props = sender.getJavaMailProperties();
                props.put(&quot;mail.smtp.starttls.enable&quot;, &quot;true&quot;);
        props.put(&quot;mail.smtp.socketFactory.class&quot;, &quot;javax.net.ssl.SSLSocketFactory&quot;);

//        props.put(&quot;mail.debug&quot;, &quot;true&quot;);

        return sender;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@PropertySource 애노테이션을 이용해 사용할 프로퍼티 경로를 지정해줍니다. host 와 port 속성은 이 지정한 프로퍼티 파일에서 가져옵니다. 프로퍼티 파일을 버전 관리 대상에 포함했기 때문에 username과 password는 프로퍼티 파일에 넣지 않고 프로그램 실행시 시스템 프로퍼티를 이용하도록 했습니다(VM 옵션에 다음과 같은 형식으로 전달: -Dmail.username=사용자계정 -Dmail.password=비밀번호).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS 전송을 위해 mail.smtp.starttls.enable 속성과 mail.smtp.socketFactory.class 속성을 추가했습니다. 위의 속성은 Jakarta Mail API 표준에 정의된 속성이 아니라 프로바이더에 특화된 속성입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package org.example.mail.main;

import org.example.mail.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;

public class Main {
    public static void main(String[] args) {
        try (GenericApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class)) {

            JavaMailSender sender = context.getBean(JavaMailSender.class);

            Environment environment = context.getEnvironment();
            String from = environment.getRequiredProperty(&quot;mail.from&quot;);
            String to = environment.getRequiredProperty(&quot;mail.to&quot;);

            SimpleMailMessage message = new SimpleMailMessage();
            message.setFrom(from);
            message.setTo(to);
            message.setSubject(&quot;메일 테스트&quot;);
            message.setText(&quot;테스트&quot;);

            sender.send(message);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 메서드가 정의된 클래스입니다. username, password와 마찬가지로 발송자(from), 수신자(to) 메일주소도 VM 옵션을 이용해 지정한 시스템 프로퍼티로부터 가져옵니다. 단순한 텍스트 메시지를 보낼 땐 위와 같이 SimpleMailMessage 클래스를 이용하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;17:55:08.808 [main] INFO  javax.mail - Jakarta Mail version 1.6.7
17:55:08.808 [main] INFO  javax.mail - successfully loaded resource: /META-INF/javamail.default.providers
17:55:08.824 [main] INFO  javax.mail - Tables of loaded providers
17:55:08.824 [main] INFO  javax.mail - Providers Listed By Class Name: {com.sun.mail.smtp.SMTPTransport=javax.mail.Provider[TRANSPORT,smtp,com.sun.mail.smtp.SMTPTransport,Oracle], com.sun.mail.imap.IMAPSSLStore=javax.mail.Provider[STORE,imaps,com.sun.mail.imap.IMAPSSLStore,Oracle], com.sun.mail.pop3.POP3Store=javax.mail.Provider[STORE,pop3,com.sun.mail.pop3.POP3Store,Oracle], com.sun.mail.smtp.SMTPSSLTransport=javax.mail.Provider[TRANSPORT,smtps,com.sun.mail.smtp.SMTPSSLTransport,Oracle], com.sun.mail.imap.IMAPStore=javax.mail.Provider[STORE,imap,com.sun.mail.imap.IMAPStore,Oracle], com.sun.mail.pop3.POP3SSLStore=javax.mail.Provider[STORE,pop3s,com.sun.mail.pop3.POP3SSLStore,Oracle]}
17:55:08.824 [main] INFO  javax.mail - Providers Listed By Protocol: {imap=javax.mail.Provider[STORE,imap,com.sun.mail.imap.IMAPStore,Oracle], smtp=javax.mail.Provider[TRANSPORT,smtp,com.sun.mail.smtp.SMTPTransport,Oracle], pop3=javax.mail.Provider[STORE,pop3,com.sun.mail.pop3.POP3Store,Oracle], imaps=javax.mail.Provider[STORE,imaps,com.sun.mail.imap.IMAPSSLStore,Oracle], smtps=javax.mail.Provider[TRANSPORT,smtps,com.sun.mail.smtp.SMTPSSLTransport,Oracle], pop3s=javax.mail.Provider[STORE,pop3s,com.sun.mail.pop3.POP3SSLStore,Oracle]}
17:55:08.824 [main] INFO  javax.mail - successfully loaded resource: /META-INF/javamail.default.address.map
17:55:08.824 [main] DEBUG javax.mail - getProvider() returning javax.mail.Provider[TRANSPORT,smtp,com.sun.mail.smtp.SMTPTransport,Oracle]
17:55:08.839 [main] DEBUG com.sun.mail.smtp - useEhlo true, useAuth false
17:55:08.839 [main] DEBUG com.sun.mail.smtp - trying to connect to host &quot;smtp.naver.com&quot;, port 465, isSSL false
17:55:08.839 [main] DEBUG com.sun.mail.util.socket - getSocket, host smtp.naver.com, port 465, prefix mail.smtp, useSSL false
17:55:08.980 [main] TRACE com.sun.mail.util.socket - create socket: prefix mail.smtp, localaddr null, localport 0, host smtp.naver.com, port 465, connection timeout -1, timeout -1, socket factory sun.security.ssl.SSLSocketFactoryImpl@20765ed5, useSSL false
17:55:08.980 [main] TRACE com.sun.mail.util.socket - connecting...
17:55:08.996 [main] TRACE com.sun.mail.util.socket - success!
17:55:09.011 [main] DEBUG com.sun.mail.util.socket - SSL enabled protocols before [TLSv1.3, TLSv1.2, TLSv1.1, TLSv1]
17:55:09.011 [main] DEBUG com.sun.mail.util.socket - SSL enabled protocols after [TLSv1.3, TLSv1.2, TLSv1.1, TLSv1]
17:55:09.011 [main] DEBUG com.sun.mail.util.socket - SSL enabled ciphers after [TLS_AES_256_GCM_SHA384, TLS_AES_128_GCM_SHA256, TLS_CHACHA20_POLY1305_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_DHE_RSA_WITH_AES_256_GCM_SHA384, TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256, TLS_DHE_DSS_WITH_AES_256_GCM_SHA384, TLS_DHE_RSA_WITH_AES_128_GCM_SHA256, TLS_DHE_DSS_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_DHE_RSA_WITH_AES_256_CBC_SHA256, TLS_DHE_DSS_WITH_AES_256_CBC_SHA256, TLS_DHE_RSA_WITH_AES_128_CBC_SHA256, TLS_DHE_DSS_WITH_AES_128_CBC_SHA256, TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384, TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384, TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_RSA_WITH_AES_256_CBC_SHA, TLS_DHE_DSS_WITH_AES_256_CBC_SHA, TLS_DHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_DSS_WITH_AES_128_CBC_SHA, TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA, TLS_ECDH_RSA_WITH_AES_256_CBC_SHA, TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDH_RSA_WITH_AES_128_CBC_SHA, TLS_RSA_WITH_AES_256_GCM_SHA384, TLS_RSA_WITH_AES_128_GCM_SHA256, TLS_RSA_WITH_AES_256_CBC_SHA256, TLS_RSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_AES_256_CBC_SHA, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_EMPTY_RENEGOTIATION_INFO_SCSV]
17:55:09.183 [main] TRACE com.sun.mail.smtp.protocol - 220 smtp.naver.com ESMTP  - nsmtp
17:55:09.183 [main] DEBUG com.sun.mail.smtp - connected to host &quot;smtp.naver.com&quot;, port: 465
17:55:09.183 [main] TRACE com.sun.mail.smtp.protocol - EHLO IP
17:55:09.183 [main] TRACE com.sun.mail.smtp.protocol - 250-smtp.naver.com Pleased to meet you
17:55:09.183 [main] TRACE com.sun.mail.smtp.protocol - 250-SIZE 20971520
17:55:09.183 [main] TRACE com.sun.mail.smtp.protocol - 250-8BITMIME
17:55:09.183 [main] TRACE com.sun.mail.smtp.protocol - 250-PIPELINING
17:55:09.183 [main] TRACE com.sun.mail.smtp.protocol - 250-AUTH PLAIN LOGIN
17:55:09.183 [main] TRACE com.sun.mail.smtp.protocol - 250 ENHANCEDSTATUSCODES
17:55:09.183 [main] DEBUG com.sun.mail.smtp - Found extension &quot;SIZE&quot;, arg &quot;20971520&quot;
17:55:09.183 [main] DEBUG com.sun.mail.smtp - Found extension &quot;8BITMIME&quot;, arg &quot;&quot;
17:55:09.183 [main] DEBUG com.sun.mail.smtp - Found extension &quot;PIPELINING&quot;, arg &quot;&quot;
17:55:09.183 [main] DEBUG com.sun.mail.smtp - Found extension &quot;AUTH&quot;, arg &quot;PLAIN LOGIN&quot;
17:55:09.183 [main] DEBUG com.sun.mail.smtp - Found extension &quot;ENHANCEDSTATUSCODES&quot;, arg &quot;&quot;
17:55:09.183 [main] DEBUG com.sun.mail.smtp - STARTTLS requested but already using SSL
17:55:09.183 [main] DEBUG com.sun.mail.smtp - protocolConnect login, host=smtp.naver.com, user=발송자, password=&amp;lt;non-null&amp;gt;
17:55:09.183 [main] DEBUG com.sun.mail.smtp - Attempt to authenticate using mechanisms: LOGIN PLAIN DIGEST-MD5 NTLM XOAUTH2 
17:55:09.183 [main] DEBUG com.sun.mail.smtp - Using mechanism LOGIN
17:55:09.183 [main] DEBUG com.sun.mail.smtp - AUTH LOGIN command trace suppressed
17:55:09.251 [main] DEBUG com.sun.mail.smtp - AUTH LOGIN succeeded
17:55:09.261 [main] DEBUG com.sun.mail.smtp - use8bit false
17:55:09.261 [main] TRACE com.sun.mail.smtp.protocol - MAIL FROM:&amp;lt;발송자@naver.com&amp;gt;
17:55:09.277 [main] TRACE com.sun.mail.smtp.protocol - 250 2.1.0 OK  - nsmtp
17:55:09.277 [main] TRACE com.sun.mail.smtp.protocol - RCPT TO:&amp;lt;수신자@gmail.com&amp;gt;
17:55:09.277 [main] TRACE com.sun.mail.smtp.protocol - 250 2.1.5 OK  - nsmtp
17:55:09.277 [main] DEBUG com.sun.mail.smtp - Verified Addresses
17:55:09.277 [main] DEBUG com.sun.mail.smtp -   수신자@gmail.com
17:55:09.277 [main] TRACE com.sun.mail.smtp.protocol - DATA
17:55:09.277 [main] TRACE com.sun.mail.smtp.protocol - 354 Go ahead  - nsmtp
17:55:09.277 [main] TRACE com.sun.mail.smtp.protocol - Date: Fri, 22 Apr 2022 17:55:09 +0900 (KST)
17:55:09.277 [main] TRACE com.sun.mail.smtp.protocol - From: 발송자@naver.com
17:55:09.277 [main] TRACE com.sun.mail.smtp.protocol - To: 수신자@gmail.com
17:55:09.277 [main] TRACE com.sun.mail.smtp.protocol - Message-ID: &amp;lt;&amp;gt;
17:55:09.277 [main] TRACE com.sun.mail.smtp.protocol - Subject: =?UTF-8?B?66mU7J28IO2FjOyKpO2KuA==?=
17:55:09.277 [main] TRACE com.sun.mail.smtp.protocol - MIME-Version: 1.0
17:55:09.277 [main] TRACE com.sun.mail.smtp.protocol - Content-Type: text/plain; charset=UTF-8
17:55:09.277 [main] TRACE com.sun.mail.smtp.protocol - Content-Transfer-Encoding: base64
17:55:09.277 [main] TRACE com.sun.mail.smtp.protocol - 
17:55:09.277 [main] TRACE com.sun.mail.smtp.protocol - 7YWM7Iqk7Yq4
17:55:09.277 [main] TRACE com.sun.mail.smtp.protocol - .
17:55:09.568 [main] TRACE com.sun.mail.smtp.protocol - 250 2.0.0 OK  - nsmtp
17:55:09.569 [main] DEBUG com.sun.mail.smtp - message successfully delivered to mail server
17:55:09.569 [main] TRACE com.sun.mail.smtp.protocol - QUIT
17:55:09.572 [main] TRACE com.sun.mail.smtp.protocol - 221 2.0.0 Closing connection  - nsmtp&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행화면 로그입니다. 메시지를 성공적으로 발송했습니다. JUL 설정을 추가했기 때문에 mail.debug 속성을 true로 지정했을 때보다 더 상세한 로그가 나옵니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JUL 설정(옵션)&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;package org.example.mail.log;

import org.slf4j.bridge.SLF4JBridgeHandler;

import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;

public class JavaUtilLoggingSetup {
    public static void setup() {
        SLF4JBridgeHandler.removeHandlersForRootLogger();  // (since SLF4J 1.6.5)
        SLF4JBridgeHandler.install();

        LogManager manager = java.util.logging.LogManager.getLogManager();
        Logger javaxMail = Logger.getLogger(&quot;javax.mail&quot;);
        Logger comSunMail = Logger.getLogger(&quot;com.sun.mail&quot;);
        javaxMail.setLevel(Level.FINEST);
        comSunMail.setLevel(Level.FINEST);
        manager.addLogger(javaxMail);
        manager.addLogger(comSunMail);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jakarta Mail은 JUL을 이용하기 때문에 jul-to-slf4j bridge를 이용해 JUL 처리를 SLF4J에 위임하는 설정을 넣습니다. FINEST 레벨은 SLF4J의 TRACE 레벨에 해당합니다. 메인 메서드에서 위의 static 메서드를 호출하면 설정이 반영됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Spring 기반 애플리케이션에서 JavaMailSender를 이용한 메일 발송에 대해 알아봤습니다. 파일 첨부, &lt;a href=&quot;https://www.baeldung.com/spring-email-templates&quot;&gt;메일 템플릿&lt;/a&gt; 이용 등은 SimpleMailMessage 가 아니라 &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/5.3.x/javadoc-api/org/springframework/mail/javamail/MimeMessageHelper.html&quot;&gt;MimeMessage&lt;/a&gt; 를 이용하시면 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Gmail 발송이 안 되는 경우 계정이 2단계 인증을 사용하고 있는지 확인합니다. 2단계 인증을 사용하는 경우 전용 &lt;a href=&quot;https://support.google.com/mail/answer/185833?hl=ko&quot;&gt;앱 비밀번호&lt;/a&gt;를 생성해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;참조&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/5.3.x/reference/html/integration.html#mail&quot;&gt;https://docs.spring.io/spring-framework/docs/5.3.x/reference/html/integration.html#mail&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://eclipse-ee4j.github.io/mail&quot;&gt;https://eclipse-ee4j.github.io/mail&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring Framework</category>
      <author>Tim(팀)</author>
      <guid isPermaLink="true">https://majortim.tistory.com/8</guid>
      <comments>https://majortim.tistory.com/8#entry8comment</comments>
      <pubDate>Fri, 22 Apr 2022 20:30:28 +0900</pubDate>
    </item>
    <item>
      <title>마이크로서비스 패턴 도서 내용 정리</title>
      <link>https://majortim.tistory.com/7</link>
      <description>&lt;p&gt;마이크로서비스 패턴 / Chris Richardson (2020). &lt;em&gt;Microservices Patterns&lt;/em&gt; (이일웅, 역). 길벗. (원본 출판 2018년)의 내용 중 일부를 정리해봤습니다.&lt;/p&gt;
&lt;h1&gt;모놀리식과 MSA&lt;/h1&gt;
&lt;p&gt;모놀리식 아키텍처 패턴은 애플리케이션을 하나의 배포단위로 구성하지만 마이크로서비스 아키텍처 패턴은 독립적으로 배포 가능하면서 자체 DB를 보유한 서비스로 시스템을 분해합니다.&lt;/p&gt;
&lt;h1&gt;서비스 디스커버리&lt;/h1&gt;
&lt;p&gt;서비스 인스턴스의 네트워크 위치나 상태가 여러 가지 사유로 달라질 수 있기 때문에 서비스 디스커버리를 사용합니다.&lt;/p&gt;
&lt;h2&gt;애플리케이션 수준의 서비스 디스커버리 패턴&lt;/h2&gt;
&lt;p&gt;서비스 클라이언트와 서비스 인스턴스 사이에 이들을 매개하는 서비스 레지스트리가 있습니다. 서비스 인스턴스는 자기 자신을 서비스 레지스트리에 등록하며, 클라이언트는 서비스 레지스트리에 인스턴스 목록을 요청해서 넘겨받습니다. 다양한 플랫폼에 적용할 수 있지만 사용하는 언어에 맞는 서비스 디스커버리 라이브러리가 필요합니다. 스프링 클라우드 기반의 클라이언트는 유레카를 기본 서비스 디스커버리로 사용합니다.&lt;/p&gt;
&lt;h2&gt;플랫폼에 내장된 서비스 디스커버리 패턴 적용&lt;/h2&gt;
&lt;p&gt;도커나 쿠버네티스 등 최신 배포 플랫폼에는 대부분 서비스 레지스트리, 디스커버리 메커니즘이 탑재되어 있습니다. 플랫폼의 일부인 등록기(Registrar)라는 서드파티가 서비스 레지스트리에 서비스 인스턴스를 등록하며, 클라이언트가 DNS명을 요청하면 플랫폼 라우터를 거쳐 인스턴스로부터 서비스를 제공받습니다. 각 서비스 인스턴스는 가상 IP와, 가상 IP를 해석한 DNS명을 가집니다. 이 방식은 디스커버리 방식이 해당 플랫폼에 종속되는 단점이 있지만 개발 언어, 라이브러리와 상관 없이 동일하게 적용할 수 있습니다.&lt;/p&gt;
&lt;h1&gt;동기 RPI 패턴&lt;/h1&gt;
&lt;p&gt;동기 RPI는 클라이언트가 서비스에 요청을 보내면 서비스가 처리 후 보내는 응답을 클라이언트가 대기하는 방식이 블로킹입니다. 책에서는 프록시 패턴으로 이 구조를 설명하는데 클라이언트의 비즈니스 로직은 프록시 인터페이스를 호출하고, 인터페이스를 구현한 RPI 프록시 어댑터 클래스에 서비스에 요청을 보내는 통신이 들어갑니다. 요청을 전달받은 RPI 서버 어댑터가 다시 서비스 인터페이스를 통해 서비스의 비즈니스 로직을 호출합니다. 로직 처리를 마친 서비스는 다시 RPI 프록시로 응답을 돌려주고 최종 결과는 클라이언트 비즈니스 로직에 반환됩니다.&lt;/p&gt;
&lt;h1&gt;회로 차단기 패턴&lt;/h1&gt;
&lt;p&gt;성공/실패 요청 개수를 지켜보다가 에러율이 주어진 임계치를 초과하면 그 이후 시도는 바로 실패 처리합니다. 타임아웃 시간 이후 클라이언트가 재시도해서 성공하면 회로 차단기는 닫힙니다.&lt;/p&gt;
&lt;h1&gt;도메인 이벤트&lt;/h1&gt;
&lt;p&gt;애그리거트에 발생한 사건으로 대부분 어떤 상태 변경을 나타냅니다. 도메인 이벤트는 과거 분사형 동사로 명명한 클래스로 표현합니다. 원시 값(Primitive Type) 또는 밸류 객체(Value Object)로 표현된 프로퍼티로 이루어져 있으며 그 중에는 이벤트 ID, 타임스탬프와 같은 메타데이터도 있습니다. 메타데이터는 상위 클래스에 정의된 이벤트 객체의 일부이거나, 특정 이벤트 프로퍼티가 아닌 이벤트 객체를 감싼 인벨로브 객체에 들어있을 수도 있습니다.&lt;/p&gt;
&lt;h1&gt;이벤트 소싱&lt;/h1&gt;
&lt;p&gt;이벤트 소싱은 애그리거트를 여러 이벤트로 저장하며, 이 이벤트를 가져와 현재 애그리거트의 상태를 다시 구성합니다.&lt;/p&gt;
&lt;h1&gt;API 조합 패턴&lt;/h1&gt;
&lt;p&gt;서비스 클라이언트가 데이터를 가진 여러 서비스를 직접 호출하여 그 결과를 조합하는 패턴입니다. 쿼리를 구현하는 간단한 방법이지만 오버헤드 증가, 가용성 저하 우려, 데이터 일관성 결여 등의 단점이 있습니다.&lt;/p&gt;
&lt;h1&gt;CQRS 패턴&lt;/h1&gt;
&lt;p&gt;CQRS(Command Query Responsibility Separation)는 영속적 데이터 모델과 그것을 사용하는 모듈을 커맨드와 쿼리, 두 편으로 가릅니다. 조회 쪽 기능은 쿼리 쪽 모듈 및 데이터 모델에, 생성/수정/삭제 기능은 커맨드 쪽 모듈 및 데이터 모델에 구현하는 것입니다. 양쪽 데이터 모델 사이의 동기화는 커맨드 쪽에서 발행한 이벤트를 쿼리 쪽에서 구독하는 식으로 이루어집니다.&lt;/p&gt;
&lt;h1&gt;API 게이트웨이&lt;/h1&gt;
&lt;p&gt;API 게이트웨이는 방화벽 외부의 클라이언트가 애플리케이션에 API 요청을 하는 단일 창구 역할을 하는 서비스입니다. 퍼사드처럼 내부 애플리케이션 아키텍처를 캡슐화하고 자신의 클라이언트에는 API를 제공합니다. 인증, 모니터링, 사용량 제한 등 부수적인 일도 담당합니다. REST API 보다 GraphQL 같은 그래프 기반의 API를 이용하는 것이 효율적입니다.&lt;/p&gt;
&lt;h1&gt;BFF 패턴&lt;/h1&gt;
&lt;p&gt;BFF(Backends For Frontends) 패턴은 각 클라이언트마다 API 게이트웨이를 따로 두는 패턴입니다. 공통 게이트웨이를 이용할 때의 책임 소재가 불분명해지는 문제를 해결하고 API 모듈이 서로 격리되어 신뢰성이 향상됩니다.&lt;/p&gt;
&lt;h1&gt;컨슈머 주도 계약 테스트&lt;/h1&gt;
&lt;p&gt;컨슈머 주도 계약 테스트는 프로바이더 API의 형상(shape)이 클라이언트의 기대에 부합하는지 확인합니다.&lt;/p&gt;
&lt;h1&gt;배포 파이프라인&lt;/h1&gt;
&lt;p&gt;배포 파이프라인은 개발자가 데스크톱에서 작성한 코드를 프로덕션에 반영하는 자동화 프로세스입니다. 배포 파이프라인은 다음과 같은 단계를 가집니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사전-커밋(pre-commit): 단위 테스트를 실행합니다.&lt;/li&gt;
&lt;li&gt;커밋 테스트(commit test) 단계: 서비스 컴파일 후 단위 테스트를 실행하고 정적 코드 분석을 수행합니다.&lt;/li&gt;
&lt;li&gt;통합 테스트(integration test) 단계: 통합 테스트를 실행합니다.&lt;/li&gt;
&lt;li&gt;컴포넌트 테스트(component test) 단계: 서비스 컴포넌트 테스트를 실행합니다.&lt;/li&gt;
&lt;li&gt;배포(deploy) 단계: 프로덕션에 서비스를 배포합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;참조&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://www.gilbut.co.kr/book/view?bookcode=BN002687&quot;&gt;마이크로서비스 패턴&lt;/a&gt;&lt;/p&gt;</description>
      <category>MSA</category>
      <author>Tim(팀)</author>
      <guid isPermaLink="true">https://majortim.tistory.com/7</guid>
      <comments>https://majortim.tistory.com/7#entry7comment</comments>
      <pubDate>Tue, 22 Feb 2022 17:34:39 +0900</pubDate>
    </item>
    <item>
      <title>enum의 필드를 Map타입 List로 변환</title>
      <link>https://majortim.tistory.com/6</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; letter-spacing: 0px;&quot;&gt;enum의 필드를 Map타입 List로 변환하는 방법을 소개합니다. 제네릭 메서드와 함수형 인터페이스를 이용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;package io.hurem.model;

public interface EnumModel {
    String getKey();
    String getValue();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;enum에서 구현할 인터페이스. getKey()는 enum 필드의 이름을, getValue()는 단일 프로퍼티를 가져온다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package io.hurem.domain.employees;

import io.hurem.model.EnumModel;

import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public enum EmployeeStatusType implements EnumModel {
    PRESENT(&quot;재직&quot;),
    ABSENT(&quot;일반휴직&quot;),
    PARENTAL(&quot;육아휴직&quot;),
    UNPAID(&quot;무급휴직&quot;),
    SUSPENDED(&quot;정직&quot;),
    RETIRED(&quot;퇴직&quot;),
    UNKNOWN(&quot;알 수 없음&quot;);

    private final String status;

    EmployeeStatusType(String status) {
        this.status = status;
    }

    @Override
    public String getKey() {
        return name();
    }

    @Override
    public String getValue() {
        return status;
    }

    private static final Map&amp;lt;String, EmployeeStatusType&amp;gt; types = Collections.unmodifiableMap(Stream.of(values()).collect(Collectors.toMap(EmployeeStatusType::getValue, Function.identity())));

    public static EmployeeStatusType find(String description) { return Optional.ofNullable(types.get(description)).orElse(UNKNOWN); }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EnumModel을 구현한 enum. 프로퍼티 값에 해당하는 enum을 찾는 메서드 find를 Map타입을 이용해 구현했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;package io.hurem.util;

import io.hurem.model.EnumModel;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

@Component
public class EnumUtils {
    @SafeVarargs
    public final &amp;lt;E extends Enum&amp;lt;E&amp;gt; &amp;amp; EnumModel&amp;gt; List&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; getEnumList(final Class&amp;lt;E&amp;gt; enumClass, E... exclusions) {
        List&amp;lt;E&amp;gt; list =  new ArrayList&amp;lt;&amp;gt;(Arrays.asList(enumClass.getEnumConstants()));
        Optional.of(exclusions).ifPresent(arr -&amp;gt; list.removeAll(Arrays.asList(arr)));

        Function&amp;lt;E, Map&amp;lt;String, String&amp;gt;&amp;gt; enumModelToMap = (model) -&amp;gt; {
            Map&amp;lt;String, String&amp;gt; result = new HashMap&amp;lt;&amp;gt;();

            result.put(&quot;name&quot;, model.getKey());
            result.put(&quot;value&quot;, model.getValue());
            return result;
        };

        return list.stream().map(enumModelToMap).collect(Collectors.toList());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 클래스. 메모리 문제를 생각해서 메서드를 static으로 정의하지 않는 대신 EnumUtils에 @Component를 붙여 빈으로 등록하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 enum에 종속되지 않도록 제네릭 메서드로 구현했다. 타입 패러미터 부분에 들어간 &amp;amp;에 주목해야 하는데, E 타입은 Enum이면서 동시에 EnumModel 인터페이스를 구현함을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드의 첫 번째 인자는 사용하려는 enum의 클래스 타입이다. 첫 번째 인자의 getEnumConstants() 메서드로 Enum의 배열을 가져오며 Arrays.asList() 메서드를 이용해 List로 변경한다. getEnumConstants() 메서드 호출이 반환하는 배열은 원소 순서가 enum에 선언된 필드 순서와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드의 두 번째 인자는 enum에서 가져오지 않을 필드의 가변인자이다. 두 번째 인자가 null이 아니면 List에서 해당 필드들을 제거한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Stream과 Function 인터페이스를 이용해 List의 원소들을 HashMap으로 매핑하고 다시 List로 취합해서 메서드 반환 값으로 전환한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;package io.hurem.domain.util;

import io.hurem.domain.employees.EmployeeStatusType;
import io.hurem.util.EnumUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
@SpringBootTest
public class EnumUtilsTest {
    @Autowired
    EnumUtils enumUtils;
    @Test
    public void listTest() {
        log.debug(&quot;list1: {}&quot;, enumUtils.getEnumList(EmployeeStatusType.class));
        log.debug(&quot;list2: {}&quot;, enumUtils.getEnumList(EmployeeStatusType.class, EmployeeStatusType.ABSENT, EmployeeStatusType.UNKNOWN));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EnumUtils가 생각한대로 동작하는지 테스트하는 코드다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;2022-02-17 23:26:15,561 DEBUG io.hurem.domain.util.EnumUtilsTest : list1: [{name=PRESENT, value=재직}, {name=ABSENT, value=일반휴직}, {name=PARENTAL, value=육아휴직}, {name=UNPAID, value=무급휴직}, {name=SUSPENDED, value=정직}, {name=RETIRED, value=퇴직}, {name=UNKNOWN, value=알 수 없음}]
2022-02-17 23:26:15,563 DEBUG io.hurem.domain.util.EnumUtilsTest : list2: [{name=PRESENT, value=재직}, {name=PARENTAL, value=육아휴직}, {name=UNPAID, value=무급휴직}, {name=SUSPENDED, value=정직}, {name=RETIRED, value=퇴직}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도한대로 List를 반환하며 제외 항목도 잘 처리한다. 순서도 enum 클래스에 정의한 순서대로 나온다.&lt;/p&gt;
&lt;h1&gt;참조&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://jojoldu.tistory.com/122&quot;&gt;Enum 활용 &amp;amp; Enum 리스트 가져오기&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;a href=&quot;https://pjh3749.tistory.com/279&quot;&gt;Enum 조회 성능 높여보기&lt;/a&gt;&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>Java</category>
      <author>Tim(팀)</author>
      <guid isPermaLink="true">https://majortim.tistory.com/6</guid>
      <comments>https://majortim.tistory.com/6#entry6comment</comments>
      <pubDate>Thu, 17 Feb 2022 23:52:17 +0900</pubDate>
    </item>
    <item>
      <title>RESTful Web API 도서 내용 정리</title>
      <link>https://majortim.tistory.com/4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;RESTful Web API - 웹 API를 위한 모범 전략 가이드(원제 RESTful Web APIs)의 내용을 정리해봤습니다.&lt;/p&gt;
&lt;!--more--&gt;
&lt;h1&gt;표준의 종류&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명목 표준 &amp;lt; 개인 표준 / 기업 표준 &amp;lt; 개방형 표준 &amp;lt; RFC&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명목 표준&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명목 표준은 행동 양식이다. 구글 API, 페이스북 API 등이 해당한다. 문서화는 할 수 있지만 다른 사람도 동일한 방식으로 해야한다는 표준의 핵심 가정이 빠져있다. 이 설계에 맞춘 작업을 하려면 이 설계를 이해해야 한다. 명목 표준을 재사용하는 것은 API가 표준을 따른다고 하지 않고 복제본이라고 부른다.&lt;/p&gt;
&lt;h1&gt;HATEOAS&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Hypermedia as the Engine of Application State&lt;/b&gt;란 하이퍼미디어 제약 조건이다. 용어에서 말하는 애플리케이션 상태란 클라이언트가 현재 있는 페이지를 말한다. 하이퍼미디어는 HTML 링크와 폼 같은 것들을 부르는 일반 용어다. 그래서 전체 용어에 대한 풀이는 클라이언트가 현재 어디에 있고 어디로 연결될 수 있는지를 말한다.&lt;/p&gt;
&lt;h1&gt;안전하지 않은 HTTP 메서드&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안전하지 않은 HTTP 메서드는 해당 요청을 보냈을 때와 보내지 않았을 때의 결과(서버에 일어나는 상태 변화)가 다르다. 안전하지 않은 메서드로는 POST, PATCH 등이 있다.&lt;/p&gt;
&lt;h1&gt;멱등성(Idempotent)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 조건 아래에서는 여러 번 시도한다고 처음 시도와 나중 시도의 결과가 달라지지 않는다.&lt;/p&gt;
&lt;h1&gt;리소스(Resource)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 페이지, 사람, 이름 등 무엇이든 리소스가 될 수 있지만 그 리소스는 자신의 URI를 가져야 한다.&lt;/p&gt;
&lt;h1&gt;표현(Representation)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스의 상태를 나타낸다. HTTP 요청이나 응답의 엔티티 바디로 볼 수도 있고 요청이나 응답 메시지 전체를 표현이라고 볼 수도 있다. 서버가 표현을 클라이언트에 보낼 때는 서버의 상태를 나타내는 것이고, 클라이언트가 서버에 표현을 보낼 때는 서버의 상태를 수정하려는 것이다.&lt;/p&gt;
&lt;h1&gt;자기 서술적 메시지(Self-descriptive Messages)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청이나 응답 메시지를 이해하기 위해 필요한 모든 정보가 그 메시지 자체에 포함되어 있거나 적어도 그 링크가 있어야 한다.&lt;/p&gt;
&lt;h1&gt;무상태(Statelessness)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 요청을 보내는 그 순간을 제외하고는 서버는 클라이언트의 존재를 모른다.&lt;/p&gt;
&lt;h1&gt;프로필(Profile)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미디어 유형을 아는 것만으로는 알 수 없는 문서의 의미 체계를 설명한다.&lt;/p&gt;
&lt;h1&gt;의미 체계 서술자(Semantic Descriptor)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스 상태의 각 조각을 명명하는 짧은 문자열. 프로파일로 보통 사람이 읽을 수 있는 설명으로 주어진다. &lt;b&gt;RESTful Web API&lt;/b&gt; 책을 위해 만들어진 단어로 표준 용어는 아니다.&lt;/p&gt;
&lt;h1&gt;API 설계 절차&lt;/h1&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API를 통해 제공할 정보를 열거한다. 이 정보들이 의미 체계 서술자가 된다. 이 정보들은 필요에 따라 계층화할 수도 있다. 현실 세계의 개체를 참조하고 더 자세한 동작을 정의할 수 있다.&lt;/li&gt;
&lt;li&gt;API 상태 다이어그램을 그린다. 화살표를 이용해 HTTP 요청이 작동시키는 상태 전이를 표현한다. 상태 전이에 HTTP 메서드를 할당할 필요는 없지만 상태 전이가 안전한지, 멱등한지 여부는 계속 알고있어야 한다(원서에서는 you should keep track of 라고 했고, 번역서에서는 주시해야 한다라고 써져있다). 의미 체계 서술자와 그 연결 관계가 만족스러울 때까지 1단계와 2단계를 반복한다.&lt;/li&gt;
&lt;li&gt;기존에 있던 프로필의 문자열을 의미에 맞게 조정한다. 조정하면서 이전 단계들을 반복할 수 있다.&lt;/li&gt;
&lt;li&gt;프로토콜 의미 체계, 애플리케이션 의미 체계와 호환되는 미디어 유형을 선택한다.&lt;/li&gt;
&lt;li&gt;애플리케이션 의미 체계를 서술하는 프로필을 작성한다. ALPS 문서, JSON-LD 콘텍스트 등을 이용할 수 있다.&lt;/li&gt;
&lt;li&gt;코드를 구현한다.&lt;/li&gt;
&lt;li&gt;API를 게시한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;참조&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://blog.insightbook.co.kr/2015/09/04/%ec%9b%b9-api-%ec%96%b4%eb%96%bb%ea%b2%8c-%eb%a7%8c%eb%93%a4%eb%a9%b4-%ec%a2%8b%ec%9d%84%ea%b9%8c-%e3%80%8erestful-web-api%e3%80%8f/&quot;&gt;RESTful Web API&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.rfc-editor.org/info/rfc6906&quot;&gt;RFC 6906 / The 'profile' Link Relation Type, MARCH 2013&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Link_relation&quot;&gt;Link relation&lt;/a&gt;&lt;/p&gt;</description>
      <category>Web</category>
      <author>Tim(팀)</author>
      <guid isPermaLink="true">https://majortim.tistory.com/4</guid>
      <comments>https://majortim.tistory.com/4#entry4comment</comments>
      <pubDate>Sun, 5 Dec 2021 23:12:37 +0900</pubDate>
    </item>
    <item>
      <title>추상 클래스와 인터페이스 비교</title>
      <link>https://majortim.tistory.com/3</link>
      <description>&lt;blockquote&gt;
&lt;p&gt;이 문서는 Java 8 기준으로 작성되었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;추상클래스와 인터페이스는 비슷하다. 객체를 만들 수 없다는 점과 둘 다 메서드를 선언만 할지 아니면 구현도 할지 선택할 수 있다는 점이 같다. 하지만 추상 클래스와 달리 인터페이스는 모든 필드가 키워드를 안 붙여도 자동으로 public static final 이 되며 모든 메서드가 public 으로 선언되거나 정의되어야 한다.&lt;/p&gt;
&lt;p&gt;그렇다면 추상 클래스와 인터페이스는 어떤 상황에 사용할까?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;이럴 때 추상클래스를 사용한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;관련성이 높은 클래스들간에 코드 공유를 하고 싶을 때&lt;/li&gt;
&lt;li&gt;상속할 공통 메서드와 필드가 많거나 접근 제어자로 public 이외의(protected 또는 private 같은) 것을 사용하고 싶을 때&lt;/li&gt;
&lt;li&gt;static 이나 final 이 아닌 필드를 선언하고 싶을 때. 이렇게하면 객체의 상태에 접근하고 수정할 수 있는 메서드를 정의하는 게 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;이럴 때 인터페이스를 사용한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;관련성이 없는 클래스들간에도 같은 기능을 구현할 때. 예를 들면 Comparable 과 Clonable을 구현하는 많은 클래스들은 서로 상관이 없음&lt;/li&gt;
&lt;li&gt;특정 데이터 타입의 행위를 명시하고 싶으면서도, 어떤 것이 그 행위를 구현할지 관심이 없을 때&lt;/li&gt;
&lt;li&gt;다중 상속의 이점을 취하고 싶을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/java/IandI/abstract.html&quot;&gt;참조&lt;/a&gt;&lt;/p&gt;</description>
      <category>Java</category>
      <author>Tim(팀)</author>
      <guid isPermaLink="true">https://majortim.tistory.com/3</guid>
      <comments>https://majortim.tistory.com/3#entry3comment</comments>
      <pubDate>Mon, 4 May 2020 21:17:22 +0900</pubDate>
    </item>
  </channel>
</rss>