<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hong-sile.log</title>
        <link>https://velog.io/</link>
        <description>끊임없이 의심하고 반증하기</description>
        <lastBuildDate>Sun, 11 Feb 2024 09:40:20 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. hong-sile.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hong-sile" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[초록 스터디 - 봄날의 첫 페이지 1주차]]></title>
            <link>https://velog.io/@hong-sile/%EC%B4%88%EB%A1%9D-%EC%8A%A4%ED%84%B0%EB%94%94-%EB%B4%84%EB%82%A0%EC%9D%98-%EC%B2%AB-%ED%8E%98%EC%9D%B4%EC%A7%80-1%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@hong-sile/%EC%B4%88%EB%A1%9D-%EC%8A%A4%ED%84%B0%EB%94%94-%EB%B4%84%EB%82%A0%EC%9D%98-%EC%B2%AB-%ED%8E%98%EC%9D%B4%EC%A7%80-1%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 11 Feb 2024 09:40:20 GMT</pubDate>
            <description><![CDATA[<h1 id="진행-내용">진행 내용</h1>
<p>이번 주차는 스터디 첫 주차이고, 스터디원들이 스프링을 처음 겪어보는 만큼 환경설정 부터 천천히 다뤄봤습니다.</p>
<h2 id="프레임워크와-프로젝트-설정">프레임워크와 프로젝트 설정</h2>
<p>스터디원들 모두가 스프링은 아니어도 다른 분야의 프레임워크(react, django 등등)들을 사용해본 경험은 있어서 프레임워크 개념에 대한 학습은 빠르게 지나갈 수 있었습니다.</p>
<p>하지만 예상했던 대로 프로젝트 설정 부분에서 시간이 좀 걸렸고, 스스로 학습하기 위해선 이 부분까지는 학습을 시켜야할 것 같아 조금 더 신경을 써서 환경을 구성할 수 있도록 해뒀습니다.</p>
<p>그리고 build.gradle에 대해서도 다뤘는데 개인적으론 build.gradle을 읽을 수 있는 것과 어떤 식으로 동작하는지에 대해 이해하는게 중요하다고 생각하여 조금 내용을 더 다뤘습니다. </p>
<h2 id="mvc---mvc-response-crud-api-예외처리">MVC - MVC Response, CRUD API, 예외처리</h2>
<p>스프링의 기능 중에선  <code>MVC - MVC Response, CRUD API, 예외처리</code> 에 대해서 학습했습니다.</p>
<p>스터디원들이 웹 요청에 대한 개념이 확실하지 않아서 그 부분도 조금 깊게 다뤘었습니다.</p>
<p>다행히 스프링의 자세한 동작들을 학습할 수 있는 학습테스트 들이 있어서 이러한 개념들을 좀 길게 다뤄도 되었던게 스터디를 진행할 때 큰 도움이 되었던 것 같습니다.</p>
<p>다시 한번 학습테스트와 강의자료를 구성해준 브리, 브라운께 감사를!</p>
<h2 id="물어보기">물어보기</h2>
<p>이전에 앞서서 초록 스터디를 진행했던 주노가 남긴 회고 <a href="https://velog.io/@junho5336/%EC%B4%88%EB%A1%9D-%EC%8A%A4%ED%84%B0%EB%94%94-1%EC%A3%BC%EC%B0%A8#%ED%95%99%EC%8A%B5-%EB%82%B4%EC%9A%A9">블로그</a>에 보면 스터디원들이 어떤 개념을 확실하게 익히고 넘어가야 하는지 명시적으로 짚어주면 좋겠다고 적어둔 부분이 있습니다.</p>
<p>이전 스터디 회고들을 보고, 저도 해당부분에 대한 필요성을 느껴서 아래와 같이 구성해보았습니다.</p>
<p>우테코 프리코스의  아고라(질문 채널)과 스터디 개념들을 적당히 혼합시켜봤습니다.</p>
<p>스터디 노션 페이지에 스터디를 하면서 나왔던 키워드들 중 알아보면 좋을 것이나 이번 주차 스터디에서 익히고 가야할 키워드를 조금씩 정리해보고 스터디원들이 보면서 자신의 학습 상황을 점검할 수 있도록 하였습니다.</p>
<p>나중엔 스터디원들이 해당 페이지에 궁금한 것들을 모아서 정리하고 이를 모든 스터디원들이 공유하면서 학습+토론 하는 것이 목표입니다.</p>
<p>각 스터디마다 존재하는 물어보기 섹션
<img src="https://i.imgur.com/i7Zy1bV.png" alt=""></p>
<p>메인 페이지에서 볼 수 있는 모든 물어보기를 모아보는 섹션
<img src="https://i.imgur.com/gDypdOQ.png" alt=""></p>
<p>지금은 저희 스터디에서만 쓰고 있지만 추후 확장해서 다른 학교의 초록 스터디 원들도 이러한 질문들을 정리할 수 있도록 하면 좋을 것 같습니다.</p>
<p>나중에 스터디가 끝나면 브리나 브라운께 한 번 건의를 드려보려 합니다.</p>
<h1 id="피드백">피드백</h1>
<p>매번 강의+미션이 끝나고 구글 폼을 만들어 피드백을 수집하고 있습니다. 4명이라 수가 적긴 하지만, 처음 스터디를 진행하는 만큼 피드백들이 많이 필요했거든요.</p>
<p>확실히 초반이라 스터디원들간의 편차가 있는 것 같았습니다. </p>
<p>웹 관련해서 경험을 해본 스터디원들은 웹 요청, 응답에 대해서는 너무 쉽다고 하였고 그렇지 못한 스터디원들은 어렵다고 하였거든요.</p>
<p>아직 초반이라 편차가 있지만, 후반으로 갈수록 모두가 공평하게 모르니까 괜찮지 않을까라고 생각하곤 있습니다. </p>
<p>난이도 조절 때문에 일부러 수도 적게 뽑고, 스프링에 대해 모르는 사람들만 뽑았었거든요.
하지만, 지속적으로 생각해볼만한 부분인 것 같습니다.</p>
<h1 id="후기">후기</h1>
<p>처음 스터디를 리딩하고 가르치다보니 아직 모자란 부분이 많은 것 같습니다. 생각보다 가르치는 건 힘들더라고요...
계속 준비하면서 개선해야할 것 같습니다.</p>
<p>스터디를 진행하다보니 제가 그 당시에 이걸 알았으면 좋지 않았을까 하는 것들을 많이 말하다보니 강의 자료 외의 내용도 많이 말하게 되는 것 같습니다.</p>
<p>다행히 스터디원들이 그런 내용들을 좋아해줘서 개인적인 경험과 생각들도 편하게 말하게 되고, 그러다 보니 딱딱하기보단 편하게 물어볼 수 있는 부드러운 느낌의 스터디가 진행되는 것 같습니다.</p>
<p>그리고 스터디를 설계하다보니 우테코에 있던 방식들을 많이 차용하게 되더라고요. 
물어보기, 리뷰, 가르치는 방식 등등
확실히 제가 우테코에서 긍정적인 영향을 많이 받기도 하였고, 스터디를 진행해보면서 우테코에서 배웠던 방식이 잘 설계된 방식이란 것을 많이 느꼈던 것 같아요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[초록 스터디 - 봄날의 첫 페이지 OT]]></title>
            <link>https://velog.io/@hong-sile/%EC%B4%88%EB%A1%9D-%EC%8A%A4%ED%84%B0%EB%94%94-%EB%B4%84%EB%82%A0%EC%9D%98-%EC%B2%AB-%ED%8E%98%EC%9D%B4%EC%A7%80-OT</link>
            <guid>https://velog.io/@hong-sile/%EC%B4%88%EB%A1%9D-%EC%8A%A4%ED%84%B0%EB%94%94-%EB%B4%84%EB%82%A0%EC%9D%98-%EC%B2%AB-%ED%8E%98%EC%9D%B4%EC%A7%80-OT</guid>
            <pubDate>Sat, 03 Feb 2024 12:14:53 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>감사하게도 좋은 기회가 생겨 2023년 2월부터 11월까지 우아한테크코스 5기 BE로 참여했습니다.</p>
<p>정말 많은 걸 배웠고, 좋은 사람들을 만날 수 있었습니다.
하지만 아쉽게도 함께하는 시간은 끝나갔고, 모두가 각자의 미래를 계획하며 우테코 이후의 삶을 준비했습니다.</p>
<p>저는 학교가 많이 남아서 학교에 다시 돌아갈 준비를 했고,
학교에 가기전 많은 경험을 쌓아보고자 여러 행사들과 개발자분들을 만났습니다.
그러다 한 발표자분께서 해주셨던 말씀이 저에게 강한 영향을 줬습니다.</p>
<blockquote>
<p>&quot;많은 개발자분들의 도움으로 여기까지 성장할 수 있었어요. 
그래서 저도 이 생태계에 조그마한 기여를 해보려고요&quot;</p>
</blockquote>
<p>생각해보니 저도 많은 선배들의 도움을 받았습니다.</p>
<p>본인의 시간을 내주어 커피챗을 해주시는 개발자 분, 
선듯 컨퍼런스에 나와서 본인의 경험을 공유해주시는 분들,
오픈채팅방에서 질문을 해도 친절하게 답변해주시는 분들</p>
<p>이러한 좋은 생태계에 조금이나마 저도 기여를 하고 싶다는 생각에 스터디를 기획하게 되었습니다.</p>
<p>스터디를 계획하던 중 우아한테크코스 코치님인 브라운과 브리가 이러한 스터디를 지원해주기 위해 초록 스터디라는 모임을 만들고 있다고 말씀해주셨고, 초록 스터디에 합류하게 되었습니다.</p>
<h1 id="목표">목표</h1>
<p>스터디를 진행하는 목표는 다음과 같습니다.</p>
<p>제가 겪었던 경험을 다른 친구들도 느꼈으면 함이 첫 번째였고, 
학교에서도 우테코 크루들과 같은 동료들을 만들어(?) 저도 같이 성장하는 경험을 느껴봤으면 했습니다.</p>
<p>또, 스터디에서 지향하는 방향은 
백엔드를 구현할 수 <strong>있다</strong>에서 그치지 않고 어떻게 하면 <strong>좋은</strong> 코드를 작성할 수 있는지에 대해 고민하는 것과
스터디원들이 서로에게 긍정적인 영향을 끼치며 성장하는 것이었습니다.</p>
<h1 id="계획">계획</h1>
<p>스터디원이 너무 많으면 방향을 잡기 어려울 것 같아서 저를 제외한 스터디원을 최대 4명으로 계획하고 인원을 모집하였습니다.</p>
<p>java, spring 기반의 스터디고, 대상자는 자바의 기초에 대해서만 알고 있는 사람들이었습니다.</p>
<p>아래와 같이 스터디를 임시로 계획해봤습니다</p>
<pre><code class="language-markdown">1. 스터디 온보딩 - 진행방식 소개
2. MVC - MVC Response, CRUD API, 예외처리 
3. JDBC - JDBCTemplate, SimpleJdbcInsert 
4. Spring Core-1 - Container, Spring Bean, Layred 
5. Spring Core-2 - Layred, 리팩터링 및 복습 
6. Spring 로그인 - 인증, 인가, session token 
7. Spring JPA 맛보기 - 영속성 컨텍스트, Spring data jpa 
8. 배포 및 인프라 - AWS, 간단한 배포
9. 스터디 회고</code></pre>
<p>사실 이 스터디 계획이 끝까지 쓰이지는 않을 것입니다. 계속 진행해나가며 스터디원 상황에 맞게 필요한 내용을 다룰 예정입니다.</p>
<h1 id="스터디-이름">스터디 이름</h1>
<p>OT를 진행하기전에 모두 숙제로 스터디 이름을 생각해오기로 했습니다.</p>
<p>다양한 후보군들이 나왔는데</p>
<blockquote>
<ul>
<li>중앙의 봄</li>
</ul>
</blockquote>
<ul>
<li>Blossom</li>
<li>봄날의 첫 페이지</li>
<li>Zpring</li>
</ul>
<p>스터디를 모집할 때 중앙대 Zeropage(zp)라는 동아리에서 인원을 뽑았기 때문에
중앙, zp관련된 이름들이 많이 나왔습니다.</p>
<p>다들 개발자치곤 나름 잘 생각해와서 만족했습니다.</p>
<p>스터디 이름은 <code>봄날의 첫 페이지</code>로 결정이 났는데, 개인적으론 개발자 같지 않은 스터디 이름이라 마음에 듭니다.
(모 초록 스터디는 이름이 BDD... 어떻게 이름이 BDD...)</p>
<h1 id="후기">후기</h1>
<p>스터디 시작하기 전에 스터디원들 모두 모여서 
어떤식으로 스터디를 진행할 지 원하는 바가 무엇인지에 대해 많은 이야기를 나눴습니다.</p>
<p>다들 열의가 넘치는 사람들이어서, 앞으로의 스터디가 매우 기대됩니다.
초록 스터디 화이팅!!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[원격 접속이 막힌 서버 복구하기]]></title>
            <link>https://velog.io/@hong-sile/%EC%9B%90%EA%B2%A9-%EC%A0%91%EC%86%8D%EC%9D%B4-%EB%A7%89%ED%9E%8C-%EC%84%9C%EB%B2%84-%EB%B3%B5%EA%B5%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hong-sile/%EC%9B%90%EA%B2%A9-%EC%A0%91%EC%86%8D%EC%9D%B4-%EB%A7%89%ED%9E%8C-%EC%84%9C%EB%B2%84-%EB%B3%B5%EA%B5%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 12 Nov 2023 08:17:38 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 진행하는 중 갑작스럽게  kerdy서버를 복구해야할 일이 있었었다.
해결과정 탐색과 어떻게 해결했는지 기록해보려고 한다.</p>
<p>단, 급박하게 prod 서버를 복구해야 했기에 몇몇 부분은 시도하지 못한 채 빠르게 넘어간 부분이 있다 가장 안정적이고, 확실한 방법을 시도해보려 했다.</p>
<h3 id="배경">배경</h3>
<p>별 문제 없이 kerdy의 서버를 운영하고 있었는데, aws가 위험 IP를 감지해주었다.</p>
<p>guardDuty가 위험한 IP에서 서버에 요청을 보냈음을 이를 감지하고 slack에 메시지를 보내줬다.</p>
<p>nginx의 accessLog를 보니,
<img src="https://i.imgur.com/TRFRzwz.png" alt=""></p>
<p>무작위 IP에 <code>/?XDEBUG_SESSION_START=phpstorm</code> 로 공격을 하기 위한 사전 정보 수집을 하고 있다.
조사 결과 php 서버면 디버그 모드로 변경할 수 있는 공격이라고 한다.</p>
<p>우리의 prod 서버는 aws 환경이었기에 NACL 로 한번에 제한하면 어떻겠나?라고 생각을 했지만, NACL 변경 권한이 우리에게 없어서, 일단은 자체적으로 막아야 했다.</p>
<p>그 때 떠오른 방법은 우분투 내장 방화벽인 ufw를 설치하여 방화벽 룰을 만드는 것이었다.</p>
<pre><code>sudo apt-get install ufw
sudo ufw enable
sudo ufw deny from (malicious IP) to any</code></pre><p>위와 같이 방화벽 세팅을 했다.
(그런 짓은 하지 말아야 했는데...)</p>
<p>방화벽 설정 후 ssh 연결을 끊고 나서 문제를 인지했다.</p>
<p>방화벽은 기본적으로  any any deny룰이 깔려 있다.</p>
<pre><code>sudo ufw status
ufw deny from (malicious IP) to any
(ufw deny from any to any) -&gt; 얘가 숨어 있음</code></pre><p>이런 형태였던 것이다.
정보보호병으로서 방화벽 룰을 그렇게 많이 만졌었는데, 이걸 잊어버리다니... 아무튼</p>
<p>위오 같은 형태였기에, 서버로 향하는 모든 요청이 다 ufw에서 거부된 것이다,</p>
<h3 id="해결-방안">해결 방안</h3>
<p>현재 해당 서버를 실제 서비스로 운영중이기에 빠르게 복구를 해야하는 상황이었다.
하지만 ssh로는 ec2 인스턴스에 대한 접속을 못하는 상황이다,</p>
<p>문제 상황을 인지하고 나서 가장 먼저 시도한건 ssh를 제외한 다른 접속 방법이 있는지 확인하는 것이다.</p>
<h3 id="aws-직렬-콘솔-접속">aws 직렬 콘솔 접속</h3>
<p>가장 먼저 시도한 건 aws 직렬 콘솔 접속이었다. 
<code>aws 직렬 콘솔 접속을 하고, ufw를 해제해야지!!</code> 라는 생각을 했었다.</p>
<p>aws 직렬 콘솔은 어떤? 이유인지는 모르겠지만 방화벽에 걸리지 않고 잘 된다. 이 방식에 대해서는 한 번 공부해봐야겠다.
네트워크가 아닌 다른 방식으로 연결하고 있다는게 추측이다.</p>
<p>현재 우리 계정의 권한으론 aws 직렬 콘솔 접속을 할 수 없어서 코치님들에게 요청해 ufw를 풀려고 했다.</p>
<p>하지만 안 된다...
aws 인스턴스에 접속을 하고 root 계정의 비밀번호를 설정하지 않으면, 직렬 콘솔 접속이 안 된다.(이번에 배웠다.)</p>
<p>우리 팀은 pem키로만 서버에 접속했기에 root 비밀번호를 설정할 일이 없었다. root 권한이 필요하면 sudo를 쓰면 됐으니까</p>
<p>ssh 접속 -&gt; 방화벽에 막힘
직렬 콘솔 접속 -&gt; 비밀번호 미설정으로 접속 불가</p>
<p>다방면으로 접속을 시도해본 결과 기존 ec2를 접속하여 복구하는 것은 불가능하다.는 결론을 얻었다.</p>
<h3 id="새로운-ec2-생성">새로운 ec2 생성</h3>
<p>결국 새로운 ec2를 만들고 해당 서버로 prod 서버를 대체해야 했다.</p>
<p>이를 인지하고 나서 (너무나도 부정하고 싶었지만)
바로 prod 서버를 구축했을 때의 문서를 확인해서 체크리스트를 만들었는데 </p>
<ul>
<li>CD를 위한 self hosted runner 교체</li>
<li>docker 설치<ul>
<li>docker hub 로그인</li>
</ul>
</li>
<li>무중단 배포 스크립트 작성</li>
<li>nginx 설치<ul>
<li>nginx 설정파일 작성</li>
<li>nginx 무중단 배포 설정 파일 작성</li>
</ul>
</li>
<li>새 ec2 s3 권한 부여</li>
<li>로그 파일 복구</li>
<li>새로운 IP 기존 도메인에 할당</li>
<li>etc...</li>
</ul>
<p>그냥 설치만 하는 것들은 문제가 없는데 설정파일을 별도로 백업해두질 않았어서 문제가 생겼다.
(+ 추가적으로 지금 서비스 상에 문제가 있어서 warn.log 와 error.log들을 분석해야할 필요성을 느꼈다.)</p>
<p>초반엔 아예 문서만을 통해서, 서버를 복구해보려 했으나 조사 중 ec2의 볼륨을 ebs라는 형태로 제공해주고 있다는 것을 알았다.</p>
<p>그러면 어떤 걸 시도해볼 수 있을까?</p>
<h3 id="ebs를-기반으로-새로운-ec2-만들기">ebs를 기반으로 새로운 ec2 만들기</h3>
<h4 id="snapshotebs으로-복구하기">snapshot(ebs)으로 복구하기</h4>
<p>ebs를 snapshot을 찍어 ec2를 만드는 것을 가장 먼저 시도해봤다.
그러면 서버가 사실 교체가 되는거니까.</p>
<p>문제가 없을 거라고 생각 해봤지만, 복구 시 ufw 룰까지 적용되어 복구될 것 같아서 배제했다.
파일의 경로가 똑같았기에 아마 ufw도 그대로 복구가 될 것이라 생각했다.</p>
<h4 id="새로운-ec2에-기존-ebs를-루트-볼륨으로-교체">새로운 ec2에 기존 ebs를 루트 볼륨으로 교체</h4>
<p><code>새로운 ec2를 만들고 해당 ec2에서 ebs를 root 볼륨으로 갈아끼운 후, ufw 설정을 바로 꺼보면 어떨까?</code>라는 생각을 하고 이를 시도해봤다.</p>
<p>하지만, 동작하는 서버의 root 볼륨을 바꾸는 작업은 다방면으로 시도해봤지만 실패했다.
시간이 있었다면 다양하게 시도해봤을텐데, 일단 복구가 우선이었기에 막힌 부분은 빠르게 넘겼다.</p>
<h4 id="새로운-ec2에-추가-파일시스템으로-기존-ebs-할당">새로운 ec2에 추가 파일시스템으로 기존 ebs 할당</h4>
<p>마지막으로 ec2에 파일 시스템을 만들고 해당 파일 시스템에 ebs를 할당하는 방법을 생각해봤다.</p>
<p><a href="https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/ebs-using-volumes.html">aws 공식문서 ebs 볼륨을 linux에서 추가하기</a>
라는 공식문서를 보고, 해당 방식의 가능성을 느끼고 시도해봤다.</p>
<p>해당 문서를 보고 많은 시도를 하다가, 나중에 꺠달음을 얻었다.</p>
<p>문서에 있는 내용 다 필요 없고, <strong>ebs 볼륨을 aws 콘솔에서 ec2에 추가한 후 mount만 하면 연결이 가능하다.</strong> (문서가 날 속였어!)</p>
<p>초기에 ebs 볼륨을 콘솔에서 ec2에 추가하면 아래와 같은 파일시스템 구조를 띈다.</p>
<pre><code>ubuntu@ip-192-168-1-148:~$ lsblk 
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS 
loop0 7:0 0 21.3M 1 loop /snap/amazon-ssm-agent/7529 
loop1 7:1 0 49.1M 1 loop /snap/core18/2794 
loop2 7:2 0 59.3M 1 loop /snap/core20/2019 
loop3 7:3 0 109.6M 1 loop /snap/lxd/24326 
loop4 7:4 0 35.5M 1 loop /snap/snapd/20102 
nvme0n1 259:0 0 20G 0 disk 
├─nvme0n1p1 259:2 0 19.9G 0 part / 
└─nvme0n1p15 259:3 0 99M 0 part /boot/efi
nvme1n1 259:1 0 20G 0 disk 
├─nvme1n1p1 259:4 0 19.9G 0 part 
└─nvme1n1p15 259:5 0 99M 0 part </code></pre><p>여기서 중요한건 <code>nvme0n1</code> 과 <code>nvme1n1</code>이다. 
<code>nvme0n1</code>은 새로운 ec2의 볼륨이고, <code>nvme1n1</code>은 prod 서버의 볼륨이다.
나는 <code>nvme1n1p1</code>을 마운트하여, 해당 디스크의 정보를 옮기는 것이 목표였다.</p>
<p>여기서 배경지식이 없었기에 많은 삽질을 했었는데, 
단순하게 <code>sudo mount /dev/nveme1n1p1 /back</code> 만 호출해주면 됐다.
공식문서에 있는 내용대로 할 필요가 없었다.</p>
<p>옮기고 나서, 아예 /back 경로에 있는 파일들을 /경로로 덮어씌우려고 했다.
이를 테면 <code>sudo cp -v /back/* /</code> 으로 말이다.
이러면 기존 서버의 볼륨을 루트 볼륨을 교체한 것 과 같은 효과를 낼 수 있을 거라고 생각했다.</p>
<p>우분투에선 모든 설정들이 파일로 관리되고 있었기에 위처럼 하면 빠르게 해결될 거라 생각했다.</p>
<p>그치만 이 작업이 너무 느리기도 하고 확신이 없어서 중간에 멈췄다. 
중간에 우분투의 동작을 담당하는 파일과 같은 중요 내용들이 덮어씌워질 때 우분투가 정상적으로 유지 될 거라는 확신이 없었다.
(추후에 시도해보려고 한다.)</p>
<p>그래서, 서버에서 실제로 작성한 파일들인
/home/ubuntu의 파일들과
/etc/nginx의 파일들만 복사하고, 나머지를 세팅해줬다.
(docker 설치, 도메인 ip 변경 등등)</p>
<p>복사한 후에는 5분만에 서버를 정상적을 배포했다.</p>
<h3 id="후기">후기</h3>
<p>이 서버 복구과정을 거치면서, 정말 힘들긴 했지만 많은 걸 배울 수 있었다.</p>
<ul>
<li>서버를 spof로 구성하면 안 되는 이유</li>
<li>os에서 직접 관리하는 방화벽에 대한 위험성</li>
<li>ebs, 파일 시스템, 볼륨에 대한 내용</li>
<li>문제가 발생했을 때 해결하기 위한 탐색 과정 등</li>
</ul>
<p>서버는 짧은 시간동안 내려갔지만, 그 동안 너무 많은 일들이 있었다.
이번 일은 절대 잊혀지지 않을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[엔티티의 id를 테스트에서 어떻게 세팅해야 할까?]]></title>
            <link>https://velog.io/@hong-sile/%EC%97%94%ED%8B%B0%ED%8B%B0-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%9D%98-id-%EA%B0%92%EC%9D%84-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@hong-sile/%EC%97%94%ED%8B%B0%ED%8B%B0-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%9D%98-id-%EA%B0%92%EC%9D%84-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Tue, 08 Aug 2023 13:24:32 GMT</pubDate>
            <description><![CDATA[<h1 id="문제-상황">문제 상황</h1>
<p>현재 Spring boot와 Spring Data Jpa를 사용하여 프로젝트를 진행하고 있었습니다.
그러다가 하나의 문제에 직면했는데요 바로 다음과 같습니다.</p>
<p>아래와 같은 Participant와 Member라는 Entity가 있습니다.</p>
<pre><code class="language-java">@Entity
@Getter
public class Participant{

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

    @ManyToOne
    @JoinColumn(nullable = false)
    private Member member;

    @ManyToOne
    @JoinColumn(nullable = false)
    private Event event;

    public void validOwner(final Member member){
        if(this.member.isSameMember(member)){
            throw new EventException(EvetExceptionType.PARTICIPANT_NOT_ONWER);
        }
    }
    /* 다른 코드들*/
}

