<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>기록용</title>
    <link>https://qwebnm7788.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Mon, 6 Apr 2026 19:17:52 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>qwebnm7788</managingEditor>
    <item>
      <title>로그를 남길 때 고려해봐야 할 것들</title>
      <link>https://qwebnm7788.tistory.com/16</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아직 개발중인 프로젝트에서 테스트하던 중 서비스가 정상적으로 동작하지 않는다는 이야기를 듣고 이를 확인해보려 했었는데 생각보다 내가 남겼던 로그만으로는 문제를 파악하기 쉽지 않았다. 그 전에도 한번 비슷한 경험이 있어서 나름대로 조치를 취한것이었는데 이번에도 동일한 문제가 생기는 것 같아 좀 아쉬움이 컸다. 다음에는 이런일이 생기지 않도록 로그를 남기는 것에 대해서 좀 더 자세하기 알아보기로 결심했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;로그 레벨을 적절히 사용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진행하는 프로젝트의 프로덕션 환경은 기본 로그 레벨을 INFO 로 사용한다. (아마 대부분의 경우 그럴것 같다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 로그 레벨은 사용하는 프로그래밍 언어와 프레임워크에 따라 다르겠지만 일반적으로 자주 사용되는 레벨이 있다고 보면 보통 INFO 로 대부분의 로그를 남기고 에러나 예외가 발생한 순간에 ERROR 레벨의 로그를 사용하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 레벨이 맨 처음 로그를 찾으려 할 때 가장 먼저 사용할 수 있는 필터의 역할을 하기 때문에 이를 잘 분류하는 것이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중에서도 가장 많이 사용되는 INFO 와 ERROR 레벨을 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;INFO&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션의 비즈니스 목적을 위해 수행하는 여러 중요한 이벤트를 기록하는데 사용하는 로그 레벨이라고 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 보면 한 서비스의 동작의 축약본이라고 생각할 수도 있다. INFO 레벨의 로그를 순서대로 따라가보면 특정 동작이 어떠한 흐름으로 진행되고 있는가를 판단할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INFO 레벨로 남길만한 것들은 다음과 같은 것들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 작업 혹은 리소스의 상태를 변경하는 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 PENDING -&amp;gt; IN PROGRESS 와 같은 상태 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스케줄링으로 발생한 Job/Task 의 완료 이벤트&lt;/li&gt;
&lt;li&gt;그 외 애플리케이션에서 발생할 수 있는 중요한 이벤트들..&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INFO 레벨의 로그는 너무 자세하거나 너무 많아서는 좋지 않은데, 기본값으로 사용하는 경우가 많고 필터링해서 보여지는 데이터 중 가장 많은 비율을 차지하게 될 것이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ERROR&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션의 특정 작업의 수행을 방해하는 경우 이를 기록하기 위해 사용할 수 있는 로그 레벨이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FATAL 과 구분해서 사용하는 경우도 있는데, 그 때에는 FATAL 은 애플리케이션이 더 이상 서비스를 유지하지 못하는 경우에 사용하고 ERROR 의 경우 로그가 발생하더라도 서비스는 계속해서 유지할 수 있는 경우에 사용하는 식으로 구분할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고 모든 에러나 예외 상황에서 ERROR 레벨을 사용할 필요는 없다. 애플리케이션의 동작이나 성능에 영향을 주지 않거나 미리 예상하고 있던 예외는 로그 레벨을 하나 낮춰서 사용하는 것이 좋을 수 있다. (예를 들면 WARN 레벨) 이렇게 해줌으로써 정말 중요한 문제를 식별할 때 노이즈를 줄여줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ERROR 레벨로 남길만한 것들은 다음과 같은 것들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 API 나 서비스가 동작하지 않는 경우 (retry 를 했음에도)&lt;/li&gt;
&lt;li&gt;connection timeout, DNS resolution failure 와 같은 네트워크 이슈&lt;/li&gt;
&lt;li&gt;JSON decode 에러와 같은 예상치 못한 에러&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 에러의 원인을 좀 더 쉽게 파악할 수 있도록 Stacktrace 도 함께 남겨주는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Structured Log&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 남길 때 아주 단순하게만 생각하면 사람이 읽기 쉽게 남기는 것이 중요하다고 생각할 수 있다. 예를 들어 개발중에 흔히 볼 수 있는 로그는 다음과 같은 형태를 갖는다.&lt;/p&gt;
&lt;pre id=&quot;code_1742114692147&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[2023-11-03 08:45:33,123] ERROR: Database connection failed: Timeout exceeded.
Nov  3 08:45:10 myserver kernel: USB device 3-2: new high-speed USB device number 4 using ehci_hcd
ERROR:  relation &quot;custome&quot; does not exist at character 15&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 형식은 사람은 보기 쉽지만 모니터링에 사용하려고하면 그렇게 효율적이지 않음을 알 수 있다. 실제로도 나는 cloudwatch 에 쌓인 로그를 사용해서 grafana 로 시각화를 하고 있었는데 어느정도 로그를 구조화해서 보내주고 있었지만 파싱을 하는 것도 꽤 까다롭고 원하는대로 동작을 할 수 없는 경우가 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우를 위해서 우리는 사람이 읽기도 쉽고, 기계가 인식하기 쉽게 일관성 있는 구조를 가져가는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 많이 활용하는 방법 중 하나는 JSON 이나 logfmt 와 같은 구조화된 형식으로 로그를 남기는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1742114902522&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;agent&quot;: &quot;Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36&quot;,
  &quot;client&quot;: &quot;172.17.0.1&quot;,
  &quot;compression&quot;: &quot;2.75&quot;,
  &quot;referer&quot;: &quot;http://localhost/somewhere&quot;,
  &quot;request&quot;: &quot;POST /not-found HTTP/1.1&quot;,
  &quot;size&quot;: 153,
  &quot;status&quot;: 404,
  &quot;timestamp&quot;: &quot;2021-04-01T12:02:31Z&quot;,
  &quot;user&quot;: &quot;alice&quot;
}

172.17.0.1 - alice [01/Apr/2021:12:02:31 +0000] &quot;POST /not-found HTTP/1.1&quot; 404 153 &quot;http://localhost/somewhere&quot; &quot;Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36&quot; &quot;2.75&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 을 사용하면 파싱을 하기 쉬워지기 때문에 여러 모니터링 툴에서 이 데이터를 쉽게 활용할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발하는 과정에서 개발자가 이를 쉽게 인식하도록 하길 원한다면 (사실 JSON 자체가 인식하기 쉬운것 같긴하지만) log output 툴을 사용해서 색상을 입혀준다던지, 콘솔에 보여주는 로그는 다른 포맷을 사용하는 것과 같은 방법을 적용시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;의미있는 로그 정보 담기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남겨진 로그 한 줄을 보고 어느정도 감을 잡을 수도 있겠지만, 복잡한 경우 해당 로그가 기록된 시점의 컨텍스트 정보가 있다면 문제 해결에 많은 도움이 될 수 있다. 이를 위해 로그에 여러 의미있는 정보를 추가해주는 것이 중요하다.&lt;/p&gt;
&lt;pre id=&quot;code_1742115137928&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;timestamp&quot;: &quot;2023-11-06T14:52:43.123Z&quot;,
  &quot;level&quot;: &quot;INFO&quot;,
  &quot;message&quot;: &quot;Login attempt failed&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 로그가 남아서 문제를 확인해야 하는 경우를 생각해보자. 물론 JSON 형식으로 구조화된 로그를 잘 남겨주긴 했지만 로그인이 실패했다는 정보 이외에는 왜 실패했는지, 누가 실패했는지와 같은 정보를 쉽게 판단할 수 없게 된다. 아마 이런 문제를 해결하기 위해서는 버그 리포트에 명시된 정보를 참조하고 코드 레벨을 순차적으로 따라가며 확인해야만 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미있는 정보 중 가장 많이 추가하는 필수적이면서 공통적인 정보에는 다음과 같은 것들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Request / Correlation ID&lt;/li&gt;
&lt;li&gt;User ID&lt;/li&gt;
&lt;li&gt;DB table name / metadata&lt;/li&gt;
&lt;li&gt;Stacktrace&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 본 로그에 이와 같은 부가적인 정보를 추가해주면 다음과 같다&lt;/p&gt;
&lt;pre id=&quot;code_1742115359982&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;timestamp&quot;: &quot;2023-11-06T14:52:43.123Z&quot;,
  &quot;level&quot;: &quot;INFO&quot;,
  &quot;message&quot;: &quot;Login attempt failed due to incorrect password&quot;,
  &quot;user_id&quot;: &quot;12345&quot;,
  &quot;source_ip&quot;: &quot;192.168.1.25&quot;,
  &quot;attempt_num&quot;: 3,
  &quot;request_id&quot;: &quot;xyz-request-456&quot;,
  &quot;service&quot;: &quot;user-authentication&quot;,
  &quot;device_info&quot;: &quot;iPhone 12; iOS 16.1&quot;,
  &quot;location&quot;: &quot;New York, NY&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 사용자가, 어떤 환경에서 몇 번의 로그인 실패를 경험했는지를 한번에 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OWASP 에서 로그에 추가하면 좋은 몇 가지 정보들을 추천하는데 우리가 아는 육하원칙과 비슷한 느낌이 든다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;언제
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그 날짜와 시간 (international format 의 형태로)&lt;/li&gt;
&lt;li&gt;이벤트의 날짜와 시간 (로그가 남겨진 시간과 실제 이벤트가 발생한 시간이 다를 수 있다)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;li&gt;서비스 명&lt;/li&gt;
&lt;li&gt;코드 위치&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;누가
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 주소 (사용자의 단말 식별자, IP 주소 등)&lt;/li&gt;
&lt;li&gt;사용자 식별자 (사용자 이름, 사용자의 DB primary key)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;li&gt;심각도 (로그 레벨로 표현할 수 있을 것 같다)&lt;/li&gt;
&lt;li&gt;설명&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;로그 샘플링&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그가 엄청나게 많이 발생하는 시스템이라면 (그냥 사용자가 많은 시스템일 수도 있다) 로그를 모두 수집하고 보여주는 것보다는 일부를 샘플링해서 사용하는 것이 비용적인 측면에서나 로그를 활용하는 측면에서 도움이 될 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 많이 사용하는 방법은 미리 샘플링의 비율을 정해두고 그 이하의 경우는 애초에 수집을 안하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 20%의 로그만을 수집한다고 하면 10번의 요청 중 8번은 로그를 넘겨주지 않도록 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샘플링을 하지 않으면 수 많은 로그 속에서 우리가 원하는 로그를 찾기도 힘들뿐더러 특히 에러가 발생하는 경우에 더 많은 어려움을 줄 수 있다. 에러가 발생하면 ERROR 로그 레벨을 사용해 Stacktrace 를 함께 로그에 남기게 되는데 잘못된 코드로 인해 발생하는 다량의 에러의 경우에는 해당 상황이 발생하면 엄청나게 많은 로그가 발생하게 된다. 이는 비용적인 측면에서도 좋지 않기 때문에 에러 로그라고 해서 무조건 다 남긴다기 보다는 적절히 샘플링하는 것이 더 좋은 선택이 될 수도 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Canonical Log&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Canonical Log 라는 것은 모든 request 의 끝에 생성되는 한 줄의 요약 로그를 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 보면 이 로그는 특정 request 에 대한 필요한 모든 정보를 담고 있는 축약본이라고 할 수 있다. 해당 로그를 보면 여러개의 로그를 하나씩 확인하지 않더라도 해당 요청에서는 어떤 일이 일어났는지를 대략적으로 빠르게 판단할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 형식으로 남긴 canonical log 는 다음과 같다&lt;/p&gt;
&lt;pre id=&quot;code_1742115933162&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;http_verb&quot;: &quot;POST&quot;,
  &quot;path&quot;: &quot;/user/login&quot;,
  &quot;source_ip&quot;: &quot;203.0.113.45&quot;,
  &quot;user_agent&quot;: &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36&quot;,
  &quot;request_id&quot;: &quot;req_98765&quot;,
  &quot;response_status&quot;: 500,
  &quot;error_id&quot;: &quot;ERR500&quot;,
  &quot;error_message&quot;: &quot;Internal Server Error&quot;,
  &quot;oauth_application&quot;: &quot;AuthApp_123&quot;,
  &quot;oauth_scope&quot;: &quot;read&quot;,
  &quot;user_id&quot;: &quot;user_789&quot;,
  &quot;service_name&quot;: &quot;AuthService&quot;,
  &quot;git_revision&quot;: &quot;7f8ff286cda761c340719191e218fb22f3d0a72&quot;,
  &quot;request_duration_ms&quot;: 320,
  &quot;database_time_ms&quot;: 120,
  &quot;rate_limit_remaining&quot;: 99,
  &quot;rate_limit_total&quot;: 100
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;canonical log 에는 주로 다음과 같은 정보들을 포함한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP request verb, path, response status&lt;/li&gt;
&lt;li&gt;사용자 인증 관련 정보들&lt;/li&gt;
&lt;li&gt;rate limit 관련 정보&lt;/li&gt;
&lt;li&gt;Timing 정보 (total request duration, DB query time 등)&lt;/li&gt;
&lt;li&gt;발생한 DB 쿼리 수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 항목들이 다 이해가 되는 결정이지만 몇몇 제약사항들 때문에 실천 불가능한 것들도 존재할 수 있다. 그래도 되도록이면 위와 같은 사항들을 생각하면서 로그를 기록하고 모니터링 할 수 있는 환경을 미리 만들어두는 것이 추후에 발생할 수 있는 많은 이슈의 해결에 큰 도움이 될 것이라고 생각한다.&lt;/p&gt;</description>
      <author>qwebnm7788</author>
      <guid isPermaLink="true">https://qwebnm7788.tistory.com/16</guid>
      <comments>https://qwebnm7788.tistory.com/16#entry16comment</comments>
      <pubDate>Sun, 16 Mar 2025 18:40:44 +0900</pubDate>
    </item>
    <item>
      <title>SQL AntiPatterns - Keyless Entry</title>
      <link>https://qwebnm7788.tistory.com/15</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;관계형 데이터베이스는 테이블 자체에 대한 설정도 중요하지만 테이블 사이의 &quot;관계&quot; 도 중요한 부분을 차지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;참조 무결성&quot; 은 이런 데이터베이스를 디자인하고 관련된 연산을 수행할 때 핵심적으로 동작해서 이런 부분에 도움을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, foreign key 제약을 선언하면 해당 컬럼의 값은 다른 테이블의 primary key 혹은 unique key 로 존재해야 한다는 제약사항을 지킬 수 있도록 도와준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNpTPg/btsMyITuw68/LgQSJdNj4HHtD2DYOy0V8K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNpTPg/btsMyITuw68/LgQSJdNj4HHtD2DYOy0V8K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNpTPg/btsMyITuw68/LgQSJdNj4HHtD2DYOy0V8K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNpTPg%2FbtsMyITuw68%2FLgQSJdNj4HHtD2DYOy0V8K%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;783&quot; height=&quot;522&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참조무결성을 사용하지 않는 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조 무결성이 도움을 주긴 하지만 몇몇 사람들은 사용을 권하지 않는 경우가 있는데 그 때 언급되는 이유는 다음과 같은 것들이 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터를 수동으로 갱신할 때 참조 무결성 제약에 위반되는 경우가 있다.&lt;/li&gt;
