<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>호승</title>
        <link>https://velog.io/</link>
        <description>개발자 되기 다시 시작</description>
        <lastBuildDate>Mon, 16 Feb 2026 07:39:35 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. 호승. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/meteor_control0" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[핸즈온 바이브 코딩]]></title>
            <link>https://velog.io/@meteor_control0/%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%8A%94-%EB%B0%A9%ED%96%A5%EC%9D%84-%EC%A0%95%ED%95%98%EA%B3%A0-AI%EA%B0%80-%EA%B5%AC%ED%98%84%ED%95%9C%EB%8B%A4</link>
            <guid>https://velog.io/@meteor_control0/%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%8A%94-%EB%B0%A9%ED%96%A5%EC%9D%84-%EC%A0%95%ED%95%98%EA%B3%A0-AI%EA%B0%80-%EA%B5%AC%ED%98%84%ED%95%9C%EB%8B%A4</guid>
            <pubDate>Mon, 16 Feb 2026 07:39:35 GMT</pubDate>
            <description><![CDATA[<h1 id="개발자는-방향을-정하고-ai가-구현한다">개발자는 방향을 정하고 AI가 구현한다</h1>
<p>유튜브를 둘러보면 많은 영상들이 외친다.
&quot;이제 개발자는 필요 없다. AI가 전부 해준다.&quot; 
신입 개발자인 나에게는 마치 사형 선고처럼 들리는 영상이 하루가 다르게 올라온다. 그리고 항상 언급되는 용어가 있다. 바로 <strong>바이브 코딩</strong>이다. 이 단어에 관심이 없다고 하면 신입 개발자라고 말하기 어려울 것이다.</p>
<p>그래서 자연스럽게 여러 바이브 코딩 관련 책에 관심이 가게 되었고 이 책을 선택했다.
단순히 바이브 코딩을 &#39;어떻게&#39;하는 방법을 넘어서 AI 시대에 개발자가 갖춰야하는 사고 방식과 원리를 배우고 싶었기 때문이다.</p>
<p>책에서 바이브 코딩의 기원부터 원리까지 굉장히 세세하게 다룬다. 바이브 코딩이 더 이상 농담과 같은 말이 아닌 개발 방법 중 하나로 떠오를 수 있던 이유와 원리를 명확하게 설명해준다. 책을 읽으며 자연스럽게 &#39;나도 할 수 있다&#39;라는 자신감을 얻을 수 있었다.</p>
<p>반면에 나에게 조금 어려웠던 점도 많았다. LLM의 등장부터 시작해서 성장해온 역사를 GPT와 클로드를 비교하며 매우 자세하게 설명해준다. 또한 바이브 코딩에서 사용되는 도구들에 대해서도 매우 자세하게 설명해주는데 IDE를 비롯해 각 AI들의 장단점을 넘어 요금제까지 꼼꼼히 다룬다. 내용은 표면적으로 이해하였지만 당장 사용하기에는 어려운 내용들이었다.</p>
<p>실습에 앞서 바이크 코딩을 하는 방식에 대해서 설명하는데 굉장히 마음에 들었다. 보통 이런 책은 실습하는 방식에 맞춰 설명을 이어나가는 방식이 많은데, 이 책은 현재 시점의 기술의 발전을 고려하여 설명해왔다. &#39;1달 단위로 기술이 발전하여 이전의 기술이 구식이 된다.&#39;는 관점에 따라 시간이 지나도 변하지 않을 원리를 기반으로 설명해왔다.</p>
<p>‘단순함 안정성 가시성’의 기술 철학을 강조하며 바이브 코딩에서 사용할 도구들을 선택하는 방식들을 설명한다. 예측 가능하고 통제 가능한 기술들을 선택하는 것이 확률적인 측면에서 유리한 점을 잘 설명해왔다. 이런 관점에서 재미있었던게 보수적인 기술을 선택하는 측면이 더 유리하다는 것이었다. ORM보다는 단순한 SQL이 유리한 것과 MCP는 최후의 보루로 사용하는 것이 좋다는 등 AI에게 최신 기술을 붙여야 좋을 것이라는 나의 인식을 바꿔주었다.</p>
<p>또한 AI 기반 코드를 신뢰하지 않는 것을 기반으로 바이브 코딩을 시작한다는 관점을 가지고 있기에 테스트와 리뷰의 중요성을 여러 번 말한다. 특히 로깅과 피드백 시스템은 AI의 &#39;눈과 귀&#39;가 되어준다는 점을 강조했다. AI는 실수하기에 개발자가 이런 실수를 알아차리는 것이 중요하기에 개발자의 역량 또한 중요하지 않은 점 또한 강조한다.</p>
<p>드디어 대망의 실습이다. 책의 중반이나 되어서 책의 실습을 시작한다는 점이 매우 신선하게 다가왔다. 그래도 그 만큼 앞의 원리 부분을 통해 잘 배웠기에 실습을 따라하는 데 큰 문제는 없었다. 책에서는 아래의  단계를 밟아 가며 바이브 코딩을 하는 방식을 설명해준다.</p>
<ol>
<li>사용자 &#39;규칙&#39; 설정</li>
<li>&#39;PRD(제품 요구 사항 문서) 작성&#39;</li>
<li>PRD를 통해 어떻게 만들지에 대한 &#39;설계 문서&#39;</li>
<li>설계 문서를 기반으로 한 &#39;작업 분할 문서&#39;</li>
</ol>
<p>각 문서들을 작성하는 방법이나 실제 작성된 문서들을 보면서 드는 생각은 내가 개발 방법 중 하나를 배우는 느낌보다는 프로젝트를 기획, 설계하는 방법을 배우고 있다고 생각하였다. 그러다 보니 책에서 강조한 <strong>개발자는 방향을, AI는 구현을</strong>이라는 철학에 깊게 공감하게 되었다.
또한 이런 문서를 나 혼자 작성하는 것이 아닌 AI와 함께 만드는 과정까지 겪어보니 개발자에게 필요한 역량이 코드 작성이 아니게 되었다는 점이 와닿게 되었다. </p>
<p>이후에는 마법처럼 내가 만든 문서를 통해 프로젝트가 완성되었다. 내가 코드 한 줄 제대로 작성하지 않았지만 멀쩡히 돌아가는 웹 애플리케이션이 완성된 것이다. 거기다가 TDD까지 적용되었다. 앞의 규칙을 정하고 문서를 작성하는 긴 시간에 비해 구현은 허무할 정도의 짧은 시간이 걸렸다. 정말 무섭다는 말이 떠오를 정도의 파괴력이었다.</p>
<p>이후에는 AI와 소통하는 방식에 대해서 자세하게 배운다. 프롬프트 엔지니어링과 패턴 언어를 어떻게 사용해야 효율적으로 사용할 수 있는 지 명확하게 알려준다. 프롬프트 엔지니어링은 평소 관심이 있어 공부를 하여 사용하고 있었지만 이 책처럼 명확하게 배워본 적은 없어 나의 부족한 점들을 많이 보완할 수 있었다.</p>
<ul>
<li>명확·구체적 지시</li>
<li>충분한 컨텍스트 제공</li>
<li>단계적 구조화</li>
<li>예시·포맷 활용</li>
<li>역할 부여</li>
<li>AI 사고 유도 및 결과 검증</li>
</ul>
<p>패턴 언어는 개발자들이 이제까지 만들어온 용어들(디자인 패턴과 같은)과 비슷한 용어였다. 나는 기존에 GPT와 같은 AI에게 프로젝트 코드의 도움을 받을 때 장황한 설명으로 만들어줄 구조에 대해서 설명해왔다. 설명이 길어질수록 결과물이 좋아지다가 어느 시점 이상으로 길어지면 결과물에 대한 오류가 줄어들지 않았는데, 이런 점들을 보완할 수 있는 방법들을 알려주었다. 그리고 나에게 개발 방법론과 같은 이론 공부의 중요성 또한 잘 알려주었다.</p>
<p>MCP에 대해서는 의외로 기본적인 설명에 굉장히 담백한 원리를 알려주었다. 많은 유튜브나 책들에서 MCP를 만능 기술로 알려주는 대신 이 책에서는 현재 부상하고 있는 기술이지만 결국 AI에게 추가 실패 지점을 만들 수 있는 점을 명시하며 CLI를 기반으로 MCP를 보조 수단으로 사용할 것을 권해왔다. 이 책을 읽기 전에는 공감하지 못할 내용이었지만 여기까지 책을 읽어온 나는 매우 완벽한 설명이라고 생각한다.
그렇다고 MCP에 부정적인 입장을 취하는 것은 아니었다. MCP는 발전하고 있는 기술이고 CLI에서 다루지 못할 기술도 점점 다루기 시작하며 언젠가 MCP를 적극 활용할 시대가 올 것이라고 얘기도 하였다. 그리고 많은 양의 양질의 MCP 서버들을 추천해주었다. 나는 눈으로 보고 넘겼지만 MCP를 적극 도입하고 있는 사람이라면 굉장히 만족할만한 정보들이라고 생각한다.</p>
<p>앞에서 AI의 코드를 믿지 않기에 리뷰가 매우 중요하다고 말한 것처럼 리뷰에 대해서 자세하게 다룬다. 굉장히 마음에 들었던 점이 리뷰가 형식적으로만 이루어지고 제대로 활용되지 못한 점을 꼬집으면서 시작하고 있다. &#39;현실에서 리뷰는 속도와 효율의 압박과 동료의 코드를 지적하는 심리적 요인에 의해 제대로 시행되기 힘들다.&#39;
리뷰를 단순한 방법으로 설명하지 않고 인간의 심리학과 연결하여 설명하여 어떻게 하면 좋은 리뷰 문화를 만들 수 있을 지 고민한 흔적이 보여 좋았다. 또한 AI가 가지는 강점을 활용한 리뷰 방식을 다룬 점도 마음에 들었다. 또한 리뷰를 어느 시점에서 하는 것이 좋은 지 각각의 체크 포인트들(설계 리뷰, 코드 리뷰)에 맞춘 리뷰에 대한 설명 또한 마음에 들었다.
책 저자는 이 리뷰에 대해서 굉장히 중요성을 강조하며 &#39;리뷰 잘하는 개발자&#39;가 곧 &#39;성공적인 개발자&#39;인 시대가 열리고 있다고 한다</p>
<p>사실 이 책의 내용을 전부 이해하고 적용하는 것은 완전 무리였다. 거기다가 실전 프로젝트 실습 파트는 실습 자체는 해보았지만 지금은 내 실력으로는 충분히 활용하기 어려웠다. LLM의 원리나 책에서 도구들의 세세한 차이점들을 설명하는 부분들도 실제 내 삶에 적용하기에는 어려움이 있었다.</p>
<p>하지만 이 한계는 나의 현재 경험치 때문이지, 책의 문제는 아니라고 느꼈다.</p>
<p>이 책을 읽으며 많은 것을 얻었고 많은 것을 느끼게 되었다. 특히 <strong>규칙을 세우고, 요구사항을 만들고, 설계 문서를 기반으로 방향을 정하는 개발자의 역할</strong>은 앞으로 내가 어떤 개발자가 되어야 할 지에 대한 큰 기준이 되었다. 
또한 AI시대에 맞춰 개발자가 어떻게 반응하고 어떻게 해야 살아남을 수 있는지 깊이 생각하게 되었다.</p>
<p>이 책은 나에게 중요한 &#39;방향&#39;을 남겨준만큼 기회가 된다면 읽어보기를 추천한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Do it! 리눅스 입문]]></title>
            <link>https://velog.io/@meteor_control0/Do-it-%EB%A6%AC%EB%88%85%EC%8A%A4-%EC%9E%85%EB%AC%B8</link>
            <guid>https://velog.io/@meteor_control0/Do-it-%EB%A6%AC%EB%88%85%EC%8A%A4-%EC%9E%85%EB%AC%B8</guid>
            <pubDate>Tue, 06 Jan 2026 10:34:56 GMT</pubDate>
            <description><![CDATA[<p>서평단에 선정되어 책을 제공받았습니다.</p>
<p>개발자로 리눅스는 언제나 가까이해야 하는 존재지만 거부감이 드는 건 어쩔 수 없다. Mac이나 Window의 GUI 환경에서 벗어나 명령어 위주의 CLI 환경에 익숙해지는 일은 쉽지 않다. &#39;매번 공부해야지 해야지&#39; 하다가 미루게 되고, 막상 필요할 때는 GPT에 의지하게 되었다.</p>
<p>리눅스 관련 도서나 강의를 통해 공부하게 되면 익숙하지 않은 내용과 개발 환경의 2가지 어려움을 동시에 겪는다. 그래서 나는 책의 내용이 어려워지기 시작하면 굉장히 힘들었다. 그래서 항상 굉장히 친절한 설명이 있는 책을 원하고 있었는데 이 책이 출간되어 기쁘다.</p>
<hr>
<p>책은 기초적인 부분을 최대한 쉽고 친절하게 설명한다. 그리고 발전할 여지가 있는 개념적인 부분은 최소한으로 줄인다. 이런 부분이 굉장히 마음에 들었다. 기존의 책이나 강의들은 개념적인 부분을 많이 다루어 큰 밑그림을 그리는 데 도움을 주지만 입문자인 나에게는 어려운 방식이었다. 하지만 이 책을 통해서는 정말 최소한의 기초를 쌓아 올릴 기회가 되어 좋았다.</p>
<p>실습 부분도 굉장히 친절하다. 다른 책들에 비해서 개발 환경을 만드는 부분도 신경 쓴 것이 보인다. 그리고 우분투와 로키의 2가지 환경을 모두 경험할 수 있도록 배려한 점도 마음에 든다. 명령어 실습 부분에서도 너무 깊지 않게 들어간다. 나처럼 리눅스 입문자라면 기본적인 사용법을 익히는데 매우 적절한 깊이라고 생각한다.</p>
<p>리눅스를 배우더라도 당장 내가 만들 수 있는 프로젝트를 해보는 일은 거의 없다고 생각했다. 나의 경우에는 리눅스를 사용하는 일이 AWS에 Docker를 활용해 이미 만들어진 프로젝트를 배포하는 일이 대부분이 었다. 그래서 내가 리눅스 환경에서 직접 웹 서버나 파일 서버를 만들어 보는 일은 신선한 경험이었다. 리눅스 환경에서 프로그램이나 서버를 만들어 보는 경험을 해보고 싶다면 적극 추천한다.</p>
<p>리눅스 관련 도서에서 꼭 다루는 부분이 라즈베리파이와 클라우드(AWS)에서 실습인데, 개인적으로 뺐으면 좋겠다고 생각하는 부분이다. 이런 부분에서 다뤄야하는 기초적인 내용(실습 준비)의 양이 적지 않기에 이런 부분만 설명하다 보면 막상 공부하는 부분은 매우 적다. 그리고 막상 이런 부분을 공부하고자 한다면 입문서가 아닌 전문적으로 다루는 책을 찾게 되기에 좀 아쉽다. 개인적으로 웹 서버와 파일 서버를 다루는 프로젝트를 2~3개 정도 더 다뤘으면 좋았을 거라 생각한다.</p>
<hr>
<p>덜어냄의 미학이라는 말이 잘 어울리는 책이라고 생각한다. 책의 내용은 다른 리눅스 입문서처럼 평이하다. 이론이나 실습 부분도 다른 두꺼운 책들에 비하면 적다고 할 수 있다. 하지만 입문자들에게 필요한 아주 적절한 깊이와 실습량을 제공한다. 그리고 리눅스로 프로젝트를 해볼 수 있는 경험을 제공해 입문자에게 리눅스를 활용할 수 있는 길을 가르쳐준다.</p>
<p>이미 리눅스에 익숙하거나 숙련된 분들이라면 슥 보고 넘어갈 내용이 많다고 생각하기에 추천하지 않는다. 그리고 깊은 이론적인 지식을 원하시는 분들에게는 책이 가볍게 느껴질 수 있다고 생각한다.</p>
<hr>
<p> 리눅스에 대해 처음 입문한다면 다른 책들이 아닌 이 책을 매우 추천한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스브링 부트 개발자 온보딩 가이드]]></title>
            <link>https://velog.io/@meteor_control0/%EC%8A%A4%EB%B8%8C%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%98%A8%EB%B3%B4%EB%94%A9-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%84%9C%ED%8F%89</link>
            <guid>https://velog.io/@meteor_control0/%EC%8A%A4%EB%B8%8C%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%98%A8%EB%B3%B4%EB%94%A9-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%84%9C%ED%8F%89</guid>
            <pubDate>Mon, 05 Jan 2026 18:23:23 GMT</pubDate>
            <description><![CDATA[<p>YES24 리뷰어클럽 서평단 자격으로 작성한 리뷰입니다</p>
<p>국내에서 가장 많이 다루는 기술이 스프링인만큼 책도 상당히 많다. 여기서 대부분의 스프링 책들은 2가지 종류로 나뉜다. 입문하는 사람들을 위한 입문서와 깊은 지식을 위한 이론서로 나뉜다. 그래서 초심자를 막 벗어난 사람들에게 와닿는 스프링 책이 없었다. 이 책이 나오기 전까지.</p>
<p>온보딩은 신입 직원이 회사에 적응해 한 명의 팀원으로 거듭나는 과정을 말한다. 제목처럼 이 책은 스프링 입문자(신입)에서 벗어나는 길을 함께한다. 실무에서 접할 수 있는 개발 환경을 기반으로 스프링 프로젝트를 처음부터 끝까지 만들 수 있도록 돕는다.</p>
<hr>
<p>책의 시작부터 실제 개발 환경과 비슷한 환경에서 프로젝트를 진행할 수 있도록 노력한 점이 마음에 들었다. 실제 개발 환경에서는 대부분 프로젝트가 AWS와 같은 클라우드에서 배포되고 있기에 리눅스 기반 환경이다. 이런 리눅스 기반 명령어 환경을 경험할 수 있도록 책에서는 WSL과 도커를 활용한다. 단순하게 스프링 프로젝트를 시작하는 것부터 시작하여 도커를 활용해 MySQL 서버를 띄우거나 테스트에서만 활용하는 테스트 전용 컨테이너를 띄우는 등 다양한 경험을 제공한다.</p>
<p>책은 기본적인 이론을 설명하는데 시간을 많이 할애하지 않는다. 대부분의 책들은 이론에 집중하고 코드 부분에 대한 설명이 짧다. 거기다가 기본적인 이론 부분을 설명하기 위한 프로젝트를 만들다 보니 실제 개발에서 잘 사용되지 않는 코드나 구조를 가지는 경우가 많다. 그래서 기본적인 스프링의 기반 이론 지식은 배우지만 실제 사용하는 코드에 대해 제대로 배우지 못하는 경우가 많다.</p>
<p>하지만 이 책은 기본적인 이론 부분 보다 실제 코드를 설명하는 부분에 집중한다. 기능을 만들기 위한 코드를 배우기에 책을 읽어가며 함께 프로젝트를 완성한다면, 자연스럽게 스프링을 사용하는 사용법에 대해 배울 수 있다. 내가 원하는 기능을 만들기 위해 어떤 코드를 사용해야 하는지, 이를 위해 필요한 지식이 무엇인지 자연스럽게 배워 나갈 수 있다.</p>
<p>그러면 &#39;코드에 너무 집중해서 이론을 부분을 버리는 것인가?&#39;라는 생각할 수 있다. 그러나 책에서 &#39;여기서 잠깐&#39;이라는 작은 코너 부분을 활용하여 부족한 이론 지식을 채워주기에 걱정할 필요가 없다. 개인적으로 이런 책의 구성이 마음에 들었다. 코드 위주로 진행하다 이론 부분이 나오면 읽는 독자의 집중이 이론 부분으로 치우칠 수도 있다 생각한다. 하지만 따로 부록처럼 빼서 설명는 구조를 선택해 독자의 집중력이 분산되지 않도록 유도하는 점이 매우 마음에 들었다.</p>
<p>GraphQL을 다루는 부분이 있는데 나처럼 익숙하지 않은 사람에게는 어렵게 다가올 수 있다고 생각한다. 하지만 개발자에게 접해본 경험이 있는 기술 스택이 늘어나는 것은 좋은 경험이라고 생각하기에, 나처럼 GraphQL을 모른다고 이 책을 읽는데 주저하지 않았으면 좋겠다. 나도 기반 지식이 전혀 없었기에 헤매는 부분이 있었지만 책을 읽고 나니 도전할만한 가치가 있었다고 생각한다.</p>
<hr>
<p>이 책을 통해 스프링에 입문하려고 한다면 말리고 싶다. 입문자를 위한 책이나 강의를 보고 프로젝트를 1 ~ 2개 경험을 한 후에 보는 것을 추천한다. 내가 원하는 기능을 만들기 위해 스프링의 사용 방법을 배우기를 원하는 사람이라면 적극 추천한다.</p>
<p>개인적인 아쉬운 점은 책이 얇다는 거다. 좀 더 넓은 내용을 다루거나 깊게 다루어 줬으면 좋았을 거라 생각한다. 또, 초보자에게는 아쉬운 점으로 느껴질 수 있는 건 버전 관리 부분이다. 조금만 숙련된 개발자라면 금방 대처할 수 있지만 초보자라면 약간 헤맬 수 있다고 생각한다.</p>
<p>스프링 관련 개발에 종사하고 있는 주니어 개발자라면 꼭 읽어보길 바란다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발자는 글을 못 쓴다고요?]]></title>
            <link>https://velog.io/@meteor_control0/%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%8A%94-%EA%B8%80%EC%9D%84-%EB%AA%BB-%EC%93%B4%EB%8B%A4%EA%B3%A0%EC%9A%94-%EC%84%9C%ED%8F%89</link>
            <guid>https://velog.io/@meteor_control0/%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%8A%94-%EA%B8%80%EC%9D%84-%EB%AA%BB-%EC%93%B4%EB%8B%A4%EA%B3%A0%EC%9A%94-%EC%84%9C%ED%8F%89</guid>
            <pubDate>Thu, 04 Dec 2025 14:51:54 GMT</pubDate>
            <description><![CDATA[<p>제이펍 서평단 자격으로 도서를 제공받고 작성한 리뷰입니다</p>
<p>신입 개발자인 나는 글을 쓸 일이 생기면 막막해진다.
잘 작성된 문서나 글을 많이 접해볼 일이 많지 않았기 때문이다.
이 책은 이런 나에게 최소한의 글쓰기 기준을 알려주었다.</p>
<p>나 같은 신입 개발자들은 잘 작성된 글을 접하거나 써본 경험이 적기 때문에 글쓰기 기준을 잡기 어렵다.
그래서 자연스럽게 인터넷 예시나 AI에 의존하게 되지만 이게 잘 쓴 글인지 아닌지 스스로 판단하기 어렵다.
이 책은 이런 상황에서 바로 참고할 수 있는 예시와 명확한 기준을 제공한다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/62960698-1925-49c8-9910-d00f5af73eb8/image.jpg" alt=""></p>
<p>저자는 개발자가 글을 잘 쓰지 못하는 이유를 제대로 된 글쓰기를 경험할 기회가 적기 때문이라고 한다. 
글을 쓸 일은 많지만 기준을 가지고 시간을 충분히 들여 퇴고하는 과정이 부족하다는 것이 점을 문제로 지적한다.
그래서 책에서는 글쓰기에서 ‘정확성’, ‘간결성’, ‘완결성’을 갖추는 것이 중요하다고 강조하며 사용할 수 있는 기술을 알려준다.
또한 글을 쓸 때는 목적과 독자를 명확하게 아는 것이 중요하다고 지속적으로 언급한다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/bacf2f81-4f0e-48fe-8a92-d8fa39f81d29/image.jpg" alt=""></p>
<p>이 책의 강점은 바로 사용할 수 있는 예시가 많다는 점이다.
신입 개발자인 나는 커밋 메시지처럼 자주 쓰지만 기준을 잡기 어려운 글은 쓸 때마다 막막했기 때문에 이 부분이 가장 도움이 되었다.
API 주석이나 리드미처럼 구조를 잡기 힘든 글도 어떻게 시작하고 어떤 방향으로 나아가야 할지 알려준다.</p>
<p>이 책을 읽고 난 후 각 글의 종류마다 어떤 기준을 적용할지 알게 되어 글쓰기 부담이 많이 줄었다. 
글을 작성하는 중간에도 길을 잃지 않게 되었다.
또 글에 대한 기준이 생기니 AI를 통해 글을 만들 때도 더 정확한 프롬프트를 넣어 더 좋은 결과물을 얻을 수 있게 되었다.
특히 AI가 만들어 준 문장에서 글의 성격과 맞지 않은 부분을 걸러낼 수 있게 된 점이 가장 큰 변화다.
이전에는 AI가 만들어 준 문장을 그대로 가져다 쓰고 앞뒤의 흐름을 맞추려는 노력을 했기에 중간 중간 흐름에 맞지 않은 문장이 많이 있었던 것 같다.
돌이켜보면 부끄러운 일이다.</p>
<p>이 책은 실무 예시 위주라 글쓰기 이론 자체는 깊지 않다.
하지만 나에게 이 부분은 전혀 단점으로 다가오지 않았다. 오히려 장점에 가까웠다.
다만 나 포함 많은 개발자들이 글을 작성할 때 AI를 많이 활용하기에 AI와 함께 글을 쓰는 과정을 조금 다뤘으면 어땠을까 하는 아쉬움이 남았다.</p>
<p>이 책은 나 같은 신입 개발자나 글쓰기 기준이 부족한 사람에게는 특히 도움이 될 것이다.
반대로 깊은 글쓰기 이론을 배우고 싶거나 AI 기반 글쓰기 전략을 찾는 독자에게는 맞지 않을 수 있다.</p>
<p>이 책은 글쓰기 기준을 바로 적용할 수 있도록 도와준다.
짧은 글부터 문서까지 어떤 식으로 써야 하는지 알려주는 선생님 같은 느낌을 받았다.
그래서 책장보다는 책상 위에 두고 자주 펼쳐보고 싶은 책이라고 느꼈다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MCP 너는 누구냐]]></title>
            <link>https://velog.io/@meteor_control0/MCP-%EB%84%88%EB%8A%94-%EB%88%84%EA%B5%AC%EB%83%90</link>
            <guid>https://velog.io/@meteor_control0/MCP-%EB%84%88%EB%8A%94-%EB%88%84%EA%B5%AC%EB%83%90</guid>
            <pubDate>Thu, 24 Jul 2025 00:20:48 GMT</pubDate>
            <description><![CDATA[<h1 id="mcp란">MCP란</h1>
<h2 id="요즘-트렌드">요즘 트렌드?</h2>
<p>최근 유튜브나 커뮤니티에서 가장 핫한 주제는 MCP라고 생각한다. 과장 조금 보태면 AI로 모든 것을 할 수 있다는 내용이다. 그래서 궁금했다. 최근 바이브 코딩이라고 AI에게 모든 코딩을 맡기는 것도 떠오르는데 MCP까지 사용한다면 키보드로 코드를 입력하지 않고 복사/붙여넣기 조차 하지 않고도 개발을 할 수 있지 않을까 궁금했다.</p>
<h2 id="mcp는-뭘까">MCP는 뭘까?</h2>
<p>MCP는 Model Context Protocol의 약어다. 사실 MCP를 하나의 도구나 프레임워크라고 생각했는데 그런 것은 아니고 하나의 프로토콜이었다. 외부 Tool, Prompt, Resource와 통신할 수 있도록 하는 컨텍스트 상호작용 프로토콜이다. LLM이 실행할 작업을 명시적으로 정의하고, 툴 선택과 실행 흐름을 통제하며, 자원 접근 구조를 포함한다는 의미인데 처음에는 읽어도 잘 모르겠었다.</p>
<h3 id="누가-만들었어">누가 만들었어?</h3>
<p><img src="https://cdn.prod.website-files.com/67ce28cfec624e2b733f8a52/68309ab48369f7ad9b4a40e1_open-graph.jpg" alt=""></p>
<p>Protocol이라고 해서 당연히 권위 높은 단체에서 만들었을 줄 알았는데 의외로 Claude를 서비스하고 있는 기업인 Anthropic 발표했다.</p>
<h2 id="역사를-이해하자">역사를 이해하자</h2>
<p>MCP에 대해서 설명하다가 뜬금없다고 생각할 수 있지만 현재 AI 역사를 이해하는 쪽이 MCP를 설명하는 가장 좋은 방법이라고 생각한다.</p>
<h3 id="ai는-chatbot">AI는 ChatBot</h3>
<p><img src="https://blog.kakaocdn.net/dna/t39aD/btsMbweJ1zt/AAAAAAAAAAAAAAAAAAAAAC80KKchw-zu6R8kuh9G7Q_vD0dDEaz9y4emml2vKarA/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1753973999&allow_ip=&allow_referer=&signature=97i9tZ8A0gwi74W118tiXtZ0pNs%3D" alt=""></p>
<p>우리가 가장 쉽게 접할 수 있는 AI는 알다시피 ChatGPT, 제미나이와 같은 것들이 있다. 그런데 이 AI는 분명 똑똑하고 일도 잘하는 것 같은데, 막상 내 일을 대신 해주진 못한다.</p>
<p>예를 들어, 내가 어떤 폴더의 파일 리스트를 주고 이 파일들을 분류해달라고 하면 다음과 같은 대답을 줄것이다.</p>
<table>
<thead>
<tr>
<th>파일명</th>
<th>용도 추정</th>
<th>분류 기준 예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>resume_lee.pdf</code></td>
<td>이력서</td>
<td><code>documents/resumes</code></td>
</tr>
<tr>
<td><code>invoice_2024_03.xlsx</code></td>
<td>청구서</td>
<td><code>documents/invoices</code></td>
</tr>
<tr>
<td><code>cat_picture.jpg</code></td>
<td>고양이 사진</td>
<td><code>media/images</code></td>
</tr>
<tr>
<td><code>project_plan_v2.docx</code></td>
<td>문서</td>
<td><code>documents/plans</code></td>
</tr>
<tr>
<td><code>design_draft.sketch</code></td>
<td>디자인 파일</td>
<td><code>designs/</code></td>
</tr>
<tr>
<td><code>main.py</code></td>
<td>소스코드</td>
<td><code>code/python</code></td>
</tr>
<tr>
<td><code>script.js</code></td>
<td>자바스크립트 코드</td>
<td><code>code/javascript</code></td>
</tr>
<tr>
<td><code>logo_final.ai</code></td>
<td>벡터 이미지</td>
<td><code>designs/vectors</code></td>
</tr>
<tr>
<td><code>user_data.json</code></td>
<td>데이터 파일</td>
<td><code>data/json</code></td>
</tr>
<tr>
<td><code>backup_2023_12_01.zip</code></td>
<td>백업</td>
<td><code>archives/</code></td>
</tr>
<tr>
<td><code>meeting_notes.txt</code></td>
<td>회의 기록</td>
<td><code>documents/notes</code></td>
</tr>
<tr>
<td><code>presentation_v1.pptx</code></td>
<td>발표자료</td>
<td><code>documents/presentations</code></td>
</tr>
<tr>
<td><code>budget_2024.xlsx</code></td>
<td>예산</td>
<td><code>documents/finance</code></td>
</tr>
<tr>
<td><code>error_log_20240701.log</code></td>
<td>로그 파일</td>
<td><code>logs/</code></td>
</tr>
<tr>
<td><code>README.md</code></td>
<td>프로젝트 설명</td>
<td><code>code/docs</code></td>
</tr>
<tr>
<td><code>thumbnail.png</code></td>
<td>썸네일 이미지</td>
<td><code>media/images</code></td>
</tr>
<tr>
<td><code>music_sample.mp3</code></td>
<td>음원 파일</td>
<td><code>media/audio</code></td>
</tr>
<tr>
<td><code>app.apk</code></td>
<td>안드로이드 앱</td>
<td><code>binaries/android</code></td>
</tr>
<tr>
<td><code>certificate.pem</code></td>
<td>인증서</td>
<td><code>security/certs</code></td>
</tr>
<tr>
<td><code>video_intro.mp4</code></td>
<td>영상 파일</td>
<td><code>media/videos</code></td>
</tr>
</tbody></table>
<p>만약 내가 이 20개의 파일을 일일이 분류한다면 많은 시간을 사용할 것이고 분류하는 과정에서 비슷한 유형의 폴더를 만드는 등 많은 어려움을 겪을 것이다.</p>
<p>하지만 AI를 이용한다면 대화 한번, 5초면 해결할 수 있다.</p>
<p>그런데 이 파일들을 담을 폴더를 만들고 파일을 옮기는 일은 누가 할까? 라는 대답은 여지 없이 <strong>나(사용자)</strong>다</p>
<p>그렇다. AI는 결국 &quot;이런 방식으로 일을 처리하면 됩니다&quot;라는 대답을 해주는 하나의 ChatBot에 불과하다.</p>
<h3 id="챗봇을-넘어서">챗봇을 넘어서</h3>
<p>개발자들은 폴더 생성/파일 이동과 같은 행동들 또한 AI가 대신 해주길 바랬다. 그래서 AI에게 여러 도구(Tool)들을 사용할 수 있도록 하고 싶었다. 따라서 AI에게 Tool의 기능을 알려주고 Tool을 직접 실행해주는 중간 관리자 같은 역할의 프로그램이 필요했다. 바로 여기서 나온 것이 AutoGPT와 같은 프로젝트다.</p>
<p>개발자들은 AutoGPT와 같은 프로젝트를 발전시켜 AI Agent를 만들어냈다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/64f46754-9ae4-4719-a58f-e99c2d81df93/image.png" alt=""></p>
<p>AI Agent는 사용자의 목표(여기서는 파일 분류)와 같은 목표를 이루기 위해 어떤 Tool을 사용해야하는지(파일 이동, 폴더 생성)과 같이 판단/계획하고, 어떤 Tool을 썼는지 이유와 결과를 기억해 다음 행동에 반영하도록 상태를 기억하는 등 지휘자에 가까운 역할을 한다.</p>
<blockquote>
<p>AI Agent의 루프 : 목표 → 계획 수립 → 도구 실행 → 결과 해석 → 반복</p>
</blockquote>
<p>물론 이것 외에도 다양한 많은 일들을 한다. 내부적으로 LLM과의 소통하는 과정에서 오류가 발생하면 오류 발생을 인지하고 다시 LLM에게 프롬프트를 보내는 거나 시스템 프롬프트와 사용자 프롬프트를 결합하는 등 보이지 않는 프로세스들을 실행한다.</p>
<h3 id="그러면-ai-agent가-주목받지-mcp가-왜-주목-받아">그러면 AI Agent가 주목받지 MCP가 왜 주목 받아?</h3>
<p>나는 처음에 이해가 가지 않았다. ChatGPT가 나의 질문을 받아 &#39;이렇게 하세요&#39;가 아닌 실제 실행까지 해주는 역할을 AI Agent가 가능하게 만들어줬다면 AI Agent가 주목 더 유명해야지, MCP가 더 주목을 받는 것인지 이해가 가지 않았다.</p>
<h4 id="ai-agent의-tool-사용의-변화">AI Agent의 Tool 사용의 변화</h4>
<p>일단 조금 더 역사를 되짚어보자.</p>
<p>AI Agent가 Tool을 사용하는 방식은 Agent와 Tool을 같은 프로그램에 작성하여 직접 함수를 호출하는 방식을 사용했다. 대부분 프로그래밍에서는 이런 강결합은 문제들이 없을 리가 없다.</p>
<p>예를 들어 웹 검색 도구나 파일 시스템 제어와 같은 도구들은 다양한 Agent에서 사용될 것이다. Agent 만들 때마다 같은 Tool 코드를 똑같이 복사해서 집어 넣는 작업을 한다? Agent를 만들 때마다 중복되는 코드가 넘쳐날 것이고 굉장히 복잡한 구조를 가지는 건 물론 수정을 할 일이 생긴다? 모든 에이전트마다 중복된 코드를 고쳐야 할 것이다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/bbada819-1821-4a14-9209-7bdd5be6808b/image.png" alt=""></p>
<p>따라서 사람들은 Tool을 하나의 서비스로 떼어내어 운영하고 Agent들이 하나의 Tool 서비스에 요청을 보내 Tool을 실행하는 방식으로 전환하게 되었다.</p>
<h4 id="tool-만들기가-주목-받은거야">Tool 만들기가 주목 받은거야?</h4>
<p>내가 새로운 Tool 서비스를 만들었다고 생각해보자. 그런데 AI Agent는 다양한 곳에서 개발하고 있고 각 Agent들이 Tool 서비스에 연결하는 방식과 규칙은 통일되지 않았다. 왜? AI 개발하는 회사들이 각자 자신들의 방식으로 Agent들을 개발해왔고 Tool도 연결해서 사용해왔으니까. 결국 개발자들은 LangChain, AutoGen 등 많은 Agent에 연결할 때마다 추가적인 작업을 해야만 했다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/fb162aee-4ebf-42f2-b145-ba6a4b7b8d2d/image.png" alt=""></p>
<p>결국 Tool 서비스를 만드는 개발자나 회사는 서비스를 만들기 위해서는 Tool에대한 공부뿐만 아니라 각 Agent마다 Tool을 연결하는 방법을 공부하거나 어댑터를 만드는 데 많은 노력과 시간을 써야만 했다. 그래서 이런 Tool 서비스들은 큰 회사에서 많이 만들고 일반 개발자나 작은 회사들에게는 주목받기 어려웠다.</p>
<h3 id="조용히-등장한-mcp">조용히 등장한 MCP</h3>
<p>여기서 등장한 것이 Model Context Protocol이다. 큰 의미에서는 AI 모델이 외부 데이터 소스나 도구에 안전하게 연결할 수 있도록 하는 표준화된 프로토콜이다. Agent와 Tool 서비스 간에 어떤 방식으로 통신하면 좋을 지 정한 것도 MCP에 포함된다. MCP는 통신 규칙 말고도 MCP Host, MCP Client, MCP Server와 같은 여러가지 개념도 가지고 있다. 그래서 사람들이 MCP라는 단어를 여러가지 의미를 담아 쓰고 약간 혼용해서 쓰기도 한다.</p>
<p>지금 사람들이 가장 많이 말하는 MCP는 MCP server에 가깝고 MCP Server는 Tool 서비스라고 생각하면 여기서는 편할 것이다. 물론 여기서 설명을 편하게 하고자 이렇게 설명한 것이고 MCP Server가 하는 일은 굉장히 많다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/d878bd9f-bbf1-4846-acad-c0dc490f38d4/image.png" alt=""></p>
<p>하지만 처음 MCP는 지금의 인기에 비해 인기가 없었다. 당연하다. 규칙이나 규약은 다른 사람들이 모두 지켜야 의미가 있는 것이지. 막말로 내가 &#39;앞으로 개발자들은 Git commit message 마지막에 냥을 붙인다&#39;라고 규칙을 정하더라도 지키는 사람이 없다면 아무런 의미가 없는 규칙이나 다름없다.</p>
<h4 id="mcp의-구원자-cursor">MCP의 구원자 Cursor</h4>
<p><img src="https://cursor.com/en/twitter-image.png?375711d39ab904b7" alt="커서"></p>
<p>여기서 등장한게 바로 Cursor다. 사용자가 굉장히 많은 AI Code Editor에서 MCP를 차용하겠다고 한 것이다. 그렇게 Cursor Editor 프로그램에서 MCP Server를 쉽게 연결할 수 있어졌다.</p>
<p>AI Code Editor는 현재 당연히 개발자들 사이에서 가장 많이 사용되는 Editor로 MCP Server를 추가할 수 있게 되자 자연스럽게 많은 개발자들이 관심을 가지게 되었다. 관심은 곧 여러 MCP Server들로 변해갔다. 개발자들이 점점 더 많은 MCP Server를 만들어내자 다양한 회사들(Notion, Slack 등)도 자신들의 서비스를 붙인 MCP Server들을 만들어 제공하게 되었다.</p>
<h2 id="이제는-mcp-알아야겠지">이제는 MCP 알아야겠지?</h2>
<p>많은 회사들이 MCP Server를 제공하자 자연스럽게 AI 회사들도 자신들의 AI Agent에 MCP Server를 연결할 수 있도록 변해갔다. 그렇게 MCP Server를 만드는 개발자/회사들과 MCP Server를 사용할 수 있는 다양한 AI들이 생겨나자 MCP 생태계가 만들어졌고 급속도로 주목받게 되었다.</p>
<p><img src="https://static.claudemcp.com/images/blog/what-is-mcp.png" alt="MCP"></p>
<p><strong>이제 우리 개발자들이 MCP를 지키는 Tool 서비스, MCP Server를 만들면 대부분의 AI 서비스와 연결할 수 있기에 별도의 연결 방법을 공부하거나 어댑터를 만들 필요가 없어진 것이다.</strong></p>
<p>지금도 많은 개발자들이 MCP Server를 만들고 있다. 다른 개발자, 회사들이 만든 MCP Server에 대해서 궁금하다면 <a href="https://smithery.ai/">https://smithery.ai/</a> 여기서 확인해볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[소플의 처음 만난 AWS]]></title>
            <link>https://velog.io/@meteor_control0/%EC%86%8C%ED%94%8C%EC%9D%98-%EC%B2%98%EC%9D%8C-%EB%A7%8C%EB%82%9C-AWS</link>
            <guid>https://velog.io/@meteor_control0/%EC%86%8C%ED%94%8C%EC%9D%98-%EC%B2%98%EC%9D%8C-%EB%A7%8C%EB%82%9C-AWS</guid>
            <pubDate>Wed, 08 Jan 2025 01:06:25 GMT</pubDate>
            <description><![CDATA[<p>YES24 리뷰어클럽 서평단 자격으로 작성한 리뷰입니다</p>
<hr>
<p>AWS를 이용해 본 사람도 이용해 보지 않은 사람도 있을 것이다. 나는 이 책을 읽기 전에 AWS를 이용해 본 적이 있다. 하지만 이용해 본 적 있다는 말이 무색하게 강의에서 시키는 대로 그저 마우스 버튼을 클릭했을 뿐 AWS라는 서비스를 이용했다는 느낌은 상당히 적었다.</p>
<p>AWS를 사용하지만, 지식이 전무하다는 생각이 들어 여러 가지 방면으로 공부를 시도했었다. 책을 보거나 짧은 유튜브 강의를 보거나 하며 나름대로 공부를 시도해 본 적이 있다. 하지만 이런 공부 방식을 시도하면서 드는 생각은 하나였다.</p>
<p>초보자를 위한 길은 많은데 입문자를 위한 길은 없나?</p>
<p>만약, 나처럼 AWS나 클라우드 컴퓨팅에 대한 지식이 전무하고 처음 이런 지식에 접근한다면 이 책을 추천해 주고 싶다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/460760bd-c44d-48e2-bafe-2a1b7a707341/image.png" alt=""></p>
<hr>
<h1 id="내용">내용</h1>
<p>책은 4파트로 나뉜다고 볼 수 있다.</p>
<h2 id="클라우드-컴퓨팅과-aws-기초">클라우드 컴퓨팅과 AWS 기초</h2>
<p>먼저 클라우드 컴퓨팅과 AWS가 무엇인지 설명한다. 설명의 깊이는 깊지 않다. 오히려 소개에 가깝다고 생각한다. 이런 기초 지식은 중요하고 깊이 알수록 좋은 것은 사실이지만 나처럼 AWS를 처음 공부한다면 간단한 소개처럼 이론 부분을 설명한 것이 오히려 좋은 점으로 다가왔다.</p>
<h2 id="aws-핵심-서비스-이해-및-실습">AWS 핵심 서비스 이해 및 실습</h2>
<p>원론적인 얘기로만 AWS의 핵심 서비스를 설명하지 않고 현실에서 있을 법한 예시로 독자들의 이해를 도와준다. 특히 선생님이 빵을 나눠주는 이야기를 사용하여 ELB를 설명한 예시는 굉장히 좋았던 예시로 기억에 남는다. 이후 실습으로 기본적인 서비스를 이용하는 방법에 대해 배운다. 실습 부분이 굉장히 친절하고 꼼꼼하다. AWS의 지식이 전혀 없더라도 실습을 이어 나가는 데 문제가 없다. 그리고 독자들이 실수할 수 있는 부분도 넘어가지 않고 짚어준다.</p>
<p>개인적인 이야기지만 과거 AWS를 다뤘을 때 탄력적인 IP를 할당하지 않고 그냥 생성만 하고 방치한 적이 있었다. IP를 할당하는 행동이 하나의 서비스를 이용하는 행동이라 생각해 요금이 나가지 않을까 걱정되었기에 할당하지 않고 방치했었다. 하지만 오히려 AWS에서는 IP를 할당하지 않는 행동이 요금이 나가게 측정되어 있어 천원 정도의 돈을 지출하게 되었다. 물론 적은 돈이지만 내가 AWS를 사용하는데 굉장히 조심스럽게 행동하게 만들었고 거부감을 가지게 했었다. 이처럼 책은 사용자의 실수로 불필요한 지출이 나갈 수 있는 부분도 잘 짚어준다.</p>
<p>금전적인 측면에서 어떤 서비스를 이용하는 것이 이득인지도 잘 알려준다. 나의 경우엔 지금 당장 요금이 들어가는 서비스를 이용하지 않겠지만 만약 이 책을 통해 서비스를 실제로 운영을 고민할 사람이라면 큰 도움이 될 거로 생각한다.</p>
<p>각 서비스 간의 유기적인 결합을 잘 보여준다. AWS의 서비스 중 하나의 서비스를 실행하는 것은 조금만 공부하면 알 수 있는 내용일지 모른다. 책은 이런 별개의 서비스를 어떻게 연결하여 사용하면 좋을지 잘 알려준다. 각 서비스가 어떤 위치에서 작용하고 어떤 서비스와 결합하여 사용하면 좋은지 잘 알려준다.</p>
<h2 id="심화-서비스와-운영-관리">심화 서비스와 운영 관리</h2>
<p>AWS의 심화 부분을 다루는 내용인데, 개인적으로 &#39;이런 서비스도 있어요&#39;와 같은 소개로 다가왔다. 입문자들을 위한 책인 만큼 AWS의 심화 부분에 어떤 것들이 있고 간단한 사용 방법을 알려준다. 이 부분을 읽으며 이런 심화 서비스를 당장 사용하거나 이용해 보겠다는 생각이 들지는 않았다. 하지만 어떤 도구들이 있는지 설명해 주는 부분은 굉장히 좋다고 생각한다. 내가 앞으로 AWS를 이용하다 보면 특정 상황에 필요한 무언가가 있다면 좋겠다는 생각을 분명 하게 될 것이다. 하지만 어떤 도구가 있는 지 모르는 상태로 상황을 해결하는 것과 어떤 도구들이 있는 지 알고 있는 상태에서 상황을 해결하려는 것은 매우 큰 차이를 만들 것이다. 어려운 이론적인 얘기로 심화 서비스를 다루지 않고 독자들에게 서비스들을 잘 소개하여 미래를 대비할 수 있게 해준다.</p>
<h2 id="프로젝트-실습">프로젝트 실습</h2>
<p>AWS를 이용한 간단한 미니 프로젝트를 만드는 실습을 진행한다. 이때까지 배운 모든 기술을 사용하여 큰 서비스를 만드는 것은 아니다. 기본적인 서비스들을 사용하여 클라우드 애플리케이션을 구성한다. 실제 프로젝트에서 하나의 AWS 서비스를 적용하는 것은 쉬울지 몰라도 여러 서비스를 유기적으로 연결해 가며 프로젝트를 완성하는 부분은 쉽지 않다고 생각한다. 그렇기에 각 서비스를 유기적으로 결합하며 하나의 프로젝트를 완성해 가는 과정이 크게 마음에 들었다.</p>
<hr>
<h1 id="느낌">느낌</h1>
<h2 id="장점">장점</h2>
<p>입문자를 위한 책이라는 말이 잘 어울리는 좋은 책이라고 생각한다. 적절한 예시와 가볍지만 핵심을 짚는 이론, 친절한 실습을 통해 AWS를 이 책을 통해 처음 접하는 사람조차 책을 읽어나가는 데 큰 어려움을 겪지 않을 거라 생각한다. </p>
<p>또한 AWS를 지금 당장 프로젝트에 적용하고 싶은 사람에게도 큰 도움을 줄 수 있을 거라 생각한다. AWS의 각 서비스들의 사용법을 친절하게 설명하며 요금에 맞는 정책들도 잘 소개해 준다. 그리고 서비스들의 유기적인 결합 또한 잘 설명해 주기에 지금 당장 AWS를 적용하고 싶은 사람에게도 좋은 책이 될 것 같다.</p>
<h2 id="아쉬운-점">아쉬운 점</h2>
<p>개인적으로 아쉬운 부분이기에 공감하지 않을 수 있다고 생각한다.</p>
<p>책을 마치며 &#39;책을 2권으로 만들었으면 어땠을 까?&#39;라는 생각을 하게 되었다. 책의 마지막 실습 부분에서 각 서비스들을 유기적으로 연결하는 부분이 마음에 들어서 이런 생각을 하게 된 것 같다. AWS의 서비스의 종류가 많기에 각 내용을 다루다 보니 마지막 미니 프로젝트 부분의 분량이 많지 않은 것은 이해는 하지만, 그래도 3개 정도로 더 많은 프로젝트를 진행했다면 좋았을 거라 생각한다. 그렇게 생각하다 보니 AWS의 심화 서비스 부분은 따로 2권으로 보내고 기본 내용과 여러 개의 미니 프로젝트로 1권을 만들었으면 하는 생각이 조금 들었다.</p>
<hr>
<h1 id="추천-독자">추천 독자</h1>
<ul>
<li>AWS와 클라우드 컴퓨팅을 이제 입문하는 사람</li>
<li>AWS를  공부하려는 초보자</li>
<li>나의 지식의 깊이가 깊지 않아 추천하기에 좀 그렇지만 중급자분들에게도 AWS의 서비스들의 유기적인 결합 부분은 도움이 되지 않을까 생각한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[아임포트 결제 (2)]]></title>
            <link>https://velog.io/@meteor_control0/%EC%95%84%EC%9E%84%ED%8F%AC%ED%8A%B8-%EA%B2%B0%EC%A0%9C2</link>
            <guid>https://velog.io/@meteor_control0/%EC%95%84%EC%9E%84%ED%8F%AC%ED%8A%B8-%EA%B2%B0%EC%A0%9C2</guid>
            <pubDate>Sun, 17 Nov 2024 07:23:46 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/meteor_control0/post/09f54c92-4140-4f4c-aa9e-13ef7dc210a7/image.png" alt=""></p>
<h1 id="코드-분석">코드 분석</h1>
<h2 id="자바를-지원한다고-그런데-문서가-좀-이상한데">자바를 지원한다고? 그런데 문서가 좀 이상한데?</h2>
<p>해당 아임포트 문서가 이전 글에서도 알 수 있듯이 프런트에서 구현이 가능한 부분을 알리고 있고 스프링은 따로 알리고 있지 않았다. 하지만 꾸준히 검색과 찾기를 반복한 결과 하나의 사이트를 아임포트에서 제공하는 깃허브 사이트를 발견하였다. 그리고 자바를 지원하는 레포지토리를 찾게 되었다.</p>
<p><a href="https://github.com/iamport/iamport-rest-client-java">아임포트 자바 문서</a></p>
<blockquote>
<h1 id="iamport-rest-client-for-java">iamport REST Client for JAVA</h1>
<p>JAVA 사용자를 위한 아임포트 REST API 연동 모듈입니다.</p>
</blockquote>
<ul>
<li><a href="https://github.com/square/retrofit">com.squareup.retrofit2</a> 모듈을 기반으로 만들어진 버전</li>
<li>maven plugin 형태로 제공<h2 id="요구사항">요구사항</h2>
JAVA 1.7이상의 버전을 요구합니다.<br>(dependency관계에 있는 <a href="https://github.com/square/retrofit">com.squareup.retrofit2</a> 이 JAVA 1.7이상의 버전을 요구합니다)<h2 id="설치">설치</h2>
<a href="https://jitpack.io/#iamport/iamport-rest-client-java">JitPack</a> 을 통해 maven설정을 하실 수 있습니다.<br>pom.xml에 아래의 내용을 추가해주세요. 
... 중략<h2 id="예제">예제</h2>
src/test/java 의 IamportRestTest.java를 참조해주세요</li>
</ul>
<p><span style="font-size:250%; color:red"><strong>그런데 음... 사용법은 왜 안가르쳐줘?</strong></span></p>
<p>처음 이 문서를 만났을 때의 나의 심정은 말로 표현하기 힘들었다. 이렇게 API도 만들고 문서도 만들었는데 왜 사용법까지 문서화하지 않은거지? 이 문서가 특이한 경우인지 몰라도 이제까지 내가 개발하면서 만난 대부분의 아니 모든 문서는 사용법을 굉장히 친절하게 작성해주었었다. 그런데 이 녀석은 왜???</p>
<h2 id="정신-붙들고-테스트-코드로">정신 붙들고 테스트 코드로</h2>
<p>해당 문서의 마지막 양심이라고 할 수 있는 &quot;예제의 IamportRestTest.java를 참조해주세요&quot;를 
따라 테스트 코드로 가보기로 하였다. </p>
<pre><code class="language-java">public class IamportRestTest {

    IamportClient client;

    private IamportClient getNaverTestClient() {
        String test_api_key = &quot;5978210787555892&quot;;
        String test_api_secret = &quot;9e75ulp4f9Wwj0i8MSHlKFA9PCTcuMYE15Kvr9AHixeCxwKkpsFa7fkWSd9m0711dLxEV7leEAQc6Bxv&quot;;

        return new IamportClient(test_api_key, test_api_secret);
    }

    private IamportClient getBillingTestClient() {
        String test_api_key = &quot;7544324362787472&quot;;
        String test_api_secret = &quot;9frnPjLAQe3evvAaJl3xLOODfO3yBk7LAy9pRV0H93VEzwPjRSQDHFhWtku5EBRea1E1WEJ6IEKhbAA3&quot;;

        return new IamportClient(test_api_key, test_api_secret);
    }

    @Before
    public void setup() {
        String test_api_key = &quot;imp_apikey&quot;;
        String test_api_secret = &quot;ekKoeW8RyKuT0zgaZsUtXXTLQ4AhPFW3ZGseDA6bkA5lamv9OqDMnxyeB9wqOsuO9W3Mx9YSJ4dTqJ3f&quot;;
        client = new IamportClient(test_api_key, test_api_secret);
    }

    @Test
    public void testGetToken() {
        try {
            IamportResponse&lt;AccessToken&gt; auth_response = client.getAuth();

            assertNotNull(auth_response.getResponse());
            assertNotNull(auth_response.getResponse().getToken());
        } catch (IamportResponseException e) {
            System.out.println(e.getMessage());

            switch (e.getHttpStatusCode()) {
                case 401:
                    //TODO
                    break;
                case 500:
                    //TODO
                    break;
            }
        } catch (IOException e) {
            //서버 연결 실패
            e.printStackTrace();
        }
    }

    ... 중략


</code></pre>
<h3 id="남이-만든-코드는-어려워">남이 만든 코드는 어려워</h3>
<p>처음 코드를 본 순간 음... 일단 모르는 문법은 없다고 생각하여 안도감 약간은 개뿔 모든 코드가 머리 속으로 들어오지 않고 튕겨나갔다. 내가 일말의 지식도 없는 남이 만든 프로젝트의 코드를 분석해야하는 상황이 처음이기에 변수, 메서드 하나 하나 전부 다 외계어처럼 다가왔다. 하지만 모른다고 물러설 상황이 아니었기에 한줄 한줄 코드를 열심히 분석할 수밖에 없었다.</p>
<h4 id="하나씩-천천히">하나씩 천천히</h4>
<p>일단 get~~ 으로 되어있는 private 함수 두 개는 일단 나랑 관련이 적어보이니 넘기고 Before을 보면 초기 설정을 하는 부분으로 보인다. 일단 테스트에 있는 IamportClient 분석해보았었다. </p>
<pre><code class="language-java">public class IamportClient {

    public static final String API_URL = &quot;https://api.iamport.kr&quot;;
    public static final String STATIC_API_URL = &quot;https://static-api.iamport.kr&quot;;
    protected String apiKey = null;
    protected String apiSecret = null;
    protected String tierCode = null;
    protected Iamport iamport = null;

    public IamportClient(String apiKey, String apiSecret) {
        this.apiKey = apiKey;
        this.apiSecret = apiSecret;
        this.iamport = this.create(false);
    }

... 중략

    public IamportResponse&lt;Payment&gt; paymentByImpUid(String impUid) throws IamportResponseException, IOException {
            AccessToken auth = getAuth().getResponse();
            Call&lt;IamportResponse&lt;Payment&gt;&gt; call = this.iamport.payment_by_imp_uid(auth.getToken(), impUid);

            Response&lt;IamportResponse&lt;Payment&gt;&gt; response = call.execute();
            if ( !response.isSuccessful() )    throw new IamportResponseException( getExceptionMessage(response), new HttpException(response) );

            return response.body();
        }

        public IamportResponse&lt;PagedDataList&lt;Payment&gt;&gt; paymentsByStatus(String status) throws IamportResponseException, IOException {
            AccessToken auth = getAuth().getResponse();
            Call&lt;IamportResponse&lt;PagedDataList&lt;Payment&gt;&gt;&gt; call = this.iamport.payments_by_status(auth.getToken(), status);

            Response&lt;IamportResponse&lt;PagedDataList&lt;Payment&gt;&gt;&gt; response = call.execute();
            if ( !response.isSuccessful() )    throw new IamportResponseException( getExceptionMessage(response), new HttpException(response) );

            return response.body();
        }

... 중략

}
</code></pre>
<p>IamprotClient 클래스를 보면 내가 원하는 것들을 많이 발견하였다. 일단 paymentByImpUid() 메서드 이름부터 주문번호로 부터 결제를 가져오는 그런 내용의 함수로 보였기 때문이다. 이렇게 내게 도움이 되는 코드들을 발견해나가며 코드를 분석해보았다. 코드들을 보면 어디선가 정보를 받아오고 토큰도 사용하고 있는 메서드가 많이 보였다. 하지만 http 요청을 보내는 부분이 해당 클래스에 직접 드러나지는 않았기에 조금 더 파보았다.</p>
<h3 id="retrofit">Retrofit</h3>
<p>결국 외부 URL을 통해 통신하는 메서드를 찾은 결과 Iamport 인터페이스를 찾게 되었다.</p>
<pre><code class="language-java">public interface Iamport {

    @POST(&quot;/users/getToken&quot;)
    Call&lt;IamportResponse&lt;AccessToken&gt;&gt; token(
        @Body AuthData auth);

    @GET(&quot;/payments/{imp_uid}/balance&quot;)
    Call&lt;IamportResponse&lt;PaymentBalance&gt;&gt; balance_by_imp_uid(
        @Header(&quot;Authorization&quot;) String token,
        @Path(&quot;imp_uid&quot;) String imp_uid
    );

    @GET(&quot;/payments/{imp_uid}&quot;)
    Call&lt;IamportResponse&lt;Payment&gt;&gt; payment_by_imp_uid(
        @Header(&quot;Authorization&quot;) String token,
        @Path(&quot;imp_uid&quot;) String imp_uid
    );

... 중략

}</code></pre>
<p>일단 단순히 생긴거로만 봐도 Http 통신을 하기위한 메서드라는 것을 알 수 있었다. 하지만 나는 이런 코들르 자바에서 본적이 없기에 일단 Call 클래스를 먼저 살펴보았다.</p>
<pre><code class="language-java">public interface Call&lt;T&gt; extends Cloneable {
    Response&lt;T&gt; execute() throws IOException;

    void enqueue(Callback&lt;T&gt; var1);

    boolean isExecuted();

    void cancel();

    boolean isCanceled();

    Call&lt;T&gt; clone();

    Request request();
}
</code></pre>
<p>사실 별 내용이 없다고 봐도 무방한 코드였기에 추가적인 정보를 위해 package 구조를 확인하자 retrofit2라는 외부 라이브러리를 사용한 것을 확인했다. </p>
<h4 id="retrofit-넌-누구냐">Retrofit 넌 누구냐</h4>
<p>여기서는 gpt의 도움을 빌렸다. 당시 내가 프로젝트를 진행하는 상황에서는 사실 gpt의 도움을 크게 받지 않았었다. 정확성이 떨어지는 정보를 자주 주었기 때문에 사용을 자제했었다. 하지만 gpt는 지식을 학습하는 첫 단계에서는 가장 뛰어나다고 생각하였기에 gpt를 통해 retrofit를 학습했었다. </p>
<p>일단 공식문서의 간단한 소개글을 보자면</p>
<blockquote>
<p>A type-safe HTTP client for Android and Java</p>
</blockquote>
<p>자바와 안드로이드를 위한 안전한 Http 라이브러리를 제공하는 것 같았다. 사용 방법이나 해당 코드가 어떻게 작동하는 지는 gpt를 통해 학습했었는데, 해당 내용을 기록으로 남기지 않아 조금 아쉽다.</p>
<p>여튼 구체적인 사용 방법은 몰라도 단순히 코드만 봐도 굉장히 직관적이었기에 해당 메서드를 실행하면 http 메서드, 헤더나 바디에 실을 정보들을 설정할 수 있기에 전체적인 코드를 이해하는데는 큰 문제가 없었다.</p>
<h2 id="나름의-분석-완료">나름의 분석 완료</h2>
<p>IamportClient를 통해 PG사와 통신을 주고 받으면서 PG사로부터 원하는 정보를 얻을 수 있는 라이브러리라는 것을 알게 되었다. 그리고 받은 정보는 response로 받아오며 해당 Payment 클레스에 내가 원하는 결제와 관련된 정보가 전부 나열되어 있었다.</p>
<pre><code class="language-java">public class Payment {

    @SerializedName(&quot;imp_uid&quot;)
    String imp_uid;

    @SerializedName(&quot;merchant_uid&quot;)
    String merchant_uid;

    @SerializedName(&quot;pay_method&quot;)
    String pay_method;

    @SerializedName(&quot;channel&quot;)
    String channel;

    @SerializedName(&quot;pg_provider&quot;)
    String pg_provider;

    @SerializedName(&quot;emb_pg_provider&quot;)
    String emb_pg_provider;

... 중략
}</code></pre>
<p>내가 주입하여 사용해야할 IamportClient와 메서드들, http통신이 어떻게 이루어지는지 확인할 Iamport 인터페이스, 그리고 메서드를 통해서 받아온 정보들의 내역을 알게되자 해당 라이브러리를 사용할만큼 분석했다는 생각이 들었다.</p>
<h1 id="구현">구현</h1>
<h2 id="외부-라이브러리는-편해">외부 라이브러리는 편해?</h2>
<p>앞의 학습 내용을 생각하면 조금 잘못된 말일 수도 있으나 막상 코드를 구현하는 과정에서는 앞의 카카오페이보다 수월하게 진행했다. 일단 http통신 부분을 라이브러리로 제공해주니 내가 작성해야하는 코드들이 상당히 많이 줄어들게 되었다.</p>
<h4 id="iamportpayservice">IamportPayService</h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Transactional
public class IamportPayService {

    private final String api_key = &quot;2160027041337455&quot;;
    private final String api_secret = &quot;E5BLH8wqTt3JuwMsXGxfkrZiXPF2dwcUBKnUNhyh0gRfHblsiqnNrXC9SWDwXToLlC0LDZ68c2ZnvV24&quot;;

    private final OrderRepository orderRepository;

    /**
     * 아임포트 연결
     * 
     * @return
     */
    private IamportClient getClient() {

        return new IamportClient(api_key, api_secret);
    }

    /**
     * 유저 검증
     * 
     * @param email
     */
    public void verifyEmail(String email, String merchant_uid) {

        // DB 정보 얻어오기
        Orders order = this.getOrdersByMerchant_uid(merchant_uid);

        // 주문 DB, 로그인 eamil 비교
        String loginEmail = email;
        String pgEmail = order.getUser().getEmail();

        if (!loginEmail.equals(pgEmail)) { // 다를 경우 주문 취소
            this.cancelPaymentByImpUid(order.getPgUid());
            throw new IamportSingleIamportPaymentVerificationEmailException();
        }

    }

    /**
     * 금액 검증
     * 
     * @param amount
     * @param imp_uid
     */
    public void verifyAmount(int amount, String imp_uid) {

        try {
            IamportClient client = getClient();

            // pg사 정보 받기
            IamportResponse&lt;Payment&gt; payment_response = client.paymentByImpUid(imp_uid);

            // pg사에 저장된 금액
            int iamportPaymentAmount = payment_response.getResponse().getAmount().intValue();

            if (amount != iamportPaymentAmount) { // 금액이 다를 경우
                this.cancelPaymentByImpUid(imp_uid);
                throw new IamportSingleIamportPaymentVerificationAmountException();
            }

        } catch (IamportResponseException | IOException e) {
            this.cancelPaymentByImpUid(imp_uid);
            throw new IamportSingleIamportPaymentConnectionInfoException();
        }

    }

    /**
     * 주문 번호 검증
     * 
     * @param requestDTO
     */
    public void verifyUid(IamportCallbackDTO requestDTO) {

        IamportClient client = getClient();

        try {

            // 아임포트 정보 받기
            IamportResponse&lt;Payment&gt; payment_response = client.paymentByImpUid(requestDTO.getImp_uid());

            // DB 주문 정보
            Orders order = this.getOrdersByMerchant_uid(requestDTO.getMerchant_uid());

            // pg사 주문 번호
            String pgMerchantUid = payment_response.getResponse().getMerchantUid();
            // db 주문 정보
            String orderMerchantUid = order.getPgUid();

            if (!pgMerchantUid.equals(orderMerchantUid)) { // 다를 경우
                this.cancelPaymentByImpUid(requestDTO.getImp_uid());
                throw new IamportSingleIamportPaymentVerificationMerchantUidException();
            }

        } catch (IamportResponseException | IOException e) {
            this.cancelPaymentByImpUid(requestDTO.getImp_uid());
            throw new IamportSingleIamportPaymentConnectionInfoException();
        }

    }

    /**
     * 특정 금액 환불 로직
     * 
     * @param requestDTO
     */
    public void cancelAmount(IamportCancelRequestDTO requestDTO) {

        IamportClient client = getClient();
        // 환불 금액
        BigDecimal cancelAmount = BigDecimal.valueOf(requestDTO.getCancel_amount());
        // 환불 데이터
        CancelData cancel_data = new CancelData(requestDTO.getMerchant_uid(), false, cancelAmount);
        // checksum 으로 검증 추가
        cancel_data.setChecksum(cancelAmount);

        // DB 검증
        Orders order = this.getOrdersByMerchant_uid(requestDTO.getMerchant_uid());
        int dbAmount = order.getTotalPrice();

        if (requestDTO.getCancel_amount() &gt; dbAmount) { // DB 총 금액과 request 환불 금액 비교
            throw new IamportRefundVerificationAmountExceptionIamport();
        }

        try {
            // 환불 실행
            IamportResponse&lt;Payment&gt; payment_response = client.cancelPaymentByImpUid(cancel_data);

        } catch (IamportResponseException | IOException e) {
            throw new IamportSingleIamportPaymentCancelException();
        }

        // DB 반영
        order.setTotalPrice(dbAmount - requestDTO.getCancel_amount());
        orderRepository.save(order);

        // TODO: 2023-05-19 특정 금액 환불 시 재고 DB반영 여부 모름
    }

    /**
     *  주문 취소
     *
     * @param imp_uid
     */
    private void cancelPaymentByImpUid(String imp_uid) {

        try {
            IamportClient client = getClient();

            // imp_uid를 이용한 전액 환불
            CancelData cancel_data = new CancelData(imp_uid, true);
            IamportResponse&lt;Payment&gt; cancel_response = client.cancelPaymentByImpUid(cancel_data);

            Orders order = this.getOrdersByPGUid(imp_uid);
            order.setStatus(OrderStatus.CANCELED);
            orderRepository.save(order);

            // TODO: 2023-05-19 전액 환불 or 주문 취소 시 재고 DB 반영 추가 여부 모름

        } catch (Exception e) {
            // TODO: 2023-05-19 전액 환불 로직에서 문제가 생긴다면 어떻게 처리해야할까?
            throw new IamportSingleIamportPaymentCancelException();
        }

    }


    /**
     * 가맹점 주문 번호를 이용하여 DB의 주문 테이블 정보 얻기
     *
     * @param merchant_uid
     * @return
     */
    private Orders getOrdersByMerchant_uid(String merchant_uid) {
        // DB 접근하여 주문 정보 가져오기
        Optional&lt;Orders&gt; result = orderRepository.findByNumber(merchant_uid);
        return result.orElseThrow(IamportDBConnectionByMerchantUidExceptionIamport::new);

    }

    /**
     * pg사 주문 번호를 이용하여 DB의 주문 테이블 정보 얻기
     *
     * @param pgUid
     * @return
     */
    private Orders getOrdersByPGUid(String pgUid) {
        // DB 접근하여 주문 정보 가져오기
        Optional&lt;Orders&gt; result = orderRepository.findByPgUid(pgUid);
        return result.orElseThrow(IamportDBConnectionByPgUidExceptionIamport::new);

    }

    /**
     * pg사 주문 번호 DB 반영
     * 
     * @param requestDTO
     */
    public void saveImpUid(IamportCallbackDTO requestDTO) {

        // 주문 정보 가져오기
        Orders order = this.getOrdersByMerchant_uid(requestDTO.getMerchant_uid());
        order.setPgUid(requestDTO.getImp_uid());
        orderRepository.save(order);

    }

    /**
     * 결제 완료 DB 반영
     * 
     * @param requestDTO
     */
    public void savePurchased(IamportVerificaitonDTO requestDTO) {

        // 주문 정보 가져오기
        Orders order = this.getOrdersByMerchant_uid(requestDTO.getMerchant_uid());
        order.setStatus(OrderStatus.PURCHASED);
        orderRepository.save(order);

        // TODO: 2023-05-20 결제 완료시 재고 관련 DB 작업 여부 모름
    }
}</code></pre>
<h1 id="느낀점">느낀점</h1>
<h2 id="모르고-어려운-코드도-한줄씩">모르고 어려운 코드도 한줄씩</h2>
<p>해당 프로젝트를 진행하며 가장 큰 시간을 들여 학습했던 내용이 바로 이 아임포트 결제였다. 아무래도 한글로 친절하게 사용법을 설명해주던 문서들에 익숙해져 있었기에 더 시간이 많이 걸렸다. 거기다가 내가 모르는 사람이 모르는 기술 스택을 사용한 프로젝트를 분석하기란 쉽지 않았다. 그래도 한줄 한줄 코드를 분석해나가는 과정이 힘들지만 보람되었다고 생각한다. 한번 간 길은 두번 가기도 쉽다는 말처럼 남이 작성한 코드, 모르는 기술이 적용된 코드라도 겁먹지 않고 힘껏 부딪힐 용기를 얻었던 것 같다.</p>
<h2 id="잘-쓴-테스트-코드는-처음">잘 쓴 테스트 코드는 처음</h2>
<p>공부하면서 많이 듣던 내용 중 하나가 </p>
<blockquote>
<p>잘 작성된 테스트 코드는 문서를 대체한다.</p>
</blockquote>
<p>였는데 이번 기회에 테스트 코드를 잘 작성하는 것이 중요하다고 느꼈다. 공부를 많이 해보면 테스트 코드를 의무적으로 작성하기를 강요받는데 사실 크게 와닿는 느낌이 없었다. 내가 실전에서 테스트를 돌리지 않는 것도 큰 이유였지만 내가 작성한 테스트 코드를 남이 보았을 때의 느낌을 잘 모르는 것이 더 큰 이유였다. </p>
<p>앞으로는 테스트 코드를 더 친절하게 작성해야할 것 같다. 개발자란 결국 협업이 기본이기에 내가 작성한 코드를 다른 동료 개발자가 보고 사용하는 경우가 많다고 생각한다. 앞으로는 실제 코드를 작성하는 것 만큼이나 친절한 테스트 코드를 작성하는 것을 신경쓰면서 개발을 이어나갈 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[아임포트 결제 (1)]]></title>
            <link>https://velog.io/@meteor_control0/%EC%95%84%EC%9E%84%ED%8F%AC%ED%8A%B8-%EA%B2%B0%EC%A0%9C1</link>
            <guid>https://velog.io/@meteor_control0/%EC%95%84%EC%9E%84%ED%8F%AC%ED%8A%B8-%EA%B2%B0%EC%A0%9C1</guid>
            <pubDate>Sat, 16 Nov 2024 16:47:26 GMT</pubDate>
            <description><![CDATA[<h1 id="카드-결제">카드 결제</h1>
<h2 id="아임-포트-선정-과정">아임 포트 선정 과정</h2>
<p>해당 웹 사이트 개발을 요청해주신 클라이언트 분의 강력한 요청에 따라 신용 카드와 같은 카드 결제를 통해 물건을 구매할 수 있는 기능을 추가하기로 했다. 사실 카드 결제에 대한 지식은 전무했기 때문에 멘토님과의 대화를 통해 아이엠포트를 이용하기로 결정했다. </p>
<h2 id="pg사와의-실제-구현은-힘들다">PG사와의 실제 구현은 힘들다.</h2>
<p>개발을 진행하기로 한 상황에서 바로 직면한 문제가 실제 PG사와 같이 구현을 하는 것이 아니었기에 문제가 되었다. 만약 실제로 아임포트에 등록하고 PG사와 계약을 진행했을 경우 PG사의 지시사항에 맞게 개발을 진행하면 되기에 생각보다 간단하게 진행된다고 멘토님이 알려주셨다. 따라서 아임포트에서 제공해주는 문서를 보며 내가 직접 구현을 해야했다.</p>
<h2 id="아임포트에서-스프링을-안내해주지-않는다">아임포트에서 스프링을 안내해주지 않는다.</h2>
<p>해당 아임포트 개발자 노트에 들어가게 되면 바로 알 수 있는 내용들이 대부분 javascript 즉 프런트에서 처리할 수 있는 코드들이 안내되어 있고 백엔드에서 불러올 수 있는 메서드나 라이브러리와 관련된 내용이 거의 없었다. 아니 그냥 없다고 봐도 무방했다. 따라서 해당 문서를 찾는 것 부터 난관이 시작되었다.</p>
<h1 id="결제-과정의-이해">결제 과정의 이해</h1>
<p>일단 결제 과정을 이해하는 것이 중요하기에 카카오 페이 결제 구현한 것을 떠올리며 결제 과정에 집중하기로 했다.</p>
<h2 id="아임포트-문서-해석">아임포트 문서 해석</h2>
<p><a href="https://developers.portone.io/opi/ko/integration/start/v1/auth?v=v1">아임포트 결제 개발자 문서</a></p>
<p>해당 문서에서 결제를 진행하며 알아야할 지식이나 코드에 대한 전반적이 내용을 안내하고 있다.</p>
<h3 id="결제-흐름">결제 흐름</h3>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/1928c439-8d70-46b3-a022-3fc29e808d50/image.png" alt=""></p>
<p>카카오 페이와 다른 점은 카카오 페이는 결제 준비 단계를 거쳐서 결제창을 통해 결제를 했다면 아임포트 결제는 결제창으로 결제를 먼저 진행 후 결제 인증을 거친다. 이후 결제 완료를 위해 서버에 다히 한번 정보를 보내는 것으로 결제 검증을 완료한다.</p>
<h4 id="결제-완료-1차">결제 완료 (1차)</h4>
<blockquote>
<p>결제가 완료되면 반환되는 응답 객체(response)의 에러 여부에 따라 처리 로직을 콜백 함수에 작성합니다. 요청이 성공했을 경우에 결제번호(imp_uid)와 주문번호(merchant_uid)를 서버에 전달하는 로직을 위와 같이 작성합니다.</p>
</blockquote>
<p>사용자가 결제창을 통해 결제를 완료하면 결제 번호와 주문 번호를 받을 수 있는데 이 정보를 다시 PG사에 전달하는 1차 검증 과정을 진해해야함</p>
<h4 id="결제-완료-검증-2차">결제 완료 검증 (2차)</h4>
<blockquote>
<p>imp_uid와 merchant_uid를 서버에 전달하면, 서버는 포트원의 결제 조회 API를 호출하여 해당 결제 건의 상태를 확인하고 결제 완료 처리를 진행하여야 합니다.</p>
</blockquote>
<p>결제가 완료되더라도 다시 한 번 검증을 통해 결제가 올바르게 이루어졌는지 확인하는 로직을 넣을 것을 권하고 있다. 개인적으로 권한다는 느낌보다는 경고에 가깝다고 생각한다.</p>
<h2 id="결제-흐름-완전-이해하기">결제 흐름 완전 이해하기</h2>
<p>우리는 가맹점이지만 결제 상황에서는 프런트와 백으로 나누어져 있으므로 해당 상황에 맞게 맞게 다시 결제 흐름에 대해서 완전하게 이해하려고 한다.</p>
<h3 id="완전-이해하기">완전 이해하기</h3>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/9cbec6be-4edf-4d97-a6c3-7e23e120a443/image.png" alt=""></p>
<h4 id="1-사용자의-결제-요청">1. 사용자의 결제 요청</h4>
<p>사용자가 브라우저를 통해 프런트로 결제를 요청함.</p>
<h4 id="2-프런트에서-결제창을-띄움">2. 프런트에서 결제창을 띄움</h4>
<p>프런트에서 띄운 결제창을 통해 사용자가 결제를 진행함.</p>
<h4 id="3-결제-완료를-위한-정보를-백으로-전달함">3. 결제 완료를 위한 정보를 백으로 전달함</h4>
<p>PG사에서 제공하는 주문 번호를 포함한 결제 정보와 사용자 정보를 백으로 전달함.</p>
<h4 id="4-프런트에서-받은-정보를-토대로-db와-pg사의-정보를-비교함">4. 프런트에서 받은 정보를 토대로 DB와 PG사의 정보를 비교함</h4>
<p>프런트에서 받은 정보를 기반으로 DB의 정보와 PG사의 정보를 비교하여 맞는 지 확인함.</p>
<h4 id="5-결제-결과를-프런트로-반환함">5. 결제 결과를 프런트로 반환함.</h4>
<p>두 정보를 비교하여 결제가 완료 여부를 프런트로 반환함.</p>
<h4 id="6-결제가-완료되었다면-결제-검증을-위해-다시-한번-백으로-정보를-보냄">6. 결제가 완료되었다면 결제 검증을 위해 다시 한번 백으로 정보를 보냄</h4>
<p>결제가 정상적으로 진행되었더라도 다시 검증을 위해 사용자 정보와 주문 정보를 백으로 보냄</p>
<h4 id="7-프런트에서-받은-정보를-토대로-db와-pg사의-정보를-다시-비교함">7. 프런트에서 받은 정보를 토대로 DB와 PG사의 정보를 다시 비교함</h4>
<p>프런트에서 받은 정보를 기반으로 DB의 정보와 PG사의 정보를 비교하여 맞는 지 다시 확인함.</p>
<h4 id="8-결제-완료를-db에-반영함">8. 결제 완료를 DB에 반영함</h4>
<p>결제가 완전하게 끝났다면 DB에 이를 반영함</p>
<h4 id="9-백에서-프런트로-프런트에서-사용자로-결제가-완료되었음을-알림">9. 백에서 프런트로 프런트에서 사용자로 결제가 완료되었음을 알림</h4>
<p>결제 완료를 사용자에게 알리고 결제 완료 창을 띄움.</p>
<h1 id="느낀점">느낀점</h1>
<h2 id="문서를-통해-정보를-얻는-능력이-늘었다">문서를 통해 정보를 얻는 능력이 늘었다.</h2>
<p>외부 API를 사용하는 경우 문서를 통해 정보를 얻어야하는 경우가 많다고 알고 있었다. 저번 카카오 페이에서 첫 경험을 하였는데 굉장히 힘들었었다. 이번 아임포트를 진행하면서는 확실히 성장했다는 느낌이 많이 들었다. 물론, 카카오페이와 같은 결제와 관련된 점이 문서를 잘 이해하는데 도움이 되었다고는 부정할 수는 없지만 그래도 해당 문서가 어떤 점을 설명하려고 하는 지 이해하는데는 큰 문제가 없을 것 같다.</p>
<p>앞으로도 외부 API를 사용하기 위해 많은 문서를 만나 이해해야할 것이다. 첫 카카오페이 문서를 보면서는 단순하게 이해하는데 초점을 맞추었다. 하지만 이번에는 이 문서가 나에게 주는 정보와  정보를 이용한 요구사항이 무엇인지 판별하며 읽었었다. 앞으로 외부 API를 적용하는데 큰 도움이 되는 개발이었다.</p>
<h2 id="어떤-정보를-검증해야-할까">어떤 정보를 검증해야 할까?</h2>
<p>사용자의 정보와 DB에 저장된 주문 정보를 모두 검증할 수 있다면 정확성은 늘어나겠지만 성능은 저하될 것이라 생각하여 많이 고민했었다. 아무래도 if문을 자주 사용하는 것은 꺼려졌다. 그래서 나름 해당 결제 흐름을 보면 어떤 결제가 검증 과정에서 문제가 있다고 확인될 경우는 PG사에서 잘못된 정보를 주거나 DB에서 문제가 발생할 경우라고 생각했다. 각각의 경우를 생각했을 때 모든 정보를 검증할 필요가 없다는 것을 조금 더 잘 알게 되었다. DB나 PG사의 정보가 잘못된 경우 모든 정보가 잘못되었을 가능성이 높기에 주문 번호와 금액을 검증하는 정도로 충분히 검증 과정이 알맞게 작동할 것이라고 생각했었다.</p>
<p><span style="font-size:200%; color:red"><strong>이런 생각은 지금 돌이켜 보면 매우 잘못 되었다.</strong></span></p>
<h2 id="잘못된-점은-짚고-넘어가면서-성장하자">잘못된 점은 짚고 넘어가면서 성장하자</h2>
<p><strong>성능을 생각하더라도 각 기능은 목적과 성격에 맞게 로직을 구현하는 것이 중요하다.</strong> 따라서 결제 로직에서는 최대한 자세하게 검증하는 과정이 필요했다. 내가 이 글을 다시 쓰게된 이유다. 목적이 우선이고 성능은 그 다음인 것이다. 이번에는 정확성을 중요시하고 성능은 그 다음으로 생각했어야했다.</p>
<p>나는 코드 한 줄에도 나름의 이유를 담아 코딩을 하는 것이 중요하다고 생각했는데 이런 생각이 조금 편향되었었다. 코드에 이유를 담는다고 생각해왔지만 이유가 아닌 성능을 담아왔다고 생각한다. 앞으로는 목적에 맞는 이유를 담아 코딩하려고 한다.</p>
<h1 id="구현">구현</h1>
<p>이 내용은 다음 블로그 글로 이어서 쓰려고한다. 이전에는 한 글에 다 작성하였는데 수정하면서 다시 작성하려고 한다.</p>
<p>다음</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Start Here 서평]]></title>
            <link>https://velog.io/@meteor_control0/spring-start-here</link>
            <guid>https://velog.io/@meteor_control0/spring-start-here</guid>
            <pubDate>Thu, 14 Nov 2024 02:48:51 GMT</pubDate>
            <description><![CDATA[<p>내가 이 책을 만난 시점은 이미 하나의 스프링 강의와 책 한 권을 마무리한 시점이었는데, 이 책을 먼저 읽지 않은 것을 후회했기에 만약 스프링의 공부를 시작하는 사람이 있다면 이 책을 가장 먼저 읽어보는 것을 강력하게 추천한다.</p>
<h1 id="책-내용">책 내용</h1>
<p>스프링을 공부하려고 읽는 책의 대부분은 비슷하다.</p>
<p><span style="font-size:350%"><strong>일단 따라 해</strong></span></p>
<p>하지만, 이 책은 다르다.</p>
<p>책의 첫 시작은 스프링이 아닌 프레임워크로 우리를 초대한다. 또한 단순한 설명만 하지 않고 현실 세계의 내용에 빗대어 설명하여 이해를 돕는다. 개발자들은 서로 비슷한 부분에서 비슷한 문제를 직면하고 비슷한 해결 방식을 사용하기에 프레임워크가 생겨났다고 한다. 내가 첫 장부터 이 책이 남다르다고 느낀 점이 바로 프레임워크를 도입하는 부분이다. 다른 책들은 프레임워크의 장점들을 열거하며 프레임워크로의 전환이 옳다라는 생각을 많이 심어준다. 하지만 이 책에서는 프레임워크의 도입을 하지 않는 것이 더 좋은 경우들을 설명하며 프레임워크가 만능이 아님을 알려준다. 프레임워크인 스프링을 소개, 설명하는 책에서는 보기 힘든 부분이라고 생각한다. 이렇게 책은 장점이 아닌 다각도에서 설명을 이어나간다.</p>
<p>스프링이라는 프레임워크는 굉장히 크고 거대하다. 코드 한 줄에도 많은 개발론과 개발 방식을 고려하여 만들어졌기에 이런 스프링을 이해하기 쉽지 않다. 그래서 일단 큰 부분에서 스프링을 대략적으로 설명하는 방식이 많은 책에서 보여진다. 반대로 이 책은 스프링의 기본 요소인 빈을 통해 우리를 스프링으로 안내한다. 스프링의 가장 기본 요소인 빈을 이용하여 스프링의 주요 구성 요소인 DI(Dependency Injection), AOP(Aspect-Oriented Programming), 그리고 POJO(Plain Old Java Object)에 대해 알려준다. 이 전에도 공부했던 내용이지만 이 책을 읽고 나서는 내가 공부를 한 것이 아니라는 생각이 들 정도로 확실하게 알게 되었다.</p>
<p>책의 중반부에서는 스프링을 통해 개발해본다. 하지만 아무래도 스프링이 굉장히 크고 거대하기에 많은 부분을 세세하게 설명해주지는 않는다. 데이터, REST API, 시큐리티 등 다루는 부분에서 이미 스프링을 해본 적이 있다면 쉬운 예제들을 만나게 되어 아쉬울지 모른다. 하지만 핵심적인 요소와 꼭 알아야 할 부분들에 대해서는 명확하게 짚고 넘어가며 적절한 그림과 현실 세계의 예시를 통해 이해를 돕는다. 특히 은행을 통해 같은 자원에 접근하는 과정에서 생기는 문제를 설명하는 부분이 가장 좋았다고 생각한다. 개인적으로 스프링에 대해서 잘 알고 있다고 할지라도 한번 쯤 볼만한 책이라고 생각한다.</p>
<p>책의 마지막에서는 테스트에 관해 설명하는데 이 책에서 가장 아쉬운 부분이라고 생각한다. 아무래도 테스트 부분이 가장 마지막에 들어가다 보니 책의 중간 개발 부분에서 작성한 코드의 테스트 코드를 작성할 기회를 놓친 게 아쉽다고 생각한다. 만약 테스트 부분에 관해서 이 책을 통해 공부하려고 한다면 앞의 예제들을 다시 작성하며 해야 할 것이다. 나 또한 책의 마지막 부분에서 테스트를 공부하기 위해 책을 2번 읽었다. 가능하다면  책의 앞부분에 테스트를 넣었다면 좋았을 거라 생각한다.</p>
<h1 id="생각">생각</h1>
<h2 id="장점">장점</h2>
<p>만약 스프링의 공부를 시작한다면 가장 먼저 읽어야 하는 책이라고 생각한다. 원론적이고 거시적인 이야기로 책을 풀어가는 것이 아닌 현실 세계의 예시와 작은 부분들을 붙이고 붙여가며 독자들이 스프링에 대해 명확하게 이해할 수 있도록 도와준다. 스프링의 기본 개념을 제대로 이해할 수 있다고 생각한다. 스프링의 전반적인 내용에서 꼭 알아야 하는 개념에 대해서도 잘 설명해준다. 책 한 권으로 스프링 개발 각각의 핵심적인 개념에 관해서 공부할 수 있다.</p>
<h2 id="단점">단점</h2>
<p>스프링의 개발에 익숙하다면 조금 아쉬울 것이다. 예제들의 난이도가 낮다. 아무래도 전반적인 내용을 다루고 기초 개념을 쌓는 것을 목표로 하기에 예제가 기초적인 내용이다. 난이도가 있는 개발 부분을 공부하고자 한다면 부족한 부분이 있다고 생각한다. </p>
<p>테스트 파트의 위치가 아쉽다. 책의 마지막 부분에 있기에 테스트에 대해서 공부한다면 책을 다시 읽어야 할 것이다.</p>
<h2 id="추천-독자">추천 독자</h2>
<ul>
<li>이제 막 자바를 끝내고 스프링을 시작하려는 사람에게는 최고의 책이라고 생각한다.</li>
<li>초보자들이 스프링의 핵심 개념들을 제대로 이해할 수 있을 거라 생각한다.</li>
<li>중급자 이상의 분들도 한번 쯤 보면 좋은 책이라고 생각한다.</li>
</ul>
<h2 id="만약-읽는-다면">만약 읽는 다면</h2>
<p>한국 번역 판인 스프링 교과서 책이 있으므로 해당 책으로 보는 것이 좋다고 생각한다.</p>
<p><a href="https://product.kyobobook.co.kr/detail/S000213355775">https://product.kyobobook.co.kr/detail/S000213355775</a></p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/558cae62-4b5c-47b7-b267-011162e0d665/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java의 '+'에서 컴파일까지]]></title>
            <link>https://velog.io/@meteor_control0/Java%EC%97%90%EC%84%9C-%EB%AC%B8%EC%9E%90%EC%97%B4-%EC%97%B0%EC%82%B0%EC%9E%90%EC%9D%98-JVM-%EC%9E%91%EC%9A%A9</link>
            <guid>https://velog.io/@meteor_control0/Java%EC%97%90%EC%84%9C-%EB%AC%B8%EC%9E%90%EC%97%B4-%EC%97%B0%EC%82%B0%EC%9E%90%EC%9D%98-JVM-%EC%9E%91%EC%9A%A9</guid>
            <pubDate>Wed, 13 Nov 2024 09:04:46 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/meteor_control0/post/6a501849-ff84-4417-8d8d-f13c1056d856/image.png" alt=""></p>
<h1 id="java-는-jvm에서-어떻게-작동할까">Java &#39;+&#39;는 JVM에서 어떻게 작동할까?</h1>
<h3 id="메모리적-이득이라니">메모리적 이득이라니??</h3>
<p>이 내용의 시작은 내가 듣던 강의에서 강사님이 알려준 방식이었다.</p>
<blockquote>
<p>이 방식이 더 메모리적으로 이득이니까 이 방법을 사용하도록 하세요</p>
</blockquote>
<pre><code class="language-java">public void updateArticle(ArticleDto dto) {
        try {
            Article article = articleRepository.getReferenceById(dto.id());
            if (dto.title() != null) article.setTitle(dto.title());
            if (dto.content() != null) article.setContent(dto.content());
            article.setHashtag(article.getHashtag());
        } catch (EntityNotFoundException e) {
            log.warn(&quot;게시글 업데이트 실패. 게시글을 찾을 수 없습니다. - dto: {}&quot;, dto);
        }
    }</code></pre>
<p>위 코드는 게시글을 업데이트하는 메서드다.</p>
<p>메서드는 게시글을 찾을 수 없는 경우 exception이 발생하고 catch에서 로그를 찍는다. 이 로그를 찍는 상황에서 log.warn() 메서드 안에 들어갈 문자열을 적는 방식에서 메모리적 이득과 손해가 발생한다는 것이다.</p>
<blockquote>
<p>&#39;+&#39; 방식과 {} 방식</p>
</blockquote>
<pre><code class="language-java">log.warn(&quot;게시글 업데이트 실패. 게시글을 찾을 수 없습니다. - dto: {}&quot;, dto); - 이득
log.warn(&quot;게시글 업데이트 실패. 게시글을 찾을 수 없습니다. - dto: &quot; + dto);  - 손해</code></pre>
<p>문자열을 작성하는 방식에서 메모리적 이득이 발생한다는 것이 이해가 가지 않았기에 이렇게 글을 작성하게 되었다.</p>
<h2 id="이해하지-못하겠으면-찾아봐야지">이해하지 못하겠으면 찾아봐야지?</h2>
<h3 id="평소에-사용하던-방식">평소에 사용하던 방식</h3>
<p>문자열과 문자열을 더하거나 문자열 + 변수, 숫자 등을 더하는 방식은 대부분 &#39;+&#39;를 많이 사용하였다. 이유는 간단하다.</p>
<p><span style="font-size:300%"><strong>편하니까</strong></span></p>
<p>&#39;+&#39;를 사용하는 방식이 어떤 상황에서든 손해가 없다는 것이 아니라는 것은 알고 있었다. 예시로 코딩 테스트를 하다보면 &#39;+&#39; 연산자를 사용할 때 StringBuilder를 보다 메모리나 연산 시간에서 손해를 보는 것으로 알 수 있다. 하지만 이런 상황은 반복문 안에서 잦은 연산이 발생하는 경우에나 발생하는 것으로 알고 있었다.</p>
<p>그래서 이번 기회에 제대로 한번 파보려고 한다.</p>
<h3 id="string-concatenation">String Concatenation</h3>
<p>내가 가장 자주 사용하는 &#39;+&#39; 연산자를 사용하는 방식으로 <strong>String Concatenation</strong>라는 이름을 가졌다. 문자열을 연결한다는 의미를 가졌다.</p>
<p>+를 이용한 STring Concatenation은 이런 특성을 가졌다.</p>
<h4 id="1-자바에서는-자체적으로--연산자를-처리하는-것이-아닌-stringbuilder를-이용하여-작동한다">1. 자바에서는 자체적으로 &#39;+&#39; 연산자를 처리하는 것이 아닌 StringBuilder를 이용하여 작동한다.</h4>
<p>다른 언어와 다르게 자바는 String이 문자열인 객체다. 따라서 객체와 객체를 더해주는 과정은 단순한 연산으로 처리하는 데 문제가 있다. 하지만 자바에서 단순하게 &#39;+&#39;를 사용해도 문자열을 더하는 데 문제가 없도록 자체적으로 처리를 해준다. 이런 자체적인 처리에서 사용되는 것이 StringBuilder다.</p>
<h4 id="2-string은-불변immutable-객체이기에-새로-만드는-string은-메모리적으로-손해">2. String은 불변(immutable) 객체이기에 새로 만드는 String은 메모리적으로 손해</h4>
<p>자바 내에서 자체적으로 StringBuilder를 사용하여 변환해주기에 간단한 내용은 사용해도 문제가 없다. 하지만 자바에서 String은 단순한 문자열이 아닌 하나의 객체다. 따라서 객체를 계속해서 만드는 과정이 들어가 있기 때문에 반복문 안에서 사용하는 것은 주의가 필요하다.</p>
<h3 id="string-interpolation">String Interpolation</h3>
<p>일단 강사님이 알려주신 방식인 </p>
<pre><code class="language-java">메서드(변수가 포함된 문자열, 변수);</code></pre>
<p>이 방식의 이름에 대해서 찾아 보았다.</p>
<p>이 방식은 알고는 있지만 굳이 잘 사용하지 않는 방식이었다. 굳이 라는 느낌이 강한 방식이라 코딩 테스트를 할 때만 가끔 활용할 뿐이었다. 일단 이름을 찾으니 <strong>String Interpolation</strong>라는 이름의 방식이었다. </p>
<p>String Interpolation은 이러한 특성들을 가졌다.</p>
<h4 id="문자열-보간은-결국-문자열-연결로-구현된다">문자열 보간은 결국 문자열 연결로 구현된다.</h4>
<blockquote>
<p>문자열 보간은 결국 내부적으로 문자열 연결(String Concatenation)로 구현됩니다. 문자열 보간은 단지 개발자가 더 간결하고 직관적인 구문으로 문자열을 구성할 수 있도록 제공되는 문법적 편의일 뿐, 최종적으로는 문자열 연결을 통해 결과가 만들어지기 때문입니다.</p>
</blockquote>
<p>chatGPT를 통해 얻은 답변으로 개발자가 문법적인 편의성을 활욜할 수 있도록 하는 하나의 방법으로 설명된다. 하지만 결국 내부적인 구현은 위의 &#39;+&#39;를 활용한 방법과 같은 StringBuilder와 같은 방법으로 구현된다.</p>
<p>그렇다면 어떻게 두 개의 방식이 메모리적 성능에 영향을 끼치는 걸까?</p>
<h4 id="lazy-evaluation">Lazy Evaluation</h4>
<h2 id="span-stylecolorredlazy-evaluationspan"><span style="color:red">Lazy Evaluation</span></h2>
<p>한국말로 하면 <strong>지연 연산</strong> 정도로 해석될 것 같다. 무엇을 지연한다고 하면 바로 </p>
<p><span style="color:blue; font-size:150%"><strong>메서드가 실행되기 전 까지는 컴파일 과정을 겪지 않는 것이다.</strong></span></p>
<p>간단하면서도 어려운 것 같다. 컴파일이 되지 않는 다는 내용이 그러면 이 메서드가 실행되는 상황에서 다시 컴파일 된다는 것인가? 라는 의문을 가지게 할 수 있는 내용이라고 생각한다. 하지만 우리가 여기서 생각해야 할 점은 자바는 단순한 컴파일러 언어가 아니라는 것이다.</p>
<h3 id="자바는-컴파일러--인터프리터-언어다">자바는 컴파일러 + 인터프리터 언어다.</h3>
<p>우리가 자바의 코드를 실행하게 되면 먼저 자바의 컴파일러에 의해 컴파일된다. 이후 인터프리터를 통해 실행된다. 자바를 처음 공부할 때 배운 내용이지만 코드를 작성하거나 실행할 때 한 번도 고려해본 적 없는 지식이었다.</p>
<p>일단 컴파일에 대한 지식이 전무하기에 gpt를 통해서 컴파일 해달라고 해봤다.</p>
<pre><code class="language-java">log.warn(&quot;게시글 업데이트 실패. 게시글을 찾을 수 없습니다. - dto: {}&quot;, dto); - 이득
log.warn(&quot;게시글 업데이트 실패. 게시글을 찾을 수 없습니다. - dto: &quot; + dto);  - 손해</code></pre>
<p>그러면 위 코드를 컴파일 해보자.</p>
<ul>
<li><strong>String Concatenation</strong></li>
</ul>
<pre><code class="language-java">L0
   LINENUMBER 10 L0
   NEW java/lang/StringBuilder
   DUP
   LDC &quot;게시글 업데이트 실패. 게시글을 찾을 수 없습니다. - dto: &quot;
   INVOKESPECIAL java/lang/StringBuilder.&lt;init&gt; (Ljava/lang/String;)V
   ALOAD 1
   INVOKEVIRTUAL java/lang/Object.toString ()Ljava/lang/String;
   INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
   INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
   INVOKEINTERFACE org/slf4j/Logger.warn (Ljava/lang/String;)V</code></pre>
<ul>
<li><strong>String Interpolation</strong></li>
</ul>
<pre><code class="language-java">L0
   LINENUMBER 6 L0
   GETSTATIC org/slf4j/LoggerFactory.getLogger(Ljava/lang/Class;)Lorg/slf4j/Logger;
   LDC &quot;게시글 업데이트 실패. 게시글을 찾을 수 없습니다. - dto: {}&quot;
   ALOAD 1
   INVOKEINTERFACE org/slf4j/Logger.warn (Ljava/lang/String;Ljava/lang/Object;)V</code></pre>
<p>일단 길이나 대략적인 내용만 봐도 +를 이용한 방식이 성능적으로 손해를 볼 것이라고 예상이 가능하다. 좀 더 자세하게 살펴보면 위의 +를 통한 방식은 컴파일 과정에서 StringBuilder를 통해서 이미 문자열을 더하는 과정을 진행한다. 하지만 아래의 String Interpolation는 지연 방식을 이용하기에 문자열을 더하는 과정이 없다. 
따라서 자바를 실행하기 위해 컴파일을 하는 시점에서 이미 성능적으로 손해를 볼 수 밖에 없는 것이다. </p>
<h4 id="그러면-log를-찍는-상황에서는-다시-컴파일-하는거야">그러면 log를 찍는 상황에서는 다시 컴파일 하는거야?</h4>
<p>내가 공부하면서 많이 헷갈린 부분인데 catch를 통해 log.warn()을 실행하는 상황이 온다면 String Concatenation은 이미 컴파일 했기에 문제가 없지만 String Interpolation는 다시 컴파일 하는 과정을 겪으니까 실행해야하는 상황에서는 오히려 손해가 큰 것이 아닌가 고민을 했었다. 하지만 자바는 인터프리터 또한 가지고 있기에 인터프리터가 런타임 시에 다시 컴파일 하는 과정없이 바이트 코드를 실행해주며 내부에서 문자열을 더하는 연산을 해준다. </p>
<p>따라서 사용도 하지 않는 부분을 컴파일 하는 것을 주의해야한다는 것을 다시 알게 되었다.</p>
<h2 id="응용해보자">응용해보자</h2>
<p><span style="color:green; font-size:150%"><strong>Lazy Evaluation을 사용하는 String Interpolation는 사용하지 않는 메서드의 컴파일은 하지 않고 인터프리터를 사용하여 실행</strong></span></p>
<h3 id="언제-사용하지-않을까">언제 사용하지 않을까?</h3>
<p>내가 작성한 코드가 매번 매 순간 다 사용되는 것은 아니지만 나름의 분기점에서 사용과 사용하지 않는 비사용으로 나눌 수 있다고 생각한다.</p>
<h4 id="try---catch">try - catch</h4>
<p>위의 예시처럼 예외 발생 시에만 사용되는 코드가 바로 사용되지 않는 시점이 많은 코드일 것이다. 따라서 예외가 발생하여야만 사용이 되는 코드에는 Lazy Evaluation이 적용되는 코드를 작성하는 것이 좋을 것이다.</p>
<h4 id="log">log</h4>
<p>로그는 보통 레벨에 따라 사용과 비사용이 나뉜다. 만약 로그의 레벨을 warn으로 한다면 info 레벨의 로그는 무슨 일이 있어도 작동하지 않을 것이므로 Lazy Evaluation이 적용되는 코드를 작성하는 것이 좋을 것이다. 그렇지 않으면 모든 로그가 있는 부분에서 컴파일 과정에서 StringBuilder를 통해서 문자열을 더하는 과정을 넣어줄 것이고 이는 메모리적으로 손해를 일으킬 것이다.</p>
<h4 id="if---else">if - else</h4>
<p>if-else 상황에서도 사용되지 않는 쪽은 StringBuilder를 만들지 않아 메모리적으로 손해를 보는 경우가 발생하지 않을 것이다.</p>
<h4 id="논리-연산자">논리 연산자</h4>
<p>||, &amp;&amp;와 같은 논리 연산자의 경우에도 앞의 내용이 이미 참이거나 거짓일 경우 작동하지 않을 것이다. 예를 들면 (0 &gt; 1 &amp;&amp; 문장)의 경우에 0 &gt; 1이 이미 false 이므로 뒤의 문장 부분은 자바가 처리하지 않고 넘어가므로 이런 경우에도 적용될 수 있을 것이다.</p>
<h1 id="마치며">마치며</h1>
<h2 id="공부를-하며-느낀-점">공부를 하며 느낀 점</h2>
<p>강사님이 지나가며 말해주신 메모리적 이득이라는 것에 꽂혀 공부를 시작했는데 생각보다 난해한 부분이 많아서 당황했다. 특히 컴파일 부분에서는 무슨 얘기를 하는 건지 몰라서 이곳 저곳 찾아도 보고 gpt한테 비슷한 질문만 계속해서 한 적도 있었다.</p>
<p><strong>하지만 어렵게 공부한만큼 얻어간 것이 많았다고 생각한다.</strong></p>
<p>자바의 메모리 구조를 다시 한번 살펴볼 수 있었고, 컴파일 + 인터프리터 언어라는 것을 상기했고, 문자열을 더하는 과정에 대해 깊게 생각해 볼 수 있었다. 또한 지연의 개념이 컴파일 과정에서 어떻게 발생하는 지 잘 알 수 있었다.</p>
<h2 id="코드-한-줄에도-나름의-이유를-담아">코드 한 줄에도 나름의 이유를 담아</h2>
<p>같은 결과를 내는 코드 한 줄에도 나름의 이유를 담아 선택하는 것이 좋은 코딩이라고 생각했지만 실천해 본 것은 이번이 처음인 것 같아서 기분이 많이 좋았다. 앞으로도 코드 한 줄에도 나름의 이유를 담을 수 있도록 노력하려 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring - RedirectAttributes 잘못된 부분 수정]]></title>
            <link>https://velog.io/@meteor_control0/Spring-RedirectAttributes2</link>
            <guid>https://velog.io/@meteor_control0/Spring-RedirectAttributes2</guid>
            <pubDate>Thu, 03 Oct 2024 08:30:12 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/meteor_control0/post/608a0c08-b01b-4264-baa8-e7cf517b86fb/image.png" alt=""></p>
<p><a href="https://velog.io/@meteor_control0/Spring-RedirectAttributes">Spring - RedirectAttributes(잘못된 부분 많음)</a></p>
<h1 id="문제">문제</h1>
<p>이 글을 쓰게 된 가장 큰 이유는 리다이렉트를 구현해야하는 상황에서 내가 이전에 쓴 글을 보다가 크게 잘못된 부분이 있어서 다시 작성하려고 한다.</p>
<h2 id="리다이렉트는-get">리다이렉트는 GET</h2>
<p>리다이렉트는 GET 요청이므로 model에 정보를 넣어서 스프링 내부에서 정보를 전달하는 일은 불가능하다. 하지만 이런 기본적인 내용을 글 작성 당시 생각하지 못하고 model로 정보를 넘겨준다고 작성했다.</p>
<h1 id="해결법">해결법</h1>
<p>Spring 환경에서 리다이렉트를 실행할 때 정보를 넘겨주는 방법들을 조사했다</p>
<h2 id="1-쿼리-파라미터-redirectattributesaddattribute">1. 쿼리 파라미터 (RedirectAttributes.addAttribute)</h2>
<ul>
<li>용도: URL을 통해 데이터를 전달하는 방법입니다. URL에 데이터가 노출되며, 검색 조건, 페이지 번호 등과 같은 정보를 전달할 때 적합함</li>
<li>특징: URL에 포함되기 때문에 사용자가 데이터를 쉽게 볼 수 있으며, 보안에 주의해야 함</li>
</ul>
<pre><code class="language-java">redirectAttributes.addAttribute(&quot;page&quot;, 1);
redirectAttributes.addAttribute(&quot;userId&quot;, user.getId());
return &quot;redirect:/board/list&quot;;</code></pre>
<p>리다이렉트 후 데이터 받기</p>
<pre><code class="language-java">@GetMapping(&quot;/board/list&quot;)
public String list(@RequestParam(&quot;userId&quot;) Long userId, Model model) {
    User user = userService.findById(userId);
    model.addAttribute(&quot;user&quot;, user);
    return &quot;board/list&quot;;
}</code></pre>
<h2 id="2-flash-attribute-redirectattributesaddflashattribute">2. Flash Attribute (RedirectAttributes.addFlashAttribute)</h2>
<ul>
<li>용도: URL에 데이터를 노출하지 않고, 일회성으로 데이터를 전달하는 방식입니다. 주로 성공/실패 메시지나 잠깐 동안 필요한 데이터를 전달할 때 사용함</li>
<li>특징: 한 번의 요청 이후에는 데이터가 사라지므로, URL에 노출되지 않으며 세션에 일시적으로 저장함</li>
</ul>
<pre><code class="language-java">redirectAttributes.addFlashAttribute(&quot;user&quot;, user);
redirectAttributes.addFlashAttribute(&quot;msg&quot;, &quot;Operation successful&quot;);
return &quot;redirect:/board/list&quot;;</code></pre>
<p>리다이렉트 후 데이터 받기:</p>
<pre><code class="language-java">@GetMapping(&quot;/board/list&quot;)
public String list(Model model) {
    if (model.containsAttribute(&quot;user&quot;)) {
        User user = (User) model.getAttribute(&quot;user&quot;);
        // user 정보 사용
    }
    return &quot;board/list&quot;;
}</code></pre>
<h2 id="3-세션-httpsession">3. 세션 (HttpSession)</h2>
<ul>
<li>용도: 리다이렉트 이후에도 여러 요청에서 지속적으로 데이터를 유지하고 싶을 때 사용됩니다. 로그인한 사용자 정보 등, 여러 요청 간 유지가 필요한 데이터를 저장할 때 주로 사용함</li>
<li>특징: 세션은 지속적이며, 서버 메모리를 사용합니다. 너무 많은 데이터를 세션에 저장하면 성능 문제를 야기할 수 있음</li>
</ul>
<pre><code class="language-java">session.setAttribute(&quot;user&quot;, user);
return &quot;redirect:/board/list&quot;;</code></pre>
<p>리다이렉트 후 데이터 받기:</p>
<pre><code class="language-java">@GetMapping(&quot;/board/list&quot;)
public String list(HttpSession session, Model model) {
    User user = (User) session.getAttribute(&quot;user&quot;);
    model.addAttribute(&quot;user&quot;, user);
    return &quot;board/list&quot;;
}</code></pre>
<h2 id="4-쿠키-httpservletresponseaddcookie">4. 쿠키 (HttpServletResponse.addCookie)</h2>
<ul>
<li>용도: 클라이언트 측에서 데이터를 저장하여 리다이렉트 후에도 데이터를 유지하는 방법입니다. 세션과 달리 브라우저에 데이터를 저장하기 때문에, 브라우저가 열려 있는 동안에만 데이터를 사용할 수 있음</li>
<li>특징: 클라이언트 측에 저장되므로 데이터 보안에 주의해야 합니다. 쿠키는 보통 사용자 설정이나 트래킹에 사용함</li>
</ul>
<pre><code class="language-java">Cookie userCookie = new Cookie(&quot;userId&quot;, String.valueOf(user.getId()));
userCookie.setMaxAge(60 * 60); // 쿠키 유효 시간 설정 (1시간)
response.addCookie(userCookie);
return &quot;redirect:/board/list&quot;;</code></pre>
<p>리다이렉트 후 데이터 받기:</p>
<pre><code class="language-java">@GetMapping(&quot;/board/list&quot;)
public String list(@CookieValue(value = &quot;userId&quot;, defaultValue = &quot;0&quot;) Long userId, Model model) {
    if (userId != 0) {
        User user = userService.findById(userId);
        model.addAttribute(&quot;user&quot;, user);
    }
    return &quot;board/list&quot;;
}</code></pre>
<h2 id="5-url-path-variable">5. URL Path Variable</h2>
<ul>
<li>용도: URL 경로의 일부로 데이터를 전달하는 방법입니다. 리소스 식별자(ID 등)를 URL에 포함하여 전달할 수 있음</li>
<li>특징: URL 경로 자체에 데이터를 포함하므로, 주로 RESTful API에서 사용함</li>
</ul>
<pre><code class="language-java">return &quot;redirect:/board/list/user/&quot; + user.getId();</code></pre>
<p>리다이렉트 후 데이터 받기:</p>
<pre><code class="language-java">@GetMapping(&quot;/board/list/user/{userId}&quot;)
public String list(@PathVariable Long userId, Model model) {
    User user = userService.findById(userId);
    model.addAttribute(&quot;user&quot;, user);
    return &quot;board/list&quot;;
}</code></pre>
<h2 id="요약">요약</h2>
<ul>
<li>쿼리 파라미터: addAttribute()로 데이터를 URL에 포함하여 전달.</li>
<li>Flash Attribute: addFlashAttribute()로 URL에 노출되지 않는 일회성 데이터를 전달.</li>
<li>세션: HttpSession을 사용하여 여러 요청 간 지속적으로 데이터를 유지.</li>
<li>쿠키: 클라이언트 측에 데이터를 저장하고 유지.</li>
<li>URL Path Variable: URL 경로의 일부로 데이터를 전달.</li>
</ul>
<p>각각 용도에 맞는 방식을 선택하는 것이 좋다.</p>
<h1 id="느낀점">느낀점</h1>
<p>이전에 쓴 글이 너무 잘못되었다는 것을 안 순간 얼굴이 화끈해지는 것을 느꼈다. 내 글을 본 사람들이 &#39;이 사람은 정말 기본도 못하는구나&#39; 라는 생각을 하는 것이 당연하다고 생각했다. 그래서 글을 처음 본 순간 바로 삭제하거나 수정하려고 했었다. 하지만 글을 수정하면서 글을 쓰게 된 이유가 나의 공부 때문이라는 것을 다시 생각하는 순간 수정을 그만뒀다. 내가 이 글을 작성하는 이유가 나의 성장 때문인데 나의 부족한 점을 가리기 위해 노력하는 것이 잘못되었다고 생각한다. 가끔은 기본도 잘 안지키고 모자란 부분 투성이지만 이렇게 수정하면서 나아가면 된다고 생각한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[카카오 페이 (2)]]></title>
            <link>https://velog.io/@meteor_control0/%EC%B9%B4%EC%B9%B4%EC%98%A4-%ED%8E%98%EC%9D%B42</link>
            <guid>https://velog.io/@meteor_control0/%EC%B9%B4%EC%B9%B4%EC%98%A4-%ED%8E%98%EC%9D%B42</guid>
            <pubDate>Thu, 08 Jun 2023 23:47:12 GMT</pubDate>
            <description><![CDATA[<h1 id="구현">구현</h1>
<p>지난 글에 이어서 카카오 페이 결제 시스템을 구현했던 과정을 적으려고 한다. 카카오 페이 결제 시스템에 관하여 집중적으로 다루기에 코드에 빈 부분이 있을 수 있다.</p>
<h2 id="✅-결제-흐름-이해하기">✅ 결제 흐름 이해하기</h2>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/96bd5a58-29a4-4482-a691-444b113d192d/image.png" alt=""></p>
<p>개발하는 과정과 결제 흐름을 섞어서 글을 적기에는 나의 글쓰기 능력이 너무나도 부족했기에 결제 흐름을 먼저 작성하고 코드를 적고 맞춰서 설명하고자 한다.</p>
<p>카카오 개발자 문서를 읽으며 개발을 진행하며 완성한 결제 시스템의 시퀀스 다이어그램이다. 유저의 요청을 받아 프런트 엔드 - 백엔드 - DB - 카카오 서버에서 결제를 주고 받는 과정들을 그리고 있다. 이제까지의 단순한 흐름이 아닌 카카오 서버에서 요구하는 요구사항들을 만족시키면서 흐름을 진행했다.</p>
<h4 id="1-유저의-요청">1. 유저의 요청</h4>
<p>유저가 프런트 엔드에 카카오 결제를 요청한다. 주문 페이지의 카카오 페이 결제 버튼을 통해서 프런트에 요청을 보낸다.</p>
<h4 id="2-프런트---백엔드-결제-준비-요청">2. 프런트 - 백엔드 결제 준비 요청</h4>
<p>프런트에서 카카오로 결제를 진행할 경우 결제에 필요한 정보를 JSON, POST로 백엔드에서 보낸다. 필요한 내용은 주문 번호, 금액 등이 있다.</p>
<h4 id="3-백엔드에서-검증">3. 백엔드에서 검증</h4>
<p>백엔드에서 프런트에서 보낸 정보들 중에서 주문 번호를 이용하여 DB의 Orders(주문) 테이블에 접근하여 DB에 저장된 주문 정보의 금액과 프런트에서 요청받은 금액을 비교하여 검증한다.</p>
<h4 id="4-카카오-페이에-결제-준비-요청을-보낸다">4. 카카오 페이에 결제 준비 요청을 보낸다.</h4>
<p>RestTemplate을 이용하여 카카오 페이 결제 준비 요청을 보낸다. POST방식 Content-type과 Authoization을 헤더에 헤더에 붙이고 MultiValueMap을 통해 카카오 문서에서 요구하는 요구 사항들(주문 번호, 금액, 상품명, approval_url ...) 등을 카카오 서버 url(&quot;<a href="https://kapi.kakao.com/v1/payment/ready&quot;)%EB%A1%9C">https://kapi.kakao.com/v1/payment/ready&quot;)로</a> 보낸다.</p>
<h4 id="5-카카오-페이에서-받은-정보를-저장-프런트로-보낸다">5. 카카오 페이에서 받은 정보를 저장, 프런트로 보낸다.</h4>
<p>준비 요청이 성공적으로 완료된다면 카카오 서버에서 제공해 주는 정보를 받는다. 만약 요청이 성공한다면 주문 정보의 상태를 &#39;승인 대기&#39;로 변경하여 DB의 주문 테이블에 저장한다.</p>
<p>이 후 프런트에서 카카오 서버로 요청을 보낼 url(next_redirect_pc_url)을 프런트 서버로 보낸다.</p>
<h4 id="6-프런트와-유저-사이의-결제-진행">6. 프런트와 유저 사이의 결제 진행</h4>
<p>카카오 페이를 진행할 경우 보여지는 QR코드를 보여주는 화면을 보여주거나 모바일의 경우 카카오 톡을 받아 비밀번호를 입력하여 결제 완료를 누른다. 사실 이 부분은 시퀀스에 포함되지 않았는데 프런트 측에서 구현하기 나름이라 생각하여 생략하였다.</p>
<h4 id="7-프런트에서-결제">7. 프런트에서 결제</h4>
<p>프런트와 유저 카카오 사이에서 결제가 완료된다면 카카오 서버에서 내가 결제 준비 요청에서 제공했던 approval_url, 즉 결제 성공 시 url로 Redirect GET 요청을 보내게 된다. 백엔드 서버에서 이 GET 요청을 받는데 카카오 서버에서 GET 요청을 보낼 때 pgToken을 쿼리 스트링으로 제공해 준다. 이 pgToken이 있어야만 결제 승인 요청을 할 수 있다.</p>
<h4 id="8-백엔드에서-db에-접근하여-주문-정보를-가져옴">8. 백엔드에서 DB에 접근하여 주문 정보를 가져옴</h4>
<p>백엔드에서 카카오 서버에서 보내준 요청을 받는데 pgToken과 주문 번호를 받는다. 그리고 이 주문 번호를 이용하여 DB의 주문 테이블에 접근하여 결제 승인 요청에 필요한 정보(가맹점 주문 번호, 결제 고유 주문 번호, 가맹점 회원 id) 등을 받아온다. </p>
<h4 id="9-카카오-페이-승인-요청">9. 카카오 페이 승인 요청</h4>
<p>다시 RestTemplate을 통해 카카오 문서에서 요구하는 사항들(POST, 헤더, 매개 변수) 등을 지켜 결제 승인 요청을 보낸다. 이때 필요한 정보 중 pgToken이 중요하다. 이 pgToken을 통해 카카오 서버에서 우리의 요청을 검증할 수 있다.</p>
<h4 id="10-카카오-페이-승인-응답">10. 카카오 페이 승인 응답</h4>
<p>카카오 승인 요청이 성공하면 문서에 적힌 응답 정보들(결제 금액, 카드 정보, 상품 정보) 등을 받아온다. 이 중에서 서버에서 저장할 필요가 있는 정보들을 DB에 접근하여 저장한다. 또한 결제가 완료 되었으니 주문의 상태를 결제 완료로 변경하여 DB의 주문 테이블에 저장한다.</p>
<h4 id="11-결제-완료">11. 결제 완료</h4>
<p>백엔드에서 결제가 완료되었음을 프런트로 알리고 프런트에서 유저에게 결제가 완료 화면으로 이동시킴으로써 카카오 페이를 통한 결제를 마무리 한다.</p>
<h2 id="코드">코드</h2>
<h4 id="kakaopaymentcontroller">KakaoPaymentController</h4>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/account/pay/kakao&quot;)
public class KakaoPaymentController {

    private final KakaoPayService kakaoPayService;

    /**
     * 결제 준비 요청
     *
     * @return
     */
    @PostMapping(&quot;/ready&quot;)
    public ResponseEntity&lt;KakaoReadyResponse&gt; readyToKakaoPay(@RequestBody @Valid KakaoPayRequsetDTO requsetDTO) {

        KakaoReadyResponse response = kakaoPayService.kakaoPayReady(requsetDTO);

        return new ResponseEntity&lt;&gt;(response, HttpStatus.OK);
    }

    /**
     * 결제 성공
     *
     * @param pgToken
     * @return
     */
    @GetMapping(&quot;/success&quot;)
    public ResponseEntity&lt;ResponseDTO&gt; afterPayRequest(@RequestParam(&quot;pg_token&quot;) String pgToken,
                                                       @RequestParam(&quot;partner_order_id&quot;) String partner_order_id) {

        kakaoPayService.approveResponse(pgToken, partner_order_id);

        return new ResponseEntity&lt;&gt;(new ResponseDTO(&quot;200&quot;,&quot;success&quot;), HttpStatus.OK);
    }

    /**
     * 결제 취소
     *
     * @return
     */
    @GetMapping(&quot;/cancel&quot;)
    public ResponseEntity&lt;ResponseDTO&gt; cancel() {

        return new ResponseEntity&lt;&gt;(new ResponseDTO(&quot;400&quot;,&quot;fail&quot;), HttpStatus.OK);
    }

    /**
     * 결제 실패
     *
     * @return
     */
    @GetMapping(&quot;/fail&quot;)
    public ResponseEntity&lt;ResponseDTO&gt; fail() {

        return new ResponseEntity&lt;&gt;(new ResponseDTO(&quot;400&quot;,&quot;fail&quot;), HttpStatus.OK);
    }

    /**
     * 결제 환불
     *
     * @return
     */
    @PostMapping(&quot;/refund&quot;)
    public ResponseEntity&lt;ResponseDTO&gt; refund(@RequestBody @Valid KakaoCancelRequestDTO requestDTO, Principal principal) {

        kakaoPayService.kakaoCancel(requestDTO);

        return new ResponseEntity&lt;&gt;(new ResponseDTO(&quot;200&quot;, &quot;success&quot;), HttpStatus.OK);
    }


}
</code></pre>
<h4 id="kakaopayservice">KakaoPayService</h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Transactional
public class KakaoPayService {

    private final OrderRepository orderRepository;

    static final String cid = &quot;TC0ONETIME&quot;; // 가맹점 테스트 코드
    static final String admin_Key = ADMIN 키; // ADMIN 키

    /**
     * 결제 요청
     * 
     * @param requsetDTO
     * @return
     */
    public KakaoReadyResponse kakaoPayReady(KakaoPayRequsetDTO requsetDTO) {

        // 카카오페이 요청 양식
        MultiValueMap&lt;String, String&gt; parameters = new LinkedMultiValueMap&lt;&gt;();

        String partner_order_id = requsetDTO.getPartner_order_id();
        Orders order = this.getOrdersByPartnerOrderId(partner_order_id);

        // 입력 받은 금액과 DB 금액이 비교
        int dbAmount = order.getTotalPrice();
        if (dbAmount != requsetDTO.getTotal_amount()) {
            throw new KakaoSinglePaymentReadyException();
        }

        // 서버와 주고 받을 정보
        parameters.add(&quot;cid&quot;, cid);
        parameters.add(&quot;partner_order_id&quot;, partner_order_id);
        parameters.add(&quot;partner_user_id&quot;, order.getUser().getEmail());
        parameters.add(&quot;total_amount&quot;, String.valueOf(order.getTotalPrice()));

        parameters.add(&quot;item_name&quot;, &quot;아이템 이름&quot;);
        parameters.add(&quot;quantity&quot;, &quot;5&quot;); // 아이템 갯수

        // 부가세, 비과세 금액으로 현재는 0으로 설정
        parameters.add(&quot;tax_free_amount&quot;, &quot;0&quot;); // 상품 비과세 금액 일단 0으로 설정
//        parameters.add(&quot;vat_amount&quot;, &quot;0&quot;); // 상품 부가세 금액 필수 아님, 없으면 0 설정

        parameters.add(&quot;approval_url&quot;, &quot;http://52.78.88.121:8080/account/pay/kakao/success?partner_order_id=&quot; +
                partner_order_id); // 성공 시 redirect url, 주문 번호 쿼리 스트링 추가
        parameters.add(&quot;cancel_url&quot;, &quot;http://52.78.88.121:8080/account/pay/kakao/cancel&quot;); // 취소 시 redirect url
        parameters.add(&quot;fail_url&quot;, &quot;http://52.78.88.121:8080/account/pay/kakao/fail&quot;); // 실패 시 redirect url

        // 파라미터, 헤더
        HttpEntity&lt;MultiValueMap&lt;String, String&gt;&gt; requestEntity = new HttpEntity&lt;&gt;(parameters, this.getHeaders());

        // 외부에 보낼 url
        RestTemplate restTemplate = new RestTemplate();

        KakaoReadyResponse response = null;
        try {
            response = restTemplate.postForObject(
                    &quot;https://kapi.kakao.com/v1/payment/ready&quot;,
                    requestEntity,
                    KakaoReadyResponse.class);
        } catch (RestClientException e) {
            throw new KakaoSinglePaymentReadyException();
        }

        // pg사 주문 번호 db 저장
        order.setPgUid(response.getTid());
        orderRepository.save(order);

        return response;
    }

    /**
     * 결제 승인
     * 
     * @param pgToken
     * @param partner_order_id
     */
    public void approveResponse(String pgToken, String partner_order_id) {

        // 카카오 요청
        MultiValueMap&lt;String, String&gt; parameters = new LinkedMultiValueMap&lt;&gt;();
        parameters.add(&quot;cid&quot;, cid);

        // DB 접근하여 주문 정보 가져오기
        Orders orders = this.getOrdersByPartnerOrderId(partner_order_id);

        parameters.add(&quot;tid&quot;, orders.getPgUid());

        parameters.add(&quot;partner_order_id&quot;, String.valueOf(orders.getNumber()));
        parameters.add(&quot;partner_user_id&quot;, orders.getUser().getEmail());
        parameters.add(&quot;pg_token&quot;, pgToken);

        // 파라미터, 헤더
        HttpEntity&lt;MultiValueMap&lt;String, String&gt;&gt; requestEntity = new HttpEntity&lt;&gt;(parameters, this.getHeaders());

        // 외부에 보낼 url
        RestTemplate restTemplate = new RestTemplate();

        try {
            KakaoApproveResponse approveResponse = restTemplate.postForObject(
                    &quot;https://kapi.kakao.com/v1/payment/approve&quot;,
                    requestEntity,
                    KakaoApproveResponse.class);
        } catch (RestClientException e) {
            throw new KakaoSinglePaymentApproveException();
        }

        // 주문 상태 DB 반영
        orders.setStatus(OrderStatus.PURCHASED);
        orderRepository.save(orders);


    }

    /**
     * 결제 환불
     * 
     * @param requestDTO
     */
    public void kakaoCancel(KakaoCancelRequestDTO requestDTO) {

        // DB 접근하여 주문 정보 가져오기
        Orders orders = this.getOrdersByPartnerOrderId(requestDTO.getPartner_order_id());

        // 카카오페이 요청
        MultiValueMap&lt;String, String&gt; parameters = new LinkedMultiValueMap&lt;&gt;();
        parameters.add(&quot;cid&quot;, cid);
        parameters.add(&quot;tid&quot;, orders.getPgUid());

        int cancelAmount = requestDTO.getCancel_amount();
        int totalAmount = orders.getTotalPrice();
        if (cancelAmount &gt; totalAmount) {
            throw new KakaoRefundVerificationAmountException();
        }

        parameters.add(&quot;cancel_amount&quot;, String.valueOf(requestDTO.getCancel_amount()));

        parameters.add(&quot;cancel_tax_free_amount&quot;, &quot;0&quot;); // 환불 비과세 금액 일단 0으로 설정
        parameters.add(&quot;cancel_vat_amount&quot;, &quot;0&quot;); // 환불 부가세 금액 일단 0으로 설정

        // 파라미터, 헤더
        HttpEntity&lt;MultiValueMap&lt;String, String&gt;&gt; requestEntity = new HttpEntity&lt;&gt;(parameters, this.getHeaders());

        // 외부에 보낼 url
        RestTemplate restTemplate = new RestTemplate();

        try {
            KakaoCancelResponse cancelResponse = restTemplate.postForObject(
                    &quot;https://kapi.kakao.com/v1/payment/cancel&quot;,
                    requestEntity,
                    KakaoCancelResponse.class);
        } catch (RestClientException e) {
            throw new KakaoRefundException();
        }

        // 환불 금액 DB 반영
        orders.setTotalPrice(totalAmount - cancelAmount);
        orderRepository.save(orders);
    }

    /**
     * 카카오 요청 header 생성
     *
     * @return
     */
    private HttpHeaders getHeaders() {
        HttpHeaders httpHeaders = new HttpHeaders();

        String auth = &quot;KakaoAK &quot; + admin_Key;

        httpHeaders.set(&quot;Authorization&quot;, auth);
        httpHeaders.set(&quot;Content-type&quot;, &quot;application/x-www-form-urlencoded;charset=utf-8&quot;);

        return httpHeaders;
    }


    /**
     * 주문 번호를 이용하여 DB의 주문 테이블 정보 얻기
     * 
     * @param partner_order_id
     * @return
     */
    private Orders getOrdersByPartnerOrderId(String partner_order_id) {
        // DB 접근하여 주문 정보 가져오기

        Optional&lt;Orders&gt; result = orderRepository.findByNumber(partner_order_id);
        return result.orElseThrow(KakaoDBConnectionException::new);

    }
}</code></pre>
<h3 id="환불">환불</h3>
<p>환불은 하나의 요청과 응답으로 이루어지는 로직으로 유저 - 프런트 - 백엔드(DB) - 카카오의 단순한 흐름을 가졌다. 따라서 결제 승인 또는 결제 준비의 하나의 요청으로 간단하게 구현할 수 있었다. 물론 결제 와 관련된 부분이기에 DB에서 검증하는 로직을 가진다.</p>
<h2 id="생각하고-고민한-흔적들">생각하고 고민한 흔적들</h2>
<p>흐름을 대략적으로 이해한 상황에서 구현을 진행하면서 여러가지 문제점을 겪었다. 내가 부족해서 겪은 문제도 있고 현업에서는 어떻게 했을까? 라는 생각에서 비롯된 문제도 있고 문서를 제대로 읽지 않아서 겪은 문제도 있다. 이런 문제들을 해결하는 과정을 적고자 한다.</p>
<h3 id="결제-과정에서-db-접근은-어떻게">결제 과정에서 DB 접근은 어떻게?</h3>
<p>사실 고민하고 고민해도 해결하기 어려운 문제라 멘토님의 도움을 받아 해결한 부분이다. 결제라는 것은 실제 돈이 오고가는 중요한 문제이다. 거기다가 주고 받는 정보 또한 민감한 정보가 많을 수 밖에 없다고 생각한다. 따라서 결제의 과정에서 주문의 상태가 변하는 것을 어떻게 DB에 반영할 지 많은 고민을 하였다.</p>
<p>이에대해 멘토님이 현업에서는 <strong>&quot;모든 결제 상태를 DB에 저장한다&quot;</strong>는 답변을 받았다. 나도 결제는 굉장히 중요한 부분이기에 모든 과정에서 DB에 저장하는 방식으로 안전성을 올리는 것을 생각했으나 DB에 접근하는 횟수가 늘어날 수록 성능이 저하되기에 고민했던 부분이다.</p>
<p>하지만 쇼핑몰 웹사이트에서 가장 중요한 부분을 차지하는 결제 시스템에서는 성능이 조금 저하되더라도 모든 정보를 DB에 저장하는 방식을 취하는 것이 웹사이트의 성격에 맞는 개발이라는 말을 들었다. 나는 개발 공부를 하고 프로젝트를 진행하며 DB에 접근하거나 throw를 통해 예외를 발생시키는 코드를 작성할 때마다 성능이 떨어지는 것을 걱정했다. 하지만 <strong>성능을 생각한 코딩도 중요하지만 프로젝트에 성격에 맞는 코딩이 더욱 중요한 것이다.</strong></p>
<p>카카오 페이 결제 과정에서도 결제에 관련된 로직이 진행되는 것을 주문 테이블에 계속해서 반영하여 프로젝트 성격에 맞는 코딩을 진행하여 코드를 작성했다. 물론 DB에 접근하는 횟수가 많아 결제 기능의 속도가 조금 느려졌지만 높은 안정성을 가지게 되었다고 생각한다.</p>
<h3 id="결제-환불-에서-오류가-발생하면">결제, 환불 에서 오류가 발생하면?</h3>
<p>사실 오류가 발생하는 것은 당연히 고려해야 했지만 실제 돈과 관련되었기에 신중하게 고려할 필요가 있었다. 단순하게 카카오 서버에 접근, 백엔드에서 에러, DB 접근하는 상황에서의 문제 등은 결제가 완벽하게 끝나기 전까지 금액이 실제 결제 되지 않기에 문제가 되지는 않았다.</p>
<p>중요한 것은 서버의 DB의 금액과 실제 유저의 금액에서 차이가 생길 경우의 문제였다.
예를 들면 실제 유저가 카카오 페이를 통한 결제가 완료되어 금액이 빠져나갔지만 DB에서는 결제가 완료되지 않은 상황이다. </p>
<p>이 부분도 멘토님에게 질문을 했는데 사실 이런 오류의 경우에는 오류가 발생한 로그를 확인하여 개별적인 작업을 통해서 처리한다고 들었다. 이 과정에서 유지/보수 팀에서 작업을 한다고 한다.</p>
<h3 id="결제-오류의-다양화">결제 오류의 다양화</h3>
<p>처음 코드를 작성했을 때 오류가 발생하면 전부 다 <code>throw new PaymentException()</code>으로 처리했다. 하지만 결제, 환불에서 발생하는 오류는 개별 처리가 필요하고 DB접근 오류도 개별 처리가 필요하는 등 결제 과정에서 발생하는 오류들을 세분화 할 필요가 있었다. 따라서 결제 관련 오류들을 세분화 하고 상속을 이용하여 Advice에서 하나로 처리하게 만들었다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/6bc1c1b2-3429-4286-b369-bf6708df44c6/image.png" alt=""></p>
<ul>
<li><p>ControllerAdvice</p>
<pre><code class="language-java">@org.springframework.web.bind.annotation.ControllerAdvice
public class ControllerAdvice {

  ...

  /**
   * 결제 오류 처리
   *
   * @return
   */
  @ExceptionHandler({IamportPaymentException.class, KakaoPaymentException.class})
  public ResponseEntity&lt;ResponseDTO&gt; PaymentExceptionHandler() {

      return new ResponseEntity&lt;&gt;(new ResponseDTO(&quot;400&quot;,&quot;fail&quot;), HttpStatus.OK);
  }
}</code></pre>
</li>
</ul>
<h3 id="결제-승인에-어떻게-주문-번호를-담을-수-있을까">결제 승인에 어떻게 주문 번호를 담을 수 있을까?</h3>
<p>사실 문서에 적혀있기는 하나 조금 더 강조해서 적어줬으면 좋았다고 생각을 많이한 부분이다. 해결 하기 위해 들었던 수고에 비해 내용은 정말 쉬운 부분이었다.</p>
<p>유저가 카카오 결제를 진행한 경우 카카오 서버에서 백엔드 서버로 pgToken을 쿼리 스트링으로 포함한 GET 요청으로 Redirect 요청을 보낸다. 이 과정에서 카카오 서버에서 요청을 보내는 것이기에 내가 쿼리 스트링에 정보를 넣을 수가 없었다. 거기다가 Http 요청이기에 이전 요청의 정보를 이용할 수도 없었다. 따라서 카카오 서버에서 백엔드 서버로 요청을 보낼 때 쿼리 스트링에 주문 번호 정보를 더해서 보내주어야 했다.</p>
<p>내가 요청을 하는 것이 아닌 카카오에서 요청하는 것이기에 세션을 이용하는 방안을 생각했으나 결제가 동시에 여러 건이 진행될 경우 문제가 생긴다고 생각하여 문제가 된다고 생각했다. 그렇다고 프런트에서 요청을 보내주는 방안을 생각하기에도 유저와 카카오 결제를 완료한 정보를 다시 프런트로 보내는 과정이 굉장히 불필요하고 pgToken이 탈취될 가능성도 있기에 보안상에서 큰 문제가 있었다.</p>
<p>사실 카카오 문서에 답은 있었다.</p>
<blockquote>
<p>Step 4.결제 승인
결제 대기 단계에서 결제 요청에 성공했다면 결제 승인 API를 호출합니다. 이때 결제 승인 요청을 인증하는 pg_token을 전달해야 합니다. pg_token은 사용자가 결제 수단을 선택하고 결제 버튼을 눌러 approval_url로 리다이렉트될 때, <strong>리다이렉트 요청의 approval_url에 포함된 query string으로 전달 받습니다.</strong></p>
</blockquote>
<p>저 approval_url에 쿼리 스트링을 추가하면 카카오 서버에서 Redirect할 때 쿼리 스트링을 포함해서 요청을 보내준다는 의미였다. 이 문제를 해결하기 위해 위에서 언급한 세션, 프런트 측 등 다양하게 시도해 보다가 구글링을 통해서 답을 알게 되었다.</p>
<p><a href="https://developers.kakaopay.com/forum/t/topic/177">https://developers.kakaopay.com/forum/t/topic/177</a> 에서 답을 찾을 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/de1af925-3dfa-4cf3-af3b-095deda3f252/image.png" alt=""></p>
<ul>
<li>KakaoPayService</li>
</ul>
<pre><code class="language-java">...

parameters.add(&quot;approval_url&quot;, &quot;http://52.78.88.121:8080/account/pay/kakao/success?partner_order_id=&quot; +
                partner_order_id); // 성공 시 redirect url, 주문 번호 쿼리 스트링 추가

...
</code></pre>
<h1 id="느낀점">느낀점</h1>
<h2 id="결제-흐름-이해하기">결제 흐름 이해하기</h2>
<p>지금까지 개발을 하며 만났던 가장 큰 고비였다고 생각한다. 카카오 서버로 보내는 단일 요청 조차 제대로 이해하지 못한 상황에서 결제 시스템의 큰 흐름을 이해하기까지의 과정은 쉽지 않았다. 거기다가 DB에 어떤 정보를 저장하고 정보르 주고 받는 타이밍과 횟수 등 또한 고려하다 보니 코딩 한 줄마다 &#39;여기 이렇게 쓰면 나중에 DB를 앞에서 한다고 하면 에러가 날 것이고 뒤에서 DB에 접근하면 이 코드가 조금 의미가 없어지는데 어쩌지?&#39;등의 생각을 많이 했다. </p>
<p>이런 상황에서 일단 해보자라는 생각으로 접근하기 보다는 하나씩 아주 잘게 하나씩 쪼개서 접근하기로 했다. 거기다가 코드를 작성하는 것이 아닌 요청과 응답의 흐름을 생각하여 결제 시스템의 목적에 맞는 흐름을 먼저 만든다는 생각으로 접근했다. 그렇게 코드를 먼저 작성하는 것이 아닌 큰 그림을 먼저 만들게 되었고 이를 바탕으로 코드를 작성하게 되었다.</p>
<p>이렇게 나만의 청사진을 그리고 코딩을 시작했다고 하더라도 문제점은 많았다. 청사진을 여러 번 수정하고 멘토님에게 질문을 던져가며 흐름을 바꾸며 개발을 진행했다. 처음 생각한 청사진과 다르게 변경되었지만 결제 시스템에 맞는 개발을 진행했다고 생각한다.(물론 아직 고쳐야할 점이나 부족한 점이 많은 것 같다.)</p>
<p><strong>결제 시스템을 이해, 즉 클라이언트가 요구하는 요구 사항에 맞는 목적성을 가진 개발을 중요시하여 이에 맞는 청사진을 그리고 코딩을 진행</strong>하는 경험을 한 것이 정말 값진 일이었다고 생각한다.</p>
<h2 id="구글링의-한계">구글링의 한계</h2>
<p>이번 결제 기능을 구현하면서 절실하게 느낀 점이 구글링으 한계를 절실하게 느꼈다. 위의 겪었던 문제들을 해결하면서 결제 기능에 대한 구체적인 구현을 다룬 글을 발견하기 매우 힘들었다. 대다수의 글들이 카카오 페이 서버에 한 번의 요청, 결제 준비 요청 한 건을 다루는 내용이 대부분이 었다. </p>
<p>유저, 프런트 엔드, 백엔드, DB, 카카오 서버의 흐름을 나타낸 글도 찾아볼 수 없었다. 사실 수박 겉핥기 같은 내용이 많다는 생각을 많이 하게 되었다. 비슷한 내용도 정말 많았다. 물론 도움이 되지 않았다면 거짓말이겠지만 그 이상의 도움은 받을 수 없었다.</p>
<p>확실히 사람들이 많이 다루는 내용은 정말 많지만 다루는 사람이 적어질수록 정보의 양이 급감하는 것을 체감했다. 내가 지금까지 공부하는 대부분의 내용들은 많은 사람이 경험한 내용이었지만 앞으로 내가 공부하고 개발하는 과정은 많은 사람들이 경험하지 못한 것들도 많을 거라고 생각한다. <strong>단순한 구글링과 블로그에서 벗어나 공식 문서, 깊이 있는 책을 가까이 하는 습관이 더욱 필요할 것 같다.</strong></p>
<h2 id="클론-코딩강의-책">클론 코딩(강의, 책)</h2>
<p>위의 구글링의 한계와는 조금 상충되는 말일지는 몰라도 어려운 개발을 경험하는 과정에서 이제까지 해오던 클론 코딩이 상당히 도움이 되었던 것을 많이 느꼈다. <strong>연습할 때 정말 자연스럽게 될 정도는 되어야 실전에서 해볼 수 있다.</strong>는 말이 있듯이 내가 강의를 많이 듣고 책을 많이 보고 코딩을 많이 해왔던 일들이 값지게 느껴졌다. <strong>&#39;그냥 자연스럽게 나온다&#39;</strong>라는 생각이 조금 들었다. 반대로 내가 조금 많이 연습해보지 못했던 내용들은 손에서 자연스럽게 나가지 않고 찾아보고 다시 공부해야했다.</p>
<p>말을 적어보니 당연히 많이 경험한 것을 실전에서 사용한다는 것이지 않냐라고 할 수 있겠지만, 내가 말하는 것은 많이 코딩해본 것, 즉 직접 자판기에 두들긴 횟수에 비례한다는 생각을 많이 했다. </p>
<p><strong>내가 알아도 코드로 작성하는 것은 별개다!</strong></p>
<p>지식의 양이 비슷한 두가지 구현에서 많이 작성해본 코드는 코드 작성도 쉽고 이 코드를 어떻게 발전시킬 수 있을 지 고민하고 기존 코드들과의 연관 관계도 바로 반영하여 좋은 코드로 만들었다. 하지만 알고 있는 내용일 지라도 코드로 작성해보지 않았던 지식은 정말 기본적인 코드로 밖에 구현하지 못했다. 객체지향적이지도 않고 함수형을 사용하여 간편화 하지도 못하고 기존 코드들과 어울리지 못하는 코드들을 만들어 낼 뿐이었다.</p>
<p><strong>&#39;백문이 불여일타&#39;</strong> 많이 쳐본 코드만이 내가 사용할 수 있다는 생각을 많이 하게 되었다.</p>
<h2 id="마무리">마무리</h2>
<p>결제를 담당한다고 지원했을 때는 패기롭게 지원했지만 결제 시스템을 구현하기 위해 노력하는 순간 순간 정말 포기하고 싶다는 생각을 많이 했었다. 이게 왜 이런거지!!!! 라는 절규를 매일 같이 하고 싶었다. 그래도 하나의 깨달음을 얻고 이를 코드로 구현해낼 때의 짜릿함은 이제까지 경험하지 못했기에 후회하지 않는다. 오히려 과거의 나에게 용기를 내어줘서 고맙다고 하고 싶다.</p>
<p>나의 개발 인생의 앞에 많은 장애물이 있을 것이다.(물론 실력 낮은 나에게만 있는 장애물일지 몰라도...) 그래도 이번 경험을 소중히 하고 포기하지 않는 태도를 가지고 앞으로 나아갈 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[카카오 페이 (1)]]></title>
            <link>https://velog.io/@meteor_control0/%EC%B9%B4%EC%B9%B4%EC%98%A4-%ED%8E%98%EC%9D%B41</link>
            <guid>https://velog.io/@meteor_control0/%EC%B9%B4%EC%B9%B4%EC%98%A4-%ED%8E%98%EC%9D%B41</guid>
            <pubDate>Wed, 07 Jun 2023 10:53:07 GMT</pubDate>
            <description><![CDATA[<h1 id="카카오-페이-구현">카카오 페이 구현</h1>
<p>이번 기업 연계 프로젝트 기간에 클라이언트(CEO)분께서 카카오 페이의 구현을 강하게 요구하셨다. 그리고 UI/UX 분들 또한 이번 프로젝트에서 결제 기능의 구현을 백엔드 팀과 상의하게 되었다. 사실 처음에는 결제 기능이 백엔드 팀원들에게 다루지 않았던 부분이었기에 진행하지 않는 방향으로 얘기가 되었다. 하지만 클라이언트 분께서 조금은 강하게 결제 부분이 있었으면 좋겠다고 명확하게 요구하셨고 UI/UX 팀 또한 이 부분을 강하게 어필하였다. 이런 요구 사항을 받아 백엔드 팀에서 상의 결과, 자신이 잘 알지 못하는 개발을 해야하는 과정에서 필연적으로 공부할 내용이 많아질 것이고 만약 구현이 어려울 경우 굉장히 많은 시간을 투자해야할 것이라고 판단했다. 따라서 결제 기능을 구현하고자 하는 인원이 지원할 경우에만 결제 기능을 구현하기로 했다.</p>
<h2 id="내가-지원한-이유">내가 지원한 이유</h2>
<p>사실 처음 결제 기능에 지원할 생각은 없었다. 이번 프로젝트를 진행하며 처음 생각한 부분은 프로젝트를 배포하는 AWS와 관련된 기능과 스프링 시큐리티와 JWT 관련된 기능을 담당하고 싶었다. 하지만 클라이언트분이 결제 기능의 구현을 크게 원하고 계셨기에 생각을 바꾸게 되었습니다.</p>
<h2 id="외부-api를-이용한-구현은-쉽지-않다">외부 API를 이용한 구현은 쉽지 않다.</h2>
<p>이제까지 많은 API나 라이브러리 등을 이용하여 기능을 구현할 때 정말 매우 많이 친절한 방식을 제공 받았고 조금만 구글링 하거나 chatGPT와 같은 AI 도움으로도 쉽게 해결할 수 있는 내용이 대부분이었다. 즉, 강의나 책에서 나온 내용은 내가 조금만 고민해도 구현할 수 있었고 내가 막히는 부분에서 쉽게 해결할 수 있었고 정답과 같은 코드도 많이 존재했다.</p>
<p>하지만 이제는 예시로 주어진 코드가 아닌 내가 외부 API에서 제공해주는 문서를 보고 나만의 코드를 통해 API를 구현하는 방식을 익혀야 했고 많은 시행착오 끝에 문서를 보고 코드를 만드는 능력을 기르는데 성공했다.</p>
<h1 id="결제-기능-구현을-위한-과정">결제 기능 구현을 위한 과정</h1>
<h2 id="카카오-페이-문서">카카오 페이 문서</h2>
<p><a href="https://developers.kakao.com/docs/latest/ko/kakaopay/common">카카오 developers</a></p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/f7e491ef-34fc-47e5-b510-bc278baecd24/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>Step 1.결제 준비
서버에서 결제 준비 API를 호출합니다. 응답이 오면 요청한 결제와 TID를 매핑(Mapping)하여 저장하고, 추후 결제 승인 API 호출 및 대사 작업에 사용합니다. 보안을 위해 TID가 사용자에게 노출되지 않도록 주의합니다. 응답 본문으로 받은 next_redirect_pc_url 값으로 결제 대기 화면을 팝업(Popup) 혹은 레이어(Layer) 방식으로 띄웁니다.</p>
<blockquote>
</blockquote>
<p>Step 2.사용자 정보 입력
사용자는 전화번호 및 생년월일을 입력합니다. 올바른 정보가 입력되면 카카오톡으로 결제 요청 메시지가 전송되고, 사용자 정보 입력 화면은 결제 대기 화면으로 변경됩니다. 사용자는 결제 요청 메시지를 통해 결제 화면으로 이동해 결제 수단을 선택합니다.</p>
<blockquote>
</blockquote>
<p>Step 3.결제 대기
결제 대기 화면은 결제 요청 결과에 따라 각각 다른 URL로 리다이렉트(redirect)됩니다. 사용자가 결제를 취소한 경우, 보안을 위해 주문 상세 조회 API를 호출하고 결제 과정을 중단해야 합니다. 조회 시 상태 값이 사용자가 결제를 중단한 상태임을 나타내는 QUIT_PAYMENT인 것을 확인하고 결제 중단 처리를 해야 합니다.</p>
<blockquote>
</blockquote>
<p>요청 결과별 리다이렉트 URL
요청 성공: approval_url로 리다이렉트
요청 취소: cancel_url로 리다이렉트
요청 유효 시간(15분) 경과: fail_url로 리다이렉트</p>
<blockquote>
</blockquote>
<p>approval_url, cancel_url, fail_url은 카카오페이 API 요청 응답을 받아 처리할 주소입니다. 이 값들의 도메인(Domain)은 앱 정보에 등록된 웹 플랫폼 도메인과 일치해야 합니다.</p>
<blockquote>
</blockquote>
<p>Step 4.결제 승인
결제 대기 단계에서 결제 요청에 성공했다면 결제 승인 API를 호출합니다. 이때 결제 승인 요청을 인증하는 pg_token을 전달해야 합니다. pg_token은 사용자가 결제 수단을 선택하고 결제 버튼을 눌러 approval_url로 리다이렉트될 때, 리다이렉트 요청의 approval_url에 포함된 query string으로 전달 받습니다.</p>
<blockquote>
</blockquote>
<p>응답을 받으면 결제 결과를 저장하고 사용자에게 결제 완료 화면을 보여줍니다. 결제 승인 API의 동작에 따라 사용자도 결제 완료 메시지를 받습니다.</p>
<h3 id="내가-겪은-어려움">내가 겪은 어려움</h3>
<p>카카오 페이 단건 결제에 관련된 문서를 읽으면서 가장 크게 느꼈던 점은 <strong>생소하다</strong>는 느낌이었다. 내가 이때까지 만났던 API들은 99% 완성되어 내가 원하는 매개 변수만 입력하면 되었었다. 하지만 카카오 개발자 문서에서는 주고 받는 방식과 주고 받는 정보의 2가지만을 기술하고 있었기에 &#39;어떻게 구현을 시작해야하는 거지?&#39;는 생각에 쉽게 개발을 시작하지 못 했었다.</p>
<p>거기다가 이때까지 내가 구현한 API는 전부 하나의 요청과 하나의 응답으로 이루어져 있었지만 카카오 페이의 결제 API는 준비와 승인의 2번의 요청과 응답으로 이루어져 있었기에 더욱 이해하는 데 힘이 들게 되었다. </p>
<p>그리고 카카오 서버에서 응답받는 정보들을 어떤 방식으로 방식으로 받고 이름이나 객체는 어떻게 처리할 것인지에 대한 내용도 쉽지 않았다.</p>
<h2 id="단일-요청-응답-따라하기">단일 요청, 응답 따라하기</h2>
<p>내가 직접 문서를 보고 이해하며 코드를 작성할 수 있다면 좋겠지만 내 실력이 부족한 것을 알기에 클론 코딩으로 일단 따라서 구현해보려고 했다. 다행히 카카오 페이의 구현을 다룬 곳이 꽤 있었기에 다행이라고 생각한다.</p>
<p>일단 유튜브의 <a href="https://www.youtube.com/watch?v=44ig2NoppbA&amp;t=542s">스프링 카카오 페이 API 구현</a>을 보면서 카카오 서버에 요청을 보내는 로직을 구현했다.</p>
<pre><code class="language-java">import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

@Controller
@RequestMapping(&quot;/pay&quot;)
public class PayController {

    @ResponseBody
    @RequestMapping(&quot;/kakao/single&quot;)
    public String singleKakaopay() {

        try {
            URL url = new URL(&quot;https://kapi.kakao.com/v1/payment/ready&quot;);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod(&quot;POST&quot;);

            // 카카오 인증 APP_ADMIN_KEY 입력
            conn.setRequestProperty(&quot;Authorization&quot;, &quot;KakaoAK &quot; + ADMIN키);
            // content-type
            conn.setRequestProperty(&quot;Content-type&quot;, &quot;application/x-www-form-urlencoded;charset=utf-8&quot;);
            // 연결에서 받을 정보가 있다
            conn.setDoOutput(true);

            // 카카오 서버로 보낼 정보
            String parameter = &quot;cid=TC0ONETIME&quot; // 가맹점 코드
                    + &quot;&amp;partner_order_id=partner_order_id&quot; // 가맹점 주문번호
                    + &quot;&amp;partner_user_id=partner_user_id&quot; // 가맹점 회원 id
                    + &quot;&amp;item_name=초코파이&quot; // 상품명
                    + &quot;&amp;quantity=1&quot; // 상품 수량
                    + &quot;&amp;total_amount=5000&quot; // 총 금액
                    + &quot;&amp;vat_amount=200&quot; // 부가세
                    + &quot;&amp;tax_free_amount=0&quot; // 상품 비과세 금액
                    + &quot;&amp;approval_url=http://localhost/&quot; // 결제 성공 시
                    + &quot;&amp;fail_url=http://localhost/&quot; // 결제 실패 시
                    + &quot;&amp;cancel_url=http://localhost/&quot;; // 결제 취소 시


            OutputStream outputStream = conn.getOutputStream();
            DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
            dataOutputStream.writeBytes(parameter);
            dataOutputStream.close();

            int result = conn.getResponseCode();

            InputStream inputStream;
            if (result == 200) {
                inputStream = conn.getInputStream();
            } else {
                inputStream = conn.getErrorStream();
            }

            InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String s = bufferedReader.readLine();

            System.out.println(s);

            return s;

        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


    }

}
</code></pre>
<ul>
<li><a href="https://kapi.kakao.com/v1/payment/ready">https://kapi.kakao.com/v1/payment/ready</a> 주소로 요청</li>
<li>POST 방식</li>
<li>요청 헤더에 문서에서 지정한 헤더 붙이기<ul>
<li>Authorization : &quot;KakaoAK &quot; + ADMIN키</li>
<li>Content-type : &quot;application/x-www-form-urlencoded;charset=utf-8&quot;</li>
</ul>
</li>
<li>문서에서 필수로 입력되어 있는 파라미터들 입력</li>
<li>요청을 받아서 출력해보면 문서에서 명시해준 변수명으로 응답함 (tid, next_redirect_app_url, next_redirect_pc_url ...)</li>
</ul>
<p>카카오 개발자 문서에서 적힌 정보들을 통해 이제는 내가 요청을 할 때 어떤 형식을 취해야 하는지 명확하게 이해하게 되었다. 응답에서는 어떤 내용의 응답이 돌아오고 응답에 담긴 정보를 다룰 수 있는 방법에 대한 이해도도 높아졌다.</p>
<h3 id="요청-방식을-스프링-방식으로-변경">요청 방식을 스프링 방식으로 변경</h3>
<p>위의 방식은 자바의 스트림을 이용한 방식이다. 물론 이번 프로젝트에서 자바를 사용하기에 따라 사용해도 되지만 스프링에서 제공하는 RestTemplate을 사용하여 더 좋은 방식으로 요청을 보내는 방식을 선택했다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Transactional
public class KakaoPayService {

    private final OrderRepository orderRepository;

    static final String cid = &quot;TC0ONETIME&quot;; // 가맹점 테스트 코드
    static final String admin_Key = ADMIN 키; // ADMIN 키

    /**
     * 결제 요청
     * 
     * @param requsetDTO
     * @return
     */
    public KakaoReadyResponse kakaoPayReady(KakaoPayRequsetDTO requsetDTO) {

        // 카카오페이 요청 양식
        MultiValueMap&lt;String, String&gt; parameters = new LinkedMultiValueMap&lt;&gt;();

        String partner_order_id = requsetDTO.getPartner_order_id();
        Orders order = this.getOrdersByPartnerOrderId(partner_order_id);


        // 서버와 주고 받을 정보
        parameters.add(&quot;cid&quot;, cid);
        parameters.add(&quot;partner_order_id&quot;, partner_order_id);
        parameters.add(&quot;partner_user_id&quot;, order.getUser().getEmail());
        parameters.add(&quot;total_amount&quot;, String.valueOf(order.getTotalPrice()));
        parameters.add(&quot;item_name&quot;, &quot;아이템 이름&quot;);
        parameters.add(&quot;quantity&quot;, &quot;5&quot;); // 아이템 갯수

        // 부가세, 비과세 금액으로 현재는 0으로 설정
        parameters.add(&quot;tax_free_amount&quot;, &quot;0&quot;); // 상품 비과세 금액 일단 0으로 설정
//        parameters.add(&quot;vat_amount&quot;, &quot;0&quot;); // 상품 부가세 금액 필수 아님, 없으면 0 설정

        parameters.add(&quot;approval_url&quot;, &quot;http://52.78.88.121:8080/account/pay/kakao/success&quot;); // 성공 시 redirect url
        parameters.add(&quot;cancel_url&quot;, &quot;http://52.78.88.121:8080/account/pay/kakao/cancel&quot;); // 취소 시 redirect url
        parameters.add(&quot;fail_url&quot;, &quot;http://52.78.88.121:8080/account/pay/kakao/fail&quot;); // 실패 시 redirect url

        // 파라미터, 헤더
        HttpEntity&lt;MultiValueMap&lt;String, String&gt;&gt; requestEntity = new HttpEntity&lt;&gt;(parameters, this.getHeaders());

        // 외부에 보낼 url
        RestTemplate restTemplate = new RestTemplate();

        KakaoReadyResponse response = null;
        try {
            response = restTemplate.postForObject(
                    &quot;https://kapi.kakao.com/v1/payment/ready&quot;,
                    requestEntity,
                    KakaoReadyResponse.class);
        } catch (RestClientException e) {
            throw new KakaoSinglePaymentReadyException();
        }


        return response;
    }


    /**
     * 결제 승인
     * 
     * @param pgToken
     * @param partner_order_id
     */
    public void approveResponse(String pgToken, String partner_order_id) {

        // 카카오 요청
        MultiValueMap&lt;String, String&gt; parameters = new LinkedMultiValueMap&lt;&gt;();
        parameters.add(&quot;cid&quot;, cid);

        // DB 접근하여 주문 정보 가져오기
        Orders orders = this.getOrdersByPartnerOrderId(partner_order_id);

        parameters.add(&quot;tid&quot;, orders.getPgUid());

        parameters.add(&quot;partner_order_id&quot;, String.valueOf(orders.getNumber()));
        parameters.add(&quot;partner_user_id&quot;, orders.getUser().getEmail());
        parameters.add(&quot;pg_token&quot;, pgToken);

        // 파라미터, 헤더
        HttpEntity&lt;MultiValueMap&lt;String, String&gt;&gt; requestEntity = new HttpEntity&lt;&gt;(parameters, this.getHeaders());

        // 외부에 보낼 url
        RestTemplate restTemplate = new RestTemplate();

        try {
            KakaoApproveResponse approveResponse = restTemplate.postForObject(
                    &quot;https://kapi.kakao.com/v1/payment/approve&quot;,
                    requestEntity,
                    KakaoApproveResponse.class);
        } catch (RestClientException e) {
            throw new KakaoSinglePaymentApproveException();
        }

    }


    /**
     * 카카오 요청 header 생성
     *
     * @return
     */
    private HttpHeaders getHeaders() {
        HttpHeaders httpHeaders = new HttpHeaders();

        String auth = &quot;KakaoAK &quot; + admin_Key;

        httpHeaders.set(&quot;Authorization&quot;, auth);
        httpHeaders.set(&quot;Content-type&quot;, &quot;application/x-www-form-urlencoded;charset=utf-8&quot;);

        return httpHeaders;
    }
}</code></pre>
<h4 id="별개의-요청이-가지는-문제점">별개의 요청이 가지는 문제점</h4>
<p>카카오 개발자 문서에 있는 결제 준비와 결제 승인의 두 가지 요청을 보내고 응답을 받아 정보를 이용하는 로직을 구현하는데는 성공했지만 결국 두 가지의 요청은 별개의 요청인 것이다. Http요청은 이전의 요청을 저장하지 않기에 두가지의 요청을 하나의 흐름에 올려두는 일이 필요했다. </p>
<p>그리고 결제는 단순하게 요청 두 번을 프런트에서 받아서 시행하는 방식이 아니기에 결제에대한 전체적인 흐름을 이해하는 것이 필요했다. 유저 - 프런트 - 백엔드 - DB - 백엔드 - 프런트 - 유저의 단순한 흐름이 아닌 결제만의 고유한 흐름을 명확하게 이해하는 것이 중요했다.</p>
<h2 id="구현">구현</h2>
<p>글이 너무 길어져서 결제 흐름부터는 다음 글에서 작성할 예정이다.</p>
<p><a href="https://velog.io/@meteor_control0/%EC%B9%B4%EC%B9%B4%EC%98%A4-%ED%8E%98%EC%9D%B42">구현</a></p>
<h1 id="느낀점">느낀점</h1>
<p>문서를 통해  API를 구현하는 어려움을 극복하며 느낀 점은 컸따. 하지만 단순한 어려움을 극복한 것뿐만 아니라 실제 고객(CEO)분이 존재하고 요구하신 요구 사항과 기한을 생각한 협업을 진행하며 느낀 점도 적지 않았다.</p>
<h2 id="문서만을-통한-구현">문서만을 통한 구현</h2>
<p>외부 API를 이용하기 위해 이제까지 해왔던 연습이나 클론 코딩없이 구현하는 것은 쉽지 않다는 것을 크게 느꼈다. 웹을 다루는 백엔드 개발자를 꿈꾸는 만큼 잘 해야하는 분야이기에 이번에 확실하게 이해하기 위해 많은 노력을 했다. 하지만 그럼에도 카카오에서 제공하는 문서만을 보고 실제 구현까지의 과정에 다를 때까지는 나의 실력으로 시간과 노력이 너무 많이 들 것이라고 생각했다.</p>
<p>처음 프로젝트를 진행하며 내가 지금까지 강의, 책 등을 통한 클론 코딩으로 공부한 방식에서 문제가 있다는 생각을 하게 되었다. 실제 클라이언트 분이 요구하는 요구 사항을 만족하기 위해서는 클론 코딩을 벗어나서 내가 찾아보고 내가 처음부터 구현해야만 했기 때문이다. 이런 문제점을 보완하기 위해 프로젝트를 시작한 지 얼마 안된 시점에 강의, 책, 구글링을 상당히 배제하고 스스로 공부하여 이번 프로젝트를 진행하려고 했다.</p>
<p>&#39;그러면 위의 동영상을 본건 뭐지?&#39;라고 생각할 수 있다. 내가 왜 동영상을 보고 공부하는 방식, 문제가 있다고 생각한 방식을 다시 시도한 이유는 명확한 이유가 있었다.</p>
<h2 id="✅-팀-프로젝트는-공부가-아니다">✅ 팀 프로젝트는 공부가 아니다!!!!!!!</h2>
<p>내가 진행한 프로젝트는 팀원이 공통된 목표를 달성하기 위해 노력하는 프로젝트였다. 하지만 내가 처음 프로젝트를 진행하는 방향성은 나의 발전이 상당히 큰 부분을 차지하고 있었다. 팀원과 프로젝트를 완성하는 것만큼이나 나의 발전도 중요하다고 생각했기에 공부와 카카오 개발 문서만을 보고 구현을 하려고 노력했었다.</p>
<p>그러다가 한 유튜브를 보면서 생각을 바꾸게 되었다. 시간이 좀 지나 전체적인 내용이 희미해졌지만 대략적으로 이제 막 취직을 한 신입 개발자가 자신이 잘 모르는 일을 하면서 겪은 문제와 해결한 방법 그리고 선배 개발자의 피드백이었다. </p>
<blockquote>
<p>신입 개발자가 잘 모르는 기능에 대한 구현을 해야하는 상황에 직면했다. 신입 개발자인 만큼 부족한 부분도 많았기에 자신의 모르는 부분을 열심히 공부해서 3<del>4일에 걸쳐서 이 문제를 해결했다. 그리고 선배 개발자 분에게 자신이 어려움을 겪었지만 시간이 걸리더라도 자기 <strong>스스로</strong> 공부를 하여 문제를 해결했다고 말했다. 선배 개발자의 피드백이 중요했는데 **&quot;팀 프로젝트에서 너가 3</del>4일에 걸쳐서 해결한 문제는 나에게 질문했을 경우 3<del>4 시간에 해결될 문제였다. 따라서 너에게 남은 3</del>4일은 다른 기능을 구현할 수 있었고 프로젝트의 더 많은 기능의 구현을 맡아 진행할 수 있었고 프로젝트가 더 많이 진행됐을 거다.&quot;** 였다.</p>
</blockquote>
<p>물론 실제 기업에서 근무하고 있는 신입 개발자를 위한 피드백이기에 나에게는 조금 다를 수 있었지만 결국 프로젝트의 성격이 중요한 것이었다. 이번 프로젝트는 나의 성장과 발전이 중요한 것이 아닌 UI/UX, 프런트 엔드, 백엔드 팀원들과 함께 공통된 목표를 달성하기 위한 프로젝트였다. 내가 프로젝트를 대하는 태도가 잘못된 것이었다.</p>
<p><strong>기한이 존재하고 모든 팀원이 클라이언트의 요구사항을 만족하는 웹 사이트를 개발하기 개발하는 팀 프로젝트는 내가 공부하는 곳이 아니다!!!</strong></p>
<p>그렇기에 카카오 페이 결제 시스템을 구현하는 동영상을 보고 클론 코딩을 하며 개발을 진행했다. 그리고 이런 클론 코딩을 또 다시 한번 하면서 내 생각도 다시 바뀌게 되었다.</p>
<h2 id="클론-코딩-강의-책을-통한-공부">클론 코딩, 강의, 책을 통한 공부</h2>
<p>사실 많은 유튜브에서 클론 코딩, 강의, 책을 통해서 하는 공부는 한계가 명확하다 좋지 않다는 내용을 다룬 영상을 많이 봤다. 나 또한 이런 따라하는 공부에 대해 경계를 하고 있지만 혼자 공부하는 상황에서는 클론 코딩을 많이 선택하게 되었다. 그리고 이번 팀 프로젝트를 시작하면서 뭔가 크게 잘못 된 방법이라는 생각을 하게 되었는데 카카오 페이 구현을 클론 코딩하면서 생각이 다시 한번 더 바뀌게 되었다.</p>
<p><strong>내가 너무나도 모르기에 처음부터 공부하여 하나씩 혼자 힘으로 구현하는 것이야 말로 진짜 제대로 된 개발 공부다!</strong> 따라서 프로젝트를 시작하며 하나씩 전부 다 공부하려고 했다. 위에서 말했듯 중간에 생각이 변하게 되어 공부하는 과정이 아닌 팀 프로젝트를 위한 개발을 하려고 생각을 바꾸게 되어 다시 한번 더 유튜브 동영상을 보며 클론 코딩을 진행하며 이 생각은 바뀌게 되었다.</p>
<p>&#39;모방은 과연 잘못 된 것일까?&#39; 모방은 창조의 어머니라는 말이 있듯이 클론 코딩이 잘못된 방식이라는 생각을 조금 다시 생각하게 되었다. 내가 전부다 하나씩 알면서 구현할 수 있다면 그것이 맞는 일이겠지만 나는 그런 실력이 되기 어렵다는 것을 잘 알게 되었다. 사실 공부라는 것이 모르는 것을 알아가는 과정이고 이 과정에서 따라하는 방식은 잘못되지 않았다고 다시 한번 느끼게 되었다. 그리고 많은 선배 개발자 분들이 &#39;클론 코딩은 진짜 코딩이 아니다&#39;라는 말을 하신 것이 어떤 의미인지를 명확하게 알게 되었다.</p>
<p>내가 모르는 것을 알기 위해 클론 코딩을 통해 공부를 하는 것은 좋은 공부 방식의 한 방식이라고 생가한다. 하지만 클론 코딩만을 하는 것이 큰 문제였던 것이다. 내가 어떤 문제를 만났고 이를 해결하기 위해 문제점을 파악하고 구현 방식을 결정하고 어떤 API, 라이브러리, 함수 등을 사용할 지 결정하고 구현 과정에서 만나는 에러와 문제점을 해결하는 과정은 굉장히 가변적이다. 따라서 내가 스스로 생각하여 문제를 해결, 구현 방식 결정 ... 등 스스로 생각해야만 하는 것이다. 책과 강의 등은 이런 과정의 한가지 예만을 보여주는 것이었다. 따라서 공부를 진행하며 코드를 바라보는 것이 아닌 문제와 구현을 바라보는 관점을 더 중요시 하면서 공부를 진행해야만 했다.</p>
<p>앞으로도 나는 계속해서 강의와 책을 통해서 공부를 진행할 것이다. 하지만 이제는 <strong>책이나 강의에서 나오는 코드가 아닌 필자가 문제를 해결하는 과정, 생각하는 사고 방식에 집중하며 공부를 진행할 것이다.</strong></p>
<h2 id="마무리">마무리</h2>
<p>단순한 개발이 아닌 UI/UX, 프런트 엔드, 다른 백엔드 팀원과의 소통하며 진행하는 팀 프로젝트는 적응하기 쉽지 않았다. 그렇기에 프로젝트를 진행하면서 계속해서 많은 변화가 있었다. 단순한 나의 공부 방식이나 개발 방법에 관한 생각이 아닌 팀 프로젝트를 대하는 생각과 팀원들과 함께 공통된 목표를 달성하기 위해 노력하는 방식, 태도, 생각 또한 많이 바뀌게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[깃허브 / 애자일 프로젝트 세팅]]></title>
            <link>https://velog.io/@meteor_control0/%EA%B9%83%ED%97%88%EB%B8%8C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@meteor_control0/%EA%B9%83%ED%97%88%EB%B8%8C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%B8%ED%8C%85</guid>
            <pubDate>Tue, 06 Jun 2023 23:40:08 GMT</pubDate>
            <description><![CDATA[<h2 id="깃허브">깃허브</h2>
<p><a href="https://github.com/private-project-shopping-mall/shopping-mall">연습 깃허브 주소</a></p>
<p><a href="https://github.com/private-project-shopping-mall/shopping-mall">https://github.com/private-project-shopping-mall/shopping-mall</a></p>
<p>깃허브를 자주 사용하는 입장에서 깃허브의 프로젝트 세팅을 어떻게 하는 것이 과연 좋을 것인가?에 대한 질문을 자주 던졌었다. 하지만 자주 고민하는 것과는 별개로 지식이 부족하였고 이렇게 하는 것이 맞는 것인가?에 대한 생각을 많이 하였었다. 하지만 여러 공부와 질문들을 통해 나름대로 깃허브 프로젝트를 진행하는 방법을 구축했다고 생각하여 글로 남겨보려고 한다.</p>
<h3 id="애자일">애자일</h3>
<p>프로젝트 방법론 중 애자일에 관하여 공부 또한 진행하였다. 이를 깃허브 프로젝트에 적용하여 연습하고 앞으로의 프로젝트에 적용, 발전해 나갈 생각이다.</p>
<h1 id="깃허브-프로젝트">깃허브 프로젝트</h1>
<p>깃허브에서 제공하는 많은 기능을 이용하여 프로젝트 진행 상황을 보기 좋게 만들고 팀원들과 소통하기 좋은 방향으로 세팅하는 방법이다.</p>
<h2 id="이슈">이슈</h2>
<p>깃허브 프로젝트에서 가장 중요하다고 생각하는 것이 이슈이다.</p>
<h3 id="라벨">라벨</h3>
<p>깃허브에서 이슈나 풀리퀘스트에 붙일 수 있든 기본적인 라벨을 제공한다. 하지만 제공받는 라벨이나 여러 깃허브 프로젝트에서 보이는 라벨들이 표현하는 한계가 존재한다 생각하여 이를 해결하기 위해 다른 방식으로 라벨을 만들었다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/9a5fde01-3da4-4475-92e9-a17b53be2c1c/image.png" alt=""></p>
<ul>
<li>라벨에 4가지 태그를 붙여서 라벨의 종류를 구분하여 이슈나 풀리퀘스트의 성격을 잘 드러내도록 하였다.<ol>
<li>Domain : 어떤 작업인지 종류를 정하도록 만들었다.(프런트, 백, 문서 ...)</li>
<li>우선 순위 : 우선 순위를 나타냄으로 팀원들이 어떤 작업을 우선시 해야하는지 명확하게 알 수 있게 하였다.</li>
<li>상태 : 지금 진행중인 이슈가 어떤 상태인지 표시하여 이슈나 풀리퀘스트가 어던 상황에 있는지 알 수 있게 하였다.</li>
<li>타입 : 이슈나 풀리퀘스트에 어떤 작업이 진행되고 있는지 알렸다.</li>
</ol>
</li>
</ul>
<p>깃허브에서 제공하는 라벨이나 많은 다른 프로젝트에서 제공하는 라벨에서 바로 이 이슈가 어떤 상황인지 알 수 있을 정도로 많은 정보를 주지 못했으나 라벨을 변경함으로써 이슈를 직접 눌러 확인하지 않아도 Project에서 바로 이슈가 어떤 상태에 있는지 어떤 진행 상황인지 등 많은 정보를 한 눈에 확인할 수 있게 만들었다.</p>
<h3 id="milestones">MileStones</h3>
<p>깃허브 마일스톤은 애자일 방식을 선택했기에 Sprint라는 이름을 붙여서 사용하였다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/64aa1ebb-b362-4d7e-8b62-e6272fda2598/image.png" alt=""></p>
<p>기한을 정하고 어떤 일을 해야하는지 나누어 작성하여 어떤 일을 해야하는지 명확하게 나타내었다.</p>
<h3 id="template">Template</h3>
<p>이슈 템플릿을 만들어 팀원들이 이슈를 작성할 때 공통되는 방식으로 작성하게 만들었다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/e65e9a04-fac7-4188-887b-76d4ab2451b7/image.png" alt=""></p>
<p>템플릿과 담당자, 라벨, 프로젝트 등을 설정하여 이슈의 상태를 명확하게 나타냈다.</p>
<h2 id="풀리퀘스트">풀리퀘스트</h2>
<h3 id="template-1">Template</h3>
<p>이슈처럼 템플릿을 만들어 사용함으로써 공통된 풀리퀘스트가 만들어 지도록 하였다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/6daebd7b-cdf7-4109-b614-50859382271d/image.png" alt=""></p>
<h2 id="위키">위키</h2>
<p><a href="https://github.com/private-project-shopping-mall/shopping-mall/wiki">https://github.com/private-project-shopping-mall/shopping-mall/wiki</a></p>
<p>프로젝트와 관련된 정보를 글로 작성하는 곳으로 나는 처음 활용해본 공간이다.</p>
<h3 id="컨밴션">컨밴션</h3>
<p>우리 팀의 규칙을 작성한 공간으로 코드 컨밴션, 깃 컨밴션 등 다양한 규칙을 정할 수 있는 공간이다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/bfd29a3e-3ccf-4141-82a9-26187b839888/image.png" alt=""></p>
<h3 id="데일리-스크럼">데일리 스크럼</h3>
<p>매일 실행하는 데일리 스크럼을 작성하는 공간으로 매일 작성하도록 한다. </p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/e219856f-1a46-42fd-83e4-925c5bbfe723/image.png" alt=""></p>
<p>팀에서 진행하고 있는 이슈들을 작성하고 각 팀원이 어제는 어떤 일을 진행했고 진행상황이나 문제점에 대해 작성한다. 이를 통해 팀원들의 진행 상황을 매일 확인하여 일의 분배나 팀원 간의 협업 등을 결정하는데 도움을 준다.</p>
<h3 id="스프린트-회고">스프린트 회고</h3>
<p>스프린트가 진행된 후의 일들을 회고한 일을 작성하는 공간으로 나를 되돌아 볼 수 있게 한다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/858dc3c1-f424-48c0-a5eb-7dda44dad961/image.png" alt=""></p>
<p>이번 스프린트 동안 진행했던 작업에 관한 회고록으로 어떤 점이 좋았는지 뭘 배웠는지 아쉬웠던 점이 무었인지 작성하여 이를 통해 성장하는 과정을 가질 수 있도록 한다.</p>
<h2 id="project">Project</h2>
<p>현재 진행중인 프로젝트를 한 눈에 볼 수 있게 해준다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/20e9d806-7d26-4686-b699-ab090e02f50b/image.png" alt=""></p>
<ul>
<li>이슈나 풀리퀘스트가 어떤 상태에 있는지 한 눈에 확인 할 수 있다.</li>
<li>라벨을 구체적으로 작성하여 이슈의 세부적인 내용을 좀 더 구체적으로 확인할 수 있다.</li>
<li>이슈나 풀리퀘스트가 생성되거나 닫히는 일이 발생하면 자연스럽게 Backlog나 Done로 이동하게 연동하였다.</li>
<li>이슈의 오른쪽 위 상단의 담당자를 보이게 하여 누가 이슈를 진행하고 있는지 쉽게 알 수 있다.</li>
</ul>
<h1 id="느낀점">느낀점</h1>
<h2 id="깃허브-1">깃허브</h2>
<p>깃허브에 연습하는 프로젝트를 많이 작성하였지만 이런 기능들은 거의 사용해본 적이 없었다. 기껏해봐야 README를 좀 더 꼼꼼히 작성하는 일이 전부였다. 물론 혼자 공부의 목적으로 만든 프로젝트에 적용하기에는 너무 과한 내용이 대부분이라서 하지 않은 점도 있지만 관심이 없었다는 말이 좀 더 정확할 것이다. 그저 깃허브를 내 프로젝트를 저장하는 하나의 공간으로 생각하고 있었다.</p>
<p>연습을 진행하고 깃허브의 많은 부분들을 건드려보면서 깃허브 개발자들이 상당히 많은 부분을 신경쓰고 있고 현재도 기능들을 만들고 베타 버전으로 제공하고 있다는 것이다. 이번에 사용해본 기능들도 굉장히 큰 도움이 된다고 생각했지만 앞으로는 더욱 더 많은 기능들을 사용해 볼 수 있을 것 같아서 기대가 되는 부분이 많았다.</p>
<h2 id="공통화">공통화</h2>
<p>이슈/ 풀리퀘스트 템플릿 작성, 라벨, 위키 컨밴션 등 개발자들이 협업을 하는 과정에서 팀원들끼리 공통적으로 사용하게 하는 것이 얼마나 중요한 지 알게 되었다. 단순히 코드를 작성하는 규칙을 정하고 커밋하는 규칙만을 정하는 것이 아닌 이슈를 작성하는 방법을 정하고 이런 이슈에 라벨을 어떤 것을 붙이고 Project의 단계 위치시키는 일들을 공통화 하는 것이 중요하다고 생각하게 되었다. </p>
<h2 id="라벨-1">라벨</h2>
<p>정말 크게 느낀 부분이 라벨이었다. 라벨만 조금 구체적으로 바꾸었을 뿐인데 정말 많은 부분이 달라졌다고 느꼈다. 이전에는 라벨을 붙이더라도 기껏해봐야 백엔드, 개발 이 정도의 라벨이 붙었기에 매번 이슈를 클릭하여 확인을 해야 했고 이런 이슈의 갯수가 5개만 넘어가더라도 이 작업이 매우 귀찮게 느껴졌다. 심지어 이슈를 확인하더라도 명확한 정보를 얻지 못하여 일일이 팀원에게 질문을 해야만 하는 상황이 문제가 된다고 생각했는데 이번 라벨을 구체적으로 작성하는 방법으로 정말 많은 발전을 할 수 있게 되었다.</p>
<h2 id="느낀점-1">느낀점</h2>
<p>앞으로 많은 개발자들과 협업을 해나갈 것이고 애자일이나 다른 방법론을 경험할 것이라고 생각한다. 이번에 진행한 연습을 통해 모든 협업을 대비하는 일은 아닐지라도 앞으로의 협업을 대하는 방식의 큰 변화가 생긴 것 같다. 그저 하나의 프로젝트에 코드를 같이 작성하는 것이 협업이 아니라고 생각하게 되었다. </p>
<blockquote>
<p>도움 받은 곳
<a href="https://seantrane.com/posts/logical-colorful-github-labels-18230/">https://seantrane.com/posts/logical-colorful-github-labels-18230/</a>
<a href="https://github.com/TeamCooks/TwoSpoon">https://github.com/TeamCooks/TwoSpoon</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[SpringBoot 3 -> SpringBoot2]]></title>
            <link>https://velog.io/@meteor_control0/Springdoc</link>
            <guid>https://velog.io/@meteor_control0/Springdoc</guid>
            <pubDate>Wed, 26 Apr 2023 19:55:22 GMT</pubDate>
            <description><![CDATA[<h1 id="상황">상황</h1>
<p>팀 프로젝트를 진행하는 중 springdoc을 추가하면서 springBoot의 버전이 3인 것을 확인하였다. 부트 버전이 3인 것이 우리 프로젝트에 도움이 되지 않는다고 생각하여 이 점을 팀원들에게 어필하고 변경하는 과정을 겪었고 이 과정에서 발생한 오류나 공부하는 방식 등을 기록하며 돌아보려고 한다.</p>
<h2 id="프로젝트">프로젝트</h2>
<ul>
<li>java 17</li>
<li>AWS</li>
<li>Intellij</li>
<li>gradle</li>
<li>springboot3 -&gt; springboot2</li>
</ul>
<h1 id="해결-과정">해결 과정</h1>
<h2 id="springboot3">springBoot3</h2>
<p>팀 프로젝트를 진행 중에 팀장님이 AWS를 통해서 서버를 올리는 작업까지 해주셨는데 이 과정에서 조금 문제가 생긴 것이 스프링 부트의 3버전을 사용하셨다. 사실 문제라고 하기는 그렇지만 워낙 최신의 버전이라 많은 Open API를 사용하는 과정에서 많은 변화가 있는 것이 문제가 되었다. </p>
<p>이 외에도 많은 문제점이 있기에 3버전이 아닌 2버전으로 버전을 옮기는 과정을 제안했고 팀원 분들의 동의를 얻어서 2버전으로 옮기려고 한다.</p>
<h2 id="springboot2-vs-springboot3">springboot2 vs springboot3</h2>
<p>사실 먼저 해결하기 보다 일단 springboot2와 springboot3의 변경된 부분과 변경하기 위한 과정들을 살펴 보았다. <a href="https://spring.io/blog/2022/05/24/preparing-for-spring-boot-3-0">스프링 부트 3 준비</a>, <a href="https://www.baeldung.com/spring-boot-3-migration">마이그레이션 방법</a></p>
<ol>
<li>java 17</li>
</ol>
<p>기본이 자바 17이다. 사실 자바 11버전을 주로 공부했기에 17버전으로 사용하더라도 별로 기능을 사용하지 못하기는 한다. 그래서 사실 큰 문제는 없다. 오히려 여러가지 호환성 문제에서 17이 더 좋은 측면이 많은 것 같다. Record 기능에 대한 관심이 있기에 공부를 해볼 생각이다.</p>
<ol start="2">
<li>Configuration Properties</li>
</ol>
<p>일부 키가 변경되었는데 따라서 Maven이나 Gradle에서 라이브러리를 입력하는 과정에서 약간의 변화가 생겼다. </p>
<ul>
<li>spring.redis -&gt; spring.data.redis</li>
<li>spring.data.cassandra -&gt; spring.cassandra</li>
<li>...</li>
</ul>
<ol start="3">
<li>Jakarta EE 10</li>
</ol>
<p>가장 익숙하지 않은 곳으로 Jakarta로 변경되고 있다는 사실만 알고 있을 뿐이지 사용을 해본적이 없어서 내가 Springboot3이 아닌 2버전으로 다시 낮추는 과정을 팀원들에게 제안한 이유다.</p>
<p>이전에 Validation관련된 Hibernate를 사용하는 과정에서 이런 저런 문제점을 겪었었는데 이번 프로젝트에도 Jpa를 사용하기에 버전을 내릴 것을 제안했다.</p>
<ol start="4">
<li>그 외...</li>
</ol>
<p>스프링 시큐리티, 하이버네이트, 스프링 배치 등 스프링 생태계에 전반적인 변화가 있었기에 사용해본 적이 없이 그냥 스프링 부트라는 이름으로 사용하기에는 조금 버거운 감이 없지 않은 것 같다.</p>
<h2 id="2버전으로의-변화">2버전으로의 변화</h2>
<p>사실 프로젝트 초반에 2버전으로 변환한 것이기에 큰 변화는 없었다. 프로젝트가 어느정도 진행된 시점에서 2버전으로 전환하려고 했다면 하나의 큰 작업이 되었을 것 같아서 다행이라고 생각한다.</p>
<p>일단 gradle에서 부트를 변환했다.</p>
<p><strong>build.gradle</strong></p>
<pre><code class="language-java">plugins {
    id &#39;java&#39;
    id &#39;org.springframework.boot&#39; version &#39;2.7.11&#39; // &#39;3.1.0&#39; -&gt; &#39;2.7.11&#39;
    id &#39;io.spring.dependency-management&#39; version &#39;1.1.0&#39;
}

...</code></pre>
<p>그 후에 프로젝트를 일단 실행해서 오류를 찾기로 했다. 사실 프로젝트가 올라가기도 전에 IntelliJ에서 오류를 찾아줬는데</p>
<pre><code class="language-java">@Bean
    public WebSecurityCustomizer webSecurityCustomizer(){
        log.info(&quot;-------------web configure-------------&quot;);
//        return (web) -&gt; web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
        return (web) -&gt; web.ignoring().requestMatchers(
//                &quot;/**&quot;,
                &quot;/swagger-ui/**&quot;,
                &quot;/api-docs/**&quot;,
                &quot;/test&quot;
        );
        // &quot;/**&quot;  임시로 모든 보안 해제시 셋팅!
    }</code></pre>
<p>에서 requestMatchers를 변경해달라는 것이었다. 여기서 requestMatchers를 antMatchers로 변경하면 문제없이 작동하게 된다. 확실히 변경점이 적은 것이 다행이라고 생각한다.</p>
<h3 id="requsetmatchers">requsetMatchers</h3>
<p>requsetMatchers는 처음 보는 내용이었기에 약간 찾아보았다. 그리고 스프링 부트 3에서 사용한다는 것은 지금 사용중인 스프링 시큐리티의 더 높은 버전이기에 내가 앞으로 사용하게 될 기능이라고 생각하여 찾아보았다.</p>
<p>일단 부트2 에서 부트3 으로 변경되면서 스프링 시큐리티 또한 6버전으로 설정이 되어있다. 일단 스프링 시큐리티 6버전의 전체적인 내용을 이해하기에는 조금 벅차기에 requsetMatchers에 집중했다.</p>
<p>일단 requestMatchers가 스프링 시큐리티 5버전에서 적용되었기에 이에 맞는 <a href="https://docs.spring.io/spring-security/reference/5.8/migration/servlet/config.html#use-new-requestmatchers">스프링 시큐리티 5의 공식 문서</a>에서 requestMatcher를 찾았다. </p>
<blockquote>
<p>Use the new requestMatchers methods
In Spring Security 5.8, the antMatchers, mvcMatchers, and regexMatchers methods were deprecated in favor of new requestMatchers methods.
The new requestMatchers methods were added to authorizeHttpRequests, authorizeRequests, CSRF configuration, WebSecurityCustomizer and any other places that had the specialized RequestMatcher methods. The deprecated methods are removed in Spring Security 6.
These new methods have more secure defaults since they choose the most appropriate RequestMatcher implementation for your application. In summary, the new methods choose the MvcRequestMatcher implementation if your application has Spring MVC in the classpath, falling back to the AntPathRequestMatcher implementation if Spring MVC is not present (aligning the behavior with the Kotlin equivalent methods).
To start using the new methods, you can replace the deprecated methods with the new ones. For example, the following application configuration:</p>
</blockquote>
<p>일단 대부분의 내용이 대체된다. 바뀌어야한다. 등의 기존의 작성했던 방식을 바꿔야 한다는 내용이 많다. 그 중에서 이전에 사용했던 antMatchers 부분을 바꿔야 한다고 적혀있고 팀장님이 이 내용을 따라서 requestMatchers 메서드를 사용하였고 내가 버전을 내리면서 다시 antMatchers로 변경하였다.</p>
<p>이 외에도 mvcMatchers나 regexMatchers 등의 다양한 Matchers를 하나의 requestMatchers로 표현할 수 있어 개발자들에게 더 좋은 방식을 제공한다.</p>
<p>사실 이 공식 문서 전체를 읽고 해석하는데에는 문제가 있어 가장 큰 장점을 Chat GPT에 물어 봤다.</p>
<blockquote>
<p>Let me know what you like using requestMatchers</p>
</blockquote>
<p>좋아진 점을 알려달라는 질문을 했고</p>
<blockquote>
<p>As an AI language model, I do not have personal preferences or feelings. However, I can tell you that requestMatchers is a useful feature in Spring Security as it allows for more fine-grained control over which requests are intercepted and processed by the security filter chain. With requestMatchers, you can define more specific criteria for matching requests, such as matching on HTTP methods, request parameters, headers, cookies, and more.
This feature can be particularly helpful when dealing with complex security requirements and when you want to selectively apply security measures to only certain endpoints or URLs. It can also help improve the performance of your application by reducing the number of requests that need to be processed by the security filter chain.
However, it is important to note that requestMatchers is only available in Spring Security 5 and above, and is not supported in earlier versions of Spring Security. If you are using an earlier version of Spring Security, you may need to use a different approach to achieve similar functionality.</p>
</blockquote>
<p>Chat GPT는 스프링 시큐리티의 requestMatchers를 사용하면서 가장 좋아진 점으로 세밀한 제어가 가능한 점을 짚었다. 그리고 처리해야하는 요청의 수를 줄여 성능 개선 또한 이루어 냈다고 말한다. 이에 대한 예시를 물어 보았고 다음과 같은 코드를 주었다.</p>
<ul>
<li><strong>기존 코드</strong><pre><code class="language-java"></code></pre>
</li>
</ul>
<p>@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .antMatchers(&quot;/admin/<strong>&quot;).hasRole(&quot;ADMIN&quot;) // permit access to admin endpoints only for users with ADMIN role
            .antMatchers(&quot;/css/</strong>&quot;, &quot;/js/<strong>&quot;, &quot;/images/</strong>&quot;).permitAll() // permit access to static resources
            .antMatchers(&quot;/h2-console/**&quot;).permitAll() // permit access to H2 console
            .anyRequest().authenticated() // any other request must be authenticated
            .and()
        .formLogin()
            .loginPage(&quot;/login&quot;)
            .permitAll()
            .and()
        .logout()
            .permitAll();
}</p>
<pre><code>
- **개선된 코드**
```java
@Configuration
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // permit access to static resources
                .requestMatchers(PathRequest.toH2Console()).permitAll() // permit access to H2 console
                .anyRequest().authenticated() // require authentication for all other requests
                .and()
            .formLogin() // enable form-based login
                .and()
            .logout(); // enable logout
    }
}</code></pre><p>가장 큰 차이점은 이제 더 이상 모든 경로를 따옴포&quot;&quot; 안에 작성할 필요가 없다는 것이다. 이제는 toStaticResources()나 toH2Console()과 같은 메서드로 작업할 수 있어졌다. 이를 통해서 더 이상 잘못 입력하는 문제나 **처럼 모든 경로라는 의미의 애매한 내용을 사용할 일이 없어졌다. 따라서 모든 경로의 요청을 보내는 것이 아닌 H2의 정확한 엔드포인트를 적용하기에 필터의 단계가 줄어들어 성능을 향상 시킬 수 있다.</p>
<p>분명 좋은 기능인 것은 사실이나 현재 우리가 사용하는 다른 라이브러리나 API등을 위해서 포기해야하는 기능이라고 생각한다. 최신의 기술을 가져와 적용함으로써 기능 향상을 노리는 것은 좋으나 그에 따른 포기해야할 다른 기술이 있다는 것을 명심하자.</p>
<h2 id="springdoc">springdoc</h2>
<p>일단 처음에는 springdoc을 스프링 부트 3버전에 맞게 해결하는 과정을 시도하려고 했다.</p>
<p><a href="https://springdoc.org/v2/">springdoc_v2</a> springdoc의 공식 홈페이지에도 스프링 부트 3버전을 사용할 경우 적용하는 법이 나와있다. 처음에는 이 문서를 참고해 springdoc의 2.1.0 버전을 적용하여 Swagger UI를 구현하려고 했다. </p>
<p>일단 반 정도 구현하는 과정에서 생각한 것이 <strong>&#39;이번 프로젝트를 하면서 많은 외부 Open API와 라이브러리를 사용할 것인데 괜찮나?&#39;</strong>라는 걱정이 커져갔다. 3버전으로 업데이트 하는 과정이 상세하게 적힌 공식 문서나 그래도 나름 괜찮은 글이 적힌 블로그가 있었지만 양의 차이가 엄청났다. 스프링의 가장 큰 장점 중의 하나가 다수의 사람들이 이용하기에 존재하는 큰 커뮤니티였는데 3버전을 이용할 경우 이런 커뮤니티를 이용하지 못하는 점이 2버전으로 내려가는 데 큰 역할을 했다.</p>
<p>일단 Maven Repository에서 가장 많이 사용하는 springdoc 디펜던시를 찾아서 추가했다.</p>
<pre><code class="language-java">// https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-ui
implementation group: &#39;org.springdoc&#39;, name: &#39;springdoc-openapi-ui&#39;, version: &#39;1.6.9&#39;</code></pre>
<p>그 후에 서버를 작동하고 <a href="http://localhost:8080/swagger-ui/index.html%EC%97%90">http://localhost:8080/swagger-ui/index.html에</a> 접속을 했다.
이전에 팀장님이 swagger의 uri를 스프링 시큐리티에서 권한 설정을 마쳐 줬기에 단순하게 dependcy만 추가해도 잘 작동하였다.</p>
<pre><code class="language-java">@Log4j2
@RequiredArgsConstructor
@EnableMethodSecurity
@EnableWebSecurity
@Configuration
public class SecurityConfig {

    ...

    // 필터에서 제외시킬 url 등록
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer(){
        log.info(&quot;-------------web configure-------------&quot;);
//        return (web) -&gt; web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
        return (web) -&gt; web.ignoring().antMatchers(
//                &quot;/**&quot;                 // 임시로 모든 보안 해제시 셋팅!
                &quot;/v3/api-docs/**&quot;,
                &quot;/swagger-ui/**&quot;,
                &quot;/test&quot;
        );

    }
}</code></pre>
<p>그런데 Swagger에 접속을 해보니 Failed to load remote configuration.라면서 api들을 Swagger에서 읽어오지를 못하고 있다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/ed87938a-7031-4547-8b88-f8b02bbfeb74/image.png" alt=""></p>
<p>왜 읽어오지 못하는 지 개발자 도구의 네트워크를 확인해 보니</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/decdf10e-c0fa-45b7-985a-538216a5b4e7/image.png" alt=""></p>
<p>403 에러로 즉 권한에 문제가 있기에 불러오지 못하고 있었다. 이를 해결하기 위해 시큐리티 설정에 &quot;/api-docs/**&quot;을 추가해 권한을 부여하였다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/799f14e7-d570-4086-babf-c4d36eaa1d30/image.png" alt=""></p>
<p>그랬더니 이번에는 404에러가 나왔다. 못 찾는다는 의미였다. 이를 해겨하기 위해서는 springdoc의 추가 설정이 필요했고 property 파일에 설정을 추가해서 문제를 해결했다.</p>
<p><strong>application.properties</strong></p>
<pre><code># springdoc Swagger 설정
# swagger-ui로 접속
springdoc.packages-to-scan=project.finalproject1backend
springdoc.default-consumes-media-type=application/json;charset=UTF-8
springdoc.default-produces-media-type=application/json;charset=UTF-8
springdoc.swagger-ui.path=/swagger-ui
springdoc.swagger-ui.tags-sorter=alpha
springdoc.swagger-ui.operations-sorter=alpha
springdoc.api-docs.path=/api-docs/json
springdoc.api-docs.groups.enabled=true
springdoc.cache.disabled=true</code></pre><h1 id="생각">생각</h1>
<h2 id="버전-도구">버전, 도구</h2>
<p>프로젝트를 시작하면서 빠르게 스프링 부트 3버전을 사용함에 있어 생기는 또는 생길 여러 문제를 미리 발견한 점이 다행이라고 생각한다. 스프링 부트 3버전이 점점 발전하면서 IDE인 IntelliJ의 기본 버전이 3버전으로 되어 있어 나도 여러번 3버전으로 시작한 적이 있었다. 그러면서 Validation이나 하이버네이트 쪽을 쓰면서 뭔가 알 수 없는 에러나 정말 이해가 안가는 상황이 몇 번 있었고 이를 통해서 버전을 잘 설정하는 것이 중요하다고 배웠기에 발견할 수 있었다고 생각한다.</p>
<p>내가 부트 버전을 잘못 선택해서 겪은 문제들을 사실 클릭 한번, 프로젝트 만들면서 관심 한 번 주지 않은 것으로 시간을 날렸다고 많이 생각했었는데 이번 기회에 이런 에러를 만난 경험을 살려서 기존의 실수들이 다 의미가 있었다고 생각한다.
또한 *<em>프로젝트를 시작하는 단계에서 어떤 버전을 사용하고 어떤 도구를 사용할 지 정하는 것이 굉장히 중요하다는 생각을 가지게 되었다. *</em> 나 혼자 진행하는 프로젝트나 작업에서는 전혀 고려하지 않은 문제로 팀 프로젝트를 진행하는 과정에서 고려해야할 중요한 사항이라는 것을 배우게 되었다.</p>
<h2 id="에러-해결-과정">에러 해결 과정</h2>
<p>공부를 하고 작업을 하면서 이제는 에러 해결하는 과정이 상당히 매끄러워진 것을 느낀다. 기존에는 문제가 생겼다? 그러면 일단 문제 에러 로그를 복사해서 구글에 검색해서 어떻게든 해결 방법만 찾으려고 노력했다면 이제는 문제의 원인 -&gt; 공부(공식 문서, 블로그, Chat GPT) -&gt; 내가 생각하는 해결 방법 (-&gt; 디버깅) -&gt; 문제 해결의 순서로 내가 이 문제가 발생한 이유를 명확하게 알고 해결하는 방식을 습득하고 적용하고 있다. 그리고 이런 과정에서 이번 에러와 조금은 결이 다르지만 그래도 배우면 좋은 것, 배워야 할 것 등 여러가지 많은 정보를 배우고 있다. 이번에는 스프링 시큐리티의 5버전의 requestMatchers가 있다. </p>
<p>또한 지식이 많이 늘어서 이제는 문제가 발생하면 크롬의 개발자 도구를 사용하여 무슨 에러가 나는지 에러 로그가 어떤 의미인지를 파악하고 이를 이용하여 스프링 시큐리티에 적용하는 <strong>나의 지식으로 문제를 해결하는 능력이 생겼다</strong>고 생각한다. 모든 문제는 아니지만 그래도 작은 문제들은 의존하지 않고도 문제를 해결할 수 있는 능력을 가지게 된 것이다.</p>
<p>검색에 나오지 않거나 조금만 내가 원하는 검색 내용이 나오지 않으면 걱정을 태산같이 하던 내가 바뀌게 된 것을 느끼게 되었다.</p>
<h2 id="팀원들에게-제안">팀원들에게 제안</h2>
<p>사실 단순하게 springboot3을 2버전으로 낮추자고 했다면 내 의견에 힘이 없었을 것이고 팀원들도 받아들이기 힘들었을 거라고 생각한다. 경험과 지식을 토대로 팀원들에게 springboot3이 아닌 2버전이 가지는 장점(더 많은 정보, 지원하는 라이브러리나 Open API)과 단점(Validation, 하이버네이트와 같은 프로젝트에서 사용할 라이브러리나 API들의 사용법 변경)을 기반으로 의견을 제시 했기에 제안이 받아들여졌다고 생가한다. </p>
<p><strong>공부를 열심히 했기에 받아들여졌다.</strong>라는 경험이 값지게 느껴졌다.</p>
<h2 id="마무리">마무리</h2>
<p>어찌보면 간단한 오류 해결 과정이었지만 이번 오류를 해결하면서 어떻게 해서든 구글 검색이나 블로그를 통해서 해결 과정만을 찾던 방식을 완전하게 벗어난 것을 느꼈다. 이제는 구글에 검색을 하더라도 해결 방식을 찾는 것이 아닌 <strong>왜 오류가 발생했는지</strong>를 찾게 되었다. 그리고 문제를 <strong>공식 문서를 통해서 이해하는 과정</strong>을 꼭 가지고 이런 과정에서  Chat GPT와 같은 인공 지능 또한 활용해 학습의 속도를 올리는 방법도 익히게 되었다. </p>
<p><strong>지식의 양을 늘려서 문제를 해결한다.</strong>라는 간단한 말을 현실로 옮기기 위해 계속해서 노력할 것이다.</p>
<blockquote>
<p>도움 받은 곳
<a href="https://velog.io/@devmin/springdoc">https://velog.io/@devmin/springdoc</a>
<a href="https://docs.spring.io/spring-security/reference/5.8/migration/servlet/config.html#use-new-security-matchers">https://docs.spring.io/spring-security/reference/5.8/migration/servlet/config.html#use-new-security-matchers</a>
<a href="https://stackoverflow.com/questions/70906081/springboot-swagger3-failed-to-load-remote-configuration">https://stackoverflow.com/questions/70906081/springboot-swagger3-failed-to-load-remote-configuration</a>
<a href="https://www.baeldung.com/spring-boot-3-migration">https://www.baeldung.com/spring-boot-3-migration</a>
<a href="https://nahwasa.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-30%EC%9D%B4%EC%83%81-Spring-Security-%EA%B8%B0%EB%B3%B8-%EC%84%B8%ED%8C%85-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0">https://nahwasa.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-30%EC%9D%B4%EC%83%81-Spring-Security-%EA%B8%B0%EB%B3%B8-%EC%84%B8%ED%8C%85-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0</a>
<a href="https://revf.tistory.com/260">https://revf.tistory.com/260</a>
<a href="https://springdoc.org/">https://springdoc.org/</a>
<a href="https://springdoc.org/v2/">https://springdoc.org/v2/</a>
<a href="https://chat.openai.com/">https://chat.openai.com/</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Exception 처리 구현]]></title>
            <link>https://velog.io/@meteor_control0/Exception-%EC%B2%98%EB%A6%AC-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@meteor_control0/Exception-%EC%B2%98%EB%A6%AC-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Mon, 24 Apr 2023 19:10:26 GMT</pubDate>
            <description><![CDATA[<h1 id="상황">상황</h1>
<p>스프링 부트 프로젝트에서 회원 가입을 하는 상황에서 여러 정보를 받는 상황에서 아이디의 길이가 짧거나 비밀번호가 양식에 맞지 않는 등 여러 종류의 Exception이 발생하게 된다. 이런 예외를 어떻게 하면 한 곳에서 처리하고 더 간편한 방법으로 처리할 수 있는 방식을 찾으면서 내가 겪었던 일들이다.</p>
<h2 id="프로젝트">프로젝트</h2>
<ul>
<li>java</li>
<li>springboot</li>
<li>spring security</li>
<li>spring data jpa</li>
<li>h2 DB</li>
</ul>
<h2 id="초기-생각">초기 생각</h2>
<p>내가 가장 중요하게 생각한 점은 <strong>공통화</strong>였다. 지금은 내가 회원 가입에서 받는 정보 중에서 내가 정한 양식(regex)에 맞지 않는 정보에 대한 예외를 발생시키지만, 다른 개발자는 게시판이나 댓글과 같은 상황에서도 이런 정보들의 양식을 검증하는 일을 할 것이라고 생각했다. 따라서 회원 정보를 검증하는 로직이라는 생각보다 어떤 정보든 내가 검증하길 원하는 정보는 어느 곳에서나 검증하고 그런 검증에 발생하는 오류 메세지를 한 곳에서 처리하고 예외처리 또한 한 곳에서 만드는 것에 집중했다.</p>
<ul>
<li>검증 로직을 어느 곳에서나 사용할 수 있게 만들고 싶다.</li>
<li>에러 메세지를 한 곳에서 관리하고 싶다.</li>
<li>예외 처리 로직을 한 곳에서 처리하고 싶다.</li>
<li><strong>공통된 부분은 모아서 처리하고 싶다.</strong></li>
</ul>
<h1 id="구현">구현</h1>
<h2 id="메세지-공통화">메세지 공통화</h2>
<p>Validation을 통해서 정보를 검증하는 로직의 메세지를 공통화하고 싶었기에 스프링의 MessageSource를 이용하였다.</p>
<p>MessageSource를 이용하기 위해 먼저 Configuration에 Bean으로 등록하고 </p>
<pre><code class="language-java">@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    /**
     * validation 메세지 위치
     *
     * @return the message source
     */
    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource source = new ReloadableResourceBundleMessageSource();
        source.setBasename(&quot;classpath:config/messages/message&quot;);
        source.setDefaultEncoding(&quot;UTF-8&quot;);
        source.setDefaultLocale(Locale.KOREA);
        return source;
    }

}
</code></pre>
<ul>
<li>MessageSource 빈 등록</li>
<li>message properties 파일 위치 지정</li>
<li>인코딩, 지역 설정</li>
</ul>
<p>현재 프로젝트는 한국에서 사용할 예정이고 이를 default로 설정하였다.</p>
<pre><code>MemberSaveForm.account.NotEmpty=계정을 입력해주세요.
MemberSaveForm.account.Email=이메일 형식이 아닙니다.
MemberSaveForm.password.NotEmpty=비밀번호를 입력해주세요.
MemberSaveForm.password.Pattern=비밀번호는 숫자+문자를 조합한 6~12자리만 입력하실 수 있습니다.
...</code></pre><p>현재는 Member 객체와 관련된 메세지만 존재하지만 다른 개발자가 쉽게 메세지를 추가할 수 있다.</p>
<p>ex)</p>
<pre><code>
BoardSaveForm.title.NotEmpty=제목을 입력해주세요
BoardSaveForm.title.Lenth=제목의 길이는 3~20글자 사이입니다.
...</code></pre><h2 id="검증-순서">검증 순서</h2>
<p>회원 가입을 진행할 때 아이디와 이름이 동시에 검증을 통과하지 못하면 어떻게 될까? 처음에는 이런 생각을 하지 않고 구현하였는데 Swagger UI에서 검증하는 과정에서 아이디의 값을 검증을 통과하지 못하게 입력하였는데 이름의 길이에 문제가 있다는 메세지를 받게 되었다.</p>
<p>이런 상황에서 나는 위에서부터 즉 아이디 - 비밀번호 - 이름 등의 순서대로 검증을 통과하지 못하는 정보에 관한 에러 메세지를 발생시키길 원했다. 그래서 @GroupSequence에 순서를 나타내는 ValidationStep 클래스로 순서를 만들어 검증 순서를 정해주었다.</p>
<p><strong>ValidationSteps</strong></p>
<pre><code class="language-java">public class ValidationSteps {

    public interface Step1 {}
    public interface Step2 {}
    public interface Step3 {}
    public interface Step4 {}
    public interface Step5 {}
    public interface Step6 {}
    public interface Step7 {}
    public interface Step8 {}
    public interface Step9 {}
    public interface Step10 {}
}</code></pre>
<p>만약 더 많은 검증을 원하고 이를 위한 순서를 생성하고 싶다면 추가적으로 쉽게 만들 수 있다.</p>
<p><strong>MemberSaveForm</strong></p>
<pre><code class="language-java">@Data
@AllArgsConstructor
@GroupSequence({
        MemberSaveForm.class,
        ValidationSteps.Step1.class,
        ValidationSteps.Step2.class,
        ValidationSteps.Step3.class,
        ValidationSteps.Step4.class,
        ValidationSteps.Step5.class,
        ValidationSteps.Step6.class,
        ValidationSteps.Step7.class,
        ValidationSteps.Step8.class,
        ValidationSteps.Step9.class,
        ValidationSteps.Step10.class,
})
public class MemberSaveForm {

    @NotEmpty(groups = ValidationSteps.Step1.class,
            message = &quot;{MemberSaveForm.account.NotEmpty}&quot;)
    @Email(groups = ValidationSteps.Step2.class,
            message = &quot;{MemberSaveForm.account.Email}&quot;)
    private String account;

    @NotEmpty(groups = ValidationSteps.Step3.class,
            message = &quot;{MemberSaveForm.password.NotEmpty}&quot;)
    // 숫자와 문자 포함 형태의 6~12자리 이내의 암호 정규식
    @Pattern(regexp = &quot;^[A-Za-z0-9]{6,12}$&quot;, groups = ValidationSteps.Step4.class,
            message = &quot;{MemberSaveForm.password.Pattern}&quot;)
    private String password;

    @NotEmpty(groups = ValidationSteps.Step5.class,
            message = &quot;{MemberSaveForm.name.NotEmpty}&quot;)
    @Length(min = 2, max = 5, groups = ValidationSteps.Step6.class,
            message = &quot;{MemberSaveForm.name.Length}&quot;)
    private String name;

    @NotNull(groups = ValidationSteps.Step7.class,
            message = &quot;{MemberSaveForm.birth.NotEmpty}&quot;)
    @Past(groups = ValidationSteps.Step8.class,
            message = &quot;{MemberSaveForm.birth.Past}&quot;)
    private LocalDate birth;

    @NotNull(groups = ValidationSteps.Step9.class,
            message = &quot;{MemberSaveForm.phoneNumber.NotEmpty}&quot;)
    @Pattern(regexp = &quot;^01(?:0|1|[6-9])(?:\\d{3}|\\d{4})\\d{4}$&quot;, groups = ValidationSteps.Step10.class,
            message = &quot;{MemberSaveForm.phoneNumber.Pattern}&quot;)
    private String phoneNumber;

}
</code></pre>
<p>이를 이용해서 내가 원하는 순서대로 검증할 수 있는 로직을 구현하였다. @GroupSequence를 사용하기 위해서 @Valid가 아닌 @Validated 사용해야 했다.</p>
<h2 id="예외-처리">예외 처리</h2>
<p>@ControllerAdvice 어노테이션을 이용하여 하나의 클래스에서 예외를 처리했다. 예외 처리를 모아서 한 군데에서 처리했다.</p>
<p><strong>RegisterExceptionAdvice</strong></p>
<pre><code class="language-java">@ControllerAdvice
public class RegisterExceptionAdvice {

    /**
     * 사용중인 계정 에러 처리, 400
     *
     * @return the http entity
     */
    @ExceptionHandler(AlreadyRegisteredUserException.class)
    public HttpEntity&lt;Response&gt; alreadyRegisteredUserExceptionHandler() {

        Response response =
                new Response(&quot;400&quot;, &quot;이미 사용중인 아이디입니다.&quot;);
        return ResponseEntity.badRequest().body(response);

    }

    /**
     * validation 실패시 에러 처리, 400
     *
     * @param e the e
     * @return the response entity
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity&lt;Response&gt; methodValidException(MethodArgumentNotValidException e) {
        BindingResult bindingResult = e.getBindingResult();
        String errorMessage = bindingResult.getAllErrors().get(0).getDefaultMessage();
        Response response = new Response(&quot;400&quot;, errorMessage);
        return ResponseEntity.badRequest().body(response);
    }

    /**
     * Aes256 암호화, 복호화 서버 에러, 500
     *
     * @return the response entity
     */
    @ExceptionHandler(AES256EncodingException.class)
    public ResponseEntity&lt;Response&gt; AES256EncodingException() {

        Response response = new Response(&quot;500&quot;, &quot;서버 장애입니다.&quot;);
        return ResponseEntity.internalServerError().body(response);
    }
}</code></pre>
<p>회원이나 모든 등록 관련한 예외 처리를 할 수 있도록 만들었고 @ExceptionHandler 어노테이션을 붙인 추가적인 예외처리도 가능하다.</p>
<p>내가 예외 처리 로직을 짜면서 신경 쓴 부분은 어떤 정보를 클라이언트 서버로 보내야 하는 것이었다. 지금 내가 짠 Response는 다음과 같다.</p>
<p><strong>Response</strong></p>
<pre><code class="language-java">@Data
@AllArgsConstructor
@NoArgsConstructor
public class Response {
    private String code;
    private String message;
}</code></pre>
<p>지금은 2개의 변수만을 가지지만 초기 Response클래스를 만들 때는 변수가 4~5개 정도가 있었다. 서버의 상태를 조금 더 자세하게 표현하여 클라이언트 서버에서 구축하는데 도움을 주려고 했다. 그런데 이 방식이 그렇게 좋지 않다는 생각을 하게 되었다. </p>
<ul>
<li>일단 많은 정보를 주고 받는 다는 것은 결국 서버간의 통신하는데 부담이 커진다는 것이다. 가볍게 만들수록 비용은 적게, 속도는 빠르게 작동할 것이다.</li>
<li>내가 프런트 개발자들을 위해서 많은 정보를 적는 것이 좋아보이지만 오히려 많은 정보를 적을수록 이 응답 객체를 알아보기 힘들어 진다는 것이다. 내가 메서드의 매개변수로 객체 1개만 주고 받는 것과 객체의 모든 변수를 주고 받는다고 생각하면 적을수록 좋은 것이다.</li>
<li>많은 정보를 준다는 것은 서버에서 어떤 방식으로 동작하는 지에 대한 정보를 많이 주는 것으로 보안에도 좋지 않다. 민감한 데이터를 실수로 보낼 수도 있고 Response객체의 정보가 많아서 실제 Member객체의 구성을 예측하게 만들 수도 있으니 조심해야한다.</li>
<li>프런트 개발자 분들이 정보가 적을 수록 최소한의 정보를 다루고 이를 통해서 주고 받는 과정에서 오류가 발생할 가능성도 적어진다. 거기다가 프런트 개발자 분들의 개발 자유성이 높아지기에 서로간에 더 높은 질의 코드를 만들어 낼 수 있다.</li>
</ul>
<p>사실 간단한 생각으로 Response 코드의 변수는 뭘 담느냐를 생각했었는데 사실 내가 먼저 고려해야했던 점은 통신이라는 개념이고 이후에는 보안 그리고 다른 개발자들의 관점에서 바라보는 것이었다.</p>
<h1 id="생각">생각</h1>
<h2 id="백엔드">백엔드</h2>
<p>백엔드의 다른 개발자가 내가 만든 API를 사용한다는 생각으로 최대한 공통화해서 구현하고 쉽게 추가, 삭제, 수정할 수 있도록 코드를 만들었는데 쉽지 않다는 생각을 많이 했다. 제일 크게 와 닿은게 중간에 주말 동안 개인적인 일이 있어서 코드를 많이 보지 못했는데 겨우 2일 동안 코드를 보지 않았을 뿐인데, <strong>&quot;내가 이렇게 코드를 짰었나? 음... 이거 왜 이렇게 짯지? 이거 작동하려면 그냥 매개변수만 넣으면 되나?&quot;</strong> 정말 많은 생각을 하게 만들었다.</p>
<p>협업의 과정에서 분명 내가 만든 코드를 남이 보게 될 것이기에 코드 하나를 짜더라도 더 좋은 코드를 생각하게 되었다.</p>
<h2 id="프런트-엔드">프런트 엔드</h2>
<p><strong>&#39;내가 더 많은 정보를 주면 더 좋지 않을까?&#39;</strong> 라는 짧은 생각을 돌아보게 되었다. 내가 생각하는 로직의 대부분의 매개변수를 Response에 넣어서 다시 보내주면 API를 만들기 편하겠지? 라는 이기적인 생각, 내가 만약 5~6개나 되는 매개변수를 가진 API를 사용해야 한다는 생각을 해보면 쉽지 않다는 것을 알고 있으면서도 이런 잘못된 행동을 했다. 객체지향적인 관점에서도 객체 사이에선 그저 단순한 메세지로 통신하고 구현은 각자의 객체에게 맡겨야 하는데 내가 프런트 엔드의 API에도 간섭하려고 했던 아주 잘못된 행동을 할 뻔했었던 것이다. </p>
<p>앞으로 협업의 과정에서 내가 다른 개발자를 위한다는 생각으로 잘못된 코드를 만드는 일을 하지 않도록 노력할 것이다.</p>
<h2 id="마무리">마무리</h2>
<p>다른 개발자들의 협업을 생각하며 더 좋은 코드를 짜기 위해 노력하는 일이 정말 쉽지 않다고 느꼈다. 특히, 프런트 개발자들과 협업하는 과정을 생각하면서 만들었던 Response 객체를 만드는 과정이 많은 생각을 하게 만들었다. </p>
<p><strong>더 좋은 코드를 만드는 일에는 정말 많은 생각이 필요하다는 것을 다시 한번 느끼게 되었다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[암호화]]></title>
            <link>https://velog.io/@meteor_control0/%EC%95%94%ED%98%B8%ED%99%94-%EB%B3%B5%ED%98%B8%ED%99%94</link>
            <guid>https://velog.io/@meteor_control0/%EC%95%94%ED%98%B8%ED%99%94-%EB%B3%B5%ED%98%B8%ED%99%94</guid>
            <pubDate>Thu, 20 Apr 2023 20:29:01 GMT</pubDate>
            <description><![CDATA[<h1 id="상황">상황</h1>
<p>내가 진행 중인 웹 프로젝트에서 암호화와 관련된 요구사항이 있었고 이를 구현하는 과정에서 느낀 점들을 정리하고자 한다.</p>
<h2 id="프로젝트">프로젝트</h2>
<p>일단 내가 진행했던 프로젝트에서 사용했던 기술들을 나열하고자 한다.</p>
<ul>
<li>java</li>
<li>springboot</li>
<li>spring security</li>
<li>spring data jpa</li>
<li>h2 DB</li>
</ul>
<h2 id="요구-사항">요구 사항</h2>
<ul>
<li>스프링 시큐리티를 적용할 것</li>
<li>웹 사이트의 회원 가입 상황에서 유저 객체의 정보를 DB에 저장할 때 민감한 개인 정보를 암호화하여 저장할 것</li>
<li>회원 정보 수정 시 DB에 암호화 되어 저장된 정보를 복호화 하여 사용할 것</li>
<li>암호 과정에서 AES256 방식을 사용할 것</li>
</ul>
<h2 id="처음-내-생각">처음 내 생각</h2>
<p>처음 암호화에 대한 생각은 너무 단순하게 DB에 저장할 때 &#39;데이터를 바꿔서 넣으면 된다&#39;의 단순한 생각이었다. 그래서 깊은 생각없이 구현 부분만 집중적으로 생각하여서 코딩을 진행했다.</p>
<h2 id="초기-구현">초기 구현</h2>
<h3 id="스프링-시큐리티">스프링 시큐리티</h3>
<p>스프링 시큐리티에서도 무조건 암호화 해야만 하는 부분이 존재하는데 바로 Password 부분이다. 실제로 PasswordEncoder를 사용하지 않으면 제대로 스프링 부트 프로젝트가 실행되지 않는다. 이런 PasswordEncoder 중에서 나는 가장 많이 사용하는 BCryptPasswordEncoder를 사용하였다.</p>
<pre><code class="language-java">@Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }</code></pre>
<h3 id="aes256encoder">AES256Encoder</h3>
<p>프로젝트에서 요구하는 유저의 개인 정보들을 저장하기 위해서 사용한 암호화 방식으로 AES256Encoder를 요구 받았고 이를 구현했다. </p>
<pre><code class="language-java">package com.miniproject.miniprojectgroupthree.util;

import com.miniproject.miniprojectgroupthree.domain.Member;
import com.miniproject.miniprojectgroupthree.error.AES256EncodingException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

import static java.nio.charset.StandardCharsets.UTF_8;


public class AES256Encoder {
    public static String alg = &quot;AES/CBC/PKCS5Padding&quot;;
    private final String key = &quot;12345678910111213&quot;;
    private final String iv = key.substring(0, 16); // 16byte

    /**
     * Encode 문자열.
     *
     * @param text the text
     * @return the string
     */
    public String encodeString(String text) {
        try {
            Cipher cipher = getCipher(Cipher.ENCRYPT_MODE);

            byte[] encrypted = cipher.doFinal(text.getBytes(UTF_8));
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
                 InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
            throw new AES256EncodingException(e);
        }
    }

    /**
     * Decode 문자열.
     *
     * @param cipherText the cipher text
     * @return the string
     */
    public String decodeString(String cipherText) {
        try {
            Cipher cipher = getCipher(Cipher.DECRYPT_MODE);

            byte[] decodedBytes = Base64.getDecoder().decode(cipherText);
            byte[] decrypted = cipher.doFinal(decodedBytes);
            return new String(decrypted, UTF_8);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
                 InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
            throw new AES256EncodingException(e);
        }
    }

}
</code></pre>
<p>AES256Encoder를 만든 후 JPA의 repository를 통해서 save하려고 했다. 그런데 조금 문제가 생겼는데 너무 많은 하드 코딩?이 MemberService에 표시해야만 했다.</p>
<pre><code class="language-java">Member member = Member.builder()
                    .account(aes256Encoder.encodeString(form.getAccount()))
                    .password(passwordEncoder.encode(form.getPassword()))
                    .name(aes256Encoder.encodeString(form.getName()))
                    .phoneNumber(aes256Encoder.encodeString(form.getPhoneNumber()))
                    .role(Role.ROLE_USER)
                    .build()
                            );</code></pre>
<h1 id="실제-구현">실제 구현</h1>
<h2 id="객체-지향적-코딩">객체 지향적 코딩</h2>
<p>처음 했던 아주 심각한 하드 코딩을 어떻게 하면 더 좋은 코드로 바꿀 수 있을까? 라는 고민을 하며 이런 저런 방법을 생각했다. 그러다 보니 자연스럽게 객체지향적인 코드로 바꿀 수 있다면 좋겠다는 생각에 닿았고 내가 읽었던 <a href="https://product.kyobobook.co.kr/detail/S000001628109">객체지향의 사실과 오해</a>의 객체의 상호작용 과정에 대해서 생각하게 되었다. 서비스 객체와 AES256Encoder 객체 사이에서 Member 객체를 주고 받는 과정에서 객체 사이의 협력 과정에서 객체가 알아야 할 정보는 정말 필요한 정보 뿐이라는 것이다.  여기서 필요한 정보는 그저 Member 객체를 주고 받는 것이다. </p>
<p><strong>AES256Encoder</strong></p>
<pre><code class="language-java">/**
     * Encode Member객체.
     * {account, name, phoneNumber, birth} 암호화
     *
     * @param member the member
     * @return the member
     */
    public Member encodeMember(Member member) {

        try {
            Cipher cipher = getCipher(Cipher.ENCRYPT_MODE);

            member.setAccount(Base64.getEncoder().encodeToString(cipher.doFinal(member.getAccount().getBytes(UTF_8))));
            member.setName(Base64.getEncoder().encodeToString(cipher.doFinal(member.getName().getBytes(UTF_8))));
            member.setPhoneNumber(Base64.getEncoder().encodeToString(cipher.doFinal(member.getPhoneNumber().getBytes(UTF_8))));


            return member;
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
                 InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
            throw new AES256EncodingException(e);
        }
    }


    /**
     * Decode Member객체.
     * {account, name, phoneNumber, birth} 복호화
     *
     * @param member the member
     * @return the member
     */
    public Member decodeMember(Member member) {
        try {
            Cipher cipher = getCipher(Cipher.DECRYPT_MODE);

            member.setAccount(new String(cipher.doFinal(Base64.getDecoder().decode(member.getAccount())), UTF_8));
            member.setName(new String(cipher.doFinal(Base64.getDecoder().decode(member.getName())), UTF_8));
            member.setPhoneNumber(new String(cipher.doFinal(Base64.getDecoder().decode(member.getPhoneNumber())), UTF_8));


            return member;

        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
                 InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
            throw new AES256EncodingException(e);
        }
    }</code></pre>
<p><strong>MemberService</strong></p>
<pre><code class="language-java">public void signup(MemberSaveForm form) {
        memberRepository.findByAccount(aes256Encoder.encodeString(form.getAccount()))
                .ifPresentOrElse(
                        user -&gt; {
                            throw new AlreadyRegisteredUserException();
                        },
                        () -&gt; {
                            Member member = aes256Encoder.encodeMember(
                                    Member.builder()
                                            .account(form.getAccount())
                                            .password(passwordEncoder.encode(form.getPassword()))
                                            .name(form.getName())
                                            .phoneNumber(form.getPhoneNumber())
                                            .role(Role.ROLE_USER)
                                            .build()
                            );

                            memberRepository.save(member);
                        }
                );
    }</code></pre>
<p>실제로 객체의 정보를 암호화 하는 부분을 객체 내부로 감춰서 서비스 객체에서는 AES256Encoder 객체가 하는 일은 알지 못하게 느슨한 결합을 할 수 있게 만들었다. 이로 인해서 서비스 객체에서만 AES256Encoder를 이용하는 것이 아닌 다른 곳에서도 사용할 수 있게 객체를 하나의 독립적인 섬과 같은 존재로 바꾸었다. 다시 말해 재사용성을 높여 다른 개발자가 내가 적은 주석만으로도 인코더 객체를 쉽게 사용할 수 있게 만들었다.</p>
<h2 id="어떤-정보를-복호화할까">어떤 정보를 복호화할까?</h2>
<p>처음 암호화하는 인코더 객체의 설계와 서비스 객체의 구현 등에서 제대로 생각하지 않고 코딩한 부분이다. 그래서 어떤 정보를 복호화하고 암호화 해야 하는 것인지 정확하게 몰랐다. </p>
<h3 id="단방향과-양방향">단방향과 양방향</h3>
<p>암호화를 할 때 중요한 것은 &quot;다시 되돌릴 상황이 있는가?&quot;라고 생각한다. 이와 같이 생각한 부분을 쉽게 만날 수 있는 것이 바로 회원 정보 수정이다. 네이버나 깃허브 등 회원 정보를 가진 많은 웹사이트에서 회원 정보 수정 화면을 보게 되면 많은 정보는 수정이 가능하게 이미 작성된 정보가 보이는 방식으로 구현되어 있다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/39219c4d-e6e6-459a-8824-e3859f8938a7/image.png" alt=""></p>
<p>하지만 그 어떤 웹 사이트에서 비밀번호를 수정할 때 미리 내가 이전에 입력해 둔 비밀번호가 입력되어 있던 경우는 본 적이 없다. 대신 새로운 비밀번호를 입력 받고 이전의 비밀번호 위에 새롭게 저장하는 형태로 되어 있다.</p>
<h4 id="단방향">단방향</h4>
<p>이와 같은 방식은 단방향과 양방향으로 설명될 수 있다. 단방향은 한쪽 방향으로 흐르는 다시 원래 데이터로 돌릴 수 없는 방식으로 비밀번호나 주민등록번호와 같은 정보를 암호화 하는데 사용한다. 내가 이번 프로젝트에서 사용한 방식은 PasswordEncoder의 BCryptPasswordEncoder방식으로 해시 알고리즘을 이용한 방식이다. </p>
<h4 id="양방향">양방향</h4>
<p>회원 정보 수정 화면에서 알 수 있듯이 내가 DB에 암호화 하여 저장한 정보를 다시 복호화 하여서 페이지에 표시하기 위해서는 다시 원래 데이터로 돌릴 수 있는 방식이다. 내가 사용한 방식은 AES256방식으로 개인 정보 중에서 다시 복호화할 이름, 계정, 휴대전화 와 같은 정보를 양방향으로 구현하였다.</p>
<h2 id="어떤-정보까지-암호화-할까">어떤 정보까지 암호화 할까?</h2>
<p>&#39;생년월일 또한 암호화 해야할까?&#39;라는 궁금증에서 시작된 것이다. <strong>암호화란 결국 누군가가 우리의 정보를 가져갔을 때 사용자를 특정하거나 악용할 여지가 있는 정보를 감추는 작업이라고 생각한다</strong>. </p>
<p>나는 이번 프로젝트인 &#39;회사에서 사용하는 연차/당직&#39;이에서 누군가가 정보를 가져간다고 가정해보았다. 내가 생각하기에 아무리 큰 회사라 할지라도 <strong>생년월일 정보로 회사원을 특정 지을 수 있다</strong>고 생각했다. 따라서 생년월일도 추가적으로 암호화 하기로 했다. 이 과정에서도 단방향, 양방향을 고려했고 내가 사용해왔던 웹 사이트들에서 볼 수 있듯이 복호화 하여 웹사이트에서 표시되는 정보이기에 양방향으로 암호화 하였다.</p>
<p><strong>AES256Encoder</strong></p>
<pre><code class="language-java">
public Member encodeMember(Member member) {

        try {
            Cipher cipher = getCipher(Cipher.ENCRYPT_MODE);

           ...
            member.setBirth(Base64.getEncoder().encodeToString(cipher.doFinal(member.getBirth().getBytes(UTF_8))));

          ...
        }
    }

...
</code></pre>
<h2 id="bean-등록">Bean 등록</h2>
<p>내가 이번에 새롭게 자바 클래스를 만들면서 고민한 것은 빈으로 등록하냐 마냐 였다. 클래스를 만들 때마다 고민하는 부분인데 이번에 조금 더 확실하게 알게 된 것 같다. 일단 내가 고려한 부분이다.</p>
<ul>
<li>싱글톤으로 관리되었으면 좋겠다.</li>
<li>AES256Encoder가 변하지 않았으면 좋겠다.</li>
<li>객체를 생성해서 사용하지 않기에 다른 빈 클래스에서 쉽게 사용할 수 있다.</li>
<li>다른 개발자가 바로 생성자 주입 방식으로 이용하기 편하다.</li>
</ul>
<p>내가 AES256Encoder를 빈으로 등록하였기에 서비스 객체에서 객체를 생성하지 않고 간단한 코드만으로 바로 사용할 수 있었다.</p>
<pre><code class="language-java">
@Service
@RequiredArgsConstructor
public class MemberService {

    ...

    private final AES256Encoder aes256Encoder;

    ...

 }
</code></pre>
<h2 id="람다식-적용">람다식 적용</h2>
<p>이전에는 람다식을 잘 사용을 못했다. 사실 못하는 것을 넘어서 사용하기 무서워했다. 그러다가 Optional에 대해서 공부할 일이 생겨서 공부하게 되고 null의 관리가 너무나도 쉽게 변하는 과정을 겪어보니 람다식에 대한 관심이 점점 강해졌다. 그래서 이번 프로젝트에서 람다식을 적극적으로 활용하여 프로젝트를 진행하게 되었다.</p>
<pre><code class="language-java">memberRepository.findByAccount(aes256Encoder.encodeString(form.getAccount()))
                .ifPresentOrElse(
                        user -&gt; { // ifPresent로 구성하여 그냥 아래 코드와 분리하기
                            throw new AlreadyRegisteredUserException();
                        },
                        () -&gt; {
                            Member member = aes256Encoder.encodeMember(
                                    Member.builder()
                                            .account(form.getAccount())
                                            .password(passwordEncoder.encode(form.getPassword()))
                                            .name(form.getName())
                                            .birth(form.getBirth().toString())
                                            .phoneNumber(form.getPhoneNumber())
                                            .role(Role.ROLE_USER)
                                            .build()
                            );

                            memberRepository.save(member);
                        }
                );</code></pre>
<p>물론 이게 잘된 일인지는 약간 모르겠다. 왜냐하면 내가 서비스 클래스에 만든 람다식이 거의 10줄에 가까운 코드인데 오히려 가독성이 더 좋지 않은 코드를 만들게 된 것이 아닌가? 라는 그런 걱정도 섞여있다. 그래서 최근에 람다식에 관련된 공부를 시작하려고 한다.</p>
<h1 id="생각">생각</h1>
<h2 id="내가-중요하다고-생각한-부분">내가 중요하다고 생각한 부분</h2>
<p>내가 이번 프로젝트를 진행하면서 가장 중요하다고 여긴 부분은 협업 과정에서 다른 개발자가 내가 만든 API를 쉽고 편하게 사용하도록 만드는 것이었다. 그래서 Springdoc을 이용하여 문서화하고 쉽게 테스트 해보도록 만들었다. 이와 같이 암호화하는 과정의 클래스에서 다른 개발자가 어떻게 하면 더 편하고 쉽게 사용할 수 있을까를 많이 고민하였다. </p>
<p>그래서 객체지향적인 관점에서 바라보았고 따라서 원래라면 다른 개발자가 DB에 어떤 정보가 어떤 방식으로 암호화해서 들어가는 지 정확하게 알고 각각의 정보를 받아와서 Member객체의 모든 정보를 get으로 받아와 String 타입을 매개변수로 작동하는 decodeString(String cipherText) 메서드를 이용하여 하나씩 복호화 하는 작업을 하는 과정을 변경하였다.</p>
<p>따라서 다른 개발자가 DB에 어떤 정보가 어떤 방식으로 암호화 했는지는 별로 중요하지 않고 그저 decodeMember(Member member)로 객체를 넘겨주기만 하면 바로 복호화해서 사용할 수 있게 만들었다. 그리고 주석으로 추가 정보를 넣어서 혹시라도 코드를 보게 될 일이 있다면 코드를 분석하는 과정을 돕도록 만들었다.</p>
<h2 id="아쉬운-부분">아쉬운 부분</h2>
<p>프로젝트를 끝내고 다시 보고 좀 아쉽다고 생각이 드는 부분들이다.</p>
<h3 id="aes256encoder-1">AES256Encoder</h3>
<p>인터페이스로 한번 감싸고 구현했다면 어땟을까? 라는 생각이 조금 든다. 서비스 클래스의 코드를 보면 조금 강하게 결합된 것 같은 생각이 많이 들어서 InfoEncoder와 같은 인터페이스를 만들고 이를 구현하는 클래스로 만들었다면 좋았다고 생각한다.</p>
<h3 id="modelmapper-사용">ModelMapper 사용</h3>
<p>MemberSaveForm객체를 Member객체로 변환하는 과정이 좀 매끄럽지 않아서 상당히 불만이 많았지만 당시의 지식과 검색으로 ModelMapper 존재를 몰랐다. 굉장히 아쉽다는 생각이 많이 드는 부분으로 만약 내가 ModelMapper를 사용할 줄 알았다면</p>
<pre><code class="language-java">
memberRepository.findByAccount(aes256Encoder.encodeString(form.getAccount()))
                .ifPresentOrElse(
                        user -&gt; { // ifPresent로 구성하여 그냥 아래 코드와 분리하기
                            throw new AlreadyRegisteredUserException();
                        },
                        () -&gt; {
                            Member member = aes256Encoder.encodeMember(
                                    modelmapper.map(form, Member.class);
                        }
                );
</code></pre>
<p>정말 가독성 높고 굉장히 간편한 코드로 만들 수 있었을 텐데... 너무 아쉬운 부분이다.</p>
<h2 id="마무리">마무리</h2>
<p>이번 프로젝트에서 가장 많이 고민한 부분이었다. 하나를 구현하더라도 이게 맞나? 라는 생각으로 찾아보고 공식문서도 보면서 구현했다. <strong>그래서 한 줄의 코드라도 이게 맞는지에 대해서 몇 시간을 고민해 본적도 있었다.</strong>(빈으로 등록하는 것이 옳은가?) 그래서 내가 만든 코드가 점점 더 발전해 가는 과정을 직접 느꼈고 하나의 메서드를 구현하는 과정에서 조차 2~3개를 배우고 또 이런 배운 점들을 적용하는 과정에서 또 구현 방식을 고민하는 그런 꼬리에 꼬리를 무는 생각을 많이 하면서 깊게 생각하고 코딩을 하게 되었다. </p>
<p><strong>이전에는 일단 돌아가면 그만 이라는 생각이 강했지만 이제는 정말 코드 한줄 한줄 더 좋은 코드가 뭘까? 더 좋은 구현이 뭘까? 라는 생각을 많이 하게 만들어준 프로젝트였다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[쇼핑몰 프로젝트 - 시작]]></title>
            <link>https://velog.io/@meteor_control0/%EC%87%BC%ED%95%91%EB%AA%B0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91</link>
            <guid>https://velog.io/@meteor_control0/%EC%87%BC%ED%95%91%EB%AA%B0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91</guid>
            <pubDate>Thu, 20 Apr 2023 19:41:50 GMT</pubDate>
            <description><![CDATA[<h1 id="쇼핑몰-프로젝트">쇼핑몰 프로젝트</h1>
<p>개인 공부용 프로젝트 입니다.</p>
<p>사용 책 : <a href="http://www.yes24.com/Product/Goods/103453774">스프링 부트 쇼핑몰 프로젝트 with JPA</a></p>
<h2 id="🚩-목표">🚩 목표</h2>
<ol>
<li>협업 연습을 위한 깃허브 설정 연습</li>
<li>쇼핑몰 프로젝트를 앞두고서 프로젝트 연습</li>
<li>JPA의 연관 관계 연습</li>
<li>결제 주문 시스템의 이해</li>
<li>단순히 책을 따라하는 것이 아닌 내가 사용하고 싶은 기술 or 코딩 방식 사용</li>
</ol>
<h2 id="참여-인원">참여 인원</h2>
<ul>
<li>MeteorLee : 본인</li>
<li>MeteorLe : 깃허브 연습용 계정</li>
</ul>
<h2 id="사용-도구">사용 도구</h2>
<ul>
<li>Java</li>
<li>SpringBoot</li>
<li>GitKraken</li>
</ul>
<h2 id="진행">진행</h2>
<h3 id="organization-생성">Organization 생성</h3>
<ul>
<li>이번에 처음으로 깃허브의 Organization을 사용해 보기로 해서 생성 후 부계정에 초대를 보냄</li>
<li>shopping-mall 레포지토리 생성</li>
<li>Shopping-mall 프로젝트 생성(보드 방식)</li>
</ul>
<h3 id="라벨-생성">라벨 생성</h3>
<p>강의를 통해서 배운 라벨을 활용해보기로 하여 여러가지 찾아보고 라벨을 생성함.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/f11f5fdf-d106-4ddd-a13e-55fc7b362a85/image.png" alt=""></p>
<p>기존의 라벨들과 다르게 상태, 우선순위, 도메인 등 구체적인 내용의 라벨을 사용하여 이슈들의 관리를 편하게 해줌. 이번에 처음 만들어 보는데 굉장히 유용한 것 같음</p>
<h3 id="초기-세팅-이슈-생성">초기 <a href="https://github.com/private-project-shopping-mall/shopping-mall/issues/2">세팅 이슈</a> 생성</h3>
<ul>
<li>라벨 등을 내가 원하는 방식으로 만들었음</li>
<li>초기 세팅을 위한 이슈를 생성하여 어떤 작업을 해야할지 설정</li>
</ul>
<p>정말 프로젝트에서 사용한다는 생각으로 이슈를 작성하였다. 생각보다 Task 부분에 넣을 것이 없어서 생각나는 부분까지만 작성했는데 실제 작업하는 상황에서 점점 많아져서 추가로 작성할까 고민하게 되었다. 하지만 추가는 하지 않았지만 조금 이슈 작성 상황에서 고려해야할 사항이 많다고 생각하게 됨</p>
<h3 id="템플릿-생성">템플릿 생성</h3>
<h4 id="이슈-템플릿">이슈 템플릿</h4>
<p>깃허브 세팅의 이슈 템플릿 메뉴에서 작성함. </p>
<h4 id="풀리퀘스트-템플릿">풀리퀘스트 템플릿</h4>
<p>.github 폴더 안에 PULL_REQUEST_TEMPLATE.md라는 특정한 이름을 통해서 풀리퀘스트 템플릿을 만들 수 있다는 것을 배웠다. 사실 풀리퀘스트 상황이 워낙 다양해서 이것을 템플릿으로 만드는 것이 이득인지는 잘 모르겠다. 이번 프로젝트 경우 나 혼자 진행하는 프로젝트라고 생각하기에 시도해 본 내용이다.</p>
<h3 id="위키를-이용하여-컨밴션-설정">위키를 이용하여 컨밴션 설정</h3>
<p>깃허브의 Wiki를 통해서 이 프로젝트의 컨밴션을 설정했다. 규칙이라고 생각하면 좋은 내용이고 코드에 관한 컨밴션은 아직 잘 와닿지 않기에 커밋에 관련된 규칙을 정했다. 내가 항상 해오던 커밋의 방식을 문서화 했다. 특히 타입 부분을 조금 확실히 정해서 항상 타입을 뭐로 해야하지? 이런 고민을 줄일 수 있게 되어 좋은 것 같다.</p>
<h3 id="readme-작성">README 작성</h3>
<p>기본적인 README를 작성하였다. 이제까지 프로젝트를 진행해도 사실 개인적인 프로젝트고 공부하는 내용이 대부분이라 작성을 안하는 경우가 많았지만 이번에는 상당히 철저하게 관리해볼 생각이다.</p>
<h1 id="생각">생각</h1>
<h2 id="배운-것들">배운 것들</h2>
<h3 id="깃허브-협업">깃허브, 협업</h3>
<p>사실 깃허브에 커밋은 여러번 하고 이래저래 하고 있는데 관리는 거의 하지 않았기에 기본적인 사항이지만 조금은 공부를 많이 해야했다. 개발자는 문서 또한 잘 작성해야 한다는 것을 경시한 대가를 이번에 좀 크게 치르는 것 같다. 기본적인 README 작성도 해보지 않았으니까 조금은 어려운 것 같았고 Issue는 알고 있지만 정말 사용하지 않은 부분이기에 답답하기 그지 없었다.</p>
<p>협업을 원활하게 하기 위한 여러가지 도구들을 세팅하는 방법들을 하는데 종류가 많아서 조금 힘들었다. 분명 도움을 위한 도구인데 왜 나를 괴롭히는 건지... 초기 설정하는 방식이나 활용하는 방식들을 계속 손에 익숙해질 때까지 사용해볼 생각이다.</p>
<h2 id="생각-1">생각</h2>
<p>지금까지는 그냥 코드만 공부했다는 생각을 많이 하게 되었다. 결국 개발자는 혼자 작업하는 것이 아닌 여러 사람들과 작업을 하고 작업 내용을 공유하는 상황을 자주 마주하게 될 것이다. 하지만 나는 이제까지 코드만 공부하고 로직에 대해서만 생각하는 방식으로만 공부를 하였다. 물론 내가 실력이 매우 부족한 점도 있었지만 이제는 나의 코딩 실력이 올라온 만큼 한 단계 성장하여 협업까지 고려한 공부를 할 생각이다.</p>
<blockquote>
<p>도움 받은 곳
<a href="https://github.com/TeamCooks/TwoSpoon">https://github.com/TeamCooks/TwoSpoon</a>
<a href="https://medium.com/@dave_lunny/sane-github-labels-c5d2e6004b63https://github.com/Relequestual/sensible-github-labels">https://medium.com/@dave_lunny/sane-github-labels-c5d2e6004b63https://github.com/Relequestual/sensible-github-labels</a>
<a href="https://2jinishappy.tistory.com/337">https://2jinishappy.tistory.com/337</a>
<a href="https://shinsunyoung.tistory.com/35">https://shinsunyoung.tistory.com/35</a>
<a href="https://github.com/TeamCooks/TwoSpoon/wiki/%ED%8C%80-%EC%BB%A8%EB%B2%A4%EC%85%98">https://github.com/TeamCooks/TwoSpoon/wiki/%ED%8C%80-%EC%BB%A8%EB%B2%A4%EC%85%98</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[오류 해결 - SpringSecurity 순환 참조]]></title>
            <link>https://velog.io/@meteor_control0/%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-SpringSecurity-%EC%88%9C%ED%99%98-%EC%B0%B8%EC%A1%B0</link>
            <guid>https://velog.io/@meteor_control0/%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-SpringSecurity-%EC%88%9C%ED%99%98-%EC%B0%B8%EC%A1%B0</guid>
            <pubDate>Sun, 09 Apr 2023 19:53:13 GMT</pubDate>
            <description><![CDATA[<h1 id="오류-상황">오류 상황</h1>
<p>스프링 시큐리티 사용 중 발생하는 순환 참조 오류</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/7cd10e89-534f-4020-8045-bde91846e660/image.png" alt=""></p>
<h2 id="발생-과정">발생 과정</h2>
<p>스프링 시큐리티를 사용하는 상황에서 이런 저런 설정을 만지고 프로젝트를 실행하니 갑자기 에러가 발생했다.</p>
<pre><code class="language-java">Error starting ApplicationContext. To display the conditions report re-run your application with &#39;debug&#39; enabled.
2023-04-09 00:23:20.816 ERROR 16504 --- [  restartedMain] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  customSecurityConfig defined in file [C:\Book\javaWeb\b01\build\classes\java\main\org\zerock\b01\config\CustomSecurityConfig.class]
↑     ↓
|  customUserDetailsService defined in file [C:\Book\javaWeb\b01\build\classes\java\main\org\zerock\b01\security\CustomUserDetailsService.class]
└─────┘


Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.


Process finished with exit code 0
</code></pre>
<p>사실 처음 만났던 에러는 아니었지만 이전의 해결 과정이 문제가 있었기에 이번에는 조금 더 공부를 하여 에러를 해결했다. (사실 공부라기 보단 그냥 내가 부족했던 것이다.)</p>
<h2 id="직접적인-오류-발생-과정">직접적인 오류 발생 과정</h2>
<p>스프링 시큐리티를 설정하였기에 자연스럽게 PasswordEncoder를 스프링 시큐리티 설정을 담당하는 클래스 CustomSecurityConfig에 입력했다. 그 후 로그인 유지를 위한 remember-me를 이용하기 위해서 CustomUserDetailsService를 userDetailsService로 주입 받게 되었다. </p>
<pre><code class="language-java">
@Log4j2  
@Configuration  
@RequiredArgsConstructor  
// 어노테이션으로 권한 설정 @PreAuthorize, @PostAuthorize 이용해서 사후, 사전 권한 설정  
@EnableGlobalMethodSecurity(prePostEnabled = true)  
public class CustomSecurityConfig {  


  // 자동 로그인 remember-me 설정 주입 필요  
  private final DataSource dataSource;  
 private final CustomUserDetailsService userDetailsService;  

  // 로그인 없이 일단 사용할 수 있도록 처리  
  @Bean  
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {  

  log.info(&quot;========================config====================================&quot;);  

  // login 설정  
  http.formLogin().loginPage(&quot;/member/login&quot;);  

  // csrf 비활성화  
  http.csrf().disable();  

  // remember-me 설정  
  http.rememberMe()  
  .key(&quot;12345678&quot;)  
  .tokenRepository(persistentTokenRepository())  
  .userDetailsService(userDetailsService)  
  .tokenValiditySeconds(60 * 60 * 24 * 30);  



 return http.build();  

  }  

  // 정적 파일 시큐리티 적용 제외  
  @Bean  
  public WebSecurityCustomizer webSecurityCustomizer() {  

  log.info(&quot;---------------------------web configure------------------------------&quot;);  


 return (web) -&gt; web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());  
  }  


  // PasswordEncoder 설정  
  @Bean  
  public PasswordEncoder passwordEncoder() {  
  return new BCryptPasswordEncoder();  
  }  

  // remember-me를 위한 토큰 레포지토리 빈  
  @Bean  
  public PersistentTokenRepository persistentTokenRepository() {  
  JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();  
  repo.setDataSource(dataSource);  
 return repo;  
  }  

}</code></pre>
<p>그리고 개발 과정인 만큼 간결한 확인을 위해서 UserDetailsService를 구현한 CustomUserDetailsService에서 직접적으로 User클래스를 이용해서 UserDetails를 설정하였다.</p>
<pre><code class="language-java">@Log4j2
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final PasswordEncoder passwordEncoder;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        log.info(&quot;loadUserByUsername = &quot; + username);

        UserDetails userDetails =User.builder()
                .username(&quot;user1&quot;)
                .password(passwordEncoder.encode(&quot;1111&quot;))
                .authorities(&quot;ROLE_USER&quot;)
                .build();


        return userDetails;
    }
}
</code></pre>
<p>이러한 상황에서 위의 에러가 발생하였다.</p>
<h1 id="오류-해결-과정">오류 해결 과정</h1>
<h2 id="오류-파악">오류 파악</h2>
<p>일단 콘솔에 나온 에러를 해석해보면 </p>
<blockquote>
<p>Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.</p>
</blockquote>
<p>대략 순환 참조를 권장하지 않고 금지하고 있으니 빈의 종속성의 주기(cycle)를 제거해달라는 것이다. 사실 이전에 봤을 때는 이게 무슨 뜻인지 정확하게 몰랐는데 빈을 주입하는 개념을 조금 더 잘 알게된 지금은 이제 무슨 의미인지 알게 되었다.</p>
<p>대략적으로 빈 사이의 주입 과정에서 서로가 서로를 참조하는 꼬리무는 과정이 있기 때문에 발생하는 오류라는 것이다.</p>
<h2 id="해결-과정">해결 과정</h2>
<p>이전에도 만난적 있던 문제였기에 조금은 더 알아보고 고치려고 했기에 검색을 통해 baeldung 사이트를 참조 하게 되었다.</p>
<p><a href="https://www.baeldung.com/circular-dependencies-in-spring">https://www.baeldung.com/circular-dependencies-in-spring</a></p>
<h3 id="문제-인식">문제 인식</h3>
<p>일단 정확하게 순환 참조가 무엇인지 알아보면</p>
<blockquote>
<ol>
<li>What Is a Circular Dependency?
A circular dependency occurs when a bean A depends on another bean B, and the bean B depends on bean A as well:
Bean A → Bean B → Bean A
Of course, we could have more beans implied:
Bean A → Bean B → Bean C → Bean D → Bean E → Bean A</li>
</ol>
</blockquote>
<p>서로가 서로를 참조하여서 발생하는 문제로 종속성이 서로가 의존할 때 발생하는 문제이다. 그리고 마치 순환하는 구조처럼 보이기에 circular라는 말이 붙은 것이었다.</p>
<h3 id="스프링에서의-순환-참조">스프링에서의 순환 참조</h3>
<blockquote>
<p>When the Spring context loads all the beans, it tries to create beans in the order needed for them to work completely.
Let&#39;s say we don&#39;t have a circular dependency. We instead have something like this:
Bean A → Bean B → Bean C
Spring will create bean C, then create bean B (and inject bean C into it), then create bean A (and inject bean B into it).
But with a circular dependency, Spring cannot decide which of the beans should be created first since they depend on one another. In these cases, Spring will raise a BeanCurrentlyInCreationException while loading context.
It can happen in Spring when using constructor injection. If we use other types of injections, we shouldn&#39;t have this problem since the dependencies will be injected when they are needed and not on the context loading.</p>
</blockquote>
<p>스프링의 순환 참조에 대해서 알아보던 중에 가장 크게 놀랐던 부분이 바로 빈의 생성 과정에서 발생한다는 것이다. 그러니까 단순하게 참조가 순환의 고리 형태라서 발생하는 것이 아닌 빈을 생성하는 과정에서 빈의 생성 순서를 스프링에서 모르는 것이 이런 에러를 발생시키는 것이었다. 만약 빈의 A -&gt; B -&gt; C -&gt; A 의 순환 형태의 구조를 가지고 있더라도 만약 빈의 생성 순서를 스프링에서 알게 만들 수 있다면 이런 순환 참조 에러가 발생하지 않는다는 것이다. </p>
<h2 id="해결-방법">해결 방법</h2>
<h3 id="lazy">@Lazy</h3>
<p>이런 순환 참조를 끊는 간단한 방법으로는 @Lazy 어노테이션을 사용하는 것이다.</p>
<blockquote>
<p>A simple way to break the cycle is by telling Spring to initialize one of the beans lazily. So, instead of fully initializing the bean, it will create a proxy to inject it into the other bean. The injected bean will only be fully created when it’s first needed.</p>
</blockquote>
<p>프록시 패턴을 이용해서 빈을 생성할 때 프록시 빈을 만든 후에 실제 빈을 호출할 때 빈을 만들어서 주입하는 방식을 사용하는 것이다. 이렇게 되면 순환하는 구조를 가지더라도 실제 빈이 아닌 프록시 빈이 생성되는 것이므로 에러가 발생하지 않는 것이다. </p>
<p>조금 더 궁금하므로 @Lazy에 대한 공식 문서를 살펴보자</p>
<p><a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Lazy.html">https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Lazy.html</a></p>
<blockquote>
<p>In addition to its role for component initialization, this annotation may also be placed on injection points marked with Autowired or Inject: In that context, it leads to the creation of a lazy-resolution proxy for all affected dependencies, as an alternative to using ObjectFactory or Provider. Please note that such a lazy-resolution proxy will always be injected; if the target dependency does not exist, you will only be able to find out through an exception on invocation. As a consequence, such an injection point results in unintuitive behavior for optional dependencies. For a programmatic equivalent, allowing for lazy references with more sophistication, consider ObjectProvider.</p>
</blockquote>
<p>프록시, 빈을 사용할 때 주입 등 같은 내용이 대부분인데 다른 것은 사용하지 않는 것을 추천한다는 내용이다. </p>
<p>스프링은 빈을 스프링 컨텍스트에서 관리해주므로 우리는 빈을 신경쓰지 않고 서비스 로직에 집중하는 방식을 사용하는 것이다. 하지만 그래도 우리는 빈이 언제 생성되고 언제 소멸하는 등 이런 빈에대한 정보를 잘 알고 사용해야 한다. 어쩌면 이런 순환 참조도 빈의 생성 과정에대한 깊은 이해가 없기 때문에 발생한 일인데 이런 직관적인 빈의 생성 과정에 대한 이해를 하지 않고 문제를 해결하기 위해 @Lazy를 사용하는 것 같아서 좋지 않다는 내용이다.</p>
<p>물론 다른 관점의 내용도 있다. 굉장히 레거시한 코드의 구조 내부에서는 빈의 생성 구조들이 이미 엉킬대로 엉켰기 때문에 @Lazy를 사용하여 동작이 가능하게 하는 것이 더 중요하다는 것이다. 레거시한 코드를 모두 고칠 생각이 아니라면 동작이 가능하게 하는 방법이 더 좋다는 좀 해결 방법?이라기 보다는 임시 방편에 가까운 방법이지만 이런 상황에 놓인다면 선택권이 없다는 것이다.</p>
<h3 id="setter-주입">Setter 주입</h3>
<blockquote>
<p>One of the most popular workarounds, and also what the Spring documentation suggests, is using setter injection.
Simply put, we can address the problem by changing the ways our beans are wired — to use setter injection (or field injection) instead of constructor injection. This way, Spring creates the beans, but the dependencies are not injected until they are needed.</p>
</blockquote>
<p>자바의 공식 문서에서 제안한 방법으로 Setter로 주입하는 방식이다. 빈이 생성은 되지만 사용되기 전에는 종속성을 주입하지 않는 것으로 이렇게 되면 빈이 사용되기 전까지는 참조가 없기 때문에 순환 참조가 발생하지 않는 것이다. 하지만 Setter방식의 주입 방식은 그다지 추천되지 않는 방식이다.</p>
<h3 id="postconstruct">@PostConstruct</h3>
<blockquote>
<p>Another way to break the cycle is by injecting a dependency using @Autowired on one of the beans and then using a method annotated with @PostConstruct to set the other dependency.</p>
</blockquote>
<p>생성자 주입이 다 이루어진 후에 메서드를 실행하는 것을 명시하는 어노테이션으로 @PostConstruct의 어노테이션을 붙인 메서드를 통해서 의존성을 주입하는 방식으로 해결하는 것이다. </p>
<p>하지만 @PostConstruct는 빈의 생성 주기에서 발생하는 것이지만 의존성 주입의 순서가 명시되는 점에서 오류가 발생할 가능성이 높아진다. 스프링의 가장 큰 장점은 빈의 생성 주기나 이런 의존성 주입을 스프링이 관리해주는 것인데 이런 장점을 깍아먹는 일이다. 스프링에서 빈을 관리할 때는 올바른 순서로 주입되던 의존성이 @PostConstruct를 통해서 순서를 정해주었기 때문에 발생하는 오류가 생길 가능성이 있다는 것이다.</p>
<h3 id="applicationcontextaware-와-initializingbean를-구현">ApplicationContextAware 와 InitializingBean를 구현</h3>
<blockquote>
<p>If one of the beans implements ApplicationContextAware, the bean has access to Spring context and can extract the other bean from there.
By implementing InitializingBean, we indicate that this bean has to do some actions after all its properties have been set. In this case, we want to manually set our dependency.</p>
</blockquote>
<p>ApplicationContextAware는 빈으로 사용될 객체의 스프링 컨테이너에 직접 접근해서 설정을 하는 방법이다.</p>
<p>InitializingBean는 빈으로 등록된 이후에 수행할 작업을 명시하는 것인데 이 이 수행할 작업에서 빈을 주입하는 것이다.</p>
<p>위의 작업들과 마찬가지로 스프링 고유의 장점을 저버리는 방식이고 또 다른 오류를 발생시킬 가능성이 있는 방식들이다.</p>
<h3 id="✅-재설계">✅ 재설계</h3>
<blockquote>
<p>When we have a circular dependency, it’s likely we have a design problem and that the responsibilities are not well separated. We should try to redesign the components properly so that their hierarchy is well designed and there is no need for circular dependencies.
However, there are many possible reasons we may not be able to do a redesign, such as legacy code, code that has already been tested and cannot be modified, not enough time or resources for a complete redesign, etc. If we can&#39;t redesign the components, we can try some workarounds.</p>
</blockquote>
<p>순환 참조라는 것은 결국 서로가 서로를 참조하는 닭과 계란의 관계와 같은 상황을 가지는 것이다. 이런 방법을 해결하는 가장 좋은 방식은 이런 빈의 참조 관계를 재설계하는 것이다. </p>
<p>내가 오류를 해결한 방식이고 나는 PasswordEncoder를 다른 @Configuration 클래스에서 빈으로 등록하는 방식을 사용했다.</p>
<pre><code class="language-java">
@Log4j2  
@Configuration  
public class PasswordEncoderConfiguration {  


  // PasswordEncoder 설정  
  @Bean  
  public PasswordEncoder passwordEncoder() {  
  return new BCryptPasswordEncoder();  
  }  


}</code></pre>
<p>결국 A -&gt; B -&gt; C -&gt; 의 구조에서 A -&gt; B , B -&gt; C의 구조로 빈의 참조 구조를 변경함으로써 순환 구조를 끊어내어 오류를 해결하였다.</p>
<h1 id="생각">생각</h1>
<h2 id="공식-문서">공식 문서</h2>
<p><a href="https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans">https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans</a></p>
<p>공식 문서의 내용을 다시 보면서 느낀 점은 처음 설계가 중요하다는 것이다. 스프링의 시작 시점에서 빈을 등록하는 과정과 스프링의 개념을 다시 생각해보면 내가 만난 순환 참조가 어쩌면 스프링의 가장 큰 장점 중에 하나였던 것이다. 스프링이 빈을 관리해주기 때문에 개발자가 서비스 로직에만 집중할 수 있다는 장점을 이번 에러를 통해서 더욱 알게 되었다.</p>
<blockquote>
<p>You can generally trust Spring to do the right thing. It detects configuration problems, such as references to non-existent beans and circular dependencies, at container load-time. Spring sets properties and resolves dependencies as late as possible, when the bean is actually created. This means that a Spring container that has loaded correctly can later generate an exception when you request an object if there is a problem creating that object or one of its dependencies — for example, the bean throws an exception as a result of a missing or invalid property. This potentially delayed visibility of some configuration issues is why ApplicationContext implementations by default pre-instantiate singleton beans. At the cost of some upfront time and memory to create these beans before they are actually needed, you discover configuration issues when the ApplicationContext is created, not later. You can still override this default behavior so that singleton beans initialize lazily, rather than being eagerly pre-instantiated.</p>
</blockquote>
<p>공식 문서에서 스프링이 컨테이너에 올라갈 때 이런 에러를 잡아주기 때문에 실제 애플리케이션이 작동 중에 발생하는 종속성 오류와 구성 오류 등을 예방할 수 있다는 것이다. 사실 여러 해결 방식 중에 @Setter나 @PostCunstruct나 결국 다른 오류를 발생시킬 가능성이 있는 완벽한 해결방식이 아닌 것처럼 시작 하는 상황에서 오류를 발생시켜서 개발자가 불완전한 구조를 계속해서 사용하는 것을 예방해 주는 것이라고 생각한다.</p>
<h2 id="😅생각">😅생각</h2>
<p>@RequiredArgsConstructor를 이용한 생성자 주입을 즐겨 사용하였고 가장 좋은 방식으로 알고 있었기에 이런 오류를 발생시킬 수 있다는 생각을 하지 못했었다. 처음에는 이런 생성자 주입 방식에서 발생한 문제기에 이런 방식이 항상 좋은 것은 아니다. 라는 생각을 가지게 되었다. 하지만 어쩌면 이번 오류는 내가 문제가 있는 순환 참조 구조를 애플리케이션에 설계하지 못하게 막아주는 것이었다. </p>
<p>이번 오류는 단순하게 애플리케이션의 어떤 동작에 발생하는 오류가 아닌 구조적인 측면에서 발생한 오류인 만큼 조금 스프링의 기본적인 동작 구조에 대한 공부를 많이 하게 되었다. </p>
<p>또한 설계의 중요성을 다시 생각하게 되었다. 사실 작은 프로젝트이고 개발을 하는 초반 상황에서 이런 설계적인 오류가 발생한다는 것에 놀랐다. 그래서 정말 정말 설계가 중요하다고 다시 한번 더 생각하게 되었다. </p>
<blockquote>
<p>도움 받은 곳
<a href="https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans">https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans</a>
<a href="https://www.baeldung.com/circular-dependencies-in-spring">https://www.baeldung.com/circular-dependencies-in-spring</a>
<a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Lazy.html">https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Lazy.html</a>
<a href="https://blog.naver.com/simpolor/221919272642">https://blog.naver.com/simpolor/221919272642</a>
<a href="https://velog.io/@limsubin/Spring-Boot%EC%97%90%EC%84%9C-PostConstruct-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90">https://velog.io/@limsubin/Spring-Boot%EC%97%90%EC%84%9C-PostConstruct-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</a>
<a href="https://antop.tistory.com/entry/Spring-Lazy">https://antop.tistory.com/entry/Spring-Lazy</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[오류 해결 - Gradle 빌드]]></title>
            <link>https://velog.io/@meteor_control0/%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-Gradle-%EB%B9%8C%EB%93%9C</link>
            <guid>https://velog.io/@meteor_control0/%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-Gradle-%EB%B9%8C%EB%93%9C</guid>
            <pubDate>Tue, 04 Apr 2023 04:38:58 GMT</pubDate>
            <description><![CDATA[<h1 id="오류-상황">오류 상황</h1>
<p>Gradle의 환경에서 JSTL을 사용하려는 상황에서 발생한 오류에 관련된 내용이다.</p>
<h2 id="발생-과정">발생 과정</h2>
<p>JSTL을 사용하기 위해 먼저 Maven Repository 사이트에 접속해서 JSTL을 검색후 <a href="https://mvnrepository.com/artifact/javax.servlet.jsp.jstl/jstl/1.2">https://mvnrepository.com/artifact/javax.servlet.jsp.jstl/jstl/1.2</a> 에서 Gradle에 있는 dependency를 복사해서 추가 하였는데 오류가 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/4ff4405d-c6f5-4ace-9f77-9de9170a2b19/image.png" alt=""></p>
<pre><code class="language-java">// https://mvnrepository.com/artifact/javax.servlet.jsp.jstl/jstl
implementation group: &#39;javax.servlet.jsp.jstl&#39;, name: &#39;jstl&#39;, version: &#39;1.2&#39;</code></pre>
<h2 id="오류-내역">오류 내역</h2>
<p><img src="https://velog.velcdn.com/images/meteor_control0/post/0dd80028-bb90-40d6-89d3-82c690da69c0/image.png" alt=""></p>
<p>오류 내역을 보면 </p>
<pre><code>Could not find javax.servlet.jsp:jstl:1.2.
     Searched in the following locations:
       - https://repo.maven.apache.org/maven2/javax/servlet/jsp/jstl/1.2/jstl-1.2.pom</code></pre><p>나오는데 결국은 내가 넣어준 dependency로 경로를 받아서 요청을 보냈는데 그런 경로가 없다는 것이다. 이 경로에 대해서 잘 보자면 <strong>/jsp/jstl/1.2/jstl-1.2.pom</strong> 부분이 이상한데 jstl도 2번 나오고 1.2의 버전도 2번이나 나온다.</p>
<h2 id="해결-과정">해결 과정</h2>
<p>다른 사용자들이 사용한 방식</p>
<pre><code>implementation group: &#39;jstl&#39;, name: &#39;jstl&#39;, version: &#39;1.2&#39;</code></pre><p>자동완성을 통한 방식</p>
<pre><code>implementation group: &#39;javax.servlet&#39;, name: &#39;jstl&#39;, version: &#39;1.2&#39;</code></pre><p>사실 결국 classPath를 제대로 설정해주면 되는 문제라서 간단한 문제이지만 왜 공식 사이트라고 불릴만한 곳에서 이런 에러가 발생하는지 좀 이해가 가지 않는다. 특히 gradle만...</p>
<h1 id="생각">생각</h1>
<h2 id="오류-다루는-이유">오류 다루는 이유</h2>
<p>간단한 오류고 사실 다루지 않아도 될 법한 내용이지만 굳이 다루는 이유는 최근 gradle을 사용하기 시작한 시점부터 이런 자잘한 오류가 많이 나고 있다. 이상할 정도로 build하는 데서 문제가 발생하는 것이다. 공식 사이트나 아니면 심지어 자동완성에서도 이런 오류가 나올 때가 있어서 뭔가 내가 잘못했나? 하고 끙끙대다가 찾아보면 이런 부분에서 오류가 나오는 일이 있어서 한 번 정리하고자 한다.</p>
<h1 id="개인적인-생각">개인적인 생각</h1>
<p>dependcy를 추가하는 부분이 사실 많은 부분이 아니기에 gradle을 사용할 때는 추가하거나 버전을 변경하는 이런 상황에서 조금은 더 유심히 살펴볼 필요가 있는 것 같다. 사용할 수록 편하다는 것은 공감되고 뛰어나다고 생각하지만 아직 이런 부분은 아직 Maven에 비해 부족한 것 같다.</p>
]]></description>
        </item>
    </channel>
</rss>