@Entity
@Getter
public class Member{

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

    /* 다른 모드들*/
    public boolean isSameMember(final Member member){
        return this.getId().equals(member.getId());
    }
}</code></pre>
<p>위와 같이 작성하고 진행하던 중 validateOwner를 테스트하는 중에 문제가 생겼습니다.</p>
<p><strong>validateOwner를 테스트하기 위해선, member의 Id가 세팅되어 있어야 한다는 문제였죠.</strong></p>
<p>service 레이어의 통합테스트에선 문제가 되지 않습니다.
영속성 컨텍스트를 이용해서, Id 값을 세팅해줄 수 있기 때문이죠.</p>
<p>하지만, Entity 단위테스트에선 어떻게 해야 할까요?</p>
<h1 id="다양한-시도들">다양한 시도들</h1>
<p>프로덕션 코드에서는 Entity 객체의 Id를 세팅하는 방법은 오로지 영속성 컨텍스트를 이용한 방법뿐입니다.</p>
<p>다른 방법으로는 세팅할 수 없죠. 하지만, 단위테스트에서는 영속성 컨텍스트를 동작시키진 않습니다.</p>
<p>즉 다른 방식으로 세팅해야 한다는 것이었죠.</p>
<p>Id를 세팅하기 위해서 다양한 방법을 시도해봤습니다.</p>
<h2 id="id를-받는-constructor를-정의">Id를 받는 constructor를 정의</h2>
<p>가장 간단한 방법입니다. Entity 클래스에 Id를 파라미터로 받는 생성자를 정의하면 됩니다.</p>
<pre><code class="language-java">@Entity
@Getter
public class Member{

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

    public Member(final Long id){
        this.id = id;
    }

    /* 다른 모드들*/
    public boolean isSameMember(final Member member){
        return this.getId().equals(member.getId());
    }
}</code></pre>
<p><strong>장점</strong> </p>
<ul>
<li>간단하게 구현할 수 있다. </li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>프로덕션에서도 의도치 않게, 생성자를 사용할 수도 있다.</li>
</ul>
<p>이 방법의 가장 강한 강점은 만들기 쉽다는 겁니다. 그냥 생성자를 정의하면 되죠.</p>
<p>하지만, 가장 큰 단점 또한 갖고 있습니다. 테스트를 위해서 프로덕션의 코드를 변경했을 뿐 아니라, 프로덕션에서 잘못 쓸 위험 또한 갖고 있죠.</p>
<p>실제로 DB에 영속화되지 않은 값인데, id값을 갖는 객체가 생길수도 있다는 단점이 너무 커서 이 방법을 가장 먼저 떠올렸지만 사용하지 않았습니다.</p>
<p>접근제어자로 생성자를 호출할 수 있는 곳을 한정지으려고 해봤지만,
Member Entity를 의존하는 Participant Entity의 패키지가 서로 달랐기에, 문제가 됬습니다. </p>
<h2 id="entityproxy-정의">EntityProxy 정의</h2>
<p>두 번째 방법은 EntityProxy를 정의하는 방법입니다. proxy를 정의하는 방법은 크게 두 가지가 있죠.
상속과 합성입니다.</p>
<h3 id="상속">상속</h3>
<p>상속으로 구현한다면 다음과 같이 할 수 있을 것입니다. </p>
<pre><code class="language-java">@Entity
@Getter
public class Member {

    /* 다른 코드들*/
    protected void setId(final Long id){
        this.id=id;
    }
}


public class MemberProxy extends Member {

  public MemberProxy(final Long id) {
    //실제로 Member 생성자엔 다양한 파라미터가 들어갔으나 설명을 위해 생략
    super();
    super.setId(id);
  }
}</code></pre>
<p>MemberProxy 객체는 테스트 패키지에 정의해두고 사용할수도 있겠죠.</p>
<p><strong>장점</strong></p>
<ul>
<li>구현이 비교적 쉽다.</li>
<li>구현된 MemberProxy는 테스트 패키지에서만 존재하므로, 프로덕션에 큰 영향을 끼치진 않는다.</li>
<li>상속했기에, Member가 쓰이는 곳 어디에서든 쓰일 수 있다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>프로덕션에 protected setter를 만들어야 한다.</li>
</ul>
<p>상속을 이용한 방법 또한 생성자를 이용한 방법보단 덜하더라도, 프로덕션에 영향이 가긴 합니다.
비록 protected로 제한을 해두긴 하였으나, 어느정도 개방했다는 점에서 별로 좋게 느껴지진 않습니다.</p>
<h3 id="합성">합성</h3>
<p>합성으로 구현한다면 다음과 같이 할 수 있을 것입니다.</p>
<pre><code class="language-java">@Entity
@Getter
public class Member{

    /* 다른 코드들*/
}


public class MemberProxy {

  private final Long id;
  private final Member member;

  public MemberProxy(final Long id) {
    this.id = id;
    this.member = new Member();
  }

  public Long getId(){
    return id;
  }
}


participant.validateOwner(memberProxy); -&gt; 타입 불일치로 오류</code></pre>
<p>합성으로 구현하면, 프로덕션에 가는 영향이 없긴 하지만, 타입 불일치로 <code>pairticipant.validateOwer</code>에서 에러가 발생합니다.</p>
<p>그래서 둘을 적절히 혼합하는 방법을 생각해봤고 아래처럼 구현해봤습니다.</p>
<h3 id="최종-프록시">최종 프록시</h3>
<pre><code class="language-java">public class MemberProxy extends Member {

  private Long id;

  public MemberProxy(final Long id) {
    //실제로 Member 생성자엔 다양한 파라미터가 들어갔으나 설명을 위해 생략
    super();
    this.id = id;
  }

  @Override
  public Long getId(){
    return id;
  }
}</code></pre>
<p><strong>장점</strong></p>
<ul>
<li>프로덕션에 영향이 하나도 가지 않는다.</li>
<li>테스트를 위해 생성한 클래스이기에 편하게 조작할 수 있다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>테스트를 진행할 때마다 매번 새로운 클래스를 정의하고 이를 관리해야 한다.</li>
</ul>
<p>위 방법처럼 하면 드디어 프로덕션에 하나도 영향을 가지 않고 테스트를 진행할 수 있습니다.
하지만, 매번 단위테스트를 할 때마다 프록시 클래스를 정의해야 한다는 단점이 있죠.</p>
<h2 id="mockito-spy">Mockito Spy</h2>
<p>프록시 객체를 정의하는 방법이 프로덕션에 영향이 안 가고 좋긴하나, 매번 새로운 클래스를 정의해야 한다는 번거로움이 있습니다.</p>
<p>어떻게 간편화 할 수 없을까? 고민하다가, 가짜 객체를 쉽게 만들어주는 Mockito를 떠올렸습니다.</p>
<p>단순 mock 객체를 만드는 건 문제가 있습니다. 저희가 원하는 건 id값에 대한 부분이지, 다른 메서드까지 모킹할건 아니었으니까요. </p>
<p>만약 Mockito.mock으로 member 객체를 생성한다면, member에서 메서드가 추가될 때마다 스터빙을 하는 코드를 작성해야 할 것이고, 이는 오히려 프록시 객체를 만드는 것보다 더 많은 소요를 발생시킬 것입니다.</p>
<p>Mockito의 spy는 기존 객체를 그대로 들고 있으면서 특정 메서드만 스터빙 할 수 있는 객체입니다.
이를 이용하면 다음과 같이 정의할 수 있죠.</p>
<pre><code class="language-java">class ParticipantTest {

  private Member member;
  private Event event;

  @BeforeEach
  void setUp() {
  //각각의 fixture들은 id값이 세팅되지 않은 객체를 반환합니다.
    member = Mockito.spy(TestFixture.memberFixture());
    event = Mockito.spy(TestFixture.eventFixture());
    when(event.getId()).thenReturn(1L);
    when(member.getId()).thenReturn(2L);
  }
}</code></pre>
<p>이렇게 spy를 이용하면 별도의 테스트를 위한 클래스를 만들 필요도 없기에 유지보수 할 필요도 없고, Id값을 원하는 방식으로 세팅할 수 있습니다.</p>
<p><strong>장점</strong></p>
<ul>
<li>별도의 클래스 파일을 만들고 관리할 필요가 없다.</li>
<li>생성이 매우 간편하다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>아직 잘 모르겠다.</li>
</ul>
<h1 id="결론">결론</h1>
<p>Jpa 를 사용하다 보니 도메인도 Jpa에 맞춰서 코드를 작성해야 했습니다. 
그러다보니 Jpa에 도메인이 의존적으로 바뀌었고(Id),
Jpa가 쓰이지 않는 단위테스트에서 어떤식으로 테스트를 해야 프로덕션에 영향이 덜가면서 신뢰할 수 있는 테스트를 작성할 수 있나 많은 고민을 했습니다.</p>
<p>다양한 방법을 시도해본 결과, spy를 이용한 방법이 젤 좋다고 생각했습니다.</p>
<p>다른 좋은 방법이 생각나지 않는 한 Jpa를 사용하는 프로젝트에서 다음과 같이 값을 세팅하여 프로젝트를 진행할 것 같습니다.</p>
<p>Jpa를 사용하는 프로젝트가 아니면 다르게 엔티티를 생성할테니 id를 받는 생성자를 열어둘 것 같습니다.</p>
<p>다 쓰고나서 보니, 너무 JPA 의존적인 개발 방식인 것 같네요.</p>
<p>상황에 따라서 잘 취사선택해야할 것 같습니다.</p>
<p>글에 대한 조언 또는 지적은 언제나 환영입니다. 댓글로 편하게 남겨주세요</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OAuth 2.0 개요]]></title>
            <link>https://velog.io/@hong-sile/OAuth-2.0-%EA%B0%9C%EB%85%90%EA%B3%BC-%EC%84%A4%EA%B3%84-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@hong-sile/OAuth-2.0-%EA%B0%9C%EB%85%90%EA%B3%BC-%EC%84%A4%EA%B3%84-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sun, 09 Jul 2023 06:55:02 GMT</pubDate>
            <description><![CDATA[<p>우아한 테크코스에서 테코톡이라는 세미나가 있습니다.
테코톡에서는 크루들이 하나의 개념 또는 기술을 정하고, 이를 10분정도 발표하여 지식을 공유하는 일종의 행사입니다.</p>
<p>저는 이번에 OAuth 2.0이라는 기술로 테코톡을 진행했습니다.</p>
<p>일상생활에서 많이 접할 수 있는 기술인데, 기존에 아예 알지 못했어서 이번 기회에 제대로 배워보고자 테코톡 주제로 골랐습니다.</p>
<p>테코톡에서 시간 관계상 다루지 못한 내용도 많아서, 블로그에 포스팅 해보려고 합니다.</p>
<h2 id="인증과-인가">인증과 인가</h2>
<p>OAuth 2.0에 대해 알고자 하면, 인증과 인가에 대해서 알고 있어야 합니다. </p>
<p>만약 인증과 인가에 대해서 잘 모르신다 하시면, 아래 추천 영상에 들어가서 학습을 하시는 것을 추천합니다.
<a href="https://www.youtube.com/watch?v=y0xMXlOAfss&amp;pp=ygUa7YWM7L2U7YahIOyduOymneqzvCDsnbjqsIA%3D">추천 영상</a></p>
<h2 id="oauth-등장-배경">OAuth 등장 배경</h2>
<p>요즘 개발되는 서비스는 독립적으로 혼자만의 기능을 쌓아올리진 않습니다.</p>
<p>타 서비스의 기능을 본인의 서비스에 접목시켜서 사용자가 사용하기 편하며, 시너지를 발생시켜 더 놀라운 기능을 제공해주죠.</p>
<p>하지만, 이 때 타사의 서비스에서 소유자만 접근 가능 한 리소스를 접근하는 것이 문제가 됩니다.</p>
<p>별도의 인증과 인가 과정이 필요하기 때문이죠.
OAuth를 적용하기 전 어떠한 방식으로 이를 구현하는 지 한번 보겠습니다.</p>
<p>유저의 구글 캘린더에 접근하여, 일정을 통합해서 관리해주는 우테코 캘린더라는 서비스를 만든다고 가정하고, 이야기를 하겠습니다.</p>
<h3 id="oauth-적용-이전">OAuth 적용 이전</h3>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/22719fb8-5884-4e0a-a03b-2c8a66671ba4/image.png" alt=""></p>
<p>우테코 캘린더는 유저만 접근 가능한 리소스인 구글 캘린더 정보에 접근해야 합니다.
이 때 캘린더에 접근하기 위해서, 다음과 같은 흐름을 거칩니다.</p>
<ol>
<li>유저에게 구글 ID와 PW를 제공 받고, </li>
<li>이를 통해서 우테코 캘린더는 구글에 로그인을 합니다.</li>
<li>구글에선 이 로그인이 유효한걸 확인하고, 사용자의 캘린더 정보를 우테코 캘린더에게 돌려 줍니다.</li>
<li>우테코 캘린더는 받은 캘린더 정보를 가공하여 유저에게 서비스를 제공합니다.</li>
</ol>
<p>위와 같은 흐름에서는 다양한 문제가 발생할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/7ed18f4c-3724-477d-ad30-50ee7b79622e/image.png" alt=""></p>
<ol>
<li><p>유저 입장에서는 우테코 캘린더를 신뢰할 수 없습니다.
유저 입장에서는 인증정보를 우테코 캘린더에게 넘기는 것이기에, 우테코 캘린더가 이를 따로 빼돌리거나, 해당 인증정보로 유저의 다른 리소스에도 접근할 수 있죠.</p>
</li>
<li><p>우테코 캘린더 입장에서도 이는 큰 부담입니다.
유저의 구글 ID와 PW 같이 중요도가 높은 인증정보를 관리하다가 유출된다면? 바로 서비스를 내려야 할 것입니다.</p>
</li>
<li><p>구글 입장에서도 이는 달가운 일이 아닙니다.
아무리 구글이 보안 수준을 높이는 방식으로 서비스를 구성하더라도, 사용자의 인증 정보 자체가 노출되면, 구축한 보안이 너무 무용해지죠.</p>
</li>
</ol>
<p>그러면 이러한 문제가 왜 생기는 걸까요?
바로 우테코 캘린더에서 유저의 ID, PW를 사용하기 때문에 일어난 문제입니다.</p>
<p>자 처음으로 돌아가서,</p>
<blockquote>
<p>우테코 캘린더는 왜 구글 ID, PW를 사용했을까요?</p>
</blockquote>
<p>유저의 캘린더에 접근 권한을 부여받기 위해섭니다.
즉, <code>인가</code>과정 때문이죠.</p>
<p>인가가 실행되기 위해선 선행되는 과정이 있습니다. 무엇일까요?
바로 <code>인증</code>입니다.</p>
<p>그래서, 우테코 캘린더는 인가를 위한 권한을 받기 위해서, 인증과정을 수행했던 것입니다.</p>
<p>그러면, 이렇게 생각해보면 어떨까요?</p>
<blockquote>
<p>인증은 유저가 직접
권한은 서비스에게 부여</p>
</blockquote>
<p>인증의 책임은 유저에게, 리소스에 접근하는 책임은 서비스에게 부여하는 것이죠.</p>
<p>이러한 아이디어에서 시작된 게 OAuth입니다.</p>
<h3 id="oauth-적용-이후">OAuth 적용 이후</h3>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/6431e718-9f48-4d3f-ae0a-c19015add4de/image.png" alt=""></p>
<p>유저는 구글에 직접 인증을 합니다. 중요한 건 이 인증과정에 참여하는 주체는 오로지 <code>유저</code>와 <code>구글</code> 뿐이라는 사실입니다.</p>
<p>우테코 캘린더는 이 과정의 어디서도 관여하지 않습니다.</p>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/b63a7219-d2ea-49cf-b4a8-1326dd4b37aa/image.png" alt="">
인증이 유효한다면, 구글은 우테코 캘린더에게 <code>권한</code>을 부여합니다.
우테코 캘린더는 이 권한으로 유저의 리소스에 접근할 수 있죠.</p>
<p>이 때 이 권한을 부여하는 과정에서 유저는 배제되어 있습니다.
권한을 전달하고, 이를 사용하는 과정을 최소화 함으로써 탈취당할 위험을 줄인거죠.</p>
<p>여기까지 보시면 알 수 있는 것은 </p>
<blockquote>
<p>OAuth는 인가를 위해서 설계된 기술이라는 것입니다. </p>
</blockquote>
<p>이것이 OAuth의 추상적인 흐름입니다. 이제 이 과정을 어떻게 구체화 했는지 살펴보죠.</p>
<h2 id="oauth-flow">OAuth Flow</h2>
<p>OAuth를 공식적으로 정의한 문서인 <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-1.2">RFC-6749</a>에서는 총 4가지 흐름을 정의했습니다.</p>
<ol>
<li>Authorization Code Flow</li>
<li>Implicit Flow</li>
<li>Resource Owner Password Credentials Flow</li>
<li>Client Credentials</li>
</ol>
<p>하지만, 대부분의 OAuth 제공사들이 Authorization Code Flow를 지원하기에 Authoriation Code Flow 기준으로 글을 진행하도로 하겠습니다.</p>
<h2 id="oauth-role">OAuth Role</h2>
<p>아까 예를 들었던 우테코 캘린더에서의 각각의 주체를 OAuth에서 부르는 용어가 있습니다. 이를 간단하게 짚고 넘어가보죠.
<img src="https://velog.velcdn.com/images/hong-sile/post/d913b3c3-9e5a-4e96-a494-1572647ee684/image.png" alt=""></p>
<ul>
<li><p>유저 - Resource Owner
인증을 수행하는 주체이면서, 실질적으로 리소스에 대한 소유권을 가진
주체입니다.
클라이언트에게 리소스에 대한 권한을 위임하죠.</p>
</li>
<li><p>우테코 캘린더 - Clinet(Third-party application)
권한을 위임 받는 주체입니다. 리소스에 소유권을 지니진 않았지만, 위임 받은 권한으로 리소스에 접근합니다.</p>
</li>
<li><p>구글</p>
<ul>
<li>Authoriziation Server 
인증의 유효성을 검증하고, 권한을 Clinet에게 부여하는 주체입니다. </li>
<li>Resource Server
인가 과정을 수행하고, 실질적으로 리소스를 Clinet에게 제공하는 주체입니다.</li>
</ul>
</li>
</ul>
<h2 id="oauth-setting">OAuth Setting</h2>
<p>OAuth를 사용하기 위해선 먼저 선행되어야 하는 것이 있습니다.
바로 OAuth 제공사에 OAuth를 사용할 서비스를 등록하는 것이죠.</p>
<p>등록하는 과정에서 개발자가 설정해야 할 것은 크게 두 가지 입니다.</p>
<ul>
<li>권한을 반환받을 RedirectUri</li>
<li>권한에 포함시킬 범위(유저 이메일, 성별, 생일 등등)</li>
</ul>
<p>redirectUri를 설정하는 것은 보안적인 이유입니다. 추후, 흐름 부분에서 부가적으로 설명하도록 하겠습니다.</p>
<p>권한에 포함시킬 범위를 지정함으로써 부여된 권한이 접근할 수 있는 리소스를 한정지을 수 있습니다.</p>
<p>이를 이용해서 무분별하게, Clinet가 리소스에 접근하는 것을 막을 수 있죠.</p>
<p>추가적으로 서비스를 등록했을 때 꼭 알아야 할 것도 있는데요.</p>
<ul>
<li>Client를 식별할 ClientId</li>
<li>권한을 발급 받을 때, 발급 요청한 주체가 OAuth 제공사에 등록된 Client인지 인증할 때 사용하는 ClinetSecret</li>
</ul>
<p>이제 구체적인 흐름을 알아볼텐데 이는 다음 포스트에서 다루도록 하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 테스트 최적화]]></title>
            <link>https://velog.io/@hong-sile/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@hong-sile/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Thu, 08 Jun 2023 16:08:37 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>스프링이 포함된 테스트는 순수 자바 코드의 테스트와 다르게 신경써야할 점들이 있습니다. </p>
