<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dailycode</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Fri, 27 Feb 2026 08:18:38 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dailycode</title>
            <url>https://velog.velcdn.com/images/o_seongblll_/profile/660a0ed7-7303-4288-a671-ade5d955c664/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dailycode. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/o_seongblll_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[OS] 공룡책 운영체제 CH 2. Operating-System Structures]]></title>
            <link>https://velog.io/@o_seongblll_/OS-%EA%B3%B5%EB%A3%A1%EC%B1%85-%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-CH-2.-Operating-System-Structures</link>
            <guid>https://velog.io/@o_seongblll_/OS-%EA%B3%B5%EB%A3%A1%EC%B1%85-%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-CH-2.-Operating-System-Structures</guid>
            <pubDate>Fri, 27 Feb 2026 08:18:38 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 내용은 공룡책이라 불리는 Abraham Silberschatz, Peter Baer Galvin, Greg Gagne의 『Operating System Concept 10th』 을 바탕으로 작성하였습니다.</p>
</blockquote>
<h2 id="21-operating-system-services">2.1 Operating-System Services</h2>
<p>운영체제는 프로그램들이 실행될 환경을 제공한다.
정확하게는 특정 서비스를 프로그램과 사용자들에게 제공하는데, 이 서비스들이 개발자가 프로그래밍을 더욱 쉽게 할 수 있게 해준다.</p>
<h4 id="유저-관점에서-유용한-서비스들">유저 관점에서 유용한 서비스들</h4>
<ul>
<li>User Interface (GUI, touch-screen interface, CLI)</li>
<li>Program execution :  프로그램을 메모리에 적재해 실행시키고, 정상/비정상 적으로 종료할 수 있다.</li>
<li>I/O operations : 실행중인 프로그램은 입출력을 요구할 수 있다. 
OS는 입출력을 수행할 수단을 제공한다. (유저가 직접 입출력 제어 x)</li>
<li>File-system manipulattion : 파일을 읽고 쓸 수 있고, 생성 및 삭제등의 기능을 수행할 수 있다. 
일부 OS들은 특정 파일이나 디렉토리에 대한 접근을 허용/거부하는 권리를 포함한다.</li>
<li>Communications : 프로세스끼리 정보를 교환해야하는 상황에 다음 방식으로 교환한다.<ul>
<li>shared memory : 메모리를 공유</li>
<li>message passing : 정보의 패킷을 옮김</li>
</ul>
</li>
<li>Error detection : OS는 에러를 즉시 찾고 고칠 수 있어야 한다. 
(적절한 행동을 취하기 위해 시스템을 멈춰서라도 조치를 취해야 한다.)</li>
</ul>
<h4 id="프로세스-관점에서-유용한-서비스들">프로세스 관점에서 유용한 서비스들</h4>
<ul>
<li>Resource allocation : 다양한 프로세스들이 동시에 실행되기에, OS는 다양한 자원 (CPU cycles, memory 등) 을 관리해야한다.</li>
<li>Logging : 프로그램이 어떤 자원을 얼마나 쓰는지 추적한다. </li>
<li>Protection and security : 프로세스가 다른 프로세스를 방해하면 안된다. 
또한 OS가 자원에 대한 접근을 통제하는 것을 보장해야 한다.
외부로부터 시스템을 보호하는 것도 포함한다.</li>
</ul>
<h2 id="23-system-calls">2.3 System Calls</h2>
<p>시스템 콜이란 운영체제가 제공하는 서비스에 대한 인터페이스를 제공한다.</p>
<p><img src="https://velog.velcdn.com/images/o_seongblll_/post/71a9b9f6-27b7-41c5-8f5d-8ede32bc686e/image.png" alt="">
a file의 내용을 b file에 복사하는 과정이다. 간단한 작업인데도 수많은 system-call이 사용된다. </p>
<p>개발자들은 이 모든 system call을 알 필요없이 API(application programming interface)를 사용해서 작업을 수행한다.
API는 특정 함수들의 집합인데 이 함수들이 system call을 대신 해준다.</p>
<p>RTE (Run time environment)가 system call의 인터페이스를 제공한다. 
API의 호출을 가로채서 실제 OS의 시스템 콜을 호출한다.</p>
<p><del>그냥 사용자는 구체적인 것을 몰라도 된다라고 생각하면 된다.</del></p>
<h4 id="운영체제에-매개변수를-전달하는-방법">운영체제에 매개변수를 전달하는 방법</h4>
<ul>
<li>(매개변수의 수가 5개 이하라면) 매개변수를 레지스터에 전달 </li>
<li>매개변수를 메모리 내 블록이나 테이블에 저장후 그 블록의 주소를 레지스터에 전달</li>
<li>스택에 push &amp; pop</li>
</ul>
<h2 id="292-system-boot">2.9.2 System Boot</h2>
<p>운영체제가 생성된 이후에 하드웨어에 의해 사용가능한 상태가 되어야 한다.
하드웨어는 다음과 같은 방법으로 커널을 적재한다.</p>
<ol>
<li>커널 찾기: 부트 로더(Boot Loader)라는 작은 코드가 커널의 위치를 파악함</li>
<li>메모리 로드: 커널을 메모리에 올리고 실행</li>
<li>하드웨어 초기화: 커널이 하드웨어 통제</li>
<li>파일 시스템 마운트: 운영체제가 파일들에 접근할 수 있도록 루트 파일 시스템을 연결</li>
</ol>
<hr>
<ul>
<li><p>BIOS라는 첫 번째 부트로더가 실행되고, 이 부트로더가 두 번째 부트로더(boot block)을 적재하는 경우도 있다. 
두 번째 부트로더가 전체 OS 를 메모리로 가져오고 실행시킨다.</p>
</li>
<li><p>오늘날에는 BIOS 대신 UEFI가 사용되는데 부트 로더가 상태 진단, 초기화등의 기능을 추가로 수행한다.</p>
</li>
</ul>
<hr>
<p>GRUB은 Linux와 Unix에서 쓰이는 부트 로더인데, 이는 부팅하는 동안 kernal 매개변수를 수정하고 특정 kernal을 선택하는 등의 기능을 가지고 있다.</p>
<p>공간을 아끼기 위해 압축되어 disk에 올려졌다가, 메모리에 올라가서 압축해제 된다.
부팅 과정에서 진짜 파일 시스템을 설치하기 위해 필요한 필수 드라이버와 kernal 모듈을 가지고 있는 일시적인 RAM 파일 시스템 &#39;initramfs&#39; 를 만든다.</p>
<p>커널이 시작되고 필수 드라이버가 설치되면 진짜 파일 시스템으로 전환하고, initial process 인 &#39;systemd&#39;를 생성한다.</p>
<p><del>Mobile은 initramfs를 진짜 파일 시스템으로 계속 사용한다.</del></p>
<p>또한 부트 로더는 하드웨어 이슈를 진단하고, 파일 시스템을 수정하고, os를 재설치할 수 있는 recovery mode도 제공한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CLOUD NATIVE] 클라우드 네이티브한 서비스 만들기[6] - 배포 준비(Kubernetes)]]></title>
            <link>https://velog.io/@o_seongblll_/CLOUD-NATIVE-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B06-%EB%B0%B0%ED%8F%ACKubernetes</link>
            <guid>https://velog.io/@o_seongblll_/CLOUD-NATIVE-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B06-%EB%B0%B0%ED%8F%ACKubernetes</guid>
            <pubDate>Wed, 07 Jan 2026 07:09:30 GMT</pubDate>
            <description><![CDATA[<p>우선 프로젝트를 진행할 당시 공모전 단체에서 클러스터를 제공해줬기에 클러스터 생성은 내가 하지 않았다. 
<del>이후에 공부할 겸 한 번 직접 만들어봤는데 <a href="https://dev-gallery.tistory.com/66">https://dev-gallery.tistory.com/66</a> 이 블로그를 보고 하니 잘 됐다.</del></p>
<p>또 공모전이 끝난 지금 내가 클러스터에 접근할 수 없기 때문에, 아래 적은 방법들은 내가 직접 배포할 때 간단하게 기록해놓은 것을 바탕으로 한다는 것을 적어둔다. </p>
<p>그래서 기록하지 말까도 생각해봤는데, 미래의 나를 위해서 시행착오를 겪은 경험이라도 적어두는게 나을 것 같아서 기록한다.
<del>그래서 사진이 없을 수도, 정확하지 않은 정보가 있을 수도 있다.</del></p>
<p>공모전 기관이 제공해준 인프라는 Naver Cloud Platform 의 Ncloud Kubernetes Service, SourcePipeline등 이다.</p>
<h2 id="쿠버네티스kubernetes-k8s">쿠버네티스(Kubernetes, k8s)</h2>
<p>쿠버네티스는 애플리케이션이 아니라 컨테이너 단위로 앱을 실행하고 관리한다. 
따라서 msa 구조에 적합한 도구이다.</p>
<h3 id="쿠버네티스-특징">쿠버네티스 특징</h3>
<p><a href="https://kubernetes.io/ko/docs/concepts/overview">https://kubernetes.io/ko/docs/concepts/overview</a></p>
<ol>
<li>자동화된 복구 : 실행중인 컨테이너가 문제가 생겨 동작하지 않으면, 자동으로 다시 시작한다.</li>
<li>로드밸런싱 : 트래픽이 한 곳으로 쏠리지 않게 여러 컨테이너에 골고루 나누어 준다.</li>
<li>무중단 배포 : 서비스를 끄지 않고도 새 버전을 배포할 수 있고, 문제가 생기면 순식간에 이전 버전으로 되돌릴 수 있다.</li>
</ol>
<h3 id="사용이유">사용이유</h3>
<p>msa 구조는 관리해야 할 서비스가 많아지기 때문에 직접 하나하나 관리하기에는 어려움이 있다. 쿠버네티스는 msa구조에서 발생할 수 있는 문제들을 자동으로 관리해주기 때문에 자연스럽게 사용하게 되었다.</p>
<h2 id="배포과정">배포과정</h2>
<p>ci/cd를 통해 자동으로 배포를 할 수도 있지만, 처음에 쿠버네티스가 제대로 동작하는지 확인하고 싶어서 처음에는 직접 이미지를 올리고 진행했다.</p>
<h3 id="컨테이너-이미지-올리기">컨테이너 이미지 올리기</h3>
<p>쿠버네티스는 컨테이너 이미지를 기반으로 실행되기 때문에 먼저 DockerFile을 작성하고 빌드해 이미지를 Container Registry에 올려두었다.</p>
<p><a href="https://guide.ncloud-docs.com/docs/container-ncr-1-3">NCP Container Registry</a> 를 참고해서 진행하였다.</p>
<ol>
<li>Dockerfile 작성<pre><code class="language-dockerfile"># user-service/Dockerfile
# 자바 런타임 환경 설정
FROM amazoncorretto:17
</code></pre>
</li>
</ol>
<h1 id="애플리케이션이-배포될-독립-디렉터리-생성">애플리케이션이 배포될 독립 디렉터리 생성</h1>
<p>RUN mkdir -p /usr/app/user-service</p>
<h1 id="애플리케이션이-위치할-경로-환경-변수로-설정">애플리케이션이 위치할 경로 환경 변수로 설정</h1>
<p>ENV APP_HOME=/usr/app/user-service</p>
<h1 id="작업-디렉토리-설정">작업 디렉토리 설정</h1>
<p>WORKDIR ${APP_HOME}</p>
<h1 id="jar-파일-복사">JAR 파일 복사</h1>
<p>ARG JAR_FILE=build/libs/user-service-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} user-service.jar</p>
<h1 id="컨테이너에서-노출할-포트">컨테이너에서 노출할 포트</h1>
<p>EXPOSE 8080</p>
<h1 id="spring-프로파일을-동적으로-설정할-수-있도록-cmd-명령어-수정">Spring 프로파일을 동적으로 설정할 수 있도록 CMD 명령어 수정</h1>
<p>ENV SPRING_PROFILES_ACTIVE=k8s
CMD [&quot;java&quot;, &quot;-Dspring.profiles.active=${SPRING_PROFILES_ACTIVE}&quot;, &quot;-jar&quot;, &quot;user-service.jar&quot;]</p>
<pre><code>서비스마다 Dockerfile을 작성하여 각각 빌드해야 한다.

2. 빌드 명령어 : `./gradlew clean build`
빌드시 무슨 에러가 엄청 뜨는데 eureka 서버가 안띄워져 있다는 뜻이었다. 

3. docker container 이미지 빌드 : `docker build -t [이미지명] .`
마지막 &#39;.&#39;을 꼭 붙여줘야 한다. (빌드 경로)

4. docker login : `docker login [레지스트리 주소]`
이후 username과 password에 api인증키 입력

5. 이미지 태그 만들기 : `docker tag [기존_이미지명]:[기존_태그] [새_이미지명]:[새_태그]`
이때 NCP Container Registry 에 올릴 거면 새 이미지를 
[레지스트리 이름].kr.ncr.ntruss.com/[이미지이름]:[태그] 
이런 식으로 만들면 편리하다.

6. 이미지를 저장소에 push : `docker push [새_이미지명]:[새_태그]`

### 클러스터 접속
클러스터를 관리하기 위해 kubectl을 사용해 클러스터에 접속해야 한다.
powershell에서 작업을 수행할 때 **관리자 권한으로 접속**하자.

1. kubectl 설치 : 클러스터를 제어하는 데 사용되는 CLI
powershell 터미널에서 진행하면 된다.
`choco install kubernetes-cli` (설치) -&gt; `kubectl version --client` (설치 및 버전 확인)

2. ncp-iam-authenticator 설치 : IAM 인증 도구
[ncp-iam-authenticator 설치 (windows)](https://guide.ncloud-docs.com/docs/k8s-iam-auth-ncp-iam-authenticator#windows-설치) 를 보고 진행하였다. 
- powershell에서 다음 명령어 수행
~~`curl -o ncp-iam-authenticator.exe -L https://github.com/NaverCloudPlatform/ncp-iam-authenticator/releases/latest/download/ncp-iam-authenticator_windows_amd64.exe`~~ 
~~(그새 명령어가 바뀐 모양이다)~~
`curl.exe -LO &quot;https://dl.k8s.io/release/v1.35.0/bin/windows/amd64/kubectl-convert.exe&quot;`

- C: 에 폴더 생성후 다운받은 실행 파일 집어넣기
![](https://velog.velcdn.com/images/o_seongblll_/post/58b1420a-5151-48c3-bbbc-465825d2ff6e/image.png)

- 시스템 속성 - 고급 - 환경 변수 - 시스템 변수 path 에 넣어주기
![](https://velog.velcdn.com/images/o_seongblll_/post/ec86183b-b82a-4f24-9c20-38413e433d77/image.png)

- ncp-iam-authenticator 설치 확인
`ncp-iam-authenticator help` 에러가 안뜨면 잘 설치가 된거다.

3. IAM 인증 kubeconfig 생성 
[IAM 인증 kubeconfig 생성 및 업데이트](https://guide.ncloud-docs.com/docs/k8s-iam-auth-kubeconfig) 를 참고했다.
- OS 환경 변수 설정 
powershell에서 $env:NCLOUD_ACCESS_KEY = [할당받은 ACCESS_KEY 입력]
확인은 $env:NCLOUD_ACCESS_KEY 를 입력해주면 된다.


![](https://velog.velcdn.com/images/o_seongblll_/post/fd11182a-9695-42c0-b3db-21fd6e48c7cb/image.png)

- configure 파일 설정 
OS 환경변수만 해도 되는건지 모르겠는데 나는 둘 다 만들었다.
**bash 터미널**을 이용해서 `vi ~/.ncloud/configure`
이후 아래 내용을 작성하고 &#39; : + w &#39; (저장) → &#39; : + q &#39; (나가기) 를 수행하면 된다.</code></pre><p>ncloud_access_key_id     = {accesskey} // 본인 access key 입력
ncloud_secret_access_key = {secretkey}
ncloud_api_url           = <a href="https://ncloud.apigw.ntruss.com">https://ncloud.apigw.ntruss.com</a></p>
<pre><code>
- kubeconfig 생성
`ncp-iam-authenticator create-kubeconfig --region KR --clusterUuid { 본인의 cluster uuid } --output kubeconfig.yaml` 
![](https://velog.velcdn.com/images/o_seongblll_/post/17822d8b-19c7-42dd-882f-94917ed1147c/image.png)

- 테스트
`kubectl get namespaces --kubeconfig kubeconfig.yaml`
![](https://velog.velcdn.com/images/o_seongblll_/post/0284866e-7d02-4dc5-8892-0f2493339aeb/image.png)

- 환경변수 등록 
기본경로가 아닌 폴더에 configure를 만들었기에 window에게 configure 파일위치를 알려줘야 한다.
`setx KUBECONFIG &quot;C:\경로\kubeconfig.yaml&quot;`
![](https://velog.velcdn.com/images/o_seongblll_/post/9307715e-a85f-4cc9-b7e7-e83c921e3f52/image.png)

- 연결 확인 (새 터미널에서) 
`kubectl get nodes`
![](https://velog.velcdn.com/images/o_seongblll_/post/296375db-0067-4ffa-b777-eeec930e9b49/image.png)

## 정리 
- kubernetes는 애플리케이션이 빌드 되는 방식이 아닌, 컨테이너 이미지를 실행되는 방식으로 운영된다.
- 관리자는 kubectl을 통해 전반적으로 kubernetes를 운영 및 관리할 수 있다.</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[CLOUD NATIVE] 클라우드 네이티브한 서비스 만들기[5] - 서비스간 통신 (Feign)]]></title>
            <link>https://velog.io/@o_seongblll_/CLOUD-NATIVE-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B05-%EC%84%9C%EB%B9%84%EC%8A%A4%EA%B0%84-%ED%86%B5%EC%8B%A0-Feign</link>
            <guid>https://velog.io/@o_seongblll_/CLOUD-NATIVE-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B05-%EC%84%9C%EB%B9%84%EC%8A%A4%EA%B0%84-%ED%86%B5%EC%8B%A0-Feign</guid>
            <pubDate>Thu, 20 Nov 2025 15:15:37 GMT</pubDate>
            <description><![CDATA[<h2 id="내부-서비스-통신">내부 서비스 통신</h2>
<h3 id="필요성">필요성</h3>
<p>msa 구조는 말그대로 마이크로서비스로 나누어져 있기에 각 서비스끼리 통신하기 위해서 모놀리식과 다른 방법이 필요하다.</p>
<p>모놀리식 방식에서는 필요한 서비스 계층의 클래스를 주입받아 사용하면 되었지만, 서비스가 분리되면 그 방식이 불가능하다.
그래서 다른 서비스에게 api 요청을 하고 그 응답을 받아 사용해야 한다.</p>
<h3 id="외래키-저장">외래키 저장</h3>
<p>더불어 모놀리식 방법에서 객체와 객체간의 관계를 FK로 사용했다면 이것도 사용불가능하다.
그래서 A서비스의 A&#39;객체가 B서비스의 B&#39;객체가 연관되어 있다면 B&#39;의 ID를 Long 으로 A&#39;객체의 저장해서 사용했다.</p>
<pre><code class="language-java">@Getter
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(
        name = &quot;group_user&quot;,
        uniqueConstraints = @UniqueConstraint(columnNames = {&quot;group_id&quot;, &quot;user_id&quot;})
) // 유니크 제약 조건을 통해 그룹에 중복 가입되는 것을 막음.
public class GroupUser extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    ...

    @Column(name = &quot;user_id&quot;, nullable = false)
    private Long userId;

    ...

  }
</code></pre>
<p>우리 서비스가 사용한 코드의 예시이다.
group-service는 user 객체를 직접 불러올 수 없으니 Group에 User가 가입할 때 feign을 사용하여 Id를 받아와 이렇게 Long으로 저장하게 하였다.</p>
<h2 id="코드-구현">코드 구현</h2>
<p>코드는 user-service 의 코드들을 사용해서 기록할 예정이다.
꼭 각 서비스에 feign 설정이나 client를 만들어줘야 사용할 수 있다.</p>
<h3 id="user-serviceapplicationyml">user-service/application.yml</h3>
<pre><code class="language-yml">  #Feign 설정
  spring:
      ...
    cloud:
      openfeign:
        client:
          config:
            default:
              connectTimeout: 1000       # 1s
              readTimeout: 2000          # 2s
              loggerLevel: BASIC</code></pre>
<p>  application.yml에 feign 설정값을 적어준다.
connectTimeOut은 다른 서비스를 연결하는데 기다리는 시간, readTimeOut은 연결 후 서비스의 응답을 기다리는 시간으로 각각 해당 시간을 초과하면 예외가 발생한다. 
(Client입장에서는 500번대 에러를 받음, 내부 서비스 로그를 확인해 어디에서 예외가 발생한지 알 수 있다.)</p>
<h3 id="user-servicegroupclient">user-service/GroupClient</h3>
<pre><code class="language-java">@FeignClient(name = &quot;group-service&quot;)
public interface GroupClient {

    @PostMapping(&quot;/api/v1/groups/me/default&quot;)
    void createDefaultGroup(
            @RequestBody DefaultGroupCreateRequest request
    );

}</code></pre>
<p>내부 서비스끼리 통신하기 위한 FeignClient이다. 
인터페이스와 어노테이션 기반으로 쉽게 만들 수 있다.</p>
<p>이렇게 만들어두고 group-service의 컨트롤러에서 다음과 같이 일반 rest api 요청을 받는 것처럼 만들어주면 된다.</p>
<h3 id="group-servicegroupcontroller">group-service/GroupController</h3>
<pre><code class="language-java">@PostMapping(&quot;/api/v1/groups/me/default&quot;)
public void createDefaultGroup(
    @RequestBody DefaultGroupCreateRequestDto requestDto
){
    groupService.createDefaultGroup(requestDto);
}</code></pre>
<p>여기서 발생한 문제점?은 각 서비스가 dto를 공유하지 않기에 서비스마다 dto를 각자 만들어야 한다는 것이었다. 
그래서 검색을 조금 해봤는데 다음과 같은 방법을 사용해도 좋을 것 같다. 
<a href="https://www.inflearn.com/community/questions/209291/msa-%EA%B0%84%EC%9D%98-%ED%86%B5%EC%8B%A0-%EC%8B%9C-dto-%EB%AC%B8%EC%9D%98">MSA 간의 통신 시 DTO 문의</a></p>
<h3 id="user-serviceuserserviceapplication">user-service/UserServiceApplication</h3>
<pre><code class="language-java">@SpringBootApplication
@EnableJpaAuditing
@EnableFeignClients // 추가하기
@Import(GlobalExceptionHandler.class)
@EnableScheduling
public class UserServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }

}</code></pre>
<p>이렇게 메인 클래스에 @EnableFeignClients를 붙여줘야 OpenFeign을 사용할 수 있다.</p>
<h2 id="정리">정리</h2>
<ul>
<li>msa구조에서는 내부 서비스끼리 통신을 해야한다.</li>
<li>OpenFeign을 이용해서 내부 서비스 통신을 쉽게 구현할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CLOUD NATIVE] 클라우드 네이티브한 서비스 만들기[4] - 요청/응답 Logging]]></title>
            <link>https://velog.io/@o_seongblll_/CLOUD-NATIVE-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B04-%EC%9A%94%EC%B2%AD%EC%9D%91%EB%8B%B5-Logging</link>
            <guid>https://velog.io/@o_seongblll_/CLOUD-NATIVE-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B04-%EC%9A%94%EC%B2%AD%EC%9D%91%EB%8B%B5-Logging</guid>
            <pubDate>Tue, 18 Nov 2025 08:22:29 GMT</pubDate>
            <description><![CDATA[<h2 id="logging">Logging</h2>
<p>애플리케이션에 문제가 생겼을 때, 문제가 발생한 원인을 찾기 위해서는 로그를 확인해보아야 한다. 
어느 시점에서 어떤 요청이 있었고, 어떤 것이 실행되었는지 그런 것들을 로깅해야한다. </p>
<p>msa구조에선 apigateway를 통과해도 내부 서비스로 가는 과정에서 오류가 생길 수 있기에 더더욱 로깅을 신경써서 해주어야 한다.</p>
<p>내가 기록할 것은 GlobalFilter로 api 요청 및 응답 로깅을 위해 사용되는 필터이다.</p>
<h2 id="코드-구현">코드 구현</h2>
<h3 id="apigateway-serviceglobalfilter">apigateway-service/GlobalFilter</h3>
<pre><code class="language-java">@Component
@Slf4j
public class GlobalFilter extends AbstractGatewayFilterFactory&lt;GlobalFilter.Config&gt; {

    public GlobalFilter() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -&gt; {
            ServerHttpRequest request = exchange.getRequest();
            String method = exchange.getRequest().getMethod().toString();
            String uri = exchange.getRequest().getURI().toString();
            ServerHttpResponse response = exchange.getResponse();

            log.info(&quot;Global Filter baseMessage: {}&quot;, config.getBaseMessage());

            if (config.isPreLogger()) {
                log.info(&quot;Global Filter Start-&gt; request id: {}, method: {}, uri: {}&quot;, request.getId(), method, uri);
            }

            return chain.filter(exchange).then(Mono.fromRunnable(() -&gt; {
                if (config.isPostLogger()) {
                    log.info(&quot;Global Filter End: response code -&gt; {}&quot;, response.getStatusCode());
                }
            }));
        };
    }

    @Data
    public static class Config {
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;
    }
}</code></pre>
<p>요청을 받으면 request에서 method와 요청을 받는 uri를, 그리고 응답에서는 response code를 받아 로그를 남긴다.</p>
<p>basemessage는 설정 파일에서 커스텀해서 사용할 수 있으며 preLogger와 postLogger를 분리해놓은 이유는 모든 서비스에서 필터가 필요없을 때 특정 필터에서 logger를 끄기 위해서 이렇게 작성하였다.</p>
<h3 id="apigateway-serviceapplicationyml">apigateway-service/application.yml</h3>
<pre><code class="language-yml">spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: user-service
              uri: http://localhost:8081
              predicates:
                - Path=/api/v1/users/**
            - id: group-service
              uri: http://localhost:8082
              predicates:
                - Path=/api/v1/groups/**
            - id: todo-service
              uri: http://localhost:8083
              predicates:
                - Path=/api/v1/todos/**
          default-filters:
            - name: AuthorizationHeaderFilter
            - name: GlobalFilter
              args:
                preLogger: true
                postLogger: true</code></pre>
<p>default-filters는 모든 route에 공통적으로 적용되게끔 만든다. 만약 특정 route에만 필터를 적용하고 싶으면</p>
<pre><code class="language-yml">          # 예시
          # 나머지 부분은 생략

          routes:
            - id: user-service
              uri: http://localhost:8081
              predicates:
                - Path=/api/v1/users/**
              filters:
                - AuthorizationHeaderFilter 
</code></pre>
<p> 이런 식으로 특정 route에만 filter를 두면 된다.</p>
<p> 위 코드에서는 preLogger와 postLogger 값을 모두 true로 설정해 GlobalFilter에서 무조건 로그가 기록되도록 하였다.</p>
<h2 id="실행결과">실행결과</h2>
<p> <img src="https://velog.velcdn.com/images/o_seongblll_/post/3d6cdf30-de29-451f-8553-e929785a22b7/image.png" alt="">
basemessage는 설정하지 않아서 null이 나오고, 나머지는 설정한대로 요청과 응답이 기록된다.</p>
<p>생각보다 로그를 많이 보게 되는데, 그 이유는 테스트 과정에서 아예 요청을 못받는 경우가 꽤나 많았다. (네트워크 이슈, 주소 오류등) 
그래서 우선 요청은 들어오는지, 확인하는 것이 많은 도움이 됐다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CLOUD NATIVE] 클라우드 네이티브한 서비스 만들기[3] - 인증과 인가  ]]></title>
            <link>https://velog.io/@o_seongblll_/CLOUD-NATIVE-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B03-%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@o_seongblll_/CLOUD-NATIVE-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B03-%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Wed, 12 Nov 2025 08:18:11 GMT</pubDate>
            <description><![CDATA[<h2 id="spring-security">Spring Security?</h2>
<p>msa구조에서는 모놀리식과 다르게 Spring Security를 사용하기 어렵다. 
우선 서비스 별로 세션을 공유할 수 없기에 세션 방식은 구현이 불가능하다.
그래서 필연적으로 토큰 방식을 사용해야하는데 Security의 필터체인을 사용하려면 서비스별로 Security를 구현해야한다.
그래서 중복적인 코드를 피하고 싶고, 인증 관련 로직은 apigateway에만 두고 싶다는 이유로 Security에 의존하지 않고 인증을 구현하였다. </p>
<h2 id="authentication">Authentication</h2>
<p>위에서 말한 것처럼 SpringSecurity없이 소셜로그인(구글)+JWT 구조로 인증을 진행할 것이다.
해당 글에서는 Security나 jwt 관련 글보다는, 클라우드 네이티브여서 모놀리식 방식과 다른 점 위주로 작성해볼 것이다.</p>
<p>또 프론트엔드에게 소셜로그인 서버에서 &#39;code&#39;를 받아 백엔드에게 넘겨주는 방식을 가지고 로그인을 구현할 것이다.</p>
<p>나는 AuthService와 Auth관련 엔드포인트를 마이크로서비스 중 user-service에 넣어서 구현하였는데 아예 따로 auth-service를 만들어서 관리해도 될 것 같다. 
<del>사견이지만 user를 생성하고 관리하는 거니까 그냥 user-service에다가 하는게 좋다.
(auth-service를 따로 만들면 유저 생성시 user-service에 요청해야 함, db 공유 안 됨, 컨테이너 추가로 관리해야함)</del></p>
<p>토큰을 만드는 곳은 auth-service고 토큰을 검증하는 곳은 apigateway-service이기에 JwtTokenProvider를 중복으로 사용해야하긴 한다. 
토큰을 생성하고, 검증한다는 차이가 있기에 이름을 달리해서 사용해도 좋을 것 같지만 둘 다 많이 쓰이는 JwtTokenProvider로 이름을 붙였다.</p>
<h2 id="코드-구현">코드 구현</h2>
<p>모든 요청은 apigateway를 거쳐서 오기 때문에 apigateway-service의 yml파일에 경로 WhiteList를 만들어서 특정 경로가 WhiteList에 속한다면 JWT 검증없이 내부 서비스에 들어갈 수 있게 구현하였다 (회원가입/로그인시 accessToken이 없기 때문에).</p>
<p>로그인 흐름은 다음 블로그를 참고해서 구현했다. <a href="https://velog.io/@hnnynh/OAuth-%ED%94%84%EB%A1%A0%ED%8A%B8-%EB%B0%B1-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%98%91%EC%97%85#1%EF%B8%8F%E2%83%A3-%EC%9D%B8%EA%B0%80-%EC%BD%94%EB%93%9C-fe--%EC%95%A1%EC%84%B8%EC%8A%A4-%ED%86%A0%ED%81%B0-be">[OAuth] 프론트/백의 소셜로그인 협업</a></p>
<h3 id="apigateway-serviceapplicationyml">apigateway-service/application.yml</h3>
<pre><code class="language-yaml">spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: user-service
              uri: http://localhost:8081
              predicates:
                - Path=/api/v1/users/**
            - id: group-service
              uri: http://localhost:8082
              predicates:
                - Path=/api/v1/groups/**
            - id: todo-service
              uri: http://localhost:8083
              predicates:
                - Path=/api/v1/todos/**
           default-filters:
             - name: AuthorizationHeaderFilter  

# 경로 화이트리스트
gateway:
  jwt:
    whitelist:
      - /api/v1/users/auth/**
      - /**/v3/api-docs
      - /swagger-ui/**
      - /swagger-ui.html

app:
  jwt:
    secret: {JWT_SECRET_KEY}</code></pre>
<p>  AuthorizationHeaderFilter는 요청마다 jwt 토큰을 검증하기 위한 필터이다.<del>아래에 구현할 것이다.</del></p>
<p>  경로 화이트리스트를 만들어 해당 경로에 오는 요청들은 필터를 거치지 않고 넘길 것이다.
  스웨거 관련 엔드포인트도 일단 열어두어 프론트가 활용하기 쉽게 구현하였다.</p>
<p>  jwt-secret key는 user-service/application.yml에 들어가는 값과 같아야 한다.</p>
<h3 id="apigateway-serviceauthorizationheaderfilterjava">apigateway-service/AuthorizationHeaderFilter.java</h3>
<pre><code class="language-java">/*
    요청마다 jwt accesstoken을 검증하기 위한 필터
 */
@Slf4j
@Component
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory&lt;AuthorizationHeaderFilter.Config&gt; {

    private final JwtTokenProvider tokenProvider;
    private final JwtProperties jwtProperties;
    private final AntPathMatcher pathMatcher = new AntPathMatcher();

    public AuthorizationHeaderFilter(JwtTokenProvider tokenProvider, JwtProperties jwtProperties) {
        super(Config.class);
        this.tokenProvider = tokenProvider;
        this.jwtProperties = jwtProperties;
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -&gt; {
            String path = exchange.getRequest().getURI().getPath();

            // 화이트리스트 체크
            if (jwtProperties.getWhitelist().stream().anyMatch(pattern -&gt; pathMatcher.match(pattern, path))) {
                return chain.filter(exchange);
            }

            HttpHeaders headers = exchange.getRequest().getHeaders();
            String authorizationHeader = headers.getFirst(HttpHeaders.AUTHORIZATION);

            if (!authorizationHeader.startsWith(&quot;Bearer &quot;)) {
                return onError(exchange, &quot;Invalid authorization header&quot;);
            }

            String token = authorizationHeader.substring(7); // &quot;Bearer &quot; 제거
            if (!tokenProvider.validToken(token)) {
                return onError(exchange, &quot;Invalid access token&quot;);
            }

            // userId를 헤더에 추가
            String subject = tokenProvider.getSubject(token);
            ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
                    .header(GateWayConstant.GATEWAY_AUTH_HEADER, subject)
                    .build();

            log.info(&quot;AuthorizationHeaderFilter: userId= {}&quot;, mutatedRequest.getHeaders().get(GateWayConstant.GATEWAY_AUTH_HEADER));

            return chain.filter(exchange.mutate().request(mutatedRequest).build());
        };
    }

    private Mono&lt;Void&gt; onError(org.springframework.web.server.ServerWebExchange exchange, String errorMsg) {
        log.error(errorMsg);

        ServerHttpResponse response = exchange.getResponse();

        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        String body = String.format(&quot;{\&quot;code\&quot;: %d, \&quot;message\&quot;: \&quot;%s\&quot;}&quot;,
                HttpStatus.UNAUTHORIZED.value(), &quot;인증되지 않은 유저입니다.&quot;);
        DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));

        return response.writeWith(Mono.just(buffer));
    }

    public static class Config {}
}</code></pre>
<p>이 포스트에서 가장 중요한 부분이라고 단언할 수 있을 코드다.
전제 조건은 apigateway-service에는 JWT 토큰 검증을 하는 tokenprovider 가 있고, user-service에서는 JWT 토큰 생성을 하는 tokenprovider가 있는 것이다.
<del>security에 관련된 코드는 너무 길어질 것 같아 제외하겠다.</del></p>
<p><a href="https://velog.io/@mrcocoball2/Spring-Cloud-Spring-Cloud-Gateway-%EC%BB%A4%EC%8A%A4%ED%85%80-%ED%95%84%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0">Spring Cloud Gateway - 커스텀 필터 만들기</a> 이 블로그를 참고해서 커스텀했다.</p>
<p>우선 프론트는 백엔드에게 비동기로 요청을 보내기 때문에 webflux-mono를 사용하였다.</p>
<p>동작과정은 경로를 받아서 화이트리스트에 존재한다면 바로 다음 chain으로 넘겨주고 (토큰 검증없이),
아니라면 헤더에서 accessToken을 검증해 토큰 값이 올바르다면 userId를 추출해 GateWayConstant.GATEWAY_AUTH_HEADER를 키 값으로 헤더에 userId를 실어준다.
이후 다음 filter로 넘겨준다.</p>
<h3 id="apigateway-servicejwtpropertiesjava">apigateway-service/JwtProperties.java</h3>
<pre><code class="language-java">/*
    .yml 파일에서 whitelist 설정값을 가져오기 위한 클래스
    whitelist : JWT 검증을 우회할 경로들
 */
@Setter
@Getter
@Component
@ConfigurationProperties(prefix = &quot;gateway.jwt&quot;)
public class JwtProperties {

    private List&lt;String&gt; whitelist = new ArrayList&lt;&gt;();

}</code></pre>
<p>whitelist를 application.yml에서 가져와서 AuthorizationHeaderFilter에서 사용할 수 있게 해주는 코드다.</p>
<h3 id="cors-설정">CORS 설정</h3>
<pre><code class="language-java">@Configuration
public class CorsConfig {

    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);           // 쿠키 허용
        config.setAllowedOrigins(Arrays.asList({허용할 도메인 입력]));       // 도메인 설정
        config.addAllowedHeader(&quot;*&quot;);               // 모든 헤더 허용
        config.addAllowedMethod(&quot;*&quot;);               // 모든 HTTP 메서드 허용

        config.addExposedHeader(&quot;Authorization&quot;); // 브라우저가 응답에서 Authorization 값을 읽을 수 있도록 설정

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration(&quot;/**&quot;, config); // 모든 경로에 적용

        return new CorsWebFilter(source);
    }
}</code></pre>
<p>인증/인가에 직접적으로 관계는 없지만, 테스트시 cors 설정이 없으면 제대로 동작을 안할 가능성이 높기 때문에 추가해주었다. <del>어차피 나중에 무조건 추가해줘야 한다.</del></p>
<h3 id="webfluxsecurityconfigjava---실패">WebFluxSecurityConfig.java - 실패</h3>
<pre><code class="language-java">@Configuration
@RequiredArgsConstructor
@EnableWebFluxSecurity
public class WebFluxSecurityConfig {

    @Bean
    public SecurityWebFilterChain configure(ServerHttpSecurity http) {
        return http
                .csrf(ServerHttpSecurity.CsrfSpec::disable) // csrf 비활성화
                .cors(corsConfigurer -&gt; corsConfigurer.configurationSource(corsConfigurationSource())) // cors 설정
                .formLogin(ServerHttpSecurity.FormLoginSpec::disable) // formLogin 비활성화
                .httpBasic(httpBasic -&gt; httpBasic.authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)))// login dialog disabled &amp; 401 HttpStatus return
                .securityContextRepository(NoOpServerSecurityContextRepository.getInstance()) // jwt 기반이므로 securityContext에 저장 x
                .authorizeExchange(auth -&gt; auth
                        .pathMatchers(&quot;/api/v1/users/auth/**&quot;).permitAll() // login 엔드포인트
                        .anyExchange().authenticated()
                )
                .build();
    }
</code></pre>
<p>처음에는 이렇게 WebFluxSecurityConfig를 만들어서 사용하려고 했다. 
하지만 계속 요청시 팝업 로그인이 뜨는 문제가 발생했다. <img src="https://velog.velcdn.com/images/o_seongblll_/post/8d8f6c96-4629-4745-89f2-d9c20db80634/image.PNG" alt=""></p>
<p>분명히 .httpBasic을 disable했지만 계속해서 문제가 생겼다. 
추측으로는 apigateway-service이후에 내부 서비스에 가서 기본 인증을 또 시도해서 팝업창이 계속 뜬 것 같다.
apigateway-service이외에는 토큰 검증로직을 넣고 싶지 않았기에 spring security를 사용하는 것을 포기하고 내가 직접 필터를 만들게 된 이유 중 하나다.</p>
<h3 id="etc">etc</h3>
<p>코드들을 작성하진 않겠지만 내가 작성한 코드들은 다음과 같다.</p>
<ul>
<li>AuthService - 로그인 서비스 로직를 수행</li>
<li>AuthController - login 엔드포인트를 받아옴</li>
<li>JwtTokenProvider - user-service와 apigateway-service 각각 구현</li>
<li>GoogleLoginService - code를 가지고 user 정보를 가져오는 서비스 </li>
<li>GoogleTokenClient - 프론트한테 인가 code를 받아서 구글 서버 accesstoken을 받아오는 서비스</li>
<li>LoginApiClient - oauth 서버에서 accessToken을 가지고 실제 user 정보를 가져오는 서비스</li>
<li>기타 도메인, dto등등</li>
</ul>
<h2 id="테스트-방법-백엔드">테스트 방법 (백엔드)</h2>
<ol>
<li><a href="https://accounts.google.com/o/oauth2/v2/auth?client_id=%7Bclient_id%7D&amp;redirect_uri=%7Bredirect_uri%7D">https://accounts.google.com/o/oauth2/v2/auth?client_id={client_id}&amp;redirect_uri={redirect_uri}</a> 들어가서 로그인 진행</li>
<li>이후 뜨는 브라우저에서 (ex. <a href="http://localhost:8080/login/oauth2/code/google?code=4%2F0AVGzR1C">http://localhost:8080/login/oauth2/code/google?code=4%2F0AVGzR1C</a> ... ) code 값을 가져옴</li>
<li>포스트맨을 활용해 method: POST, url: localhost:8080/{로그인 백엔드 엔드포인트}?code={code} 로 전송</li>
</ol>
<p><del>당연히 소셜 서버에 redirect_uri 같은 설정들을 잘 해주어야 한다.</del></p>
<h2 id="정리">정리</h2>
<ul>
<li>msa 구조에선 토큰 방식으로 로그인을 진행한다.</li>
<li>apigateway에서만 토큰 검증을 진행하고 싶어서 spring security를 사용하지 않고 커스텀 필터(AuthorizationHeaderFilter)를 사용하였다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 11286 자바]]></title>
            <link>https://velog.io/@o_seongblll_/%EB%B0%B1%EC%A4%80-11286-%EC%9E%90%EB%B0%94</link>
            <guid>https://velog.io/@o_seongblll_/%EB%B0%B1%EC%A4%80-11286-%EC%9E%90%EB%B0%94</guid>
            <pubDate>Tue, 11 Nov 2025 07:10:26 GMT</pubDate>
            <description><![CDATA[<p>문제를 풀고 다른 사람들의 코드로 더 적합한 방법이 있나 찾아보았는데, 다들 compare을 오버라이딩해서 문제를 풀었다는 사실을 알 수 있었다.
틀린 방법은 아니지만 나는 직관적으로 이해하기 어려웠고, 내가 푼 방법이 적혀있지 않은 것 같아서 내 코드를 공유해보고자 한다.</p>
<pre><code class="language-java">import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;

// 11286
public class Main {

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        int n = Integer.parseInt(br.readLine());
        List&lt;Integer&gt; arr = new ArrayList&lt;&gt;();

        PriorityQueue&lt;Integer&gt; pq = new PriorityQueue&lt;&gt;();

        while(n--&gt;0){
            int x = Integer.parseInt(br.readLine());
            int abs = Math.abs(x);

            if(x==0){ // remove
                if(pq.isEmpty()) bw.write(0 + &quot;\n&quot;);
                else {
                    int out = pq.poll();
                    if(arr.contains(out)){
                        bw.write(&quot;-&quot; + out + &quot;\n&quot;);
                        arr.remove(Integer.valueOf(out));
                    }else{
                        bw.write(out + &quot;\n&quot;);
                    }
                }
            }else{ // insert
                pq.add(abs);
                if(x&lt;0){
                   arr.add(abs);
                }
            }
        }

        bw.flush();
        bw.close();
        br.close();
    }
}
</code></pre>
<p>특별한 점은 추가적인 list가 존재한다는 것이다.
이 list는 음의 정수를 기록하기 위해 만들었다.</p>
<p>우선 priority queue의 비교값은 입력값의 절댓값이기에 Math.abs()를 사용해서 pq를 채워준다. </p>
<p>문제 조건에서 
&quot;절댓값이 가장 작은 값이 여러개일 때는, 가장 작은 수를 출력하고, 그 값을 배열에서 제거한다.&quot;
즉 만약 -1과 1이 있다면 -1을 우선순위로 두어야한다는 뜻이다.</p>
<p>그래서 pq에 abs된 절댓값을 넣고, x가 음수라면 list에도 추가로 절댓값을 넣는다.</p>
<p>출력시(즉 x==0일 때) 추가적인 if문으로 리스트에 pq에서 remove되는 요소가 list에 있다면 그 값은 음수로 출력하도록 코드를 작성하였다.</p>
<p>중복 출력을 방지하기 위해 리스트에서 사용하면 그 값을 제거해주는 로직도 있어야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CLOUD NATIVE] 클라우드 네이티브한 서비스 만들기[2] - common 모듈]]></title>
            <link>https://velog.io/@o_seongblll_/CLOUD-NATIVE-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B02-common-%EB%AA%A8%EB%93%88</link>
            <guid>https://velog.io/@o_seongblll_/CLOUD-NATIVE-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B02-common-%EB%AA%A8%EB%93%88</guid>
            <pubDate>Tue, 04 Nov 2025 08:32:40 GMT</pubDate>
            <description><![CDATA[<h2 id="common-모듈---사용-이유">common 모듈 - 사용 이유</h2>
<p>모놀리식 구조에서는 서비스들이 들어가는 도메인말고 common 폴더를 만들어 공통적으로 필요한 코드들을 구현하였다. </p>
<p>예를들어 gateway에서 JWT토큰 검증후 내부 서비스로 userId를 보낼 때 사용할 헤더 키(상수), 전반적으로 터지는 예외를 처리해주는 GlobalExceptionHandler 등이 그 예이다.</p>
<p>MSA 구조에서는 각 서비스마다 공통된 코드를 구현해야하나? 의문이 들었다.
각각 구현하게 되면 공통된 코드라고 할 수 있나 싶고, 같은 코드를 여러개 구현하는 것은 좋지 않은 방법이라고 생각했다.</p>
<p>그래서 common 모듈을 라이브러리로 만들어 다른 서비스에서 의존성에 추가해서 사용하면 될 것 같다는 생각이 들었다.</p>
<h2 id="common-에-구현한-것">common 에 구현한 것</h2>
<p>공통된 로직들을 여러 서비스에서 사용하려고 common을 만든 만큼, 비즈니스 로직이나 특정 서비스에서만 사용되는 로직을 넣지 않으려고 하였다.</p>
<p>우선 내가 구현한 것은 다음과 같다.</p>
<ul>
<li>ErrorCode 및 GlobalExceptionHandler : 전역 예외 처리 </li>
<li>ApiResponse : 응답 포맷을 통일 </li>
<li>BaseEntity : 엔티티에 공통된 필드 도입 (createdAt, updatedAt)</li>
<li>Constant : 공통 상수 (GATEWAY_AUTH_HEADER)</li>
</ul>
<p>공통적으로 사용되는 유틸리티 함수나 dto가 있다면 넣어도 좋을 것 같다.</p>
<h2 id="코드-구현">코드 구현</h2>
<p>라이브러리로 만들어서 각 서비스에서 가져다 쓰는 형식이기에 실행 파일(main())도 없고 따로 서버를 띄우지도 않는다.</p>
<p>즉 순수한 자바로 이루어져 있는 파일이어야 하는데, 나는 어떻게 만드는지 몰라서 스프링부트 이니셜라이저를 활용해서 틀을 만들고 필요없는 파일, 코드들을 지우는 식으로 만들었다.</p>
<p>BaseEntity, ApiResponse, GlobalExceptionHandler는 일반 모놀리식 방식과 만드는 방법에 차이가 없어 다음 설명에서는 제외하겠다. </p>
<h3 id="1-commonbuildgradle">1. common/build.gradle</h3>
<pre><code class="language-java">bootJar {
    enabled(false)
}
jar {
    enabled(true)
}

repositories {
    mavenCentral()
}

dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-web:3.2.0&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa:3.2.0&#39;
    compileOnly &#39;org.projectlombok:lombok:1.18.28&#39;
    annotationProcessor &#39;org.projectlombok:lombok:1.18.28&#39;

    testImplementation &#39;org.springframework.boot:spring-boot-starter-test&#39;
    testRuntimeOnly &#39;org.junit.platform:junit-platform-launcher&#39;
}
</code></pre>
<p>bootjar은 실행가능한 jar 파일로 만들기 때문에 이를 끄고, 
일반 jar파일만 생성하도록 하였다. </p>
<h3 id="2-gatewayconstant">2. GateWayConstant</h3>
<pre><code class="language-java">public class GateWayConstant {
    public static final String GATEWAY_AUTH_HEADER = &quot;X-GATEWAY-AUTH-HEADER&quot;;
}</code></pre>
<p>내가 구현한 msa구조에서는 apigateway에서만 JWT 검증을 하고 이후 내부 서비스에서는 검증을 하지 않는다. 
즉 내부 서비스는 토큰 값을 알지 못하고 오로지 userId를 가지고 서비스 로직을 수행하도록 설계하였다.</p>
<p>이렇게 만든 이유는 &#39;모든 요청은 gateway를 거치는데, 각 서비스에서 또 jwt 검증을 할 필요가 있을까?&#39; 였다.
물론 2차 검증을 하면 보다 안전하긴 하겠지만, 그만큼 시간도 느려지고 같은 동작을 두 번 반복한다고 생각했다. <del>apigateway에서 오는 내부 요청을 믿지 못하는 상황이라면 검증이 필요할 거 같기도하다.</del></p>
<p>그래서 gateway는 토큰을 검증후 일방적으로 userId를 헤더에 실어서 내부 서비스로 요청을 보내는데 이때 사용할 헤더의 키를 상수로 구현하였다.</p>
<p>이후 내부 서비스의 로직을 구현할 때 컨트롤러 단에서 </p>
<pre><code class="language-java">@PostMapping
public ResponseEntity&lt;GroupResponseDto&gt; createGroup(
        @ModelAttribute GroupCreateRequestDto requestDto,
        @RequestHeader(GATEWAY_AUTH_HEADER) Long userId
){</code></pre>
<p>이런 식으로 헤더 값을 받아 사용하게끔 하였다.</p>
<h2 id="빌드-방법">빌드 방법</h2>
<p>common 라이브러리를 사용하기 위해서는 각 서비스가 빌드된 라이브러리를 참조하거나, 
라이브러리를 패키지로 배포하여 사용하는 방법이 있다고 한다. </p>
<p><a href="https://github.com/eGovFramework/egovframe-msa-edu/tree/main/backend/module-common/build/libs">eGovFramework</a> 
전반적으로 프로젝트를 진행하면서 많이 참고한 코드인데, 이 프로젝트에서 전자의 방법을 사용했기에 나도 그렇게 진행하였다.</p>
<p>우선 common 모듈을 빌드해준다.</p>
<pre><code class="language-bash">./gradlew clean build</code></pre>
<p>이후 common/build/libs/common-0.0.1-SNAPSHOT-plain.jar 라는 실행파일이 생성된다.</p>
<p>이제 각 내부 서비스에 위 실행파일을 참조하도록 의존성에 넣어주어야 한다.</p>
<pre><code class="language-java">// user-service/build.gradle

dependencies {
    implementation files(&#39;../common/build/libs/common-0.0.1-SNAPSHOT-plain.jar&#39;)       
}</code></pre>
<p>각 서비스를 완전 독립적인 레포로 관리한다면 라이브러리를 배포해야 할 것 같다. 
우리 팀은 코드 자체는 단일 레포지토리로 관리하기에 위와 같은 방식을 사용했다.</p>
<h2 id="common-코드-수정시">common 코드 수정시</h2>
<p>각 서비스가 이미 만들어진 jar파일을 의존하기에 common 모듈의 코드를 수정하면 자동으로 불러오지 못한다. 
그래서 코드 수정시 매번 빌드를 해줘야 한다. 
만약 빌드 결과물의 이름이 달라지면 그것도 갱신해줘야 한다.</p>
<h2 id="baseentity--globalexceptionhandler-적용하기">BaseEntity / GlobalExceptionHandler 적용하기</h2>
<pre><code class="language-java">@SpringBootApplication
@EnableJpaAuditing
@Import(GlobalExceptionHandler.class)
public class UserServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }

}</code></pre>
<p>만약 common 에서 BaseEntity와 GlobalExceptionHandler을 구현했다면 각 서비스의 실행 클래스에 다음과 같은 어노테이션을 적용해줘야 제대로 동작한다.</p>
<p>BaseEntity -&gt; @EnableJpaAuditing
GlobalExceptionHandler -&gt; @Import(GlobalExceptionHandler.class)</p>
<h2 id="정리">정리</h2>
<ul>
<li>각 서비스에서 사용되는 공통된 코드를 common 모듈을 라이브러리화에서 관리할 수 있다.</li>
<li>각 서비스에서 jar 파일을 의존하기에 생성/수정시 common 모듈을 빌드해줘야 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CLOUD NATIVE] 클라우드 네이티브한 서비스 만들기[1] - Gateway 와 Eureka server]]></title>
            <link>https://velog.io/@o_seongblll_/CLOUD-NATIVE-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B01-Gateway-%EC%99%80-Eureka-server-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4</link>
            <guid>https://velog.io/@o_seongblll_/CLOUD-NATIVE-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B01-Gateway-%EC%99%80-Eureka-server-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4</guid>
            <pubDate>Sun, 21 Sep 2025 12:19:26 GMT</pubDate>
            <description><![CDATA[<p>이전 게시물에서 간단하게 msa의 특징을 적어보았다. 
msa에서는 각 서비스가 독립적으로 존재하기에, 그 서비스들을 연결해주는 서비스와 프론트가 요청을 보낼 서버가 각각 존재한다. 이것들을 구현하는 작업을 기록해보려고 한다.</p>
<h2 id="gateway">Gateway</h2>
<h3 id="gateway-사용-이유">gateway 사용 이유</h3>
<p>마이크로서비스들은 독립적으로 여러개의 서비스로 나뉜다. 
다르게 설명하면 완전한 서비스를 이용하기 위해서는 스프링 애플리케이션을 N개 띄워야 한다는 것이다. </p>
<p>그러면 클라이언트는 각 서비스를 이용하기 위해서 각자의 주소로 요청을 보내야하나?
그렇게 되면 너무 복잡해지고 각 서비스의 주소가 변경될때마다 계속 코드가 수정되어야 하는 문제가 생긴다. 그래서 클라우드 네이티브한 방식에서는 Gateway 를 사용한다.</p>
<h3 id="gateway-란">gateway 란?</h3>
<p>msa 구조에서 클라이언트와 각 서비스를 잇는 단일 진입점이다.
클라이언트는 마이크로서비스의 주소들을 알고, 그곳에다 요청을 보낼 필요 없이 gateway 주소에 보내면 gateway가 알아서 요청에 알맞는 서비스에 라우팅해준다. 
또 각 서비스의 부하를 분산시켜준다.
<img src="https://velog.velcdn.com/images/o_seongblll_/post/0705275f-362a-406d-bee3-937e42574512/image.png" alt="">
<sub> 사진출처: <a href="https://velog.io/@tedigom/MSA-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-3API-Gateway-nvk2kf0zbj">https://velog.io/@tedigom/MSA-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-3API-Gateway-nvk2kf0zbj</a> </sub>
<sub> (IP주소는 예시일 뿐이다) </sub></p>
<p>이렇게 하면 모든 요청은 gateway를 거치기 때문에 공통된 로직(인증/인가 등)을 gateway에서 처리할 수도 있다. 또 내부 마이크로서비스의 주소를 클라이언트한테 알릴 필요없이 동작할 수 있다.</p>
<h2 id="eureka-server">Eureka Server</h2>
<h3 id="eureka-server-사용-이유">Eureka server 사용 이유</h3>
<p>각 서비스는 각각의 포트번호를 가진다 (혹은 각각의 ip주소). 
클라우드 네이티브의 성격 중에 트래픽에 따라 서비스 컨테이너를 동적으로 배치하는 동적 오케스트레이션이 있었다. 
이 특징 때문에 각 서비스는 고정된 주소를 가지지 않고 유동적으로 변하는 주소를 갖게 된다. 클라이언트는 각 서비스의 주소를 몰라도 되지만, 동적으로 변하면 gateway도 알기 어려워진다. 
그래서 각 서비스의 주소를 기록하기 위해 eureka server가 사용된다. </p>
<p><img src="https://velog.velcdn.com/images/o_seongblll_/post/d605a983-58fe-452d-aa18-1d3ff54edf97/image.png" alt=""><sub>사진출처: <a href="https://ksh-coding.tistory.com/137">https://ksh-coding.tistory.com/137</a></sub></p>
<h3 id="eureka-server-란">Eureka server 란?</h3>
<p>스프링 공식 사이트에서는 Service Registration and Discovery 라는 말을 사용한다.
<sub><a href="https://spring.io/guides/gs/service-registration-and-discovery/">https://spring.io/guides/gs/service-registration-and-discovery/</a></sub>
말 그대로 서비스를 등록하고 발견하는 것이다. 서비스가 동적으로 생성/소멸 되어도 서비스의 위치를 자동으로 관리하고 알려주는 역할을 하는 것이다.</p>
<p>Eureka는 크게 두 가지로 나뉜다.</p>
<ol>
<li>Eureka server - 서비스 주소록 </li>
<li>Eureka client - 등록되는 각 서비스</li>
</ol>
<p>마이크로서비스는 실행시 Eureka server에 자신의 정보를 등록하고 gateway는 등록된 정보를 받아와서 라우팅등의 역할을 수행한다.</p>
<p>Service-discovery는 server-side, client-side 두 가지 방식으로 나뉘는데 eureka는 client-side에 해당한다. 조금 더 구현이 쉽고 애플리케이션 단에서 처리할 수 있기 때문에 client-side, 즉 eureka를 사용해볼 것이다.</p>
<h2 id="코드-구현">코드 구현</h2>
<p>코드를 구현하기 앞서 전제 조건은 모놀리식이 아니고 기능별로 독립적인 마이크로서비스로 구현되어 있어야 한다는 것이다. <img src="https://velog.velcdn.com/images/o_seongblll_/post/73dca90c-764a-42d4-b112-7e07be0719d5/image.png" alt="">
예를 들어 user-service에서는 유저 관련 서비스, group-service에서는 그룹 관련 로직이 들어간 서비스이다. 
user-service에는 user 관련 DB가 있고, group-service에는 group 관련 DB가 따로 존재한다.</p>
<p>그러면 &#39;그룹 안에 user 가 있어야하지 않나?&#39; 라는 의문점이 들 수 있다. 
서비스는 각자 DB를 가지고 필요한 정보는 서비스끼리 통신을 하여 서로의 정보를 주고받는다. 
DB 내부에서는 FK가 아니고 단순히 id를 String으로 저장하는 식으로 저장된다.</p>
<p>각 서비스는 평소대로 의존성을 넣어서 만들어주면 되고 apigateway와 eureka는 몇 개의 의존성을 특별히 추가하면 된다.</p>
<p><del>아래 적힐 코드들은 스프링이나 db 관련 코드들을 제외하고 eureka나 gateway 관련 코드들에 초점을 두고 기록하였다</del></p>
<h3 id="1-apigateway-servicebuildgradle">1. apigateway-service/build.gradle</h3>
<pre><code class="language-java">ext {
    set(&#39;springCloudVersion&#39;, &quot;2025.0.0&quot;)
}

dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-actuator&#39; // 선택: 애플리케이션 상태 모니터링/관리
    implementation &#39;org.springframework.boot:spring-boot-starter-webflux&#39; // 필수: WebFlux 기반 (비동기) Gateway
    implementation &#39;org.springframework.cloud:spring-cloud-starter-gateway-server-webflux&#39; // 필수: Gateway 핵심 기능
    implementation &#39;org.springframework.cloud:spring-cloud-starter-netflix-eureka-client&#39; // 필수: Eureka 사용하기 위해서는 필수 
    // lombok이나 jwt 등 의존성을 필요시 추가
}

dependencyManagement {
    imports {
        mavenBom &quot;org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}&quot;
    }
}</code></pre>
<h3 id="2-eureka-servicebuildgradle">2. eureka-service/build.gradle</h3>
<pre><code class="language-java">ext {
    set(&#39;springCloudVersion&#39;, &quot;2025.0.0&quot;)
}

dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-actuator&#39; // 선택
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39; // 필수: eureka server는 http 요청을 받아야 함
    implementation &#39;org.springframework.cloud:spring-cloud-starter-netflix-eureka-server&#39; // 필수: eureka server 기능 제공
    // 필요시 의존성 추가
}

dependencyManagement {
    imports {
        mavenBom &quot;org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}&quot;
    }
}</code></pre>
<h3 id="3-eureka-serviceeurekaserviceapplication">3. eureka-service/../EurekaServiceApplication</h3>
<p>그리고 eureka-service의 애플리케이션 실행 파일에 다음과 같이 어노테이션을 붙여준다.</p>
<pre><code class="language-java">@SpringBootApplication
@EnableEurekaServer // eureka server 기능 활성화
public class EurekaServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServiceApplication.class, args);
    }

}</code></pre>
<p>이 어노테이션으로 이 애플리케이션은 서비스 레지스트리 역할을 한다.</p>
<h3 id="4-eureka-serviceapplicationyml">4. eureka-service/../application.yml</h3>
<p>그리고 이제 거의 모든것이 설정파일에서 설정된다.</p>
<pre><code class="language-java">server:
  port: 8761 // eureka 서비스 구동 포트 지정

spring:
  application:
    name: eureka-server 

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false</code></pre>
<p>꼭 포트번호가 8761일 필요는 없다! <del>관습적으로 사용되는 포트이긴하다</del>
애플리케이션 이름도 그냥 명시적으로 적어주자 <del>서비스가 많기 때문에</del></p>
<p>eureka-server는 서버로 동작할 거기 때문에 다른 eureka 서비스에 등록되면 안되니 false,
다른 eureka 서버에서 정보를 가져올 필요도 없기 때문에 false로 설정해주면 된다.</p>
<p>이 상태로 eureka 서비스를 실행한 다음 <a href="http://localhost:8761">http://localhost:8761</a> 에 들어가면 다음과 같이 eureka page를 볼 수 있다.
<img src="https://velog.velcdn.com/images/o_seongblll_/post/b9775829-07a5-4dae-823a-52b619d4e48c/image.png" alt="">
아직 다른 서비스를 등록하지 않았기 때문에 위와 같이 나타난다.</p>
<h3 id="5-apigateway-serviceapplicationyml">5. apigateway-service/application.yml</h3>
<p>운영환경에서는 동적으로 구성되기에 다음과 같이 포트번호를 지정하면 안된다. 
랜덤 포트번호를 사용하기 위해서는 0으로 적으면 되고, 추가로 설정이 필요하다. 
우선은 개발의 편의성을 위해서 포트번호를 고정하고 개발을 진행하였다.</p>
<pre><code class="language-java">server:
  port: 8080

spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: user-service
              uri: http://localhost:8081 // 정적 주소는 개발시 편의를 위해 사용됨 // 운영시 변경 필요
              predicates:
                - Path=/api/v1/users/**
            - id: group-service
              uri: http://localhost:8082
              predicates:
              - Path=/api/v1/groups/**

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/</code></pre>
<p>gateway의 서버 포트번호는 8080으로 설정하였다. 
클라이언트는 다른 포트번호(주소)를 알 필요없이 8080만 알면 된다.</p>
<p>마찬가지로 명시적으로 apigateway-service라고 이름을 작성해준다. (이 이름으로 eureka에 등록된다)</p>
<p>routes에 설정된 것들을 해석해보면</p>
<ul>
<li>user-service : 라우트 식별자</li>
<li>uri: 실제 서비스가 동작하는 주소</li>
<li>predicates: path의 요청 경로를 보고 이 서비스로 보낸다.</li>
</ul>
<p>이렇게 .yml 파일을 지정해두고 각 서비스의 설정을 마치고 동작시켰을 때,
&#39;localhost:8080/api/v1/users/hello&#39; 에 클라이언트가 요청을 보내면 gateway는 &#39;/api/v1/users&#39;를 읽고 &#39;localhost:8081/api/v1/users/hello&#39;로 라우팅해준다.</p>
<p>eureka 서버 설정은 gateway를 eureka 서버에 등록해주는 것이다.</p>
<h3 id="6-user-serviceapplicationyml">6. user-service/../application.yml</h3>
<pre><code class="language-java">server:
  port: 8081

spring:
  application:
    name: user-service

 eureka:
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://localhost:8761/eureka/</code></pre>
<p> user-service의 .yml파일로 예시를 들어보면 위와같이 설정하면 된다.
 포트번호를 apigateway-service의 .yml 파일에 적어둔 것과 맞춰주고, applcation 이름도 헷갈리지 않게 똑같이 적어준다.
 이 서비스는 eureka-server에 등록되어야 하고 다른 서비스의 주소를 가져와야하기에 true로 설정해두고, 가져올 eureka-server의 주소를 적어주면 된다.</p>
<h2 id="테스트">테스트</h2>
<p>테스트를 하기 위해서는 세 개의 애플리케이션을 실행시켜줘야 한다.
<del>docker-compose로 한 번에 실행시킬 수도 있다</del></p>
<p>직접 세 개의 애플리케이션을 실행시키려면 다음 순서로 실행시켜주면 된다.
<del>순서는 중요하지 않은 것 같기도 하다.</del>
eureka-service -&gt; apigateway-service -&gt; user-service (실험용 서비스)
<img src="https://velog.velcdn.com/images/o_seongblll_/post/b6c515d4-b9fd-4c78-8e6d-0d511e213e2d/image.png" alt="">
실행하고 나서 <a href="http://localhost:8761">http://localhost:8761</a> 에 다시 들어가보면 이렇게 마이크로서비스가 동작중인 것을 확인할 수 있다.</p>
<p>이후 user-service에 테스트 컨트롤러를 만들고 8080으로 보내보면 
<img src="https://velog.velcdn.com/images/o_seongblll_/post/aa3aca82-cd77-4a1f-8351-7a5150b26e73/image.png
" width="600"></p>
<p>8080으로 보냈지만 8081의 user-service에 접근하는 것을 볼 수 있다!</p>
<h2 id="정리">정리</h2>
<ul>
<li>msa 구조에서는 각 마이크로서비스에 접근하기 위해 gateway를 거친다.</li>
<li>msa 성질을 활용하면 동적으로 주소가 바뀔 수 있기에 eureka를 사용한다.</li>
<li>모든 요청은 gateway를 거치기에, gateway에서 인증/인가 등의 공통된 로직을 수행할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CLOUD NATIVE] 클라우드 네이티브한 서비스란]]></title>
            <link>https://velog.io/@o_seongblll_/MSA-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</link>
            <guid>https://velog.io/@o_seongblll_/MSA-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</guid>
            <pubDate>Fri, 12 Sep 2025 13:21:19 GMT</pubDate>
            <description><![CDATA[<h2 id="클라우드-네이티브">클라우드 네이티브</h2>
<p>클라우드 네이티브란 클라우드 환경에서 애플리케이션을 구축, 배포 및 관리하는 것을 뜻한다. 
예전에는 서버를 직접 사서 관리했지만, 오늘날에는 AWS같은 클라우드를 활용한다. 즉 모놀리식(기존 방식)보다 클라우드 친화적인 방법이 클라우드를 사용하는데 더 유리하다.</p>
<h3 id="모놀리식-vs-클라우드-네이티브">모놀리식 vs 클라우드 네이티브</h3>
<p><img src="https://velog.velcdn.com/images/o_seongblll_/post/858daa8a-f648-457b-bd5b-7b8eb3cc765e/image.png" alt="">
<sub>출처: <a href="https://www.redhat.com/ko/topics/microservices/what-are-microservices">https://www.redhat.com/ko/topics/microservices/what-are-microservices</a></sub></p>
<h4 id="모놀리식-기존-방식">모놀리식 (기존 방식)</h4>
<ol>
<li>하나의 큰 애플리케이션에 모든 기능 포함</li>
<li>전체 애플리케이션 배포</li>
<li>한 기능의 장애가 전체 기능에 영향을 미침</li>
</ol>
<h4 id="클라우드-네이티브-1">클라우드 네이티브</h4>
<ol>
<li>여러 개의 작은 독립 서비스로 구성(마이크로서비스) </li>
<li>서비스 단위로 배포</li>
<li>한 기능의 장애가 그 서비스에만 영향을 미침 </li>
</ol>
<p>클라우드 네이티브가 유리해보이지만 서비스 수가 많아지는 만큼 운영이 어려워지고, 데이터 관리가 어렵다는 단점 등이 있다. </p>
<p>하지만 컨테이너화, 탄력성, CI/CD 등에서 확실한 이점을 가지게 된다. 
클라우드 네이티브의 추가적인 특징은 다음과 같다.</p>
<ol>
<li>컨테이너화: 서비스를 컨테이너로 만들어 어디서든 실행 가능하도록 한다.</li>
<li>동적 오케스트레이션: 트래픽이 늘고 줄음에 따라 동적으로 컨테이너를 배치/확장/복구</li>
<li>비용절감: 필요한 자원만큼만 사용하기에 비용 최적화에 유리</li>
<li>CI/CD : 서비스 단위이기에 빠른 업데이트 가능 &amp; 자동화된 배포</li>
<li>빠른 개발: 서비스 단위이기에 빠른 개발 가능</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링부트로 실시간 채팅 구현하기[1]]]></title>
            <link>https://velog.io/@o_seongblll_/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8%EB%A1%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B01</link>
            <guid>https://velog.io/@o_seongblll_/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8%EB%A1%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B01</guid>
            <pubDate>Tue, 05 Aug 2025 00:49:12 GMT</pubDate>
            <description><![CDATA[<p>채팅 기능이 각종 앱에서 다양하게 쓰이기 때문에, 스프링 + 웹소켓을 활용하여 채팅 기능을 만들어보면 좋을 것 같아 공부중이다.</p>
<p>그 과정에서 왜 이렇게 만들었고, 어떤 개념들이 있는지 정리해보고자 한다.</p>
<h2 id="0-요구사항">0. 요구사항</h2>
<p>채팅기능을 만들면서 어떤 기능들을 넣을 것인가 생각나는대로 다 적어보았다.</p>
<ul>
<li>1:1 채팅 및 그룹 채팅</li>
<li>메세지를 디비에 저장</li>
<li>읽음/안읽음 표시</li>
<li>파일 전송 기능</li>
</ul>
<h2 id="1-websocket-개념">1. WebSocket 개념</h2>
<h3 id="1-1-websocket">1-1. WebSocket</h3>
<ul>
<li>단방향 프로토콜인 http 프로토콜을 넘어 양방향 통신을 제공하기 위한 프로토콜</li>
<li>tcp 연결을 맺은 이후 클라이언트, 서버간의 연결을 지속적으로 유지하기에 실시간 통신이 가능</li>
</ul>
<h3 id="1-2-stomp">1-2. STOMP</h3>
<ul>
<li>STOMP은 WebSocket위에서 동작하는 또 다른 프로토콜</li>
<li>메세지 송수신만 지원하는 WebSocket을 보완해 메세지 구독, 목적지로 전송등의 기능을 구성하기 위한 프로토콜 </li>
</ul>
<h2 id="2-기능-구현">2. 기능 구현</h2>
<p>채팅을 구현할 때는 당연히 유저 인증이 필요하지만, 빠르게 채팅만 공부해보고 싶어서 시큐리티는 건너뛰고 간단한 유저 도메인만 구현할 것이다. </p>
<h3 id="2-1-웹소켓-설정-파일">2-1. 웹소켓 설정 파일</h3>
<pre><code class="language-java">// WebSocketConfig.java 
@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 서버 -&gt; 클라이언트
        // 구독자가 해당 주소를 구독하고 있다면, 발행자가 해당 경로로 메세지를 보내면 메세지 브로커가 메세지를 구독자들에게 전달한다.
        config.enableSimpleBroker(&quot;/sub&quot;); // 구독 주소 prefix


        // 클라이언트 -&gt; 서버
        // 클라이언트가 해당 경로로 메세지를 보내면, 메세지는 @MessageMapping 으로 이동해 가공할 수 있게 한다.
        config.setApplicationDestinationPrefixes(&quot;/pub&quot;); // 메시지 보낼 prefix
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 웹소켓 연결을 위한 엔드포인트
        registry.addEndpoint(&quot;/ws-chat&quot;)
                .setAllowedOriginPatterns(&quot;*&quot;)//cors 허용
                .withSockJS(); // websocket을 지원하지 않는 브라우저를 위해 SockJS 활성화
    }
}</code></pre>
<p>STOMP를 활용하기 때문에 WebSocketMessageBrokerConfigurer 인터페이스를 사용해서 설정파일을 구성했다 (@EnableWebSocketMessageBroker도 사용해야함 !) . </p>
<p>여기서 가장 중요한 것은 다음과 같다.</p>
<ul>
<li>웹소켓을 연결할 때는 &quot;/ws-chat&quot; 엔드포인트에 연결</li>
<li>메세지를 보내는 엔드포인트는 &quot;/pub&quot;으로 시작해야함</li>
<li>메세지를 받기 위해서는 &quot;/sub&quot;으로 시작하는 엔드포인트를 구독해야함</li>
</ul>
<h3 id="2-2-도메인-구현">2-2. 도메인 구현</h3>
<p><img src="https://velog.velcdn.com/images/o_seongblll_/post/355a9381-37bb-422d-9ef2-f6d919d5f14f/image.png" alt=""></p>
<p>추후 수정할 수도 있겠지만 초기에는 이렇게 구성했다. 디비에 저장하기 위해서 jpa를 사용하였다.</p>
<p>한 유저가 여러개의 채팅방에 들어갈 수 있고, 또 한 채팅방에 여러 명의 유저가 있을 수 있기 때문에 다대다 관계를 ChatRoomUser라는 중간 테이블을 넣어서 해결하였다.</p>
<h4 id="chat-엔티티">Chat 엔티티</h4>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Chat {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;chat_id&quot;)
    private Long id;

    @Enumerated(EnumType.STRING)
    private MessageType messageType;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;room_id&quot;)
    private ChatRoom chatRoom;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;sender_id&quot;)
    private User sender;

    private String content;

    private LocalDateTime sentAt;

    @Builder
    public Chat(MessageType messageType, ChatRoom chatRoom, User sender, String content, LocalDateTime sentAt){
        this.messageType = messageType;
        this.chatRoom = chatRoom;
        this.sender = sender;
        this.content = content;
        this.sentAt = sentAt;
    }
    // private boolean isRead;
}</code></pre>
<h4 id="chatroom-엔티티">ChatRoom 엔티티</h4>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ChatRoom {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;chatRoom_id&quot;)
    private Long id;

    @Column(nullable = false)
    private String roomName;

    /*
        cascade를 사용해서 저장하는게 적절해보이는데, cascade를 사용하면 chatRoomUser 테이블에 roomId가 null인 컬럼들이 들어가서
        임시적으로 ChatService에서 직접 일일이 저장하는 식으로 구현
        // @OneToMany(mappedBy = &quot;room&quot;, cascade = CascadeType.ALL)
     */
    @OneToMany(mappedBy = &quot;room&quot;)
    private List&lt;ChatRoomUser&gt; userList;

    @Builder(access = AccessLevel.PRIVATE)
    private ChatRoom(List&lt;ChatRoomUser&gt; userList, String roomName){
        this.userList = userList;
        this.roomName = roomName;
    }

    public static ChatRoom createChatRoom(List&lt;ChatRoomUser&gt; userList, String roomName){
        return ChatRoom.builder()
                .userList(userList)
                .roomName(roomName)
                .build();
    }

}</code></pre>
<h4 id="chatroomuser-엔티티">ChatRoomUser 엔티티</h4>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
public class ChatRoomUser {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;user_id&quot;)
    private User user;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;room_id&quot;)
    private ChatRoom room;

    public ChatRoomUser(User user) {
        this.user = user;
    } // id list에서 ChatRoomUser로 매핑하기 위한 메소드

    public static ChatRoomUser of(ChatRoom room, User user){
        return new ChatRoomUser(room, user);
    }

    private ChatRoomUser(ChatRoom room, User user){
        this.room = room;
        this.user = user;
    }
}</code></pre>
<p><del>또 dto와 유저엔티티도 구현하였다</del></p>
<h3 id="2-3-방-생성-기능">2-3. 방 생성 기능</h3>
<pre><code class="language-java">// ChatController
@PostMapping(&quot;/create&quot;)
public ResponseEntity&lt;ChatRoomDto&gt; createChatRoom(@RequestBody CreateChatRoomRequestDto request){
    return ResponseEntity.status(HttpStatus.OK).body(chatService.createChatRoom(request.participantsId(), request.roomName()));
}</code></pre>
<pre><code class="language-java">// ChatService
public ChatRoomDto createChatRoom(List&lt;Long&gt; participantsId, String roomName) {
    List&lt;User&gt; userList = userRepository.findAllById(participantsId);

    // 채팅방 생성
    ChatRoom chatRoom = ChatRoom.createChatRoom(userListToChatRoomUserlist(userList), roomName);

    for(User user : userList){
        ChatRoomUser chatRoomUser = ChatRoomUser.of(chatRoom, user);
        chatRoomUserRepository.save(chatRoomUser);
   }

    chatRoomRepository.save(chatRoom);

    return ChatRoomDto.from(chatRoom);
}

private List&lt;ChatRoomUser&gt; userListToChatRoomUserlist(List&lt;User&gt; users){
    return users.stream()
                .map(ChatRoomUser::new)
                .toList();
}</code></pre>
<p>&quot;/create&quot; 엔드포인트로 요청이 오면 채팅방을 만들어 디비에 저장하도록 하였다. 
ChatRoom, ChatRoomUser에 각각 저장하도록 하였으며, 만들때 참가자 리스트를 받아서 만들기 때문에 for문을 돌려 ChatRoomUser를 유저당 각각 저장하도록 하였다 .
<del>(Cascade를 활용해서 엔티티에서 처리할 수 있을 것 같은데 생각하는대로 잘 안되서 우선 이렇게 구현하였다..)</del></p>
<h3 id="2-4-메세지-전송-기능">2-4. 메세지 전송 기능</h3>
<pre><code class="language-java">// ChatController
@MessageMapping(&quot;/chat/send&quot;)
public void sendMessage(@RequestBody ChatMessageDto chatMessageDto){
    chatService.sendMessage(chatMessageDto);
}</code></pre>
<pre><code class="language-java">// ChatService
public void sendMessage(ChatMessageDto chatMessageDto){
    // db 저장
    ChatRoom room = chatRoomRepository.findById(chatMessageDto.chatRoomId())
            .orElseThrow(() -&gt; new CustomException(ErrorCode.CHATROOM_NOT_FOUND));

    User sender = userRepository.findById(chatMessageDto.senderId())
            .orElseThrow(() -&gt; new CustomException(ErrorCode.USER_NOT_FOUND));

    Chat message = Chat.builder(
        .chatRoom(room)
        .sender(sender)
        .content(chatMessageDto.content())
        .sentAt(chatMessageDto.sentAt()) // front에서 sentAt 보내준다고 가정
        .build();

    chatRepository.save(message);

    // 전송
    template.convertAndSend(&quot;/topic/chat/room&quot; + chatMessageDto.chatRoomId(), chatMessageDto);
}</code></pre>
<p>메세지 전송을 테스트 해보기 위해 다음 페이지를 활용했다. 
<a href="https://jiangxy.github.io/websocket-debug-tool/">https://jiangxy.github.io/websocket-debug-tool/</a></p>
<p>다음과 같이 작성하면 테스트가 가능하다. 
<img src="https://velog.velcdn.com/images/o_seongblll_/post/191f7407-a847-459a-979a-3b6c25fbb77c/image.PNG" alt="">
<del>왜 websocket을  끝에 붙여야하는지는 다음 문서에 적혀있다</del> <a href="https://docs.spring.io/spring-framework/reference/web/websocket/fallback.html#websocket-fallback-sockjs-enable">spring SockJS Fallback</a></p>
<p>이후 connect를 눌러 다음과 같이 작성해준다.</p>
<ul>
<li><p>메세지 보내기
<img src="https://velog.velcdn.com/images/o_seongblll_/post/c51abcd3-3759-4e80-ab14-6cd027f95c80/image.PNG" alt="">
보낼때는 dto에 맞게끔 json을 보내주면 된다. 
<del>만약 security 설정을 했다면 헤더에서 토큰 설정을 활용하면 될 것 같다</del></p>
</li>
<li><p>메세지 받기
<img src="https://velog.velcdn.com/images/o_seongblll_/post/b802489e-4a1d-4c9d-ac30-0351ec986d41/image.PNG" alt="">
이렇게 메세지가 온 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/o_seongblll_/post/d673235c-2f8c-41b8-b473-9d1881088ffa/image.png" alt=""></p>
</li>
</ul>
<p>db에도 저장된 것을 확인할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[정적 팩토리 메서드 + builder]]></title>
            <link>https://velog.io/@o_seongblll_/%EC%A0%95%EC%A0%81-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9C-builder</link>
            <guid>https://velog.io/@o_seongblll_/%EC%A0%95%EC%A0%81-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9C-builder</guid>
            <pubDate>Sun, 20 Jul 2025 14:58:16 GMT</pubDate>
            <description><![CDATA[<p><a href="https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%A0%95%EC%A0%81-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%83%9D%EC%84%B1%EC%9E%90-%EB%8C%80%EC%8B%A0-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90">정적 팩토리 메서드 참고 블로그</a></p>
<p>정적 팩토리 메서드를 활용하면 생성 목적에 대해 이름지을 수도 있고, 객체의 생성을 제어할 수 있다.</p>
<h3 id="단순-정적-팩토리-메서드">단순 정적 팩토리 메서드</h3>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ChatRoom {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;chatRoom_id&quot;)
    private Long id;

    @OneToMany(mappedBy = &quot;room&quot;, cascade = CascadeType.ALL)
    private List&lt;ChatRoomUser&gt; userList;

    private ChatRoom(List&lt;ChatRoomUser&gt; userList) {
        this.userList = userList;
    }

    public static ChatRoom from(List&lt;ChatRoomUser&gt; userList) {
        return new ChatRoom(userList);
    }
}</code></pre>
<p>구현하고 있는 것은 채팅시스템의 채팅 방 엔티티 코드이다.</p>
<p>당장 필요한 것은 방의 사용자 리스트뿐이기에 이렇게 private 생성자를 만들어두고 from 메서드로 새로운 객체를 반환하는 식으로 사용할 수도 있다. 
from 메서드는 public으로 열려있기에 다른 곳에서도 호출이 가능하다.</p>
<p>이렇게 객체의 생성을 제어할 수 있다.</p>
<h3 id="도메인의-필드가-많아진다면">도메인의 필드가 많아진다면?</h3>
<pre><code>    private ChatRoom(List&lt;ChatRoomUser&gt; userList, String roomName, Long adminId, LocalDateTime createdAt) {
        this.userList = userList;
        this.roomname = roomName;
        this.adminId = adminId;
        this.createdAt = createdAt;
    }

    public static ChatRoom of(List&lt;ChatRoomUser&gt; userList, String roomName, Long adminId, LocalDateTime createdAt ) {
        return new ChatRoom(userList, roomName, adminId, createdAt);
    }</code></pre><p>예를 들기 위해 세 가지 필드를 추가해보았다. 매개변수가 늘어났기에 메서드 이름이 from에서 of로 변했다 <del>(참고한 블로그 참조)</del>. 만일 필드 수가 무한정 추가된다면 실수 위험이 커진다.</p>
<h3 id="정적-팩토리-메서드--builder-패턴">정적 팩토리 메서드 + builder 패턴</h3>
<pre><code class="language-java">    @Builder(access = AccessLevel.PRIVATE)
    private ChatRoom(List&lt;ChatRoomUser&gt; userList, String roomName, Long adminId, LocalDateTime createdAt){
       this.userList = userList;
        this.roomName = roomName;
        this.adminId = adminId;
        this.createdAt = createdAt;
    }

    public static ChatRoom createChatRoom(List&lt;ChatRoomUser&gt; userList, String roomName, Long adminId, LocalDateTime createdAt){
        return ChatRoom.builder()
                .userList(userList)
                .roomName(roomName)
                .adminId(adminId)
                .createdAt(createdAt)
                .build();
    }</code></pre>
<p>도메인의 필드가 늘어나는 확장성을 고려해서 정적 팩토리 메서드에 builder 패턴을 새로운 메서드를 만들어 사용하면 순서 실수할 일 없이 사용할 수 있다.</p>
<p>도메인의 필드 수가 적다면 단순히 정적 팩토리 메서드로 충분하겠지만, 프로젝트를 하다보면 필드 수가 끝도 없이 늘어날 수 있기에 이렇게 미리 빌더 패턴으로 확장성을 챙겨가는 것도 좋은 방법인 것 같다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 10809 자바(HashMap을 이용하여)]]></title>
            <link>https://velog.io/@o_seongblll_/%EB%B0%B1%EC%A4%80-10809-%EC%9E%90%EB%B0%94HashMap%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@o_seongblll_/%EB%B0%B1%EC%A4%80-10809-%EC%9E%90%EB%B0%94HashMap%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC</guid>
            <pubDate>Sun, 30 Mar 2025 12:48:28 GMT</pubDate>
            <description><![CDATA[<p>문제를 보고 HashMap을 활용하면 풀 수 있겠다는 생각을 하여, 문제를 푸는데는 어려움이 없었다.
정답을 맞추고 다른 사람의 코드들을 살펴보니 대부분이 아스키코드를 활용하여 풀어낸 것을 보고 이렇게 풀어낼 수도 있구나 라는 생각을 했다. 하지만 내가 푼 방법이 대체로 존재하지 않기에 코드를 공유해보자 한다.</p>
<pre><code class="language-java">import java.util.*;
import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        HashMap&lt;Character,Integer&gt; map = new HashMap&lt;&gt;();
        String alphabet = &quot;abcdefghijklmnopqrstuvwxyz&quot;;

        for(int i = 0; i &lt; alphabet.length(); i++){
            map.put(alphabet.charAt(i),-1);
        }

        String input = br.readLine();

        for(int i = 0; i &lt; input.length(); i++){
            char c = input.charAt(i);
            if(map.get(c) == -1){
                map.replace(c,i);
            }
        }

        for(Character c : map.keySet()){
            bw.write(map.get(c)+&quot; &quot;);
        }
        bw.flush();
        bw.close();
        br.close();
    }
}</code></pre>
<p>alphabet을 직접 임의로 입력하여 for문으로 HashMap에 넣고 다음 for문에서 value값이 -1 이 아니면 map에 인덱스를 넣는 식으로 작성하였다.</p>
<p><del>아스키코드를 사용하는게 시간상으로도 더 유리할 것이고, 문제의도와 더 가깝다고 생각이 들긴하지만, 어쨌든 정답이니까..ㅎ</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[6차 WIL]]></title>
            <link>https://velog.io/@o_seongblll_/6%EC%B0%A8-WIL</link>
            <guid>https://velog.io/@o_seongblll_/6%EC%B0%A8-WIL</guid>
            <pubDate>Mon, 03 Feb 2025 14:11:45 GMT</pubDate>
            <description><![CDATA[<p>개발을 본격적으로 들어가기 전에 기존에 보던 인강을 마무리하고 개발에 들어가고 싶어서 강의록을 가지고 이해하고 이해가 안되는 부분만 인강을 보는 식으로 빨리 진행하였다.</p>
<p>그러던중 동적 query에 대한 부분이 있었는데, 이 부분에 대해서 자세히 나오지 못해 아쉬웠다. 강의록에 있던 것은 JPQL이나 JPA Criteria 로 처리하는 것이었는데, 두 방법 다 한계가 있었다. 주로 동적 query를 위해 사용되는 QueryDSL에 대해서는 간단히 설명하고 다음 파트로 넘어가서 간단하게라도 QueryDSL을 공부해보기로 했다. (프로젝트에서도 검색, 즉 동적쿼리가 사용될 예정이다)</p>
<p>JPQL이나 JPA Criteria는 너무 복잡하고, 구현하기도 어렵다는 단점이 있었다.
대응책인 QueryDSL은 자바 코드로 sql query를 작성하게 해준다는 점에서 컴파일 시 오류를 잡는 등의 장점이 있었다.</p>
<p>다음은 각각 chatgpt가 짜준 JPQL 과 QueryDSL 이다.
(조건 검색 (나이가 20 이상인 회원 조회))</p>
<pre><code class="language-java">//JPQL
String jpql = &quot;SELECT m FROM Member m WHERE m.age &gt;= :age&quot;;
List&lt;Member&gt; members = em.createQuery(jpql, Member.class)
                         .setParameter(&quot;age&quot;, 20)
                         .getResultList();</code></pre>
<pre><code class="language-java">//QUERYDSL
List&lt;Member&gt; members = queryFactory
    .selectFrom(QMember.member)
    .where(QMember.member.age.goe(20)) // greater or equal (&gt;=)
    .fetch();

</code></pre>
<p>JPQL은 문자열로 쿼리를 만들어내고 QueryDSL은 자바 코드로 쿼리가 작성된다. 
그리고 jpql보다 sql에 더 가까운 형태를 띄고 있기에 직관적이다. (select문, where문등)
또한 자바로 작성되기에 IDE의 도움을 받는 등의 이점을 얻을 수 있다.</p>
<p>gradle 설정, Q클래스, BooleanBuilder등 단순히 jpql을 사용하는 것보다는 많은 설정이 필요하지만, 개발자의 실수를 잡기 어렵고 구현이 어려운 것보다는 나은 선택이라고 생각한다.</p>
<p>프로젝트 내에서 검색 기능이 있어서 어떻게 구현이 되는지 궁금했는데 이런 방식으로 구현될 수 있다는 것을 알았다.</p>
<h3 id="계획">계획</h3>
<p>이제는 진짜 개발을 미룰 수 없다. 내가 맡은 부분인 크롤링, db관리, 카카오 소셜 로그인등을 위해 더 열심히 공부하고 진행해야겠다는 생각이다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[5차 WIL]]></title>
            <link>https://velog.io/@o_seongblll_/5%EC%B0%A8-WIL</link>
            <guid>https://velog.io/@o_seongblll_/5%EC%B0%A8-WIL</guid>
            <pubDate>Thu, 23 Jan 2025 13:06:51 GMT</pubDate>
            <description><![CDATA[<h2 id="기획과-개발-전까지">기획과 개발 전까지</h2>
<p>기획명세서를 작성하고 다음엔 백엔드 레포지토리 초기 세팅, 그리고 erd 설계를 진행했다.
또 도커를 써보라는 선배의 조언에 도커 공부를 하고 개인 프로젝트에 연결해보는 과정을 진행해보았다.</p>
<p>도커를 연결하고 mysql을 다뤄보고 있었는데, 기초데이터베이스 수업에서 배운 GRANT query를 사용해야하는 상황도 있었다. <del>(무엇인가 설정을 잘못해서)</del>
생각보다 학교에서 배우는 것은 개발과정에서 상당히 필요로 한다는 것을 계속해서 느끼고 있다,, </p>
<h2 id="erd-설계">erd 설계</h2>
<p>처음 프로젝트를 하며 erd를 설계했을때는 데이터베이스를 배우지 않고 속성으로 공부하고 진행하였는데, 이번 학기 중에 수업을 듣고 erd를 설계해보니 조금 더 수월하게, 그리고 과거의 내가 미쳐 보지 못했던 실수등을 볼 수 있게 되었다. 물론 지금의 erd가 완벽하다고는 생각이 들지 않아 추후 팀원분과 erd수정을 하며 더 확실한 erd를 만들고 db를 설계해야 할 것 같다는 생각이다.</p>
<p><img src="https://velog.velcdn.com/images/o_seongblll_/post/2b605756-3238-4748-8662-b5998d233b3c/image.PNG" alt=""></p>
<h2 id="앞으로의-계획">앞으로의 계획</h2>
<p>이제는 개발을 해야할 때가 왔다. 이론공부만 하는 것보다는 부딪혀가며 배워야 깨달을 수 있다고 생각하는 사람으로서 지난 한 달동안의 과정보다는 재미있는 시간이 되지 않을까 싶다.</p>
<p>간단한 api 설계를 하고, jpa공부와 프로젝트를 병행하며 진행할 예정이다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker+MYSQL+Springboot+환경변수  ]]></title>
            <link>https://velog.io/@o_seongblll_/DockerMYSQLSpringboot%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98</link>
            <guid>https://velog.io/@o_seongblll_/DockerMYSQLSpringboot%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98</guid>
            <pubDate>Mon, 13 Jan 2025 06:03:08 GMT</pubDate>
            <description><![CDATA[<p>application.properties에 데이터베이스 주소와 비밀번호등을 올린 후 깃허브에 올리면 안되지 않나? 
<del>디비 교수님이 강조한 보안의 중요성</del>
(지난 프로젝트에서 내가 얼마나 부족했던지 알게 된 부분이다. 각종 비밀번호가 깃허브에 올라가있다.. 그 이외에도 모종의 이유로 레포지토리를 private 해놓을 수 밖에 없었다)</p>
<p>그래서 Docker를 활용해 Mysql과 환경변수를 사용하기 위한 과정을 기록해보려고 한다.</p>
<h2 id="docker-설치window">Docker 설치(window)</h2>
<p><a href="https://www.docker.com/products/docker-desktop/">Docker 홈페이지</a>에서 다운로드 하고 다음 명령어로 설치를 확인한다.</p>
<pre><code>$ docker -v
Docker version 27.4.0, build bde2b89</code></pre><h2 id="docker-컨테이너-실행">Docker 컨테이너 실행</h2>
<pre><code>$ docker pull mysql 
latest: Pulling from library/mysql
0b9dc7ad7f03: Download complete
a841bff36f3c: Download complete
1f87d67b89c6: Download complete
80ba30c57782: Download complete
5e49e1f26961: Download complete
cb5a6a8519b2: Download complete
570d30cf82c5: Download complete
ced670fc7f1c: Download complete
2c0a233485c3: Download complete
cd0d5df9937b: Download complete
Digest: sha256:0255b469f0135a0236d672d60e3154ae2f4538b146744966d96440318cc822c6
Status: Downloaded newer image for mysql:latest
docker.io/library/mysql:latest

$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
mysql        latest    0255b469f013   3 months ago   828MB

$ docker run --name &lt;컨테이너명&gt; -e MYSQL_ROOT_PASSWORD=&lt;rootPassword&gt; -d -p 3306:3306 mysql:latest

$ docker ps -a 
CONTAINER ID   IMAGE          COMMAND                   CREATED          STATUS         PORTS                               NAMES
3cc3d3f49058   mysql:latest   &quot;docker-entrypoint.s…&quot;   10 seconds ago   Up 8 seconds   0.0.0.0:3306-&gt;3306/tcp, 33060/tcp   jpaUse1
</code></pre><img src="https://velog.velcdn.com/images/o_seongblll_/post/626bdaad-5553-4429-a357-2a37f8bbb53b/image.PNG">
위와 같이 Docker 앱에서도 확인이 가능하다.




<blockquote>
<h4 id="포트-충돌-에러">포트 충돌 에러</h4>
<p>처음 Docker를 깔고 포트를 연결할때는 기존의 컴퓨터에 깔려있는 MYSQL의 3306포트와 충돌이 발생하여 에러가 발생했다.</p>
</blockquote>
<p>docker Error invoking remote method &#39;docker-start-container&#39;: Error: (HTTP code 500) server error - Ports are not available: exposing port TCP 0.0.0.0:3306 -&gt; 0.0.0.0:0: listen tcp 0.0.0.0:3306: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted.</p>
<blockquote>
</blockquote>
<p>$ netstat -ano | findstr 3306 
결과값에서 pid = 6868임을 알 수 있었다 
$ tasklist | findstr 6868 
으로 어떤 프로세스가 3306 포트를 차지하고 있는지 알아보기 
-&gt; 기존에 컴퓨터에 깔려있던 MYSQL이 사용중
-&gt; Docker를 사용하기 위해 3306 포트 비우기 
$  taskkill /pid 6868 /f</p>
<blockquote>
</blockquote>
<p>포트를 3306이 아닌 것으로 옮기는 방법도 있었는데 그 방법은 실행해보지 않았다</p>
<blockquote>
</blockquote>
<hr>
<p>친구가  window + r -&gt; services.msc를 입력하고 들어가 mysql을 중지 시키는 방법도 있다고 한다</p>
<h2 id="docker-컨테이너-volume을-이용">Docker 컨테이너 (Volume을 이용)</h2>
<p>Docker 컨테이너 내부에 mysql을 설치해서 사용해도 되지만, 컨테이너가 지워질 경우 데이터도 지워져버리고, 컨테이너에 의존적이기에 다른 컨테이너에서 사용이 어렵다는 단점이 있다.
그래서 volumn을 이용해 외부에 저장하는 방법이 선호된다고 한다.</p>
<p>위의 설치해둔 컨테이너를 지우고 진행하기 위해 다음과 같은 명령어로 시작된다.
<del>앱에서 지우는 것이 훨씬 쉽다</del></p>
<pre><code>$ docker stop &lt;컨테이너명&gt;
$ docker ps 
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
$ docker ps -a
CONTAINER ID   IMAGE          COMMAND                   CREATED          STATUS                     PORTS     NAMES
3cc3d3f49058   mysql:latest   &quot;docker-entrypoint.s…&quot;   33 minutes ago   Exited (0) 7 seconds ago             jpaUse1
$ docker rm &lt;컨테이너ID&gt;</code></pre><p>루트디렉토리(src파일이 있는 위치)에 .env와 docker-compose.yml을 작성한다. 
데이터베이스 비밀번호등 노출되면 안되는 코드를 .env파일에 적는다.
컨테이너에서 새로 mysql을 관리하기 때문에 루트비밀번호와 db이름등을 새로 설정하면 된다.</p>
<pre><code>MYSQL_ROOT_PASSWORD = &lt;루트비밀번호&gt;
MYSQL_DATABASE = &lt;db이름&gt;
MYSQL_USER = &lt;mysql유저&gt;
MYSQL_PASSWORD = &lt;유저비밀번호&gt;</code></pre><p>MySQL을 Docker 컨테이너로 실행하기 위한 docker-compose.yml 파일을 생성한다.
.env와 같은 디렉토리에 있으면 자동으로 docker-compose.yml에서 읽어들일 수 있다.</p>
<pre><code class="language-yml">services:
  mysql:
    image: mysql:latest
    restart: always
    container_name: jpaUse1Container # 임의로 지정한 컨테이너 이름
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    ports:
      - &quot;3306:3306&quot;
    volumes:
      - mysql-data:/var/lib/mysql</code></pre>
<blockquote>
<p>위와 같이 작성하고 다음 명령어를 실행하니 에러가 발생했다.
$ docker-compose up -d
service &quot;mysql&quot; refers to undefined volume mysql-data: invalid compose project</p>
</blockquote>
<p>찾아보니 volumes 섹션을 새로 만들고 거기서 mysql-data를 정의해줘야 한다고 한다.</p>
<h4 id="최종-docker-composeyml-파일">최종 docker-compose.yml 파일</h4>
<pre><code class="language-yml">services:
  mysql:
    image: mysql:latest
    restart: always
    container_name: jpaUse1Container
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    ports:
      - &quot;3306:3306&quot;
    volumes:
      - mysql-data:/var/lib/mysql

volumes:
  mysql-data:
    driver: local</code></pre>
<p>명령어를 실행해보니 container와 volume이 만들어졌다고 보인다. <del>(앱에서도 확인 가능)</del></p>
<pre><code class="language-bash">$ docker-compose up -d
[+] Running 1/1
 ✔ mysql Pulled                                                                          3.6s 
[+] Running 3/3
 ✔ Network jpashop_default      Created                                                  0.2s 
 ✔ Volume &quot;jpashop_mysql-data&quot;  Created                                                  0.0s 
 ✔ Container jpaUse1Container   Started  </code></pre>
<hr>
<ul>
<li>참고<ul>
<li><a href="https://gran007.tistory.com/entry/%EC%9C%88%EB%8F%84%EC%9A%B0-%EB%8F%84%EC%BB%A4Docker-%EC%84%A4%EC%B9%98">https://gran007.tistory.com/entry/%EC%9C%88%EB%8F%84%EC%9A%B0-%EB%8F%84%EC%BB%A4Docker-%EC%84%A4%EC%B9%98</a></li>
<li><a href="https://gran007.tistory.com/entry/%EB%8F%84%EC%BB%A4-MySQL-%EC%84%A4%EC%B9%98">https://gran007.tistory.com/entry/%EB%8F%84%EC%BB%A4-MySQL-%EC%84%A4%EC%B9%98</a></li>
<li><a href="https://velog.io/@saintho/Error-%EB%8F%84%EC%BB%A4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%83%9D%EC%84%B1%EC%8B%9C-%ED%8F%AC%ED%8A%B8-%EC%B6%A9%EB%8F%8C">https://velog.io/@saintho/Error-%EB%8F%84%EC%BB%A4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%83%9D%EC%84%B1%EC%8B%9C-%ED%8F%AC%ED%8A%B8-%EC%B6%A9%EB%8F%8C</a></li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[4주차 WIL]]></title>
            <link>https://velog.io/@o_seongblll_/4%EC%A3%BC%EC%B0%A8-WIL</link>
            <guid>https://velog.io/@o_seongblll_/4%EC%A3%BC%EC%B0%A8-WIL</guid>
            <pubDate>Sat, 11 Jan 2025 07:22:15 GMT</pubDate>
            <description><![CDATA[<h2 id="프로젝트-팀-빌딩-및-기획">프로젝트 팀 빌딩 및 기획</h2>
<p>프로젝트를 진행하면서 가장 재미없는 것이 기획인 것 같다.
늘 기획단계에서 많은 문서를 남겨야 했었고, 이번 프로젝트는 더더욱 제대로 해보고 싶기에 또다시 문서 지옥에 빠질 것을 생각하면 벌써부터 아찔하다.</p>
<p>첫 빌딩과 코어타임을 가지고 우리 팀의 주제는 구체화되었다.
인강사이트(1차로는 개발 인강 (ex) 인프런, 유데미)의 강좌들을 비교하고 리뷰와 평점등을 사용자가 보다 편리하게 사용할 수 있도록 만들어주는 사이트를 만들기로 했다.</p>
<p>몇 개월만에 다시 기능명세서를 작성하다보니 프로젝트를 다시 시작한다는 설렘?과 또 열심히 공부해야할 것 같다는 두려움이 느껴졌다.
<img src="https://velog.velcdn.com/images/o_seongblll_/post/92c3e95e-3223-47bb-9382-eb48a88a8b5c/image.PNG" alt="">
<del>(옛 멘토님께 받은 기능 명세서 형식을 계속 사용해서 다른 사람들이 어떤 방식으로 기능명세서를 작성하는지 모른다.. 공부해봐야할 것 같다)</del></p>
<h2 id="스프링-핵심-원리---기본강좌-복습">스프링 핵심 원리 - 기본강좌 복습</h2>
<p>작년 1월에 이미 한 번 듣고 기록해둔 경험이 있어, 강좌를 보지 않고 강의록으로 이해하며 코드를 구현해보았다. 한 번 들어본 경험 + 프로젝트 경험이 쌓여 처음에는 이해되지 않었던 것들이 이해가 되고, 강좌를 안보고 진행하다보니 왜 이 코드가 여기 들어가야하는지등을 혼자 공부할 수 있어서 조금 더 깊은 이해를 할 수 있게 된 것 같다. </p>
<p>가장 기억에 남는 주제는 다음과 같다.</p>
<ul>
<li>구현 클래스에 의존하지 않고, 인터페이스에 의존하기</li>
<li>스프링은 어떻게든 객체를 싱글톤으로 관리해준다.</li>
</ul>
<h2 id="다음-목표">다음 목표</h2>
<p>김영한님의 jpa활용편 인강을 듣고 이해하는 것이 목표이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[3주차 WIL]]></title>
            <link>https://velog.io/@o_seongblll_/3%EC%A3%BC%EC%B0%A8-WIL</link>
            <guid>https://velog.io/@o_seongblll_/3%EC%A3%BC%EC%B0%A8-WIL</guid>
            <pubDate>Thu, 26 Dec 2024 14:06:46 GMT</pubDate>
            <description><![CDATA[<p>3주차 개념 </p>
<h2 id="enum">enum</h2>
<p>열거형(enum)은 요소라 불리는 명명된 값의 집합을 이루는 자료형이다. (상수 데이터들의 집합이라고 생각하면 된다)</p>
<pre><code class="language-java">enum Day{
    MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY;
}</code></pre>
<p>자바에서의 enum은 인터페이스와 같이 독립된 특수한 클래스이다.</p>
<p>장점:</p>
<ul>
<li>코드가 단순해지며 가독성이 좋아짐</li>
<li>혀용 가능한 값을 제한함 → 유형 안전 제공</li>
<li>키워드 enum을 사용하기에 구현의 의도 열거를 확실하게 알 수 있음</li>
<li>IDE의 적극적인 지원을 받을 수 있음</li>
</ul>
<h2 id="예외처리">예외처리</h2>
<p>프로그래밍의 오류에는 세 가지 종류가 있다.</p>
<ol>
<li>컴파일 에러 </li>
<li>런타임 에러</li>
<li>논리적 에러</li>
</ol>
<p>그 중 가장 주의깊게 공부하고 다뤄야 할 것은 런타임 에러이다. 
런타임 에러는 에러와 예외가 나뉜다.</p>
<ol>
<li>에러 : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류</li>
<li>예외 : 프로그램 코드에 의해서 수습될 수 있는 오류</li>
</ol>
<p>예외처리를 위한 try, catch문의 기본 구조</p>
<pre><code class="language-java">try{
    //로직
}
catch(xxx e){
  //예외처리
}</code></pre>
<p>try영역에서 발생한 xxx예외상황은 catch영역에서 처리됨</p>
<h2 id="어려웠던-점">어려웠던 점</h2>
<p>처음부터 과제가 주어진게 아니고 주차에 나눠서 쪼개어지다 보니 명세가 애매한 부분도, 내가 잘 구현하지 못한 부분도 존재하게 되었다. 그러다보니 내가 구현해 놓았던 것도 다시 되돌리는 식으로 프로그래밍을 하다보니 에러가 많이 났던 것 같다.
<del>객체지향적 설계를 하지 못한게 아닐까</del></p>
<h2 id="앞으로의-계획">앞으로의 계획</h2>
<p>종강하고 분명히 강의를 듣겠다했는데 하나도 듣지 않였다.. 이제 프로젝트 시작인데 팀원들에게 민폐를 끼치지 않기 위해 목표를 확실히 정해 강의를 듣고 공부를 해야겠다는 생각이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2주차 WIL]]></title>
            <link>https://velog.io/@o_seongblll_/2%EC%A3%BC%EC%B0%A8-WIL</link>
            <guid>https://velog.io/@o_seongblll_/2%EC%A3%BC%EC%B0%A8-WIL</guid>
            <pubDate>Sun, 01 Dec 2024 13:10:08 GMT</pubDate>
            <description><![CDATA[<h2 id="2주차-과제--vodtodao">2주차 과제 : VO/DTO/DAO</h2>
<p>VO(value object) : 값을 표현하는 객체 </p>
<pre><code class="language-java">class Color {
        private final int red;
        private final int green;
        private final int blue;

        public Color(int red, int green, int blue) {
            if(0 &lt;= red &amp;&amp; red &lt;= 255) {
                this.red = red;
            } else {
                this.red = 0;
            }
        }
    }</code></pre>
<p>int, float 이렇게 변수 하나를 선언하는 것이 아닌 객체는 여러 개가 들어갈 수 있음
또 final로 설정해서 처음부터 값이 변하지 않도록 설정할 수 있음(상수)
→ 생성된다면 불변하며 확실한 값을 가지고 있기에 개발자는 후에 값이 변경되는 것을 걱정하지 않고 작성할 수 있다</p>
<p>DTO(data tranfer object) : 데이터 전송을 위해 사용되는 객체 
전달해야하는 매개변수가 많을 때 이를 다 포함하는 객체로 넘겨주면 편함(ex. 학생의 이름, 전화번호, 이메일등을 넘기는 것보다 객체를 넘기는 경우)
VO와 다르게 경우에 따라 값이 변경되어 전달될 수도 있음(값이 불변하지 않음)</p>
<p>DAO(data access object) : 데이터 접근 객체
서비스 로직과 데이터베이스 로직을 분리하기 위해서 사용됨
즉 모든 데이터베이스 관련 로직은 DAO에서 이루어짐(crud, db 연결, sql문 실행등)</p>
<h2 id="내가-정의하는-스프링이란-">내가 정의하는 스프링이란 ?</h2>
<p>스프링은 자바 언어 기반의 프레임워크이고, 자바의 가장 큰 특징은 객체 지향 언어이다.
즉, 스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크이다.
스프링은 제어의 역전, 의존관계 주입등을 지원하여 다형성을 극대화해서 이용할 수 있도록 도와준다.</p>
<h2 id="어려웠던-점">어려웠던 점</h2>
<p>명세를 너무 믿은 나머지 의심하지 않고 스프링 부트가 아닌 스프링으로 의존성을 추가해서 몇 시간동안 톰캣과 서블릿 디스패처의 에러와 싸웠다.. 그래도 두 가지를 조금이라도 공부할 수 있었으니,, 완전 럭키비키,,, (근데 이제 시험기간에ㅠㅠ)</p>
<h2 id="앞으로의-계획">앞으로의 계획</h2>
<p>일단 시험을 다 보고, 종강하면 스프링 강의를 추가로 들을 생각이다.
종강한 내가 들을지는 모르겠지만, 이제는 열심히 살아야 할 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[1주차 WIL ]]></title>
            <link>https://velog.io/@o_seongblll_/1%EC%A3%BC%EC%B0%A8-WIL</link>
            <guid>https://velog.io/@o_seongblll_/1%EC%A3%BC%EC%B0%A8-WIL</guid>
            <pubDate>Sat, 23 Nov 2024 07:04:17 GMT</pubDate>
            <description><![CDATA[<h2 id="gdsc-프로젝트-트랙을-시작하며">GDSC 프로젝트 트랙을 시작하며</h2>
<p>장고로 프로젝트를 해본 경험뿐이었기에, 또 한국에서 개발자로 살아남기 위해 스프링으로 프로젝트 트랙을 지원하고 시작하게 되었다. </p>
<h2 id="1주차-과제">1주차 과제</h2>
<p>1주차 과제는 크게 두 가지였다. 객체지향과 인터페이스 알아보기, 또 자바를 사용하여 턴제 게임 구현하기.</p>
<p>객체 지향이란 특정한 기능을 가진 객체들이 모여서 협력하여 문제를 해결하는 것이다.
인터페이스란 자바의 다형성을 활용하여 더 유연한 프로그램을 만들게 해주는 기법이다.
다중 상속이 가능하기에 단순히 클래스를 사용하는 것보다 더 유연하게 프로그램을 짤 수 있기 때문이다.</p>
<p>턴제 게임은 주어진 명세에 맞게 턴을 진행하며 게임을 하도록 인터페이스를 활용하여 코드를 짜는 것이 과제였다.</p>
<h2 id="어려웠던-점">어려웠던 점</h2>
<p>인터페이스라는 것을 공부하고 분명히 알았다고 생각했는데 실제로 사용해보려고 하니 생각보다 쉽지 않았다. 또 막상 코드를 짜니 인터페이스를 적극적으로 사용하지 않는 내 코드를 볼 수 있었다..</p>
<h2 id="느낀-점">느낀 점</h2>
<p>새로운 사람들과 같이 공부하며 추후 프로젝트를 할 생각에 재밌기도, 두렵기도 하다.
그래도 이렇게 공부할 가이드 라인을 줘서 조금은 다행이라는 생각이 들었다. 또, 늘 기록해야지 기록을 귀찮아서 안하게 되었는데, 이렇게 약간의 강제성이 부여되니 기록을 하게 되는 점이 좋다고 생각한다. </p>
<h2 id="앞으로-계획">앞으로 계획</h2>
<p>인터페이스를 공부할 때 &quot;객체는 클래스가 아닌 인터페이스로 참조하라&quot; 라는 말이 있었다.
순서가 잘못 되었지만 인터페이스를 공부하기 위해 시간이 된다면 이미 짠 코드를 인터페이스를 조금 더 활용하여 동작하게끔 수정해보고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 30802 파이썬]]></title>
            <link>https://velog.io/@o_seongblll_/%EB%B0%B1%EC%A4%80-30802-python</link>
            <guid>https://velog.io/@o_seongblll_/%EB%B0%B1%EC%A4%80-30802-python</guid>
            <pubDate>Tue, 16 Jul 2024 15:28:38 GMT</pubDate>
            <description><![CDATA[<p>쉽게 풀 수 있을 것 같았는데 생각보다 많이 틀린 문제이다,,
구글에 파이썬 풀이가 안나와서 직접 써보기로 했다</p>
<p>틀린 이유는 특정 사이즈의 티셔츠를 신청한 사람이 0명일 수도 있다는 것,,
위만 알고 있으면 쉽게 풀 수 있을 것 같다</p>
<pre><code class="language-python">n=int(input())
lst=list(map(int,input().split()))
t,p = map(int,input().split())
t_bundle=0

for i in lst:
    if i==0:
        continue
    elif i&lt;=t:
        t_bundle+=1
    elif i%t==0:
        t_bundle+=i//t
    else:
        t_bundle+=i//t+1

p_bundle=n//p
pen=n%p

print(t_bundle)
print(f&#39;{p_bundle} {pen}&#39;)</code></pre>
]]></description>
        </item>
    </channel>
</rss>