<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>seokhwan-an.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 03 Mar 2025 04:27:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. seokhwan-an.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/seokhwan-an" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[https 통신 적용하기]]></title>
            <link>https://velog.io/@seokhwan-an/https-%ED%86%B5%EC%8B%A0-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seokhwan-an/https-%ED%86%B5%EC%8B%A0-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 03 Mar 2025 04:27:49 GMT</pubDate>
            <description><![CDATA[<p>dayone 서버 배포 과정 중 마지막으로 수행한 작업은 https를 적용하는 것이었습니다. http로도 통신을 원활하게 할 수 있습니다. 그럼에도 크롬에서는 <code>주의 요함</code> 을 나타내며 https를 적용하는 것을 권장하고 있습니다. 왜 https를 적용해야하는 것일까요?</p>
<h2 id="http-vs-https">Http VS Https</h2>
<p>HTTP는 두 대 이상의 컴퓨터가 데이터를 주고 받을 때 이용하는 프로토콜이며 HTTPS는 데이터 전송의 보안을 강화하기 위해 컴퓨터 끼리 주고 받는 통신에 암호화를 적용한 방식입니다. </p>
<p>암호화 방식에는 대칭키 방식과 비대칭키 방식이 있으며 각각의 방식에 대해서 알아보겠습니다.</p>
<h3 id="대칭키-방식-및-비대칭-키-방식">대칭키 방식 및 비대칭 키 방식</h3>
<p><code>대칭키 방식</code>은 동일한 키를 통해서 데이터를 암호화하고 받는 쪽에서 복호화하는 방식입니다. 다음은 hello라는 메세지를 보내기 위한 과정입니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/aeb1cd96-c63a-4980-834c-97187eb6e37d/image.png" alt=""></p>
<ol>
<li>남성이 hello라는 메세지를 키를 통해 암호화 하여 여성에게 전송합니다.</li>
<li>전송된 메시지를 키를 통해 복호화하여 hello라는 메세지를 읽습니다.</li>
</ol>
<p>대칭키 방식은 두 통신자가 같은 키를 가지고 데이터를 암호화 하고 복호화 한다는 점이 직관적이지만 한편으로는 키가 악의적인 사용자에게 탈취된다면 기밀성이 보장되지 않는 문제가 발생할 수 있을 것 같습니다. 기밀성은 허가된 사용자만 정보에 접근할 수 있는 속성을 의미합니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/036a6b8b-68bb-499e-b795-9423c5ff3c2b/image.png" alt=""></p>
<p>위의 그림과 같이 외부의 사용자에게 키를 탈취 당한다면 그 누구든지 저 둘의 대화를 볼 수 있을 것입니다.</p>
<p><code>비대칭 키 방식</code>은 공개키를 방식으로 불리며 서로 다른 키를 통해 데이터를 암호화하고 복호화하는 방식입니다. <code>데이터를 암호화할 때는 공개키</code>로 암호화를 하고 <code>복호화할 때에는 개인키</code>를 이용합니다. 공개키와 개인키는 한 쌍이며 공개키로 암호화한 것은 오직 개인키로만 복호화할 수 있기에 누구든지 공개키를 가져도 문제가 되지 않습니다. (공개키는 누구에게나 공유 되어 있는 키이며, 개인키는 특정 사용자만 가지고 있는 키입니다.)</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/a5adc22e-18a6-442d-86ea-10cae09b4656/image.png" alt=""></p>
<p>이렇게 한다면 외부에서 공개키를 탈취하더라도 데이터를 복호화하지 못해서 해당 내용의 정보를 볼 수 없을 것입니다. </p>
<p>비대칭키 방식이 만능일 것처럼 보이지만 항상 비대칭키 방식이 만능인 것은 아닙니다. 먼저 비대칭키 방식의 경우 대칭키 방식에 비해 복잡한 연산을 활용해 암호화 및 복호화 과정이 필요하기에 대칭키 방식에 비해서 통신 속도가 느리다는 단점을 가지고 있습니다. 또한 다음과 같은 사례를 보면 비대칭키 방식을 활용하더라도 데이터가 탈취될 수 있는 문제가 발생할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/09aeaf25-489c-4585-b081-7db714e01244/image.png" alt=""></p>
<p>악의적인 사용자가 공개키를 위변저하여 클라이언트에게 제공하고 클라이언트가 암호환 데이터를 가로채 해당 정보를 볼 수 있습니다. 이를 중간자 공격이라고 합니다. 위의 그림처럼 클라이언트는 자신이 문제없이 통신을 하고 있다고 느끼지만 사실은 자신의 데이터가 탈취되고 있는 상황이 발생할 수 있습니다.</p>
<p>결국에는 비대칭키 방식고 대칭키 방식 모두 하나로는 온전하게 안전한 보안을 유지할 수 있는 방안은 아닙니다. 그렇다면 HTTPS는 어떻게 보안을 유지할 수 있는 것일까요? </p>
<h3 id="https에서의-암호화-방식">HTTPS에서의 암호화 방식</h3>
<p>HTTPS는 대칭키 방식과 비대칭키 방식을 모두 이용합니다. 대칭키 방식은 데이터를 암호화 하고 복호화 하는데 이용하며 비대칭키 방식은 클라이언트의 대칭키 서버의 공개키로 암호화하고 서버의 개인키로 복호화 해 서버가 클라이언트의 대칭키를 받는데 이용됩니다. 그렇다면 HTTPS에서는 어떻게 중간자 공격을 방지했을까요? </p>
<p>CA(인증 기관)을 통해 서버의 공개키가 변조되지 않았음을 보장함으로써 중간자 공격을 막을 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/2a2d8c93-4401-436a-a500-f8ebe5e2fb12/image.png" alt=""></p>
<ol>
<li>서버는 인증 기관에 자신의 공개키와 사이트 정보를 제공한다.</li>
<li>인증 기관은 자신의 공개키로 받은 정보를 암호화하여 인증서를 발급하고 이를 서버에게 전달합니다.</li>
<li>클라이언트는 인증기관으로부터 인증기관의 공개키를 받아 브라우저에 저장해둡니다.</li>
<li>서버에 접속 시 서버는 인증서를 클라이언트에게 제공하고 클라이언트는 인증 기관의 공개키를 통해 인증서를 복호화하여 서버의 공개키를 흭득합니다.</li>
<li>클라이언트는 자신의 대칭키를 서버의 공개키로 암호화하여 서버에 전달을 하고 서버는 자신의 개인키로 복호화하여 클라이언트의 대칭키를 흭득합니다.</li>
<li>이후로는 대칭키 방식으로 클라이언트와 서버가 통신을 합니다.</li>
</ol>
<h2 id="dayone에-https-적용하기">DayOne에 HTTPS 적용하기</h2>
<p>서비스에 HTTPS를 적용하는 방법에는 크게 2가지 방법을 찾을 수 있었습니다.</p>
<ol>
<li>nginx와 Let’s encrypt를 이용해서 HTTPS를 적용하는 방법</li>
<li>Cloudfront를 이용해서 HTTPS를 적용하는 방법</li>
</ol>
<p>이 둘을 비교하면 Cloudfront를 이용하면 AWS와 연동해서 작업이 가능하면 인증서를 자동으로 관리해주는 장점이 있지만 추가적인 비용이 발생할 수 있는 가능성이 있는 반면 nginx와 Let’s encrypt는 인증서를 직접 관리해야하지만 비용이 무료라는 점입니다. 저는 이 두 방식 중 무료라는 장점이 더 크게 다가와서 nginx와 Let’s encrypt를 이용해서 HTTPS를 적용했습니다.</p>
<p>https를 적용하기에 앞서서 ec2가 HTTPS 통신을 할 수 있게 443 포트를 인바운드 규칙에서 허용해 줍니다.</p>
<h3 id="nginx-설치-및-실행">nginx 설치 및 실행</h3>
<pre><code class="language-bash"># nginx 설치 명령어
sudo apt install nginx -y

# nginx의 설치 확인 명령어
nginx -version

# nginx를 실행시키는 명령어
sudo systemctl start nginx

# ec2를 재부팅했을 때 자동으로 동작하도록 적용
sudo systemctl enable nginx</code></pre>
<h3 id="certbot을-통해-lets-encrypt-ssl-인증서-발급">certbot을 통해 Let’s Encrypt SSL 인증서 발급</h3>
<pre><code class="language-bash"># cerbot 설치
sudo apt install certbot python3-certbot-nginx -y

# 인증서 발급
sudo certbot --nginx -d example.com(자신의 사이트) -d www.example.com(자신의 사이트)</code></pre>
<p>인증서를 발급하면 certbot에서 자동으로 Nginx 설정을 수정해주어서 http 통신으로 요청을 보내더라도 https로 통신이 이루어지게 리다이랙트를 해줍니다. </p>
<p>해당 과정까지 끝나면 인증서 발급이 마무리가 되었지만 저희는 하나의 작업을 더 해주어야 합니다. 현재 과정을 그림으로 표현하면 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/9eae829e-67f3-4e85-adf8-f9aac66f7571/image.png" alt=""></p>
<p>사용자의 요청이 ec2 내부에 nginx까지는 접근했지만 해당 요청이 spring으로 이어지지 않는 상황입니다. 그렇기에 요청이 spring으로 이어질 수 있게 포트포워딩 설정이 필요합니다.</p>
<h3 id="포트포워딩-설정">포트포워딩 설정</h3>
<p>포트포워딩은 외부에서 내부 장치로 접근할 수 있도록 하기 위해 특정 포트로 들어오는 네트워크 트래픽을 내부 장치로 전달하는 것을 의미합니다. 즉, 이번에 처리해야하는 것은 HTTPS(443) 요청이 넘어오면 spring(8080)로 요청을 전달하는 설정을 nginx에서 추가해주어야 합니다.</p>
<p>/etc/nginx/site-avaialble/default에 다음 설정을 추가해주면 됩니다. </p>
<pre><code class="language-bash">server {
    listen 80;
    server_name example.com(자신의 사이트) www.example.com(자신의 사이트);
    return 301 https://$host$request_uri;  # HTTP -&gt; HTTPS 리디렉션
}

server {
    listen 443 ssl;
    server_name example.com(자신의 사이트) www.example.com(자신의 사이트);

    ssl_certificate /etc/letsencrypt/live/example.com(자신의 사이트)/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com(자신의 사이트)/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;  # HTTPS 요청을 내부 8080 포트로 전달
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }
}</code></pre>
<p>이후에 nginx의 설정 테스트를 수행하고 성공한다면 nginx를 재실행 해주면 설정이 적용됩니다.</p>
<pre><code class="language-bash"># nginx 설정 테스트
sudo nginx -t

# nginx 재시작
sudo systemctl restart nginx</code></pre>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://www.cloudflare.com/ko-kr/learning/ssl/what-is-https/">https://www.cloudflare.com/ko-kr/learning/ssl/what-is-https/</a></li>
<li><a href="https://www.youtube.com/watch?v=wPdH7lJ8jf0">https://www.youtube.com/watch?v=wPdH7lJ8jf0</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[운영환경과 동일한 테스트 환경을 만들자]]></title>
            <link>https://velog.io/@seokhwan-an/%EC%9A%B4%EC%98%81%ED%99%98%EA%B2%BD%EA%B3%BC-%EB%8F%99%EC%9D%BC%ED%95%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%99%98%EA%B2%BD%EC%9D%84-%EB%A7%8C%EB%93%A4%EC%9E%90</link>
            <guid>https://velog.io/@seokhwan-an/%EC%9A%B4%EC%98%81%ED%99%98%EA%B2%BD%EA%B3%BC-%EB%8F%99%EC%9D%BC%ED%95%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%99%98%EA%B2%BD%EC%9D%84-%EB%A7%8C%EB%93%A4%EC%9E%90</guid>
            <pubDate>Sat, 22 Feb 2025 06:26:02 GMT</pubDate>
            <description><![CDATA[<p>지난 배포 과정(<a href="https://velog.io/@seokhwan-an/DayOne-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-%EB%B0%B0%ED%8F%AC%ED%95%B4%EB%B3%B4%EC%9E%90">DayOne-서비스를-배포해보자</a>) 중 spring을 build하는 과정에서 에러가 발생했습니다.
<img src="https://velog.velcdn.com/images/seokhwan-an/post/c6cc8370-7900-4de8-89d5-e64b71c0a5fd/image.png" alt=""></p>
<p>build 과정에서 에러 정보를 자세하기 보기 위해서 <code>./gradlew build -i</code> 로 진행한 결과 다음과 같았습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/19726e88-aa48-479d-8d42-63e28bdec842/image.png" alt=""></p>
<p>테스트 과정에서 db가 생성되지 않아서 entityManagerFactory bean을 생성하는 것이 불가능해 발생한 예외상황이었습니다.</p>
<p>로컬 환경에서는 docker에 mysql 컨테이너를 실행하여 테스트를 진행해왔습니다. 하지만 ec2에서는 테스트 시에는 mysql에 접근할 수 없기 때문에 앞선 에러가 발생하는 것입니다. 그렇다면 테스트 시에 mysql를 접속할 수 있게 되면 build 과정에서 문제가 발생하지 않다는 것을 의미했습니다. 현재 한정된 자원에서 테스트를 동작시킬 수 있는 방안을 3가지 정도를 추려보았습니다.</p>
<ul>
<li>db 서버에서 테스트 database를 만들어서 테스트를 진행한다.</li>
<li>h2 memory db를 활용해서 테스트 진행하기</li>
<li>TestContainer를 도입해서 처리하기</li>
</ul>
<h2 id="db-서버에-테스트-database를-만들어서-테스트-진행하기">DB 서버에 테스트 database를 만들어서 테스트 진행하기</h2>
<p>현재 Dayone 서비스의 시스템 구조도는 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/49309f5e-982a-49de-a573-e52de65d0dd1/image.png" alt=""></p>
<p>운영 서버에서 db 서버로 통신하면 api를 처리하는 방식을 가지고 있습니다. 테스트를 진행하는 과정에서 mysql과 연동되면 build가 실패하는 일은 발생하지 않는 것이기에 DB서버에 테스트 용 database를 만드는 방안을 떠올렸습니다.</p>
<p>가장 걱정이 되는 부분은 현재 DB 서버의 경우 t2.mirco의 인스턴스를 이용하여 cpu 개수가 1개이기에 실사용자가 서비스를 이용하는 과정에서 새로운 버전의 배포가 발생해 build 과정이 병행된다면 cpu 사용률이 높아질 것으로 예측되어 사용자들의 요청 응답이 늦어지거나 혹은 build 과정이 오래 걸리는 문제가 발생할 것이라 생각했습니다. 그래서 해당 방법은 직면한 문제를 해결할 수 있으나 잠정적으로 성능 문제를 유발할 수 있다는 문제점을 인식했습니다. </p>
<h2 id="h2-memory-db-활용하기">H2 memory DB 활용하기</h2>
<p>Spring boot 같은 경우 기본적으로 H2 메모리 데이터베이스를 지원해주고 있습니다. 그렇기에 아래와 같이 의존성과 yml 설정을 추가한다면 간단하게 memory db를 구성할 수 있습니다.</p>
<p>build.gradle</p>
<pre><code class="language-bash">dependencies {
    implementation &#39;com.h2database:h2&#39;
}</code></pre>
<p>application-test.yml</p>
<pre><code class="language-bash">spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:dayone;MODE=MySQL</code></pre>
<p>이와 같이 설정하고 build를 수행하면 끝인 방법입니다. 어떻게 보면 해당 방식이 따로 database를 만들거나 할 필요 없이 memory에서 처리를 하는 거다 보니 가장 효율적인 방안이 될 수 있습니다. </p>
<p>하지만 운영환경에서는 mysql을 이용하고 build 과정에서는 H2를 이용하는 것은 근본적으로 올바른 해결책은 아니라고 느껴졌습니다. 물론 H2를 mysql mode로 실행할 수 있지만 이 마저도 mysql과 완전히 동일하게 동작하는 것은 아니기에 언제든지 build는 성공하지만 실 운영에서는 실패하는 상황이 발생할 수 있습니다. 그래서 해당 방법은 보류했습니다.</p>
<h2 id="testcontainer-활용하기">TestContainer 활용하기</h2>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/81e44ed6-2d00-4123-b9e7-69525bc8512a/image.png" alt=""></p>
<p>테스트 컨테이너는 java 코드를 통해서 도커 컨테이너를 제어하여 통합테스트를 도와주는 라이브러리 입니다. 테스트 컨테이너를 이용하면 실제 db를 구축하지 않고도 도커 컨테이너를 활용하여 DB 인스턴스를 실행합니다. 이러한 점은 현재 리소스가 한정적인 제게 매력적인 방안으로 다가왔습니다.</p>
<p>하지만 테스트 컨테이너를 잘못 활용하게 되면 테스트 속도가 많이 늘어질 수 있습니다. 테스트 컨테이너는 기본적으로 각 테스트 마다 격리성을 보장하기 위해 컨테이너를 올렸다 내렸다 반복해 테스트 속도가 느린 문제가 있습니다. 실제로 테스트 컨테이너를 도입하기에 앞서서 로컬 환경에서 적용하고 실행한 결과보면 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/67f5577d-0c4b-4142-8485-9ba3f7afd879/image.png" alt=""></p>
<p>현재 결과만 보면 두 배정도 더 많은 시간이 걸리는 것처럼 보이지만 해당 정보에는 컨테이너가 생성되고 소멸되는 시간은 포함되지 않아 사실상 3배, 4배더 많은 시간이 걸리는 것으로 볼 수 있습니다.</p>
<p>테스트 컨테이너를 이용하는 방안과 h2를 이용하는 방안 중 속도가 느리더라도 운영환경과 동일한 테스트 환경을 구성하는 것이 더 중요하다고 판단을 했고 대신 테스트 컨테이너를 이용하는 방안을 고도화 하고자 했습니다.</p>
<h2 id="testcotainer-잘-활용하기">TestCotainer 잘 활용하기</h2>
<p>테스트 컨테이너를 사용하면서 가장 큰 병목은 테스트 실행 전에 컨테이너를 생성하고 삭제하는 과정이었습니다. 이를 개선하기 위해 컨테이너를 매번 새로 띄우는 대신, 한 번만 생성한 후 내부 데이터를 초기화하는 방식으로 테스트 간의 격리성을 보장하고자 했습니다.</p>
<pre><code class="language-java">@Container
MySQLContainer mysql = new MySQLContainer(&quot;mysql:8.0.27&quot;)
            .withDatabaseName(&quot;dayoneTest&quot;)
            .withUsername(&quot;root&quot;)
            .withPassword(&quot;mysql&quot;);</code></pre>
<p>초기에는 위와 같이 테스트 컨테이너를 이용했다면 컨테이너를 한번만 띄우기 위해서는 아래와 같이 static를 활용했습니다.</p>
<pre><code class="language-java">private static final MySQLContainer MYSQL_CONTAINER;

static {
    MYSQL_CONTAINER = new MySQLContainer(&quot;mysql:8.0.27&quot;)
        .withDatabaseName(&quot;dayone&quot;)
        .withUsername(&quot;root&quot;)
        .withPassword(&quot;mysql&quot;);
    MYSQL_CONTAINER.start();
}</code></pre>
<p>그리고 테스트 간 격리성을 보장하고자 EntitiyManager를 활용해 테스트 진행 후 모든 테이블을 truncate하는 방식을 채택했습니다.</p>
<pre><code class="language-java">@Component
public class DataCleaner {

    private static final String TRUNCATE_FORMAT = &quot;TRUNCATE TABLE %s&quot;;
    private static final String CAMEL_CASE_REGEX = &quot;([a-z])([A-Z]+)&quot;;
    private static final String SNAKE_CASE_REGEX = &quot;$1_$2&quot;;

    private List&lt;String&gt; tableNames;

    @PersistenceContext
    private EntityManager entityManager;

    @PostConstruct
    public void findDatabaseTableNames() {
        tableNames = entityManager.getMetamodel().getEntities().stream()
            .filter(DataCleaner::isEntityClass)
            .map(DataCleaner::convertCamelCaseToSnakeCase)
            .collect(Collectors.toList());
    }

    private static boolean isEntityClass(final EntityType&lt;?&gt; e) {
        return e.getJavaType().getAnnotation(Entity.class) != null;
    }

    private static String convertCamelCaseToSnakeCase(final EntityType&lt;?&gt; e) {
        return e.getName().replaceAll(CAMEL_CASE_REGEX, SNAKE_CASE_REGEX).toLowerCase();
    }

    @Transactional
    public void clear() {
        entityManager.flush();
        entityManager.clear();
        truncate();
    }

    private void truncate() {
        for (String tableName : tableNames) {
            entityManager.createNativeQuery(String.format(TRUNCATE_FORMAT, tableName))
                .executeUpdate();
        }
    }
}</code></pre>
<p>덕분에 비교적 테스트 속도가 처음에 컨테이너가 생성되는 시간만 제외하고는 기존의 테스트 속도와 동일하게 처리되었습니다.</p>
<h2 id="testcontainer를-ec2에-적용하기">TestContainer를 EC2에 적용하기</h2>
<p>테스트 컨테이너를 적용하고 ec2에서 build를 하는데 다음과 같은 에러가 발생했습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/3b300390-03b3-47e8-a5e9-8ebd33ce9bfc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/4a925207-3142-4af7-930b-a45da2cfd306/image.png" alt=""></p>
<p>해당 에러를 찾아보니 <a href="https://www.baeldung.com/java-exceptionininitializererror">https://www.baeldung.com/java-exceptionininitializererror</a> 글에 접근할 수 있었고 다음과 같이 설명되어 있습니다.</p>
<blockquote>
<p><strong>The <em>ExceptionInInitializerError</em> indicates that an unexpected exception has occurred in a <a href="https://www.baeldung.com/java-static">static initializer</a>.</strong></p>
</blockquote>
<p>static으로 초기화 하는 과정에서 예상치못한 에러가 발생했다는 것을 의미했으며 테스트 컨테이너가 생성하는 과정에서 문제가 발생했다는 것을 유추할 수 있었습니다. 원인을 분석한 결과, 테스트 컨테이너가 생성되는 과정에서 Docker에 접근하지 못해 컨테이너가 정상적으로 생성되지 않은 것이 문제의 원인이었습니다.</p>
<h2 id="docker-설정하기">docker 설정하기</h2>
<pre><code class="language-bash">// 도커 설치
sudo apt install docker.io

// ec2 user를 docker를 접근할 수 있는 권한 주기
sudo chmod 666 /var/run/docker.sock</code></pre>
<p>테스트 컨테이너를 이용하기 위해서는 도커가 필요하기 때문에 먼저 도커를 다운로드 했습니다. 도커를 다운로드 한 이후에는 EC2 사용자가 docker에 접근할 수 있게 권한을 부여해주어야 합니다.</p>
<p>마지막으로 다시 build를 실행하면 다음과 같은 결과를 얻을 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/69dea03d-38ca-4ae3-8e13-93d45029de9e/image.png" alt=""></p>
<h2 id="느낀-점">느낀 점</h2>
<p>이번에 운영환경과 동일한 테스트 환경을 구축하는 과정에서 새로운 기술인 테스트 컨테이너를 처음 도입해 보았습니다. 일단은 직면한 문제를 빠르게 해결하기 위해서 테스트 컨테이너 사용법만 익히고 내부로는 어떻게 동작을 하는지 파악하지 못한 점이 아쉬웠던 것 같고 프로젝트가 조금 여유로워 질 때 한번 deep dive를 해보고자 합니다. 그래도 build 과정이 원활하게 동작한 것은 큰 다행입니다.</p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://www.baeldung.com/spring-boot-h2-database">https://www.baeldung.com/spring-boot-h2-database</a></li>
<li><a href="https://java.testcontainers.org/test_framework_integration/junit_5/">https://java.testcontainers.org/test_framework_integration/junit_5/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[DayOne 서비스를 배포해보자]]></title>
            <link>https://velog.io/@seokhwan-an/DayOne-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-%EB%B0%B0%ED%8F%AC%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@seokhwan-an/DayOne-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-%EB%B0%B0%ED%8F%AC%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sat, 22 Feb 2025 06:25:16 GMT</pubDate>
            <description><![CDATA[<p>DayOne 서비스의 모든 기능이 완성된 것은 아니지만, 회원이 이용하는 데 필수적인 독서 기록 남기기, 독서 기록 확인하기, 좋아요 표시하기 기능이 구현되었습니다. 이에 빠르게 배포하여 회원들의 피드백을 받고자 했습니다.</p>
<p>그래서 이번 시간에는 DayOne 서버 배포 과정을 글로 정리해 두고자 합니다.</p>
<h2 id="시스템-구성도-구축하기">시스템 구성도 구축하기</h2>
<p>현재 수입이 없는 상황이므로 최소한의 비용으로 서버를 구축하고자 했습니다. 그럼에도 불구하고 Spring과 DB를 분리하여 EC2에 배포하기로 결정했습니다.</p>
<p>그 이유는 DB 장애가 Spring 애플리케이션에 영향을 주지 않도록 하기 위함이며 또한 EC2 프리 티어를 최대한 활용할 계획이었기에 하나의 인스턴스에서 서버와 DB를 함께 운영하면 리소스 부족이 발생할 것으로 예상했기 때문입니다.</p>
<p>이에 따라 설계한 시스템 구성도는 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/25838c90-fd6a-4964-82d2-87192c189062/image.png" alt=""></p>
<h2 id="ec2-인스턴스-생성">EC2 인스턴스 생성</h2>
<p>먼저 DB와 Spring을 배포하기에 앞서서 가상의 컴퓨터인 인스턴스를 생성하는 과정이 필요합니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/f68b99f0-448d-4aea-9ade-3227c4c767ff/image.png" alt=""></p>
<p>인스턴스를 생성하는 과정은 구글에 검색을 하면 잘 나타나기에 생략하겠습니다. 이번에 배포를 하면서 느낀 거지만 spring은 적어도 메모리가 2GB인 인스턴스를 선택해야 한다는 것을 느꼈습니다. 처음에는 t2.micro 인스턴스(메모리 1GB)를 생성하고 그 위에서 build를 시도했을 때 메모리가 부족해서 build가 온전히 끝나지 못하고 중간에 멈추는 현상이 발생했습니다.</p>
<p>결론적으로 DB 서버는 프리티어인 t2.micro 인스턴스를 활용했고 운영 서버는 t2.small 인스턴스를 활용했습니다.</p>
<p>인스턴스를 생성할 때 가장 중요한 부분은 인바운드 규칙을 생성하는 부분이라고 생각합니다. 인바운드 규칙은 해당 인스턴스가 외부에서 수신할 수 있는 트래픽을 제어하는 규칙을 의미합니다. 조금 더 쉽게 이야기하면 어떤 요청들이 ec2에 접근할 수 있을지 설정하는 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/eef14f03-2fd2-4172-996c-380ff8376803/image.png" alt=""></p>
<p>현재 인스턴스 간의 요청처리는 다음과 같습니다. 외부에서 api 요청이 발생하면 운영 서버는 DB 서버에 요청을 보내 필요한 데이터를 받는 방식입니다. 즉, 여러 사용자는 운영서버로 api 요청을 보낼 수 있기에  <code>운영 서버</code>는 외부에서 여러 IP로 넘어오는 요청에 대해서 접근하는 것을 허용해주는 것이 필요한 반면 <code>DB 서버</code>는 모든 요청을 허용하는 것이 아닌 운영 서버의 요청만 접근할 수 있게 구성해야했습니다.(DB는 민감한 정보를 포함할 수 있기에 외부에서 접근할 수 있게 허용하는 것은 위험하다고 판단했습니다.)</p>
<p>운영 서버 인바운드 규칙</p>
<ul>
<li>내 ip 주소에서는 ssh 연결이 가능하도록 설정</li>
<li>모든 ip 주소에서 spring 서버 (8080 포트)로 TCP 통신이 가능하도록 설정</li>
</ul>
<p>DB 서버 인바운드 규칙</p>
<ul>
<li>운영 서버의 ip 주소를 통해 ssh 연결이 가능하도록 설정</li>
<li>운영 서버의 ip 주소만 mysql(3306 포트)로 TCP 통신이 가능하도록 설정</li>
</ul>
<h2 id="탄력적-ip-부여하기">탄력적 IP 부여하기</h2>
<p>탄력적 IP는 AWS에서 제공하는 고정 공인 IP 주소입니다. ec2 인스턴스의 경우 기본적으로 public ip가 자동으로 할당되지만 인스턴스를 재시작할 경우 ip 주소가 변경되게 됩니다. ip 주소가 변화하게 된다면 외부에서 접속하는 과정이 불편하기에 고정 Ip 주소를 설정해주는 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/8be4b629-a8b6-4c64-a4f0-6f360aa91321/image.png" alt=""></p>
<p>위의 캡쳐화면 과 같이 운영서버와 DB 서버 인스턴스 각각 탄력적 ip를 설정해주었습니다. </p>
<h2 id="서버-내부-구성하기---db">서버 내부 구성하기 - DB</h2>
<p>지금까지 서버와 DB를 돌리기 위한 컴퓨팅 자원을 생성하는 과정을 마무리 지었고 이제는 해당 컴퓨팅 자원에 우리가 원하는 프로그램을 돌리기 위해 필요한 파일들을 구성해주어야 합니다. 먼저 mysql부터 구성해보겠습니다.</p>
<p>처음 ec2가 생성되면 새로운 컴퓨터에 OS만 설치되어 있는 상태라고 보면 됩니다. 그렇기에 아무런 파일이 존재하지 않고 우리의 입맛에 맞게 설정해 주면됩니다. 지금부터는 명령어를 위주로 DB를 설정하는 방법에 대해 알아보겠습니다.</p>
<h3 id="mysql-다운로드-하기">MySQL 다운로드 하기</h3>
<pre><code class="language-bash">// mysql 다운로드
sudo apt update
sudo apt install -y mysql-server</code></pre>
<p>mysql을 설치하기 앞서서 <code>sudo apt update</code> 를 실행해 패키지를 최신화 해줍니다. 그리고 아래 명령어를 통해서 mysql server를 설치합니다. </p>
<pre><code class="language-bash">mysql --version</code></pre>
<p>mysql이 잘 설치되었는지 확인하기 위해서는 위의 명령어를 수행하면 됩니다. 잘 설치되었다면 mysql이 버전 정보와 함께 나타날 것입니다.</p>
<h3 id="mysql-사용자-만들기">MySQL 사용자 만들기</h3>
<p>Spring에서 MySQL과 통신하기 위해서는 yml 혹은 properties 파일에 설정 정보를 추가해주어야 하는데요 이 때 username과 password를 입력해주어야 합니다. root 사용자를 이용하는 방안이 있겠지만 root 사용자의 경우 최상위의 권한을 가지고 있기 때문에 악의적인 요청(다른 데이터베이스에  OO 테이블을 drop 하는 요청 등)에 대해서도 처리될 수 있는 문제가 발생할 수 있습니다.</p>
<p>그래서 저는 MySQL 내 새로운 사용자를 만들고 특정 데이터 베이스에 대한 권한만 가질 수 있도록 구성했습니다.</p>
<pre><code class="language-bash">// mysql 접속
sudo mysql -u root -p

// mysql 사용자 만들기
create user &#39;사용자 명칭&#39;@&#39;접근할 ip 주소&#39; IDENTIFIED BY &#39;비밀번호&#39;;

// mysql user 목록 보는 명령어
SELECT user, host FROM mysql.user;</code></pre>
<p><code>create user &#39;사용자 명칭&#39;@&#39;접근할 ip 주소&#39; IDENTIFIED BY &#39;비밀번호&#39;;</code> 해당 명령어를 입력하면 mysql 내부에 새로운 사용자가 만들어집니다. <code>&#39;접근할 ip 주소&#39;</code>  부분에는 해당 사용자를 이용하는 ip 주소로 설정하면 됩니다. (’%’ 입력하면 모든 ip 주소에서 해당 사용자를 이용할 수 있지만 이는 보안 문제가 발생할 수 있기에 저는 운영서버 인스턴스 ip 주소를 활용했습니다.)</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/7b0ec3da-6320-4dc4-8818-f86063d78169/image.png" alt=""></p>
<p><code>SELECT user FROM mysql.user;</code>  명령어를 입력하면 위의 사진처럼 사용자의 목록이 나타나며 이번에 추가 dayone이 추가된 것을 볼 수 있습니다.</p>
<h3 id="mysql-사용자에게-권한-부여하기">MySQL 사용자에게 권한 부여하기</h3>
<p>dayone 사용자는 root 사용자가 아니기 때문에 모든 database에 접근할 수 있는 권한이 없습니다. 그렇기에 dayone 사용자가 database에 접근하고 쓰기 작업이 가능할 수 있도록 권한을 주어야 합니다.</p>
<pre><code class="language-bash">// 사용자에게 database에 권환 제공
GRANT &#39;권한 정보&#39; ON &#39;database 명칭&#39;.* TO &#39;사용자 명칭&#39;@&#39;접근할 ip 주소&#39;;
FLUSH PRIVILEGES;</code></pre>
<p>GRANT 명령어를 통해 사용자에게 권한 정보를 제공해주면 됩니다. 서비스를 운영하는데 있어서 데이터를 쓰고 수정하고 삭제하고 조회하는 기능이 필요하여 저는 새로 만든 사용자에게 SELECT, INSERT, UPDATE, DELETE 권한을 주었습니다.</p>
<h3 id="mysql-외부에서-접근-가능하도록-구성하기">MySQL 외부에서 접근 가능하도록 구성하기</h3>
<p>mysql을 설치하면 기본적으로 내부(localhost)에서만 접근한 허용하도록 설정파일이 구성되어 있습니다. 현재 우리는 운영 서버 인스턴스에서 db 인스턴스로 접근하는 구조를 가지고 있기에 외부에서도 mysql에 접근할 수 있게 허용해주어야 합니다.</p>
<pre><code class="language-bash">sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf</code></pre>
<p>위의 명령어를 ec2에 입력하면 mysql의 세팅파일을 볼 수 있습니다. 거기서 <code>bind-address</code>를 원하는 ip 주소로 설정하여 접근할 수 있게 합니다. (0.0.0.0으로 설정하면 모든 ip에서 접근이 가능합니다.</p>
<p>지금까지의 과정을 순차적으로 처리하면 mysql에 대한 설정을 마무리가 된 것입니다. 우리는 이제 운영서버를 설정하는 과정에 대해서 살펴보겠습니다.</p>
<h2 id="서버-내부-구성하기---spring">서버 내부 구성하기 - spring</h2>
<p>spring을 인스턴스에 구성하는 것은 DB에 비해 간단합니다. 대신 CD(Continuous Deployment)에 사용할 수 있는 배포스크립트를 구성하는데 집중했습니다.</p>
<h3 id="java-설치하기">Java 설치하기</h3>
<pre><code class="language-bash">sudo apt install -y openjdk-필요한 버전-jdk</code></pre>
<p>spring을 실행하기에 앞서서 java가 필요하기 때문에 자신이 필요한 버전에 맞는 자바를 설치하면 됩니다. 저는 17 버전이 필요하여 17버전을 다운로드 하였습니다.</p>
<h3 id="프로젝트-clone-다운로드-받기">프로젝트 clone 다운로드 받기</h3>
<pre><code class="language-bash">git clone &lt;repo 주소&gt;</code></pre>
<p>자신의 프로젝트를 로컬 환경에 클론하듯이 ec2 서버에도 해주면 됩니다.</p>
<h3 id="jar-파일-생성하기">jar 파일 생성하기</h3>
<p>로컬 환경에서는 인텔리제이나 이클립스 ide를 통해서 실행을 하지만 ec2 내부에는 해당 툴이 존재하지 않게 jar 파일을 build를 통해 생성하고 해당 jar 파일을 실행주어서 동작시켜야 합니다.</p>
<pre><code class="language-bash">./gradlew clean test
./gradlew test
.. 등</code></pre>
<p>jar 파일이 생성되었다면 이를 실행하면 비소로 우리가 원하던 작업을 배포 과정을 마무리 할 수 있습니다.</p>
<h3 id="yml-관리하기">yml 관리하기</h3>
<p>DayOne 프로젝트는 네이버 책 검색 api, jwt 사용 등 민감한 정보를 따로 분리해서 관리하고 있었습니다. submodule를 이용하는 방법도 있겠지만 전 프로젝트에서 활용했을 때 버전이 맞지 않은 문제가 종종 발생해 속을 썩인 경험이 있어서 이번에는 아예 따로 분리해서 관리했습니다. </p>
<pre><code class="language-bash">nohup java -jar /build/libs/*SNAPSHOT.jar --spring.config.location=/home/ubuntu/application-prod.yml</code></pre>
<p>프로젝트를 실행하려면 해당 정보들이 필요하기에 해당 정보를 포함하는 yml 파일을 ec2에 생성하고 jar파일을 해당 yml로 실행시키면 비로소 우리가 원하는 장면이 등장하게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/9241b429-1b22-4656-bb32-54fb2fa21ed2/image.png" alt=""></p>
<h3 id="배포스크립트-작성하기">배포스크립트 작성하기</h3>
<p>배포는 마무리가 되었지만 새로운 기능이 추가될 때마다 ec2에서 접근해서 최신 버전을 pull 받고 기존의 jar 파일을 삭제하고 다시 build하고 기존의 서버를 내리고 새로운 서버를 올리는 과정을 반복하는 것은 비효율적인 방법이라 느껴져서 한번에 실행할 수 있는 배포스크립트를 구성해보았습니다.</p>
<pre><code class="language-bash"># 1. /home/ubuntu 에 있는 .jar 확장자를 가진 파일 삭제
rm -f /home/ubuntu/*.jar

# 2. 프로젝트 이름/build/libs에 있는 모든 .jar 파일 삭제
rm -f ~/&#39;프로젝트 이름&#39;/build/libs/*.jar

# 3. 프로젝트 디렉토리로 이동
cd ~/&#39;프로젝트 이름&#39; || exit

# 4. GitHub에서 최신 코드 가져오기 (pull)
git pull origin main

# 5. 프로젝트 빌드 (Gradle)
./gradlew clean build

# 6. 실행 가능한 JAR 파일만 /home/ubuntu 로 이동
mv build/libs/*SNAPSHOT.jar /home/ubuntu/

# 7. 기존 8080 포트에서 실행 중인 프로세스 종료

PID=$(lsof -t -i:8080)
if [ -n &quot;$PID&quot; ]; then
    echo &quot;🔹 Killing process with PID: $PID&quot;
    kill -9 $PID
else
    echo &quot;🔹 No process found on port 8080&quot;
fi

# 8. 새로운 JAR 파일 실행 (application-prod.yml 적용)
nohup java -jar /home/ubuntu/*.jar --spring.config.location=/home/ubuntu/application-prod.yml</code></pre>
<p>총 8단계가 진행되며 각 단계에 대해서 간단하게 설명하겠습니다.</p>
<ol>
<li>/home/ubuntu에 있는 구 버전의 jar 파일을 삭제하는 과정입니다.</li>
<li>/build/libs 안에 있는 모든 jar 파일을 삭제하는 과정입니다.</li>
<li>프로젝트 내부로 이동</li>
<li>최신 버전을 pull 받기</li>
<li>build 수행 </li>
<li>새롭게 생긴 jar 파일을 /home/ubuntu로 이동</li>
<li>기존에 수행되고 있는 프로세스 종료하기</li>
<li>새로 생긴 jar로 서버 실행하기</li>
</ol>
<p>해당 스크립트를 구성한 이후로는 <code>./deploy.sh</code> 만 입력하면 해당 과정을 순차적으로 처리해주어서 배포효율성을 높일 수 있었습니다. (deploy.sh에 실행권한을 주어야 합니다.) </p>
<h2 id="느낀점">느낀점</h2>
<p>지금껏 항상 배포를 하게 되면 구글에 사람들이 배포한 과정을 찾아보면서 오랜 시간이 걸렸는데 오늘 작성한 글을 통해서 앞으로는 배포할 때 제 글을 참고하는 날이오면 좋겠다는 생각을 했습니다. 그리고 그전에는 서비스를 배포 한다라는 것에 집중해 그동안 명령어가 무엇을 의미하는지 잘 모른 채 이용했다면 이번에는 각각 정리해보면서 어떤 의미를 가지는지를 파악하는 시간이 되었습니다.</p>
<p>다음에는 배포 과정에서 구성한 배포스크립트를 이용해서 CD(Continuous Deployment) 과정을 수행해보도록 하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[어떻게 하면 중복 예약이 발생하지 않는 시스템을 구축할 수 있을까?]]></title>
            <link>https://velog.io/@seokhwan-an/%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%98%EB%A9%B4-%EC%A4%91%EB%B3%B5-%EC%98%88%EC%95%BD%EC%9D%B4-%EB%B0%9C%EC%83%9D%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%84-%EA%B5%AC%EC%B6%95%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@seokhwan-an/%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%98%EB%A9%B4-%EC%A4%91%EB%B3%B5-%EC%98%88%EC%95%BD%EC%9D%B4-%EB%B0%9C%EC%83%9D%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%84-%EA%B5%AC%EC%B6%95%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Fri, 14 Feb 2025 07:53:45 GMT</pubDate>
            <description><![CDATA[<p>초록 스터디 예약 시스템 미션을 수행하면서 중복 예약을 막는 법에 대해서 스터디원들과 논의할 수 있는 시간을 가졌습니다.</p>
<p>미션의 목표는 Spring 입문자들이 Spring을 익숙하게 활용할 수 있도록 돕는 것이었으며 주요 내용은 예약 시스템에 예약 데이터를 추가하는 것이었습니다. 저는 스터디원들과 spring 사용 방법을 넘어서 예약 시스템 구축하는 데 핵심인 중복 예약에 대해 어떻게 해결할 수 있을지에 대해서 이야기를 나누었습니다. </p>
<h2 id="문제-정의">문제 정의</h2>
<p>해결책을 이야기 하기 앞서서 중복 예약 문제가 발생할 수 있는 시나리오에 대해서 먼저 정의했습니다. 크게 2가지 시나리오가 도출되었습니다.</p>
<ol>
<li>이미 존재하는 시간, 날짜에 예약이 들어온 경우</li>
<li>동시 타이밍에 같은 날짜와 시간에 예약이 들어온 경우</li>
</ol>
<p>위의 두 시나리오에 대해서 해결 방안에 대해서 이야기를 나누어보았습니다.</p>
<h2 id="문제-해결법">문제 해결법</h2>
<h3 id="이미-존재하는-시간-날짜에-예약이-들어온-경우">이미 존재하는 시간, 날짜에 예약이 들어온 경우</h3>
<ol>
<li><p>관리자가 새로운 예약을 추가한다.</p>
</li>
<li><p>저장소(메모리 혹은 db)에서 예약이 이미 존재하는지 파악</p>
<p> 2 - 1. 이미 존재하는 경우 예외처리</p>
<p> 2 - 2. 그렇지 않는 경우는 예약 추가를 하는 방안에 대해서</p>
</li>
</ol>
<p>Reservation.java (편하게 다루기 위해서 date와 time은 String으로 처리하겠습니다.)</p>
<pre><code class="language-java">public class Reservation {

    private final Long id;
    private final String name;
    private final String date;
    private final String time;

    public Reservation(Long id, String name, String date, String time) {
        this.id = id;
        this.name = name;
        this.date = date;
        this.time = time;
    }
}</code></pre>
<p>Reservations.java</p>
<pre><code class="language-java">public class Reservations {

    private final List&lt;Reservation&gt; reservations;

    public Reservations(List&lt;Reservation&gt; reservations) {
        this.reservations = reservations;
    }   
}</code></pre>
<p>위와 같이 Reservation과 여러 Reservation을 관리하는 일급 컬랙션인 Reservations가 있다면 중복 예약을 막는 방법은 간단하게 처리할 수 있습니다. 먼저 Reservation이 같은 날짜와 시간을 가지는 경우에 대해서 같은 객체로 인식할 수 있게 equals와 hashCode를 재정의 합니다.</p>
<p>Reservation.java에 추가할 부분</p>
<pre><code class="language-java">@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Reservation that = (Reservation) o;
    return Objects.equals(date, that.date) &amp;&amp; Objects.equals(time, that.time);
}

@Override
public int hashCode() {
    return Objects.hash(date, time);
}</code></pre>
<p>equals를 재정의하면 Collection을 이용할 때 객체의 주소 값이 아닌 원하는 필드 값으로 비교가 용이해져 코드의 가독성이 보다 높아집니다.</p>
<p>Reservations.java에 새로운 예약 시간을 추가하는 기능 구현</p>
<pre><code class="language-java">public void addReservation(Reservation reservation) {
    if (reservations.contains(reservation)) {
        throw new IllegalArgumentException(&quot;이미 해당 날짜와 시간에 예약이 존재합니다.&quot;);)
    }
    reservations.add(reservation);
}</code></pre>
<p>Reservation 리스트에 새롭게 추가할 reservation이 존재하는지 먼저 확인하고 존재하지 않는다면 새로운 예약을 추가합니다. 이와 같이 손쉽게 해당 중복 예약 시나리오를 보다 쉽게 해결할 수 있습니다. 여기서 조금 더 나아가 스터디 원들과 적절한 Collection 선택에 대해서도 고민해보았습니다. 현재 Reservations는 contains()를 통해 예약이 이미 존재하는지 파악하고 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/fab1a548-4e0b-41b3-8c6b-cdc54dba02b7/image.png" alt=""></p>
<p>List의 Contains() 내부 동작 코드를 보면 for문을 통해 직접 탐색하는 것을 볼 수 있습니다. 그렇기에 O(N)의 시간이 발생합니다. 여기서 기존 예약 객체의 개수가 적다면 성능 상에 문제가 없겠지만 점점 예약이 많아지게 되면 성능 저하가 발생할 수 있을 것입니다. 대신 HashSet이나 Hash 기반의 자료 구조를 이용한다면 O(1)에 시간으로도 같은 날짜와 시간의 예약이 이미 존재하는지 파악할 수 있다는 것을 스터디원들에게 공유했습니다.</p>
<h3 id="동시-타이밍에-같은-날짜-시간을-원하는-예약이-들어온-경우">동시 타이밍에 같은 날짜, 시간을 원하는 예약이 들어온 경우</h3>
<p>먼저 스터디 원들에게 테스트 코드를 통해 앞선 포함여부 처리만 가지고는 중복 예약을 막을 수 없다는 것을 보여주었습니다. </p>
<pre><code class="language-java">@DisplayName(&quot;동시 타이밍의 요청에서 중복 예약이 발생할 수 있다.&quot;)
@Test
void createDuplicationReservation_ShouldFail() throws InterruptedException {
    // given
    Reservations reservations = new Reservations(new ArrayList&lt;&gt;());

    // 동시 실행할 스레드 개수
    int reserveCount = 1000;
    final ExecutorService executorService = Executors.newFixedThreadPool(100);
    final CountDownLatch countDownLatch = new CountDownLatch(reserveCount);

    for (int i = 0; i &lt; reserveCount; i++) {
        executorService.submit(() -&gt; {
            try {
                reservations.addReservation(new Reservation(1L, &quot;예약자&quot;, &quot;2023-08-05&quot;, &quot;15:40&quot;));
            } catch (Exception e) {
            } finally {
                countDownLatch.countDown();
            }
        });
    }

    countDownLatch.await();

    // then
    assertThat(reservations.getReservations()).hasSize(1); // 동시성 문제로 인해 중복 데이터가 발생해야 한다
}</code></pre>
<p>해당 테스트를 실행하면 다음과 같은 예외가 발생합니다.(중복 예약 상황이 자주 발생할 것이라 생각했지만 생각보다 자주 발생하지 않고 간헐적으로 발생합니다.)</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/680f3bf2-9f5e-473c-81a7-39661ac657c5/image.png" alt=""></p>
<p>즉, 두 개의 동일한 Reservation 객체가 Reservations에서 포함된 상황입니다. </p>
<p>해당 문제가 발생하게 된 원인은 무엇인지 살펴보겠습니다. 우리는 여러 addReservation()이 발생할 때 addReservation이 하나씩 하나씩 처리되는 것을 기대할 것입니다. 하지만 이는 실제 동작과는 다릅니다. 지금과 같이 두개의 중복 예약이 발생한 상황을 자세하게 보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/d4da89ff-463e-4089-a971-6e3cab0a137c/image.png" alt=""></p>
<p>위의 그림처럼 두 쓰레드가 동시에 if문을 통과하면서 중복 요청이 발생하는 것입니다. 자바에서는 요청을 동기화하는 기법으로 synchronized 키워드를 활용할 수 있습니다. </p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/b73fed9e-61f8-4914-9513-92602bcf21ae/image.png" alt=""></p>
<p><code>synchronized</code>를 이용하면 두 쓰레드가 해당 메소드에 동시에 접근을 할 수 있는 것이 아닌 순차적으로 접근을 하게 됩니다. 그렇기에 우리가 의도했던 방향인 하나의 요청이 마무리 되고 다음 요청이 처리되어 중복 예약이 발생하지 않게 됩니다. 다만 synchronized의 경우 동시 요청 처리를 포기한 만큼 성능 저하가 발생할 수 있기에 synchronized를 붙이는 메소드는 많은 양의 일처리를 하지 않게 구성하는 것이 좋을 것입니다.</p>
<p>synchronized의 이용 방법에 대해 알아보고자 한다면 <a href="https://www.baeldung.com/java-synchronized">https://www.baeldung.com/java-synchronized</a> 를 참고하시면 좋을 것 같습니다.</p>
<h2 id="서비스가-발전해서-분산시스템을-확장된다면">서비스가 발전해서 분산시스템을 확장된다면</h2>
<p>먼저 분산시스템이 되면 기존에 객체를 통해 예약을 관리하는 것이 어려워집니다.</p>
<p>각 서버에서 메모리에 예약을 관리하게 되면 중복된 예약이 각각 서버 내부에서는 발생하지 않겠지만 전체적인 예약 시스템 관점에서는 중복 예약이 발생하게 됩니다. 그 이유는 synchronized는 하나의 서버 내부에서 동기화 작업은 처리해주지만 다른 서버에서 처리하는 요청에 대해서는 제어를 할 수 없기 때문입니다.</p>
<p>이 때에는 동시 요청 처리 방식이 변경되어야 할 것 같습니다. 여러가지 방안이 있겠지만 스터디 원들과 이야기를 나눠본 결과는 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/a8e80e30-1ec4-4ff0-991c-f256ab85b3c1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/f6cb787c-6ea2-44c6-af46-69ee0d5f5385/image.png" alt=""></p>
<h2 id="정리">정리</h2>
<p>스터디원들과 중복 예약 처리 방법에 대해서 이야기를 나눠보면서 이론으로만 알고 있던 synchronized의 동작원리를 파악할 수 있었고 더불어 서비스가 발전해 변화하는 상황에서도 중복 예약 방지를 어떻게 해야하지에 대해서 고민할 수 있었던 시간이 되어서 좋았던 것 같습니다.</p>
<p>앞으로도 스터디원들과 지속적으로 의견을 나누고 함께 리뷰하는 과정을 통해, 서로 성장해 나가는 데 힘쓰고자 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[어떻게 하면 화면에 나타내는 데이터를 효율적으로 나타낼 수 있을까?]]></title>
            <link>https://velog.io/@seokhwan-an/%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%98%EB%A9%B4-%ED%99%94%EB%A9%B4%EC%97%90-%EB%82%98%ED%83%80%EB%82%B4%EB%8A%94-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9C%BC%EB%A1%9C-%EB%82%98%ED%83%80%EB%82%BC-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@seokhwan-an/%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%98%EB%A9%B4-%ED%99%94%EB%A9%B4%EC%97%90-%EB%82%98%ED%83%80%EB%82%B4%EB%8A%94-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9C%BC%EB%A1%9C-%EB%82%98%ED%83%80%EB%82%BC-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Tue, 04 Feb 2025 04:27:37 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/598a0af8-5db0-491b-bee4-881b2a602388/image.png" alt=""></p>
<p>DayOne 서비스를 구축하는 과정에서 BookLog(책에 대한 자신의 생각을 남긴 글) 데이터를 클라이언트(프론트)에게 제공하는 것에 대해서 프론트 분과 이야기를 나누었습니다. 기존의 legacy 코드에서는 해당 데이터들을 모두 제공하고 클라이언트(프론트) 측에서 해당 데이터를 모두 나타내는 방향으로 구현되어 있었습니다.</p>
<p>전체 데이터를 반환하는 것은 적은 데이터 량에서는 큰 문제가 없겠지만 데이터가 늘어나는 상황에서는 조회성능에 문제가 발생할 것이라고 생각했습니다. 
아직 배포가 되지 않았지만 MySQL에서 실제 데이터 10,000 개가 있을 때 성능을 살펴보았습니다.(배포 후에는 실 환경에서 테스트를 해보고자 합니다.)</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/01f790cc-0b01-43c6-aa5f-ce4512a369c9/image.png" alt=""></p>
<p>순수하게 db에서 데이터를 조회하는데 걸리는 시간이 1,106ms이고 사용자까지 전달되는 네트워크 통신까지 고려하며 1.5초가 넘는 시간이 발생할 수 있다고 판단했습니다.</p>
<p>그래서 전체 데이터를 한번에 제공하는 대신에 일정한 개수의 데이터를 제공하는 <code>페이지네이션</code>을 적용해 예상되는 조회 성능 문제를 예방하고자 했습니다.</p>
<h2 id="페이지-네이션pagination이란">페이지 네이션(Pagination)이란?</h2>
<p>페이지 네이션(Pagination)은 조회 데이터를 불러올 때 데이터를 부분적으로 불러오는 기법입니다.페이지 네이션의 방식에는 Off-set 방식과 Cursor 방식이 있었고 이 두 방식 중 어떤 방식을 DayOne 서비스에 적용할지 고민했고 결정에 앞서서 이 둘을 비교했습니다.</p>
<h3 id="offset-pagination">OffSet Pagination</h3>
<p>OffSet 페이지네이션 방식은 offset과 limit을 활용해 데이터를 페이징하는 방식입니다. <code>offset</code>은 가져올 데이터의 시작위치를 의미하며 <code>limit</code>은 보여줄 데이터의 량을 의미합니다. OffSet 페이징 방식은 많은 게시판에서 자주 활용되며 구글에서도 활용하는 방식입니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/c12873ff-dcc5-4aab-9388-9949330f5db0/image.png" alt=""></p>
<p>OffSet 페이지네이션의 SQL의 형태는 다음과 같습니다. (정렬은 있을 수도 있고 없을 수도 있습니다.)</p>
<pre><code class="language-sql">SELECT 조회할 정보
FROM 테이블
ORDER BY 정렬 컬럼
LIMIT 10 OFFSET 10;</code></pre>
<p>해당 쿼리는 offset이 10이고 10개의 데이터를 조회하는 쿼리입니다. 해당 쿼리가 실행되는 방식에 대해서 살펴보겠습니다. </p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/22c42eb9-0ec3-4152-a242-e8ffd164b546/image.png" alt=""></p>
<p>응답에 필요한 데이터를 조회하기 앞서 offset이 10인 것은 만족하기위해서 테이블 내 데이터를 순차적으로 불러온 후에 필요한 데이터를 반환하는 방식입니다. 해당 방식의 문제로는 offset의 값이 커질 수록 그만큼 데이터를 탐색하는 시간이 늘어나 성능저하가 발생할 수 있습니다.</p>
<p>데이터를 늘려가면서 확인해보겠습니다.</p>
<ul>
<li>데이터 개수가 10,000개일 때 OffSet이 9950이고 Limit이 10개인 경우 → 55ms</li>
</ul>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/5158e780-bb29-4eef-acf2-37368616bde4/image.png" alt=""></p>
<ul>
<li>데이터 개수가 1,000,000개일 때 OffSet이 999,950이고 Limit이 10개인 경우 → 305ms</li>
</ul>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/adf9d17a-8ab0-444f-88cd-e2f5b220e656/image.png" alt=""></p>
<ul>
<li>데이터 개수가 20,000,000개일 때 OffSet이 19,999,950 Limit이 10개인 경우 → 4s</li>
</ul>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/28abc4c4-9b6d-4c3b-a52f-bea57ec6e41f/image.png" alt=""></p>
<p>데이터가 많아지고 예전 데이터를 조회할 때 성능이 기하 급수적으로 낮아지는 것을 볼 수 있습니다.</p>
<p>추가적으로 OffSet 페이지네이션의 경우 최신 순 데이터를 불러오는 과정에서는 중복된 데이터 조회가 발생할 수 있다는 특징이 있습니다. 간단하게 예시를 살펴보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/aa45c9a0-36e2-46c8-9ffb-4dccc11f0e10/image.png" alt=""></p>
<ol>
<li>사용자1이 페이지1에 속한 데이터를 조회한다.</li>
<li>사용자2가 새로운 데이터를 추가한다.</li>
<li>사용자1이 페이지2에 속한 데이터를 조회한다.</li>
<li>사용자1은 페이지1에서 조회했던 데이터를 페이지2에서도 조회합니다. (이름이 최장민 이라는 데이터가 중복되어서 조회 됩니다.)</li>
</ol>
<p>해당 원인은 OffSet 방식은 특정 컬럼값 이후로 데이터를 조회하는 것이 아닌 앞선 데이터의 개수를 기반으로 데이터를 조회하다보니 발생할 수 있는 해프닝입니다. </p>
<h3 id="cursor-pagination">Cursor Pagination</h3>
<p>Cursor 페이지 네이션은 non-off set 페이지네이션이라고 불리며 Cursor와 Limit 정보를 바탕으로 데이터를 조회하는 방식입니다. Cursor는 불러올 데이터의 포인터이며 Limit은 OffSet 페이지네이션과 마찬가지로 보여줄 데이터의 개수를 의미합니다. </p>
<p>Cursor Pagination의 SQL의 형태는 다음과 같습니다.</p>
<pre><code class="language-sql">SELECT 조회할 정보
FROM 테이블
WHERE Cursor 컬럼 &gt; 특정한 포인트 정보
LIMIT 10</code></pre>
<p>해당 쿼리는 point 정보를 기반으로 10개의 데이터를 불러오는 쿼리입니다. 해당 쿼리가 어떻게 동작하는지 알아보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/aae8c497-229e-47c2-8fa4-1284fcfd0e0e/image.png" alt=""></p>
<p>다음과 같이 유저 id가 3보다 큰 유저정보 10개를 불러오는 SQL 요청이 발생하면 유저 id가 3인 컬럼을 찾을 후 응답 데이터를 순차 탐색해 반환해줍니다. WHERE절을 통한 데이터 필터 방식이기에 인덱스를 활용하면 조회 성능을 높일 수 있습니다. (인덱스를 컬럼을 설정하면 정렬이 되기 때문에 해당 조건을 빠르게 탐색할 수 있습니다.)</p>
<ul>
<li>데이터 개수가 10,000개일 때 Cursor가 9950이고 Limit이 10개인 경우 → 59ms</li>
</ul>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/a938f5d5-2da8-4b2f-84f6-10c0da0842b7/image.png" alt=""></p>
<ul>
<li>데이터 개수가 1,000,000개일 때 Cursor가 999,950이고 Limit이 10개인 경우 → 70ms</li>
</ul>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/4a35637e-940f-4533-89cb-ef6204345e0f/image.png" alt=""></p>
<ul>
<li>데이터 개수가 20,000,000개일 때 Cursor가 19,999,950이고 Limit이 10개인 경우 → 81ms</li>
</ul>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/34112f1d-b8b4-409f-b3d5-e4c5a73cb701/image.png" alt=""></p>
<p>데이터가 늘어남에도 조회 성능차이가 크게 나지 않는다는 것을 볼 수 있습니다.</p>
<h2 id="그래서-어떤-방식을-사용할-것인가요">그래서 어떤 방식을 사용할 것인가요?</h2>
<p>지금까지 Cursor 방식과 Offset 방식을 비교하며 데이터를 조회하는 방법을 살펴보았습니다. 데이터의 양이 많지 않고 데이터가 자주 생성되지 않는 경우라면 두 방식 중 어느 것을 선택해도 큰 문제가 발생하지 않을 것입니다. 현재 DayOne 서비스는 기수제로 운영되고 있고 많은 회원이 있는 상황은 아니며, 1인당 매주 최대 4개의 BookLog를 작성하도록 되어 있습니다. 이를 바탕으로 예측해보면, 최대 데이터는 주당 약 100개의 BookLog에 불과합니다. 그렇기에 데이터가 자주 생성되지 않아 Offset 방식의 문제인 중복된 데이터 조회 문제가 자주 발생하지 않을 것입니다.</p>
<p>그럼에도 불구하고 이번에는 Cursor 방식을 선택했는데, 그 이유는 “사용자 편의성”에 중점을 두었기 때문입니다. DayOne 서비스는 모바일 친화적인 UI를 제공하고 있으며, 사용자는 인스타그램이나 유튜브 쇼츠처럼 빠르게 목록을 탐색하는 방식에 익숙한 상태입니다. 이러한 사용 패턴을 고려했을 때, 무한 스크롤 방식이 적합하다고 판단했습니다.</p>
<p>반면, Offset 방식을 사용할 경우 데이터가 증가함에 따라 페이지 번호가 늘어나게 됩니다. 이는 모바일 화면에서 적절히 표시하기 어려울 뿐 아니라, 사용자가 번호를 클릭해 탐색해야 하는 불편함을 초래할 수 있습니다. 이러한 이유로 저는 Cursor 방식을 채택하여 사용자 경험을 제공하고자 했습니다.</p>
<h2 id="cursor-pagination-적용하기">Cursor Pagination 적용하기</h2>
<p>JPA에서 페이지네이션을 구현하는 것은 크게 어렵지 않았습니다. JpaRepository는 <code>PagingAndSortingRepository</code>를 상속받고 있기에 Pagable 객체를 인자로 받아 손쉽게 페이지네이션을 적용할 수 있습니다. Pagable은 JPA에서 페이징 처리를 쉽게 하기 위해 제공하는 인터페이스 입니다. Pagable에는 ‘페이지 번호’와 ‘페이지 크기’ 요소가 있는데 Cursor 페이지 네이션의 경우 페이지 번호가 중요하지 않습니다. </p>
<p>DayOne에서는 bookLog를 최신 순으로 제공하는 방식을 정책으로 결정하여 Cursor에 이용할 정보를 id로 설정했습니다. Cursor 페이지 네이션을 구현하기 위해서는 다음에 조회할 데이터 위치에 대한 정보를 클라언트 측으로 부터 받아야 합니다. 그렇기에 백엔드 쪽에서도 다음에 조회할 데이터의 존재 유무와 위치 정보를 응답으로 제공해주어야 하고 다음과 같은 응답 형색을 구성했습니다.</p>
<pre><code class="language-json">{
    &quot;next&quot; : false, // 뒤에 데이터가 존재하면 true 그렇지 않으면 false
    &quot;book_logs&quot; : [ {
      &quot;id&quot; : 1,
      &quot;passage&quot; : &quot;의미있는 구절&quot;,
      &quot;comment&quot; : &quot;내가 느낀 감정&quot;,
      &quot;like_count&quot; : 1,
      &quot;book_title&quot; : &quot;책 제목&quot;,
      &quot;created_at&quot; : &quot;2025-02-04T11:41:48.605719&quot;
    } ],
    &quot;next_cursor&quot; : -1 // 뒤에 데이터가 존재하면 다음 조회 id 제공 없으면 -1
}</code></pre>
<h3 id="repository">Repository</h3>
<p>Cursor 페이지네이션을 적용하기 위해 두가지 시나리오를 떠올렸습니다.</p>
<ol>
<li>BookLog를 처음으로 조회하는 경우</li>
<li>특정 BookLog 뒤에 이어진 데이터를 조회하는 경우</li>
</ol>
<p>1번과 같이 처음 데이터를 조회하는 경우에는 Cursor에 정보가 필요 없고 가장 최신 데이터부터 반환하는 방식을 적용했고 2번과 같이 특정 데이터 이후에 정보를 조회하기 위해서는 Cursor 정보인 id를 기준으로 뒤의 데이터를 불러오는 방식을 선택하여 다음과 같이 구현했습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/b603508f-fc32-4d9f-8670-fb4aeef9b707/image.png" alt=""></p>
<p>각 메소드들은 생성일자를 기준으로 역정렬(최신순)을 한 뒤 pageable을 통해 필요한 데이터 개수만큼 조회하는 방식입니다. 이때 반환되는 데이터 타입을 List를 하지 않고 Slice 타입을 이용했습니다. Slice 타입은 JPA에서 제공하는 페이지 처리 결과 타입 중 하나로 다음 페이지가 존재하는지 여부를 쉽게 파악할 수 있는 특징이 있습니다. (Page 객체를 이용하는 방법도 있겠지만 전체 데이터 개수, 전제 페이지 수가 필요한 것은 아니기에 Slice를 이용했습니다.)</p>
<h3 id="service">Service</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/f5510868-b3df-45eb-a381-d7093c8914db/image.png" alt=""></p>
<p>Service에서는 cursor 정보를 인자로 받아 초기 BookLog 요청인지 아닌지 파악하고 알맞은 응답을 제공해줍니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/d7d957c2-5a69-4911-972d-4795a983a5d0/image.png" alt=""></p>
<p>앞선 처음에 실험했던 동일한 데이터에 대해서 페이지네이션 쿼리를 실행했을 때 57ms가 나오는 것을 볼 수 있습니다.</p>
<h2 id="정리">정리</h2>
<p>페이지네이션을 구현하는 방식은 어렵지 않았습니다. 하지만 이번 경험을 통해 전체 데이터를 어떻게 효율적으로 제공할 수 있을까에 대해서 고민하면서 페이지네이션을 접했고 이를 구현한 두 가지 방식에 대해 학습할 수 있습니다. </p>
<p>페이징처리 방식은 서비스가 발전하면서 변화될 수도 있겠지만 지금처럼 왜 적용하려고 하는지 잘 생각해보면서 적용하는 것이 중요할 것 같습니다. (단순히 좋아보인다고 해서 적용하는 것은 지양하고자 합니다.)</p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://stackoverflow.com/questions/55744926/offset-pagination-vs-cursor-pagination">https://stackoverflow.com/questions/55744926/offset-pagination-vs-cursor-pagination</a></li>
<li><a href="https://hudi.blog/spring-data-jpa-pagination/">https://hudi.blog/spring-data-jpa-pagination/</a></li>
<li><a href="https://medium.com/@oshiryaeva/offset-vs-cursor-based-pagination-which-is-the-right-choice-for-your-project-e46f65db062f">https://medium.com/@oshiryaeva/offset-vs-cursor-based-pagination-which-is-the-right-choice-for-your-project-e46f65db062f</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[데드락에 대해서 알아보자]]></title>
            <link>https://velog.io/@seokhwan-an/%EB%8D%B0%EB%93%9C%EB%9D%BD%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@seokhwan-an/%EB%8D%B0%EB%93%9C%EB%9D%BD%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 23 Jan 2025 09:01:18 GMT</pubDate>
            <description><![CDATA[<p>지난 글에서는 공유 자원에 대해서 여러 스레드 및 프로세스가 접근했을 때 발생하는 문제와 이를 동기화하는 방법에 대해 알아보았습니다. 이번 시간에는 데드락(Dead lock)에 대해서 알아보겠습니다.</p>
<h2 id="데드락이란">데드락이란?</h2>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/197741b6-ff9f-4543-9367-594b0338182c/image.png" alt=""></p>
<p>위의 그림은 데드락을 설명하는데 자주 이용되는 그림입니다. 그림처럼 데드락은 오도가도 못하는 상태를 의미합니다. 좀 더 전문적인 시선에서 데드락을 바라보면 데드락은 두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있는 상태입니다. 즉, 각각 서로 상대방이 가진 자원이 필요한데 이를 얻지 못해 두 작업 모두 끝을 맺지 못하는 상황을 의미합니다. 이렇게 된다면 CPU 자원의 낭비뿐만 아니라 시스템의 품질 저하를 유발하기에 데드락이 발생하는 것을 막거나 예방하는 것이 중요합니다. 먼저 데드락은 어떤 상황에서 발생하는지 알아보겠습니다. </p>
<h2 id="데드락-발생조건">데드락 발생조건</h2>
<p>데드락의 발생조건은 4가지가 있으며 해당 네 조건이 모두 만족한 상황에서만 데드락이 발생합니다. 각 조건에 대해서 알아보겠습니다.</p>
<ul>
<li>상호배제 : 자원은 하나의 프로세스 혹은 스레드만 접근할 수 있습니다.(자원을 공유해서 사용할 수 없습니다.)</li>
<li>점유대기 : 스레드 및 프로세스가 하나의 자원을 흭득한 상태로 다른 자원을 대기합니다. (한번에 필요한 자원을 모두 얻는 것이 아닌 순차적으로 자원을 얻는 상황입니다.)</li>
<li>비선점 : 다른 스레드 및 프로세스에 할당된 자원을 빼앗을 수 없습니다.(자원의 반납은 자원을 흭득한 스레드 및 프로세스만 할 수 있습니다.)</li>
<li>순환대기 : 스레드 및 프로세스들이 순환의 형태로 서로의 자원을 기다리고 있는 상태를 의미합니다.</li>
</ul>
<h2 id="데드락-해결-방법">데드락 해결 방법</h2>
<p>데드락을 해결하는 방안에는 <code>데드락 방지</code>, <code>데드락 회피</code> , <code>데드락 감지와 복구</code> , <code>데드락 무시</code> 가 있습니다. 각각에 대해서 알아보겠습니다.</p>
<h3 id="데드락-방지">데드락 방지</h3>
<p>데드락 방지 기법은 데드락의 발생조건 네 가지 중 하나가 충족되지 않게 시스템을 설계하는 방식입니다. 네 가지 조건 중 하나만 성립하지 않아도 데드락이 발생하지 않기에 어느 한 조건을 방지하는 방식으로 구현됩니다. 각각의 조건을 방지하는 방법에 대해 살펴보겠습니다. </p>
<ul>
<li>상호배제 방지 : 리소스를 여러 스레드 및 프로세스가 공유해서 사용하게 하는 방법 → 해당 방식은 사실상 불가능</li>
<li>점유대기 방지 : 스레드 및 프로세스가 작업을 시작하기에 앞서서 필요한 모든 자원을 흭득한 후에 작업을 수행 (어느 하나의 자원을 얻지 못하면 다른 필요한 자원들도 흭득하지 않는 방식) → 획득하려는 자원 중에 다른 작업에서도 자주 사용하는 자원이 있을 경우 기아현상이 발생할 수 있습니다.</li>
<li>선점 방식 : 스레드 및 프로세스가 다른 스레드 및 프로세스의 자원을 선점할 수 있는 방법</li>
<li>순환 대기 방지 : 모든 자원에 순서 체계를 부여하여 오름차순으로 자원을 흭득하는 방법</li>
</ul>
<h3 id="데드락-회피">데드락 회피</h3>
<p>다음으로는 데드락 회피 방법에 대해서 살펴보겠습니다. 데드락 회피는 말 그대로 데드락이 발생할 것 같은 상황을 피하는 방식입니다. 데드락의 발생을 회피를 하기 위해서는 추가적인 정보를 통해 데드락이 발생할 것 같은지 파악해야 합니다. (추가적인 자료에는 자원에 대한 정보, 현재 자원을 사용 여부 등이 있습니다.)</p>
<p>데드락 회피를 위한 알고리즘에는 <code>자원 할당 그래프 알고리즘</code>과 <code>은행원 알고리즘</code>이 있으며 자원 할당 그래프 알고리즘에 대해서 알아보기에 앞서서 먼저 자원 할당 그래프에 대해서 간단하게 살펴보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/6d3927b4-c844-43e6-adf1-025dec0ce7f5/image.png" alt=""></p>
<p>자원할당 그래프는 자원과 프로세스(스레드)간의 관계를 나타낸 방향 그래프입니다. 자원 → 프로세스(스레드)는 프로세스(스레드)가 자원을 현재 획득하는 것을 의미하며 프로세스(스레드) → 자원은 프로세스(스레드)가 해당 자원을 획득하고자 하는 것을 의미합니다. 해당 그래프를 통해서 교착상태가 발생하는지 아닌지를 파악할 수 있는데 이때의 전제 조건은 자원(R1과 R2)이 단일 개인 것이 보장되어야 합니다. (자원의 개수가 여러 개 이면 사이클을이 발생하더라도 항상 데드락이 발생하는 것은 아닙니다)</p>
<p>위의 그래프를 보면 R1 → T1, 이고 R2 → T2 이므로 T1은 R1을 가지고 있고 T2는 R2를 가지고 있는 상황입니다. 더불어 T1 → R2 이고 T2 → R1 이므로 T1은 R2를 필요로 하고 T2는 R1을 필요로 하는 상황으로 데드락 상태입니다. 좀 더 쉽게 파악하는 것은 해당 그래프에서 사이클이 발생하지는 파악하면 됩니다. 현재 그래프는 T1 → R2 → T2 → R1 → T1 인 사이클이 발생하기에 데드락이 발생하는 것을 알 수 있습니다.</p>
<p>자원할당 그래프 알고리즘은 해당 그래프에 예약 간선을 추가하는 것으로 예약 간선을 통해서 사이클이 생성되는 것이 감지되면 해당 과정에서 데드락이 발생할 수 있다고 판단해 자원을 제공하지 않고 사이클이 발생하지 않는 상태에서 자원을 제공하는 방식입니다. 해당 알고리즘은 프로세스(스레드)들이 필요로하는 자원에 대한 정보가 필요합니다. </p>
<p><code>은행원 알고리즘</code>의 경우 프로세스(스레드)가 작업을 시작하기에 앞서서 필요로 하는 자원에 대한 정보를 미리 알리는 방식으로 해당 자원을 할당해 주었을 때 데드락이 발생한다면 제공하지 않고 그렇지 않다면 자원을 제공해주는 방식입니다. </p>
<h3 id="데드락-감지와-복구">데드락 감지와 복구</h3>
<p>데드락 감지와 복구 방식은 일단 데드락을 막는 방식이 아닌 데드락을 허용하되 데드락이 발생하게 되면 이를 복구하는 전략입니다. 복구 전략에는 <code>프로세스 종료</code>와 <code>자원의 일시적인 선점</code>을 허용하는 방식이 있습니다. </p>
<p>프로세스를 중지하는 방법은 교착상태가 발생한 모든 프로세스를 중지시키는 방법과 교착 상태가 제거될 때까지 하나의 프로세스를 제거하는 방법이 있습니다. 프로세스를 중간에 종료하는 것은 큰 비용이 발생하는 작업이기에 다양한 요소들을 고려해야합니다. (프로세스의 우선순위, 남은 작업시간, 프로세스가 점유한 자원과 해당 자원의 수, 프로세스에 더 필요한 자원의 수, 종료되어야 하는 프로세스의 수 등)</p>
<p>자원의 일시적인 선점 방식은 교착상태가 사라질 때까지 한 프로세스의 자원을 다른 프로세스에게 제공하는 방식입니다. 자원 선점 방식을 활용하기 위해서는 고려할 점이 있습니다. 먼저 희생자가 선택될 때에는 작업 비용이 적은 프로세스(스레드)를 선정해야한다는 것입니다. 두 번째는 같은 프로세스가 여러번 희생자가 되지 않는 것입니다. </p>
<h3 id="데드락-무시">데드락 무시</h3>
<p>데드락 무시는 OS 레벨에서 데드락에 대해서 무시하는 방식입니다. 데드락의 처리는 프로그램 부분에서 처리하게 하는 방식으로 무책임하다고 느낄 수 있지만 많은 OS가 채택하고 있는 방식입니다.</p>
<h2 id="정리">정리</h2>
<ul>
<li>데드락은 2개 이상의 프로세스 및 스레드가 서로의 자원을 기다리면서 아무런 작업을 수행하고 있지 않는 상태를 의미합니다.</li>
<li>데드락의 조건에는 상호배제, 점유 및 대기, 비선점, 환형 대기가 있습니다.</li>
<li>데드락을 해결하기 위한 방식에는 예방, 회피, 감지 및 회복, 무시 방법이 있습니다.</li>
<li>예방은 데드락 4가지 조건 중 1개의 조건을 예방해 데드락을 해결하는 방식입니다.</li>
<li>회피는 추가적인 정보를 바탕으로 데드락이 발생할 것을 예측하고 피하는 방식입니다. (자원 할당 그래프 알고리즘, 은행원 알고리즘)</li>
<li>감지 및 회복 방식은 데드락이 발생한 후에 이를 해결하는 방법으로 모든 프로세스를 종료시키는 방법과 자원의 일시적인 선점을 허용하는 방식이 있습니다.</li>
<li>무시는 데드락의 발생을 OS 레벨에서는 무시하고 이는 프로그램 레벨에서 처리하도록 하는 방식입니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[공유 자원에 대해서 여러 프로세스(스레드)들은 어떻게 동작할까?]]></title>
            <link>https://velog.io/@seokhwan-an/%EA%B3%B5%EC%9C%A0-%EC%9E%90%EC%9B%90%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%97%AC%EB%9F%AC-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EC%8A%A4%EB%A0%88%EB%93%9C%EB%93%A4%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@seokhwan-an/%EA%B3%B5%EC%9C%A0-%EC%9E%90%EC%9B%90%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%97%AC%EB%9F%AC-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EC%8A%A4%EB%A0%88%EB%93%9C%EB%93%A4%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Tue, 21 Jan 2025 08:00:52 GMT</pubDate>
            <description><![CDATA[<p>이전 글(<a href="https://velog.io/@seokhwan-an/%EC%97%AC%EB%9F%AC-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EB%8A%94-%EC%96%B4%EB%96%A4-%EC%88%9C%EC%84%9C%EB%A1%9C-%EB%8F%99%EC%9E%91%ED%95%A0%EA%B9%8C)%EC%9D%84">https://velog.io/@seokhwan-an/여러-프로세스는-어떤-순서로-동작할까)을</a> 통해서는 운영체제가 여러 프로세스들이 한정된 자원인 Cpu을 이용하도록 스케줄링하는 방법에 대해서 알아보았습니다. CPU 같은 경우는 스케줄링 기법을 통해 우선순위를 통해 프로세스의 작업 순서를 정하고 CPU를 할당해주었습니다. 그러면 다른 공유 자원에 대해서는 여러 프로세스들이 접근해서 이용하려고 하면 어떻게 작동할까요? 스케줄링과 같이 여러 프로세스들이 순차적으로 동작하게 하면 되지 않을까요? 이번 글에서 알아보고자 합니다.</p>
<h2 id="경쟁-조건race-condition과-임계-영역critical-section">경쟁 조건(race condition)과 임계 영역(critical Section)</h2>
<p>경쟁 조건은 여러 프로세스나 스레드가 동시에 공유 자원에 접근해 데이터를 이용할 때 접근 순서에 따라 결과가 달라지는 상황을 의미합니다. 티켓팅 사이트를 예를 들어서 간단히 살펴보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/452ce66c-71f6-49f3-8d2a-6fd5f2130368/image.png" alt=""></p>
<p>두 스레드가 티켓이라는 공유 자원에 접근해서 예매를 하는 경우를 보겠습니다. 저희가 기대하는 동작은 한 스레드만 티켓팅만 성공하는 것일 겁니다. 하지만 해당 그림을 보면 두 스레드 모두 성공하는 상황이 발생합니다.</p>
<ol>
<li>스레드1이 티켓 수량을 조회합니다. (티켓 수량: 1)</li>
<li>스레드2가 티켓 수량을 조회합니다. (티켓 수량: 1)</li>
<li>스레드1이 예매 시도 및 성공 (조회한 티켓 수량이 1이상이기 때문)</li>
<li>스레드2가 예미 시도 및 성공 (조회한 티켓 수량이 1이상이기 때문)</li>
</ol>
<p>이렇게 되면 존재하는 티켓 개수와 티켓 예매 데이터의 개수가 다른 데이터 정합성 문제가 발생합니다. </p>
<p>해당 문제의 원인은 두 스레드가 티켓이라는 자원에 동시에 접근해서 발생한 문제입니다. 그러면 하나의 스레드가 우선 예매 작업을 수행하고 완료된 후 다른 스레드가 이를 처리하면 저희가 의도했던 동작이 이루어질 것입니다. 이를 <code>동기화</code>라고 합니다.
<code>임계 영역</code> 은 공유자원을 활용하기 위해 접근하는 공간이며 임계 영역에는 동기화 방식에 따라 하나 혹은 정해진 수 만큼의 스레드 및 프로세스가 접근 가능합니다.</p>
<h2 id="동기화를-처리하는-방법">동기화를 처리하는 방법</h2>
<p>프로세스 및 스레드간 동기화 작업은 상호배제를 통해 실현됩니다. 상호 배제란 하나의 공유 자원에 대해 한 번에 하나의 프로세스 및 스레드만 접근할 수 있도록 하는 메커니즘으로 lock을 이용해 구현됩니다. </p>
<p>상호 배제를 구현하는 방식은 스핀락, 뮤텍스, 세마포어가 있고 하나씩 알아보겠습니다. 모든 방식의 원리는 락 흭득 → 임계 영역 실행 → 락 반납 형식으로 동작합니다.</p>
<h3 id="스핀락spinlock">스핀락(SpinLock)</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/f3d6d10f-9126-4722-ae10-64c7ff3a620d/image.png" alt=""></p>
<p>스핀락은 스레드가 락을 얻을 때까지 무한 루프를 돌며 기다리고 락을 얻은 하나의 스레드 및 프로세스만 작업을 하는 상호배제 기법입니다. 스핀락의 특징은 락을 얻기까지 여러 스레드나 프로세스가 휴면 상태로 가는 것이 아닌 무한루프를 돌며 활성화된 상태라는 특징이 있습니다. 이를 <code>Busy Wait</code>이라고 합니다.</p>
<p>Busy Wait은 문맥 교환이 필요없기에 스레드 및 프로세스간의 문맥 교환이 자주 일어나는 곳에서는 오버헤드가 발생하지 않는다는 장점이 있습니다. 다만, 스레드나 프로세스가 활성화 상태라는 것은 cpu 자원을 활용하는 것을 의미하기에 임계영역의 작업시간이 길어져 대기하는 스레드 및 프로세스가 많아지게 되면 cpu의 자원이 낭비되는 문제가 있습니다.</p>
<h3 id="뮤텍스mutex">뮤텍스(Mutex)</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/12b94e4a-f71c-4568-bc61-77da4afdaafb/image.png" alt=""></p>
<p>뮤텍스는 단일 공유 자원에 대해서 상호 배제를 적용한 기법입니다. 공유 자원에 접근을 하면 락을 흭득하고 작업을 마무리 하면 락을 반납합니다. 공유 자원에 락을 설정하는 경우 다른 스레드 혹은 프로세스는 공유 자원에 접근이 불가능합니다. 뮤텍스의 특징은 락을 적용하는 주체와 해제하는 주체가 같다는 특징이 있습니다. 즉, lock을 반납하는 주체를 예측할 수 있습니다.</p>
<p>뮤텍스의 경우 락을 흭득하지 못한 프로세스나 스레드의 경우 큐에서 관리되며 앞선 스레드 및 프로세스의 작업이 마무리가 된다면 큐 중에 하나의 스레드 및 프로세스를 활성화 합니다. 이를 <code>Non Busy Wait</code>이라고 합니다. (공유 자원을 흭득하지 못한 스레드 및 프로세스에 대해서 cpu 낭비를 하지 않습니다.)</p>
<h3 id="세마포어semaphore">세마포어(Semaphore)</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/1b8cf791-fe6a-44ec-8e9e-6d7d4585008d/image.png" alt=""></p>
<p>뮤텍스는 한 번에 하나의 스레드 및 프로세스가 임계 영역에 접근할 수 있는 방식이라면 세마포어는 카운팅 매커니즘을 이용해 동시에 여러 스레드 및 프로세스가 자원에 접근할 수 있는 방식입니다. (정수값을 통해 공유자원의 접근 스레드 및 프로세스를 관리합니다.)</p>
<p>스레드나 프로세스가 공유 자원에 접근하려면 wait을 통해 lock을 흭득합니다. lock을 흭득한 스레드나 프로세스가 생기면 정수값을 감소시키고 정수값이 0이되면 더이상 스레드 및 프로세스를 접근하지 못하게 막습니다. 작업을 마무리 한 스레드 및 프로세스가 signal을 통해 lock을 반납한 후에 대기중인 스레드 및 프로세스를 활성화해 lock을 흭득하는 방식으로 동작합니다.</p>
<p>접근할 수 있는 스레드 및 프로세스가 1개인 세마포어를 binary 세마포어라고 하고 접근할 수 있는 스레드 및 프로세스가 1개보다 많은 경우를 Counting 세마포어라고 합니다.</p>
<h3 id="스핀락-vs-뮤텍스-vs-세마포어">스핀락 VS 뮤텍스 VS 세마포어</h3>
<table>
<thead>
<tr>
<th><strong>특징</strong></th>
<th><strong>스핀락 (Spinlock)</strong></th>
<th><strong>뮤텍스 (Mutex)</strong></th>
<th><strong>세마포어 (Semaphore)</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>기본 개념</strong></td>
<td>반복적으로 잠금 상태를 확인하며 잠금 해제를 기다림</td>
<td>단일 소유자만 접근 가능한 공유 자원 잠금</td>
<td>카운터를 기반으로 다수의 스레드가 접근할 수 있는 공유 자원 제어</td>
</tr>
<tr>
<td><strong>잠금 대상</strong></td>
<td>1개의 스레드</td>
<td>1개의 스레드</td>
<td>N개의 스레드 (동시 접근 제한 가능)</td>
</tr>
<tr>
<td><strong>잠금 획득 방식</strong></td>
<td>루프를 통해 잠금 상태를 확인하며 획득</td>
<td>잠금 상태 변경 (Lock/Unlock 메서드 호출)</td>
<td>카운터 감소 (0이면 대기)</td>
</tr>
<tr>
<td><strong>잠금 해제 방식</strong></td>
<td>잠금 상태를 확인하여 직접 해제</td>
<td>소유자가 직접 해제</td>
<td>카운터 증가</td>
</tr>
<tr>
<td><strong>CPU 효율성</strong></td>
<td>Busy Wait으로 Cpu 낭비 발생 O</td>
<td>Non Busy Wait으로 대기의 과정에서 CPU 낭비 발생 X</td>
<td>Non Busy Wait으로 대기의 과정에서 CPU 낭비 발생 X</td>
</tr>
<tr>
<td><strong>컨텍스트 스위칭</strong></td>
<td>컨텍스트 스위칭 없음 (대기 시간 길 경우 비효율적)</td>
<td>대기 중 컨텍스트 스위칭 발생</td>
<td>대기 중 컨텍스트 스위칭 발생</td>
</tr>
</tbody></table>
<h2 id="정리">정리</h2>
<ul>
<li>공유 자원에 여러 쓰레드나 프로세스가 접근하게 되면 의도하지 아는 동작이 발생할 수 있다.</li>
<li>race condition은 경우 자원에 동시에 접근하는 스레드 및 프로세스에 대하여 접근 순서에 따라 결과가 달라지는 현상을 의미합니다.</li>
<li>critical section의 경우 공유자원에 접근 하는 영역을 의미합니다.</li>
<li>여러 스레드 및 프로세스가 공유자원의 사용을 순차적으로 하는 방식을 동기화라고 하면 동기화는 상호배체를 통해서 실현됩니다. (Lock을 사용)</li>
<li>상호배제 기법에는 스핀락, 뮤텍스, 세마포어가 존재합니다.</li>
<li>스핀락과 뮤텍스는 모두 1개의 스레드 및 프로세스만 공유자원에 접근할 수 있는 상호배제 기법이며 세마포어는 특정 정수값을 활용해 여러 스레드 및 프로세스가 공유자원에 접근 가능합니다.</li>
<li>스핀라과 뮤텍스의 차이는 스핀락은 대기중인 스레드 및 프로세스가 활성화 되어 있는 반면 뮤텍스의 경우 대기중인 스레드 및 프로세스는 비활성화 상태입니다. → 스핀락은 대기하는 스래드 및 프로세스가 많아질 수록 CPU낭비가 발생합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[여러 프로세스는 어떤 순서로 동작할까?]]></title>
            <link>https://velog.io/@seokhwan-an/%EC%97%AC%EB%9F%AC-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EB%8A%94-%EC%96%B4%EB%96%A4-%EC%88%9C%EC%84%9C%EB%A1%9C-%EB%8F%99%EC%9E%91%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@seokhwan-an/%EC%97%AC%EB%9F%AC-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EB%8A%94-%EC%96%B4%EB%96%A4-%EC%88%9C%EC%84%9C%EB%A1%9C-%EB%8F%99%EC%9E%91%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Thu, 16 Jan 2025 06:56:10 GMT</pubDate>
            <description><![CDATA[<p>운영체제의 핵심은 하드웨어 자원을 응용프로그램으로부터 보호하는 것도 있지만 여러 프로그램들이 공정하게 하드웨어 자원(CPU, 메모리)등을 균등하게 사용할 수 있게 도와주는 역할을 합니다. 우리의 컴퓨터를 보면 여러가지가 프로그램이 프로세스의 형태로 메모리에 적재되어 있는 것을 확인할 수 있습니다. 그리고 하나의 코어를 가진 CPU는 프로세스를 한번에 하나씩만 처리를 할 수 있습니다. 그러면 운영체제는 어떻게 프로세스들에게 순서를 정해주는지 알아보겠습니다.</p>
<h2 id="cpu-스케줄링이란">CPU 스케줄링이란?</h2>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/573df534-c44a-4985-bbda-6a9ed8ccf20f/image.png" alt=""></p>
<p>우리는 앞선 <a href="https://velog.io/@seokhwan-an/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B4%80%EB%A6%AC-%EB%90%A0%EA%B9%8C">프로세스는 무엇이고 어떻게 관리될까</a> 포스트를 통해서 프로세스는 여러 상태를 가진다는 것을 배웠습니다. 그 중에서 운영체제는 <code>Ready</code> 상태에 있는 프로세스들 중에서 가장 우선순위가 높은 프로세스에게 CPU를 부여합니다. 이 과정에서 <code>Dispatcher</code> 를 통해 프로세스가 실행가능한 상태로 변경됩니다.(프로세스 상태를 Ready에서 Run으로 변경 및 기존에 실행되던 프로세스와 컨텍스트 스위칭 발생)</p>
<h3 id="cpu-스케쥴링의-종류">CPU 스케쥴링의 종류</h3>
<p>cpu 스케줄링은 <code>선점형 스케줄링</code> 과 <code>비선점형 스케줄링</code> 으로 구분됩니다. 선점이라는 말은 “남보다 앞서서 차지하는 것”의미하며 해당 용어를 운영체제 관점에서 바라보면 현재 실행 중인 프로세스나 태스크를 강제로 중단시키고 다른 프로세스나 태스크를 실행하는 것을 나타냅니다. 비선점 스케줄링은 기존의 작업이 마무리되고 난후에 다른 프로세스가 수행되는 스케줄링 기법입니다. 이 둘을 비교하는 표를 보면 이해가 조금더 쉬울 것 같습니다.</p>
<table>
<thead>
<tr>
<th></th>
<th><strong>선점형 스케줄링</strong></th>
<th><strong>비선점형 스케줄링</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>개념</strong></td>
<td>실행 중인 프로세스를 중단하고 우선순위가 높은 프로세스를 실행함</td>
<td>실행 중인 프로세스가 끝날 때까지 다른 프로세스로 변경하지 않음</td>
</tr>
<tr>
<td><strong>프로세스 변경 시점</strong></td>
<td>우선순위가 높은 프로세스가 도착하거나, 특정 조건(시간 초과 등)이 발생할 때</td>
<td>현재 실행 중인 프로세스가 작업을 완료한 후</td>
</tr>
<tr>
<td><strong>응답 시간</strong></td>
<td>짧음: 우선순위가 높은 프로세스를 빠르게 처리 가능</td>
<td>길어질 수 있음: 우선순위가 낮은 프로세스가 실행 중일 경우 대기 시간이 증가</td>
</tr>
<tr>
<td><strong>복잡도</strong></td>
<td>상대적으로 높음: 문맥 교환(Context Switching)이 빈번히 발생함</td>
<td>상대적으로 낮음: 문맥 교환이 적음</td>
</tr>
<tr>
<td><strong>CPU 이용률</strong></td>
<td>더 높을 수 있음: 짧은 작업들을 빠르게 처리하여 CPU를 효율적으로 사용</td>
<td>낮을 수 있음: 긴 작업이 끝날 때까지 다른 프로세스들이 대기 상태에 머무를 수 있음</td>
</tr>
<tr>
<td><strong>예시 알고리즘</strong></td>
<td>Round Robin, SRTF(Shortest Remaining Time First), Priority Scheduling</td>
<td>FCFS(First-Come, First-Served), SJF(Shortest Job First)</td>
</tr>
<tr>
<td><strong>장점</strong></td>
<td>우선순위 높은 작업을 신속히 처리 가능, 긴급 작업에 유리</td>
<td>문맥 교환 오버헤드가 적고, 알고리즘이 단순하여 구현과 관리가 쉬움</td>
</tr>
<tr>
<td><strong>단점</strong></td>
<td>문맥 교환 오버헤드로 인해 시스템 성능 저하 가능</td>
<td>우선순위가 낮은 작업은 기아(Starvation) 상태에 빠질 수 있음</td>
</tr>
</tbody></table>
<blockquote>
<p>💡 CPU 스케줄링은 어떻게 성능을 평가할까?
CPU 스케줄링은 시스템 관점에서와 사용자 관점에서 성능 평가요소가 있습니다. 시스템 관점에서는 CPU가 프로세스를 얼만큼 잘 처리했는지를 파악하는 것이 핵심이고 사용자 관점에서는 실행한 프로세스가 원할하게 CPU를 얻고 사용했는지 파악하는 것이 핵심입니다. </p>
</blockquote>
<p>시스템 관점 성능평가 요소</p>
<ul>
<li>CPU 이용률 : 전체 시간 중에서 CPU가 일을 한 시간의 비율을 의미</li>
<li>프로세스 처리량 : 단위 시간 동안 ready 상태인 프로세스들을 얼만큼 처리했는지<blockquote>
</blockquote>
사용자 관점 성능평가 요소</li>
<li>소요시간(Turnaround Time) : 프로세스가 ready 상태로 기다린 시간 + cpu를 사용한 시간 </li>
<li>대기시간(Waiting Time) : CPU 버스트 기간 중 프로세스가 ready 상태로 기다린 시간</li>
<li>응답시간(Response Time) : CPU를 처음 얻기까지 걸린 시간<blockquote>
</blockquote>
</li>
</ul>
<h3 id="fcfsfirst-come-first-service">FCFS(First-Come First-Service)</h3>
<p>편의점에 음료가 소비되는 것처럼 Ready 큐에 도착한 순서대로 작업을 먼저 처리하는 스케줄링 알고리즘입니다. 저희가 주로 사용하는 자료구조인 Queue를 생각하면 이해하기 쉬울 것 같습니다. FCFS는 비선점형 스케줄링으로 겉으로 보기에는 공정하다고 느껴질 수 있습니다.</p>
<p>하지만 빠르게 처리할 수 있는 프로세스가 작업시간이 긴 프로세스 뒤에 오게 되면 해당 작업이 모두 마무리 된 후에 작업을 수행할 수 있습니다. 또한 해당 알고리즘은 요청이 작업이 들어온 순서에 따라서 프로세스의 대기시간이 천차만별로 달라질 수 있다는 특징이 있습니다.</p>
<h3 id="sjfshortest-job-first">SJF(Shortest Job First)</h3>
<p>SJF는 FCFS의 단점인 작업시간이 적은 프로세스의 작업이 긴대기시간을 가질 수 있다는 점을 개선하기 위해 등장한 CPU 스케줄링 알고리즘입니다. SJF는 선점형 방식과 비선점형 방식으로 구현이 될 수 있으며 해당 알고리즘에서는 작업 처리 시간이 작은 작업을 우선을 처리합니다.</p>
<p>이상적으로 보았을 때 작업량이 적은 일부터 빠르게 처리해나가면 프로세스마다 평균 대기시간도 줄어들어 효율적인 스케줄링 기법으로 볼 수 있습니다. 하지만 지속적으로 작업시간이 적은 프로세스가 ready 큐에 들어오게 된다면 긴 작업 시간을 가진 프로세스는 CPU를 할당받지 못하는 현상이 발생할 것 입니다. 이를 <code>기아 현상(starvation)</code>이라고 합니다. </p>
<blockquote>
<p>💡 운영체제는 프로세스의 작업량(작업시간)을 어떻게 알 수 있을까?
운영체제는 프로세스의 작업처리 시간을 미리 알 수 없습니다. 그래서 이전 작업 정보를 통해서 예측하는 방안으로 프로세스의 작업시간을 파악합니다.</p>
</blockquote>
<h3 id="priority-scheduling">Priority Scheduling</h3>
<p>우선순위 스케줄링은 프로세스의 우선순위를 정해 작업을 처리하는 기법입니다. 우선순위는 정수의 값으로 관리하며 운영체제는 해당 우선순위 값을 기준으로 프로세스의 순서를 결정합니다. 우선순위 스케줄링 역시 선점형 방식과 비선점형 방식으로 구현이 가능합니다.</p>
<p>우선순위 스케줄링 기법의 경우도 SJF 알고리즘과 마찬가지로 기아 현상이 발생합니다. 예를 들면 사용자 프로세스 보다는 커널에 처리하는 프로세스가 보편적으로 우선순위가 높은데 지속적으로 커널 프로세스의 작업 요청이 발생하면 사용자 프로세스는 후순위로 밀리게 됩니다. 우선순위 스케줄링을 이와 같은 문제를 해결하기 위해서 <code>Aging</code> 기법을 도입해 기아현상을 발생하지 않도록 도와줍니다. Aging 기법은 프로세스가 대기 큐 속에서 우선순위가 계속 유지되는 것이 아닌 점진적으로 증가시키는 방법입니다.</p>
<h3 id="rrround-robin">RR(Round Robin)</h3>
<p>라운드 로빈 스케줄링은 프로세스에 동일한 크기의 CPU 사용시간을 할당해 주는 방식입니다. 해당 방식은 Time quantum이 설정해 해당 시간이 넘어가기 되면 다른 프로세스로 전환하는 스케줄링 기법입니다. 그렇기에 Time quantum을 잘 설정하는 것이 중요합니다.</p>
<p>Time quantum을 프로세스 처리 시간이 비해 길게 설정한다면 결국 FCFS와 같은 방식으로 동작을 하게 될 것이고 반대로 Time quantum을 너무 짧게 설정한다면 해당 잦은 컨텍스트 스위칭에 대한 오버해드가 발생합니다. 그렇기에 프로세스를 처리하기 시작해 I/O로 전환되는 시점까지를 예측해 설정하는 것이 가장 최선의 방법일 것있습니다. </p>
<h3 id="multi-level-queue">Multi-level Queue</h3>
<p>앞선 스케줄링 알고리즘들은 하나의 대기 큐를 활용했다면 Multi-level Queue는 이름과 같이 여러 개의 대기 큐를 이용한 방식입니다. 여러 대기 큐들은 서로 다른 우선순위를 가지고 있습니다. 그래서 높은 우선순위를 가지는 작업들은 높은 우선순위의 대기 큐에 들어가고 낮은 우선순위를 가지는 프로세스들은 낮은 우선순위의 대기 큐에서 관리됩니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/ed719623-3a1a-466a-84a1-35294dcfb95a/image.png" alt=""></p>
<p>각 대기 큐 내부에서는 주로 각각의 다른 스케줄링 알고리즘을 활용합니다. 높은 우선수위를 가지는 대기 큐에서는 빠르게 작업들을 전환하기 위해서 라운드 로빈(RR) 스케줄링을 활용하고 Batch 프로세스와 같이 CPU 연산 비율이 높은 프로세스를 관리하는 낮은 우선순위의 대기 큐에서는 주로 FCFS 방식을 통해 불필요한 컨텍스트 스위칭의 오버해드를 줄입니다.</p>
<p>한정된 CPU에서 여러 큐의 작업들을 번갈아가면서 작업하기 위해서는 큐들을 대상으로도 스케줄링이 필요합니다. 큐들간의 스케줄링 기법에는 <code>고정 우선순위 방식</code> 과 <code>타임 슬라이스 방식</code> 이 있습니다. 고정 우선순위 방식은 큐별로 우선순위를 정해놓고 처리하는 방안이고 타임 슬라이스 방식의 경우는 큐에 대한 기아현상을 방지하는 방안으로 각 큐에 CPU 활용 시간을 비율로 나누어서 활용하는 방안입니다. </p>
<h3 id="multi-level-feedback-queue">Multi-level Feedback Queue</h3>
<p>다단계 피드백 큐 스케줄링 방식과 다단계 큐 스케줄링 방식은 비슷하면서도 차이가 있습니다. 다단계 큐 스케줄링 방식은 여러 큐 들의 프로세스가 CPU를 할당 받는 비율을 다르게 해서 처리하는 방식이라면 다단계 피드백 큐 스케줄링 방식은 프로세스가 하나의 큐에만 있는 것이 아닌 다른 큐로도 이동이 가능한 스케줄링 기법입니다. 각 큐들간에 이동은 설계하기 나름이 될 것입니다. (상위 큐에서 하위 큐로 이동이 가능하고 하위 큐에서 상위 큐로 이동이 가능할 것입니다.)</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/b6e43882-3dac-49c2-a66f-9cc39eba793d/image.png" alt=""></p>
<p>상위 큐에서 하위 큐로 이동하는 예를 들어 보겠습니다. 큐1과 큐2는 라운드 로빈 기법을 활용하고 큐3는 FCFS 기법을 활용한다고 하겠습니다. 프로세스가 처음으로 대기 큐에 진입하면 가장 우선순위가 높은 큐에서 대기를 하면 CPU를 할당받습니다. 해당 프로세스가 CPU를 할당 받고도 작업이 완료되지 않는다면 큐2로 이동해 대기합니다. 이후에 CPU를 더 할당 받고도 해당 프로세스가 완료되지 않는다면 cpu 작업이 많은 프로세스라고 판단해 큐3로 보내져 문맥교환이 발생하지 않는 방식으로 프로세스를 처리해 나갑니다.</p>
<p>반대로 하위 큐에서 상위 큐로 이동을 하는 예시로는 각 프로세스 마다 Aging 기법을 통해 age가 높은 프로세스들을 상위 큐로 이동시키는 방법이 있습니다. </p>
<h2 id="정리">정리</h2>
<ul>
<li>운영체제는 여러 프로세스가 CPU를 공정하게 사용할 수 있게 CPU 스케줄링을 처리합니다.</li>
<li>CPU 스케줄링 기법에는 선점형 방식과 비선점형 방식으로 구분되며 선점형 방식은 기존의 프로세스를 멈추고 우선순위가 높은 프로세스가 실행되는 방식이며 비선점형 방식은 실행되는 프로세스가 종료된 후 다음 프로세스를 처리하는 방식입니다.</li>
<li>FCFS 스케줄링은 프로세스가 대기 큐에 들어온 순서대로 작업을 처리하는 방식입니다.</li>
<li>SJF 스케줄링은 프로세스 처리 시간이 짧은 작업부터 수행하는 방식입니다.</li>
<li>priority 스케줄링은 각 프로세스 마다 우선순위를 통해 작업을 수행하며 Aging 기법을 통해 기아현상을 방지합니다.</li>
<li>Round Robin 스케줄링은 time quantum을 두어 프로세스가 주어진 시간동안에만 CPU를 할당 받는 방식입니다.</li>
<li>Multi-level queue 스케줄링 기법은 여러 대기 큐를 통해서 프로세스를 관리하는 스케줄링으로 대기 큐간의 우선순위를 관리합니다.</li>
<li>Multi-level feedback queue 스케줄링 기법은 프로세스가 하나의 큐에만 속하는 것이 아닌 다른 큐로 이동이 가능한 기법입니다.</li>
</ul>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://www.geeksforgeeks.org/first-come-first-serve-cpu-scheduling-non-preemptive/">https://www.geeksforgeeks.org/first-come-first-serve-cpu-scheduling-non-preemptive/</a></li>
<li><a href="https://www.geeksforgeeks.org/program-for-shortest-job-first-or-sjf-cpu-scheduling-set-1-non-preemptive/">https://www.geeksforgeeks.org/program-for-shortest-job-first-or-sjf-cpu-scheduling-set-1-non-preemptive/</a></li>
<li><a href="https://www.geeksforgeeks.org/program-for-round-robin-scheduling-for-the-same-arrival-time/">https://www.geeksforgeeks.org/program-for-round-robin-scheduling-for-the-same-arrival-time/</a></li>
<li><a href="https://www.geeksforgeeks.org/multilevel-queue-mlq-cpu-scheduling/">https://www.geeksforgeeks.org/multilevel-queue-mlq-cpu-scheduling/</a></li>
<li><a href="https://www.geeksforgeeks.org/multilevel-feedback-queue-scheduling-mlfq-cpu-scheduling/">https://www.geeksforgeeks.org/multilevel-feedback-queue-scheduling-mlfq-cpu-scheduling/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[스레드에 대해서 알아보자]]></title>
            <link>https://velog.io/@seokhwan-an/%EC%8A%A4%EB%A0%88%EB%93%9C%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@seokhwan-an/%EC%8A%A4%EB%A0%88%EB%93%9C%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 09 Jan 2025 04:56:40 GMT</pubDate>
            <description><![CDATA[<p>지난 포스트에서 프로세스에 대해서 알아보았습니다. 프로세스의 특징은 독립적인 메모리 공간을 가지고 있어서 멀티 프로세스 환경에서는 서로 영향을 미치지 않아서 안정성이 높은 반면 새로운 프로세스를 생성하는 것과 프로세스의 문맥전환 과정에서 많은 오버헤드가 발생한다는 특징이 있습니다. 이와 같은 문제를 해결하고자 프로세스보다 작은 단위의 실행 수단을 프로세스 내에서 만들어 한 프로세스에서 각각 별도의 진행 플로우를 만들자는 아이디어가 나왔고 그의 결과물이 스레드의 등장입니다.</p>
<h2 id="스레드란">스레드란?</h2>
<p>스레드는 프로세스 내에서 실행되는 흐름의 단위를 의미합니다. 프로세스 내에는 적어도 한개의 스레드가 존재하며 한 프로세스 내 여러 쓰레드는 메모리를 공유해 나가며 작업을 수행합니다. 간단하게 예를 들면 유튜브에 여러 요청이 발생했을 때를 보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/d7649756-122e-4cbd-ac16-4619ec689927/image.png" alt=""></p>
<p>여러 사용자가 동영상 재생 요청을 보내게 되면 유튜브 내부에서는 여러 쓰레드가 독자적으로 각 요청에 대한 처리를 진행합니다. </p>
<h3 id="스레드의-메모리-구조">스레드의 메모리 구조</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/1e8d0edd-5f6d-4d79-8fe1-7bd85bbc1106/image.png" alt=""></p>
<p>스레드는 프로세스 내에서 스택 영역만 할당 받고 힙, 데이터, 코드 영역을 서로 공유해서 이용합니다. 힙영역을 공유해서 사용하기 있기에 스레드간의 협력은 프로세스간의 협력에 비해 비교적 간단하게 진행됩니다. 하지만 힙 영역의 데이터를 공유해서 이용하다보니 여러 스레드가 동시에 동작하는 과정에서 race condition 문제와 데드락이 발생할 수 있습니다.</p>
<blockquote>
<p>💡 왜 스택 영역만 독자적으로 가지는 것일까?
스택 영역은 함수 호출 시 매개 변수, 지역 변수가 저장되는 영역입니다. 해당 영역을 각각 가진다는 것은 스레드마다 독립적으로 함수 호출이 가능하다는 것을 의미하고 이는 여러 스레드가 같이 동작하는 것이 아닌 독립적인 실행 흐름을 가질 수 있다는 것을 의미합니다.</p>
</blockquote>
<h3 id="스레드-제어블록">스레드 제어블록</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/ee1e3be5-30ab-4af9-b43a-17b2be2286c7/image.png" alt=""></p>
<p>스레드는 프로세스 내에서 생성 및 동작하기 있기에 프로세스 제어블록 내에서 스레드 제어블록을 참조해 운영체제가 스레드 제어블록을 관리할 수 있게 합니다. </p>
<table>
<thead>
<tr>
<th><strong>요소</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>스레드 ID (TID)</strong></td>
<td>각 스레드를 구분하기 위한 고유한 식별자. 프로세스 내에서 각 스레드를 식별하는 데 사용됩니다.</td>
</tr>
<tr>
<td><strong>상태 (State)</strong></td>
<td>스레드의 현재 상태를 나타냅니다. 예: Running, Ready, Blocked 등.</td>
</tr>
<tr>
<td><strong>프로그램 카운터 (PC)</strong></td>
<td>현재 실행 중인 명령어의 주소를 저장합니다. 스레드가 중단되었다가 재개될 때 실행 위치를 추적합니다.</td>
</tr>
<tr>
<td><strong>스택 포인터 (SP)</strong></td>
<td>스레드의 <strong>스택</strong>이 위치하는 메모리 주소를 가리킵니다. 각 스레드는 독립적인 스택을 가집니다.</td>
</tr>
<tr>
<td><strong>레지스터 값</strong></td>
<td>스레드가 실행 중일 때 사용하는 CPU 레지스터의 값을 저장합니다. 각 스레드마다 독립적인 레지스터 세트가 필요합니다.</td>
</tr>
<tr>
<td><strong>우선순위 (Priority)</strong></td>
<td>스레드의 실행 우선순위를 나타냅니다. 스케줄링을 할 때 우선순위에 따라 실행 순서가 결정됩니다.</td>
</tr>
<tr>
<td><strong>소속 프로세스 정보</strong></td>
<td>해당 스레드가 속한 프로세스에 대한 정보입니다. 보통 PCB(프로세스 제어 블록)를 참조합니다.</td>
</tr>
</tbody></table>
<h3 id="스레드-종류">스레드 종류</h3>
<p>스레드는 크게 <code>사용자 스레드(User Level Thread)</code>와 <code>커널 스레드(Kernel Level Thread)</code>로 나눌 수 있습니다. </p>
<p><code>사용자 스레드</code>는 프로그래밍 언어 내에서 라이브러리를 통해 구현된 스레드를 의미합니다. 커널의 지원 없이 사용자 영역에서 스레드의 생성과 관리가 이루어집니다. 커널은 해당 프로세스만 인식할 뿐 스레드를 인식하지 못하며 그로인해 스레드간 문맥 교환과정에서 커널 모드의 전환이 필요없기에 성능상의 이점이 있습니다. 하지만 해당 프로세스가 특정 상황(I/O 등)등으로 인해서 block이 되면 내부 모든 스레드 작업들이 모두 멈추는 문제가 발생합니다.</p>
<p>반면 <code>커널 스레드</code>는 운영체제가 직접 관리하며, 커널이 스케줄링의 대상으로 인식하고 관리합니다. 이를 통해 스레드 단위로 작업을 스케쥴링할 수 있다는 점과 멀티 프로세서에서 여러 스레드를 병렬로 처리가 가능합니다. 하지만 스레드간의 스케쥴링을 통해서 문맥교환이 발생하고 이로인해 오버헤드가 발생할 수 있습니다. </p>
<p>CPU에서는 커널 스레드가 동작되기에 CPU에 사용자 스레드가 할당되고 스케줄링이 되기 위해서는 사용자 스레드와 커널 스레드가 반드시 연결이 되어야 합니다. 사용자 스레드와 커널 스레드의 연결 방식에 대해서 알아보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/ab1d648e-b5f3-469d-893e-6ce6a6c82d6d/image.png" alt=""></p>
<p>✏️ one-to-one model</p>
<ul>
<li>사용자 스레드와 커널 스레드가 1대1로 매핑되는 방식</li>
<li>스레드 관리를 운영체제에게 위임(스케쥴링 등)</li>
<li>하나의 사용자 스레드에서 i/O가 발생하더라도 다른 스레드들은 동작이 가능</li>
<li>race condition이 발생할 수 있음</li>
</ul>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/8465e4c1-242d-4032-aa4c-5b2b4eeb7ad0/image.png" alt=""></p>
<p>✏️ many-to-one model</p>
<ul>
<li>커널 스레드 1개에 여러 사용자 스레드가 매핑되는 방식</li>
<li>one-to-one 방식에 비해 문맥 교환이 빠름(사용자 레벨에서만 스레드 교환 발생)</li>
<li>one-to-one 방식에 비해 상대적으로 race condition이 발생할 가능성이 적음</li>
<li>멀티 코어 활용이 어려움</li>
<li>하나의 사용자 스레드에서 I/O가 발생해 이와 연결된 커널 스레드가 block이 되면 나머지 사용자 스레드도 동작이 불가능</li>
</ul>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/b0a12343-f5e5-42d8-b7d5-92869903cb75/image.png" alt=""></p>
<p>✏️ many-to-many model</p>
<ul>
<li>n개에 커널 스레드와 m개의 사용자 스레드가 매핑되는 방식</li>
<li>ono-to-one 방식과 many-to-one 방식의 장점을 모은 방식</li>
<li>하나의 사용자 스레드가 i/o를 하더라도 다른 사용자 스레드는 동작가능하며 사용자 스레드 간의 문맥 교환이 일어나 오버헤드가 작음</li>
</ul>
<h2 id="멀티-프로세스-vs-멀티-쓰레드">멀티 프로세스 VS 멀티 쓰레드</h2>
<p>이번에는 멀티 프로세스와 멀티 쓰레드의 특징을 비교해보도록 하겠습니다. 두 방식 모두 병렬 처리를 위한 방법이지만, 각각의 장단점이 있어 상황에 따라 적절한 선택이 필요합니다.</p>
<table>
<thead>
<tr>
<th><strong>비교 기준</strong></th>
<th><strong>멀티 프로세스</strong></th>
<th><strong>멀티 쓰레드</strong></th>
</tr>
</thead>
<tbody><tr>
<td>메모리 사용</td>
<td>각 프로세스마다 독립적인 메모리 영역 필요</td>
<td>스레드간 메모리 공유로 효율적</td>
</tr>
<tr>
<td>통신 방식</td>
<td>IPC를 통한 통신 필요</td>
<td>공유 메모리를 통한 간단한 통신</td>
</tr>
<tr>
<td>안정성</td>
<td>하나의 프로세스가 죽어도 다른 프로세스에 영향 없음</td>
<td>하나의 스레드 문제가 전체 프로세스에 영향</td>
</tr>
</tbody></table>
<p>멀티 프로세스와 멀티 스레드의 차이를 살펴보면, 멀티 프로세스는 안정성이 높지만 메모리 사용량이 많고 프로세스 간 통신이 복잡한 반면, 멀티 스레드는 메모리를 효율적으로 사용하고 통신이 간단하지만 안정성이 상대적으로 낮다는 특징이 있습니다. 따라서 시스템의 요구사항과 특성에 따라 적절한 방식을 선택하는 것이 중요합니다.</p>
<p>이러한 특성들을 고려할 때, 웹 서버와 같이 많은 클라이언트 요청을 처리해야 하는 서비스에서는 멀티 스레드 방식이 자주 사용됩니다. 반면, 하나의 작업이 실패해도 다른 작업에 영향을 주지 않아야 하는 중요한 시스템에서는 멀티 프로세스 방식이 선호됩니다. 실제 많은 현대 시스템들은 두 방식을 혼합하여 사용함으로써 각각의 장점을 최대한 활용하고 있습니다.</p>
<h3 id="멀티-쓰레드의-주의할점">멀티 쓰레드의 주의할점</h3>
<p>멀티 쓰레드를 사용할 때는 여러 가지 주의해야 할 점들이 있습니다. 가장 중요한 것은 여러 스레드가 공유 자원에 동시에 접근할 때 발생할 수 있는 동기화 문제입니다. 이러한 문제를 해결하기 위해 뮤텍스(Mutex)나 세마포어(Semaphore)와 같은 동기화 메커니즘을 적절히 사용해야 합니다.</p>
<p>또한 데드락(Deadlock) 상황을 방지하기 위해 자원 할당 순서를 일관되게 유지하고, 적절한 타임아웃을 설정하는 것이 중요합니다. 스레드 풀(Thread Pool)을 사용하여 스레드의 생성과 소멸에 따른 오버헤드를 줄이는 것도 효율적인 멀티스레드 프로그래밍을 위한 좋은 방법입니다.</p>
<h2 id="정리">정리</h2>
<ul>
<li>스레드는 프로세스의 실행단위를 의미합니다.</li>
<li>스레드는 독립적인 스택영역을 제외하고 힙, 데이터, 코드 영역은 공유해서 사용합니다.</li>
<li>스레드 제어블록을 통해서 운영체제가 스레드를 관리할 수 있습니다.</li>
<li>스레드는 크게 사용자 스레드와, 커널 스레드로 구분되며 사용자 스레드는 라이브러리를 통해 사용자가 생성한 스레드이고 커널 스레드는 OS가 활용하는 스레드 입니다.</li>
<li>멀티 스레드를 이용하는 것은 멀티 프로세스에 비해서 문맥 교환시 오버헤드가 적고 메모리를 효율적으로 사용할 수 있지만 공유 메모리의 데이터를 다룰 때에는 데드락과 race condition 문제거 발생할 수 있습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로세스는 무엇이고 어떻게 관리 될까?]]></title>
            <link>https://velog.io/@seokhwan-an/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B4%80%EB%A6%AC-%EB%90%A0%EA%B9%8C</link>
            <guid>https://velog.io/@seokhwan-an/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B4%80%EB%A6%AC-%EB%90%A0%EA%B9%8C</guid>
            <pubDate>Wed, 08 Jan 2025 07:40:51 GMT</pubDate>
            <description><![CDATA[<p>우리가 자주 사용하는 크롬, 카카오톡, 노션 등은 프로그램입니다. 프로그램은 일련의 작업을 포함하고 있는 코드 파일이며 이는 하드디스크에 저장되어 있습니다. 프로그램을 조금 복잡하게 설명했지만 단순하게 말하면 코드 덩어리를 의미합니다. 사용자가 프로그램(코드 파일)을 실행하면 운영체제는 하드디스크에 저장된 코드 파일을 메모리에 할당하고 스케쥴링을 통해 메모리에 올라간 프로그램을 CPU가 실행하면서 사용자는 원하는 작업을 할 수 있습니다. 이와 같이 프로그램이 시스템 자원을 얻고 실행 중인 상태를 프로세스 라고 합니다. 이번 글에서는 <code>프로세스</code>에 대해서 자세하게 알아보겠습니다. </p>
<h2 id="프로세스">프로세스</h2>
<p>프로세스를 좀 더 간단하게 설명하면 프로그램이 돌아가고 있는 상태를 의미합니다. 다른 말로는 컴퓨터의 작업 단위라고도 합니다. 프로세스는 윈도우에서는 “작업관리자”를 통해서 볼 수 있고 mac에서는 “활성 상태 보기”를 통해 볼 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/1b864fca-0aa9-44ad-bec6-59dfb8828f35/image.png" alt=""></p>
<h3 id="프로세스-메모리-구조">프로세스 메모리 구조</h3>
<p>프로세스는 독자적인 메모리를 운영체제로부터 할당받으며 메모리 구조는 4가지 영역을 가지고 있습니다. (코드, 데이터, 스택, 힙)</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/a47a77eb-65bd-44b4-bbf9-b293855a8323/image.png" alt=""></p>
<p>✏️ 스택(Stack) </p>
<ul>
<li>지역 변수, 매개변수 등이 저장되며 함수 호출과 함께 할당되며 함수 호출이 만료되면 해당 메모리에서 변수 정보가 제거됩니다.</li>
<li>운영체제가 프로세스를 생성할 때 크기가 정해집니다.</li>
<li>함수 호출 및 재귀 호출이 많아 스택 영역을 초과하면 스택 오버플로우가 발생합니다.</li>
</ul>
<p>✏️ 힙(Heap) </p>
<ul>
<li>생성자, 인스턴스와 같이 동적으로 생성되는 데이터들이 할당되는 데이터 영역입니다.</li>
<li>프로세스 실행 중에 해당 영역의 크기가 동적으로 변경될 수 있습니다.</li>
<li>많은 객체가 힙 영역에서 해제 되지 않고 스택 영역을 침범하면 OOM(Out Of Memory)가 발생합니다.</li>
</ul>
<p>✏️ 데이터(Data)</p>
<ul>
<li>전역변수(global) 및 정적 변수(Static) 변수가 할당되는 데이터 영역입니다.</li>
<li>데이터 영역은 초기화된 변수가 저장된 영역과 초기화 되지 않은 변수가 저장된 영역(BSS)으로 구분됩니다.</li>
<li>실행시점에 고정된 크기를 가집니다.</li>
</ul>
<p>✏️ 코드(Code)</p>
<ul>
<li>실행할 프로그램의 명령어가 저장됩니다. (기계어 형태)</li>
</ul>
<h3 id="프로세스-문맥">프로세스 문맥</h3>
<p>이전 포스트(주소)를 보면 멀티프로그래밍 시스템과 시분할 시스템(멀티 테스트)의 경우 CPU가 여러 프로그램(프로세스)들은 전환해 나가면서 CPU의 사용률을 높이고 마지 동시에 프로그램이 동작하는 것처럼 느껴지게 했습니다. 이 과정에서 CPU가 하나의 작업을 온전히 끝내고 다른 작업으로 넘어가면 좋겠지만 작업 중간에 I/O나 인터럽트가 발생하면 진행중인 작업을 멈추고 다른 프로세스로 전환됩니다. 이후 원래 프로세스으로 돌아와 이어서 작업하기 위해서는 프로세스 전환이 발생하기 전에 정보를 저장해두고 있어야 하는데 이 정보를 <code>프로세스 문맥</code>  이라고 합니다. </p>
<p>프로세스 문맥은 “하드웨어 문맥”, “커널상의 문맥”으로 구분할 수 있습니다. “하드웨어 문맥”은  CPU가 현재 어디까지 작업을 진행했는지 나타내는 것으로 ProgramCounter와 각종 레지스터에 저장하고 있는 정보들을 의미합니다. “커널상의 문맥”은 프로세스를 제어하기 위한 자료구조(PCB)를 의미합니다.</p>
<blockquote>
<p>💡문맥 교환(Context switching)
문맥 교환은 기존에 작업하고 있는 프로세스에서 다른 프로세스로 전환되는 과정으로 문맥 교환이 발생하면 기존에 작업 중인 프로세스의 내용을 저장하고 작업할 프로세스의 내용이 불러옵니다.</p>
</blockquote>
<h3 id="프로세스-제어-블록pcb">프로세스 제어 블록(PCB)</h3>
<p>프로세스 제어 블록(PCB)는 운영체재 내에서 프로세스를 관리하기 위해 프로세스 마다 정보를 가지고 있는 커널 내의 자료 구조입니다. 운영체제는 PCB를 통해서 프로세스 간의 문맥교환, 통신, 스케줄링 및 자원관리를 진행합니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/7b944a98-b3ce-405e-9078-19c7b8badc2d/image.png" alt=""></p>
<table>
<thead>
<tr>
<th><strong>항목</strong></th>
<th><strong>설명</strong></th>
<th><strong>활용 예시</strong></th>
</tr>
</thead>
<tbody><tr>
<td>프로세스 ID (PID)</td>
<td>프로세스를 식별하는 고유 번호.</td>
<td>프로세스를 검색하거나 프로세스 간 통신(IPC) 시 사용.</td>
</tr>
<tr>
<td>프로세스 상태</td>
<td>준비, 실행, 대기, 종료 등 현재 상태.</td>
<td>CPU 스케줄러가 어떤 프로세스를 실행할지 결정.</td>
</tr>
<tr>
<td>CPU 레지스터</td>
<td>CPU의 현재 상태를 저장 (프로그램 카운터, 스택 포인터 등).</td>
<td>문맥 전환 시 저장 및 복원하여 프로세스 실행을 이어감.</td>
</tr>
<tr>
<td>메모리 관리 정보</td>
<td>페이지 테이블, 세그먼트 테이블, 메모리 경계 등.</td>
<td>프로세스가 점유하는 메모리 추적 및 충돌 방지.</td>
</tr>
<tr>
<td>열려 있는 파일 목록</td>
<td>프로세스가 접근 중인 파일의 정보.</td>
<td>파일 입출력 관리 및 자원 해제.</td>
</tr>
<tr>
<td>신호 처리 정보</td>
<td>프로세스가 받을 신호와 처리 방식.</td>
<td>프로세스 간 통신(IPC) 및 예외 처리.</td>
</tr>
<tr>
<td>우선순위</td>
<td>프로세스의 실행 우선순위.</td>
<td>스케줄러가 어떤 프로세스를 먼저 실행할지 결정.</td>
</tr>
</tbody></table>
<h3 id="프로세스-상태-변화">프로세스 상태 변화</h3>
<p>프로세스는 다양한 상태를 가지고 있고 운영체제는 해당 정보를 통해 프로세스에게 CPU를 할당할지 말지 결정합니다. 프로세스의 각 상태에 대해서 알아보고 어떤 상황에서 상태가 변환되는지 알아보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/d4a7bbae-cbd8-461f-9dae-9bbac6cfcd73/image.png" alt=""></p>
<table>
<thead>
<tr>
<th><strong>상태</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>New</strong></td>
<td>프로세스가 생성된 상태로, 메모리를 할당 받지 못한 상태</td>
</tr>
<tr>
<td><strong>Ready</strong></td>
<td>실행 대기 상태로, 프로세스가 CPU 할당을 기다리고 있음.</td>
</tr>
<tr>
<td><strong>Running</strong></td>
<td>CPU를 점유하여 실행 중인 상태.</td>
</tr>
<tr>
<td><strong>Waiting/Blocked</strong></td>
<td>I/O 작업 등 외부 이벤트를 기다리고 있는 상태. (cpu를 얻더라도 바로 작업이 수행되지 못한 상태)</td>
</tr>
<tr>
<td><strong>Terminated</strong></td>
<td>프로세스가 실행을 완료하고 종료된 상태.</td>
</tr>
</tbody></table>
<p>상태 변화가 일어나는 과정</p>
<ul>
<li>프로세스가 처음 생성되면 New의 상태를 가집니다.</li>
<li>메모리를 할당 받으면 상태가 New → Ready로 전환됩니다.</li>
<li>Ready상태의 프로세스가 CPU를 할당받으면 Running으로 상태가 전환되고 작업이 수행됩니다.</li>
<li>작업 시간 중 타임슬라이스가 넘어가 TimeOut이 발생하면 Ready로 네트워크 통신이 이루어져 I/O가 발생하는 경우는 Waiting으로 상태가 변환됩니다.</li>
<li>프로세스 작업이 마무리 되면 Terminated로 상태가 변환됩니다.</li>
</ul>
<h3 id="멀티-프로세스">멀티 프로세스</h3>
<p>프로그램은 단일 프로세스로 처리하게 되면 효율성이 떨어지는 부분이 발생합니다. 예를 들면 I/O 작업으로 인해서 프로세스가 대기상태로 들어가게 되면 해당 프로그램 내 다른 작업을 처리할 수 없게 됩니다.(하나의 프로세스가 모든 작업을 처리해야하는 상황인데 I/O를 대기하고 있기 때문) 이와 같은 문제를 해결하고자 프로세스를 여러개 생성해서 처리하는 방식이 등장하는데 이를 멀티 프로세스라고 합니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/365be73d-51f3-4a8d-9a2d-b9e4cfd6262e/image.png" alt=""></p>
<p>멀티 프로세스의 구조를 보면 하나의 프로세스에서 여러 개의 프로세스를 생성해 다중 프로세스를 구성합니다. 유닉스 계열에서는 프로세스가 시스템 호출 중 fork()를 통해 프로세스를 생성하며 생성을 호출한 프로세스를 부모 프로세스라고 하고 생성된 프로세스를 자식 프로세스라고 합니다. 부모 프로세스는 자식 프로세스의 PID를 알 수 있으며 자식 프로세스는 PPID를 통해부모의 프로세스의 ID를 알 수 있습니다. </p>
<p>부모 프로세스와 자식 프로세스는 초기에는 같은 메모리 정보를 가지고 있지만 외부의 요청으로 변화가 발생하게 되면 메모리의 복사가 발생하고 이후부터는 부모와 자식은 각각의 독립적인 메모리 공간을 가지게 되어 서로 독립적으로 동작하게 됩니다.</p>
<p> ✏️ 장점</p>
<ul>
<li>멀티 프로세스는 각 프로세스가 독립적인 메모리공간을 가지고 있기에 하나의 프로세스의 장애가 다른 프로세스로 이어지지 않습니다.</li>
</ul>
<p>✏️ 단점</p>
<ul>
<li>컨텍스트 전환을 하는데 있어서 모든 메모리 정보를 전환해주어야 하기 때문에 잦은 컨텍스트 전환이 발생하면 오버헤드가 발생합니다.</li>
</ul>
<blockquote>
<p>💡 여러 개의 프로세스는 어떻게 통신할까?
하나의 큰 작업을 처리할 때에는 하나의 프로세스가 처리하는 것이 아닌 여러 프로세스가 처리해야하는 상황이 발생합니다. 이 때 두 프로세스가 통신하기 위해서는 IPC 기술을 활용합니다. IPC 방식에는 메세지 전달 방식과 메모리 공유 방식이 있습니다.</p>
<p>메세지 전달 방식</p>
<ul>
<li>커널을 통해 메세지를 전달하는 방식</li>
<li>시스템 콜이 필요하면 잦은 통신이 발생하면 오버헤드가 발생</li>
<li>pipe, signal, message queueing, socket 방식 등</li>
</ul>
<p>공유 메모리 방식</p>
<ul>
<li>공유 메모리를 구축해 여러 프로세스가 같은 데이터를 주고 받는 방식</li>
<li>커널을 거치지 않기에 통신이 자유롭고 속도가 빠름</li>
<li>자원을 서로 공유하기에 동기화 문제가 발생</li>
<li>세마포어를 이용한 방식 등</li>
</ul>
</blockquote>
<h2 id="정리">정리</h2>
<ul>
<li>프로그램은 개발자가 문제를 해결하기 위해 작성된 코드 파일이며 프로세스는 프로그램인 운영체제로부터 자원을 얻어 실행중인 상태를 의미합니다.</li>
<li>프로세스는 각각 독립적인 메모리 공간을 가지며 4가지 영역으로 구분됩니다. (스택, 힙, 코드, 데이터)</li>
<li>운영체제는 프로세스를 관리하기 위해서 프로세스 제어 블록(PCB)형태의 자료구조를 관리합니다.</li>
<li>프로세스는 여러 상태를 가지며 운영체제는 상태를 보고 CPU를 부여할지 가져올지 결정합니다.</li>
<li>멀티 프로세스는 하나의 프로그램을 단일 프로세스가 아닌 여러 프로세스로 처리하는 방식으로 부모 프로세스에서 fork()를 통해 자식 프로세스를 생성해 처리합니다. 초기에 부모프로세스와 자식 프로세스가 모두 같은 메모리 주소 값을 가지고 있지만 한쪽에서 변화가 발생하면 새로운 메모리가 할당돼 독립적으로 동작합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[우리는 어떻게 여러 프로그램을 동시에 이용할 수 있는 것일까?]]></title>
            <link>https://velog.io/@seokhwan-an/%EC%9A%B0%EB%A6%AC%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%97%AC%EB%9F%AC-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8%EC%9D%84-%EB%8F%99%EC%8B%9C%EC%97%90-%EC%9D%B4%EC%9A%A9%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EA%B2%83%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@seokhwan-an/%EC%9A%B0%EB%A6%AC%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%97%AC%EB%9F%AC-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8%EC%9D%84-%EB%8F%99%EC%8B%9C%EC%97%90-%EC%9D%B4%EC%9A%A9%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EA%B2%83%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Wed, 01 Jan 2025 03:19:29 GMT</pubDate>
            <description><![CDATA[<p>대부분의 사람들이 컴퓨터를 이용할 때 하나의 프로그램만 이용하는 것이 아닌 여러 프로그램을 실행하며 컴퓨터를 이용하고 있습니다. 노래를 들으면서 게임을 하거나 개발을 하면서 검색을 하는 것처럼 여러개의 프로그램을 실행합니다. 이 때 각각의 프로그램은 동시에 동작하는 것처럼 느껴집니다. CPU 코어 수보다 더 많은 프로그램을 실행하는데에도 동시에 동작하는 것처럼 느껴지는 것은 어떻게 하는 것일까요?</p>
<h2 id="한-번에-하나의-프로그램만-실행되는-경우단일-프로세스-시스템">한 번에 하나의 프로그램만 실행되는 경우(단일 프로세스 시스템)</h2>
<p>먼저 CPU가 한 번에 하나의 작업만 처리하는 상황을 생각해봅시다. 만약 CPU가 작업을 순차적으로 처리한다면, 여러 작업이 동시에 진행되는 것처럼 느껴지지 않을 것입니다. 예로 들어, 사용자가 노래를 들으며 검색을 하는 상황을 살펴보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/43f67c86-e6e8-4cfa-925c-d31355d0678a/image.png" alt=""></p>
<p>위 그림과 같이 CPU가 한 번에 한 작업만 처리한다면, 노래를 듣는 작업과 검색 작업을 번갈아 가며 처리하게 됩니다. 이는 다음과 같은 문제를 유발합니다.</p>
<ul>
<li>노래를 끝까지 들은 후에야 검색이 가능합니다.</li>
<li>검색을 진행하는 동안에는 노래를 들을 수 없습니다.</li>
</ul>
<p>이와 같은 방식은 사용자에게 불편함을 초래할 뿐만 아니라 CPU 자원을 비효율적으로 사용하는 방식입니다. 이를 알아보기 위해 검색의 과정을 조금 더 세분화 하겠습니다.</p>
<ul>
<li>사용자가 검색할 내용 입력</li>
<li>검색 실행 및 대기</li>
<li>검색 내용 조회</li>
</ul>
<p>이 중 “<strong>검색 실행 및 대기”</strong> 단계는 네트워크를 통해 외부 서비스와 통신(I/O 작업)하는 과정으로, 외부로부터 응답이 올 때까지 CPU가 대기 상태에 놓이게 됩니다. 이 대기 시간 동안 CPU는 다른 작업을 수행하지 않고 낭비됩니다. 이와 같이 CPU사용률의 문제를 해결하고자 다중 프로그래밍 시스템이 등장합니다.</p>
<h2 id="다중-프로그래밍-시스템">다중 프로그래밍 시스템</h2>
<p>다중 프로그래밍 시스템은 단일 프로세스 시스템의 단점인 CPU 사용율 문제를 해결하기 위해 메모리에 여러 프로그램을 적재하여 CPU가 번갈아가면서 실행하는 시스템입니다. 다중 프로그래밍 시스템의 목적은 CPU의 사용률을 극대화하는 것입니다. 다중 프로그래밍 시스템에서 I/O 작업이 발생하면 다른 프로그램을 실행합니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/402b7f24-16db-4158-948b-7b2b29537337/image.png" alt=""></p>
<p>앞선 단일 프로세스 시스템과 다르게 노래를 실행하는 과정에서 I/O가 발생하면 CPU가 대기를 하는 것이 아닌 검색(다른 작업)을 수행한다는 점에서 단일 프로세스 시스템에 비해 CPU의 사용률이 높다는 것을 볼 수 있습니다.</p>
<p>하지만 CPU가 처리하는 작업이 I/O가 발생하지 않는 작업이라면 다중 프로그래밍 시스템 역시 단일 프로세스 시스템과 마찬가지로 하나의 작업이 처리되고 다른 작업을 처리하는 방식으로 수행됩니다 즉, 다중 프로그래밍 시스템도 단일 프로세스 시스템과 마찬가지로 사용자에게는 동시에 여러 프로그램이 동작하는 느낌을 제공할 수 없습니다.</p>
<h2 id="시분할-시스템">시분할 시스템</h2>
<p>시분할 시스템은 타임슬라이스를 도입하여 <code>프로그램이 아주 짧은 시간동안만 CPU를 점유하는 방법</code>으로 앞선 다중 프로그래밍 시스템에서 발생하는 문제(하나의 I/O 없는 작업이 CPU를 끝까지 점유하는 상황)를 해결하기 위해 등장했습니다. 시분할 시스템에서 작업 전환은 <code>I/O</code>뿐만 아니라 정해진 <code>타임슬라이스</code>초과했을 때 발생합니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/434b94f0-e30f-44aa-9140-84c52407b4c2/image.png" alt=""></p>
<p>위의 그림과 같이 노래 듣기 작업과 검색 작업이 빠르게 전환이 되면서 사용자 입장에서는 마치 노래를 들으면서 검색하는 것처럼 느끼게 됩니다. 이와 같이 시분할 시스템이 도입되면서 CPU 사용률 향상과 더불어 사용자의 편의성 모두 증가했지만 한정된 메모리 안에서 여러 프로그램을 실행하다보니 메모리 부족현상이 발생했습니다.</p>
<blockquote>
<p>💡CPU는 타임슬라이스가 완료되었다는 것을 어떻게 알 수 있을까?
타임슬라이스는 타이머 인터럽트를 통해 구현됩니다. 즉, CPU는 인터럽트를 통해서 해당 작업이 타임슬라이스를 넘어갔다는 것을 알 수 있습니다.</p>
</blockquote>
<h2 id="인터럽트란">인터럽트란</h2>
<p>앞서서 시분할 시스템에서는 타이머 인터럽트를 통해서 CPU가 해당 작업의 타임슬라이스가 넘었는지 파악한다는 것을 알았습니다. 여기서 말하는 인터럽트는 무엇일까요? 인터럽트는 프로그램을 수행하는 도중에 발생하는 이벤트를 의미합니다. 인터럽트가 발생하면 CPU는 진행하던 작업을 멈추고 이벤트로 넘어온 작업을 처리해 나가야 합니다.</p>
<p>인터럽트는 크게 하드웨어 인터럽트와 소프트웨어 인터럽트로 구분됩니다. 이 두 인터럽트에 대해서 간단하게 알아보겠습니다.</p>
<h3 id="하드웨어-인터럽트">하드웨어 인터럽트</h3>
<p>하드웨어 인터럽트는 키보드, 마우스 또는 프린터 등과 같은 외부 하드웨어 장치로부터 발생하는 인터럽트입니다. 하드웨어의 인터럽트의 경우 CPU가 폴링작업으로 외부 작업이 완료되었는지 기다리는 것을 방지하고자 도입되었습니다. 하드웨어 인터럽트의 예시는 다음과 같습니다.</p>
<ul>
<li>키보드 입력 시</li>
<li>마우스 클릭 시</li>
<li>디스크 드라이브 읽기 및 쓰기 작업이 완료되었을 시</li>
</ul>
<blockquote>
<p>💡폴링은 무엇인가요?
폴링은 하나의 장치가 다른 장치의 상태를 주기적으로 검사하여 원하는 조건을 달성했는지 확인하는 방법 입니다. 해당 방식은 CPU가 계속 다른 장치의 변화를 주기적으로 파악하기 때문에 자원의 낭비로 볼 수 있습니다.(CPU가 다른 프로세스의 작업을 처리하는 것이 아닌 해당 장치의 변화를 계속 확인하고 있기 때문)</p>
<p>폴링 VS 인터럽트</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/d99629a4-a756-4a74-bf04-0d26c5745196/image.png" alt=""></p>
<ul>
<li>폴링은 CPU가 메인 요청을 한 후 지속적으로 계속 상태변화를 체크한다.</li>
<li>인터럽트는 CPU가 요청을 한 후 다른 작업을 수행하며 상태변화가 완료된 것을 장치로부터 알게된다.</li>
</ul>
<p>일상 생활에서의 예시 (병원)</p>
<ul>
<li>폴링 : 간호사가 주기적으로 환자를 체크하는 상황</li>
<li>인터럽트 : 환자가 위급해져 간호사를 호출하는 상황</li>
</ul>
</blockquote>
<h3 id="소프트웨어-인터럽트">소프트웨어 인터럽트</h3>
<p>소프트웨어 인터럽트는 컴퓨터의 내부 시스템(프로그램)에 의해서 발생하는 인터럽트를 의미합니다. 프로그램이 실행되는 도중에서 시스템 호출을 하는 경우나 예외가 발생하는 경우에 소프트웨어 인터럽트가 발생합니다. 이는 트랩이라고도 불립니다. 소프트웨어 인터럽트에 대한 예시는 다음과 같습니다.</p>
<ul>
<li>프로그램 내 0으로 정수를 나누려고 하는 경우</li>
<li>프로그램 내 파일을 불러오거나 읽기, 쓰기 작업을 하는 경우 (open(), read(), write() 등)</li>
</ul>
<h2 id="정리">정리</h2>
<ul>
<li>단일 프로세스 시스템은 CPU가 하나의 작업을 처리한 이후에 다른 작업을 수행한다. → I/O 작업 시 CPU의 대기로 인해 CPU 사용률 저하 문제 발생</li>
<li>단일 프로세스 시스템의 문제를 보완하고자 I/O 발생 시 다른 프로그램을 작업하는 다중 프로그래밍 시스템 도입  → I/O 작업이 없는 프로그램이 CPU를 점유하는 시간이 많아져 다른 프로그램의 작업이 밀리는 문제 발생</li>
<li>다중 프로그래밍 시스템의 문제를 개선하고자 작업 타임슬라이스를 시분할 시스템 도입 → I/O 작업 뿐만 아니라 CPU 작업 시간의 작게 잡아 여러 프로그램을 작업하여 CPU 사용률 및 사용자 편의성 향상</li>
<li>인터럽트는 크게 하드웨어 인터럽트와 소프트웨어 인터럽트로 구분 (하드웨어 인터럽트를 하드웨어 장치로부터 발생하는 인터럽트, 소프트웨어 인터럽트는 실행하는 프로그램 내에서 발생하는 에러 및 시스템 호출로 인해 발생하는 인터럽트)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[운영체제란 무엇인가?]]></title>
            <link>https://velog.io/@seokhwan-an/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@seokhwan-an/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Wed, 01 Jan 2025 03:15:02 GMT</pubDate>
            <description><![CDATA[<h2 id="운영체제는-무엇일까">운영체제는 무엇일까?</h2>
<p>우리는 컴퓨터로 다양한 응용프로그램을 실행하며 여러 작업을 처리합니다. 예를 들어, 저는 주로 구글, 유튜브, 인텔리제이와 같은 프로그램을 동시에 실행하여 여가시간을 보내거나 개발 작업을 합니다. 이렇게 여러 응용프로그램이 원활하게 동작하려면 CPU, 메모리 등 다양한 자원이 필요합니다. 그런데 응용프로그램이 자원을 직접 관리하도록 하면 어떤 문제가 발생할 수 있을까요?</p>
<h3 id="컴퓨터-자원-독점-문제">컴퓨터 자원 독점 문제</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/90ba500e-9dc2-4182-86ae-85b7ac6d4665/image.png" alt=""></p>
<p>첫 번째로 발생할 수 있는 문제는 컴퓨터 자원의 독점입니다. 한 응용프로그램이 CPU를 독점하게 되면 다른 프로그램이 제대로 동작하지 않을 수 있습니다. 예를 들어, 유튜브로 영상을 보는 동안 구글 검색을 하지 못하거나, 노래를 들으면서 개발을 할 수 없는 상황이 생길 수 있습니다. 이는 컴퓨터 자원의 비효율적인 사용으로 이어질 수 있습니다.</p>
<h3 id="보안-문제-발생">보안 문제 발생</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/39ebaefb-9a3d-462d-86ad-46a8d410a305/image.png" alt=""></p>
<p>또 다른 문제는 보안상의 취약점입니다. 응용프로그램이 자원에 직접 접근하는 경우, 악성 프로그램이 실행 중인 다른 프로그램의 메모리와 데이터에 접근하여 정보를 탈취하거나 변조할 수 있습니다. 이러한 문제는 개인 정보 유출, 데이터 조작과 같은 심각한 결과를 초래하며, 프로그램이 의도한 대로 동작하지 못하게 만드는 원인이 될 수 있습니다.</p>
<p>위와 같이 컴퓨터 자원과 응용프로그램 사이에서 발생할 수 있는 문제를 해결하고자 <code>운영체제</code>가 등장했습니다. 운영체제는 사용자와 하드웨어 사이의 인터페이스를 제공해 효율적으로 응용프로그램이 동작하도록 지원하고 시스템 자원을 효율적으로 관리하여 응용프로그램들이 원활하게 동작하는데 도움을 주는 소프트웨어입니다.</p>
<blockquote>
<p>운영체제와 커널의 차이는 무엇인가?
커널은 운영체제의 핵심 요소로 운영체제가 커널, 파일 시스템, 사용자 인터페이스 등 여러 가지 작업을 통해 사용자와 통신을 하는 것이라면 커널은 메모리, 프로세스, cpu 등 하드웨어 리소스를 관리하는 소프트웨어 입니다. 즉, 운영체제는 사용자와 컴퓨터 자원을 이어주는 다리역할이라면 커널은 응용프로그램과 하드웨어를 이어주는 다리역할입니다. </p>
<p>참고자료 </p>
<ul>
<li><a href="https://www.tutorialspoint.com/difference-between-operating-system-and-kernel">https://www.tutorialspoint.com/difference-between-operating-system-and-kernel</a></li>
<li><a href="https://www.geeksforgeeks.org/difference-between-operating-system-and-kernel/">https://www.geeksforgeeks.org/difference-between-operating-system-and-kernel/</a></li>
</ul>
</blockquote>
<h2 id="응용프로그램은-어떻게-원활하게-동작할-수-있을까">응용프로그램은 어떻게 원활하게 동작할 수 있을까?</h2>
<p>앞서서 운영체제는 여러 응용프로그램이 원활하게 동작하는데 도움을 주는 소프트웨어라는 것을 학습했습니다. 그러면 응용프로그램들은 어떻게 운영체제를 통해서 적절한 자원을 얻을 수 있을까요? 이를 알아보기 위해서 컴퓨터 시스템의 구조에 대해서 알아보겠습니다.</p>
<h3 id="유저-모드와-커널-모드">유저 모드와 커널 모드</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/e1504d57-7627-4169-85d8-82ffe746fcc8/image.png" alt=""></p>
<p>위의 그림을 보면 컴퓨터 시스템은 <code>유저모드</code>와 <code>커널모드</code>로 구분되는 것을 볼 수 있습니다. 각각의 모드를 간단하게 살펴보면 <code>유저모드</code>는 제한된 모드로 사용자가 응용프로그램을 실행시키기 위해 접근하는 영역이고 <code>커널모드</code> 는 컴퓨터의 자원관리 및 하드웨어와 통신을 처리하는 영역입니다. 컴퓨터의 시스템이 두개의 모드로 분리된 이유는 응용프로그램에서는 직접 컴퓨터 자원 및 연산에 접근하지 못하게 하기 위함입니다. 다시 돌아와서 응용프로그램은 어떻게 CPU 및 메모리 등 자원을 획득 및 접근할 수 있을까요? 두 모드에서 통신은 어떻게 이루어지는 것일까요?</p>
<h3 id="시스템-콜">시스템 콜</h3>
<p>유저모드에서 커널모드와 통신하기 위해서는 <code>시스템 콜(System call)</code>을 이용합니다. 시스템 콜은 개발에서 프론트앤드와 백엔드가 api를 통해 통신하는 것과 같이 유저모드에서 커널이 제공하는 기능를 이용하기 위한 인터페이스입니다. </p>
<p>시스템 콜 종류에는 프로세스 제어(Process Control), 파일 작업(File Management), 장치 관리(Device Management), 통신(Communication), 메모리 관리(Memory Management) 등이 있습니다.</p>
<p>지금까지 유저모드, 커널모드, 시스템 콜에 대해서 알아보았습니다. 이를 통해서 실제 프로그램이 실행될 때 유저모드와 커널 모드가 어떻게 동작을 하는지 알아보겠습니다.</p>
<h3 id="예시--power-point에서-그림을-불러오는-상황">예시 : Power Point에서 그림을 불러오는 상황</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/3975f8e0-6411-4d83-8c8f-2cd567a4710c/image.png" alt=""></p>
<ol>
<li>파워포인트가 실행</li>
<li>파워포인트에 필요한 메모리 흭득(커널모드로 전환)</li>
<li>파워포인트에서 사진 혹은 그림을 불러오는 상황(커널모드로 전환)</li>
<li>파일을 불러온 후 다시 유저모드로 전환 및 다른 작업 수행</li>
<li>파워포인트 내 글자를 타입핑하는 상황(커널모드로 전환)</li>
<li>타입핑 입력 신호 처리 후 유저모드로 전환 및 다른 작업 수행</li>
</ol>
<h2 id="정리">정리</h2>
<ul>
<li>운영체제는 시스템 자원을 보호 및 효율적으로 사용하여 여러 응용시스템이 원활하게 동작하는데 도움을 주는 소프트웨어이다. (커널은 운영체제의 핵심 요소로 하드웨어와의 통신을 제공하는 소프트웨어이다.)</li>
<li>컴퓨터 시스템은 유저모드와 커널모드로 구분되고 이 사이에서는 시스템 콜을 통해서 응용프로그램이 하드웨어에 접근이 가능하다.</li>
</ul>
<h3 id="참고자료">참고자료</h3>
<ul>
<li><a href="https://www.geeksforgeeks.org/what-is-an-operating-system/">https://www.geeksforgeeks.org/what-is-an-operating-system/</a></li>
<li><a href="https://www.differencebetween.com/difference-between-user-mode-and-vs-kernel-mode/">https://www.differencebetween.com/difference-between-user-mode-and-vs-kernel-mode/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[조회 성능을 높이는 인덱스는 무엇일까?]]></title>
            <link>https://velog.io/@seokhwan-an/%EC%9D%B8%EB%8D%B1%EC%8A%A4%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%EC%99%9C-%EC%93%B0%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@seokhwan-an/%EC%9D%B8%EB%8D%B1%EC%8A%A4%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%EC%99%9C-%EC%93%B0%EB%8A%94%EA%B0%80</guid>
            <pubDate>Mon, 16 Dec 2024 07:33:07 GMT</pubDate>
            <description><![CDATA[<h2 id="인덱스란">인덱스란?</h2>
<p>인덱스는 특정 컬럼값을 기준으로 row(데이터)를 빠르게 탐색할 수 있는 자료구조로 insert, update, delete의 성능을 희생하고 select의 성능을 높이는 데 이용합니다. 인덱스의 가장 큰 특징은 인덱스를 설정한 컬럼 기준으로 데이터를 정렬해 관리한다는 것입니다. 여기서 데이터의 정렬이 왜 빠른 조회에 도움이 되는지 궁금하실 것 같습니다. (저도 인덱스를 학습할 때 궁금해 했었습니다)</p>
<p>이를 간단하게 예시를 통해서 살펴보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/9b388ac9-6bd6-4fb4-9366-69661fe5d6d6/image.png" alt=""></p>
<p>숫자 1부터 10까지가 랜덤으로 섞인 리스트와 숫자 1부터 10까지가 오름차순으로 정렬된 리스트가 있다고 하겠습니다. 여기서 우리가 7을 찾는다고 할 때 랜덤으로 섞인 리스트의 경우에는 처음부터 탐색할 수밖에 없습니다. 운이 좋으면 원하는 데이터를 한번에 찾을 수 있겠지만 가장 마지막에 있게 된다면 최대 10번의 탐색이 필요할 것입니다. 반대로 정렬된 리스트에서는 중앙값을 통해 찾으려는 범위를 줄여 나가면서 탐색 횟수를 줄여나가 두 번만에 7을 찾을 수 있습니다.</p>
<p>이를 DB에 반영해 보면 데이터가 랜덤으로 저장되어 있다면 원하는 데이터를 찾을 때 테이블의 처음부터 끝까지 탐색해야 하지만 인덱스와 같이 데이터가 정렬되어 있다면 탐색범위를 줄이고 데이터를 찾을 수 있어 조회 성능을 높일 수 있습니다. 그래서 조회 성능을 높이는 데 인덱스를 이용합니다.</p>
<p>하지만 인덱스도 결국에는 데이터 파일(MySQL에서는 page 단위로 관리)이기에 새로운 데이터가 추가, 변경, 삭제 시에 관리해야 할 포인트가 늘어나는 것입니다. 특히 인덱스는 데이터를 정렬해 둔 상태를 유지해야 하기에 데이터 쓰기 작업의 경우 데이터만 추가되는 것이 아닌 정렬과정이 매번 발생하는 오버헤드가 존재합니다.</p>
<h2 id="인덱스는-어떻게-구현되어-있는가">인덱스는 어떻게 구현되어 있는가?</h2>
<p>인덱스의 데이터는 다양한 자료구조를 기반으로 관리될 수 있으며 일반적으로 hash 및 B-Tree 자료구조 기반으로 관리됩니다. MySQL의 innoDB의 경우 hash와 B-tree 기반의 인덱스를 모두 이용하며 대부분의 인덱스 데이터는 B-tree 자료구조를 기반으로 합니다. hash 인덱스와 B-tree 인덱스에 대해 각각 알아보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/8b865853-8af4-42bd-b45c-e40b57e26aef/image.png" alt=""></p>
<p>위의 그림과 취미 정보를 담는 Member table을 활용하여 각각의 인덱스에 데이터가 어떻게 저장되고  관리되는지 알아보겠습니다. </p>
<h3 id="hash기반-인덱스">hash기반 인덱스</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/fda1c01b-289a-47c3-8c77-d4c7db575d62/image.png" alt=""></p>
<p>인덱스의 핵심은 정렬이라고 했지만 hash 인덱스의 경우 데이터를 정렬해서 관리하는 것이 아닌 key-value 형태로 데이터를 관리합니다. key에는 사용자가 지정한 컬럼의 해시의 값(현재 그림에서는 이름 컬럼)을 저장되고 value에는 실제 데이터가 있는 disk 주소값을 가지고 있습니다. </p>
<pre><code class="language-sql">SELECT 이름, 주민등록번호,취미 FROM Member WHERE 이름 = &#39;김철수&#39;;</code></pre>
<p>위의 쿼리와 같이 이름 컬럼 기반으로 동등 검색을 하는 경우 ‘김철수’ 값을 해시 함수로 변환하고 이를 인덱스 테이블 내 key 값과 비교해 일치하면 레코드 주소를 활용해 실제 데이터를 불러옵니다. 이를 통해 Disk 내 모든 데이터에 접근할 필요없이 hash key 값을 이용하여 데이터에 접근하여 Disk i/o를 줄일 수 있습니다. 다만 인덱스를 생성하는 과정에서 해시 충돌이 발생한다면 데이터를 조회하는 과정에서 Disk i/o가 늘어날 수 있습니다.</p>
<p>앞선 조회 과정에서 보았듯이 hash 인덱스는 동등 비교(==)에서는 빠른 조회시간을 가집니다. 반면 다른 인덱스와 다르게 정렬된 데이터를 관리하는 것이 아니기에 범위 조회(&lt;, &gt;)와 데이터 정렬 과정에서는 Optimizer가 해당 인덱스를 활용하지 않습니다.</p>
<h3 id="b-tree기반-인덱스">B-Tree기반 인덱스</h3>
<p>B-Tree기반 인덱스에 대해서 알아보기에 앞서서 먼저 B-Tree 자료구조에 대해서 간단하게 알아보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/c736a0fe-a787-437b-90e5-9a5da25a7100/image.png" alt=""></p>
<p>B-Tree는 이진 트리를 확장한 자료구조로 이진트리와 다르게 하나의 노드가 2개의 자식노드를 가지는 것이 아니라 여러개를 가질 수 있습니다. 같은 량의 데이터에 대하여 자식 노드의 수가 늘어남에 따라 트리의 전체 높이가 낮아져 빠른 탐색 속도를 가질 수 있다는 특징이 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/35ba3cb4-ce8f-43f5-b634-3b6b26daa434/image.png" alt=""></p>
<p>B-Tree 기반의 인덱스는 root → branch → leaf 페이지로 관리가 됩니다. root 페이지와 branch 페이지에는 인덱스 키와 자식 노드의 정보를 가지고 있고 leaf 페이지의 경우 인덱스의 종류에 따라 가지고 있는 값이 다릅니다. 클러스터링 인덱스(PK 기반 인덱스)에 경우에는 실제 데이터를 가지고 있지만 세컨더리 인덱스의 경우에는 PK 정보를 가지고 있습니다.</p>
<p>B-tree 인덱스는 hash 인덱스와 다르게 데이터를 정렬해서 관리합니다. 그렇기에 B-Tree 인덱스의 경우 동등 비교(=)와 범위 조회(&lt;, ≤, &gt;, ≥, between)에 이용되며 빠른 성능을 가집니다. 그렇다고 해서 모든 조회에서 인덱스가 활용되는 것은 아닙니다. 다음의 예시를 보겠습니다.</p>
<pre><code class="language-sql">SELECT 이름, 주민등록번호, 취미 FROM Member WHERE 이름 LIKE &#39;%민%&#39;; </code></pre>
<p>해당 쿼리는 이름 내 “민”이 포함된 데이터를 불러오는 것입니다. “민”으로 시작하는 데이터는 인덱스를 찾을 수 있겠지만 중간 혹은 마지막에 “민”이 포함된 정보를 찾기 위해서는 결국 모든 데이터를 확인해야 하기에 인덱스를 활용하지 못합니다.그렇기에 첫글자가 아닌 중간 혹은 마지막 글자로 찾은 요청의 성능 향상시키고자 해당 컬럼에 인덱스를 추가하여도 개선이 되지 않으니 성능 개선 시 유의해야합니다. </p>
<p>이 두 인덱스를 정리해보면 hash 인덱스는 b-tree 인덱스에 비해 동등비교에서는 빠른 성능을 내지만 범위 연산에 활용하지 못하는 방면 b-tree 인덱스는 범위 연산에서도 빠른 조회를 능력을 가지고 있습니다. 그렇기에 실제 MySQL의 innoDB에서는 두 방식의 인덱스를 모두 활용하고 있습니다. b-tree 인덱스는 사용자 요청에 따른 필요한 데이터를 빠르게 찾는데 활용되고 hash 인덱스의 경우는 데이터를 찾는데 자주 활용되는 인덱스를 관리해 조회 성능을 높입니다. </p>
<h2 id="마무리">마무리</h2>
<ul>
<li>인덱스는 데이터를 정렬해서 관리해 조회 성능을 높이는 자료구조 입니다.</li>
<li>인덱스 내 데이터는 hash, b-tree 자료구조를 통해 관리됩니다.<ul>
<li>hash 인덱스는 동등 비교는 가능하지만 범위 비교를 불가</li>
<li>b-tree 인덱스는 동등 비교 및 범위 비교가 가능하지만 중간 글자나 마지막 글자를 통한 검색 불가능</li>
</ul>
</li>
<li>MySQL의 innodb에서는 어느 한 방식의 인덱스만 활용하는 것이 아닌 두 방식의 인덱스를 활용</li>
</ul>
<p>그 동안 인덱스를 이용하면 성능이 개선된다는 것을 직접 경험해보면서 알고 있었지만 내부 구조가 어떻게 되어있고 어떤 동작원리를 가지고 있는지 이번 기회에 글로 정리해보면서 더 이해가 잘 된 것 같습니다. 더불어 자주 사용하는 MySQL의 내부 동작 과정도 알 수 있어서 의미있는 시간이었습니다.</p>
<p>다음에는 MySQL을 통해서 실제 인덱스를 활용해보면서 인덱스 사용시 유의점에 대해서 정리해보고자 합니다. </p>
<h3 id="참고자료">참고자료</h3>
<ul>
<li><p><a href="https://dev.mysql.com/doc/refman/8.4/en/index-btree-hash.html">https://dev.mysql.com/doc/refman/8.4/en/index-btree-hash.html</a></p>
</li>
<li><p><a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-adaptive-hash.html">https://dev.mysql.com/doc/refman/8.4/en/innodb-adaptive-hash.html</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[테스트의 격리성이 보장되지 않아요]]></title>
            <link>https://velog.io/@seokhwan-an/%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%9D%98-%EA%B2%A9%EB%A6%AC%EC%84%B1%EC%9D%B4-%EB%B3%B4%EC%9E%A5%EB%90%98%EC%A7%80-%EC%95%8A%EC%95%84%EC%9A%94</link>
            <guid>https://velog.io/@seokhwan-an/%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%9D%98-%EA%B2%A9%EB%A6%AC%EC%84%B1%EC%9D%B4-%EB%B3%B4%EC%9E%A5%EB%90%98%EC%A7%80-%EC%95%8A%EC%95%84%EC%9A%94</guid>
            <pubDate>Fri, 13 Dec 2024 07:09:25 GMT</pubDate>
            <description><![CDATA[<h2 id="문제사항">문제사항</h2>
<p>단일 테스트를 진행할 때 성공했던 테스트가 전체 동작 테스트에서 실패를 하는 상황이 발생했습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/4370fe6c-07ec-4e5c-9e88-9f7e729788df/image.png" alt=""></p>
<p>위는 부스에 댓글을 추가하는 기능을 테스트를 하는 것이 단일로 실행을 할 경우에는 성공을 하지만 테스트 전체 코드를 실행 했을 때에는 실패하는 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/0703326b-fb8f-4b5b-aec1-326c0c3c2520/image.png" alt=""></p>
<p>오류 내용을 보면 result의 id가 1L인 것을 예상했지만 실제 결과는 3L이 나왔다는 것을 나타내고 있습니다. 이 에러 뿐만 아니라 데이터를 조회하는 테스트에서도 단일 테스트로 실행했을 때에는 통과했지만 전체 테스트를 실행한 경우 테스트의 순서에 따라 의도한 데이터 개수보다 더 많은 개수의 데이터가 나타나는 것을 확인할 수 있었습니다.</p>
<p>이와 같은 문제가 발생하는 이유는 무엇일까요?</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/516d0c2f-f611-4713-a000-b780db6e1061/image.png" alt=""></p>
<p>이 문제는 여러 테스트가 데이터베이스와 같은 공통 자원을 이용했을 때 발생하는 문제였는데요. 즉, 이전에 테스트에서 발생한 데이터들이 롤백이 되는 것이 아니라 그대로 유지가 되어 이후에 테스트에 영향을 미치지는 상황이었습니다. (물론 <code>@DataJpaTest</code>의 경우에는 내부에 <code>@Transactional</code>이 있어 롤백이 되지만 id가 초기화되지 않는 문제가 발생했고 <code>@SpringBootTest</code>의 경우 <code>@Transactional</code> 적용되지 않아 테스트 후 데이터의 롤백이 자동화 되어 있지 않아 다른 테스트에 영향을 미치는 상황이었습니다.)</p>
<h2 id="해결방안">해결방안</h2>
<p>문제 발생 원인이 database가 테스트마다 동일한 환경을 가지지 못해서 발생한 문제였기에 database를 초기화하는 방법에 대해서 찾아보았습니다. </p>
<ul>
<li><code>@Transactional</code> 이용하기</li>
<li><code>@DirtyContext</code> 를 이용하기</li>
<li>entityManager를 이용해서 테이블 초기화 하기</li>
</ul>
<p>각각의 방식에 대해서 알아보겠습니다.</p>
<h3 id="transcational-어노테이션-이용하기">Transcational 어노테이션 이용하기</h3>
<blockquote>
<p>Annotating a test method with <code>@Transactional</code> causes the test to be run within a transaction that is, by default, automatically rolled back after completion of the test.</p>
</blockquote>
<p>spring framework의 공식문서를 보면 테스트 코드에서 <code>@Transactional</code> 을 이용하면 테스트가 완료된 이후에 자동으로 rollback이 수행된다고 합니다.</p>
<p>해당 내용을 보면 간단하게 테스트 데이터를 rollback할 수 있어 <code>@Transactional</code> 을 테스트 코드에 이용하는 것이 현재 직면한 테스트간의 db 격리성을 보장하는데 용이할 것 입니다. 하지만 <code>@Transaction</code>을 테스트 코드에 이용하면 유의해야할 점이 발생합니다. </p>
<p>“실제 로직 환경과 테스트의 환경이 동일하지 않게 됩니다.”</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/74b69640-4cac-4ed6-96bd-c26e16719830/image.png" alt=""></p>
<p>테스트코드에 <code>@Transaction</code>을 이용하게 되면 비즈니스 로직에만 적용되던 트랜잭션이 테스트 메소드 전체로 확장이 됩니다. 확장된 트랜잭션 범위로 인해서 의도하지 않은 결과가 발생할 수 있습니다.</p>
<p>그 예시로 비즈니스 로직에 <code>@Transaction</code> 을 빼먹은 경우에 대해서도 테스트는 성공하는 상황이 발생할 수 있습니다. 간단한 예시를 살펴보겠습니다. 현재 Booth와 Menu는 1대다의 연관관계를 맺고 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/8e1f8a12-6e4c-4bb0-badc-9324ffdd482e/image.png" alt=""></p>
<p>위의 메소드는 booth의 menu를 조회하는 기능으로 현재 <code>@Transactional</code>을 적용되지 않은 상태입니다. 해당 기능은 테스트에서 성공을 하지만 실제 운영상황에서는 menu를 불러오는 과정에서 <strong>LazyInitializationException</strong>이 발생하게 됩니다. 이렇듯 개발자의 실수가 있었지만 실제환경과 테스트 환경이 달라지면서 해당 문제를 파악하지 못할 수 있습니다.</p>
<p>두번째 상황은 RestAssured를 통한 인수테스트에서 데이터를 불러오지 못하는 문제가 발생할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/d1fd9379-3839-47ec-be0f-562f73745e8e/image.png" alt=""></p>
<p>위의 테스트는 RestAssured를 통한 인수 테스트로 사용자가 booth에 속한 menu 정보를 잘 불러오는지 테스트 하는 것입니다. 해당 테스트 메소드에 @Transactional 을 이용하면 데이터를 불러오지 못하는 문제가 발생합니다. </p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/1806f030-caa6-4967-b1d0-f8cd6bc7d1fa/image.png" alt=""></p>
<p>분명 테스트를 위한 API 요청 전에 테스트 데이터를 저장하는 과정이 있지만 해당 정보를 불러오지 못하는 이유는 다음과 같습니다.</p>
<ol>
<li>현재 메소드에 <code>@Transactional</code> 으로 인해서 transaction의 범위가 테스트  테스트 데이터는 db에 flush가 되지 않은 상태입니다.</li>
<li>RestAssured의 경우에는 별도의 thread를 활용해서 동작합니다.</li>
<li>트랜잭선은 thread 별로 생성되고 그로인해 jpa의 1차캐시 역시 thread 별로 관리됩니다. (여러 thread에서 트랜잭션이나 영속성 컨텍스트를 공유하지 않습니다.)</li>
</ol>
<p>위의 3가지 내용을 잘 조합해 테스트 코드를 보겠습니다.</p>
<ol>
<li>현재 테스트 데이터는 영속성 컨택스트에만 있고 db에는 저장되지 않은 상태입니다.</li>
<li>RestAssured를 통해 api가 실행될 때 새로운 transaction과 영속성 컨택스트가 활성화 됩니다.</li>
<li>데이터를 불러오는 과정에서 db에 요청을 보냈지만 해당 테스트 데이터는 db에 존재하지 않아 에러를 발생합니다.</li>
</ol>
<p>이렇듯 <code>@Transactional</code> 을 테스트 코드에 이용하는 것은 데이터 롤백 측면에서는 간단하지만 예상치 못한 상황에서 문제가 발생할 수 있습니다.</p>
<h3 id="dirtiescontext-어노테이션-이용하기">DirtiesContext 어노테이션 이용하기</h3>
<p>먼저 <code>@DirtiesContext</code> 를 적용하기에 앞서서 <code>@DirtiesContext</code>에 대한 정보를 찾아보면 다음과 같이 나타나 있습니다.</p>
<blockquote>
<p>DirtiesContext
Test annotation which indicates that the ApplicationContext associated with a test is dirty and should therefore be closed and removed from the context cache.</p>
<p>Use this annotation if a test has modified the context — for example, <span style="color:#c2255c"><strong><em>by modifying the state of a singleton bean, modifying the state of an embedded database, etc. Subsequent tests that request the same context will be supplied a new context.</em></strong></span></p>
</blockquote>
<p>@DirtiesContext may be used as a class-level and method-level annotation within the same class or class hierarchy. In such scenarios, the ApplicationContext will be marked as dirty before or after any such annotated method as well as before or after the current test class, depending on the configured methodMode and classMode.</p>
<blockquote>
</blockquote>
<p>This annotation may be used as a meta-annotation to create custom composed annotations.
As of Spring Framework 5.3, this annotation will be inherited from an enclosing test class by default. See @NestedTestConfiguration for details.</p>
<blockquote>
</blockquote>
<p>두번째 문단을 보면 DirtiesContext를 이용하는 상황을 알 수 있습니다. DirtiesContext는 이미 올라온 context의 bean이 오염이 되거나 혹은 database가 변경되었을 때 이용하는 것으로 새롭게 ApplicationContext구성하여 테스트를 하는 것입니다. 즉, 각 테스트마다 초기의 context를 생성해 테스트를 진행하는 것입니다.</p>
<p>이와 같이 테스트 메소드마다 새롭게 ApplicationContext를 만들어서 진행하는 것은 느낀점은 테스트 동작 시간이 길어진다는 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/355b870d-9c89-407e-9829-bd4059c1819e/image.png" alt=""></p>
<p>위와 같이 테스트 메소드가 실행되기 전에 새롭운 ApplicationContext를 구성하도록 하여 테스트를 진행해보았습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/51dfa7fb-9e9f-451e-a8d8-4f274aea3f42/image.png" alt=""></p>
<p>위에서 말했던 것과 같이 모든 테스트가 같은 환경에서 동작을 하다보니 모든 테스트가 의도했던 대로 동작하는 것을 확인할 수 있습니다. 하지만 5개 테스트가 동작하는데 1.6초가 발생한 것을 볼 수 있습니다. </p>
<p>DirtiesContext를 이용하지 않는 방식으로 테스트를 진행했을 때(약 0.3초)보다 약 5배정도 차이가 나는 것을 확인할 수 있습니다. </p>
<p>지금은 테스트의 개수가 적지만 테스트의 개수가 많아지게 되면 이는 빠르게 테스트를 진행하는데 어려움을 줄 수 있습니다. 물론 DirtiesContext를 class 단위가 아닌 필요한 메소드에만 붙여서 진행하여 ApplicationContext의 생성횟수를 줄일 수 있을 것이라 생각합니다.</p>
<h3 id="entitymanager활용하기">EntityManager활용하기</h3>
<p>EntitiyManager를 통해 동적으로 전체 테이블을 불러와서 테이블을 초기화해주는 컴포넌트를 만들어 테스트 간의 격리성을 보장해주는 방법입니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/8c73c3f4-ae95-4e66-937b-40218c618859/image.png" alt=""></p>
<p>동작 방식은 크게 테이블 정보를 로드하고 이를 통해 테이블을 truncate하는 방식입니다.</p>
<ol>
<li>먼저 <code>@Entity</code> 인 클래스를 모두 불러오고 이를 테이블 이름에 맞게 Carmel 케이스에서 Snake 케이스로 변경을 합니다. </li>
<li>영속성 컨텍스트를 비운 후 테이블 이름 통해 truncate table SQL을 처리합니다.</li>
</ol>
<p>이를 테스트 <code>@AfterEach</code> 에 적용한 후 테스트를 실행하면 테스트 종료 후 다음과 같은 쿼리문을 볼 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/55f7a539-d7ce-449b-8a27-558516b4e596/image.png" alt=""></p>
<p>이를 통해서 테스트 간에 database가 격리되어 여러 테스트를 동작시켜도 서로 영향을 주지 않아 테스트 메서드가 독립적으로 동작합니다.</p>
<p>하지만 이 방식에도 문제점이 존재합니다.</p>
<ol>
<li>동적으로 테이블을 불러와서 테이블을 초기화하가 보니 불필요한 테이블까지도 truncate를 한다는 것입니다. 즉 테이블의 개수가 많아지게 되면 이 부분도 리소스가 많이 발생할 수 있습니다.</li>
<li>테스트에 사용하는 db SQL 문에 종속적입니다. mongoDB에서는 truncate 문을 허용하지 않듯 데이터베이스가 변경될 때 문제가 발생할 수 있습니다.</li>
</ol>
<p>3가지 방법에 대해서 알아보고 비교해보면서 저는 EntityManager를 통해서 테스트 격리성을 보장하는 방안을 택했습니다. </p>
<p>Transactional 어노테이션을 이용하는 것이 가장 간단했지만 생각보다 많은 부분에서 문제가 될 수 있다는 것을 <a href="https://jojoldu.tistory.com/761">테스트 데이터 초기화에 @Transactional 사용하는 것에 대한 생각</a>  글을 통해서 더 볼 수 있었습니다. 그리고 테스트 목적은 해당 코드가 실제 환경에서도 잘 동작하는지를 검증하는 것이기에 테스트의 환경이 실제 환경과 달라지는 것은 좋지 않다고 판단했습니다. </p>
<p>DirtiesContext 역시 사용법은 간단했지만 테스트의 성능이 저하되는 것은 서비스를 운영하는 점에서 치명적으로 다가올 수 있다고 생각했습니다. 예를 들면 CI/CD 과정은 보편적으로 build 후 진행되는데 이때 테스트가 동작합니다. 즉 테스트 성능이 낮아질 수록 CI/CD의 속도도 느려진다는 것입니다. 이는 신속하게 에러를 해결해야하는 상황에서 병목지점이 될 수 있다고 판단했습니다.</p>
<h2 id="느낀점">느낀점</h2>
<p>이번에 테스트가 실패하면서 해당 문제의 원인 및 해결방법을 찾아보고 적용해보면서 각 방식의 장단점을 몸소 배울 수 있었고 테스트 코드 역시 비즈니스 코드 처럼 신경써야 한다는 것을 느꼈습니다.</p>
<p>테스트 간 격리성을 보장하는 다른 방법으로 <a href="https://www.youtube.com/watch?v=PDhN6aiF7QQ">테스트에서 @Transactional 을 사용해야 할까?</a> 을 접했는데 이 방식도 테스트 코드가 많아서 성능을 높이기 위해 테스트 코드를 병렬적으로 수행하는데 적용해보면 좋을 것 같다고 느꼈습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[사용자 식별 없이 좋아요를 만들고 싶어요]]></title>
            <link>https://velog.io/@seokhwan-an/%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%8B%9D%EB%B3%84-%EC%97%86%EC%9D%B4-%EC%A2%8B%EC%95%84%EC%9A%94%EB%A5%BC-%EB%A7%8C%EB%93%A4%EA%B3%A0-%EC%8B%B6%EC%96%B4%EC%9A%94</link>
            <guid>https://velog.io/@seokhwan-an/%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%8B%9D%EB%B3%84-%EC%97%86%EC%9D%B4-%EC%A2%8B%EC%95%84%EC%9A%94%EB%A5%BC-%EB%A7%8C%EB%93%A4%EA%B3%A0-%EC%8B%B6%EC%96%B4%EC%9A%94</guid>
            <pubDate>Mon, 09 Dec 2024 07:22:41 GMT</pubDate>
            <description><![CDATA[<p>렛츠 끼릿의 메인 페이지에서는 공연정보와 더불어서 당일에 가장 인기 있는 부스(주점, 플리마켓 등등)를 나타내주고 있습니다. </p>
<center>
  <img src = "https://velog.velcdn.com/images/seokhwan-an/post/95002316-abd2-416f-a769-eaf57ab2c2d0/image.png" width = 400 height = 500></center>

<p>위의 사진과 같이 인기 있는 부스를 5개만 추출하여 나타내주고 있었습니다. 이 때 인기있는 부스의 기준으로는 여러가지가 의논이 되었습니다.</p>
<ol>
<li><code>댓글이 많은 것</code>을 인기있는 부스라고 하자</li>
<li><code>조회수가 많은 것</code>을 인기있는 부스라고 하자</li>
<li><code>좋아요가 많은 것</code>을 인기있는 부스라고 하자</li>
</ol>
<p>이 3가지 중에서 <code>좋아요가 많은 것</code>을 인기있는 부스로 하자는 결정이 나왔습니다. 그 이유는 댓글같은 경우 사용자들이 접근하는 장벽이 높다는 의견이 많았었고 조회수 같은 경우에는 새로고침으로 무한하게 늘릴 수 있다는 의견이 있어서 였습니다. 그래서 한 사람당 한번만 할 수 있는 <code>좋아요</code>를 인기있는 부스의 척도로 삼았습니다.</p>
<h2 id="문제상황">문제상황</h2>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/389b5d8a-4a0c-498d-b360-70b7c281954b/image.png" alt=""></p>
<p><code>좋아요</code>라는 것은 특정 사용자가 식별이 되어야 하는데 이번에 많은 학교 학우들이 편리하게 서비스를 이용할 수 있도록 로그인이라는 장벽을 두지 않았습니다. 그래서 좋아요를 누른다고 한들 사용자가 정보가 없는 상태에서 이를 사이트를 나갔다가 들어왔을 때 어떻게 인식을 해주어야 할지 고민이 되었고 여러 방면으로 이를 해결할 수 있는 방안에 대해 모색을 했습니다.</p>
<h3 id="해결-방안">해결 방안</h3>
<p>좋아요를 누른 사용자를 식별하는 방법으로 크게 2가지 의견이 나왔습니다.</p>
<ol>
<li>ip 주소를 이용해서 사용자를 식별하는 것</li>
<li>쿠키를 이용해서 사용자를 식별하는 것 </li>
</ol>
<p>이 두 방식 중 ip 주소로 사용자를 식별하는 것에는 <em>큰 결함</em>이 있다고 판단했습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/2604b0cc-976f-4361-a1db-583be812a577/image.png" alt=""></p>
<p>이는 위의 그림과 같이 교내에서 여러 학우들이 같은 wifi를 이용하는 경우 내부적으로 각 기기는 다른 ip 주소(사설 ip 주소)를 가지지만 외부 서비스에 입장에서 ip는 같게 나타난다는 것입니다. 그래서 여러 사용자가 같은 ip 주소로 처리가 되어 ip주소로는 사용자를 식별하는 것이 불가능한 상황이 됩니다. 또한 유동 ip로 인해서 사용자의 단말기 ip 주소가 변경이 된다면 기존에 좋아요를 눌렀던 정보도 파악할 수 없는 상황도 존재했습니다.  </p>
<p>이와 같은 이유로 사용자를 식별하는데 ip주소를 이용하지 않았습니다. 그래서 남은 방법인 쿠키를 이용하는 방식으로 문제를 해결했습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/05012665-e620-456b-b354-91907642f452/image.png" alt=""></p>
<p>위의 그림과 같이 사용자가 어느 한 부스에 좋아요를 누르면 서버는 쿠키를 발급을 해줍니다. 쿠키는 겹치지 않는 정보를 가지게 했고 쿠키를 통해서 좋아요를 식별하는 방식을 선택했습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/fb0408a8-0403-402d-9c4c-3cd82e1c880b/image.png" alt=""></p>
<p>이후 요청에는 쿠키가 함께 보내지게 되고 서버는 쿠키 정보를 통해서 사용자가 좋아요를 눌렀는지 아닌지 여부를 반환해줍니다. 즉, <em>사용자를 식별하는 방식이 아닌 쿠키로 좋아요를 식별하는 방식을 채택했습니다.</em></p>
<h2 id="구현-방식">구현 방식</h2>
<p>쿠키는 서버의 데이터 조각이며 개발자 도구를 통해서 사용자들이 확인을 할 수 있습니다. 즉, 사용자에게 쉽게 노출되어 있는 구조입니다. 그렇기에 중요한 정보를 담으면 안되고 각 쿠키의 값이 식별되는 것이 핵심이었습니다. 그래서 저희팀은 Java에서 제공하는 UUID를 이용해서 쿠키의 value를 설정했습니다.</p>
<h3 id="like-entity">Like Entity</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/86599344-2df3-45ed-ac23-8e1af0b566cc/image.png" alt=""></p>
<p>좋아요 객체에 경우 크게 세가지의 컬럼을 가지도록 하였습니다. Pk인 id와 쿠키의 value가 될 cookieKey 그리고 어떤 부스에 좋아요인지를 파악하기 위해 Booth 객체와 관계를 가지도록 구성을 했습니다. </p>
<p>다음은 서비스 Layer에 대해서 살펴보겠습니다.</p>
<h3 id="like-service">Like Service</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/6e9c8f80-6603-46af-a6bc-6be46b96bc34/image.png" alt=""></p>
<p>create메소드에서는 부스 id와 해당 요청자의 쿠키의 key, value를 포함한 정보를 인자로 받습니다. 이를 통해 해당 사용자가 이미 해당 부스에 좋아요를 했는지 검증한 후 Like의 Key를 생성하고 이를 DB에 저장합니다. Like의 Key 값은 서비스 내에서 유일해야하기 때문에 고유성을 보장하는데 이용하는 UUID를 활용했습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/60550cc5-dad2-4785-9790-78fcbd1cad20/image.png" alt=""></p>
<p>현재 UUID를 생성하는 객체는 한번 추상화를 하여 인터페이스를 구현하도록 구성을 했는데요! 그 이유는 테스트의 편의성을 높이기 위함이었습니다. 일련의 문자열을 UUID의 randomUUID() 메소드를 통해 반환되는데 이를 예측해서 테스트를 하는 것을 불가능에 가까웠습니다. 그래서 테스트에서는 일정한 값을 반환하는 UniqueKeyMaker의 구현체를 만들고 진행하였습니다.</p>
<p>마지막으로 controller에 대해서 살펴보겠습니다.</p>
<h3 id="like-controller">Like Controller</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/2af0adbd-7e65-43fe-b4de-82284aef60f3/image.png" alt=""></p>
<p>web 계층에서는 service 계층으로 부터 받아온 like의 UUID를 이용해서 쿠키를 생성해주는 역할을 합니다.</p>
<h2 id="정리">정리</h2>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/7d65e108-d2b4-43ee-8f4b-6f8baa6b2f3b/image.png" alt=""></p>
<ol>
<li>사용자가 부스에 좋아요를 누른다.</li>
<li>서버는 사용자의 요청을 받으면 이미 존재하는 좋아요인지 쿠키를 통해 파악한다.</li>
<li>이 때 좋아요 쿠키가 없다면 좋아요를 생성하고 이를 쿠키로 담아서 사용자에게 전달한다.</li>
<li>그 이후에 요청에는 쿠키가 담겨져서 보내져 사용자가 좋아요를 누른 부스임을 알 수 있다.</li>
</ol>
<h3 id="추가로-더-고민해본-사항">추가로 더 고민해본 사항</h3>
<p>현재 방식도 부스 데이터가 적은 서비스를 운영하는데에는 큰 문제가 발생하지 않았던 방법있습니다. 하지만 곰곰히 생각을 해보면 문제점이 있었습니다.</p>
<p>현재는 사용자를 식별하지 않고 좋아요를 식별하는 구조이기 때문에 사용자가 좋아요를 많이 누르게 되면 그만큼 쿠키를 발급받아서 브라우저에 쌓이는 방식입니다. 즉 한 사용자가 5개의 부스에 좋아요를 누르면 5개의 쿠키가 생기게 됩니다. RFC 6265에 따르면 cookie에는 제약사항이 있습니다.</p>
<ul>
<li>쿠키당 크기는 4096 바이트이어야 한다.</li>
<li>한 도메인당 최대 50개의 쿠키만 발급 가능하다.</li>
<li>쿠키의 총량은 3000개 이어야한다.</li>
</ul>
<p>위의 규칙을 적용하면 저희 서비스에서는 50개의 부스에만 좋아요를 할 수 있는 상황이 발생합니다. 교내 축제때에는 부스가 많지 않아 운영시에는 문제가 발생하지 않았지만 이는 확장성이 있는 방식은 아니었습니다. 이 부분을 더 개선할 수 있는 방안을 고민해야겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[축제 사이트 개발 배경 및 결과]]></title>
            <link>https://velog.io/@seokhwan-an/%EC%B6%95%EC%A0%9C-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EA%B0%9C%EB%B0%9C-%EB%B0%B0%EA%B2%BD-%EB%B0%8F-%EA%B2%B0%EA%B3%BC</link>
            <guid>https://velog.io/@seokhwan-an/%EC%B6%95%EC%A0%9C-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EA%B0%9C%EB%B0%9C-%EB%B0%B0%EA%B2%BD-%EB%B0%8F-%EA%B2%B0%EA%B3%BC</guid>
            <pubDate>Thu, 28 Nov 2024 11:07:59 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/e44b14fd-6a8a-47a2-9b18-5bc9a2e9942a/image.png" alt=""></p>
<h2 id="축제-사이트를-구축한-배경">축제 사이트를 구축한 배경</h2>
<p>2022년 코로나 19가 창궐하던 시기가 점점 진정되면서 비대면이었던 수업이 대면 수업으로 전환되었습니다. 그리고 그동안 진행되지 않았던 학교 축제가 다시 기획되었습니다. 축제 시작에 앞서서 다양한 정보(공연, 푸드트럭, 플리마켓, 공지사항 등)들이 여러 SNS에 분산되어 제공되었습니다. 분산된 정보들은 저를 포함한 동기 및 학우들이 축제와 관련된 정보를 찾는데 pain point로 다가 왔습니다. 해당 pain point를 해결해 나가고자 축제 정보를 한 곳에 모아보자라는 아이디어가 동아리원들 사이에서 나왔고 2~3주라는 짧은 시간이지만 완성하고 배포해 학우들에게 제공하는 것을 목표로 했습니다.</p>
<h2 id="축제-사이트의-목표">축제 사이트의 목표</h2>
<p>축제 사이트의 목표는 학우들에게 축제와 관련된 정보를 통합해서 제공해주는 것인 만큼 부가적인 기능을 넣기 보다는 필요한 기능만 제공하는 것으로 설정했습니다.</p>
<h3 id="축제-사이트에서-제공하고자-하는-정보">축제 사이트에서 제공하고자 하는 정보</h3>
<ul>
<li>공연 타임테이블 제공</li>
<li>부스 정보 제공(주점, 푸드트럭, 플리마켓 등)</li>
<li>축제 공지사항 제공</li>
</ul>
<p>학우들이 축제에 가장 관심을 가지는 정보인 공연, 부스, 공지사항을 한 웹사이트로 모아서 제공하고자 했습니다.</p>
<h3 id="요구사항-및-기능-정리하기">요구사항 및 기능 정리하기</h3>
<p>위의 제공해야할 정보를 기준으로 필요한 기능을 구성해나갔습니다.</p>
<p>공연 정보 제공</p>
<ul>
<li>각 요일별 활성화된 공연 타임 테이블 제공하기</li>
</ul>
<p>부스 </p>
<ul>
<li>부스 검색 기능<ul>
<li>검색은 부스 이름, 장소, 메뉴 이름을 기반으로 검색가능</li>
</ul>
</li>
<li>부스 상세 정보 제공<ul>
<li>부스의 이름, 위치, 주점 소개, 메뉴 정보를 제공</li>
<li>부스에 좋아요 기능 제공</li>
<li>부스에 댓글 기능 제공</li>
</ul>
</li>
<li>부스 생성 기능<ul>
<li>각 학과장들이 학과의 부스를 등록할 수 있는 기능 제공</li>
</ul>
</li>
<li>인기 있는 부스 정보 제공<ul>
<li>좋아요를 가장 많이 받은 5개의 부스를 메인페이지에서 제공</li>
</ul>
</li>
</ul>
<p>공지사항 정보 제공</p>
<ul>
<li>축제와 관련된 공지사항을 제공</li>
</ul>
<h2 id="엔티티-구성도">엔티티 구성도</h2>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/6a240875-54c8-4899-84ea-03c341ee67f9/image.png" alt=""></p>
<p>아쉽게도 짧은 기간에 개발을 진행하다보니 축제 공연 타임 테이블의 경우는 프론트 측에서 이미지로 처리했습니다.</p>
<h2 id="축제-사이트-결과물">축제 사이트 결과물</h2>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/66914f3e-8102-4c99-ac78-5c341e1fdf5c/image.png" alt=""></p>
<p>축제 사이트는 4개의 페이지(메인 페이지, 공지사항, 타임테이블, 부스정보)로 구성했습니다.</p>
<h3 id="메인페이지">메인페이지</h3>
<p>메인페이지에서는 당일 축제에 진행중인 공연 타임테이블에 대한 정보를 우선으로 제공했습니다. 더불어 부스 정보를 검색할 수 있게 검색 기능을 넣었습니다. 마지막으로 당일 인기있는 부스 5개를 메인페이지에 나타나겠습니다.</p>
<h3 id="공지사항">공지사항</h3>
<p>공지사항은 축제 기획단에서 올린 축제 공지사항을 정보를 게시판처럼 한눈에 볼 수 있게 구성했습니다.</p>
<h3 id="타임테이블">타임테이블</h3>
<p>타임테이블 페이지는 메인페이지와 다르게 축제기간동안 운영되는 공연 및 행사 정보를 제공하는 페이지로 학우들이 축제 일정을 한눈에 볼 수 있게 구성했습니다. <code>부스 보러가기</code> 버튼을 통해 해당일에 운영되는 부스 페이지로 이동하도록 구성했습니다.</p>
<h3 id="부스-페이지">부스 페이지</h3>
<p>부스 페이지에는 부스와 관련된 정보를 담았습니다. 각 부스에서 판매하는 음식 및 물건의 가격 정보와 부스가 운영되고 있는 장소를 표시해 학우들이 쉽게 부스를 찾을 수 있도록 구성했습니다.</p>
<h3 id="3일간-운영한-결과">3일간 운영한 결과</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/b116e775-dace-4efb-a4b2-4488f4e5949c/image.png" alt=""></p>
<p>3일간 약 10,000 명의 이용자가 서비스에 접속해서 이용한 것을 확인했습니다. 평균 1분 50초를 머문 것은 조금 아쉬운 수치라고 느껴지지만 메인페이지에 축제 타임테이블과 인기 부스 정보를 제공해주는 것을 생각하면 빠르게 정보를 보고 나갔다고 느낄 수 있을 것 같습니다. 더불어 학우들로부터 긍정적인 피드백을 받았을 때 2주 정도는 밤새가면 개발했고 여러 오류들을 만나 해결해 나가는 과정이 어려웠지만 개발의 즐거움을 느낄 수 있어서 좋았습니다. </p>
<h3 id="느낀-점">느낀 점</h3>
<p>축제 사이트를 구성하고 3일간 운영하면서, 서비스 개발에서 가장 중요한 점은 다양한 기능을 포함하기보다 사용자의 <strong>Pain Point</strong>를 정확히 파악하고 이를 해결할 수 있는 간단한 기능을 제공하는 것임을 깨달았습니다.</p>
<p>이전까지는 프로젝트를 진행하며 학습하고 싶은 기술이나 다양한 기능 구현에 중점을 두었고, 정작 실제 사용자의 필요성을 충분히 고려하지 못했던 것 같습니다.</p>
<p>이번 경험을 통해, 앞으로는 새로운 프로젝트를 시작할 때 명확한 목표 타겟을 설정하고, 그들의 어려움을 우선적으로 해소할 수 있는 기능을 고민하며 개발에 임하는 것이 중요하다는 점을 배웠습니다.</p>
<p>앞으로는 개발하면서 아쉬웠던 부분을 개선해 나가볼려고 합니다.(backend 부분을 수정해 나가려고 합니다.) 당시에는 저장되는 데이터가 작다보니 응답시간을 크게 고려하면서 로직을 작성할 필요가 없었는데 이에 대해서도 대량의 데이터에서는 어떻게 될지에 대해서 실험 및 개선해나가면서 학습을 이어나가고자 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[확장성 있는 OAuth 인증 시스템 구축하기]]></title>
            <link>https://velog.io/@seokhwan-an/%ED%99%95%EC%A0%95%EC%84%B1-%EC%9E%88%EB%8A%94-OAuth-%EC%9D%B8%EC%A6%9D-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@seokhwan-an/%ED%99%95%EC%A0%95%EC%84%B1-%EC%9E%88%EB%8A%94-OAuth-%EC%9D%B8%EC%A6%9D-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 10 Nov 2024 07:24:00 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@seokhwan-an/%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8OAuth-%EB%8F%84%EC%9E%85%EA%B8%B0">소셜 로그인(OAuth) 도입기</a> 글에 이어서 작업한 내용으로 확장성 있는 소셜 로그인 시스템을 구축한 과정에 대해서 정리하고자 합니다.</p>
<h2 id="문제-상황">문제 상황</h2>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/f75a1d29-f6bb-4f25-83a5-893f476a7938/image.png" alt=""></p>
<p>이 코드는 당시 구글 소셜 로그인을 구축할 때 구현할 코드입니다. 이후 카카오 소셜 로그인을 추가하려다 보니 기존 구조에 문제가 있음을 깨달았습니다. 먼저 현재 코드를 보면 구글 소셜 로그인에 의존하는 부분이 존재합니다. 그렇기에 새로운 소셜 로그인 플랫폼이 추가될 때마다 <code>OAuthService</code> 코드를 수정하는 상황이 발생합니다. 두 가지 상황을 보겠습니다.</p>
<h3 id="소셜-로그인-플랫폼-별-로그인-메소드를-구현하는-방법">소셜 로그인 플랫폼 별 로그인 메소드를 구현하는 방법</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/c37e6422-dcc4-4675-9e05-c4f53cd109ec/image.png" alt=""></p>
<p>위 방안은 소셜 로그인 플랫폼마다 로그인 메소드를 구현하는 방안입니다. 소셜 로그인 플랫폼 별로 로그인 메소드를 관리하고 있기에 각 플랫폼 별로 로그인 과정을 커스텀할 수 있지만 OAuth 2.0은 모든 소셜 로그인 플랫폼에서 로그인 과정이 동일하게 동작하는 것이 큰 장점이자 특징이기에 이를 분리해서 관리할 필요가 없다고 느꼈습니다. 즉, 사용자 정보를 불러오는 요청만 플랫폼 별로 다를 뿐 동일한 로그인 흐름을 가진 메소드들이 중복된다는 것을 볼 수 있습니다.</p>
<h3 id="소셜-로그인-플랫폼-별-if문을-추가해서-처리하는-방법">소셜 로그인 플랫폼 별 if문을 추가해서 처리하는 방법</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/075fcfa2-9717-4459-8070-501e21625207/image.png" alt=""></p>
<p>위 방안은 동일한 로그인 흐름이 중복되는 것을 제거한 방법으로 플랫폼 정보을 인자로 받고 해당 정보를 바탕으로 알맞은 소셜 로그인을 처리하는 방식입니다. 해당 방안은 소셜 로그인의 공통된 흐름이 중복되지 않는다는 장점이 있지만 여전히 새로운 소셜 로그인 플랫폼이 추가될 때마다 의존필드가 추가된다는 점과 if문이 추가되어야 한다는 문제점이 있습니다.</p>
<p>두 가지 방안 모두 기능이 동작하는데에는 큰 문제가 있는 것은 아니지만 코드를 관리하는 입장에서 새로운 변화가 발생했을 때 관리 point가 발생한다는 점이 큰 아쉬움이라고 느껴졌습니다.</p>
<h2 id="해결-방안">해결 방안</h2>
<p>문제를 해결하기에 앞서서 먼저 s-hook의 소셜 로그인 흐름을 살펴보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/325f2ad5-2e19-44c7-99b3-1ee01c54c1e6/image.png" alt=""></p>
<ol>
<li>클라이언트 측에서 Authorization Server로 부터 Authorization Code를 발급받는다.</li>
<li>Authroization Code로 로그인 요청을 보낸다.</li>
<li>Authroization Code를 통해 사용자 resource에 접근할 수 있는 accessToken을 발급받는다.</li>
<li>accessToken을 통해 사용자 resource를 발급받는다.</li>
<li>자체 token을 발급해서 클라이언트에 제공한다.</li>
</ol>
<p>s-hook의 소셜 로그인은 5단계로 수행됩니다. </p>
<p>여기서 살펴볼 것은 3번과 4번과 과정은 특정 소셜 로그인 플랫폼에서만 동작하는 것이 아닌 모든 플랫폼에서 동일하게 동작하는 부분입니다.(단 플랫폼 별로 api 엔드 포인트나 요청 매개 변수는 내부적으로 다를 수 있습니다.) 그 부분에 해당 하는 OAuthService의 코드는 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/7fa4f7b9-05b3-43ff-890b-4e97c57ae90f/image.png" alt=""></p>
<p><code>OAuthService</code> 기준으로 보았을 때 빨간 네모 부분이 구글 기반인지 카카오 기반인지 페이스북 기반인지에 대해서 알필요가 없다는 것이 느꼈습니다. 대신 중간 매개체를 두어 <code>OAuthService</code>가 직접 플랫폼 Provider에 의존하는 것 대신 중간 매개체(<code>OAuthProviderFinder</code>)에서 원하는 소셜 로그인 플랫폼 Provider를 불러오는 구조가 더 좋다고 생각했습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/9c55175d-c098-4571-a86e-59c7219c31cb/image.png" alt=""></p>
<p>제가 생각한 구조는 위의 그림과 같습니다. </p>
<ol>
<li>OAuthInfoProvider 인터페이스를 두고 각 소셜 로그인은 해당 구현체를 가진다.</li>
<li>OAuthProviderFinder는 Map 자료구조로 구성해 OAuthInfoProvider의 각 구현체들을 가지고 있는다.</li>
<li>OAuthService는 소셜 로그인 구현체에 의존하는 것이 아닌 OAuthProviderFinder에 의존해 원하는 소셜 로그인 구현체를 불러와서 이용한다.</li>
</ol>
<h3 id="oauthinfoprovider-인터페이스-구성">OAuthInfoProvider 인터페이스 구성</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/c4a52439-90b8-4671-ad08-76620aae952e/image.png" alt=""></p>
<p>OAuth 2.0의 공통 부분인 authorizationCode를 통해 accessToken을 불러오는 것과 accessToken을 기반으로 사용자 resource를 불러오는 것을 추상화 했습니다.</p>
<h3 id="oauthproviderfinder-구성">OAuthProviderFinder 구성</h3>
<p>이후 Map 자료 구조를 활용해 인터페이스의 구현체를 관리하는 OAuthProviderFinder를 구성했습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/3a16f27b-5bbf-4e89-922d-726bd8eb4795/image.png" alt=""></p>
<p><code>OAuthProviderFinder</code>는 <code>OAuthService</code>와 각 플랫폼별 <code>OAuthInfoProvider</code> 구현체 간의 매개체로, <code>OAuthService</code>가 원하는 플랫폼의 구현체를 불러오는 것이 핵심 역할입니다. <code>OAuthProviderFinder</code>는 초기 빈으로 초기 등록될 때 init()메소드가 호출되어 map을 초기화 하도록 구성했습니다. map의 key는 플랫폼의 이름이고 value에는 플랫폼 별로 구현한 <code>OAuthInfoProvider</code> 인터페이스의 구현체로 구성됩니다. getOAuthProvider() 메소드를 통해 <code>OAuthService</code>에서 원하는 소셜 로그인 플랫폼의 구현체를 이용할 수 있게 구성했습니다.</p>
<h3 id="oauthservice-코드의-최종-변화">OAuthService 코드의 최종 변화</h3>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/22e68936-4a55-42e1-a743-f89b85df6a51/image.png" alt=""></p>
<p>기존에는 특정 소셜 로그인 플랫폼의 Provider에 직접 의존했지만, 중간 매개체를 의존하는 방식으로 변경되었습니다. 이로 인해 <code>OAuthService</code>는 필요에 따라 특정 플랫폼의 Provider를 불러와 처리할 수 있게 되어, 새로운 소셜 로그인 플랫폼이 추가되더라도 <code>OAuthService</code>의 코드를 수정할 필요가 없어 OCP를 만족하는 만족하는 구조를 구축했습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[테스트가 용이한 코드는 어떤 코드 일까?]]></title>
            <link>https://velog.io/@seokhwan-an/%ED%85%8C%EC%8A%A4%ED%8A%B8%EA%B0%80-%EC%9A%A9%EC%9D%B4%ED%95%9C-%EC%BD%94%EB%93%9C%EB%8A%94-%EC%96%B4%EB%96%A4-%EC%BD%94%EB%93%9C-%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@seokhwan-an/%ED%85%8C%EC%8A%A4%ED%8A%B8%EA%B0%80-%EC%9A%A9%EC%9D%B4%ED%95%9C-%EC%BD%94%EB%93%9C%EB%8A%94-%EC%96%B4%EB%96%A4-%EC%BD%94%EB%93%9C-%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Tue, 22 Oct 2024 13:01:14 GMT</pubDate>
            <description><![CDATA[<p>이번 초록 스터디 사다리 미션의 1단계 리뷰 과정에서는 <code>테스트가 용이한 코드란 무엇일까?</code>에 대해 깊이 고민할 수 있는 기회가 되었습니다.</p>
<h3 id="미션-설명">미션 설명</h3>
<p>사다리 미션의 1단계는 4x4 사다리를 생성하는 것이었습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/efde43fc-a270-4941-a033-a879fcef68e5/image.png" alt=""></p>
<p>4x4 사다리를 위의 그림과 같이 생성을 하되 핵심은 <strong><code>|-----|-----|</code></strong> 모양이 포함되지 않는 것이었습니다. (<strong><code>|-----|-----|</code> 이 포함될 경우 이후 단계에서 사다리를 탈때 어느 특정 방향으로 이동하는 것이 어렵기 때문에 포함되지 않게 구성해야 했었습니다)</strong></p>
<p>대부분의 스터디원은 크게 <code>Point</code>, <code>Line</code>, <code>Ladder</code> 객체로 사다리를 관리했습니다.</p>
<p>Point 객체 : 건널 수 있는지 혹은 건널 수 없는지를 나타내는 객체
Line 객체 : Point의 일급컬렉션으로 사다리 중 가로 한줄을 나타내는 객체
Ladder 객체 : Line의 일급컬렉션으로 사다리 전체를 나타내는 객체</p>
<h3 id="코드리뷰하기-전-요구사항을-읽은-후-나의-생각">코드리뷰하기 전 요구사항을 읽은 후 나의 생각</h3>
<p>이 요구사항을 보고 가장 먼저 떠오른 생각은, 사다리가 생성될 때 <code>|-----|-----|</code> 와 같은 구조를 포함하고 있는지를 우선으로 검증하는 것이 필요하겠다고 생각했습니다.(Line 객체에서 연속된 두 Point 객체가 모두 true를 가지고 있는지 검증 필요) 더불어 그 부분을 테스트하는 것이 이번 미션의 핵심이라고 생각했습니다. 따라서 이번 코드 리뷰에서는 해당 검증이 코드로 나타나 있는 것을 우선으로 생각을 했고 그 다음으로는 테스트 코드로 직접 테스트가 되어 있는지를 차순으로 생각했습니다. </p>
<h3 id="스터디원들의-코드리뷰를-하면서-생각한-개선방향">스터디원들의 코드리뷰를 하면서 생각한 개선방향</h3>
<p>위와 같이 제 나름의 가이드라인을 가지고 코드리뷰를 진행했습니다. 코드 리뷰 결과 스터디원 세 분이 작성한 코드에는 공통점이 있었습니다. 세 분 모두 Line 객체에서 연속된 건널목에 대한 검증 과정이 포함되어 있지 않았습니다. 대신 스터디원들은 Line을 생성하는 과정에서 연속된 건널목이 생성되지 않게 코드를 통해 Line을 생성했습니다.</p>
<p>한 스터디원의 코드를 예시로 들겠습니다.</p>
<pre><code class="language-java">public class Line {

    private final List&lt;Boolean&gt; points;

    public Line(int lineSize) {
        points = generatePoint(lineSize);
    }

    private List&lt;Boolean&gt; generateLine(int lineSize) {
                // 연속된 건널목이 없이 생성(Random 객체 사용)
                Random random = new Random();
                List&lt;Boolean&gt; points = new ArrayList&lt;&gt;();
                for (int i = 0; i &lt; lineSize; i++) {
                        generatePoint(points, random);
                }
    }

    private void generatePoint(List&lt;Boolean&gt; points, Random random) {
            if (points.isEmpty()) {
                    points.add(random.nextBoolean());
                    return;
            }

            generateNext(points);
    }

    private void generateNext(List&lt;Boolean&gt; points, Random random) {
            boolean isConnected = false;
        int lastIndex = points.size() - 1;
        if(!points.get(lastIndex)){
            isConnected = random.nextBoolean();
        }
        points.add(new Point(isConnected));
    }
}

// 테스트 부분
@DisplayName(&quot;사다리 행 연결 중복 테스트&quot;)
@Test
public void lineDuplicationTest() {
    Line line = new Line(4);
    assertThat(isDuplicate(line)).isEqualTo(false);
}

public boolean isDuplicate(Line line) {
    boolean result = false;
    boolean before = false;
    for(Point point : line.getPoints()) {
        result = result || (before &amp;&amp; point.isConnected());
        before = point.isConnected();
    }
    return result;
}</code></pre>
<p>언뜻 보면 현재 코드는 요구 상황에 맞게 동작하고 테스트 결과도 올바르게 나타나지만 제 생각에는 좋은 테스트 방식은 아니라고 생각했습니다.</p>
<ol>
<li>요구사항에 변화의 변경점이 2개가 된다.</li>
</ol>
<p>위와 같은 상황에서 요구사항이 변경된다면 Line의 요소를 생성하는 <code>generatePoint()</code> 메서드와 테스트 코드 내부에 연속된 건널목 유무를 검증하는 <code>isDuplicate()</code> 메소드가 수정될 것입니다. 테스트 코드 역시 비즈니스 코드와 마찬가지로 하나의 리소스라고 생각하기 때문에 변경에 민감하지 않게 코드를 구성하는 것이 좋다고 생각합니다. </p>
<ol start="2">
<li>제어불가능한 부분이 테스트에 포함되어 있다.</li>
</ol>
<p>현재 generateLine() 메소드를 보면 내부에 Random 객체를 활용해서 사다리의 건널목을 추가할지 말지를 랜덤하게 정해나가고 있습니다. 그렇기에 테스트에서는 이 부분을 제어하기 어렵다고 생각합니다. 물론 현재의 코드는 많이 복잡하지 않기에 비즈니스부분의 코드를 보면 손쉬게 연결된 건널목이 등장하지 않을 것이라는 것을 알 수 있습니다. 하지만 로직이 복잡해진다면 이를 파악하는데 어려울 수 있다고 생각했습니다. 따라서 Line 객체의 핵심인 연속된 건널목의 유무만 판단하면 되는 것이기에 외부에서 완성된 Boolean 리스트를 받아서 연결된 건널목 유무만 검증만 하면 좋을 것 같다고 생각했습니다. </p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/277455a3-62b8-45d9-9d9d-5229563b0ab6/image.png" alt=""></p>
<p>이 리뷰 남긴 후 제가 생각한 테스트 코드를 구현해보았고 다음과 같습니다.</p>
<pre><code class="language-java">public class Line {

    private final List&lt;Boolean&gt; points;

    public Line(List&lt;Boolean&gt; points) {
            validateHasContinuousCross(points);
            this.points = points;
    }

    private void validateHasContinuousCross(List&lt;Boolean&gt; points) {
            // 연속적으로 건널목이 있는 검증
    }
}

@Test
@DisplayName(&quot;사다리 가로 한 라인에서 연속으로 건널목이 있으면 예외를 발생한다.&quot;)
void constructorTest() {
    // given
    List&lt;Boolean&gt; points = List.of(true, true, false, true);

    // when &amp; then
    Assertions.assertThatThrownBy(() -&gt; new Line(points))
        .isInstanceOf(&quot;[정의한 예외 객체]&quot;);
}</code></pre>
<p>위의 코드와 같이 테스트를 구성한 이유는 다음과 같습니다.</p>
<ol>
<li>테스트 코드 내용이 직관적으로 확인이 가능하다.</li>
</ol>
<p>제가 생각하기에 테스트 코드는 작성한 비즈니스 코드가 올바르게 동작하는지 검증하는 역할도 하지만 기능 명세서 역할도 한다고 생각합니다. 제 경험상 저는 협업을 할 때 팀원의 코드가 잘 이해되지 않는 경우에는 테스트 코드를 통해 가이드 라인을 잡고 코드를 이해했었습니다. 그렇기에 테스트 코드를 작성할 때 직관적으로 알아볼 수 있는 것도 중요하다고 생각합니다.</p>
<ol start="2">
<li>테스트의 목적이 명확하다.(제어가 불가능한 부분을 배제했다.)</li>
</ol>
<p>위의 테스트 코드는 Line 객체가 List<Boolean>을 인자로 받아 연속된 true만 있는지 검증하고 있을 뿐 해당 List<Boolean>가 어떻게 생성되었는지에 대해서는 고려를 하고 있지 않기 때문에 테스트의 목적이 명확하다고 생각했습니다. 또한 외부에서 List를 제공하는 것이기에 우리가 원하는 다양한 케이스를 손 쉽게 정할 수 있습니다. (ex List.of(true, true, true, false) 등 다양한 경우를 빠르게 테스트할 수 있다.)</p>
<h3 id="코드리뷰-후-느낀-점">코드리뷰 후 느낀 점</h3>
<p>이번 코드리뷰를 하면서 느낀 점은 제어하기 어려운 부분이 비즈니스 로직에 포함이 되어 있으면 테스트 하는 것이 어렵다는 것을 느꼈습니다. 그렇기에 제어하기 어려운 부분을 외부에서 넘겨받게 코드를 구성하는 것이 다양한 케이스에 대해서 손쉽게 테스트 할 수 있다는 것을 배웠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로또 미션 회고]]></title>
            <link>https://velog.io/@seokhwan-an/%EB%A1%9C%EB%98%90-%EB%AF%B8%EC%85%98-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@seokhwan-an/%EB%A1%9C%EB%98%90-%EB%AF%B8%EC%85%98-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 03 Sep 2024 07:18:41 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/3e66453a-2980-4f35-a65b-1f779fe192a5/image.png" alt=""></p>
<p>6월부터 시작된 초록스터디 로또 미션이 8월이 되어서 마무리가 되었습니다. 로또 미션은 우리가 시중에서 구매할 수 있는 로또 시스템을 만드는 것으로 본 미션의 핵심은 <code>클린 코드</code> 였습니다. 해당 목적을 달성하기 위해 <code>원시값 포장</code>, <code>일급 컬렉션</code> 등의 키워드가 등장했습니다. 이번 미션을 진행하면서 경험을 학습 측면의 관점과 운영적인 측면의 관점에서 회고를 진행하고자 합니다.</p>
<h2 id="운영적인-측면">운영적인 측면</h2>
<p>운영 측면에서 가장 큰 변화는 진행 방식의 개선이었습니다. 이전 자동차 미션에서 발생했던 일정 지연과 2단계씩 진행할 때 집중도가 낮아지는 문제를 해결하기 위해, 이번 로또 미션에서는 한 스프린트 당 2개의 단계를 진행하는 대신, 1개의 단계만 진행하는 방식으로 변경했습니다.</p>
<p>스프린트 단위를 줄인 목표는 미션의 진행 주기를 짧게 하여, 미션을 늘어지지 않고 신속하게 완료하는 것이었습니다. 또한 PR 요청 범위가 줄어들면 리뷰어 입장에서 부담이 적어지고, 리뷰의 질이 향상될 것이라고 기대했습니다.</p>
<p>새로운 운영 방식을 적용한 결과, 로또 미션은 반은 성공적이었고, 반은 그렇지 않았다고 느꼈습니다. 우선 성공적인 부분은 대부분의 스터디원들이 미션을 한 단계씩 진행하면서 부담이 줄었고, 기간 내에 미션을 완료하기 수월했다는 점입니다. 또한, 이전보다 각 단계에 집중할 수 있었다는 긍정적인 피드백을 받았습니다.</p>
<p>반면, 실패한 부분 혹은 개선이 필요했던 부분은 여전히 미션 진행이 늘어지는 문제였습니다. 야근과 취업 준비로 인해 일부 미션이 지연된 경우도 있었지만, 전반적으로 구현 속도와 코드 리뷰 속도 사이의 차이가 원인이었습니다. 대부분 스터디원들이 기간 내에 구현을 완료했으나, 코드 리뷰 과정에서 지연이 발생했습니다. 이 문제의 원인에 대해 고민한 결과, 다음과 같은 두 가지를 생각해 보았습니다.</p>
<ol>
<li>다른 사람의 코드를 읽는 데 어려움을 겪음</li>
<li>코드 리뷰를 처음 해보는 상황에서 가이드라인의 부족</li>
</ol>
<p>이 두 가지가 코드 리뷰 시간 지연의 주요 원인이라고 판단했습니다. 1번 문제는 코드 리뷰에 익숙해지면서 해결될 수 있을 것이라고 생각했고, 2번 문제는 당장 가이드라인을 만드는 것이 어려운 상황이므로, 다음 사다리 미션에서는 제가 모든 스터디원들의 코드 리뷰를 맡아 가이드라인을 만들어볼 계획입니다.</p>
<h2 id="학습적인-측면">학습적인 측면</h2>
<p>저는 로또 미션을 진행하면서 <code>방어적 복사</code> 를 잘 활용해야한다는 것과 도메인과 뷰사이에 보이지 않은 의존성을 잘 제거해야한다는 것을 느꼈습니다. 이 둘과 관련된 사항은 이전 포스팅 으로 정리를 했었습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/85594089-db6b-4f10-8056-2aa74cb3b9be/image.png" alt=""></p>
<p>로또 미션 중 발생한 참조 문제를 간단히 요약하면, 로또 생성 과정에서 여러 개의 로또가 모두 동일한 결과값을 가지는 문제가 있었습니다. 이 문제의 원인은 캐싱된 로또 숫자 리스트(Numbers: 1부터 45 사이의 수)를 사용하여 로또를 생성할 때, 생성된 로또들이 캐싱된 리스트를 참조했기 때문입니다. 그 결과, Numbers를 가공할 때마다 이미 생성된 로또들도 영향을 받았습니다.</p>
<p>이 문제를 해결하기 위해서는, 캐싱된 로또 숫자 리스트에서 로또를 추출한 후, 방어적 복사를 통해 Numbers와의 의존성을 끊어야 합니다. (자세한 내용 보기: <a href="https://velog.io/@seokhwan-an/%EB%A1%9C%EB%98%90-%EB%AF%B8%EC%85%98-%EC%A4%91-%EB%B0%9C%EC%83%9D%ED%95%9C-%EC%B0%B8%EC%A1%B0-%EB%AC%B8%EC%A0%9C">로또 미션 중 발생한 참조 문제</a>)</p>
<p>다음으로는 도메인과 뷰 사이의 간접적인 의존성 문제입니다. 도메인이 뷰에 의존하지 않아야 하는 이유는, 화면과 비즈니스 로직을 분리하여 각각 독립적으로 발전시킬 수 있기 때문입니다. 도메인이 뷰에 의존하게 되면, 화면상의 변화(비즈니스 로직의 변화가 아님)에도 도메인 코드가 수정되어야 하는 상황이 발생할 수 있습니다. 이는 유지보수 과정에서 예측 불가능한 문제를 초래할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/38f8b115-b48d-49b9-9637-cdec04c22bc9/image.png" alt=""></p>
<p>한 스터디원의 코드 구조도(의존도)가 위와 같았습니다. 현재 뷰는 DTO에 의존하고 있어 도메인에는 직접적인 영향이 없을 것처럼 보였습니다. 하지만 실제로는 도메인이 DTO를 생성하면서 도메인이 DTO에 의존하는 문제가 발생했습니다. 이로 인해 발생할 수 있는 문제 상황은 다음과 같습니다.</p>
<p>만약 뷰에서 화면에 노출해야 할 정보가 수정되면, DTO도 수정되어야 합니다. 이 경우, 도메인이 DTO를 생성하고 있기 때문에 도메인의 코드 역시 변경되어야 하는 상황이 발생합니다. 즉, 도메인이 뷰에 직접적으로 의존하지는 않지만, 간접적으로 화면 변화에 의해 영향을 받는다는 점을 알 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/e8b50d51-fe93-4d67-9079-af5cac94d856/image.png" alt=""></p>
<p>이를 해결하기 위한 방법으로는 DTO가 스스로를 생성하는 방안을 고려할 수 있습니다. 즉, DTO가 도메인을 파라미터로 받아 자신을 생성하는 방식입니다. 이렇게 하면 의존도가 변경되어, 도메인과 뷰가 서로 직간접적으로 의존하지 않게 됩니다. (자세한 내용 보기: <a href="https://velog.io/@seokhwan-an/Domain%EC%9D%B4-DTO%EB%A5%BC-%EC%83%9D%EC%84%B1%ED%95%B4%EB%8F%84-%EB%90%A0%EA%B9%8C">Domain이 DTO를 생성해도 될까</a>)</p>
<p>로또 미션을 우아한테크코스 프리코스 때 처음 구현해보고, 이번에 다시 구현하면서 1년간 큰 성장을 했음을 느꼈습니다. 이전에는 단순히 미션을 구현하는 데 급급했지만, 이제는 다양한 개념을 적용하고 발생한 문제를 보다 논리적으로 파악할 수 있게 되었습니다. 이 경험을 통해 최근 느꼈던 성장에 대한 의구심이 자신감으로 바뀌기 시작했습니다.</p>
<h3 id="내가-구현한-pr">내가 구현한 PR</h3>
<ul>
<li><a href="https://github.com/next-step/java-lotto-clean-playground/pull/31">로또 미션 1단계 PR</a></li>
<li><a href="https://github.com/next-step/java-lotto-clean-playground/pull/35">로또 미션 2단계 PR</a></li>
<li><a href="https://github.com/next-step/java-lotto-clean-playground/pull/49">로또 미션 3단계 PR</a></li>
<li><a href="https://github.com/next-step/java-lotto-clean-playground/pull/54">로또 미션 4단계 PR</a></li>
<li><a href="https://github.com/next-step/java-lotto-clean-playground/pull/58">로또 미션 5단계 PR</a></li>
</ul>
<h3 id="내가-리뷰한-pr">내가 리뷰한 PR</h3>
<ul>
<li><a href="https://github.com/next-step/java-lotto-clean-playground/pull/32">https://github.com/next-step/java-lotto-clean-playground/pull/32</a></li>
<li><a href="https://github.com/next-step/java-lotto-clean-playground/pull/37">https://github.com/next-step/java-lotto-clean-playground/pull/37</a></li>
<li><a href="https://github.com/next-step/java-lotto-clean-playground/pull/39">https://github.com/next-step/java-lotto-clean-playground/pull/39</a></li>
<li><a href="https://github.com/next-step/java-lotto-clean-playground/pull/40">https://github.com/next-step/java-lotto-clean-playground/pull/40</a></li>
<li><a href="https://github.com/next-step/java-lotto-clean-playground/pull/50">https://github.com/next-step/java-lotto-clean-playground/pull/50</a></li>
<li><a href="https://github.com/next-step/java-lotto-clean-playground/pull/53">https://github.com/next-step/java-lotto-clean-playground/pull/53</a></li>
<li><a href="https://github.com/next-step/java-lotto-clean-playground/pull/59">https://github.com/next-step/java-lotto-clean-playground/pull/59</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Domain이 DTO를 생성해도 될까?]]></title>
            <link>https://velog.io/@seokhwan-an/Domain%EC%9D%B4-DTO%EB%A5%BC-%EC%83%9D%EC%84%B1%ED%95%B4%EB%8F%84-%EB%90%A0%EA%B9%8C</link>
            <guid>https://velog.io/@seokhwan-an/Domain%EC%9D%B4-DTO%EB%A5%BC-%EC%83%9D%EC%84%B1%ED%95%B4%EB%8F%84-%EB%90%A0%EA%B9%8C</guid>
            <pubDate>Fri, 26 Jul 2024 11:10:07 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/c23ef6f2-d8d9-4b89-bab1-bd1fbeca7615/image.png" alt=""></p>
<p>초록스터디 로또 미션에서 한 스터원이 당첨 통계를 출력할 때 View가 Domain에 의존해 화면을 출력하고 있어서 View에서 Domain을 의존하지 않게 수정해보는 것을 리뷰로 남겼었습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/29cd7689-4267-4c8a-b153-ccbd1c404154/image.png" alt=""></p>
<p>이와 같은 리뷰를 남긴 이유는 위의 사진에도 나와있듯 View에서 Domain 객체를 이용해 오용할 수 있는 가능성이 있어서 였습니다. 물론 현재는 혼자서 구현을 하는 것이다 보니 오용할 가능성이 낮지만 View가 Domain에 의존하면 오용의 가능성이 생긴다는 것을 스터디원에게도 전달하고 싶었습니다.</p>
<p>이를 통해서 제가 의도한 개선 방향은 View 메소드에 Domain 객체를 파라미터로 받는 대신에 Getter를 통해 필요한 정보만 파라미터로 전달하는 것이었습니다. 간단하게 코드로 설명하면 아래와 같습니다.(실제 코드와는 다르며 한눈에 볼 수 있게 간소화 했습니다.)</p>
<pre><code class="language-java">public class Lottos {
        private final List&lt;Lotto&gt; lottos;
        ...

        public List&lt;Integer&gt; getNumbers() {
                return numbers;
        }

        public Map&lt;Rank, Integer&gt; makeStatistics(final Lotto winningLotto) {
                Map&lt;Rank, Integer&gt; statistic = new HashMap&lt;&gt;();
                // 통계 생성
                ...
                return statistic;
        }

        public double makeProfitRate(final Map&lt;Rank, Integer&gt; statistics, final Money purchaseMoney) {
                // 수익률을 만드는 로직
                ...
                return profitRate;
        }
}

public class OutputView {

        // 기존의 코드
        public void printLotto(final Lottos lottos) {}

        // 개선했으면 하는 방향
        public void printLottoResult(final Map&lt;Rank, Integer&gt; statistics, final double profitRate) {}
}</code></pre>
<p>코드에 대해서 간단히 설명을 하면  <code>makeStatistics()</code>는 당첨 번호와 구매한 로또를 비교해 등수를 정하는 메소드이고 <code>makeProfitRate()</code>는 로또 구입 비용과 상금을 통해 수익률을 구하는 메소드 입니다.</p>
<p>View가 Domain에 의존하지 않기 위해 <code>printLottoResult()</code> 메소드에서 Lottos 객체를 받는 대신에 Map&lt;Rank, Integer&gt;와 double을 파라미터로 받는 것을 의도했었습니다.</p>
<p>제 의도와 다르게 스터디원은 DTO를 이용해서 view에 의존성을 제거하는 방향을 택했습니다. DTO(Data Transfer Object)는 여러 계층 사이에서 데이터를 전달하는 객체로 그 안에는 비즈니스 로직을 포함하지 않고 순수하게 데이터를 가진다는 특징을 가지고 있습니다.  </p>
<p>이와 같은 특징을 보았을 때 DTO를 이용하는 것 역시 view에서 비즈니스 로직의 오용을 막을 수 있는 방안이어서 좋은 방식이라고 생각했습니다. 하지만 스터디원의 수정된 코드를 보면서 의문점이 생기기 시작했습니다.(실제 코드를 간소화 한 것입니다.)</p>
<pre><code class="language-java">public class Lottos {
        private final List&lt;Lotto&gt; lottos;

        // 의문을 가진 코드
        public StatisticsDto makeStatistics() {
                // 통계를 생성하는 로직
                return new StatistsDto();
        }
}

public class StatisticsDto {
        private final Map&lt;Rank, Integer&gt; statistic;
        private final double profitRate;
}

public class OutputView {
        public void printStatistic(final StatisticsDto statistic) {
                // 로또 결과 출력
        }
}</code></pre>
<p>제가 의문점을 가진 부분은 domain에서 Dto를 생성하는 팩토리 메소드(makeStatistics())였습니다. 제가 의문섬을 가지기 시작한 이유에 앞서서 전체적인 의존도를 도식화 하면 아래와 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/72283f13-0bca-4d33-8010-de65c607f073/image.png" alt=""></p>
<p>도식화한 것의 특징은 domain과 view 모두를 DTO를 의존한다는 것입니다. 이런 상황에서 DTO가 변경되거나 수정되면 Domain의 코드(DTO를 생성하는 메소드)의 수정이 발생합니다. 이 상황이 문제가 된다고 생각했는데 그 이유는 다음과 같습니다.</p>
<p>현재 DTO는 주로 비즈니스 요구사항 보다는 화면에 나타내야하는 정보가 변경되어야 할 때 변경 및 수정이 발생합니다. 즉, 화면 요구사항의 변경 → DTO 수정 → DTO를 생성하는 Domain 코드의 수정으로 이어지게 되어Domain이 View에 직접적으로 의존하는 것은 아니지만 View의 변화에 의해 Domain의 변경이 발생합니다. 이는  MVC의 본질인 <strong><code>관심사 분리</code></strong>가 명확하게 분리되지 않았음을 의미한다고 생각했습니다.</p>
<p>그러면 DTO의 특징(비즈니스 로직을 포함하지 않고 순수한 데이터를 가지는 객체)을 유지하면서 View의 변화가 Domain까지 이어지지 않게 DTO를 사용하기 위해서는 어떻게 해야할까요? 제 스스로 내린 해결책은 DTO의 생성 책임을 Domain에서 하는 것이 아닌 DTO 스스로가 처리하여 Domain이 DTO의 의존하는 상황을 제거하는 방안입니다.</p>
<pre><code class="language-java">public class StatisticsDto {
        private final Map&lt;Rank, Integer&gt; statistics;
        private final double profitable;

        public StatisticsDto(final Map&lt;Rank, Integer&gt; statistics, final double profitable) {
                this.statistics = statistics;
                this.profitable = profitable;
        }

        public static StatisticsDto of(final Lottos lottos, final Lotto winningLotto, final Money purchaseMoney) {
                Map&lt;Rank, Integer&gt; statistics = lottos.makeStatistics(winningLotto);
                double profitRate = lottos.makeProfitRate(statistics, purchaseMoney);
                return new StatisticDto(statistics, profitRate);
        }
} </code></pre>
<p>위의 코드와 같이 DTO의 생성 책임을 스스로 처리한다면 의존 방향에 변화가 생깁니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/58afe29e-588c-4eb7-92c5-c19299af1170/image.png" alt=""></p>
<p>생성을 DTO가 스스로 책임짐으로써 파라미터로 Domain 객체를 받기 때문에 Domain이 DTO에서 의존하던 것에서 DTO가 Domain에 의존하는 방향으로 변경됩니다. 이를 통해서 DTO에 새로운 필드가 추가되거나 삭제가 되더라도 변화가 Domain까지 이어지지 않아 Domain이 화면 변화에 영향을 받지 않게 됩니다.</p>
<p>현재 스터디원이 DTO의 생성 책임을 스스로 처리하도록 리뷰를 남겨놓은 상태입니다.</p>
<p><img src="https://velog.velcdn.com/images/seokhwan-an/post/22486de3-2d72-4437-afe0-38554733428f/image.png" alt=""></p>
<h3 id="결론">결론</h3>
<p>DTO는 두 영역(Domain, View) 사이에서 의존성을 제거하는데 이용할 수 있다. 하지만 DTO를 잘 못 사용하면 View와 Domain 사이에 직접 적인 의존관계가 아니더라도 한쪽 변화에 의해 다른 한쪽도 영향을 받는 상황이 발생할 수 있다.(DTO의 변화에 Domain 코드가 변경되는 상황) 그렇기에 DTO의 생성은 DTO 스스로가 책임지도록 구성하여 화면상에 변화가 Domain까지 이어지지 않게 해야한다.</p>
]]></description>
        </item>
    </channel>
</rss>