<p>개인적으로 스프링에 테스트를 짤 때 신경써야할 점에 대해서, 정리해보도록 하겠습니다.</p>
<h1 id="테스트-속도">테스트 속도</h1>
<p>스프링에서 테스트를 하면, 순수 자바의 테스트보다 속도가 느립니다.</p>
<p>스프링에선 빈을 사용하기 위해 ApplicationContext를 사용하기 때문입니다. 실제 테스트 시간의 대부분은 ApplicationContext를 만드는 데 쓰입니다.</p>
<img src = "https://velog.velcdn.com/images/hong-sile/post/fb3dd328-003a-458f-a720-78178653d41b/image.png" width="50%" >
<img src = "https://velog.velcdn.com/images/hong-sile/post/fd2ac5c2-033d-4b42-acdb-1763b3197cb8/image.png" width="50%">

<p>ApplicationContext를 사용하지 않는 단순한 도메인 테스트(OrderTest)는 417ms가 걸린 반면,
ApplicationContext를 사용하는 통합 테스트(OrderIntegrationTest)는 1sec 282ms가 걸린 것을 볼 수 있습니다.</p>
<p>테스트 속도는 테스트 코드 품질 중에서 무시할 수 없는 요소입니다. 테스트 자체가 빠른 피드백을 위한 도구인데, 속도가 느리다면 장점이 퇴색되기 때문이죠.</p>
<p>그렇다면, 속도를 높이기 위해서 저희는 어떤 시도를 할 수 있을까요?</p>
<h2 id="캐싱">캐싱</h2>
<blockquote>
<p>Once the TestContext framework loads an <code>ApplicationContext</code> (or <code>WebApplicationContext</code>) for a test, that context is cached and reused for all subsequent tests that declare the same unique context configuration within the same test suite.</p>
</blockquote>
<p><em>테스트에 대한 ApplicationContext(또는 WebApplicationContext)를 로드하면 해당 컨텍스트는 캐시되어 동일한 테스트 스위트 내에서 동일한 고유 컨텍스트 구성을 선언하는 모든 후속 테스트에 재사용됩니다.</em></p>
<p><a href="https://docs.spring.io/spring-framework/reference/testing/testcontext-framework/ctx-management/caching.html">스프링 공식문서</a>에 나온 내용입니다.</p>
<p>간단하게 테스트에서 ApplicationContext를 로드하면 이를 캐싱하고, 이후의 동일한 구성의 ApplicationContext가 필요한 테스트에서 재사용한다는 내용입니다.</p>
<p>이러한 캐싱을 잘 이용한다면, 테스트에 걸리는 시간을 크게 단축시킬 수 있습니다.</p>
<p>이전에 저는 @Import 어노테이션을 사용하여, 해당 테스트에서만 사용하는 Bean들만 ApplicationContext에 등록시키는 방식으로 테스트를 작성하였습니다.</p>
<p>이렇게 사용하면, 테스트에서만 사용된 빈들만 생성되므로, 조금 더 효율적이고 빠른 테스트가 작성될 줄 알았습니다.</p>
<p>하지만, <strong>그렇지 않았죠</strong>.</p>
<p>이런식으로 특정 테스트에 최적화하는 식으로 Bean들을 등록하면, 캐싱된 ApplicationContext를 사용할 수 없습니다. </p>
<p>등록하려는 빈을 4,5개 줄이려다가 캐싱기능을 활용하지 못했기에, 오히려 더 많은 시간이 걸린 것입니다.</p>
<p>그래서 저는 사용하지 않는 빈이 등록되더라도, 기존 캐싱된 Context를 활용하는 방식으로 테스트를 작성하는 방식으로 수정했습니다.</p>
<p>하지만, 테스트를 작성할 때마다 매번 동일한 구성을 기억해서 사용하는 것은 매우 귀찮습니다. </p>
<p>그렇다고, 모든 테스트에서 @SpringBootTest를 붙일 수도 없습니다. 특정 테스트에서는 다양한 설정을 </p>
<p>다음과 같은 방법으로 동일한 구성의 컨텍스트를 유지하면서 사용하게 할 수 있습니다.</p>
<h3 id="custom-annotation-정의">Custom Annotation 정의</h3>
<p>이 방법은 우테코 크루에게 공유받은 방법입니다. (정말 뛰어난 사람들이 많은 것 같습니다.)</p>
<pre><code class="language-java">@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@JdbcTest(includeFilters = {
        @Filter(type = FilterType.ANNOTATION, value = Repository.class)
})
public @interface RepositoryTest {
}</code></pre>
<p>위와 같은 식으로, @Repository 어노테이션이 붙은 클래스를 모두 빈으로 등록하는 새로운 Annotation을 정의할 수 있습니다.  </p>
<p>여기서, 조금 더 다양한 설정을 추가할 수도 있습니다. @Profie을 이용해 해당 테스트에서만 사용하는 application.properties를 정의할 수도 있죠. 사용하기에 따라 다양한 방향으로 확장할 수 있는 방법입니다.</p>
<p>사용할 때 단순하게 해당 Annotation만 붙이면 되니, 더욱 편하게 컨텍스트의 설정을 관리할 수 있습니다.</p>
<h3 id="abstactclass-상속">AbstactClass 상속</h3>
<pre><code class="language-java">@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public abstract class IntegrationTest {
    @Autowired
    protected MockMvc mockMvc;
}</code></pre>
<p>다음은 테스트에서 abstract class를 상속하는 방법입니다. </p>
<p>클래스를 상속한다는 것은, 사실 부모클래스가 먼저 초기화 되고, 자식클래스가 초기화되는 방식이기에, 부모클래스에 적용된 어노테이션을 자식클래스에게 모두 적용되는 효과를 얻을 수 있습니다.</p>
<p>이렇게 하면, @Autowired를 통해 주입받은 객체 또한 편하게 자식 테스트 클래스에서 사용할 수 있다는 장점도 있습니다.</p>
<h1 id="e2e-테스트-작성방법">E2E 테스트 작성방법</h1>
<p>웹 환경이 도입되면서, 입력부터 출력까지 검증하는 E2E테스트를 추가로 작성하게 됬습니다.</p>
<p>스프링에서는 E2E 테스트를 위해 크게 두 가지 도구를 이용할 수 있는데, 두 개 다 사용해보고 난 장단점을 정리해봤습니다.</p>
<h2 id="restassured">RestAssured</h2>
<p>RestAssured는 실제 웹환경을 띄워서 테스트하는 방식으로 E2E테스트를 지원합니다.</p>
<p>아래와 같이 작성할 수 있죠.</p>
<pre><code class="language-java">@Sql(&quot;/init-integration-data.sql&quot;)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class IntegrationTest {
    @LocalServerPort
    private int port;

    @BeforeEach
    void setUp() {
        RestAssured.port = port;
    }
}</code></pre>
<p>SpringBootTest에서 RANDOM_PORT로 설정을 하고, 이를 RestAssured에 port로 등록하는 과정이 필요합니다.</p>
<p>RestAssured는 실제 웹 요청을 하기에, 어떠한 포트로 요청을 보낼지 세팅을 해줘야 하는 것이죠.</p>
<p>그렇게 해서 다음과 같이 테스트를 작성할 수 있습니다.</p>
<pre><code class="language-java">
@DisplayName(&quot;경로 조회 기능 테스트&quot;)
class PathIntegrationTest extends IntegrationTest {

    @DisplayName(&quot;출발점과 도착점으로 경로와 총 거리와 금액을 구한다.&quot;)
    @Test
    void findPath() {
        final PathRequest pathRequest = new PathRequest(SAMSUNG.getName(), SEONGLENUG.getName());
        final StationResponse samsung = new StationResponse(SAMSUNG.getId(), SAMSUNG.getName());
        final StationResponse seongLenug = new StationResponse(SEONGLENUG.getId(), SEONGLENUG.getName());
        final PathResponse expectedResponse =
                new PathResponse(List.of(samsung, seongLenug), 1250, 5);

        final ExtractableResponse&lt;Response&gt; response = RestAssured
                .given().log().all()
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .body(pathRequest)
                .when().get(&quot;/path&quot;)
                .then().log().all().extract();

        // then
        final PathResponse pathResponse = response.as(PathResponse.class);

        assertThat(pathResponse)
                .usingRecursiveComparison()
                .isEqualTo(expectedResponse);
    }
}</code></pre>
<p>RestAssured는 as와 같은 메서드를 지원해서, 실제 요청의 반환값을 자바 객체로 쉽게 매핑할 수 있습니다. 테스트하기가 더욱 용이해지지요.</p>
<p>그리고 given when then의 bdd 스타일이기에 가독성이 뛰어나다는 점도 있습니다.</p>
<p>하지만, RestAssured는 큰 단점이 있는데 바로 @Transactional을 사용하지 못한다는 것 입니다.</p>
<p>그렇기에, 테스트간 격리를 위해서 매번 <code>@Sql(&quot;/init-integration-data.sql&quot;)</code> 와 같은 어노테이션으로 데이터베이스를 초기화 해줘야 하죠.</p>
<h2 id="mockmvc">MockMvc</h2>
<p>많은 사람들이 MockMvc를 이용해서 Controller나 Interceptor의 sliceTest를 하고 있습니다.</p>
<p>하지만 MockMvc또한, E2E테스트에 사용할 수 있는 도구입니다.</p>
<p>MockMvc는 웹 환경을 Mocking하는 것으로 실제로 서블릿 컨테이너를 모킹한 객체입니다. 그렇기에 모든 Bean을 등록하는 @SpringBootTest와 함께 사용할 수 있죠.</p>
<pre><code class="language-java">@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public abstract class IntegrationTest {

    @Autowired
    protected MockMvc mockMvc;
} </code></pre>
<p>@AutoConfigureMockMvc는 자동으로 MockMvc 객체를 만들어주는 어노테이션입니다. 해당 어노테이션으로 유저가 만든 Filter들도 다 MockMvc에 적용되게 하죠.</p>
<p>실제로 위와 같은 클래스를 정의하고 아래와 같이 사용할 수 있습니다.</p>
<pre><code class="language-java">class OrderIntegrationTest extends IntegrationTest {

    @DisplayName(&quot;쿠폰을 사용하지 않은 주문내역을 조회한다.&quot;)
    @Test
    void findOrderWithoutCoupon() throws Exception {
        //given
        final Product savedProduct = productRepository.insertProduct(PRODUCT_1);
        final CartItem savedCartItem = cartItemDao.save(new CartItem(3, MEMBER_1, savedProduct));
        final List&lt;CartItem&gt; cartItems = List.of(savedCartItem);
        final Order savedOrder = orderRepository.save(Order.of(MEMBER_1, cartItems));

        //when
        final MvcResult result = mockMvc
                .perform(get(&quot;/orders/&quot; + savedOrder.getId())
                        .header(&quot;Authorization&quot;, MEMBER_1_AUTH_HEADER))
                .andDo(print())
                .andExpect(status().isOk())
                .andReturn();

        //then
        final String resultJsonString = result.getResponse().getContentAsString();
        final OrderResponseDto response = OBJECT_MAPPER.readValue(resultJsonString, OrderResponseDto.class);

        assertThat(response)
                .usingRecursiveComparison()
                .isEqualTo(OrderResponseDto.from(savedOrder));
    }
}</code></pre>
<p>mockMvc에서 결과를 자바객체로 매핑해주는 기능을 지원하지 않기에, ObjectMapper를 이용하여서, 직접 역직렬화를 해줘야 합니다.</p>
<p>하지만, WebEnviroment가 RandomPort가 아닌 Mock이기에, @Transactional을 사용할 수 있습니다. 별도의 trucnate를 하는 sql을 작성하고 관리할 필요는 없죠. </p>
<p>덕분에 테스트를 격리하는 데 굉장히 편합니다.</p>
<p>두 테스트 방법을 모두 사용해봤는데, 의미적인 차이는 딱 <strong><code>서블릿 컨테이너를 모킹했냐, 직접 웹 환경을 띄우냐</code></strong> 정도의 차이입니다.</p>
<p>가장 크게 와닿았던 차이는 @Transactional을 사용할 수 있냐, 사용할 수없냐 정도의 차이가 있습니다.</p>
<p>그리고, 실제 MockMvc는 실제 서블릿 컨테이너를 띄우지 않기에, 속도가 조금 더 빠르고, andDo(print())로 콘솔로 봤을 때 실제로 어떤 요청이 오갔는지 파악하기 편한다는 장점이 있습니다.</p>
<p>각자의 장단점이 있기에 본인에게 편한 방식을 사용하면 좋을 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 DB 테스트 격리하기]]></title>
            <link>https://velog.io/@hong-sile/%EC%8A%A4%ED%94%84%EB%A7%81-DB-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B2%A9%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hong-sile/%EC%8A%A4%ED%94%84%EB%A7%81-DB-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B2%A9%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 07 May 2023 08:39:03 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>이번 레벨 2 장바구니 미션을 진행하면서, 본격적으로 통합테스트를 작성해봤습니다.</p>