&lt;li&gt;성능에 안좋은 영향을 준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 개발 초기 단계에는 스키마에 변동이 자주 생기곤 하는데 이 때 제약조건에 걸려 불가능한 경우가 생길 수 있다. 또한 장애 대응을 빠르게 하기 위해 DB에 직접 접근하여 수동으로 데이터를 조작해야 하는 경우가 생길 수 있는데 해당 동작이 막혀있다면 빠르게 대응하지 못할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참조 무결성을 사용하지 않을 때의 대응&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 외래키가 동작하는 방식 그대로를 애플리케이션 로직으로 잘 구현할 수 있다면 테이블에 외래키 제약을 걸어둘 필요는 없다고 생각할 수 있다. 예를 들어 새로운 row 를 추가하고자 할 때 외래키로 설정하고 싶은 컬럼의 값이 참조하는 테이블에 존재하는지 확인하는 로직을 작성해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이런 방법이 잘 동작하기 위해서는 로직이 늘 잘 짜여져 있다는 전제조건이 있어야 하고 만약 다른 팀에서 이를 사용하고자 할 때에도 적절한 가이드를 제공해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Lock 을 적절히 사용하지 않으면 데이터가 추가되는 시점과 조회되는 시점에 발생하는 차이로 인해 로직을 정확하게 작성했더라도 잘못된 데이터가 테이블에 들어갈 수 있는 확률도 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘못된 데이터가 들어가는 경우를 위해서 이를 확인하고 정정해주는 스크립트를 작성해놓고 주기적으로 실행하는 방법을 생각해볼수도 있다. 이 때는 어떤 주기로 스크립트를 실행해야 할 지, 데이터가 많아졌을 때 스크립트를 돌림으로써 서비스에 어떠한 영향을 주게 될 지를 고민해 보아야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;외래키 제약이 방해가 되는 경우&lt;/h4&gt;
&lt;pre id=&quot;code_1740923651226&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;A (
 id int,
 status string
 foreign key (status) references B(status)
)