<p>DB가 연관되서 통합테스트를 진행하려다보니 테스트간 디비 격리가 잘 되지 않았는데요. </p>
<p>이번 포스팅에서는 인수테스트, 단위테스트, 프로덕션간의 디비를 분리하고, 각각의 테스트 메서드를 어떻게 격리했는지를 보여드리도록 하겠습니다.</p>
<p>해당 포스팅에서 언급하는 단위테스트는 persistence layer에서 사용하는 JdbcTest이고,
인수테스트는 RestAssured 기준으로 작성하였습니다.</p>
<h1 id="분리해야할-환경">분리해야할 환경</h1>
<h2 id="프로덕션db와-테스트db">프로덕션DB와 테스트DB</h2>
<p>가장 먼저 분리해야할 환경입니다. </p>
<p>별도의 설정을 하지 않으면, 테스트에서는 DB와 관련된 설정을 할 때 Production의 application.properties를 참고하여 DataSource를 만들고, 해당 DataSource로 DB연결을 하여 테스트를 진행합니다.</p>
<p>프로덕션 코드가 실행되는 도중 인수 테스트를 실행하면 테스트가 프로덕션 DB에 영향을 주게 됩니다.</p>
<p>이러한 불상사를 방지하기 위해, 테스트 전용 application.propreties를 정의하여 datasource의 url을 분리합니다.
<img src="https://velog.velcdn.com/images/hong-sile/post/e426f738-7f1e-4299-8d9e-1920947e22b1/image.png" alt=""></p>
<p>위 처럼 test에서 application.properties를 정의하고 datasource.url을 다르게 정의하면 프로덕션과 테스트간의 DB를 분리할 수 있습니다.</p>
<h2 id="인수-테스트와-단위-테스트">인수 테스트와 단위 테스트</h2>
<p>그러면 같은 datasource를 사용하는 인수테스트와 단위테스트는 어떻게 분리해야할까요?</p>
<p>사실 이는 큰 문제가 없습니다. <code>@JdbcTest</code> 어노테이션에 보면 다음과 같은 어노테이션이 붙어있습니다.</p>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/e5418a91-6794-43fd-88f2-b255d345cc0e/image.png" alt=""></p>
<p>@AutoConfigureTestDataBase라는 어노테이션은 가짜 DB를 만들어서, DB관련한 테스트를 쉽게 할 수 있도록 도움을 줍니다.</p>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/74fcef3f-63b2-4d9f-b5ca-2847662162a2/image.png" alt=""></p>
<p>실제 test가 실행될 떄 로그를 보면 완전 다른 datasource.url인 ‘jdbc:h2:mem:6d095…’가 사용된 것을 볼 수가 있습니다.</p>
<p>그렇기에, jdbcTest 어노테이션을 붙이면 다른 테스트에 영향이 가지 않도록 DB환경을 분리를 할 수 있습니다.</p>
<h1 id="테스트-방식">테스트 방식</h1>
<h2 id="테스트-메서드-마다-db에-값-넣기">테스트 메서드 마다 DB에 값 넣기</h2>
<p>가장 처음에 활용한 방법입니다. DB관련 로직을 수행할 때 테스트 데이터를 매 실행시마다 넣어주도록 하였습니다.</p>
<h3 id="repository의-save-메서드-활용하기">Repository의 save 메서드 활용하기</h3>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/b8cd948a-3cdb-4ded-b0a1-f5bfc60ee8ad/image.png" alt=""></p>
<p>가장 처음에 활용한 방법입니다. 조회,수정,삭제를 테스트하기위해 repository의 save 메서드를 활용하여, 초기 데이터를 넣었습니다.</p>
<p>하지만 이 방법은 모든 CRUD 로직 테스트가 save 메서드에 의존적이게 되기도 하고, 값이 여러개 있는 케이스를 테스트하기 위해 여러번 쿼리를 날리게 됩니다.</p>
<p>batchInsert를 정의하면 되긴 하지만, 프로덕션에서 사용하지 않는 로직을 테스트를 위해 작성하는 것이 부담스러웠습니다.(심지어 DAO 로직)</p>
<h3 id="sql-어노테이션으로-값-넣기">Sql 어노테이션으로 값 넣기</h3>
<p>그 다음에 찾아 학습한 방식은 Sql 어노테이션을 이용한 방법이었습니다.(역시 밸덩)</p>
<p><a href="https://www.baeldung.com/spring-jdbctemplate-testing">Spring JdbcTemplate Unit Testing | Baeldung</a></p>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/09910ac7-3f09-42c8-b0ed-73f7097e297b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/66894f3c-e296-4136-8bb9-ffd43bc96f2f/image.png" alt=""></p>
<p>testFixture.sql을 정의하고, 테스트 메서드에 붙였습니다.</p>
<p>테스트 로직이 실행되기전에 testFixture.sql이 실행되도록 하였습니다.</p>
<p>이러면 이전 방법보다 여러개의 데이터를 세팅할 떄 connection 비용을 절약할 수도 있고, save 메서드에 의존하지 않게 만들 수 있습니다.</p>
<p>단, 이 방법도 결국엔 매 테스트 메서드마다 connection 비용이 발생합니다.</p>
<p>이정도에서 멈춰도 되지만, 더 좋은 방법이 없을까? 싶어서 다른 방법을 찾아보았습니다. </p>
<h2 id="초기-더미데이터-세팅하고-transactional-이용하기">초기 더미데이터 세팅하고, Transactional 이용하기</h2>
<p>Spring에는 @Transactional 이라는 간단한 어노테이션을 이용해서, 디비의 값을 롤백시킬 수 있습니다.</p>
<p>테스트메서드에 붙이면, 테스트가 종료된 후 DB의 값을 롤백해주죠.(스프링 매직!)</p>
<p>물론 테스트에서, @Trasnactional을 지양하는 사람들도 있긴 합니다. </p>
<p>하지만 이는 규모가 크고 여러 명이 협업하는 상황에서 문제가 되는 것이기에, 이정도 규모의 프로젝트에서는 충분히 사용할 수 있다고 생각하여 이를 활용해보았습니다. </p>
<h3 id="beforeall에서-데이터-삽입하고-테스트-진행하기">BeforeAll에서 데이터 삽입하고, 테스트 진행하기</h3>
<p>테스트에서 BeforeAll은 모든 테스트 메서드가 실행되기전에 딱 한번 호출되는 메서드입니다.</p>
<p>그래서 맨 처음에 BeforeAll메서드를 정의하고, <code>해당 메서드에 @Sql 어노테이션을 붙이면 되지 않을까?</code> 라고 생각하였습니다. 바로 시도해봤죠.</p>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/3a834a01-f917-4f5e-9b95-4e7c69967b6e/image.png" alt=""></p>
<p>결과는 대실패… 어쩐일인지 테스트 메서드에서 확인을 해봤을 때, 아무런 값이 들어가지 않은 상태로 테스트가 진행되었습니다.</p>
<p>테스트 디비가 생성되기전에 script를 날려서 그런가?라는 생각도 해봤지만, 
그랬으면 SqlSytnaxError가 떠야하는데, 정상적으로 테스트가 진행됬으니 디비가 생성된 후 쿼리가 사용되었다는 것을 알 수 있습니다.</p>
<p>혹시 가짜DB가 생성되기 전에 script가 실행되서 그런가 싶었지만, 그런것도 아니었습니다.</p>
<p>Transaction 매니저가 롤백했나? 싶었지만, 로그에서 TransactionManager를 보았을 때 setUp을 rollback한다는 로그는 어디에도 없었습니다.</p>
<p>이 부분은 아직도 파악하지 못한 상태긴 합니다. 혹시 아시는 분은 댓글 부탁드립니다. </p>
<h3 id="datasql-이용하기">data.sql 이용하기</h3>
<p>beforeAll에서 초기화 하는것이 막혔으니, 이제는 db가 초기화될 때 딱 한번 실행하는 script인 data.sql을 이용해보기로 하였습니다.</p>
<p>그렇게 test 전용 data.sql을 만들었는데,,,</p>
<p>테스트를 돌려보니 프로덕션의 data.sql과 테스트의 data.sql이 둘 다 실행되는 것이었습니다.</p>
<p>다행히 프로덕션에서는 프로덕션의 data.sql만 실행되었습니다.</p>
<p>그렇다하더라도 의도치 않게 테스트에서 초기 데이터를 설정하는 것에 프로덕션의 의존성이 생겨버렸습니다. 유지보수하기 더 힘들어지곘죠.</p>
<h3 id="프로덕션과-테스트-datasql-분리하기">프로덕션과 테스트 data.sql 분리하기</h3>
<p>test패키지 하위에 있는 data.sql을 적용하는 방법을 찾다가 다음과 같은 옵션을 application.properties에서 발견했습니다.</p>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/cdab5ead-e6d7-4c9e-8c50-5f2f5ee9c3ec/image.png" alt=""></p>
<p>data.sql의 경로를 임의로 지정할 수 있는 옵션이었죠. test의 application.properties에서 해당 옵션을 적용하여, 테스트 패키지 하위에 있는 data.sql만 적용되도록 하였습니다.</p>
<p>이제, 저희는 data.sql와 application.properties로 JdbcTest에서, 일관된 디비 데이터를 유지할 수 있으므로, JdbcTest 클래스에서 테스트 메서드간 디비 격리를 수행할 수 있습니다.</p>
<h3 id="문제">문제</h3>
<p>이 방법에도 한가지 맹점이 있습니다. </p>
<p>@Transactional 어노테이션을 이용하는 방식 Transactional 어노테이션을 사용할 수 없는 RestAssured에서는 위와 같은 방법을 적용할 수 없습니다.</p>
<h2 id="인수-테스트에서-격리">인수 테스트에서 격리</h2>
<p>RestAssured에서는 Transactional이 동작하지 않기에, <code>초기 더미데이터 세팅하고, Transactional 이용하기</code> 와 같은 방법을 사용할 수 없습니다.</p>
<p>이번에 인수테스트에서 테스트간 격리를 위해 시행한 방법은 두 가지였습니다.</p>
<p>하나는 이전에 사용했던  <code>Sql 어노테이션으로 값 넣기</code> 였고, 다른 하나는 @DirtiesContext입니다.</p>
<h3 id="dirtiescontext">@DirtiesContext</h3>
<p>이 어노테이션이 붙어있으면, 현재 테스트가 실행될 때 기존의 ApplicationContext가 존재하더라도,  새로운 ApplicationContext를 로드합니다. 그렇기에, 테이블도 다시 만들어지고, data.sql도 동작하기에 초기상태를 계속 유지할 수 있죠.</p>
<p>단, 이 방법은 컨텍스트를 계속 로드한다는 점에서, 굉장히 좋지 못한 방법입니다.</p>
<p>저는 학습용으로만 한 번 써보고, 그 다음에 바로 지웠습니다.</p>
<p>최종적으로는 @Sql 어노테이션을 이용한는 방법을 택하였습니다.</p>
<h1 id="최종-형태">최종 형태</h1>
<h2 id="공통">공통</h2>
<p>가장 먼저 테스트용 application.properties를 정의하여서 프로덕션과 테스트 디비를 분리하였습니다. 이는 인수테스트든 단위테스트든 공통적으로 적용되는 사항입니다.</p>
<h2 id="인수-테스트">인수 테스트</h2>
<p>인수 테스트에서는 fixutre.sql을 정의하여서 매 테스트 메서드가 실행될 떄 fixture.sql이 동작하도록 하였습니다.</p>
<p>fixture에서는 truncate 문과 더미 데이터를 insert하는 script가 있습니다.</p>
<h2 id="단위-테스트">단위 테스트</h2>
<p>단위 테스트에서는 application.properties의 spring.sql.init.data-location 옵션으로</p>
<p>테스트에서만 사용할 더미데이터가 있는 data.sql을 정의하고, @Transactional 어노테이션을 이용해서 롤백을 하는 방식으로 테스트간 격리를 하였습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링에선 어떻게 Exception을 처리해야할까?]]></title>
            <link>https://velog.io/@hong-sile/%EC%8A%A4%ED%94%84%EB%A7%81%EC%97%90%EC%84%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-Exception%EC%9D%84-%EC%B2%98%EB%A6%AC%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@hong-sile/%EC%8A%A4%ED%94%84%EB%A7%81%EC%97%90%EC%84%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-Exception%EC%9D%84-%EC%B2%98%EB%A6%AC%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Mon, 24 Apr 2023 06:34:43 GMT</pubDate>
            <description><![CDATA[<h1 id="고민">고민</h1>
<p>처음으로 Spring이라는 웹 프레임워크를 사용했습니다. </p>
<p>페어와 같이 1단계 미션을 진행하다, 스프링에서 어떻게 Exception을 처리할지 고민하고, 여러 방법을 찾아봤습니다.</p>
<p>후술할 내용은 이번 미션을 진행하면서 겪은 경험 반 고민 반이 섞인 글입니다.</p>
<h1 id="시도해본-방법">시도해본 방법</h1>
<h2 id="에러-페이지로-리다이렉트">에러 페이지로 리다이렉트</h2>
<p>가장 먼저 시도해본 것은 에러페이지로 리다이렉트 시키는 것이었습니다.
에러페이지를 만들어 보내줄 때도, 저희가 도메인에서 정의한 에러 메시지를 통해 에러페이지를 랜더링해서 보내려고 했습니다.</p>
<p>처음엔 금방 완성할 것 같았습니다. @ExceptionHandler가 붙은 hanlde 메서드에서는 파라미터로 Exception을 받을 수 있었고, 해당 exception의 message를 이용하면 동적으로 전달해 줄 수 있을 것 같았죠.</p>
<p>그리고, Thymleaf를 이용하면 에러메시지를 보여줄 수 있는 이쁜 페이지를 만들 수 있을 것 같았죠.</p>
<p>하지만 하나 큰 문제가 있었습니다. 저희가 만드는 애플리케이션은 RESTful Application이었기에, 서버사이드 랜더링 페이지는 의미가 없었습니다…</p>
<h2 id="responseentity에-메시지-담기">ResponseEntity에 메시지 담기</h2>
<p>다음으로 떠올린 방법은 학습테스트에서 사용했던 방법이었습니다.</p>
<p>Http응답으로 badRequest에 에러메시지를 담아서 반환해줍니다. </p>
<pre><code class="language-java">@ExceptionHandler(RacingCarException.class)
public ResponseEntity&lt;String&gt; handle(RacingCarExcetption e) {
    return ResponseEntity.badRequest().body(e.getMessage());
}</code></pre>
<p>일단, 현재 Spring과 프로젝트 구조 상 이 상태가 최선이라고 여겼습니다.</p>
<h1 id="추가적인-의문">추가적인 의문</h1>
<p>미션을 진행하다, 리뷰어가 새로운 질문을 저에게 던져주었습니다. </p>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/4146e2f1-8c84-499b-8db2-5c68a42cefda/image.png" alt=""></p>
<p>애플리케이션에서 예상하지 못한 다른 예외가 터지면 어떻게 될까? 레벨 1단계에서는 생각지도 못한 질문이었습니다.</p>
<p>이제 웹 애플리케이션으로 넘어가면서, 이전 콘솔과 달리 에러가 발생할 포인트가 늘어났습니다.
단적인 예로, DB와의 연결문제나, 외부 API를 사용할 때의 문제가 있겠죠.</p>
<p>이 경우는 자바 코드 내부에서 발생하는 문제가 아니라, 의존하는 외부 환경(DB, API)에 따라 발생하는 문제입니다. 어떻게 예방할 수 있는 문제가 아니죠</p>
<p>이에 대해서 어떻게 처리를 할 지 고민을 해봤습니다.</p>
<h2 id="발생하는-exception의-종류">발생하는 Exception의 종류</h2>
<p>먼저 Exception을 크게 다음과 같이 분류할 수 있을 것입니다.</p>
<ol>
<li>클라이언트의 잘못으로 발생한 Exception</li>
<li>서버의 잘못으로 발생한 Exception</li>
</ol>
<h3 id="클라이언트의-잘못으로-발생한-exception">클라이언트의 잘못으로 발생한 Exception</h3>
<p>사실 이 부분은 크게 고민할 여지가 없었습니다. 클라이언트의 잘못으로 발생한 Exception이란 말은 다음 말과 유사합니다. </p>
<p>클라이언트의 <code>잘못된 요청</code>으로 발생한 Exception</p>
<p>여기서 잘못된 요청이란 크게 두 가지가 있겠죠</p>
<ol>
<li>잘못된 Http 요청(존재하지 않는 API, 권한)</li>
</ol>
<p>위와 같은 경우는 Spring이 자동으로 반환해주는 Error 가 있기에 크게 걱정할 필요는 없습니다.(Ex. 404 Not Found.)</p>
<ol>
<li>도메인 로직상 유효하지 않는 값이 입력된 경우</li>
</ol>
<p>위와 같은 경우는 ExceptionHandler에서 400대 에러로 변환시켜서 반환해주면 좋을 것입니다.</p>
<p>추가적으로 상세한 ErrorMessage를 Response body에 추가해 반환해준다면 더 좋겠죠.
위에서 본 예시가 있을 것입니다.</p>
<pre><code class="language-java">@ExceptionHandler(RacingCarException.class)
public ResponseEntity&lt;String&gt; handle(RacingCarExcetption e) {
    return ResponseEntity.badRequest().body(e.getMessage());
}</code></pre>
<h3 id="서버의-잘못으로-발생한-exception">서버의 잘못으로 발생한 Exception</h3>
<p>서버의 잘못으로 발생한 Exception 또한 크게 두 가지로 나뉠 것입니다.</p>
<ol>
<li>자바 내부 로직에서 미쳐 정의하지 못한 Exception</li>
</ol>
<p>이 Exception 같은 경우에는 잘 찾아서, 최대한 Exception이 발생하는 원인을 파악하고, 로직에서 조취를 취하면 됩니다. (validation로직 추가 작성 등)</p>
<ol>
<li>외부 환경에 의존하기 때문에 발생하는 Exception</li>
</ol>
<p>DB 서버의 연결과 같은 에러가 있겠죠. 아쉽게도 이 부분은 자바 내부의 코드를 고친다고 해결되는 문제가 아닙니다.</p>
<p>그렇지만 이와 같은 Exception을 어쩔 수 없지 하고 방치해둘 수는 없습니다.</p>
<h2 id="떠올린-해결-방안">떠올린 해결 방안</h2>
<h3 id="첫-번째-해안">첫 번째 해안</h3>
<p>가장 처음에 떠올렸던 단순한 해결 방법은, 모든 Exception을 400대 에러로 포장해서 반환해주는 것이었습니다. 이유는 다음과 같았습니다.</p>
<p>“500대 에러를 클라이언트에 노출해봤자, 어차피 클라이언트가 해줄 수 있는 것은 없으니까, 그냥 400대 에러로 포장해서 반환해주자.”</p>
<pre><code class="language-java">@ExceptionHandler(Exception.class)
public ResponseEntity&lt;String&gt; handle() {
    return ResponseEntity.badRequest().body();
}</code></pre>
<p>하지만, 코드를 작성하자마자 “이게 맞나?” 라는 생각이 떠올랐습니다.</p>
<blockquote>
<p>이런식으로 다 400대로 묶어서 처리할거면 500대 에러 코드는 왜 있을까?</p>
</blockquote>
<blockquote>
<p>만약 서버의 문제인데, 클라이언트가 본인이 잘못 요청했다고 판단한다면, 서버에서 발생한 문제는 어떻게 발견할 수 있을 것인가?</p>
</blockquote>
<p>조금 관점을 바꿔 봤습니다. 만약 이미 완성된 어플리케이션이 있다 하더라도, 그 어플리케이션에는 아직 발견되지 않은 에러가 있을 수 있습니다. </p>
<p>정말 정말 잘 만들어서 로직에서 발생할 수 있는 모든 예외처리를 한 로직이 있다 하더라도, 의존하는 외부환경에서 발생하는 Exception은 어떻게 해결할 수 없죠.</p>
<h3 id="두-번째-해결방안">두 번째 해결방안</h3>
<p>생각을 조금 바꿔봤습니다. 기존에는 Exception을 미리 예방을 하는 방식이었습니다. 계속하여 validation 로직을 짰던 것이 그 일환이죠.</p>
<p>그것이 개발자로서 중요한 덕목이라 생각했고, 실제로 중요하니까요.</p>
<p>하지만, 저는  <code>모든 Exception을 미리 예방한다는 것은 불가능하다</code>라는 생각이 이번 미션에서 들었습니다. </p>
<p>예방을 못하니까, <code>빠른 조치를 할 수 있는 방법</code>을 생각해봤습니다.</p>
<p>그 방법으로 생각해낸 것이 log였습니다. </p>
<p>서버를 관리하다보면, 아마 Error log를 주기적으로 확인할 것 같습니다.<del>(그런게 없다면…돔황챠)</del></p>
<p>서버가 문제가 생겼다면 가장 먼저 log로 원인을 파악할 테니까요.</p>
<p>사실 저희가 작성한 프로젝트가 계속 사용될 게 아니기에, 로그를 남기는 로직은 오버엔지니어링이지만, 학습을 위한 목적으로 한번 작성해보았습니다.</p>
<pre><code class="language-java">@RestControllerAdvice
public class RacingCarControllerAdvice {

    private final Logger logger = LoggerFactory.getLogger(getClass());
    ...

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity&lt;String&gt; loggingUnExpectedException(final RuntimeException e) {
        logger.error(UNEXPECTED_ERROR_LOG_FORMAT, convertToString(e));
        return ResponseEntity.internalServerError()
                .body(UNEXPECTED_ERROR_MESSAGE);
    }

    private String convertToString(final Exception e) {
        final StringWriter sw = new StringWriter();
        final PrintWriter pw = new PrintWriter(sw);
        e.printStackTrace(pw);
        return sw.toString();
    }
}</code></pre>
<p>간단하게 RuntimeException이 발생하면 해당 exception의 stackTrace를 로그로 남기는 로직입니다.</p>
<p>다른 세련되게 로그를 남기는 방법이 있긴 하겠지만, stackTrace를 찍어서, 에러가 발생된 시점을 로깅하는게 지금 제가 생각하기에 가장 좋은 방법이었습니다.</p>
<p>(다른 좋은 방법이 있다면 댓글 부탁드립니다)</p>
<p>로깅을 한 후 500대 에러를 반환합니다. 하지만, 에러 메시지는 고정된 메시지를 반환합니다. 클라이언트가 추가적으로 조치를 취해줄 것이 없기 때문에, 반환되는 바디에는 굳이 유의미한 값을 담지는 않습니다.</p>
<p>대신 개발자가 에러로그를 보았을 때, 외부 환경의 문제면 외부 환경을 개선할 것이고, 
어플리케이션의 문제면, 어플리케이션의 로직에서 validation 로직을 추가하여 프로그램을 개선할 수 있을 것입니다. </p>
<h1 id="결론">결론</h1>
<p>위에서의 내용을 간략하게 요약해보겠습니다.</p>
<ol>
<li>클라이언트의 잘못된 입력인 경우, 에러 메시지를 상세하게 하여 반환합니다.</li>
<li>서버의 잘못인 경우 로깅은 상세하게 하고, 추상적인 에러메시지를 반환합니다.</li>
</ol>
<p> 결과적으로 다음과 같은 클래스를 추가하여 예외처리를 하였습니다.</p>
<pre><code class="language-java">@RestControllerAdvice
public class RacingCarControllerAdvice {

    private final Logger logger = LoggerFactory.getLogger(getClass());
    ...

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity&lt;String&gt; loggingUnExpectedException(final RuntimeException e) {
        logger.error(UNEXPECTED_ERROR_LOG_FORMAT, convertToString(e));
        return ResponseEntity.internalServerError()
                .body(UNEXPECTED_ERROR_MESSAGE);
    }

    private String convertToString(final Exception e) {
        final StringWriter sw = new StringWriter();
        final PrintWriter pw = new PrintWriter(sw);
        e.printStackTrace(pw);
        return sw.toString();
    }

    @ExceptionHandler(RacingCarException.class)
    public ResponseEntity&lt;String&gt; handle(RacingCarExcetption e) {
        return ResponseEntity.badRequest().body(e.getMessage());
    }
}</code></pre>
<p>미션 링크
<a href="https://github.com/woowacourse/jwp-racingcar/pull/78#discussion_r1168112893">미션 PR 링크</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[와일드카드 VS 타입 파라미터]]></title>
            <link>https://velog.io/@hong-sile/%EC%99%80%EC%9D%BC%EB%93%9C%EC%B9%B4%EB%93%9C-VS-%ED%83%80%EC%9E%85-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0</link>
            <guid>https://velog.io/@hong-sile/%EC%99%80%EC%9D%BC%EB%93%9C%EC%B9%B4%EB%93%9C-VS-%ED%83%80%EC%9E%85-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0</guid>
            <pubDate>Sat, 08 Apr 2023 09:29:35 GMT</pubDate>
            <description><![CDATA[<h1 id="의문점--왜-와일드카드가-필요한가">의문점 : 왜 와일드카드가 필요한가?</h1>
<p>이전 글(<a href="https://velog.io/@hong-sile/%EC%A0%9C%EB%84%A4%EB%A6%AD-%ED%83%80%EC%9E%85-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0%EC%99%80-%EC%99%80%EC%9D%BC%EB%93%9C-%EC%B9%B4%EB%93%9C">링크</a>)에서 정리할 때는 와일드카드가 다음과 같은 상황에 때문에 필요하다고 하였습니다.</p>
<blockquote>
<p>Generic을 정의한 클래스에서 모든 인스턴스들이 공통적으로 사용할 메서드를 외부에서 정의할 때 문제가 된다.</p>
</blockquote>
<p>다음과 같은 예시가 있었습니다.</p>
<pre><code class="language-java">@Test
void test() {
    final List&lt;Integer&gt; integers = List.of(1, 2, 3);
    final String convertedString = convertString(integers);
}

public String convertString(final List&lt;?&gt; objects) {
    final StringBuilder stringBuilder = new StringBuilder();
    for (final Object object : objects) {
        stringBuilder.append(object.toString());
    }
    return stringBuilder.toString();
}</code></pre>
<p>위처럼 한정된 와일드카드를 이용하여서, 모든 List에서 사용할 수 있는 메서드를 외부에서 정의하였습니다. 하지만 사실 이는 타입 파라미터로도 구현 가능합니다.</p>
<h1 id="제네릭-메서드">제네릭 메서드</h1>
<p>이전 예시에서는 타입 파라미터를 클래스 선언부에서만 사용했습니다.</p>
<p>제네릭 메서드란 타입 파라미터를 메서드의 선언부에 정의하여 사용하는 것을 의미합니다.
와일드카드를 이용한 위의 코드를 다음과 같이 수정할 수 있습니다.</p>
<pre><code class="language-java">@Test
void test() {
    final List&lt;Integer&gt; integers = List.of(1, 2, 3);
    final String convertedString = convertString(integers);
}

public &lt;T&gt; String convertString(final List&lt;T&gt; objects) {
    final StringBuilder stringBuilder = new StringBuilder();
    for (final Object object : objects) {
        stringBuilder.append(object.toString());
    }
    return stringBuilder.toString();
}</code></pre>
<p>처음엔 무의식적으로 와일드카드에 대해 배울 땐 다음과 같이 생각하였습니다. “아 이래서 필요하다고 한거구나.”</p>
<p>그러다 한 번 코드로 작성해보니까, 모든 와일드카드를 사용한 부분을 다 제네릭 메서드로 처리할 수 있을 것 같았습니다.</p>
<p>실제 라이브러리에서 다음과 같이 와일드카드를 사용하였는데, 이 또한 제네릭 메서드로 고칠 수 있습니다.</p>
<pre><code class="language-java">interface Collection&lt;E&gt; {
    public boolean containsAll(Collection&lt;?&gt; c);
    public boolean addAll(Collection&lt;? extends E&gt; c);
}

interface Collection&lt;E&gt; {
    public &lt;T&gt; boolean containsAll(Collection&lt;T&gt; c);
    public &lt;T extends E&gt; boolean addAll(Collection&lt;T&gt; c);
}</code></pre>
<p>그럼 도대체 왜 제네릭이 나온거고 유연성을 높여줄 수 있다고 한 것일까?</p>
<h1 id="장단점">장단점</h1>
<h2 id="와일드카드">와일드카드</h2>
<h3 id="하한-경계">하한 경계</h3>
<table>
<thead>
<tr>
<th></th>
<th>상한 경계</th>
<th>하한 경계</th>
</tr>
</thead>
<tbody><tr>
<td>타입 파라미터</td>
<td>O</td>
<td>X</td>
</tr>
<tr>
<td>와일드카드</td>
<td>O</td>
<td>O</td>
</tr>
</tbody></table>
<h3 id="가독성">가독성</h3>
<p>와일드카드 같은 경우에는 일반적인 타입 파라미터보다 가독성이 좋다. 예를 들어 아래 코드를 보자</p>
<pre><code class="language-java">void static copy(final SimpleList&lt;? extends T&gt; copier, final SimpleList&lt;T&gt; copied) {
    for (int index = 0; index &lt; copier.size(); index++) {
        final T t = copier.get(index);
        copied.add(t);
    }
}</code></pre>
<p>copier에 있는 element들을 copied로 복사하는 코드입니다. 이 코드도 와일드카드 모두 타입 파라미터로 수정 가능합니다.</p>
<pre><code class="language-java">void static &lt;K extends T&gt; copy(final SimpleList&lt;K&gt; copier, final SimpleList&lt;T&gt; copied) {
    for (int index = 0; index &lt; copier.size(); index++) {
        final T t = copier.get(index);
        copied.add(t);
    }
}</code></pre>
<p>이 코드 또한 별 문제 없이 동작합니다. 
하지만, 타입 파라미터만 봐도 되는 와일드카드와 달리, 메서드 시그니처 앞 부분까지 읽어야 copier의 타입에 대해 파악할 수 있습니다.</p>
<p>좀 더 극단적인 예시를 봐보도록 하죠. 다음은 Map에서 unmodfiableMap을 만드는 방식입니다.</p>
<pre><code class="language-java">public static &lt;K,V&gt; Map&lt;K,V&gt; unmodifiableMap(Map&lt;? extends K, ? extends V&gt; m) {
    return new UnmodifiableMap&lt;&gt;(m);
}</code></pre>
<p>실제 Collections의 코드입니다.  간단하게 map을 받아서 새로운 map을 만들어 반환해주죠.</p>
<p>만약 이 부분에서 와일드카드를 제거한다면 어떻게 될까요?</p>
<pre><code class="language-java">public static &lt;K, V, T extends K, M extends V&gt; Map&lt;K, V&gt; unmodifiableMap(Map&lt;T, M&gt; m) {
    return new UnmodifiableMap&lt;&gt;(m);
}</code></pre>
<p>가독성은 주관적인 부분이라 다르게 느낄 수 있겠지만, 저는 가독성이 매우 안 좋아보입니다.</p>
<h2 id="타입-파라미터">타입 파라미터</h2>
<h3 id="2개-이상의-파라미터에서-강한-연관관계-설정">2개 이상의 파라미터에서 강한 연관관계 설정</h3>
<p>generic이 1개일 때는 타입 파라미터나, 와일드카드나 차이가 크게 없습니다. 모두 동일한 역할을 합니다.</p>
<p>하지만, 2개 이상일 때는, 고려해야될게 조금 생깁니다.</p>
<p>예를 들어 다음과 같은 copy 코드가 있다고 생각해 봅시다.</p>
<pre><code class="language-java">&lt;T extends Number&gt; void copy(final SimpleList&lt;T&gt; copier, final SimpleList&lt;T&gt; copied) {
    for (int index = 0; index &lt; copier.size(); index++) {
        final T num = copier.get(index);
       copied.add(num);
    }
}</code></pre>
<p>위와 같은 코드는 정상적으로 컴파일 됩니다. copier의 타입과 copied의 타입이 동일한 것을 보장할 수 있습니다. 만약 이를 와일드카드로 바꾼다면 어떻게 될까요?</p>
<pre><code class="language-java">void static copy(SimpleList&lt;? extends Number&gt; copier, SimpleList&lt;? extends Number&gt; copied) {
    for (int index = 0; index &lt; copier.size(); index++) {
        Number num = copier.get(index);
        copied.add(t);
    }
}</code></pre>
<p>위와 같은 코드가 정상적으로 작동할까요? 오류가 발생합니다. copier의 타입과 copied의 타입이 동일하지 않을 수도 있습니다. </p>
<p>copy를 호출할 때 copier엔 List<Integer>가 copied에는 List<Float>가 들어갈 수도 있습니다.
그렇기에 copied.add(t)에서 오류가 발생하죠. 두 파라미터 간의 연관성을 정의할 수 없습니다.</p>
<h3 id="반환-타입으로-사용-가능">반환 타입으로 사용 가능</h3>
<p>와일드카드 같은 경우에는 반환타입으로 사용해서는 안됩니다. 다음과 같은 코드를 보죠.</p>
<p>파라미터로 받은 List에서 연산을 하고, 해당 값을 반환하는 형태입니다.</p>
<pre><code class="language-java">@Test
void test() {
    final List&lt;Integer&gt; integers = List.of(1, 2, 3);
    final List&lt;Integer&gt; processed = processNumbers(integers);
}

public static List&lt;? extends Number&gt; processNumbers(List&lt;? extends Number&gt; numbers) {
    List&lt;? extends Number&gt; result = new ArrayList&lt;&gt;();
    // numbers에 대한 작업 수행
    // result에 작업 결과를 저장
    return result;
}</code></pre>
<p>위와 같은 코드는 어떨까요? 그렇지 않습니다. 
processNumbers의 반환 타입은 List&lt;? extends Number&gt;이지, List<Number>가 아닙니다. 
마치 List<Float>를 List<Number>에 대입해줄 수 없듯이 말이죠. 아래와 같이 수정해야 할 것입니다.</p>
<pre><code class="language-java">@Test
void test() {
    final List&lt;Integer&gt; integers = List.of(1, 2, 3);
    final List&lt;? extends Number&gt; processed = processNumbers(integers);
    processed.add(3);
}</code></pre>
<p>그럼 이 코드를 정상적으로 사용할 수 있을까요? 아닙니다. <code>processed.add(3)</code>에서 에러가 납니다. 
List의 element 타입은 ? extends Number지 Integer가 될 수 없습니다. </p>
<ol>
<li>만약 Integer를 받을 수 있도록 했다면, Float나 Double도 들어갈 수 있을 것이고, 그렇다면, processed의 내부 element들의 통일성이 깨질 것입니다.</li>
<li></li>
</ol>
<p>그러면 어떻게 사용할 수 있을까요? 다음과 같이 억지로 사용할 수 있긴 합니다.</p>
<pre><code class="language-java">@Test
void test() {
    final List&lt;Integer&gt; integers = List.of(1, 2, 3);
    final List&lt;Integer&gt; numbers = (List&lt;Integer&gt;) processNumbers(integers);
    numbers.add(3);
    numbers.forEach(System.out::println);
}</code></pre>
<p>사용자가 와일드카드를 직접 다루고, 타입 캐스팅을 해야한다는 점에서 와일드카드를 반환 타입을 사용하는 것은 좋지 못합니다.</p>
<p>반면, 타입 파라미터를 반환 타입으로 사용하면 깔끔하게 사용할 수 있죠.</p>
<pre><code class="language-java">@Test
void test() {
   final List&lt;Integer&gt; integers = List.of(1, 2, 3);
   final List&lt;Integer&gt; numbers = processNumbers(integers);
   numbers.add(3);
   numbers.forEach(System.out::println);
}

public &lt;T extends Number&gt; List&lt;T&gt; processNumbers(final List&lt;T&gt; values) {
    List&lt;T&gt; result = new ArrayList&lt;&gt;();
    // numbers에 대한 작업 수행
    // result에 작업 결과를 저장
    return result;
}</code></pre>
<h1 id="결론">결론</h1>
<h2 id="제네릭-메서드-vs-와일드카드">제네릭 메서드 vs 와일드카드</h2>
<ol>
<li>파라미터가 하나인 경우에는, 와일드카드나 타입 파라미터나 큰 차이가 없다.</li>
<li>Geneirc 값들간의 연관관계를 강하게 설정해줄 때는 타입 파라미터를 사용한다.</li>
<li>Generic 값을 반환하 때는 타입 파라미터로 반환한다.</li>
<li>한정된 Generic을 사용할 때는 가독성을 위해서 와일드카드를 고려한다.</li>
<li>하한경계를 사용할 때는 와일드카드를 사용한다.</li>
</ol>
<h1 id="풀리지-않는-의문점">풀리지 않는 의문점</h1>
<h2 id="왜-타입-파라미터는-하한경계를-지원하지-않나">왜 타입 파라미터는 하한경계를 지원하지 않나?</h2>
<p>와일드카드와 타입 파라미터를 비교할 때 가장 먼저 든 생각이 왜 타입 파라미터는 하한경계를 지원하지 않을까? 였습니다.</p>
<p>그래서 가장 먼저 타입 파라미터와 와일드카드가 내부적으로 어떻게 상한,하한경계를 설정하는지 알아 보았습니다.</p>
<h3 id="타입-소거">타입 소거</h3>
<p>내부적으로 자바에서 Generic은 컴파일 타임에 모두 제거됩니다.</p>
<p>제한을 걸지 않은 타입 파라미터와 와일드카드는 Object로, &lt;? extends Number&gt;나 <T exttends Number>는 각각 ?와 T가 Number로 바뀝니다.</p>
<p>아래 예시를 볼까요</p>
<p>타입 파라미터와 와일드카드만 제외하면 완벽하게 동일한 두 코드가 있습니다.</p>
<pre><code class="language-java">public static void printFirstElementsWildCard(List&lt;? extends Number&gt; first, List&lt;? extends Number&gt; second) {
    Number n = first.get(0);
    Number m = second.get(0);
    List&lt;Number&gt; third = new ArrayList&lt;&gt;();
    third.add(n);
    third.add(m);
    System.out.println(third);
}

public static &lt;T extends Number&gt; void printFirstElementsTypeParaemter(List&lt;T&gt; first, List&lt;T&gt; second) {
    T n = first.get(0);
    T m = second.get(0);
    List&lt;T&gt; third = new ArrayList&lt;&gt;();
    third.add(n);
    third.add(m);
    System.out.println(third);
}</code></pre>
<p>두 코드를 한 번 컴파일 시킨 후, 바이트 코드를 확인해보았습니다.</p>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/6c6a9ef0-0c3c-4cd8-b15b-cf7f7e3fcd62/image.png" alt=""></p>
<p>주석을 보면 Generic은 Signature에서 T를 Number로 치환합니다. &lt;T:Ljava/lang/Number;&gt;</p>
<p>그 후, 파라미터를 저장하는 부분인 L0와 L1에서 CheckCast Number 를 통해서, 실제 T가 Number인지 확인합니다.</p>
<p>T는 실제 코드에선 전혀 사용되지 않죠. 컴파일 타임에 한번 타입을 체크해주고 사라집니다.</p>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/5c567ecf-291a-41ef-ae4e-3939dd4e7951/image.png" alt=""></p>
<p>와일드카드도 내부적으로 모든 코드가 동일합니다. 주석만 빼면 똑같은 코드죠.</p>
<p>그래서 저는 가장 궁금했던 것이 “<strong>왜 와일드카드만 하한경계를 지원하냐?”</strong> 였습니다.</p>
<p>저희가 코드를 작성할 때 경계값을 정하는 것은 와일드카드나 타입 파라미터나 똑같습니다. </p>
<p>그래서 혹시 내부적인 내용이 다르지 않을까 싶었습니다.</p>
<p>그래서, 바이트코드를 봤지만 상한경계 부분은 동일하게 컴파일 되는 것을 확인할 수 있었습니다.</p>
<p><strong>그렇다면, JDK 설계자들이 타입 파라미터에도 하한경계를 사용할 수 있도록 구현 가능하지 않았을까요? 왜 구현하지 않았을까요?</strong></p>
<h2 id="내부적으로-하한경계를-어떻게-체크하는-걸까">내부적으로 하한경계를 어떻게 체크하는 걸까?</h2>
<p>상한 경계를 컴파일 타임에 체크하는 것은 쉽게 파악할 수 있었습니다.</p>
<p>바로 extends 뒤에 붙은 클래스로 타입 파라미터나 와일드카드를 대체하면 됬어요. </p>
<p>그러면 다형성에 의해서 체크가 가능했으니까요.</p>
<p>하지만, 하한경계는 어떨까요? 단순하게 어떠한 한 클래스로 타입 파라미터나 와일드카드를 대체할 순 없어보였습니다.
부모가 아닌 자식이 제한되는 것이었기에, 내부적으로 타입 파라미터나 와일드카드는 Object형식을 유지해야 합니다.</p>
<p>제 생각은 다음과 같습니다.</p>
<p><strong>내부적으론 와일드카드나 타입 파라미터를 Object로 치환하고, 다형성을 체크하는 것처럼 상속관계를 체크한 후 컴파일시킵니다.</strong></p>
<p>하지만 어디에 명확하게 나와있는 자료가 아니기에, 단순 추측이고, 의문으로만 남았습니다.</p>
<p>글에 대한 피드백은 항상 환영입니다.</p>
<h1 id="참고-자료">참고 자료</h1>
<p><a href="https://stackoverflow.com/questions/18176594/when-to-use-generic-methods-and-when-to-use-wild-card">https://stackoverflow.com/questions/18176594/when-to-use-generic-methods-and-when-to-use-wild-card</a></p>
<p><a href="https://www.baeldung.com/java-generics-type-parameter-vs-wildcard">https://www.baeldung.com/java-generics-type-parameter-vs-wildcard</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[순수 자바에서 JDBC 쿼리 테스트 해보기]]></title>
            <link>https://velog.io/@hong-sile/%EC%88%9C%EC%88%98-%EC%9E%90%EB%B0%94%EC%97%90%EC%84%9C-JDBC-%EC%BF%BC%EB%A6%AC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@hong-sile/%EC%88%9C%EC%88%98-%EC%9E%90%EB%B0%94%EC%97%90%EC%84%9C-JDBC-%EC%BF%BC%EB%A6%AC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Fri, 07 Apr 2023 09:58:01 GMT</pubDate>
            <description><![CDATA[<h1 id="시작하며">시작하며</h1>
<p>이번 미션에서 처음으로 DB를 적용했습니다.</p>
<p>DB를 이번 미션에서 적용하면서, 가장 고민했던 것은 <strong>“테스트를 어떻게 할까?”</strong> 였습니다.</p>
<p>(이번 프롤로그는 일종의 JDBC 쿼리 테스트 회고입니다. 그냥 이렇게도 해봤구나 하면서 봐주시면 감사하겠습니다.)</p>
<h1 id="테스트하기-어려웠던-이유">테스트하기 어려웠던 이유</h1>
<p>테스트는 <strong>멱등</strong>을 유지해야 합니다.
n번 실행해도 n번 같은 결과를 반환해야 하죠.</p>
<p>DB가 아닌 일반 코드에서는 이런 면이 크게 어렵진 않았습니다.</p>
<p>테스트를 실행할 때 자바 코드만 신경쓰면 됬고, 테스트를 종료하면 메모리가 전부 깨끗이 비우기에 이전 테스트가 다음 테스트에 영향을 주지도 않았죠. </p>
<p>하지만 데이터베이스 테스트는 다릅니다. 자바 코드외에 DB에도 의존적이기에, 신경써야할 것이 많죠. 
저는 아래와 같은 문제점을 찾았습니다.</p>
<h2 id="db-테스트-환경">DB 테스트 환경</h2>
<ul>
<li>자바 코드 외에 DB 서버와 연결에 대해 신경을 써야합니다.</li>
<li>테스트 실행 전에, 테스트에 필요한 데이터를 DB에 세팅해야 합니다.
다양한 테스트 케이스로 테스트하는 것에 대한 걸림돌이 됩니다.</li>
<li>매번 테스트 실행 후 다음 테스트에 영향이 가지 않게, 테스트 실행 동안 데이터베이스에 적재된 데이터를 비워줘야 합니다.</li>
</ul>
<p>값을 비워주지 않는다면, 테스트의 멱등이 깨집니다. 물론 값을 비워주지 않아도 되는 방향으로 테스트를 작성할 수 있지만, 한정적인 테스트밖에 작성하지 못합니다.</p>
<h2 id="테스트하는-기능의-양방향-의존성">테스트하는 기능의 양방향 의존성</h2>
<ul>
<li>테스트하는 기능(메서드)간의 양방향 의존성이 발생합니다. </li>
</ul>
<p>데이터베이스에 값을 저장, 변경하는(update) 기능을 테스트하고, 기능이 유효한지 확인하려면 값을 조회(select)하는 메서드가 필요하고,
데이터베이스에 값을 조회하는(select) 기능을 테스트하고, 기능이 유효한지 확인하려면 값을 저장,변경(update)하는 메서드가 필요합니다.</p>
<h1 id="시도해본-것들">시도해본 것들</h1>
<h2 id="db-테스트-환경-1">DB 테스트 환경</h2>
<h3 id="프로덕션-db-테스트-db-분리">프로덕션 DB, 테스트 DB 분리</h3>
<p>일단 가장 먼저 한 것은 테스트DB를 프로덕션DB와 분리한 것이었습니다. </p>
<p>도메인 로직 테스트 같은 경우에는 외부 환경에 영향을 받지 않습니다. 자바 코드에만 영향을 받죠.</p>
<p>그렇기에, 테스트환경과 프로덕션 환경을 코드에서 분리가 가능했고, 테스트에서 어떠한 일을 하더라도 실제 프로덕션에 영향을 주지 않았습니다.</p>
<p>하지만 DB 테스트는 외부 환경(DB)에 영향을 주고, 영향을 받습니다. 
만약, 프로덕션과 테스트에서 같은 DB에 접근하여 사용한다면, 프로덕션이 테스트에, 테스트가 프로덕션에 영향을 주고 받는 일이 생길 것입니다.</p>
<p>그래서, 가장 먼저 아래와 DB연결을 분리하였습니다.</p>
<pre><code class="language-java">public interface ConnectionGenerator {

    String OPTION = &quot;?useSSL=true&amp;serverTimezone=UTC&quot;;
    String USERNAME = &quot;root&quot;;
    String PASSWORD = &quot;root&quot;;

    Connection getConnection();
}

public class ConnectionGeneratorImpl implements ConnectionGenerator {

    private static final String SERVER = &quot;localhost:13306&quot;;
    private static final String DATABASE = &quot;chess-production&quot;;

    public Connection getConnection() {
        try {
            return DriverManager.getConnection(&quot;jdbc:mysql://&quot; + SERVER + &quot;/&quot; + DATABASE + OPTION, USERNAME, PASSWORD);
        } catch (final SQLException e) {
            throw new DataBaseCanNotConnectException();
        }
    }
}

public class TestConnectionGenerator implements ConnectionGenerator {

    private static final String SERVER = &quot;localhost:13307&quot;;
    private static final String DATABASE = &quot;chess-test&quot;;

    public Connection getConnection() {
        try {
            final Connection connection = DriverManager.getConnection(
                    &quot;jdbc:mysql://&quot; + SERVER + &quot;/&quot; + DATABASE + OPTION, USERNAME, PASSWORD);
            connection.setAutoCommit(false);
            return connection;
        } catch (final SQLException e) {
            throw new DataBaseCanNotConnectException();
        }
    }
}</code></pre>
<p>스키마가 완전히 동일한 두 데이터베이스를 만들었습니다.</p>
<p>프로덕션은 13306, 테스트는 13307 포트로 정의하여 접속하게 하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/b656e029-919d-48de-979e-2c7b9cb35d3a/image.png" alt=""></p>
<p>이렇게 가장 먼저 프로덕션과 테스트가 서로에게 영향을 끼치지 않도록 DB를 분리하였습니다.</p>
<p>테스트 DB라는 일종의 샌드박스를 만들었습니다.</p>
<h3 id="테스트-후-테스트-db-롤백하기">테스트 후 테스트 DB 롤백하기</h3>
<p>사실, 순수 자바가 아닌 Spring에서는 <code>@Transactional</code>이라는 어노테이션을 이용하여, DB에 변경사항을 아주 손쉽게 롤백해줄 수 있습니다.</p>
<p>하지만, 아쉽게도.. JDBC에는 해당 기능을 제공해주지 않습니다.</p>
<p>그러면 어떻게 테스트 DB를 롤백할까?에 대해 고민하였습니다. 가장 먼저 떠오른 것은, 
BeforeEach와 AfterEach를 사용해서, 테스트를 할 때마다 값을 지우는 것이었습니다.</p>
<pre><code class="language-java">@BeforeEach
void setUP(){
   //테스트에 필요한 값을 데이터베이스에 세팅하는 로직
}

@AfterEach
void tearDown(){
   //적재된 데이터베이스에 있는 값들을 전부 지우는 로직
}</code></pre>
<p>사실 위와 같은 방법이 가장 깔끔하고, 현재 프로젝트 규모에선 가장 적절한 방식입니다.</p>
<p>하지만, 테스트 DB에 디폴트로 많은 데이터가 적재되어있는 경우 위와 같은 방식이 문제가 될 수 있습니다. 
또 다음과 같은 문제 때문에 rollback을 구현하는 방향으로 리팩터링하였습니다.</p>
<p>만약 테스트 코드에서, Exception이 발생하여 테스트가 종료되는 경우, @AfterEach에 작성된 코드가 수행되지 않기에, 테스트 멱등이 깨질 수도 있습니다.</p>
<p>하지만 connection의 setAutoCommit을 false로 정의했다면, Connection이 비정상적으로 종료가 됬을 때, rolback을 해주기 때문에 이런 면에서 더 안정적입니다.</p>
<p>추가적으로 후술할 기능 의존성에서 한 가지 이점이 있습니다.</p>
<p>한번 아이디어가 떠올라서 이번 미션에서 구현해보았습니다.</p>
<p>먼저, JDBC Template을 정의합니다.</p>
<pre><code class="language-java">public interface JdbcTemplate {

    //값을 변경하는 쿼리를 실행시키는 메서드
    void executeUpdate(final String query, final Object... parameters);

    //값을 추가하고, AutoIncrement한 PK를 가져오기 위해 정의한 메서드
    &lt;T&gt; T executeUpdate(String query, RowMapper&lt;T&gt; rowMapper, Object... parameters);

    //값을 조회하는 쿼리를 실행시키는 메서드
    &lt;T&gt; T executeQuery(final String query, final RowMapper&lt;T&gt; rowMapper, final Object... parameters);
}</code></pre>
<p>실제 프로덕션에서 사용하는 template</p>
<pre><code class="language-java">public class JdbcTemplateImpl implements JdbcTemplate {

    private static final ConnectionGenerator CONNECTION_GENERATOR = new ConnectionGeneratorImpl();

    @Override
    public void executeUpdate(final String query, final Object... parameters) {
        try (final Connection connection = CONNECTION_GENERATOR.getConnection();
             final PreparedStatement preparedStatement = connection.prepareStatement(query)) {
            for (int i = 1; i &lt;= parameters.length; i++) {
                preparedStatement.setObject(i, parameters[i - 1]);
            }
            preparedStatement.executeUpdate();
        } catch (final SQLException e) {
            throw new QueryFailException();
        }
    }
... 다른 정의된 메서드들
}</code></pre>
<p>테스트에서 사용하는 template</p>
<pre><code class="language-java">public class TestJdbcTemplate implements JdbcTemplate {

    private final Connection connection;

    public TestJdbcTemplate(final Connection connection) {
        this.connection = connection;
    }

    @Override
    public void executeUpdate(final String query, final Object... parameters) {
        try (final PreparedStatement preparedStatement = connection.prepareStatement(query)) {
            for (int i = 1; i &lt;= parameters.length; i++) {
                preparedStatement.setObject(i, parameters[i - 1]);
            }
            preparedStatement.executeUpdate();
        } catch (final SQLException e) {
            throw new QueryFailException();
        }
    }

    public void rollBack() throws SQLException {
        connection.rollback();
        connection.close();
    }
}</code></pre>
<p>위 처럼 test는 rollback이라는 메서드를 정의하고 이를 afterEach에서 호출해주는 방식으로 코드를 작성하였습니다.</p>
<p>testJdbcTemplate과 JdbcTemplateImpl에서 내부 코드의 중복이 발생합니다.
이러한 중복을 제거하고 싶다면, 크게 두 가지 방법이 있습니다.</p>
<ol>
<li><p>프로덕션에서 try-with-resources를 포기하고, 똑같이 Connection을 필드로 갖는 방법이 있습니다.</p>
</li>
<li><p>try문 안에 있는 메서드를 JdbcTemplate Interface에서 정의하고, 이를 파라미터로 넘기는 방식</p>
<p>저는 프로덕션의 try-with-resources를 포기하기 싫었고, try문 안에 있는 메서드를 추출하여 파라미터로 넘기는 것이 오히려 더 이해하기 힘들다고 생각하였습니다.</p>
</li>
</ol>
<p>그리고 JdbcTemplate같은 경우에 추가적인 변경여지가 없다고 생각하여서 위와 같이 작성하였습니다.</p>
<p>위와 같이 사용해서 아래처럼 테스트를 작성할 수 있었습니다.</p>
<pre><code class="language-java">private static final long TEST_GAME_ID = 1L;
private static final Board TEST_BOARD = new BoardFactory().createInitialBoard();
private static final Turn TEST_TURN = new Turn(Color.WHITE);

private TestJdbcTemplate testJdbcTemplate;
private DataBasePieceDao pieceDao;

@BeforeEach
void setUp() {
    testJdbcTemplate = new TestJdbcTemplate(CONNECTION_GENERATOR.getConnection());
    pieceDao = new DataBasePieceDao(testJdbcTemplate);
}

@AfterEach
void tearDown() throws SQLException {
    testJdbcTemplate.rollBack();
}

@Test
@DisplayName(&quot;보드를 불러오는 기능 테스트&quot;)
void test_loadBoard() {
    final Board loadedBoard = pieceDao.loadBoard(TEST_GAME_ID, TEST_TURN);

    assertThat(loadedBoard.getBoard())
            .containsAllEntriesOf(TEST_BOARD.getBoard());
} 

... 다른 기능 테스트들</code></pre>
<p>이 방식이 완벽하게 최적화된 방식이라고는 말 못하겠지만, 저는 위와 같은 방식으로 이번 미션에서 DB 테스트를 구현해봤습니다.</p>
<h2 id="테스트하는-기능의-양방향-의존성-1">테스트하는 기능의 양방향 의존성</h2>
<p>결론부터 말하자면, 이 부분은 완벽히 해결하지 못했습니다. </p>
<p>Insert, Update와 같이 테이블에 값을 추가하거나 변경하는 기능을 테스트하려면 필연적으로 조회하는 메서드에 의존성이 생깁니다.</p>
<p>먼저 Insert와 Update로 값을 추가, 변경하고 나면 이 기능의 유효성을 확인하기 위해서는실제로 <code>어떤 값이 추가되었는지</code> 나 <code>어떤 값이 변경되었는지</code>를 검증할 것입니다.</p>
<p>순수 java에서는 getter로 값이 어떻게 변했는지 확인하여 유효성을 테스트하였지만,현재로서는 DB의 값에 접근할 수 있는 방법이 JDBC의 SQL 쿼리문밖에 없기에, 결국 다른 메서드에 의존성이 생기게 됩니다.</p>
<p>반대로 조회하는 메서드를 테스트할 때에는, 원하는 값을 셋팅해야 하기에, 상태를 변경하는(Insert,Update)하는 메서드에 의존성이 생깁니다.(java에서는 생성자로 원하는 값을 설정할 수 있지만, DB에서는 그렇지 못하니..)</p>
<p>결국 다음과 같은 합의점을 찾아냈습니다. 
테스트 데이터베이스에 디폴트로 많은 값을 적재시켜두고, 조회하는 메서드를 다양하게 테스트하여 확실하게 검증합니다. 이 방법은 rollback으로 DB테스트를 하기에 가능한 방법입니다.</p>
<p>데이터베이스의 <code>값을 바꾸는 기능</code>에서 <code>조회하는 기능</code>의 의존은 어쩔수 없더라도, 
데이터베이스의 <code>값을 조회하는 기능</code>에서 <code>값을 바꾸는 기능</code>의 의존성이 생기지 않도록, 초기 DB에 기본값을 세팅해두고 이 값들을 이용해 검증합니다.</p>
<h1 id="마치며">마치며</h1>
<p>이번에 처음으로 JDBC를 배웠고, Test 또한 배운 적이 얼마 되지 않았기에, 위와 같은 방법들을 시도해보았습니다.
개선해야할 점 또는 더 좋은 방법이 있으면 알려주시면 감사하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[제네릭: 타입 파라미터와 와일드 카드]]></title>
            <link>https://velog.io/@hong-sile/%EC%A0%9C%EB%84%A4%EB%A6%AD-%ED%83%80%EC%9E%85-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0%EC%99%80-%EC%99%80%EC%9D%BC%EB%93%9C-%EC%B9%B4%EB%93%9C</link>
            <guid>https://velog.io/@hong-sile/%EC%A0%9C%EB%84%A4%EB%A6%AD-%ED%83%80%EC%9E%85-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0%EC%99%80-%EC%99%80%EC%9D%BC%EB%93%9C-%EC%B9%B4%EB%93%9C</guid>
            <pubDate>Fri, 07 Apr 2023 09:50:30 GMT</pubDate>
            <description><![CDATA[<h1 id="제네릭이란">제네릭이란?</h1>
<p>제네릭이 도입되기 전에, 여러 타입을 사용하는 클래스와 메서드에서 Object를 사용했습니다. Object는 최상위 클래스이기에 모든 데이터를 받을 수 있기 때문이었지요.</p>
<p>하지만 반환된 객체를 사용하려면 다시 원하는 타입으로 캐스팅해야했죠.</p>
<p>이 타입 캐스팅(Obejct를 캐스팅하는 경우)의 경우 컴파일타임에 잡을 수 없고, 런타임에서만 발견되기에 많은 불편함이 있었습니다.</p>
<p>하지만 JDK 1.5에서 Generic이 도입되면서 이러한 불편점을 해소하였습니다.</p>
<p>Generic의 경우 컴파일 단계에서 이미 파일이 정해지기에, 런타임에 발견될 오류를 미리 컴파일타임에 발견할 수 있었습니다.</p>
<p>자바에선 아래와 같은 두 가지 방법으로 제네릭 타입을 지원합니다.</p>
<h2 id="타입-파라미터">타입 파라미터</h2>
<p>가장 간단하게 타입 파라미터를 이용해서 다양한 타입을 받을 수 있습니다.</p>
<p>일반적으로 타입 파라미터를 generic type이라고 부릅니다. </p>
<p>아래와 같이 타입 파라미터를 사용할 수 있습니다.</p>
<pre><code class="language-java">public class SimpleArrayList&lt;T&gt; {
    private T[] values;

    void add(T value){
    // 값을 추가하는 기능
    }
}

final SimpleArrayList&lt;Integer&gt; integers = new SimpleArrayList&lt;&gt;();
integers.add(1);
integers.add(&quot;error&quot;); -&gt; 컴파일 오류 발생!</code></pre>
<p>위처럼 List에 맞지 않는 타입이 들어오면 컴파일 단계에서 에러가 발생한다. </p>
<p>하지만 generic도 만능은 아니다. 모든 SimpleArrayList가 공통적으로 사용하는 메서드를 만들려고 하면 문제가 생긴다.</p>
<p>아래는 모든 원소들을 String 형식으로 append해 반환하는 로직이다.</p>
<pre><code class="language-java">void test() {
    final List&lt;Integer&gt; integers = List.of(1, 2, 3);
    convertString(integers); --&gt; List&lt;Integer&gt;는 List&lt;Object&gt;의 하위타입이 아니기에 오류 발생
}

public String convertString(final List&lt;Object&gt; objects) {
    final StringBuilder stringBuilder = new StringBuilder();
    for (final Object object : objects) {
        stringBuilder.append(object.toString());
    }
    return stringBuilder.toString();
}</code></pre>
<p>라이브러리 설계자가 위와 같은 메서드를 내부에서 정의한다면 만들 수 있지만, 프로그래머가 필요에 의해서 메서드를 만들 때는 문제가 된다.</p>
<p>위와 같은 문제를 해결하기 위해 와일드카드가 등장했다.</p>
<h2 id="와일드카드">와일드카드</h2>
<p>와일드 카드는 <strong>?</strong> 키워드로 사용할 수 있다. <strong>?</strong>는 어떤 타입이든 올 수 있는 것을 의미한다.</p>
<p>Object와는 다르다. 아까 위에서 사용햇던 메서드를 아래와 같이 사용할 수 있다.</p>
<pre><code class="language-java">void test() {
    final List&lt;Integer&gt; integers = List.of(1, 2, 3);
    final String convertedString = convertString(integers);
}

public String convertString(final List&lt;?&gt; objects) {
    final StringBuilder stringBuilder = new StringBuilder();
    for (final Object object : objects) {
        stringBuilder.append(object.toString());
    }
    return stringBuilder.toString();
}</code></pre>
<p>하지만 위와 같은 방법도 문제가 있다.  메서드 내부에서 Object의 메서드인 toString을 사용할 수 있지만 Object의 메서드들이 아닌 다른 메서드를 사용한다면 문제가 된다.</p>
<p>예를 들어 내부 element에서 최대값을 구해 반환하는 메서드를 정의한다고 하자.</p>
<pre><code class="language-java">@Test
void test() {
    final List&lt;Integer&gt; integers = List.of(1, 2, 3);
    max(integers);
}

public double max(final List&lt;?&gt; values) {
    double max = Double.MIN_VALUE;
    for (final Double value : values) { --&gt; 이 부분에서 오류발생
        max = Math.max(value, max);
    }
    return max;
}</code></pre>
<p><code>final Double value : values</code> 부분이 문제가 된다. ? 타입이 Double로 캐스팅 될 수 있다는 보장이 없기에 위처럼 사용할 수 없다.</p>
<p>런타임에서 에러가 터지게 두고 허용하게 둘 수도 있지만, 그러면 제네릭을 쓰는 이유가 없기에, 설계자들이 위와 같은 경우 컴파일을 못하게 막아두었다.</p>
<p>그래서 자바에서는 제한된 Generic이라는 기능을 제공해 위와 같은 케이스를 구현 가능하게 한다.</p>
<h2 id="제한된-generic">제한된 Generic</h2>
<p>제한된 Generic이란 Generic에 제한을 둬서, 모든 타입이 아닌 특정 범위의 변수만 받을 수 있도록 한 것이다.</p>
<h3 id="와일드카드-1">와일드카드</h3>
<ul>
<li>상한 경계</li>
</ul>
<p>extends 키워드를 사용하여, 자식 타입만 올 수 있도록 제한을 둘 수 있다. </p>
<p><code>&lt;? extneds Number&gt;</code> 이처럼 사용하면, Number와 Number의 자식들만 올 수 있다</p>
<pre><code class="language-java">void test() {
    final List&lt;Integer&gt; integers = List.of(1, 2, 3);
    final List&lt;Double&gt; doubles = List.of(Double.MIN_VALUE);
    final List&lt;String&gt; strings = List.of(&quot;1&quot;, &quot;2&quot;);
    max(integers);
    max(doubles);
    max(strings); ---&gt; 이 부분에서 예외 발생
}

public double max(final List&lt;? extends Number&gt; values) {
    //이 부분에서 로직이 수행된다.
}</code></pre>
<p>max의 파라미터로 List&lt;? extends Number&gt;로 제한을 두었다.</p>
<p>Number의 하위 타입인 List<Integer>, List<Double>을 받는 경우 예외가 생기지 않는다.</p>
<p>하지만, Number의 하위 타입이 아닌 String의 경우에는 컴파일 단계에서 예외가 발생한다.</p>
<ul>
<li>하한 경계</li>
</ul>
<p>하한 경계는 상한과 반대다. super 키워드를 이용하여, 자기 자신과 부모타입들만 들어올 수 있도록 제한을 둘 수 있다. </p>
<pre><code class="language-java">void test() {
    final List&lt;Number&gt; numbers = List.of(1, 2, 3);
    final List&lt;Object&gt; objects = List.of();
    final List&lt;Integer&gt; integers = List.of(1, 2);
    max(nubers);
    max(objects);
    max(integers); ---&gt; 이 부분에서 예외 발생
}

public double max(final List&lt;? super Number&gt; values) {
    //이 부분에서 로직이 수행된다.
}</code></pre>
<p>Number와 Object는 문제가 없지만, integers는 문제가 된다.</p>
<h3 id="타입-파라미터-1">타입 파라미터</h3>
<ul>
<li>상한 경계</li>
</ul>
<p>타입 파라미터 또한 와일드카드와 동일한 문법으로 사용할 수 있다.
<code>&lt;T extneds Number&gt;</code> 이처럼 사용하면, 와일드카드와 동일한 방식으로 사용이 가능하다.</p>
<pre><code class="language-java">public class SimpleNumberList&lt;T extends Number&gt;{
    ... 클래스 내부 구현
}

final SimpleNumberList&lt;Integer&gt; integers = new SimpleArrayList&lt;&gt;();
final SimpleNumberList&lt;Double&gt; doubles = new SimpleArrayList&lt;&gt;();
final SimpleNumberList&lt;String&gt; strings = new SimpleArrayList&lt;&gt;(); --&gt; 여기서 예외 발생</code></pre>
<h2 id="제한을-두는-이유">제한을 두는 이유</h2>
<p>그러면 제한을 왜 두는 걸까? 앞에서 와일드 카드에서 다뤘던 코드를 다시 보자.</p>
<pre><code class="language-java">@Test
void test() {
    final List&lt;Integer&gt; integers = List.of(1, 2, 3);
    max(integers);
}

public double max(final List&lt;?&gt; values) {
    double max = Double.MIN_VALUE;
    for (final Double value : values) { --&gt; 이 부분에서 오류발생
        max = Math.max(value, max);
    }
    return max;
}</code></pre>
<p>위 같은 경우 문제가 되는 것이 ?로 온 타입이 Double 형태로 변할지 확인이 안되기 때문에 그런 것이다. 와일드 카드에 제한을 두면 이를 해결 할 수 있다</p>
<h3 id="상한-경계">상한 경계</h3>
<pre><code class="language-java">@Test
void test() {
    final List&lt;Integer&gt; integers = List.of(1, 2, 3);
    max(integers);
}

public double max(final List&lt;? extends Number&gt; values) {
    double max = Double.MIN_VALUE;
    for (final Number number : values) {
        Double value = number.doubleValue();
        max = Math.max(value, max);
    }
    return max;
}</code></pre>
<p>위와 같은 코드는 정상 작동한다. 파라미터에서 <code>&lt;? extends Number&gt;</code>로 제한을 두었기 때문에, values의 element들은 모두 Number거나 Number의 하위 타입이다.</p>
<p>그래서  <code>for (final Number number : values)</code> 와 같은 구문이 가능하다.</p>
<h3 id="하한-경계">하한 경계</h3>
<p>상한 경계는 위처럼 사용할 수 있고, 하한 경계는 다음과 같은 경우에 사용될 수 있다.</p>
<p>예를들어 파라미터로 받은 Collection에 int값인 0를 추가하여 반환하는 케이스가 있을 것이다.</p>
<pre><code class="language-java">void addInt0(Collection&lt;? super Integer&gt; c) {
    c.add(Integer.valueOf(0));
}</code></pre>
<p>만약 단순 ? 형태였다면, int값인 0이 들어갈 수 있는지 판단이 안 되서 오류가 나겠지만, super로 하한 제한을 두었기에, 위와 같이 사용할 수 있다.</p>
<h1 id="정리">정리</h1>
<p>generic을 이용해서 유연한 코드를 작성할 수 있으며, 코드의 중복을 줄일 수 있다.</p>
<p>위와 같은 내용을 공부하며 생긴 의문점이 있는데 그 의문점들은 다음 글에 정리해보도록 하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Collections.of에 대한 의문]]></title>
            <link>https://velog.io/@hong-sile/Collections.of%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%98%EB%AC%B8</link>
            <guid>https://velog.io/@hong-sile/Collections.of%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%98%EB%AC%B8</guid>
            <pubDate>Sun, 26 Mar 2023 12:50:46 GMT</pubDate>
            <description><![CDATA[<h1 id="0-들어가기-전에">0. 들어가기 전에</h1>
<p>Collections.of 는 다양한 override 메서드를 지니고 있습니다. List를 예로 들면 List.of (E e1), List.of(E e1, E e2), … ,List.of(E … elemtns)까지 있는데, 하나의 List.of(E … elements)면 되지 않을까? 라는 질문에서 Collections.of를 학습하게 되었습니다.</p>
<h1 id="1-collectionsof란">1. Collections.of란?</h1>
<blockquote>
<p>Returns an unmodifiable Collections containing elements.</p>
</blockquote>
<p>많은 크루들이 사용하였겠지만, List.of, Set.of, Map.of 등 다양한 Collection을 of라는 팩터리 메서드로 생성할 수 있습니다. 
생성된 Collection은 Unmodifiable 즉 불변한 객체로서, 값을 추가, 삭제, 변경하려는 메서드를 호출하면 UnsupportedOperationException이 발생합니다.</p>
<h1 id="2-모던-자바인-액션의-collectionsof">2. 모던 자바인 액션의 Collections.of</h1>
<p>모던 자바인 액션에서 위 질문에 대한 답이 있습니다.</p>
<blockquote>
<p>내부적으로 가변 인수 버전은 추가 배열을 할당해서 리스트로 감싼다. 
따라서 배열을 할당하고 초기화하며 나중에 가비지 컬렉션을 하는 비용을 지불해야 한다. 고정된 숫자의 요소(최대 열개까지)를 API로 정의하므로 이런 비용을 제거할 수 있다. </p>
</blockquote>
<ul>
<li>모던 자바 인 액션 중<blockquote>
</blockquote>
</li>
</ul>
<p>처음에 해당 글을 보고, 의문이 해결되었습니다. 가변인자로 전부 받는 경우에는 내부적으로 배열이 생성되어 비용이 소모된다. 실제로 그런지 한번 List.of의 구현부를 확인해봤습니다.</p>
<h1 id="3-실제-collectionsof-구현">3. 실제 Collections.of 구현</h1>
<p>여기서는 List를 예로 들겠습니다.</p>
<h2 id="리스트의-사이즈가-02인-경우">리스트의 사이즈가 0~2인 경우</h2>
<pre><code class="language-java">static &lt;E&gt; List&lt;E&gt; of() {
    return ImmutableCollections.emptyList();
}

static &lt;E&gt; List&lt;E&gt; of(E e1) {
    return new ImmutableCollections.List12&lt;&gt;(e1);
}

static &lt;E&gt; List&lt;E&gt; of(E e1, E e2) {
    return new ImmutableCollections.List12&lt;&gt;(e1, e2);
}

static final class List12&lt;E&gt; extends AbstractImmutableList&lt;E&gt;
            implements Serializable {

        @Stable
        private final E e0;

        @Stable
        private final E e1;

        List12(E e0) {
            this.e0 = Objects.requireNonNull(e0);
            this.e1 = null;
        }

        List12(E e0, E e1) {
            this.e0 = Objects.requireNonNull(e0);
            this.e1 = Objects.requireNonNull(e1);
        }
    ...
}</code></pre>
<p>보시면 요소가 0,1,2개인 경우에는 위와 같이, 가변인자를 사용(내부적으로 배열을 사용)하지 않고, 각각 emptyList와 List12<E>라는 구현체로 List를 생성하고 있었습니다.
이 부분을 봤을 땐, 모던 자바인 액션에서 이야기하는 “가변인자를 사용할때의 비용을 절감”한다는 설명에 수긍을 했지만 size가 3이상일 때 부터는 그러지 않았습니다.</p>
<h2 id="리스트의-사이즈가-3이상-인-경우">리스트의 사이즈가 3이상 인 경우</h2>
<pre><code class="language-java">static &lt;E&gt; List&lt;E&gt; of(E e1, E e2, E e3) {
    return new ImmutableCollections.ListN&lt;&gt;(e1, e2, e3);
}

static &lt;E&gt; List&lt;E&gt; of(E e1, E e2, E e3, E e4) {
    return new ImmutableCollections.ListN&lt;&gt;(e1, e2, e3, e4);
}

...

static &lt;E&gt; List&lt;E&gt; of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10) {
    return new ImmutableCollections.ListN&lt;&gt;(e1, e2, e3, e4, e5,
                                            e6, e7, e8, e9, e10);
}

static final class ListN&lt;E&gt; extends AbstractImmutableList&lt;E&gt;
            implements Serializable {

        // EMPTY_LIST may be initialized from the CDS archive.
        static @Stable List&lt;?&gt; EMPTY_LIST;

        static {
            VM.initializeFromArchive(ListN.class);
            if (EMPTY_LIST == null) {
                EMPTY_LIST = new ListN&lt;&gt;();
            }
        }

        @Stable
        private final E[] elements;

        @SafeVarargs
        ListN(E... input) {  // &lt;------- 요 부분, 어차피 생성자에서 가변인자로 받음.
            // copy and check manually to avoid TOCTOU
            @SuppressWarnings(&quot;unchecked&quot;)
            E[] tmp = (E[])new Object[input.length]; // implicit nullcheck of input
            for (int i = 0; i &lt; input.length; i++) {
                tmp[i] = Objects.requireNonNull(input[i]);
            }
            elements = tmp;
        }
        ...
}</code></pre>
<p>사이즈가 3 이상일 때부터는 이상합니다. 구현체 ImmutableCollections.ListN&lt;&gt;을 반환하는데, ListN의 생성자에서는 <strong>“가변인자로 값을 받습니다”</strong>. 결국, List.of의 오버로딩 버전도 내부적으로 가변인수를 사용하는 것입니다.</p>
<p>그렇다면 모던자바 인 액션에서 나온 <strong>“고정된 숫자의 요소(최대 열개까지)를 API로 정의하므로 이런 비용을 제거할 수 있다.”</strong> 라는 말이 틀린 것입니다.</p>
<p>그래서 실제 가변 인수로 받는 List.of(E… elemtns)도 궁금하여 확인해보았습니다.</p>
<pre><code class="language-java">static &lt;E&gt; List&lt;E&gt; of(E... elements) {
    switch (elements.length) { // implicit null check of elements
        case 0:
            return ImmutableCollections.emptyList();
        case 1:
            return new ImmutableCollections.List12&lt;&gt;(elements[0]);
        case 2:
            return new ImmutableCollections.List12&lt;&gt;(elements[0], elements[1]);
        default:
            return new ImmutableCollections.ListN&lt;&gt;(elements);
    }
}</code></pre>
<p>신기하게도 List.of(E… elements)에서도, 똑같이 emptyList, List12, ListN을 반환하는 모습을 볼 수 있었습니다.</p>
<h3 id="가변인자와-오버로딩이-같이-있는-경우">가변인자와 오버로딩이 같이 있는 경우</h3>
<p>그리고, List.of(E… elements)에 매칭될 일도 없는 length가 0~10인 경우 또한 정의가 되어있습니다.</p>
<p>가변인자와 오버로딩 메서드가 같이 있는경우, 오버로딩 메서드를 호출하도록 결정됩니다.</p>
<pre><code class="language-java">@Test
void test1() {
    for (int i = 0; i &lt; 10; i++) {
        print(1, 2);
    }
}

public void print(int... ints) {
    System.out.println(&quot;얘는 가변인수 버전입니다.&quot;);
}

public void print(int int1, int int2) {
    System.out.println(&quot;얘는 오버로딩 버전입니다.&quot;);
}

//호출해보니 print(int int1, int2) 만 호출됨.</code></pre>
<p>그렇다면 List.of(E… elements)에서 elements.length가 0~2인경우도 필요 없고, </p>
<p>단순히 구현 내부가 <code>return new ImmutableCollections.ListN&lt;&gt;(elements)</code> 한 줄로 끝나야 하는게 아닐까요?</p>
<p>실제로 올 일이 없는 elements.length가 0,1,2인 경우에 대해 조건검사를 하고 있으니까요.</p>
<h1 id="4-총-의문점">4. 총 의문점</h1>
<h3 id="q-어차피-listof의-size가-310인-경우나-nn10개인-경우나-가변인수로-배열을-생성하는데-왜-오버로딩-버전이-따로-있나">Q. 어차피 List.of의 size가 3~10인 경우나, N(N&gt;10)개인 경우나 가변인수로 배열을 생성하는데, 왜 오버로딩 버전이 따로 있나?</h3>
<p>추측 중 하나는 JDK를 설계했을 시에는, 배열을 생성하지 않도록(성능 차이가 나지 않도록) 오버로딩 버전과 가변인수 버전을 나눠놓았는데, JDK를 구현하면서, correto JDK가 위처럼 설계되지 않았을까... 하는 추측이 있습니다.</p>
<p>너무 궁금하고, 답을 찾지 못해서 stackoverflow에 글을 남겨보았습니다.
<a href="https://stackoverflow.com/questions/75717670/what-diffrence-is-list-ofe-e1-e-e2-e-e3-vs-list-ofe-elements">링크</a></p>
<p>근데 글을 봐도 명확한 답을 모르겠네요. 시간이 나면 바이트 코드를 까볼 듯합니다.</p>
<h3 id="q-listofe-elements에서-case-02인-경우가-오지-않는데-왜-case-02인-경우를-체크를-하나">Q. List.of(E… elements)에서, case 0<del>2인 경우가 오지 않는데, 왜 case 0</del>2인 경우를 체크를 하나?</h3>
<p>어차피 List.of(), List.of(E e1), List.of(E e1, E e2)에서 elements.length가 0<del>2인 경우가 매핑되어 List.of(E... elements)에서 length가 0</del>2인 경우는 오지 않습니다.
하지만 List.of(E... elements) 메서드에서 요소의 개수에 제한이 없기 때문에, 길이가 0, 1, 2인 경우를 처리해주는 것이 좋은 프로그래밍 습관입니다. 또한, JDK 개발자들도 예외 상황에 대비하여 길이가 0, 1, 2인 경우를 처리하는 코드를 추가하였을 수 있습니다. 명시적으로 표현하기 위해 위처럼 썼다는게 제 생각입니다. </p>
<p>+) List.of()의 인자로 E[] elements가 오는 경우도 있으니,
 이에 대한 케이스를 처리하기 위해 List.of(E... elements)에서도 
사이즈에 대한 체크를 하고 있는 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Collections.forEach vs Stream.forEach]]></title>
            <link>https://velog.io/@hong-sile/Collections.forEach-vs-Stream.forEach</link>
            <guid>https://velog.io/@hong-sile/Collections.forEach-vs-Stream.forEach</guid>
            <pubDate>Sun, 19 Mar 2023 10:15:29 GMT</pubDate>
            <description><![CDATA[<h1 id="foreach">forEach</h1>
<p>forEach는 for문을 <strong><em>연속된 데이터를 가진 집합</em></strong> 에서 편하게 사용하기 위해 추가된 문법이다.</p>
<p>자바에서 연속된 데이터를 가진 집합엔 컬렉션과 stream이 있고, 둘 모두 forEach를 지원한다.</p>
<h2 id="collections">Collections</h2>
<p>컬렉션은 대표적인 데이터들의 집합이며, 자바에서 가장 많이 사용되는 API중 하나지 않을까 싶다.</p>
<p>컬렉션은 forEach가 도입되기 전 다음과 같이 반복을 사용하였다.</p>
<pre><code class="language-java">List&lt;String&gt; crewNames = List.of(
    &quot;홍실&quot;, &quot;에단&quot;, &quot;로지&quot;, &quot;져니&quot;, &quot;블랙캣&quot; ,&quot;준팍&quot;);
int size = crewNames.size();
for(int i = 0; i &lt; size; i++){
    System.out.println(crewNames.get(i));
}</code></pre>
<p>위는 전통적인 for-loop입니다. 직접적인 index를 이용해, Collection을 순회하고 있습니다. forEach를 이용해 같은 결과를 내는 코드를 아래와 같이 바꿀 수 있습니다.</p>
<pre><code class="language-java">List&lt;String&gt; crewNames = List.of(
                &quot;홍실&quot;, &quot;에단&quot;, &quot;로지&quot;, &quot;져니&quot;, &quot;블랙캣&quot;, &quot;준팍&quot;);
for (String crewName : crewNames) {
    System.out.println(crewName);
}</code></pre>
<p>직접적으로 index를 이용하여 순회하지 않고, Collections의 Iterator를 이용하여, 순회합니다.
내부 Iterator를 쓰기에, 다음과 같은 장점을 가집니다. <br></p>
<ol>
<li>IndextOutOfBoundException과 같은 에러가 발생할 여지가 없습니다.</li>
<li>Iterator로 순회하기에 get과 같은 메서드로 값을 참조하는 기존 for-loop보다 빠릅니다.</li>
</ol>
<p>장점만 있는 것은 아닙니다. 내부적으로, Iterator를 통해서 순회하기 때문에, 반복을 직접적으로 핸들링 할 수 없습니다.
각 요소를 한 번만 순회할 수 있습니다.</p>
<p>추가적으로 Collections의 forEach는 람다 표현식을 사용하여 아래와 같이 사용할 수 도 있습니다.</p>
<pre><code class="language-java">List&lt;String&gt; crewNames = List.of(
                &quot;홍실&quot;, &quot;에단&quot;, &quot;로지&quot;, &quot;져니&quot;, &quot;블랙캣&quot;, &quot;준팍&quot;);
crewNames.forEach(System.out::println);</code></pre>
<h2 id="stream">Stream</h2>
<p>stream 또한 연속된 데이터들의 집합을 처리하는 연산입니다.
스트림에도 forEach가 있고, 아래와 같이 사용할 수 있습니다.</p>
<pre><code class="language-java">Stream&lt;String&gt; stream = Stream.of(&quot;홍실&quot;, &quot;에단&quot;, &quot;로지&quot;, &quot;져니&quot;, &quot;블랙캣&quot;, &quot;준팍&quot;);
stream.forEach(System.out::println);</code></pre>
<p>사용하는 방법과 결과 모두 Collections.forEach와 동일합니다.</p>
<h2 id="collections-vs-stream">Collections vs Stream</h2>
<p>둘 모두 forEach라는 메서드를 가지고 있고, Consumer를 파라미터로 받습니다.</p>
<p>그럼 저희는 forEach로, 연속적인 값들을 이용하고 싶을 때, 어느것을 사용하면 될까요?
동일한 파라미터를 받고, 동일한 결과를 반환하니 아무거나 사용하면 될까요?</p>
<p>정답부터 말씀드리자면, Stream.forEach보다 Collections.forEach를 사용하는게 좋습니다.</p>
<p>이유는 크게 두 가지 입니다,</p>
<h3 id="순서">순서</h3>
<p>stream의 forEach는 순서를 보장하지 않습니다. 
일반적인 stream에서는 순서가 보장되지만,
만약 parallelStream으로 생성된 stream에서는 순서가 보장되지 않습니다.</p>
<pre><code class="language-java">final List&lt;Integer&gt; ints = List.of(1, 2, 3, 4, 5, 6);
ints.parallelStream()
        .forEach(System.out::println);
//출력 순서
4
5
6
1
2
3</code></pre>
<p>forEachOrdered라는 메서드로, 순서를 보장할 수 있긴합니다. 
하지만, 그런식으로 순서를 보장할거면, Collections.forEach가 더 낫습니다.</p>
<pre><code class="language-java">default void forEach(Consumer&lt;? super T&gt; action) {
    Objects.requireNonNull(action);
    for (T t : this) 
        action.accept(t);
    }
}
//실제 Collections.forEach의 내부 코드</code></pre>
<p>내부 iterateor로 순회하므로 순서가 항상 보장됩니다.</p>
<h2 id="exception">Exception</h2>
<h3 id="collections-1">Collections</h3>
<p>Collections.forEach에서 내부 요소가 순회되는 중 기존 Collection에 값이 추가 또는 삭제 되면, 바로 ConcurrentModificationException을 던집니다.</p>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/32e7f9ac-267e-4ca3-9859-39187997768c/image.png" alt=""></p>
<h3 id="stream-1">Stream</h3>
<p>하지만 stream은 모든 요소를 전부 순회한 후에 Exception을 던집니다.
<img src="https://i.imgur.com/UDM17Bl.png" alt="Imgur"></p>
<h3 id="연산-후">연산 후</h3>
<p>심지어, Exception을 나중에 던지기 때문에, 문제가 있는 연산을 계속해서 실행합니다.</p>
<h3 id="collection">collection</h3>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/ca9d85af-5fbe-42ce-8efe-9e847796880d/image.png" alt=""></p>
<h3 id="stream-2">stream</h3>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/ac7a3b2b-9cf6-4240-9651-6b6d3bc83bdd/image.png" alt=""></p>
<p>try-catch에서 출력된 상태를 보면, stream은 나중에 Exception을 던지기 때문에 모든 원소들이 뒤에 추가된 것을 알 수가 있습니다.</p>
<h2 id="결론">결론</h2>
<p>거의 모든 상황에서, Stream의 forEach보다 Collections.forEach가 낫습니다. 
collections.forEach가 사용가능한 상황이면, stream.forEach를 사용하지 맙시다.</p>
<p>참고 자료 : <a href="https://www.baeldung.com/java-collection-stream-foreach">https://www.baeldung.com/java-collection-stream-foreach</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우아한테크코스 사다리 타기 회고]]></title>
            <link>https://velog.io/@hong-sile/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EC%82%AC%EB%8B%A4%EB%A6%AC-%ED%83%80%EA%B8%B0-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hong-sile/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EC%82%AC%EB%8B%A4%EB%A6%AC-%ED%83%80%EA%B8%B0-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 12 Mar 2023 12:01:46 GMT</pubDate>
            <description><![CDATA[<p>미션이 끝날 때마다 쓰는 회고다. 이번엔 레벨1의 두 번째 미션인 사다리 타기 회고이다.</p>
<h1 id="미션-진행-내역">미션 진행 내역</h1>
<blockquote>
<p>사다리 타기 Repository
<a href="https://github.com/hong-sile/java-ladder">https://github.com/hong-sile/java-ladder</a></p>
</blockquote>
<blockquote>
<p>1단계 PR
<a href="https://github.com/woowacourse/java-ladder/pull/71">https://github.com/woowacourse/java-ladder/pull/71</a></p>
</blockquote>
<blockquote>
<p>2단계 PR
<a href="https://github.com/woowacourse/java-ladder/pull/169">https://github.com/woowacourse/java-ladder/pull/169</a></p>
</blockquote>
<p>우테코의 미션에는 각각 부제가 붙어있습니다.</p>
<p>자동차 경주 - 단위 테스트, 사다리 타기 - TDD와 같이요.</p>
<p>이번 미션 같은 경우는 TDD가 메인이었기에 많은 시도를 해보았습니다. TDD에 대한 생각은 <a href="https://velog.io/@hong-sile/TDD%EC%97%90-%EB%8C%80%ED%95%9C-%EB%82%98%EC%9D%98-%EC%83%9D%EA%B0%81">여기</a>에 있습니다. </p>
<p>그래서 이번 글에서는 사다리 타기를 어떤 단계를 거쳐서 구현하였는지를 적어보려고 합니다. 먼저 요구사항은 아래와 같습니다</p>
<h1 id="요구사항">요구사항</h1>
<h2 id="기능-요구사항">기능 요구사항</h2>
<ul>
<li>사다리 게임에 참여하는 사람에 이름을 최대5글자까지 부여할 수 있다. 사다리를 출력할 때 사람 이름도 같이 출력한다.</li>
<li>사람 이름은 쉼표(,)를 기준으로 구분한다.</li>
<li>사람 이름을 5자 기준으로 출력하기 때문에 사다리 폭도 넓어져야 한다.</li>
<li>사다리는 랜덤으로 생성된다.</li>
<li>사다리 타기가 정상적으로 동작하려면 라인이 겹치지 않도록 해야 한다.<ul>
<li><strong><code>|-----|-----|</code></strong> 모양과 같이 가로 라인이 겹치는 경우 어느 방향으로 이동할지 결정할 수 없다.</li>
</ul>
</li>
<li>사다리 실행 결과를 출력해야 한다.</li>
<li>개인별 이름을 입력하면 개인별 결과를 출력하고, &quot;all&quot;을 입력하면 전체 참여자의 실행 결과를 출력한다.</li>
</ul>
<h2 id="추가된-프로그래밍-요구사항">추가된 프로그래밍 요구사항</h2>
<ul>
<li>모든 기능을 TDD로 구현해 단위 테스트가 존재해야 한다. 단, UI(System.out, System.in) 로직은 제외<ul>
<li>핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.</li>
<li>UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다.</li>
</ul>
</li>
<li>함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현한다.<ul>
<li>함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.</li>
</ul>
</li>
<li>배열 대신 컬렉션을 사용한다.</li>
<li>Java Enum을 적용한다.</li>
<li>모든 원시 값과 문자열을 포장한다</li>
<li>줄여 쓰지 않는다(축약 금지).</li>
<li>일급 컬렉션을 쓴다.</li>
</ul>
<h2 id="실제-클래스-의존관계">실제 클래스 의존관계</h2>
<p><img src="https://i.imgur.com/4uw9Wpi.png" alt="Untitled"></p>
<p>최대한 객체의 메시지를 넘기는 방식으로 프로그래밍 하다보니, Service Layer가 필요 없어져서 위와 같이 클래스 의존관계가 형성되었습니다.</p>
<h1 id="구현">구현</h1>
<h2 id="사다리">사다리</h2>
<p>처음에 페어와 어떻게 사다리를 표현해야 될까 고민을 많이 했다. 맨 처음 나온 아이디어는 다음과 같았습니다.  </p>
<p><img src="https://i.imgur.com/YlUv4S2.png" alt="Untitled"></p>
<p>사다리는 줄(Line)의 집합이고, 줄(Line)은 하나의 점(빨간 동그라미)의 집합이라고 생각했습니다. 각 점은 왼쪽 또는 오른쪽을 연결한다는 의미를 부여하면 될것이라 생각하였습니다.</p>
<p>그리고, 각각의 점에서 양쪽을 연결할 수 없으니 한 점에서 왼쪽 또는 오른쪽을 갖는게 좋은 상태라 생각했습니다.</p>
<p>하지만 위와 같은 아이디어는 얼마 안가 폐기되었습니다. n-1번째의 점에서 오른쪽을 연결했다는 상태를 저장하고, n번째 점에서 왼쪽을 연결했다는 상태를 지닙니다. 똑같은 정보를 중복 저장하고 있는 것입니다. 그래서 아래와 같은 아이디어를 떠올렸습니다.</p>
<p><img src="https://i.imgur.com/9syQLlX.png" alt="Untitled"></p>
<p>연결 여부(Link)를 List형태로 저장한 것입니다. 이러면 중복적인 정보 저장 없이 사다리를 표현할 수 있다고 생각하여 위와 같이 진행하였습니다.</p>
<p>이번 미션 또한 UI를 제외한 모든 단위테스트를 작성해야 했습니다.</p>
<p>다른 로직을 테스트하는 건 쉬웠으나, 랜덤으로 생성되는 사다리 생성 로직을 테스트하는 것이 상당히 어려웠습니다. </p>
<h3 id="랜덤으로-생성되는-사다리-그러나-조건을-곁들인">랜덤으로 생성되는 사다리, 그러나 조건을 곁들인…</h3>
<hr>
<p>사다리는 랜덤으로 생성됩니다. 하지만 특수한 조건이 있죠</p>
<blockquote>
<p><strong><code>|-----|-----|</code></strong> 모양이 나오면 안 된다.</p>
</blockquote>
<p>그래서 저희는 다음과 같이 로직을 구성했습니다.</p>
<pre><code class="language-java">public Line generate(final PersonCount personCount) {
    final Deque&lt;Link&gt; line = new LinkedList&lt;&gt;();
    if (personCount.getValue() != 1) {
        line.add(linkGenerator.generate());
    }
    for (int index = 1; index &lt; personCount.getValue() - 1; index++) {
        addValidatedLink(line);
    }
    return new Line(List.copyOf(line));
}

private void addValidatedLink(final Deque&lt;Link&gt; line) {
    if (line.getLast() == Link.LINKED) {
        line.add(Link.UNLINKED);
        return;
    }
    line.add(linkGenerator.generate());
}</code></pre>
<p>Line을 생성하는 로직이 복잡하므로 LineGenerator라는 클래스로 분리를 하였고, 아래와 같은 역할을 수행합니다.</p>
<p>line에 랜덤으로 값을 추가하기 전에, 이전 원소가 연결되어 있는 상태(Linked)이면 바로 다음 상태는 연결되지 않는 상태(Unlinked)를 추가하였고,</p>
<p>이전 상태가 Linked가 아니라면 랜덤으로 값을 추가하였죠.</p>
<p>line이 deque형태인 이유는 마지막 원소를 확인하는 메서드인 getLast가 있었기에, 생성하는 Line은 deque 형태로 만들었습니다.</p>
<h3 id="테스트">테스트</h3>
<p>다른 로직은 크게 어렵지 않았습니다만, <code>조건이 있는 랜덤 사다리 생성</code> 이라는 기능을 테스트하기가 젤 난감했습니다.</p>
<p>랜덤이라는 조건만 있었으면 테스트하기 쉬웠을 겁니다. 전략패턴을 이용해서, 랜덤성을 제어해서 테스트할 수가 있고, 이전 미션에서도 그런 식으로 테스트를 했으니까요.</p>
<p>하지만 이번엔 달랐습니다. <code>조건이 있는 랜덤</code> 이었습니다. </p>
<p>심지어 이번엔 TDD로 개발을 했습니다. 그래서, 기능을 구현하기 전 테스트를 작성해야 했는데, 기능을 만들기 전 어떤식으로 테스트할 지 떠올리지 못하여 아래와 같은 방식으로 테스트를 하였습니다.</p>
<ol>
<li>조건에 맞는 사다리를 랜덤으로 생성하는 기능을 테스트하면 어떻게 해야할까?</li>
<li>랜덤은 제어할 수 있어도, 조건에 맞는 랜덤은 제어하기 힘들다.</li>
<li>그렇다면, 아예 제어하기 힘든 부분에선 손을 놓자.</li>
<li>우리가 원하는건 랜덤으로 생성된 사다리가 <code>조건에 맞는지</code> 이고, 이것이 기능의 전부이다.</li>
<li>그렇다면, 생성된 사다리를 조건에 맞게 테스트하는 validation을 test class에서 짜고, 이를 repeatedTest로 100번 정도 돌리면 괜찮지 않을까…?</li>
</ol>
<p><a href="https://imgur.com/7sQfEez"><img src="https://i.imgur.com/7sQfEez.png" width = "45%"/></a></p>
<pre><code class="language-java">@RepeatedTest(100)
@DisplayName(&quot;랜덤으로 생성된 Line이 유효한지 테스트&quot;)
void randomLineValidateTest() {
    int personCount = 4;
    final Line generatedLine = lineGenerator.generate(personCount);
    Assertions.assertDoesNotThrow(() -&gt; validateLine(generatedLine));
}

private void validateLine(final Line line) {
    Link pastLink = Link.UNLINKED;
    for (final Link link : line.getLinks()) {
        pastLink = comparePastPointAndPresentPoint(pastLink, link);
    }
}

private Link comparePastPointAndPresentPoint(Link pastLink, final Link link) {
    if (link.isLink() &amp;&amp; pastLink.isLink()) {
        throw new IllegalArgumentException();
    }
    pastLink = link;
    return pastLink;
}</code></pre>
<p>음 다시봐도 정말 바보 같은 생각입니다. 하지만 그 당시에 떠올린 다른 마땅한 아이디어는 없었기에 처음에 위처럼 진행하였습니다. </p>
<p>뭐 어찌어찌 잘 동작하기는 했습니다. 실제로 사다리를 생성하는 로직을 변경하고 테스트를 돌릴 때, 잘못 생성된 케이스를 잡아주기도 하였습니다. 제대로 된 아이디어를 떠올리기 까지 임시방편정도의 역할은 충분히 해주었습니다.</p>
<p>페어 프로그래밍 때는 위와 같이 마무리했지만, 너무 너무 찝찝한 코드였습니다. 실제로, 위와 같은 방식으로 작성한 코드의 신뢰성이 떨어져 저희는 코드를 변경하고 나서도 여러번 테스트를 실행하였습니다.</p>
<p>그렇게 찝찝한 상태로 페어 프로그래밍이 끝나고, 리뷰를 받고나서 생각이 떠올랐습니다.</p>
<p>테스트에서 <code>랜덤으로 생성된 사다리가 조건에 맞는지</code> 를 테스트하는 것이 아니라, 
<code>라인을 생성하는 로직이 내가 원하는 방식대로 동작하는지</code>를 테스트하는 것이었습니다.</p>
<pre><code class="language-java">@Test
@DisplayName(&quot;generate 메서드가 조건에 맞는 라인을 반환하는지 테스트&quot;)
void generateTest() {
    //given
    final List&lt;Link&gt; input = List.of(Link.LINKED, Link.UNLINKED, Link.LINKED);
    final LineGenerator lineGenerator = new LineGenerator(new TestLinkGenerator(input));
    //when
    final Line line = lineGenerator.generate(new PersonCount(5));
    //then
    Assertions.assertThat(line.getLinks())
            .containsExactly(Link.LINKED, Link.UNLINKED, Link.UNLINKED, Link.LINKED);
}
//랜덤으로 값을 추가할 때 연결, 비연결, 연결이 반환되면 실 로직에서는
//연결, 비연결, 비연결, 연결이 반환되어야 한다.</code></pre>
<p>테스트하는 대상을 추상화된 목적(랜덤으로 생성된 사다리가 조건에 맞는지)이 아닌, 딱 라인을 생성하는 로직이 내가 원하는 대로 동작하는지를  테스트 하는 것이었습니다.</p>
<p>여기서 TDD의 단점을 느낄 수 있었습니다. expected 값이 내가 제어할 수 없는 값일 때, 우리는 기능을 작성하기전 Test 코드를 작성하는 것이 굉장히 어렵습니다.</p>
<p>나중에 더 나은 방법으로 테스트할 수 있는 것은 제가 조건에 맞는 라인을 생성하는 로직(LineGenerator.generate)을 구현했고 ,해당 로직이 어떻게 동작했는지 알 수 있었기 때문입니다.</p>
<p>TDD 단계에서는 절대로 나중과 같은 로직을 작성할 수 없죠. 작성했다면, 이미 구현 방안이 머릿속에 다 있었던 것과 마찬가지일 것입니다.</p>
<p>하지만 TDD에도 장점은 있습니다. <code>생성하는 로직이 원하는 방식으로 동작하는지</code> 로 테스트했을 때의 단점은 다음과 같습니다. </p>
<p>만약 구현되는 로직(LineGenerator.generate)의 내부가 달라진다면? 로직을 테스트하는 것은 정상적으로 동작하지 않을 것입니다.</p>
<p>하지만, 해당 로직의 목적인 <code>랜덤으로 생성된 사다리가 **조건에 맞는지</code>** 를 테스트한 로직은 내부 구현이 바뀌어도 정확도는 떨어질 지언정 이전과 비슷하게 정상적으로 동작했겠죠.</p>
<p>확실히 TDD는 기능을 추상화하고, 이를 검증하는 로직을 먼저 구현한다는 점에서 내부구현이 변경되는 것에서 자유롭지만, </p>
<p>제어할수 없는 값이 오는 경우는 기능을 추상화 하기 어렵기 때문에, 이 떄는 사용하기 힘든 것 같습니다.</p>
<p>각각의 상황에 맞춰서 이용해야 할 것 같습니다.</p>
<h2 id="유저와-상품들">유저와 상품들</h2>
<p>유저와 상품들은 크게 어렵지 않았습니다.</p>
<p>유저와 상품은 각각 이름을 가지고 있고, Users와 Prizes라는 일급 컬렉션에서 List형태로 관리하였습니다.</p>
<h2 id="사다리-타기-게임-실행">사다리 타기 게임 실행</h2>
<p>이 부분이 이제, 2단계의 메인 입니다. 생성된 사다리를 가지고 사다리 게임을 실행하게 하는 것이죠.</p>
<p>처음 떠올렸던 아이디어는 다음과 같습니다.</p>
<ol>
<li>User와 Prize는 Position이라는 상태를 갖는다.</li>
<li>Ladder에서 한 Line씩 순회한다.<ol>
<li>Line에서 한 Link씩 순회한다.<ol>
<li>Link가 연결되어있다면 인접한 Position에 있는 두 User의 Position을 swap한다.</li>
<li>연결되어 있지 않다면 swap하지 않는다.</li>
</ol>
</li>
</ol>
</li>
<li>Ladder에서 순회가 끝나면, User의 Position과 일치하는 Position을 가진 Prize를 반환한다.</li>
</ol>
<p>사다리 게임을 하나의 라인을 여러번 반복하는 것으로 쪼개고, 하나의 라인을 각각의 Linked에 따른 연산으로 분리하였습니다. 이렇게 하면 한 번만 순회하면 모든 결과를 얻을 수 있었죠.</p>
<p>그렇게 위와 같은 방식으로 구현을 진행하다가 하나를 깨달았습니다.</p>
<p>아 어차피 Users에서 List<User>로 들고 있고, List는 순서가 포함되어있는 Collection이니, Position의 역할을 List가 대체해줄 수 있겠구나.</p>
<p>심지어 Collections.swap이라는 위치를 바꿔주는 아주 좋은 API도 있었습니다. Prize도 List형태로 저장하고 있었으니, 순서가 내포되어있는 건 동일해서 Prize에서도 Position을 빼주었습니다.</p>
<p>그래서 초기에는 User와 Prize가 Position을 들고 있었지만, 순서라는 책임을 List에게 위임하여, 구현하였습니다.</p>
<p>그러다보니 1단계에서 2단계로 넘어갈 때 클래스가 단순하게 상품을 나타내는 Prize와 Prizes만 추가하고, 구현하였습니다.</p>
<h1 id="미션하다-생긴-궁금증-및-나의-생각">미션하다 생긴 궁금증 및 나의 생각</h1>
<h2 id="tdd로-미션을-진행할때의-커밋-단위">TDD로 미션을 진행할때의 커밋 단위</h2>
<p>우테코에서는 git commit convention을 지키는 것이 기본 요구사항으로 들어가 있습니다. </p>
<p>git commit convention에는 기능을 추가해을 때 붙이는 feat와 test를 추가했을 때 붙이는 test가 있습니다.</p>
<p>TDD로 진행할 때는, test를 작성하고나서, 기능을 작성하고, 이를 리팩터링 하기에, 각각의 단계마다 한 번씩 커밋을 해야 하나? 라는 의문이 들어 페어와 논의를 해보았습니다.</p>
<p>페어와 제가 내린 결론은 TDD 사이클이 끝나고 기능 구현이 완료 된 상태를 feat로 커밋하는 것이었습니다.  </p>
<p>테스트로 만든 기능이 완성되는 것 까지가 기능 구현 단계라고 보았기에 위와 같이 결정하였습니다.</p>
<p>또, 어떤 커밋 단계로 돌아가도, 기능이 정상적으로 작동해야 한다고 생각하여, TDD 사이클이 끝난 후 feat로 커밋하였습니다.</p>
<h2 id="오버엔지니어링">오버엔지니어링</h2>
<blockquote>
<p>현재 필요한 것 보다 더 과하게 제품을 디자인 하는 것이다. 즉, 제품을 더 견고하게 만들거나, 더 복잡하게 만드는 것이다. 핵심개념만을 담아 최대한 단순하게 만들자는 최소주의와는 대비되는 개념이다.
보통 오버엔지니어링 되어 있으면 이후 제품을 운영 할 때 어려움을 줄 때도 많다. 단순한 구조에서는 간단히 할 수 있는 일을 더 복잡하게 해야 하는 등이 일이 발생하기 때문이다.</p>
</blockquote>
<p>위 궁금증은 <a href="https://velog.io/@hong-sile/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EC%9E%90%EB%8F%99%EC%B0%A8-%EA%B2%8C%EC%9E%84-%ED%9A%8C%EA%B3%A0">자동차 미션</a>에서 부터 있었던 궁금증이었습니다. </p>
<p>저는 확장성이 좋은 코드가 무조건 좋은 코드라 생각했습니다. 하지만, 오버 엔지니어링에 대해 알고 나서 그렇지 않다는 것을 깨달았습니다. 어떠한 변화에도 대응할 수 있으니까요.</p>
<p>오버엔지니어링이란 개념을 알게되고 저는 다음과 같은 새로운 궁금증이 생겼습니다. </p>
<p><strong>어디까지가 오버 엔지니어링이고, 어디까지가 확장성을 고려한 개발일까?</strong></p>
<p>도저희 답이 안나와서 리뷰어께 여쭤봤습니다.</p>
<p><img src="https://i.imgur.com/SZmcrtP.png" alt="Untitled"></p>
<p>위 답을 듣고 YAGNI 원칙에 대해 알아봤습니다.</p>
<blockquote>
<p>실제로 필요할 때 무조건 구현하되, 그저 필요할 것이라고 예상할 때에는 절대 구현하지 말라.
-론 제이프스</p>
</blockquote>
<p>법칙 자체는 단순합니다. 필요할 것이라 예상만 되는 상황에선 만들지 말아라.</p>
<p>크루들은 미션을 진행할 때 다양한 것을 고려합니다. 만약에 룰이 바뀐다면? 만약에 유저가 엄청나게 많아진다면? 등 다양한 것을 고려하여, 캐싱과 같은 여러 기술들을 적용하죠. 적용한 기술 중엔 현 상황에선 필요가 없는 기술들도 몇몇 있습니다.  </p>
<p>위와 같은 개념을 공부하다보니 최근에 강의에서 네오가 말해준 “자바는 블루칼라 언어다.” 라는 말이 더 깊게 다가오는 것 같아요. 요구사항을 구현할 때 시간과 비용을 최대한 절약한다는 의미니까요.</p>
<p>아직도, 어디까지가 오버엔지니어링이고 어디까지가 적정엔지니어링인지 명확하진 않습니다. 하지만 YAGNI 원칙을 한번 나 자신에게 물어보면서 오버엔지니어링이 많이 줄었다고 생각해요. 앞으로도 많이 배워보고, 기준을 세워 나갈 것 같습니다.</p>
<h2 id="view의-static-메서드">view의 static 메서드</h2>
<p><img src="https://i.imgur.com/uWcoSEV.png" alt="Imgur"></p>
<p>위 질문은 리뷰어께서 먼저 던진 질문으로 시작하였습니다.</p>
<p>그에 대한 제가 생각한 답은 다음과 같습니다.</p>
<h3 id="static-키워드를-붙인-이유">static 키워드를 붙인 이유</h3>
<ol>
<li>InputView, OutputView가 상태를 지니고 있지 않기에, static으로 선언해도 문제가 없습니다.</li>
<li>InputView, OutputView의 메서드들이 멱등성을 보장합니다.
(몇 번을 반복하든 똑같은 값을 입력하면, 동일한 결과를 반환합니다.)</li>
<li>추후 InputView와 OutputView에 의존하는 객체가 많아진다면(controller가 여러 개가 된다면), 각 객체에서 불필요하게 view를 생성할 필요 없이, 바로 static으로 접근이 가능하기 때문에 static이 더 효율적입니다.
(이 부분은 오버엔지니어링일수도 있을 것 같습니다. 현재 접근하는 곳이 하나기 때문에...)</li>
<li>현재 로직에서 유효하지 않은 입력을 하였을 경우, 다시 입력을 받도록 구현하였습니다. 이렇 듯 InputView의 메서드가 반복적으로 호출이 되는데, 메모리 낭비를 줄이기 위해 static 키워드를 붙였습니다.(non-static 메서드는 호출될 때마다 메모리를 할당 받고, 해제하기 때문에)</li>
<li>편합니다…</li>
</ol>
<p>그리고, 제가 생각해봤을 때, static 키워드를 붙였을 때 단점은 다음과 같습니다.</p>
<h3 id="메서드에-static-키워드를-붙이면서-발생할-수-있는-단점">메서드에 static 키워드를 붙이면서 발생할 수 있는 단점</h3>
<ol>
<li>static 메서드는 컴파일 할 때 메모리에 올라가고, 프로그램 종료시에 해제되기 때문에, static 메서드를 호출하지 않는다면, 불필요한 메모리를 낭비하는 것입니다.
→ 하지만 view의 메서드는 현 프로그램에선 무조건 호출됩니다.</li>
<li>추후 inputView에 대한 테스트(System.in)를 진행할 때, mocking을 해야만 테스트를 진행할 수 있다.Scanner는 생성할 때 System.in을 인자로 받아 생성되고, static member로 생성하면 생성 시점은 컴파일 단계이기에, 입력값에 대한 설정(System.setin)을 할 수 없습니다.
이 부분은 이전 자동차 경주 미션에서 겪고, 결론을 내린 문제입니다.</li>
</ol>
<p>저는 InputView와 OutputView가 Util처럼 바라봤습니다.</p>
<p>utils와 view가 다른 점이라 한다면, 코드 외부(입출력)에 영향을 주거나 받냐의 차이 인 것 같은데,이로 인해서 발생할 수 있는 문제가 없다고 생각하여, static을 붙였다고 답을 드렸습니다.</p>
<p>그에 대한 리뷰어의 답변:</p>
<p><img src="https://i.imgur.com/QGIp92M.png" alt="Imgur"></p>
<p>조금 더 강력하게 Model에서 View를 접근하는 경우를 막을 수 있을 것입니다. InputView하고 점(.)을 눌렀는데 바로 메서드가 접근이 가능하면, 그런 실수도 발생할수도 있을 것 같다는 생각이 드네요.</p>
<p>static을 붙였을 때의 이점을 간단히 정리하면 메모리와 편안함인데, 이를 제거하면, 객체를 생성하는 과정이 생기긴 하지만, 위와 같은 실수가 발생할 여지가 없어지긴 합니다.</p>
<p>지금은 개인으로 코딩하기에 static을 붙여 사용하고 있지만, 확실히 팀으로 가면 제거할 것 같습니다.</p>
<h1 id="총정리">총정리</h1>
<p>이번 미션은 나름 무난하게 흘러갔습니다. 이전 자동차 경주 미션에서 겪고 떠올린 사항들을 바로 적용했고, 구현도 괜찮았으니까요.</p>
<p>domain의 값들을 어떻게 view에 넘겨줄지에 대한 고민이 있지만, 여러방법을 써보고 더 나은 방법을 적용해볼까 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우아한테크코스 자동차 게임 회고]]></title>
            <link>https://velog.io/@hong-sile/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EC%9E%90%EB%8F%99%EC%B0%A8-%EA%B2%8C%EC%9E%84-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hong-sile/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%EC%9E%90%EB%8F%99%EC%B0%A8-%EA%B2%8C%EC%9E%84-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 24 Feb 2023 10:38:24 GMT</pubDate>
            <description><![CDATA[<p>우테코에서 첫 미션이 끝나고 일주일 정도 지났다. 시간이 많이 지나기 전에, 회고를 하려고 한다.</p>
<h2 id="미션-진행-내역">미션 진행 내역</h2>
<blockquote>
<p>자동차 경주 Repository</p>
</blockquote>
<p><a href="https://github.com/hong-sile/java-racingcar/tree/step2">GitHub - hong-sile/java-racingcar at step2</a></p>
<blockquote>
<p>1단계 PR</p>
</blockquote>
<p><a href="https://github.com/woowacourse/java-racingcar/pull/459">https://github.com/woowacourse/java-racingcar/pull/459</a></p>
<blockquote>
<p>2단계 PR</p>
</blockquote>
<p><a href="https://github.com/woowacourse/java-racingcar/pull/613">https://github.com/woowacourse/java-racingcar/pull/613</a></p>
<h1 id="자동차-경주-게임">자동차 경주 게임</h1>
<h2 id="기능-요구사항">기능 요구사항</h2>
<ul>
<li>주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.</li>
<li>각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.</li>
<li>자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.</li>
<li>사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.</li>
<li>전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 전진하고, 3 이하의 값이면 멈춘다.</li>
<li>자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다.</li>
</ul>
<h2 id="프로그래밍-요구사항">프로그래밍 요구사항</h2>
<ul>
<li><strong>모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외</strong></li>
<li><strong>자바 코드 컨벤션을 지키면서 프로그래밍한다.</strong><ul>
<li>참고문서: <a href="https://google.github.io/styleguide/javaguide.html">https://google.github.io/styleguide/javaguide.html</a> 또는 <a href="https://myeonguni.tistory.com/1596">https://myeonguni.tistory.com/1596</a></li>
</ul>
</li>
<li><strong><code>규칙 1: 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.</code></strong>를 지키며 구현한다.<ul>
<li>예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.</li>
<li>힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메소드)를 분리하면 된다.</li>
</ul>
</li>
<li><strong><code>규칙 2: else 예약어를 쓰지 않는다.</code></strong>를 지키며 구현한다.<ul>
<li>힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.</li>
<li>else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.</li>
</ul>
</li>
<li><strong>함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다.</strong><ul>
<li>함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다.</li>
</ul>
</li>
</ul>
<h1 id="페어-프로그래밍">페어 프로그래밍</h1>
<blockquote>
<p><em><a href="https://zetawiki.com/wiki/%ED%8E%98%EC%96%B4_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D">페어 프로그래밍</a>은 애자일 개발 방법론 중의 하나로 하나의 개발 가능한 PC 에서 두 명의 개발자가 함께 작업하는 것을 말합니다. 네비게이터(navigator)가 전략을 제시하고 드라이버(driver)가 실제 코드를 작성하며, 이 열할을 각자 번갈아가며 수행합니다. 짝 프로그래밍이라고도 합니다.</em></p>
</blockquote>
<p>우아한 테크코스만의 독특한 점이 있는데, 모든 미션이 페어 프로그래밍으로 진행된다는 것이다.</p>
<p>나 역시 이번 미션에서 페어와 함께 미션을 진행했다.</p>
<p>페어 프로그래밍을 하며 되게 좋은 경험을 할 수 있었지만, 첫 페어 프로그래밍이다 보니 미숙한 점이 많아 아쉬운 점도 있었다.</p>
<h2 id="좋았던-점">좋았던 점</h2>
<p>나의 첫 페어는 실무 경험이 많은 크루였다. 서비스를 만든 경험이 있고, 아직도 그 서비스를 유지하고 있다.(대단한 크루다)</p>
<p>덕분에 많은 것을 배울 수 있었다. 여러 인텔리제이 단축키부터, 다양한 패턴들까지 경험에서 나오는 기술들을 많이 배울 수 있었다. 그리고 페어가 경청을 엄청 잘해줘서, 나도 의견을 내는데 거리낌이 없었다.</p>
<p>페어 프로그래밍 특성 상 한 코드여도 드라이버와 네비게이터 둘 모두 이해하고 있어야 했는데, 소통이 잘 되어서 코드에 대한 이해가 잘됬었던 것 같다.</p>
<h2 id="아쉬웠던-점">아쉬웠던 점</h2>
<p>  이번이 첫 페어 프로그래밍이기에, 거기서 나오는 미숙함이 너무 많았다. 페어 프로그래밍 시작전에 어떤 식으로 진행하는지 검색이라도 할껄</p>
<p>  빠르게 코드를 작성해야한다는 강박에 코치님들의 설명만 듣고 바로 페어를 시작했다.  역할 전환(드라이버 ↔ 네비게이터) 시간도 안 정하고, 하다가 “음 이정도면 바꿔볼까요?” 하면서 역할 전환을 했었다.</p>
<p>  네비게이터와 드라이버의 역할을 빈번하게 바꿔 코드에 대한 이해가 강제(?)되도록 했어야 했는데, 그 점이 좀 아쉬웠다. 그 외에도 룰을 정했어야 했는데, 그러지 못한게 아쉬웠다.</p>
<p>  이런 경험을 겪고나서 다음 미션에서는 시작전에 페어와 꼭 룰을 정하고 시작했다. 페어 전환하는 주기부터 쉬는 시간까지 또 사람마다 다르기 떄문에 서로 알아가면서 룰을 정하는게 엄청 중요한 것 같다.</p>
<h1 id="미션-진행">미션 진행</h1>
<h2 id="step1-구현">step1 구현</h2>
<p>  첫 미션이었던만큼 실수가 되게 많았던 것 같다. 최대한 프리코스 때 학습한 지식을 다 때려넣으려고 했다.</p>
<p>최대한 확장 가능하게 설계하고 구현하는 것은 많은 지식을 필요로 하기에, 나는 무조건 확장성이 높고, 쓸 수 있는 기술을 다 적용하는 것이 좋은 방법이라고 생각하였다. </p>
<p>  다른 분들에게 리뷰를 받는 것이기에, 더더욱 그렇게 이번 미션을 구현하였고, 이러한 생각이 문제가 있다는 것을 나중에 알게 되었다.(이 부분은 뒤에 더 자세히 이야기 하겠다.)</p>
<p>  이번 미션자체는 어려운 로직이 들어가진 않아서, 기본적인 기능을 구현하는데는 큰 어려움이 없었다. </p>
<p>  모든 로직에 테스트를 작성하라는 요구사항을 구현하는 것이 어렵긴 했다. 특히 이번 미션엔 랜덤성과 같은 제어할 수 없는값이 나와 이 부분이 문제였는데, 페어와 나 모두 전략패턴이라는 적절한 방법을 알고 있었고, 해당 방법을 이용해 랜덤 값에 대한 테스트도 진행할 수 있었다.</p>
<h2 id="step1에-대한-피드백">step1에 대한 피드백</h2>
<p>  나는 도메인과 뷰의 의존성을 분리하기 위하여, 도메인에서 뷰로 값을 전달하는데 dto라는 방식을 사용했다. 
  뭐 dto를 사용할 수준의 프로젝트인가를 제쳐두고, 나는 dto를 사용하는 목적이 하나 더 있었다. <strong>바로 getter를 쓰지 않기 위해서이다.</strong></p>
<p>  객체지향 프로그래밍에 대해 당시 얕은 지식이 있었고, 이를 미션에 적용하려했다. 바로 디미터 원칙이다. 
  디미터 원칙은 <strong>객체의 값을 가져와 원하는 일을 처리하지 말고, 객체 자체에 메시지를 보내 원하는 일을 처리하라</strong>는 원칙이다.
  나는 여기서, 객체의 값을 가져오지 말라는 의미를 왜곡해서 받아들여, 극단적으로 <strong>getter를 쓰지 말아라</strong> 라고 이해했다. <del>(이래서 아무것도 모르는 사람보다 책 한권만 읽은 사람이 무섭다..)</del></p>
<p>  뭐 다른 도메인 로직이야 어찌어찌 getter를 쓰지 않고, 메시지를 보내는 식으로 처리할 수 있지만, 화면에 모델의 값을 출력해야 했기에 뷰에 데이터를 전달하기 위해 getter를 써야했다.</p>
<p>  하지만, 패턴에 잡아 먹혀버린 나는, 그냥 객체에 getter라는 메서드를 만드는 것을 싫어했고, 페어를 억지로 설득해, private한 필드를 넘기기 위해 그 클래스에서  dto를 생성해 넘겨버리는 방식을 채택하게된다.</p>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/0daf451e-6fb0-4c77-9847-07645110c3ea/image.png" alt=""></p>
<p>(view가 domain에 의존하지 않지만, domain이 view에 의존해버리는 아이러니한 상황….)</p>
<p>디미터 법칙이 전하고자 하는 바를 이해하지 못하고, 그저 법칙만 무지성으로 따르다가 일어난 일이다.</p>
<p>나중에 아예 dto를 삭제하고, getter를 이용해 도메인에서 뷰에 데이터를 전달하게 하였다.</p>
<h2 id="step2-구현">step2 구현</h2>
<hr>
<p>원래 step2에서 기존 step1에서 추가적인 요구사항이 들어간다. 하지만 이번에 들어온 요구사항은 뷰와 모델의 의존성 분리였고, step1에서 이미 뷰와 모델의 의존성을 분리하였기에, 나는 테스트 코드 작성에 집중하였다.</p>
<p>  테스트 코드에 대해서 프리코스 때 처음 배웠고, 미션 요구사항에도 테스트 코드를 작성하라는 내용이 있었기에, 가능한 모든 메서드에 대해서 테스트를 진행하려고 하였다.</p>
<h3 id="테스트-코드와-프로덕션-코드">테스트 코드와 프로덕션 코드</h3>
<hr>
<p>  그런 나에게 가장 큰 고민은 “테스트를 위해 어디까지 프로덕션 코드가 변경될 수 있는가?”였다.</p>
<p>  테스트 코드에 대해 공부하면 공부할 수록, 테스트 코드가 <strong>구현의 보조적인 부분이 아니라</strong>, <strong>구현의 필수적인 부분</strong>으로 생각하게 되었다. 
  그래서 나는 “테스트 코드를 위하여, 실제 설계 코드의 구조가 변경될 수 있다!”고 생각한다.
아마 다들 이 말에 동의할 것이다. </p>
<blockquote>
<p>책임이 잘 분리되고, 설계가 좋은 코드는 테스트하기 쉽다.</p>
</blockquote>
<p>실제로 각 메서드가 하나의 책임만을 갖고 있다면, 테스트를 짜기가 쉽다. </p>
<blockquote>
<p>테스트하기 어려운 코드는 책임이 잘 분리되지 않았고, 설계가 좋지 못하다.</p>
</blockquote>
<p>이 말은 이전에 나온 말에 대한 대우관계이다. 애당초 테스트 코드를 짜기 어렵다면, 프로덕션 코드의 설계 자체가 잘못되어 있을 확률이 높다.</p>
<p>그 외에도, 어느정도 테스트를 위해서 구조를 변경하는게 코드를 작성하는데 비용이 더 적게 든다.</p>
<p>자 여기까지 읽었으면, “테스트 코드를 위해 실제 설계 코드의 구조가 변경될 수 있다”는 사실에 동의 할 것이다. 그렇다면 이제 다른 문제를 고민해야 한다. </p>
<p>🧑‍💻 “<strong>설계 코드의 구조가 변경될 수 있다면 어디까지 프로덕션 코드에 변화를 줄 수 있을까?”</strong></p>
<p>위 문제에 대해서 정말 많이 고민을 하였지만, 명확한 답을 내리기 힘들어 리뷰어분께 헬프를 청해봤다…</p>
<p><img src="https://velog.velcdn.com/images/hong-sile/post/e794b8e7-ad0e-4373-9eae-8f9eb823f89e/image.png" alt=""></p>
<p>진짜 리뷰를 하나씩 받고 질문에 대한 리뷰어분의 이야기를 들을 때마다 우테코 너무 좋아진다.</p>
<p>이 글을 읽고나서, 아직 명확하다고 말할 정도는 아니지만, 어느정도 테스트 코드를 짜기 위해 어떻게 해야할지에 대한 방향성을 잡을 수 있었다. </p>
<h3 id="단위-테스트에서-메서드의-의존성">단위 테스트에서 메서드의 의존성</h3>
<hr>
<p>  또, 테스트를 할때 메서드간의 의존성에 대해서도 고민을 많이했었다. 
내가 만약 상태를 변화시키는 “명령”을 하는 메서드의 실행여부를 확인하려한다면 어떻게 해야할까?</p>
<p>  getter를 쓰거나, “조회”를 하는 메서드를 만들어야 할것이다.</p>
<p>  하지만 view에 전달될 도메인 클래스에 getter를 만드는 거까지는 납득했지만, 그냥 테스트를 위해서  getter를 만든다? 이건 진짜 아니라고 생각했다. 
아무리 테스트를 위해 프로덕션을 변경할 수 있다곤 하지만, 프로덕션 코드에서 사용될 여지가 없는(아 물론 쓰면 몇몇 로직이 간편해지겠지만 그것은 객체지향적이지 못하기에) getter를 만드는 것을 납득하지 못했다. 
아 view에 전달하기 위해 getter가 있는 도메인 객체에서는 getter를 사용해 테스트 하였다.</p>
<p>  그렇다고 “조회”를 하는 메서드를 이용해 “명령”을 한 메서드의 실행결과를 확인한다? 
  만약 “조회”를 하는 메서드가 정상적으로 동작을 하지 않는다면? 
  목표로 한 “명령”을 하는 메서드를 정상적으로 테스트했다고 말하기 어려울 것이다. 
  바로 메서드간의 의존성이 생겨서 이게 좋지 못하다고 생각했다.</p>
<p>그렇게 고민하다가 결국 두 가지를 떠올렸는데</p>
<ul>
<li>Mocking을 하여 내부의 메서드가 원하는 방식대로 사용되었는지를 확인한다(행위 검증)
→ 현 프로젝트에선 mock 라이브러리가 없음…</li>
<li>그냥 getter를 만들어서 쓴다.(상태 검증)</li>
</ul>
<p>두 개 다 만족스럽지 않았다. 근데 로직상 잘 찾아보니 상위 객체에서 정의된 getter를 활용할 수가 있었어서,  목표한 객체를 필드로 갖는 상위객체를 정의하고 그 곳에 목표 객체를 주입하여 해결하였다.</p>
<p>etc. AssertJ의 extracting에 대해서 알고 있었지만 반환형태가 AbstractObjectAssert 였기에, List에서 사용할 수있는 contains와 같은 메서드를 사용하지 못했다. 
지금은 asInstanceOf라는 메서드를 알게되어, 진짜 getter를 하나도 쓰지 않고, 상태에 대한 검증을 할 수 있게 되었다!!! </p>
<h2 id="step2-피드백">step2 피드백</h2>
<hr>
<p>step2와 같은 경우는 크게 변경할 점이 없어서, 따로 큰 피드백이 없었다. 자잘한 코딩 컨벤션 또는 주석에 대한 내용이어서, 꼼꼼하게 체크하면 해결될 문제였다.</p>
<h1 id="총정리">총정리</h1>
<hr>
<p>첫 번째 미션이었던 만큼 열정적으로 미션을 진행하였지만, 제대로 프로젝트를 진행해본 적도 없고, 페어 프로그래밍도 진행해본적이 없었기에 많은 실수들이 있었지만, 그 실수들로부터 배우는 것들이 많았다.</p>
<p>이 글을 쓰는 시점에, 2번째 미션을 끝냈는데, 1번째 미션에서 배웠던 것들을 다 적용할 수 있었어서, 좋았다. 성장해가는 것이 실시간으로 느껴진다. 지속적으로 셀프 피드백을 하며 성장해 나가야지.</p>
]]></description>
        </item>
    </channel>
</rss>