B (
 id int,
 status string
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A.status 가 B.status 를 외래키로 참조하는 경우를 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 status의 값을 INVALID 로 변경하고 싶을 때 다음과 같은 상황을 마주하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1740923890927&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UPDATE B SET status = 'INVALID' WHERE status = 'TEST'
UPDATE A SET status = 'INVALID' WHERE status = 'TEST'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 두 쿼리는 동시에 적용되어야지만 참조 무결성을 깨뜨리지 않고 갱신될 수 있다. 만약 이를 각각 실행하게 된다면 각 쿼리는 무결성을 해치기 때문에 에러가 발생하며 해당 작업을 수행할 수 없게 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해결 방법&lt;/h4&gt;
&lt;pre id=&quot;code_1740924298053&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE A (
  status VARCHAR(20) NOT NULL DEFAULT 'NEW'
  foreign key (status) references B(status)
  on update cascade
  on delete set default
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외래키 제약을 걸 때 cascading update 를 적용하면 위와 같은 문제에서 벗어날 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ON UPDATE CASCADE 는  B 테이블에 대한 갱신이 자동으로 A에 전파되게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 ON UPDATE, ON DELETE 를 외래키 제약에 추가하게 되면 cascading operation 이 발생했을 때 어떤 식으로 처리할 지를 직접 관여해서 설정할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;나의 생각&lt;/h4&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 진행하는 프로젝트의 스키마도 계속해서 변경되고 있는데 이번에는 외래키 제약을 걸어서 활용해 보고자 한다. 현재는 파일을 많이 다루기도 하고 orphan data 가 생기면 사용되지 않는 파일이 스토리지를 계속해서 차지하게 되는 문제가 있을 것 같다는 생각이 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 흐른 뒤에 지금 나의 선택이 프로젝트에 어떤 영향을 주었는지를 되돌아보면서 외래키에 대한 선택이 어땠고 어떤 효과를 만들어냈는지를 되돌아 볼 수 있었으면 좋겠다.&lt;/p&gt;</description>
      <author>qwebnm7788</author>
      <guid isPermaLink="true">https://qwebnm7788.tistory.com/15</guid>
      <comments>https://qwebnm7788.tistory.com/15#entry15comment</comments>
      <pubDate>Sun, 2 Mar 2025 23:08:09 +0900</pubDate>
    </item>
    <item>
      <title>AWS Lambda 살펴보기</title>
      <link>https://qwebnm7788.tistory.com/14</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;serverless 를 활용하려고 하는 환경에서 작성한 코드를 실행하려고 하는 경우 활용할 수 있는 컴퓨팅 자원 중 하나로 AWS Lambda 를 생각해볼 수 있다. 특정 시점에 빠르게 스케일업하고 그 외 사용하지 않는 시점에는 스케일 다운을 해야 하는 경우에 람다 사용을 고려해볼 수 있다. 개발자는 코드만 신경써주게 되면 그 외 메모리, CPU 와 같은 환경은 AWS에서 알아서 관리해주게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lambda Function&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 람다로 실행시키려 하면 가장 먼저 마주하는 것은 Function 이다. 람다에서의 Function 은 애플리케이션을 구현하기 위한 하나의 단위로 취급된다. 즉 특정 이벤트에 반응해 실행되는 코드의 일부 조각이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트가 발생하면 Function 에 설정해놓은 handler 함수가 실행된다. 이 때 이벤트가 포함하고 있는 데이터가 handler 함수에 전달되어 이벤트에 따라 원하는 동작을 수행할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;VPC&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 람다 함수는 AWS Lambda 서비스가 관리하는 별도 VPC 에서 실행 및 관리된다. 이는 개발자에게 노출되지 않기 때문에 개발자는 이런 부분을 신경쓰기 않고 편하게 코드에 집중할 수 있다는 장점이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 RDS, EC2 와 같은 리소스가 별도로 생성한 VPC 내에서 사용중이고 여기에 람다가 접근해야 한다면 그에 맞는 처리가 필요할 수 있다. 예를 들어 private subnet 에 존재하는 자원이라면 외부에서 접근이 불가능할 수 있는데 이 경우에는 해당 VPC에 람다를 붙혀서 사용해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;887&quot; data-origin-height=&quot;367&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ugsoq/btsMlEaZcer/ZpB9UD0ZpbrYyTARWEY2m1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ugsoq/btsMlEaZcer/ZpB9UD0ZpbrYyTARWEY2m1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ugsoq/btsMlEaZcer/ZpB9UD0ZpbrYyTARWEY2m1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fugsoq%2FbtsMlEaZcer%2FZpB9UD0ZpbrYyTARWEY2m1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;887&quot; height=&quot;367&quot; data-origin-width=&quot;887&quot; data-origin-height=&quot;367&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPC에 람다 함수를 붙히기 위해서는 네트워크 인터페이스를 생성하고 관리할 수 있는 권한을 람다에 부여해야 한다. 가장 간단한 방법은 AWS 에서 제공하는 &lt;b&gt;AWSLambdaVPCAccessExecutionRole&lt;/b&gt; 을 부여해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 오는 접근이 아닌 내부 서비스의 일부분을 처리하기 위해 람다를 사용한다면 VPC와의 통합을 잘 고려해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비동기 호출&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;람다도 다른 AWS 서비스와 마찬가지로 비동기로 호출할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 람다의 결과를 받아 바로 처리해야하는 경우가 아니라면 비동기 호출을 통해 좀 더 빠르게 요청을 처리할 수 있게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;627&quot; data-origin-height=&quot;244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhmdbU/btsMjRJKvx6/k9cZ8ztNMFOx5GdqkmKkd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhmdbU/btsMjRJKvx6/k9cZ8ztNMFOx5GdqkmKkd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhmdbU/btsMjRJKvx6/k9cZ8ztNMFOx5GdqkmKkd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhmdbU%2FbtsMjRJKvx6%2Fk9cZ8ztNMFOx5GdqkmKkd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;627&quot; height=&quot;244&quot; data-origin-width=&quot;627&quot; data-origin-height=&quot;244&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 호출이 전달되면 람다는 해당 이벤트를 event queue 에 넣고 성공 응답을 반환해준다. 이후 별도 프로세스가 동작하여 큐에서 이벤트를 읽어 Lambda function 에 전달해주고 처리하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 호출을 처리할 때 에러가 발생하는 경우를 살펴보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;람다가 event queue 에 있던 이벤트를 꺼내 함수를 호출할 때, 함수가 에러를 반환했다면 기본적으로는 1분 간격으로 retry 를 수행하게 된다. 함수가 에러를 반환하는 경우는 실행하는 코드가 에러를 반환한 경우부터, execution runtime 에서 timeout 이 발생하는 것까지 대부분의 경우를 포함한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;event queue 는 대부분의 큐 서비스와 비슷하게 eventually consistent 정책을 따른다. 이 경우 함수가 동일한 이벤트를 여러번 전달받을 수 있는 확률이 존재하게 된다. 그렇기 때문에 람다 함수의 코드를 작성하는 경우에는 중복된 이벤트를 전달받는 경우가 있다는 점을 염두해두어야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 event queue 가 너무 길어지게 되면 내부의 이벤트가 만료되어 실제로 처리되기 전에 이벤트가 제거될 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;serverless 서비스를 사용해서 구현할 때 가장 어려움을 겪는 부분 중 하나는 테스트이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 확실하게 테스트 하는 방법은 클라우드 환경에서 수행하는 것이다. 로컬 환경과 클라우드 환경에서 오는 차이가 있기 때문에 아무리 로컬 환경에서 테스트가 잘 동작하더라도 실제 클라우드 환경에 배포되었을 때는 문제가 발생할 수 있기 때문에 이런 환경을 맞춘 뒤 테스트 하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;클라우드 기반 테스트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;람다 함수를 테스트 하는 가장 간단한 방법은 AWS Management Console 에 들어가 test event JSON 을 넣고 실행하는 것이다. 나도 최근 람다 함수를 작성할 때 이 방법을 사용했는데 가장 확실하기도 하고 문제가 발생했을 때 CloudWatch 로 로그를 확인하고 수정할 수 있어서 초반 환경을 세팅할 때는 잘 활용할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 방법의 단점은 사용자가 손수 콘솔에 들어가 함수를 실행시켜야 하기 때문에 개발 속도가 느려진다는 점이다. 이 부분을 보완하기 위해서는 자동화를 수행해볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SAM CLI 를 잘 활용하면 콘솔에 접근하지 않고 동일한 동작을 로컬에서 자동으로 수행할 수 있다. 이렇게 하면 CI/CD 과정에 통합해서 빠르고 쉽게 클라우드 기반으로 테스트를 수행할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/aws-samples/serverless-test-samples&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/aws-samples/serverless-test-samples&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1739691849137&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - aws-samples/serverless-test-samples: This repository is designed to provide guidance for implementing comprehensive tes&quot; data-og-description=&quot;This repository is designed to provide guidance for implementing comprehensive test suites for serverless applications. - aws-samples/serverless-test-samples&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/aws-samples/serverless-test-samples&quot; data-og-url=&quot;https://github.com/aws-samples/serverless-test-samples&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dx9yHY/hyYfUtnMGA/kekKVEKM6WMMIo2Ur5Vrr0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/Kaecs/hyYfDrDLFA/COELwkTbzHE5m3JrjKTR81/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/aws-samples/serverless-test-samples&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/aws-samples/serverless-test-samples&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dx9yHY/hyYfUtnMGA/kekKVEKM6WMMIo2Ur5Vrr0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/Kaecs/hyYfDrDLFA/COELwkTbzHE5m3JrjKTR81/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - aws-samples/serverless-test-samples: This repository is designed to provide guidance for implementing comprehensive tes&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This repository is designed to provide guidance for implementing comprehensive test suites for serverless applications. - aws-samples/serverless-test-samples&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라우드 기반의 테스트는 다음과 같은 장/단점을 갖는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;/li&gt;
&lt;li&gt;모든 개발자가 자신의 로컬 개발 환경과 무관하게 동일한 테스트 환경을 빠르고 쉽게 구성할 수 있다.&lt;/li&gt;
&lt;li&gt;사용하는 모든 서비스에 대한 테스트가 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;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;/li&gt;
&lt;li&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;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Mock 기반 테스트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 클라우드 자원을 사용하지 않고 코드로 해당 자원의 동작을 흉내내 동작을 테스트 하는 방법을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범용적인 Mock 프레임워크 (e.g. Mockito) 를 사용하거나 AWS SDK 처럼 특정 클라우드 솔루션에 대한 mock 을 제공하는 라이브러리를 활용해서 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 보면 유닛 테스트를 작성할 때 떠올릴 수 있는 가장 간단한 방법을 그대로 적용한 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mock 을 활용한 테스트는 다음과 같은 장/단점을 갖는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라우드 기반의 테스트에서는 서비스 장애를 손수 일으키는 것은 꽤 복잡한 작업이고 때로는 다른 곳에 영향을 줄 수 있기 때문에 테스트가 어렵지만, Mock 을 활용한다면 이런 부분들을 좀 더 쉽게 테스트할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;범용 Mock 프레임워크를 사용한다면 클라우드 서비스의 신규 기능이 추가되었을 때도 이를 직접 구현할 수 있기 때문에 빠르게 대응해 테스트 해볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스의 동작을 Mocking 하는 작업에 시간이 많이 소요된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 서비스의 특정 행동에 대한 mock response 를 직접 설정해주어야 하는데 이 과정이 꽤나 귀찮고 오래걸린다.&lt;/li&gt;
&lt;li&gt;또한 개발자가 이를 담당해야 하기 때문에 관리해야 하는 영역이 넓어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Mock test 가 성공하더라도 실제 클라우드 환경에서는 그 외의 원인들로 인해 실패할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 람다를 활용해서 작업을 수행해야 하는 일이 있었는데 테스트를 하는 과정도 쉽지 않았고 응답 속도를 빠르게 하기 위해 비동기 처리를 진행했는데 내부적으로 어떻게 동작하는 가에 대한 의문점들이 많이 있었다. 공식 문서를 좀 더 주의깊게 보면 이렇게 다양한 정보를 얻을 수 있으니 앞으로는 개발하기 전에 좀 더 잘 읽어보는 습관을 들여야겠다&lt;/p&gt;</description>
      <author>qwebnm7788</author>
      <guid isPermaLink="true">https://qwebnm7788.tistory.com/14</guid>
      <comments>https://qwebnm7788.tistory.com/14#entry14comment</comments>
      <pubDate>Sun, 16 Feb 2025 17:05:31 +0900</pubDate>
    </item>
    <item>
      <title>고가용성 정리 [1]</title>
      <link>https://qwebnm7788.tistory.com/13</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서비스의 고가용성을 유지하기 위해서 대부분의 서비스에서는 여러 대의 서버를 활용하는데 하나의 서버를 활용할때와 다르게 2개 이상의 서버가 존재하는 시점부터는 트래픽을 누가 어떻게 분배할 것인가에 대한 고민이 생기게 된다.&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반적인 로드 밸런싱&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GYj2B/btsL4xc9dFv/AjkUXEKD4oMGPFBDDTK0Q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GYj2B/btsL4xc9dFv/AjkUXEKD4oMGPFBDDTK0Q1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GYj2B/btsL4xc9dFv/AjkUXEKD4oMGPFBDDTK0Q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGYj2B%2FbtsL4xc9dFv%2FAjkUXEKD4oMGPFBDDTK0Q1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;626&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 많이 사용하는 방식은 TCP 기반으로 동작하는 로드 밸런서이다. 클라이언트는 로드 밸런서의 public IP 주소로 연결 요청을 보내고 로드 밸런서는 어떤 서버 인스턴스를 사용할 지 결정한 뒤 해당 요청을 포워딩 해주게 된다. 이 때는 port, protocol 과 같은 기본적인 TCP 정보를 기반으로 대상을 선정한다. 로드 밸런서가 서버와 클라이언트 사이 통신에 적극적으로 개입하는 시점은 이렇게 첫 연결 요청 부분이고 이후에는 그냥 거쳐가는 정도로만 개입하게 된다. 컨테이너 환경 속에서의 로드 밸런싱이나 여러 클라우드 서비스에서 제공하는 로드 밸런서가 이런 방식으로 동작하고 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Global Server Load Balancing (GSLB)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;gslb.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mcOAB/btsL3q6U4AB/ZmTakG4lFXCRwV0728J7iK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mcOAB/btsL3q6U4AB/ZmTakG4lFXCRwV0728J7iK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mcOAB/btsL3q6U4AB/ZmTakG4lFXCRwV0728J7iK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmcOAB%2FbtsL3q6U4AB%2FZmTakG4lFXCRwV0728J7iK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;596&quot; data-filename=&quot;gslb.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;596&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GSLB는 좀 더 상위 레벨에서 동작하는 로드 밸런싱 방식이라고 볼 수 있다. 예를 들어 여러 데이터 센터에 서버가 존재한다면 그 중 하나를 선택해서 사용할 수 있도록 만들어 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GSLB 는 크게 보면 다음과 같은 순서로 동작한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Global Load Balancer 에 IP 주소를 물어본다.&lt;/li&gt;
&lt;li&gt;1번에서 받은 IP 주소에 요청한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 어떻게 보면 로드 밸런서 앞에 또 다른 로드 밸런서가 있는 구조라고 할 수 있다. 이런 GSLB가 필요한 규모의 서비스인 경우라면 2번 과정에서 얻은 IP 주소는 또 다른 로드 밸런서 (앞서 본 일반적인 로드밸런서의 형태) 의 것인 경우가 대부분이다. 이렇게 되면 2번 과정 이후에 일반적인 로드밸런서과 클라이언트가 통신하기 위한 과정이 연달아 일어나게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GSLB 의 실제 동작은 DNS에 의존하는 경우가 많다. 클라이언트가 특정 도메인에 요청하면 DNS resolution 과정을 거쳐 그에 해당하는 IP 주소를 반환하는 것이다. 필요하다면 어떤 IP 를 반환할 지에 서비스 특성에 맞는 알고리즘을 첨가해 더 효과적으로 트래픽을 관리할 수 있다. GSLB는 DNS에 의존적이기 때문에 이를 사용하는 시점에 클라이언트가 서버와 통신할 때 부가적인 지연시간이 추가된다는 점도 알아두면 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GSLB 를 사용하면 클라우드, 온프레미스와 같은 다양한 곳으로 트래픽을 분산시킬 수 있다는 장점이 생긴다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;L7 로드 밸런싱&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L7 로드 밸런싱은 말 그대로 Layer 7, 대부분의 경우 HTTP 정보를 기반으로 수행하는 로드 밸런싱을 말한다. 많이 사용하는 요소로는 HTTP method, Content-Type 등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 URI 정보도 활용할 수 있기 때문에 API versioning 을 URI 에 포함하고 있다면 이를 확인해서 그에 맞는 적절한 서비스로 트래픽을 포워딩 해줄 수 있다. 이렇게 하면 강제 업데이트가 불가능한 상황 속에서 좀 더 유연하게 서비스를 운영해 갈 수 있다. 예를 들어 예전 버전의 서버를 해당 버전 사용자 수에 맞춰 스케일링 할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AWS 의 Application Load Balancer&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 를 기반으로 한 인프라를 구축한 서비스라면 로드 밸런서로 ELB를 사용하게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ELB 에는 여러가지 타입이 존재하지만 가장 많이 사용하는 것은 Application Load Balancer (ALB) 와 Network Load Balancer (NLB) 이다. 둘의 가장 큰 차이는 ALB는 L7 로드 밸런싱을 하고 NLB 는 L4 로드 밸런싱을 한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ELB 는 하나 이상의 Availability Zone (AZ) 내에 미리 등록한 타깃 (EC2 instance 와 같은) 으로 트래픽을 분배해주는 역할을 담당한다. 이 때 앞서 살펴본 것과 같이 health check 를 통해 실제 서비스 가능한 타깃으로만 트래픽이 전달될 수 있도록 해서 고가용성을 유지하는데 도움을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ELB 중 ALB 타입을 가장 많이 사용하고 있기 때문에 우선 ALB에 대해 조금 더 자세히 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ALB 의 구성요소&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;555&quot; data-origin-height=&quot;243&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIGlx3/btsL3SIRImX/8EvpGLnh9fUuTxiyvrftwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIGlx3/btsL3SIRImX/8EvpGLnh9fUuTxiyvrftwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIGlx3/btsL3SIRImX/8EvpGLnh9fUuTxiyvrftwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIGlx3%2FbtsL3SIRImX%2F8EvpGLnh9fUuTxiyvrftwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;243&quot; data-origin-width=&quot;555&quot; data-origin-height=&quot;243&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ALB 에는 리스너라는 구성요소가 존재하는데 리스너는 사용하는 프로토콜과 포트가 설정되어 있어 그에 맞는 리스너를 사용할 수 있도록 하고 있다. 또한 리스너에는 규칙을 정의할 수 있는데 이 규칙에 따라 리스너에 온 요청을 어떤 타깃그룹의 타깃에 전달할지를 결정하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 말하는 타깃 그룹은 하나 이상의 타깃을 포함하고 있는 그룹을 말한다. 해당 그룹에 헬스체크를 설정해둘 수도 있는데 이렇게 되면 그룹 내의 모든 타깃의 헬스체크를 수행한 뒤 그 전체적인 결과를 취합해 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ALB 는 앞서 살펴본 로드 밸런싱 방식 중 L7 로드 밸런싱을 수행하는 로드 밸런서에 가깝다고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 리스너의 규칙에서 HTTP 와 관련된 여러가지 요소들을 활용해서 여러가지 조건들을 추가할 수 있다. 예를 들어 HTTP header 를 사용한다거나 query string 이나 cookie 를 활용할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Fail-over&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fail-over 는 primary 시스템이 불능 상태에 들어갔을 때 이를 자동 혹은 수동으로 여분의 시스템으로 전환해주는 과정을 말한다. 이 과정을 통해 시스템에 실패가 발생했을 때 다운타임을 최소화하여 고가용성을 유지하는데 도움을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fail-over 에는 크게 다음 두 가지 타입이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;active-passive&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;active 와 passive 서버가 존재하고 둘 사이에 heartbeat 를 주고 받는 패턴이다. 만약 heartbeat 이 정상적으로 전달되지 못하면 active 서버가 정상적으로 동작하지 못한다고 판단하여 passive 서버의 IP 주소가 대신해서 사용되어 서비스를 재개한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 패턴에서 생기는 다운타임은 passive 서버가 cold standby 인지 hot standby 인지에 따라 달라진다. 만약 cold standby 상태라면 서버가 새롭게 뜨고 서비스 가능 상태가 될 때까지 시간이 필요하기 때문에 좀 더 긴 다운타임이 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;active-active&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;active 상태의 두 서버를 유지하고 트래픽을 두 서버가 모두 받는 패턴이다. 이렇게 하면 트래픽을 나누어 받을 수 있다는 장점이 생긴다. 만약 두 서버가 모두 public 에 오픈된 상태라면 DNS가 두 서버의 IP 주소를 모두 알도록 해두어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 본 ELB 에서 health check 과 Auto Scaling Group 을 활용하면 fail-over 를 간단하게 구현해낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ELB 는 타깃 그룹에 대한 liveness check 를 수행해주는데 만약 응답이 정상적이지 않다면 해당 인스턴스를 서비스에서 제외하고 새로운 인스턴스를 띄워 미리 지정한 인스턴스의 수를 유지해 서비스가 정상적으로 동작할 수 있도록 해준다.&lt;/p&gt;</description>
      <author>qwebnm7788</author>
      <guid isPermaLink="true">https://qwebnm7788.tistory.com/13</guid>
      <comments>https://qwebnm7788.tistory.com/13#entry13comment</comments>
      <pubDate>Sun, 2 Feb 2025 18:56:07 +0900</pubDate>
    </item>
    <item>
      <title>SQL AntiPatterns - Primary key</title>
      <link>https://qwebnm7788.tistory.com/12</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;SQL AntiPatterns 라는 책의 내용 중 최근에 관심있게 살펴보았던 Primary Key 관련 내용을 정리해보았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Primary Key&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;primary key 는 테이블의 모든 행에 대해 유일성을 보장해 행이 중복해서 저장되는 것을 방지해주고 특정 행을 유일하게 식별하는데 사용할 수 있다. 또한 외래키에 의해 참조되는 경우도 많기 때문에 굉장히 많은 곳에서 사용되는 제약사항이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 컬럼을 PK로 사용할 지에 대한 고민을 해볼 수 있는데 우선 PK가 되기 위해서는 다음 조건을 만족해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;각 행별로 유일한 값을 갖는 컬럼이어야 한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NOT NULL 제약을 가져야 한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Natrual Key / Surrogate Key&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Key 와 관련해 알고 있어야 하는 기본 개념을 잠깐 짚고 넘어가자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;natural key (혹은 business key)&lt;/b&gt; 는 애플리케이션의 도메인에 이미 존재하는 실제 속성으로 구성된 키를 말한다. 인위적으로 값을 만들어내는 것이 아닌 비즈니스 로직에서 파생된 값으로 만들어진 키를 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 주민등록번호나 핸드폰번호 혹은 책의 ISBN 같은 값을 natural key 라고 생각할 수 있다. (물론 키로 사용할 수 있는가는 잘 판단해야 한다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;natural key 를 사용하게 되면 이미 비즈니스 상에서 의미있는 값을 사용하는 것이기 때문에 이해하기 쉽고 별도 컬럼을 정의할 필요가 없다는 장점이 생긴다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Surrogate key (혹은 pseudo key)&lt;/b&gt; 는 인위적으로 만들어낸 키 값을 말한다. 보통 데이터베이스 내에서 시스템이 생성해낸 값을 사용하게 된다. 정수값 혹은 UUID 를 사용하며 auto-increment 와 같은 기능을 통해 사용할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&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;/li&gt;
&lt;li&gt;시간이 지나도 발생되는 방식이나 기존 값에 변화가 생기지 않기 때문에 안정적이라고 할 수 있다.&lt;/li&gt;
&lt;li&gt;정수형태의 surrogate key 를 사용하게 되면 natural key 에 비해 효율적이기 때문에 성능상에 이득이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&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;/li&gt;
&lt;li&gt;테이블 간의 연관관계를 정의할 때 불필요한 복잡도를 높일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;이제 다시 돌아와서 어떤 컬럼을 primary key 로 사용하는 것이 좋을까?&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;나의 경우에는 사실 딱히 primary key 에 대해 크게 생각해본 경우가 별로 없다. 그냥 테이블에 반드시 있어야 하고 &lt;b&gt;id&lt;/b&gt; 라는 이름으로 AUTO_INCREMENT 를 하면 되지 않을까? 하고 사용했던 것 같다. 그냥 종종 32-bit 을 사용할지 64-bit 을 사용할지 고민했던 정도.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 대부분의 경우에는 이게 잘 동작할 수 있지만 다음과 같은 경우에는 조금 더 생각해 볼 만 해진다.&lt;/p&gt;
&lt;pre id=&quot;code_1737269214386&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE Article (
    id INT PRIMARY KEY,
    article_id VARCHAR(10) UNIQUE,
    content VARCHAR(1000),
    ...
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 테이블에서는 &lt;b&gt;article_id&lt;/b&gt; 컬럼에 unique constraint 가 이미 걸려져 있다. 앞서 살펴본 primary key 의 조건은 &lt;b&gt;&lt;u&gt;1. 유일한 값을 가져야 하고&lt;/u&gt;&lt;/b&gt; &lt;u&gt;&lt;b&gt;2. NOT NULL 제약을 가져야 한다&lt;/b&gt;&lt;/u&gt; 였는데 1번 조건을 만족하고 있다. 물론 한가지 주의해야 할 점은 UNIQUE constraint 이 NULL 값에 대해서는 중복을 허용한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 해당 컬럼에 NOT NULL 조건이 추가될 수 있다면 사실 &lt;b&gt;id&lt;/b&gt; 컬럼을 완전히 대체할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 비슷한 상황이 자주 발생하는 경우는 테이블 간에 &lt;u&gt;many-to-many 연관관계&lt;/u&gt;가 있는 경우이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Article 테이블과 Category 테이블이 서로 many-to-many 관계를 갖는다고 생각해보자. (하나의 Article 의 여러 Category 에속할 수도 있고, 하나의 Category 에 여러 Article 이 속할 수 있다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 관계의 매핑을 위해 다음과 같은 테이블이 존재한다고 해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1737269871439&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE CategoryArticles (
    id INT PRIMARY KEY,
    category_id INT NOT NULL,
    article_id INT NOT NULL,
    FOREIGN KEY (category_id) REFERENCES ON Category(category_id),
    FOREIGN KEY (article_id) REFERENCES ON Article(article_id)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 (category_id, article_id) 조합이 이 테이블에 한번만 등장하도록 만들고 싶을 때 어떻게 해야할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &lt;b&gt;id&lt;/b&gt; primary key 만으로는 이 요구사항을 만족시킬 수 없다. 왜냐면 우리가 중복을 막고 싶어하는 컬럼에 대한 어떤 유일성 보장을 해주지 않기 때문이다. 가장 간단한 방법은 그냥 (category_id, article_id) 에 대해 UNIQUE constraint 를 추가하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 여기서 앞에서 본 상황이 다시 발생한다. &lt;u&gt;&lt;b&gt;1. 유일한 값을 갖고&lt;/b&gt;&lt;/u&gt;, &lt;u&gt;&lt;b&gt;2. NOT NULL 제약을 갖는&lt;/b&gt;&lt;/u&gt; 키가 등장한다. 이번에는 두 컬럼이 각각 NOT NULL 제약을 갖기 때문에 &lt;u&gt;1, 2번 조건을 모두 만족&lt;/u&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우라면 더더욱 id 컬럼의 존재 이유가 사라지고 (category_id, article_id) compound key 가 primary key 로 사용할 수 있는 상황이 만들어진다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Primary Key의 이름 짓기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래밍에서 변수나 클래스의 이름 더 나아가 특정 서비스의 이름을 잘 짓는 것은 전체적인 서비스의 이해도를 높히거나 복잡도를 줄이는 데 꽤 큰 영향을 준다. 그렇다면 &lt;b&gt;id&lt;/b&gt; 라는 이름은 어떨까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 &lt;b&gt;id&lt;/b&gt; 라는 이름이 프로그래밍쪽에서 꽤 다양한 목적으로 사용되기도 하고 이름 자체 만으로는 그냥 유일한 값이라는 의미만을 추론할 수 있기 때문에 그 외의 부가적인 정보를 주진 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리를 짜다 보면 종종 다음과 같이 id 컬럼이 겹쳐서 alias 를 사용하게 되는 경우가 생긴다.&lt;/p&gt;
&lt;pre id=&quot;code_1737270344440&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT a.id, c.id
FROM Article a
JOIN Category c ON (c.article_id = a.id);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭐 물론 그냥 이렇게 겹칠때 alias 로 회피해 줄 수도 있지만 (그리고 약간 취향의 문제인 것 같기도 하지만) 좀 더 이해하기 쉬운 이름을 부여할 수도 있다. 가장 쉽게는 해당 키가 표현하는 엔티티의 타입을 담고 있을 수 있다. Article 엔티티에 대한 키라면 &lt;b&gt;article_id&lt;/b&gt; 와 같이 말이다. 그리고 외래 키로 해당 값을 참조해야 할 때 동일한 이름을 사용해주면 좀 더 직관적으로 쿼리를 작성할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1737270522225&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE CategoryArticles (
    category_id INT NOT NULL,
    article_id INT NOT NULL,
    PRIMARY KEY (category_id, article_id),
    FOREIGN KEY (category_id) REFERENCES Category(category_id),
    FOREIGN KEY (article_id) REFERENCES Article(article_id)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Primary key .. 꼭 필요한거야?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가끔가다 PK 가 테이블에 필요 없어 보이는 경우가 있다. 우선 지금까지 확인한 PK가 필요한 이유는 다음과 같다.&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;li&gt;쿼리를 통해 개별 행을 가져와야 하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나 굳이 이유를 추가해본다면 database replication 을 생각해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;replication 방식에는 여러가지가 있겠지만 primary-secondary replication 에서는 모든 쓰기 요청이 primary node 에 간 뒤 이를 secondary node에 전파하는 방식으로 동작한다. 이 때 log 를 통해 row 에 가해진 변경점을 적용하게 되는데 여기서 primary key 를 활용하게 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;row 에 변경이 생겼을 때 (insert, update, delete 등으로 인해) 이는 replication log 에 영향을 준 행의 primary key 와 함께 기록되게 된다.&lt;/li&gt;
&lt;li&gt;primary key 의 유일성 특성을 활용해서 replication 시 특정 행을 정확하게 식별해 모든 replica 에 이를 전파할 수 있게 된다. 또한 이 과정에서 conflict 가 발생하지 않았다는 점도 primary key 사용을 통해 쉽게 알 수 있게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 primary key 없이 replication 을 진행해야 한다면 시스템이 어떤 데이터를 유일하게 식별하는데 어려움을 겪을 수도 있고 데이터가 중복되었다는 사실을 곧바로 인지하기 어려울 수도 있다. 또한 항상 인덱스가 걸려있는 컬럼을 사용하지 않을 수 있기 때문에 성능 측면에서도 차이가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블의 데이터를 사용하면서 위와 같은 요구사항이 없는 경우는 사실 거의 없다. 또한 이런 목적이 과한 상황이라고 하더라도 이를 대체하기 위한 작업 자체가 매우 복잡해진다. 예를 들어 중복 행을 방지하는 것을 직접 수행할 수도 있는데 이렇게 되면 복잡도가 급격하게 커진다. 그렇기 때문에 primary key 가 불필요해 보인다면 정말 그런 상황이 맞는지 제대로 고민해야 할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;INT vs BIGINT&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 잠깐 고민했던 문제이다. &lt;b&gt;id&lt;/b&gt; 컬럼을 만들 때 타입을 INT 로 하는게 좋을까 BIGINT 로 하는게 좋을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 BIGINT 를 고민하는 이유는 AUTO_INCREMENT 를 걸어두었을 때 최대값에 도달하는 경우를 방지하고자 하는 목적이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 이 값이 최대값에 도달하면 어떻게 되는걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 얼마나 많은 시간이 흘러야 최대값에 도달할 수 있을지 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;signed 32-bit integer 를 기준으로 한다면 최대값은 2^31 - 1 으로 분당 10,000 row 가 추가되는 환경이라면 약 150일 정도가 소요된다고 한다. 어림잡아 반년이 지나기전에 모든 값을 소모한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;signed -&amp;gt; unsigned 로 변경하면 1개의 비트를 추가로 사용할 수 있으므로 2배 더 많은 값을 사용하여 약 300일 정도로 1년이 좀 안되는 시간이 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;64-bit integer 를 생각해보면 이는 32-bit integer 의 두배가 아니라 무려 2^32 배 더 많은 시간이 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 위와 똑같이 계산해보면 다쓰기 위해서는 약 1,754,827,252 년이 걸린다고 한다. 우리의 애플리케이션이 저 시간동안 동작할 일이 있을까? gpt에게 물어봐도 아직 실제로 일어난 사례는 없어보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이게 실제로 발생한다 하더라도 sharding 이나 UUID 등을 사용해서 대응할 방법이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 중 어떤 데이터 타입을 선택할 것인가는 각자가 만들고자 하는 애플리케이션의 상황에 따라 판단하는 것이 좋다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&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;/li&gt;
&lt;li&gt;저장소 요구사항
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;INT 는 4byte, BIGINT 는 8 byte 를 사용하기 때문에 저장소 용량이 두 배 차이가 난다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;성능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2번과 비슷하게 index 역시 INT 타입이 더 작게 유지되기 때문에 index 연산 또한 BIGINT 보다 약간은 더 빠르다고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 경우라면 INT 를 선택하는 것이 좋다. 테이블의 row 가 INT 의 최대치 근처에도 도달하지 못할 것이라면 사실 굳이 BIGINT 를 선택할 이유가 없어보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블을 생성할 때 가장 기본이 되는 Primary Key 에 관련된 몇 가지 고민거리를 살펴보았다. 어떻게 보면 당연한 이야기인 것 같지만 종종 의문을 갖게 되는 부분들이 있는데 한번쯤은 생각해 볼 수 있는 시간을 가지면 좋을 것 같다.&lt;/p&gt;</description>
      <author>qwebnm7788</author>
      <guid isPermaLink="true">https://qwebnm7788.tistory.com/12</guid>
      <comments>https://qwebnm7788.tistory.com/12#entry12comment</comments>
      <pubDate>Sun, 19 Jan 2025 18:03:46 +0900</pubDate>
    </item>
    <item>
      <title>2024년 회고</title>
      <link>https://qwebnm7788.tistory.com/11</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;유난히 시간이 빨리 지나간 것 같은 2024년이 지나고 새해가 밝았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회고를 매년 쓰는 건 아니지만 올해는 그냥 한번 써보고 싶어서 1년동안 가장 기억에 남았던 것들을 주제별로 뽑아서 기록해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;올해의 책&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;헤일메리.png&quot; data-origin-width=&quot;837&quot; data-origin-height=&quot;1200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W92Tr/btsLCtwB2BS/nh3n9kd2Cy1ReYbKnWmgN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W92Tr/btsLCtwB2BS/nh3n9kd2Cy1ReYbKnWmgN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W92Tr/btsLCtwB2BS/nh3n9kd2Cy1ReYbKnWmgN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FW92Tr%2FbtsLCtwB2BS%2Fnh3n9kd2Cy1ReYbKnWmgN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;327&quot; height=&quot;469&quot; data-filename=&quot;헤일메리.png&quot; data-origin-width=&quot;837&quot; data-origin-height=&quot;1200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년은 책을 많이 읽었다고 하기도 어렵고 읽은 책들도 대부분 재테크 관련된 주제가 대부분이었다. 그 중에서 몇 안되는 소설책이었던 &lt;u&gt;&lt;i&gt;&lt;b&gt;프로젝트 헤일메리&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;를 올해의 책으로 뽑아보았다. 내 죽어가는 상상력을 잘 자극해주기 때문에 SF 장르를 좋아하는 편인데 근래 본 SF관련 콘텐츠 중에 가장 흥미로운 내용이었던 것 같다. 스포가 될 수 있으니 대략적으로만 말해보자면 만약 지구에 사는 사람들이 우주에 나가야만 한다면 이런 이유지 않을까? 하는 것과 우주에 나가면 이런 일이 생길수도 있지 않을까? 라고 생각했던 내용이 그대로 책의 내용으로 옮겨져서 머릿속에서 그 모습을 상상하면서 봤던 것 같다. 출퇴근 시간에 책을 주로 읽는 편인데 이 책을 보는 동안은 지하철을 내려야하는게 아쉬웠을 정도랄까.. 영화로도 출시될 예정이라고 하니 개봉하면 내가 상상했던 모습과 얼마나 비슷할지 꼭 확인해봐야겠다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;올해 기억에 남는 여행&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-01-05-14-24-59 002.jpeg&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;1411&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pBMCV/btsLEgvSuHe/0kNL9LUqKBS5YROFqFWBl0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pBMCV/btsLEgvSuHe/0kNL9LUqKBS5YROFqFWBl0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pBMCV/btsLEgvSuHe/0kNL9LUqKBS5YROFqFWBl0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpBMCV%2FbtsLEgvSuHe%2F0kNL9LUqKBS5YROFqFWBl0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;426&quot; height=&quot;568&quot; data-filename=&quot;KakaoTalk_Photo_2025-01-05-14-24-59 002.jpeg&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;1411&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크게 굴곡없는 삶을 지향하는 나에게 인생에 특별한 일이 별로 일어나지 않지만 그나마 새로운 경험을 한다면 그건 바로 여행이다. 24년에도 여행을 많이 간 건 아니지만 도쿄에도 가보고 제주도도 가고 나름 비행기를 두번이나 타게 되었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 중에서 가장 기억에 남는 건 친구들과 갔던 속초 여행이다. 예전에 외국 여행도 좋지만 우리나라에도 다양한 지역이 있기 때문에 한번씩은 가볼만하다는 이야기를 들었는데 그래서 한번도 가보지 않았던 속초에 가보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_KakaoTalk_Photo_2025-01-05-14-24-59 001.jpeg&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjEjCw/btsLE2Yb7bG/kmK2r72VJyhxhjCQiQU7nK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjEjCw/btsLE2Yb7bG/kmK2r72VJyhxhjCQiQU7nK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjEjCw/btsLE2Yb7bG/kmK2r72VJyhxhjCQiQU7nK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjEjCw%2FbtsLE2Yb7bG%2FkmK2r72VJyhxhjCQiQU7nK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;446&quot; height=&quot;595&quot; data-filename=&quot;edited_KakaoTalk_Photo_2025-01-05-14-24-59 001.jpeg&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;속초로 떠나는 날 내가 사는 지역에는 역대급 폭설이 내렸는데 막상 속초에 도착하니 눈 하나 오지 않은 매우 맑은 날씨여서 뭔가 다른 나라에 온 것 같은 느낌이 들었다. 이번 여행이 기억에 남는 이유는 가서 먹었던 음식들이 전부 다 맛있었기 때문이 아닐까 싶다. 그 중 내 원픽은 감자옹심이였는데 추운 날씨에 가서 먹어서 그랬는지 태어나서 처음 먹어본 음식이었는데 진짜 너무너무 맛있었다. 지금 내 기억에 남는 걸 기준으로 한다면 24년도 제일 맛있었던 한 끼 였지 않을까? (또 먹고 싶다..)&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;올해 처음 해본 일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무것도 새로울 게 없다고 생각했던 한 해였는데 되돌아보니 나름 처음으로 시도해본 일들이 좀 있어서 정리하면서 좀 놀랐다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;전화영어&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 개인적으로 영어가 굉장히 중요하다고 생각한다. 종종 사람들이 AI가 발전해서 이제 번역이 자동으로 될 테니 따로 안배워도 된다고 하는 경우도 있는데 나는 생각이 좀 다르다. 오히려 영어가 더 중요해진게 아닐까? 암튼 나도 정통 한국 교육 커리큘럼을 밟아서 어릴때부터 엄청나게 긴 시간동안 영어 교육에 노출되어왔지만 말은 하나도 할 줄 모르는 상태였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;24년도 시작과 동시에 또 다시 영어 정복 욕구가 솟구쳐서 생각해낸게 전화영어였다. 동네에 있는 대부분의 영어학원들은 토익이나 오픽과 같은 시험 점수를 위한 수업이 대부분이어서 회화 위주의 수업을 원했던 나에겐 전화영어가 잘 맞고 가성비나 시간적인 측면에서도 괜찮았다고 판단했다. 단 하나 망설였던 점은 외국인과 30분동안 영어로 떠들 수 있을까? 라는 두려움..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2025-01-04 at 11.52.41 PM.png&quot; data-origin-width=&quot;1330&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/be2m5m/btsLEhO5kIK/P4w72rkjz98KMAlV1qAg6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/be2m5m/btsLEhO5kIK/P4w72rkjz98KMAlV1qAg6k/img.png&quot; data-alt=&quot;레벨 테스트를 받으면 이렇게 결과를 받을 수 있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/be2m5m/btsLEhO5kIK/P4w72rkjz98KMAlV1qAg6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbe2m5m%2FbtsLEhO5kIK%2FP4w72rkjz98KMAlV1qAg6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;645&quot; height=&quot;258&quot; data-filename=&quot;Screenshot 2025-01-04 at 11.52.41 PM.png&quot; data-origin-width=&quot;1330&quot; data-origin-height=&quot;532&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;레벨 테스트를 받으면 이렇게 결과를 받을 수 있다&lt;/figcaption&gt;
&lt;/figure&gt;
&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;a href=&quot;https://www.jimenglish.co.kr/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;짐잉글리시&lt;/a&gt;라는 곳을 선정해서 지금까지 거의 반년정도 계속해서 수업을 받아오고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 영어 실력이 많이 늘었어? 라고 묻는다면 솔직히 아니라고 대답하겠지만 그래도 영어로 대화는 커녕 들을 기회조차 거의 없는 나에게 하루 30분 영어로 대화하는 시간이 영어에 대한 두려움을 많이 없애주었다는 생각이 든다. 그래서 앞으로도 계속해서 해볼 예정&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;크로스핏&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 내가 크로스핏을 등록해서 하고 있을줄은 상상도 못했다. 예전에 친구들과 함께 체험을 해본 적이 있었는데 그 때 너무너무 힘들었고 심지어 끝나고 다리에 쥐가 나서 너무 고통스러웠던 기억만 남겼었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소에 운동을 아예 안하는건 아니지만 헬스장을 가서 열심히 하진않고 그냥 출석도장을 찍는 정도만 했기 때문에 날이갈수록 체력이 떨어진다는 느낌이 들었는데 어떻게 해야 좀 강제로 운동을 열심히 할 수 있을까 고민하다 주변에 크로스핏을 하는 지인이 추천해줘서 기억이 희미해지기도 했고 용기를 좀 내서 등록하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 집 주변 박스는 초보자분들이 많아서 천천히 강도를 조절하면서 잘 따라갈 수 있었다. 물론 그럼에도 너무 힘들어서 처음 3개월 정도는 일주일에 1~2번 밖에 가질 못했다. 근육통이 너무 심해서 다음날은 운동을 아예 할 수가 없었다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크로스핏을 시작한지 이제 반년이 조금 넘은 것 같은데 여전히 운동이 힘들지만 그래도 어느정도 적응이 된 것 같기도 하구.. 헬스장을 1년 넘게 다닌 것보다 여기서의 반년이 더 큰 효과가 있는 것 같아서 꾸준히 계속 다녀보려고 생각중이다. 부상의 위험이 있는 편인 것 같아서 다치지 않게 조심해서 해야될 것 같긴 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;올해의 소비&lt;/h4&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;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3840&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjcAW5/btsLFgBUPJ7/oXLyKXnAiGw77lFVXzxKfk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjcAW5/btsLFgBUPJ7/oXLyKXnAiGw77lFVXzxKfk/img.jpg&quot; data-alt=&quot;내가 구매한 로보락 로봇청소기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjcAW5/btsLFgBUPJ7/oXLyKXnAiGw77lFVXzxKfk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjcAW5%2FbtsLFgBUPJ7%2FoXLyKXnAiGw77lFVXzxKfk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;666&quot; height=&quot;375&quot; data-origin-width=&quot;3840&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;내가 구매한 로보락 로봇청소기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주변에서 로봇청소기, 건조기, 식기세척기가 삶의 질 향상에 큰 도움이 된다길래 제일 고민이었던 청소를 위해 큰맘먹고 비싼돈이지만 로봇청소기를 구매했다. 처음 일주일을 써본 뒤 &quot;아 이걸 왜 이제 샀지?&quot; 라는 생각이 들 정도로 너무나 만족스러웠다. 퇴근 후 집에 돌아왔을 때 바닥에 먼지가 없는 그 뽀송뽀송한 느낌이 너무 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가격대가 조금 있는 편이지만 청소에 쓰는 시간이나 그로인해 생기는 스트레스에서 조금은 벗어날 수 있게 해줬기 때문에 충분히 투자할만한 가치가 있다고 생각했다. 누군가 고민중이라면 무조건 사라고 말해주고 싶다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2025년 목표&lt;/h3&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;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;수영&lt;/b&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;/li&gt;
&lt;li&gt;&lt;b&gt;금융소득 만들기&lt;/b&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;/li&gt;
&lt;li&gt;&lt;b&gt;개발&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자니깐 사실 회사에 가면 주5일 프로그램 작성은 많이 하고 있는데 왜 또 개발을 여기에 썼냐면 문득 내가 가진 능력을 회사를 위해서만 썼지 나를 위해 써본적이 없다는 생각이 들었기 때문이다. 종종 사람들 중에 자기가 쓸 프로그램을 직접 만들어서 잘 쓰는 걸 보면 대단하다고 느끼곤 했는데 올해는 나도 내 능력을 조금 나를 위해 사용해보는 건 어떨까 싶어 올해 목표에 추가해보았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;되돌아보니 여기에 적지 않은 일들도 포함해서 꽤나 많은 일들이 있었던 한 해 였던 것 같다. 사실 목표를 정하고 실천해서 결과를 얻는 것도 중요하지만 건강하고 행복하게 사는게 0순위가 아닐까 싶긴하다. 작년부터 국가적으로도 내 개인적으로도 많은 일들이 일어나고 있는데 잘 이겨내고 그냥 다 행복하게 살았으면 좋겠다. 2025년은 나 자신도 잘 챙기고 내 주변사람들은 더 잘 챙기는 그런 1년이 되었으면 좋겠다.&lt;/p&gt;</description>
      <author>qwebnm7788</author>
      <guid isPermaLink="true">https://qwebnm7788.tistory.com/11</guid>
      <comments>https://qwebnm7788.tistory.com/11#entry11comment</comments>
      <pubDate>Sun, 5 Jan 2025 14:52:36 +0900</pubDate>
    </item>
    <item>
      <title>OAuth 2.0 개념정리 (1)</title>
      <link>https://qwebnm7788.tistory.com/10</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;용어 정리&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 OAuth 에서 자주 등장하는 용어에 대해서 간단하게 정리를 해본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Confidential Client&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client_secret 값을 기밀로 유지할 수 있는 클라이언트를 말한다. 다른 사람들이 소스코드에 접근할 수 없는 서버사이드의 애플리케이션을 지칭하는 경우가 대부분이다. private github 과 같은 소스코드 저장소를 사용하면 외부에 코드 자체를 노출할 일도 없고 실제 애플리케이션의 산출물 또한 클라우드 환경이나 온프레미스 머신에서 구동되기 때문에 외부 사용자가 접근할 일은 더욱 더 적다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Public Client&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 본 Confidential Client 와는 반대로 client_secret 값을 기밀로 유지할 수 없는 클라이언트를 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모바일 애플리케이션이나 Javascript 앱이 여기에 속한다. 마찬가지로 private github 에 저장하면 소스코드 형상 자체에는 접근하기 여럽지만 모바일 앱의 경우 산출물로 나온 바이너리를 디컴파일하면 난독화가 되어 있더라도 String 값을 어느정도 식별할 수 있고, Javascript 앱의 경우 크롬 개발자 도구를 사용하는 것과 같이 현재 실행중인 앱의 소스코드를 쉽게 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 클라이언트 앱의 경우에는 client_secret 을 사용하지 않는 것이 보안상 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Authorization code&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버사이드 애플리케이션을 사용한 OAuth Flow 에서 사용되는 중간 토큰을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 과정 이후에 클라이언트에게 반환되고 클라이언트는 이를 사용해서 엑세스 토큰을 발급받아서 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0에는 네 가지의 서로 다른 역할이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Resource owner&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0 에서는 사용자를 &quot;resource owner&quot; 라고 칭한다. 왜냐면 사용자의 정보가 리소스가 되기 때문이라고 생각된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 자신의 계정에 대해 일부 권한을 애플리케이션에 주려고 시도한다. 사용자를 대신해서 이런 리소스(사용자의 정보나 권한)에 접근하고자 하는 시스템은 해당 리소스에 접근하기 전에 반드시 먼저 해당 리소스에 대한 권한을 얻어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Resource server&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자(resource owner)에 대한 정보를 담고 있는 서버를 말한다. 어떤 시스템이 특정 사용자에 대한 정보를 얻고자 한다면 사용자에게 접근하는 것이 아닌 resource server 에 요청하여 얻어와야 한다. 서버는 전달받은 엑세스 토큰을 검증하여 사용자가 리소스에 대한 접근을 허용했는지를 확인한 뒤 허용되었다면 요청한 정보를 반환해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Authorization Server&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 사용자의 계정정보에 대한 접근을 요청할 때 대응하는 서버를 말한다. OAuth prompt 를 띄워주는 서버가 바로 이 서버가 된다. 사용자가 권한을 승인하면 그에 맞는 엑세스 토큰을 발급해주는 역할을 담당한다.&amp;nbsp; Resource server 랑 통합해서 하나의 서버로 운영하는 경우도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Client&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자를 대신해 리소스에 접근하고자 하는 애플리케이션을 말한다. 모바일/웹 애플리케이션이 될 수도 있고 서버사이드 애플리케이션이 될 수도 있다. 사용자를 authorization server로 이동시키거나 애플리케이션이 직접 서버에 접근해서 권한을 받아낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 Role 이 전체적인 OAuth 흐름에서 어떤 부분을 담당하는지는 다음 그림을 참고해보면 좋다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;493&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0Vade/btsLtuUNxPm/p1OFpySFPNaqiNfzan9gMK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0Vade/btsLtuUNxPm/p1OFpySFPNaqiNfzan9gMK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0Vade/btsLtuUNxPm/p1OFpySFPNaqiNfzan9gMK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0Vade%2FbtsLtuUNxPm%2Fp1OFpySFPNaqiNfzan9gMK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;790&quot; height=&quot;493&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;493&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OAuth 2.0 흐름 분석&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OAuth 프로젝트 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 를 연동한다고 했을 때 가장 먼저 해야하는 일은 우선 우리가 사용할 애플리케이션을 생성해서 등록하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Google OAuth 를 연동할 때는 Google API Console 에 들어가서 프로젝트를 생성해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 애플리케이션의 이름, 로고, 웹사이트 등 정보를 입력하고 프로젝트를 생성하는데 생성이 완료되면 client_id 를 발급받게 된다. 경우에 따라서는 client_secret 을 함께 주는 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 생성할 때 가장 중요한 부분은 하나 이상의 redirect URL 을 등록하는 부분이다. OAuth 서비스가 애플리케이션에 대한 인증을 완료한 뒤에 사용자를 어디로 리다이렉션 시켜줄 것인가를 미리 등록하는 것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;공격자가 redirection attack 을 시도하는 것을 막기 위해 미리 등록된 URL로만 이동하도록 만드는 것이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그리고 인증 과정에서 authorization code 가 탈취되는 것을 방지하기 위해서 이러한 redirect URL은 https 를 사용하는 엔드포인트만 사용이 가능하다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 중에는 OAuth 흐름을 다양한 경로에서 시작해야 하는 경우도 있다. 예를 들어 모바일 애플리케이션과 웹 애플리케이션 양쪽에서 모두 시도해야 하는 경우가 있을 수 있다. 이 경우에는 redirect URL 을 각각에 맞게 여러개를 등록할 수도 있지만, 이 때에는 state 파라미터를 활용하는 것이 더 적합하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;state 파라미터는 애플리케이션의 &quot;상태&quot; 를 인코딩해서 전달하는 역할을 담당한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;초기 인증 요청에 state 파라미터가 함께 전달되면 인증이 완료된 후에 사용자에게 반환될 때 이 값이 그대로 클라이언트에게 전달된다. 클라이언트는 이 값을 사용해서 인증 이후의 흐름에 활용하면 된다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버 사이드 클라이언트 연동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 개발을 하는 경우에는 서버사이드의 클라이언트를 사용해서 OAuth 를 연동하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 용어정리에서 보았듯이 서버사이드 애플리케이션은 client_secret 를 기밀로 보관할 수 있는 confidential client 로 분류된다. 따라서 client_secret 을 활용하는 다음과 같은 흐름으로 진행되게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;oauth.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRCLSB/btsLrbhX4WL/kPKoxetFBkm9n3xegkdCg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRCLSB/btsLrbhX4WL/kPKoxetFBkm9n3xegkdCg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRCLSB/btsLrbhX4WL/kPKoxetFBkm9n3xegkdCg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRCLSB%2FbtsLrbhX4WL%2FkPKoxetFBkm9n3xegkdCg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;230&quot; data-filename=&quot;oauth.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림에서는 example-app.com 이 우리가 구현하고자 하는 서버사이드 클라이언트를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림을 통해 알 수 있듯이 사용자는 resource server 에 직접 접근하지 않고 우리가 구현한 클라이언트를 통해서만 접근하게 된다. 즉 클라리언트에 자신의 계정 정보에 대한 접근 권한을 부여해서 자신을 대신해서 사용할 수 있도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 사이드 클라이언트는 authorization code 라는 grant type 을 사용한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;authorization code 란 클라이언트가 엑세스 토큰으로 교환할 수 있는 중간 단계의 임시 코드를 말한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 과정을 순서대로 살펴보면 다음과 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 사용자가 인증 요청에 대한 승인 혹은 거절을 수행할 수 있도록 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 로그인 버튼에 링크를 만들어서 사용자가 이를 클릭하면 인증을 수행할 수 있는 페이지로 이동할 수 있는 링크의 형태로 만든다. 가장 많이 사용하는 형식의 링크는 다음과 같은 형태를 띈다.&lt;/p&gt;
&lt;pre id=&quot;code_1734849716292&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;https://authorization-server.com/oauth/authorize
?client_id=a17c21ed
&amp;amp;response_type=code
&amp;amp;state=5ca75bd30
&amp;amp;redirect_uri=https%3A%2F%2Fexample-app.com%2Fauth
&amp;amp;scope=photos&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;i&gt;&lt;b&gt;response_type = code&lt;/b&gt;&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;응답으로 authorization code 를 받겠다는 의미이다. 서버사이드 클라이언트가 활용할 수 있는 방식을 말한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;client_id = a17c21ed&lt;/b&gt;&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OAuth 프로젝트를 등록하면 받는 애플리케이션의 식별자이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;redirect_url = https%3A%2F%2Fexample-app.com%2Fauth&lt;/b&gt;&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트 생성 시 등록했던 redirect URL 을 전달한다. 등록했던 URL과 동일한 값을 넣어야 한다. 이 값을 넣는 것은 선택사항이지만 넣는 것이 권장된다고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;scope = photos&lt;/b&gt;&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;space-separated 형태로 하나 이상의 scope 를 전달한다. 이는 어디까지의 접근을 허용할지를 설정하는데 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;state = 5ca75bd30&lt;/b&gt;&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리다이렉트 될 때 전달받을 값으로써 사용하거나 인증 후에 수행해야 할 액션에 필요한 정보를 넣어주는데 사용한다. 요청마다 랜덤한 값을 넣어 전달한다면 CSRF protection 의 목적으로도 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 사용자가 인증 페이지에 도달하면 해당 요청에 대한 설명을 해준다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 애플리케이션이 사용자의 어떠한 정보에 대한 접근 권한을 요청했는가에 대한 설명을 해주고 이를 승인 할 것인지 거절 할 것인지를 선택하도록 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 사용자가 승인하면 이미 등록해두었던 redirect URL 로 사용자를 리다이렉션 시켜준다. 이 때 authorization_code 를 포함해서 전달해주고 만약 존재한다면 요청할 때 함께 전달했던 state 파라미터의 값도 포함해서 전달해준다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;만약 요청을 거절한다면 어떻게 될까?&lt;br /&gt;이 경우에도 사용자를 등록된 redirect URL 로 리다이렉트 해주지만 authorization code 를 포함하는 대신 query-string 으로 error=access_denied 를 포함한 채로 이동시킨다.&lt;br /&gt;애플리케이션은 이를 보고 사용자가 요청을 거절했음을 인지하게 되고 이에 대한 알맞은 처리를 수행하면 된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. authorization code 를 access token 으로 교환&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션은 token endpoint 에 POST 요청을 보내서 access token 을 생성해 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;i&gt;&lt;b&gt;grant_type&lt;/b&gt;&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서는 authorization code 를 사용해 인증했음을 알려주기 위해 이를 authorization_code 로 설정한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;code&lt;/b&gt;&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증 완료 후 authorization server 에게서 받은 authorization code 의 값으로 설정한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;redirect_uri&lt;/b&gt;&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최초 인증 요청에 설정했던 redirect url 과 같은 값으로 설정해주면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 생성 요청시에는 보통 Client Authenticate 가 필요하다. 보통은 HTTP Basic Auth 를 사용하는 경우가 많긴 하지만 client_id 와 client_secret 을 POST body 에 포함하는 것으로 대체하는 경우도 있다. 이는 자신이 사용하고자 하는 플랫폼의 OAuth spec 을 확인해서 그에 맞게 구현해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. access token 을 사용해 리소스 획득&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번 과정을 통해 얻은 access token 을 사용해서 resource server 에 요청을 보내면 이제 획득한 권한에 대응되는 리소스를 획득할 수 있게 된다. 이를 사용해서 클라이언트의 로직을 구현해주면 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;엑세스 토큰&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 직접 authorization server 를 구현하는 경우거나 JWT 를 사용해서 애플리케이션만의 토큰을 생성하고자 하는 경우라면 엑세스토큰의 유효기간을 설정해야한다. 이 때 유효기간을 얼마나 길게 설정하느냐는 정답이 있는 문제라기보단 각자가 처한 환경에 맞추어 설정하는 것이 좋다. 크게 보면 다음 두 가지 형태로 구현해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;short-lived access token + long-lived refresh token&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔하게 사용되는 방식으로 엑세스 토큰을 사용하는 방식에서는 가장 보안이 좋으면서 유연한 방법이라고도 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 엑세스 토큰을 직접적으로 만료시킬 수 있는 방법이 없기 때문에 (별도로 DB에 저장하고 있지 않는 이상) 유효기간을 최대한 짧게 설정하고 만료가되면 refresh 를 자주 하도록 만들어 중간 중간 revoke 할 수 있는 기회를 제공하는 형태가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엑세스 토큰의 누출에서 오는 위험을 줄이고자 할 때 많이 사용하곤 하는데 개발 단계에서는 refresh 로직을 직접 작성하는 것이 귀찮기도 하고 매번 로그인을 새로해야 하기 때문에 유효기간을 늘려서 사용하는 경우가 많이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;short-lived access token + no refresh token&lt;/b&gt;&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 일정 시간이 지나면 사용자에게 재인증을 요구함으로써 엑세스 토큰이 탈취당했을 경우 발생하는 위험을 유효기간이내로 제한시키는 효과를 얻을 수 있다.&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>
      <author>qwebnm7788</author>
      <guid isPermaLink="true">https://qwebnm7788.tistory.com/10</guid>
      <comments>https://qwebnm7788.tistory.com/10#entry10comment</comments>
      <pubDate>Sun, 22 Dec 2024 16:27:06 +0900</pubDate>
    </item>
    <item>
      <title>Kotlin X JPA 사용해보기</title>
      <link>https://qwebnm7788.tistory.com/9</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 는 Java Persistence API 라는 이름에서도 알 수 있듯이 Java 에서 Persistence layer 에 사용하는 표준 API 를 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin 도 JVM 진영의 언어이긴 하지만 언어의 특성이 다르기 때문에 사용할 때 주의해야 할 점이 있는 편이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ORM 을 사용하는 것도 처음이기도 하고 이를 Kotlin 으로 사용하려고 하니 대부분의 문서는 Java 를 기준으로 하기 때문에 보고 따라하면 예상치 못한 결과를 마주하는 경우도 많이 있어서 여러 글들을 찾아보고 나중에 찾아보기 쉽도록 내용을 정리해본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ORM&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 애플리케이션 로직을 작성할 때 주로 객체지향 언어를 사용하고 관계형 데이터베이스에 데이터를 저장하거나 검색할 때는 SQL 문을 사용한다. 관계형 데이터베이스는 테이블의 컬럼으로 데이터를 표현하고 데이터 간의 관계는 외래키와 같은 제약사항으로 처리하는 반면 객체지향 언어에서는 클래스와 내부 필드와 메서드로 데이터를 표현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇듯 두 기술은 서로 다른 관점으로 데이터를 다루기 때문에 이 둘 사이를 변환해주는 ORM 은 이 둘의 간극을 좁혀주는 문제를 도와준다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;JPA &amp;amp; Hibernate&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 는 Java 의 ORM 에 대한 표준을 말한다. 따라서 이 표준을 지킨 구현체를 사용하면 자바 애플리케이션에서 ORM 을 일관성 있게 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate 는 JPA 의 구현체이다. 최근에는 가장 많이 사용하는 구현체인 것 같아 보인다. Hibernate 를 사용하면 겪는 여러 가지 제약사항들이 실은 JPA 자체의 제약사항인 경우일 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 에서는 몇 가지 표준을 제시하는데 그 중 하나가 엔티티의 라이프사이클이다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;715&quot; data-origin-height=&quot;455&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNvv5f/btsKT86IqJv/YStqKcTZTy67CsyjrjUcok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNvv5f/btsKT86IqJv/YStqKcTZTy67CsyjrjUcok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNvv5f/btsKT86IqJv/YStqKcTZTy67CsyjrjUcok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNvv5f%2FbtsKT86IqJv%2FYStqKcTZTy67CsyjrjUcok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;715&quot; height=&quot;455&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;715&quot; data-origin-height=&quot;455&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 문서를 보면 엔티티 클래스에 대해서 다음과 같은 요구사항이 만족되어야 한다고 쓰여있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The entity class must have a no-arg constructor. The entity class may have other constructors as well. &lt;br /&gt;The no-arg constructor must be public or protected.&lt;br /&gt;The entity class must not be final. No methods or persistent instance variables of the entity class may be final.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin 을 사용해서 JPA Entity 를 정의하려고 할 때 설정해야 하는 여러 사항들은 대부분 위 요구사항을 만족하기 위한 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔티티 클래스는 no-arg 생성자를 가져야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ORM 은 no-arg 생성자를 사용해서 객체를 우선 생성한 뒤 필드 값을 채워넣는 방식으로 동작한다. Kotlin 은 기본적으로 immutable 을 지향하기 때문에 명시적으로 mutable 로 선언하지 않는 이상은 사용하지 않는다. 매번 명시적으로 선언하는 것 대신 이를 자동으로 수행해줄 수 있는 kotlin-jpa 플러그인을 사용하면 컴파일러가 좀 더 간편하게 이를 처리해준다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔티티 클래스는 final 로 선언되지 않아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin 클래스의 property 는 기본적으로 final 로 선언되어 있다. 이 조건을 만족시켜주기 위해서는 open 키워드를 붙혀서 final 제약조건을 풀어주어야 하는데 이도 마찬가지로 직접 처리하는 대신 all-open 플러그인을 사용하면 자동으로 수행해준다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Data Class&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 Kotlin 으로 도메인 모델을 정의하려고 하는 경우라면 data class 를 떠올려서 사용하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Entity 도 마찬가지로 data class 로 정의하는 것이 자연스러워 보이지만 데이터 클래스가 자동으로 제공해주는 equals(), hashCode(), toString() 이 JPA 에서 요구하는 사항과 잘 맞지 않을 뿐더러 Kotlin 언어에서 세워둔 data class 의 제약사항은 immutable 한 상태인데 이를 억지로 mutable 하게 사용하는 것이 언어적인 특성을 활용한다는 점에 있어서 잘 맞지 않는다. 따라서 일반 클래스로 엔티티를 정의하는게 지금까지 찾아본 바로는 최선의 방법처럼 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 엔티티 클래스를 정의해보면 다음과 같이 작성해볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1732367137283&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;Person&quot;)
class PersonEntity(
	@Id
	val id: UUID,
    var firstName: String,
    var lastName: String
) {

	override fun hashCode(): Int = javaClass.hashCode()
    
    override fun equals(other: Any?): Boolean {
    	if (this === other) return true
        if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false
        other as PersonEntity
        return id == other.id
    }
    
    override fun toString(): String {
    	return this::class.simpleName + &quot;(id = $id, firstName = $firstName, lastName = $lastName)&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 했을 때 나의 신경이 쓰였던 부분은 늘 클래스의 property 를 val 로 선언해서 immutable 을 보장해주었지만 이젠 그렇게 할 수 없다는 점인데 이는 JPA Entity 에서 필요한 부분이기 때문에 이를 보완하는 차원에서 다음과 같이 setter에 대한 접근자를 변경해서 처리할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1732371523054&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;person&quot;)
class Person(
	firstName: String
) {
	@Id
	var id: Long? = null
	var firstName: String = firstName
    	protected set
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;firstName 이라는 값을 생성자에서 받은 뒤 명시적으로 선언한 property 에 할당해주었다. 그리고 setter 를 protected 접근 제한자로 제한하여 클래스 본인 혹은 상속받은 클래스에서만 이를 설정할 수 있도록 하였는데 final 로 선언된 클래스에서는 상속이 불가능하기 때문에 private setter 와 동일하겠지만 앞서 본 all-open 플러그인을 사용했다면 @Entity 애노테이션이 붙은 클래스를 open 으로 처리해주기 때문에 protected setter 로써의 역할을 그대로 수행할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Repository&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 나는 Repository 를 정의할 때 JPARepository 가 아닌 CrudRepository 를 상속받고 별도로 추가적인 메서드 구현이 필요한 경우에는 직접 정의한 인터페이스를 상속하여 구현하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직까지는 아주 간단한 쿼리만을 사용하고 있기 때문에 페이징이나 기타 불필요한 메서드가 추가되는 것이 별로라고 생각했기 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 이전 안드로이드 애플리케이션을 개발할 때는 repository layer 와 domain layer 에서 사용하는 모델이 서로 달랐었기 때문에 이 부분을 적용해보고자 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 앞서 살펴봤던 Entity 클래스의 mutability 와 관련된 문제는 repository layer 에서만 발생할 수 있도록 격리해두고 실제 비즈니스 로직에서는 별도로 정의한 도메인 모델 클래스를 사용해서 처리해주면 조금 더 예쁘게 코드를 작성할 수 있다. 물론 그때나 지금이나 복잡하지 않은 애플리케이션에서 코드가 매우 빠르게 증가한다는 큰 단점이 존재하긴 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 위의 예시를 이 방식을 적용해서 살펴보면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1732372899174&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class Person(
	val firstName: String,
    val lastName: String
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 로직에서 사용되는 Person 객체를 위와 같이 정의했다. 이 객체는 실제 persistence layer 와는 상호작용하지 않을 것이기 때문에 지금까지 고민하며 해결해왔던 것들을 적용할 필요가 없어 가장 합리적인 선택인 data class 를 사용해서 정의해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Persistence layer 와 상호작용할 수 있는 이에 대응되는 클래스는 지금과 마찬가지로 다음과 같이 정의해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1732373050296&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;person&quot;)
class PersonEntity(
	firstName: String,
    lastName: String
 ) {
 	@Id
 	var id: Long? = null
    var firstName: String = firstName
    	protected set
    var lastName: String = lastName
    	protected set
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository 에서는 Person 도메인 객체를 받아 이를 Entity 객체로 변환한 뒤 ORM 과 관련된 처리를 할 수 있도록 만들어주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1732373256726&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class PersonRepositoryImpl: PersonRepository {

	fun save(person: Person): Person {
    	val personEntity = person.toEntity()
        // QueryDSl 이나 다른 구현체를 통해 PersonEntity 에 대한 처리 수행
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>qwebnm7788</author>
      <guid isPermaLink="true">https://qwebnm7788.tistory.com/9</guid>
      <comments>https://qwebnm7788.tistory.com/9#entry9comment</comments>
      <pubDate>Sat, 23 Nov 2024 23:29:23 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security 문서 살펴보기 (1)</title>
      <link>https://qwebnm7788.tistory.com/8</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security 는 Spring 에서 &lt;u&gt;&lt;b&gt;인증&lt;/b&gt;&lt;/u&gt;, &lt;u&gt;&lt;b&gt;인가&lt;/b&gt;&lt;/u&gt;, &lt;u&gt;&lt;b&gt;외부 공격으로부터의 보호&lt;/b&gt;&lt;/u&gt;를 제공해주는 프레임워크이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet 기반과 Reactive 기반을 모두 지원하기 때문에 Spring MVC 나 Spring Webflux 중 어느것을 사용하더라도 이를 적용할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 에서 Spring Security 를 의존성에 추가하게 되면 Security Auto Configuration 이 동작해 다양한 보안관련 설정들이 적용되는데 그 중에서 일부 항목들에 대해 알아보도록 하자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTTP Basic Authentication&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security 는 form-based login/logout 과 더불어 HTTP Basic Authentication 을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 프로토콜은 access control 과 authentication 을 위한 범용적인 프레임워크를 제공하는데 &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7235&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RFC 7235&lt;/a&gt;에서 이에 대한 내용을 정의하고 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 프레임워크는 &lt;u&gt;&lt;b&gt;Challenge Response Authentication Mechanism, 줄여서 CRAM 이라는 방식&lt;/b&gt;&lt;/u&gt;을 기반으로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CRAM 은 다음과 같은 방식으로 동작한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 특정 사이트 혹은 리소스에 접근한다&lt;/li&gt;
&lt;li&gt;사용자에게는 challenge 가 주어지며 이에 대한 답을 전송해야 한다&lt;/li&gt;
&lt;li&gt;전송한 답이 맞다면 해당 사이트 혹은 리소스에 대한 접근이 허가된다. 아니라면 접근할 수 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 말하는 challenge 는 클라이언트가 인증된 사용자임을 확인할 수 있는 일종의 서버측의 질문이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 challenge 에는 두 가지 종류가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;static challenge&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 자신이 답할 challenge 를 정해 답을 전달하는 방식을 말한다. 대표적으로 &quot;비밀번호 찾기&quot; 를 할 때 계정 생성시 미리 선정한 질문에 대한 답을 해 맞추면 비밀번호를 알려주거나 바꿀 수 있도록 하는 경우가 있다.&lt;/li&gt;
&lt;li&gt;이렇듯 static challenge 의 경우에는 시간이 지나더라도 해당 challenge 에 대한 답이 변하지 않았음을 가정하는 경우가 대부분이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;dynamic challenge&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;challenge 가 랜덤하게 선택되는 방식을 말한다. 그렇기 때문에 매 요청마다 다른 답을 반환해야 할 수 있다. 이는 만약 진짜 인증된 사용자라면 어떤 질문을 하더라도 정확한 답을 내릴 수 있다는 가정을 기반으로 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식으로 인증을 처리하는 것은 CAPCHA, Password, SCRAM 등과 같은 곳들이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 방식은 입력하는 답이 반복적으로 사용된다는 단점이 생길 수 있다. 서버는 전달받은 답이 정확하다는 것만을 알 뿐 실제 사용자임을 확신할 수 없다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-11-10 at 2.32.56 PM.png&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mubSQ/btsKEmiT73p/aDJ3yTAErnMcyFR8l1Qnf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mubSQ/btsKEmiT73p/aDJ3yTAErnMcyFR8l1Qnf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mubSQ/btsKEmiT73p/aDJ3yTAErnMcyFR8l1Qnf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmubSQ%2FbtsKEmiT73p%2FaDJ3yTAErnMcyFR8l1Qnf0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;610&quot; height=&quot;578&quot; data-filename=&quot;Screenshot 2024-11-10 at 2.32.56 PM.png&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;1058&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;다시 돌아가 HTTP Basic Authentication 의 동작을 보면 다음과 같다&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서버는 클라이언트에 401 응답코드와 함께 &lt;b&gt;WWW-Authenticate&lt;/b&gt; 헤더에 어떤 방식으로 인증을 수행해야 하는가와 최소 하나 이상의 challenge 값을 전달한다&lt;/li&gt;
&lt;li&gt;인증하기 원하는 클라이언트는 이를 기반으로 그에 맞는 credential 값을 &lt;b&gt;Authorization&lt;/b&gt; 헤더에 포함해서 요청한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 서버가 유효하지 않은 credential 값을 받았다면 &lt;b&gt;401 Unauthorized&lt;/b&gt; 응답코드를 반환해서 이를 사용자에게 알려주어 유효한 값을 넣도록 해주면 된다. 혹은 유효한 credential 이었지만 적합한 권한을 가지지 못한 경우라면 &lt;b&gt;403 Forbidden&lt;/b&gt; 응답코드를 반환해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 &lt;b&gt;404 Not Found&lt;/b&gt; 를 반환해줄수도 있는데, 적절한 권한이 없거나 인증되지 않은 사용자에게 해당 페이지의 존재 여부를 숨기고 싶다면 이런식으로 구현해주는 것도 가능하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Security 에서의 구현&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-11-10 at 3.30.49 PM.png&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OR5qE/btsKEHG84Rn/cC25mR2GxX0QT7RAxojZck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OR5qE/btsKEHG84Rn/cC25mR2GxX0QT7RAxojZck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OR5qE/btsKEHG84Rn/cC25mR2GxX0QT7RAxojZck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOR5qE%2FbtsKEHG84Rn%2FcC25mR2GxX0QT7RAxojZck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1350&quot; height=&quot;620&quot; data-filename=&quot;Screenshot 2024-11-10 at 3.30.49 PM.png&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 Spring Security 에서 &lt;b&gt;WWW-Authenticate&lt;/b&gt; 헤더를 돌려받게되는 과정을 보여준다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;보호되고 있는 리소스에 대한 요청이 SecurityFilterChain 에 들어온다&lt;/li&gt;
&lt;li&gt;AuthorizationFilter 가 해당 요청이 거부되었음을 알리기 위해 AccessDeniedException 을 던진다&lt;/li&gt;
&lt;li&gt;해당 Exception 은 ExceptionTranslationFilter 에 의해 처리되는데 AuthenticationEntryPoint 를 사용해서 &lt;b&gt;WWW-Authenticate&lt;/b&gt; 헤더를 설정해서 응답으로 내려주게된다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WWW-Authenticate&lt;/b&gt; 헤더를 가진 응답을 본 클라이언트는 username &amp;amp; password 과 같은 credential 을 담아 다시 요청을 보내게 된다. 이 경우에는 다음과 같은 흐름으로 인증 과정이 수행된다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-11-10 at 3.35.01 PM.png&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;1082&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GiIJE/btsKCeHaiSO/KbDNBww6iB0Pl19yhw37O1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GiIJE/btsKCeHaiSO/KbDNBww6iB0Pl19yhw37O1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GiIJE/btsKCeHaiSO/KbDNBww6iB0Pl19yhw37O1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGiIJE%2FbtsKCeHaiSO%2FKbDNBww6iB0Pl19yhw37O1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;728&quot; height=&quot;584&quot; data-filename=&quot;Screenshot 2024-11-10 at 3.35.01 PM.png&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;1082&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;username &amp;amp; password 를 포함한 요청을 보내면 BasicAuthenticationFilter 가 요청에서 해당 값을 꺼내 UsernamePasswordAuthenticationToken 을 생성해서 이를 AuthenticationManager 에게 전달한다.&lt;/li&gt;
&lt;li&gt;해당 매니저를 통해 해당 사용자가 인증된 사용자인지를 확인한다&lt;/li&gt;
&lt;li&gt;인증이 성공한 경우라면, Authentication 객체가 SecurityContextHolder 에 저장되고 다음 필터를 거쳐 애플리케이션 로직을 수행할 수 있도록 한다&lt;/li&gt;
&lt;li&gt;인증이 실패한 경우라면, SecurityContextHolder 가 초기화되고 앞서 본 것과 같이 &lt;b&gt;WWW-Authenticate&lt;/b&gt; 헤더를 설정해 응답으로 보내서 재인증을 요구하게 된다&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CSRF&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSRF 공격이 성립할 수 있는 이유는 우리가 만든 사이트에서 오는 요청과 외부 사이트에서 오는 요청을 완벽히 동일하게 만들어둔다면 서버에서 이를 구분할 방법이 없기 때문이다. 이를 방지하기 위해서는 우리가 만든 사이트에서 온 요청이라는 사실을 알 수 있는 무언가가 요청에 포함되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 방지하는 방법에는 크게 두 가지가 존재한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SameSite Attribute&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠키에 SameSite 라는 속성을 명시하는 방법이다. 서버에서 이 값을 설정해줌으로써 외부 사이트에서는 해당 쿠키가 설정되지 않아야 함을 브라우저에게 알려주게 된다. 다만 Spring Security 에서는 session cookie 생성에 직접 관여할 수 없어서 별도의 방법을 사용해야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Synchronizer Token Pattern&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 많이 사용되는 보편적인 방법이다. 이는 HTTP 요청마다 session cookie 와 더불어 CSRF Token 이라 불리는 랜덤하게 생성된 값을 포함하도록 하는 방식을 말한다. 서버는 이 값을 실제 토큰값과 비교해서 우리가 기대하는 사이트에서 온 요청이라는 것을 구분할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식을 사용할 때 중요한 점은 CSRF 토큰을 담는 영역이 브라우저가 자동으로 포함하는 곳이 아니어야 한다는 점이다. 예를 들어 쿠키의 경우에는 브라우저가 요청을 보낼 때 자동으로 포함하기 때문에 CSRF 토큰을 담기에 적절한 곳이 아니다. HTTP parameter 나 header 에 설정해주어야 이를 명시적으로 보냈음을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 safe HTTP method 들이 read-only 로 동작해야지만 좀 더 안정적으로 이 패턴을 구현할 수 있는데 여기서 말하는 safe HTTP method 는 GET, HEAD 와 같은 애플리케이션의 상태를 변경시키지 않는 요청들을 말한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Multipart 요청에서의 CSRF protection&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일업로드에 대한 CSRF 공격 방어는 약간 생각할 부분이 추가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Synchronizer Token Pattern 에서는 CSRF 공격을 막기위해서는 이를 위해 추가한 토큰을 읽어 비교하는 과정이 추가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Multipart 요청에 대해서도 동일하게 토큰을 얻기 위해 Body 를 읽어버리게 되면 서버의 임시공간에 파일이 업로드되어버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 현상으로 인해 우리는 두 가지 방법 중 하나를 선택해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CSRF 토큰을 Body 에 위치시킨다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request body 는 authorization 과정 이전에 읽혀지기 때문에 인증되지 않은 사용자들이 파일을 서버의 임시공간에 아무렇게 생성할 수 있다는 단점이 생긴다. 대부분의 경우에는 그냥 이 방식을 사용하는데, 그 이유는 대부분의 경우 단순히 파일을 임시공간에 올렸다고 해서 크게 문제가 되지 않기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CSRF 토큰을 URL 에 위치시킨다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임시 파일의 생성을 절대 허용하지 않겠다고 한다면 이 방법을 선택할 수 있다. 다만 이렇게 되면 URL 을 통해 토큰이 노출되기 때문에 애플리케이션 보안 자체가 취약해질 수 있다.&lt;/p&gt;</description>
      <author>qwebnm7788</author>
      <guid isPermaLink="true">https://qwebnm7788.tistory.com/8</guid>
      <comments>https://qwebnm7788.tistory.com/8#entry8comment</comments>
      <pubDate>Sun, 10 Nov 2024 21:56:31 +0900</pubDate>
    </item>
    <item>
      <title>Java/Kotlin 에서의 timezone 처리</title>
      <link>https://qwebnm7788.tistory.com/7</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;JVM 기반의 애플리케이션을 설치해서 사용하는 경우 해당 애플리케이션이 기본적으로 사용하게 되는 타임존은 애플리케이션이 띄워져있는 OS의 시간 설정을 따르게 된다. 모바일 기기의 경우 OS에 의해 주기적으로 타임존이 변경된다. 해외 여행을 가게되면 핸드폰의 시간이 자동으로 그 지역 타임존으로 변경되어 설정되어 있는 경험을 하는것도 바로 그러한 이유 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산환경에서 애플리케이션을 구동하는 경우에는 기본값을 그대로 사용하는 경우 원치않는 결과를 얻게될 수 있다. 여러 지역에 걸쳐 서비스가 구동되고 있는 경우라면 동일한 애플리케이션이라도 사용하는 타임존의 값이 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 환경에서 가장 많이 사용하는 방법은 모든 시간을 UTC (혹은 GMT) 를 기준으로 사용하는 것이다. 이 경우 데이터베이스에 저장된 값이나 로그 파일에 있는 시간값을 조회할 때 우리는 클라이언트의 타임존을 UTC 시간으로 변환하는 과정이 필요하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 에서 date/time 정보를 표현하는 타입은 정말 다양하게 존재한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;1292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z6Rcb/btsKk1N7ui5/xQxC95qIjQXcOQWR8sj7qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z6Rcb/btsKk1N7ui5/xQxC95qIjQXcOQWR8sj7qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z6Rcb/btsKk1N7ui5/xQxC95qIjQXcOQWR8sj7qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZ6Rcb%2FbtsKk1N7ui5%2FxQxC95qIjQXcOQWR8sj7qk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;647&quot; height=&quot;815&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;1292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Java 8 이전 날짜/시간 API&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;java.sql.Date, java.sql.Timestamp&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC API 에서 date/time 정보를 표현하기 위한 타입이다. 패키지명에서도 알 수 있듯이 JDBC 와 관련된 작업을 위해서 존재하는 만큼 일반적인 비즈니스 로직에서 사용하기에 적합하지 않다. (JDBC 와의 결합도가 증가된다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;java.util.Date&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 시점을 밀리세컨드단위까지의 정확도로 표현할 수 있는 타입으로 원래 의도는 UTC 를 반영한 시간을 표현하는 것이다. JPA 를 사용하다보면 다음과 같이 @Temporal 애노테이션을 통해 어떤 형태로 Date 클래스를 다룰건지 선택할 수 있다. (앞서 살펴본 sql.Date 와 sql.Timestamp 와의 호환성을 위해 존재하는 것으로 보인다.)&lt;/p&gt;
&lt;pre id=&quot;code_1730010085674&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Temporal(TemporalType.DATE)
private Date date;

@Temporal(TemporalType.TIMESTAMP)
private Date time;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;java.util.Calendar&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Calendar 타입은 시간의 특정 시점을 캘린더의 특정 날짜로 변환하기 위한 메서드를 제공하는 추상 클래스이다. 이를 통해 2024년 10월 27일 15시와 같이 달력 상의 날짜처럼 타임스탬프로 표현되는 시간을 변환해줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에는 앞서 살펴본 Date 타입이나 Calendar 타입을 많이 사용하지 않는데, 그 이유는 &lt;b&gt;1) 해당 타입들은 대부분의 경우 thread-safe 하지 않으며&lt;/b&gt;, &lt;b&gt;2) 타임존이 존재하는 경우 이를 적용하거나 변환하는 것이 명확하지 않기&lt;/b&gt; 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Java8&amp;nbsp; 이후 날짜/시간 API&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java8 이 릴리즈 되면서 JSR 310 명세로 java.time 패키지가 추가되는데 이는 앞서 본 타입들의 문제점을 보완하였기 때문에 최근 대부분의 경우에는 java.time 패키지를 사용해 날짜/시간을 표현한다. LocalDate, LocalTime, LocalDateTime, ZonedDateTime 과 같이 날짜와 시간을 별도로 분리해 사용할 수 있도록 하였으며 기존과는 다르게 immutable 하며 따라서 thread-safe 한 클래스들이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;java.time.ZonedDateTime&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중에서도 이전까지 가장 큰 고통을 주었던 타임존 관련 문제를 해결하기 위해 등장한 클래스가 바로 ZonedDateTime 이다. 이는&amp;nbsp;&lt;b&gt;2007-12-03T10:15:30+01:00 Europe/Paris&lt;/b&gt;&amp;nbsp;과 같은 ISO-8601 캘린더 시스템내의 datetime 을 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;java.time.Instant&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타임라인에서 특정한 시점을 표현하는 클래스로 일반적으로는 timestamp 를 표현하는데 사용된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그렇다면 언제 어떤 타입을 사용하는 것이 좋을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;java.time 패키지의 날짜/시간 관련 클래스를 사용하는 경우에는 다음과 같은 기준을 두고 사용하면 좀 더 일관성있게 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Instant 클래스는 컴퓨터, 그 외 날짜/시간 클래스들은 사람이 읽기쉬운 타입으로 처리하자&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 클래스의 값을 로그로 찍어보면 보여지는 형태는 사실 큰 차이가 없다. 왜냐면 java.time 패키지들은 대부분 ISO-8601 형식을 준수하려 하기 때문에 Instant 의 toString 이 ISO-8601 로 보여주기 때문이다. 그렇지만 타임라인 상의 특정 시점을 표현한다는 점을 염두해두면 컴퓨터에게는 시간을 표현하는 가장 자연스러운 방식이 된다. 다만 사람은 시간을 보통 그런식으로 인식하지 않고, 특정 타임존에서 시계를 볼 때 보는 시점으로 인식하기에 이를 같이 사용하기엔 어려움이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터베이스와 관련된 곳에서는 항상 UTC 를 사용하자&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 연결에서는 UTC를 기준으로 하는것이 분산환경에서 접근할 때 어려움을 최소화 할 수 있는 방식이라고 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 를 사용하는 경우라면&amp;nbsp;&lt;i&gt;hibernate.jdbc.time_zone&lt;/i&gt;&amp;nbsp;속성을 UTC 로 설정하면 UTC로의 변환을 자동으로 수행해줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 매번 클라이언트와 데이터베이스 사이에서 그에 맞는 타임존으로 변경하는 코드를 매번 작성할 필요가 사라지게된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 DB 컬럼으로 날짜/시간을 다룰 때는 MySQL 을 기준으로&amp;nbsp;timestamp&amp;nbsp;와&amp;nbsp;date&amp;nbsp;타입을 사용할 수 있는데, 좀 더 포괄적이라는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유만으로 모든 타입을 timestamp 로 사용하지 말자. 이는 불필요한 복잡도를 증가시키고, 저장공간을 좀 더 사용하게 된다. (예를 들어 생년월일과 같은 값을 저장하는 경우에는 date 면 충분하다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;엔티티 클래스, JSON 요청/응답 필드와 같이 사용처에 따라 일관성있는 타입을 사용하자&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA 엔티티의 시간 필드는 DB에 저장되는 값이며 컴퓨터가 사용하는 타입이므로 Instant 를 사용하는 것이 좋다.&lt;/li&gt;
&lt;li&gt;JSON 요청/응답을 처리하기 위한 DTO 클래스에서는 LocalDateTime 이나 String 타입을 사용한 뒤 이를 파싱해서필요한 타입으로 변환해서 사용하면 좀 더 유연하게 클라이언트의 요청을 처리할 수 있다.
&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇 가지 케이스를 나누어 살펴보았지만 결론적으로는 UI와 같이 사용자가 눈으로 확인할 수 있거나 타임존이 중요하게 동작하는 곳이라면ZonedDateTime 을 사용해 타임존 정보를 확실하게 처리해주는 것이 좋고, &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Instant 클래스는 DST나 타임존에 대한 처리를 개발자가 따로 해줄 필요가 없기 때문에 그 외의 경우에서 사용하면 개발시의 복잡도를 줄여주는데 도움이 될 수 있다.&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;참고&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;kotlinx.datetime&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 코틀린의 멀티플랫폼 특성을 지원하기 위해 &lt;a href=&quot;https://github.com/Kotlin/kotlinx-datetime&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;kotlinx.datetime&lt;/a&gt; 이라는 라이브러리가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 JVM 기반 애플리케이션 개발 시 Kotlin 을 사용하는 경우에는 앞서 살펴본 &lt;b&gt;java.time&lt;/b&gt; 패키지의 클래스를 활용하겠지만 만약 멀티플랫폼을 지원해야 하는 환경에서는 그에 맞는 라이브러리를 사용해야 지원하는 플랫폼에서 호환될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제공되는 대부분의 API 는 java.time 패키지와 동일하다고 볼 수 있다. ISO-8601 포맷을 기반으로 날짜/시간을 표현하는 것도 대부분의 시간관련 라이브러리의 특성과 유사하다고 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jetbrains 에서 지정한 날짜/시간 관련 멀티플랫폼 공식 라이브러리이지만 현시점에는 알파버전이기 때문에 멀티플랫폼을 구현해야 하는 경우가 아니라면 접해볼 수 있는 일은 드물 것 같다.&lt;/p&gt;</description>
      <author>qwebnm7788</author>
      <guid isPermaLink="true">https://qwebnm7788.tistory.com/7</guid>
      <comments>https://qwebnm7788.tistory.com/7#entry7comment</comments>
      <pubDate>Sun, 27 Oct 2024 16:42:59 +0900</pubDate>
    </item>
  </channel>
</rss>