<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>iamjooon2님의 블로그</title>
    <link>https://iamjooon2.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 31 Jul 2026 17:02:05 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>iamjooon2</managingEditor>
    <image>
      <title>iamjooon2님의 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/5329763/attach/70037e5260b14e79b6f03d9eb572dc3f</url>
      <link>https://iamjooon2.tistory.com</link>
    </image>
    <item>
      <title>갱신 분실(lost update) 문제를 어떻게 해결할 수 있을까</title>
      <link>https://iamjooon2.tistory.com/66</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;학교 창업팀에서 재밌게 개발하던 중, 갱신 분실 이슈에 대해 고민할 상황을  만났다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항은 다음과 같았다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;매일 자정, 모든 사용자에게 재화를 1개 지급해주세요&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 spring boot와 jpa를 사용중이므로, 단순하게 생각하면 아래와 같이 할 수 있을 것이다&lt;/p&gt;
&lt;pre id=&quot;code_1738395078013&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@Transactional
@RequiredArgsConstructor
public class SchedulerWorker {

    private final MemberRepository memberRepository;
    
    @Scheduled(cron = &quot;0 0 0 * * *&quot;, zone = &quot;Asia/Seoul&quot;)
    public void giveAttendanceReward() {
        memberRepository.findAll()
            .forEach(member -&amp;gt; member.giveReward(1));
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 자체는 어렵지 않지만, 다른 개발자분이 작업 도중  문제가 생길 수 있는 지점을 말씀해주셨다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;지급과 동시에 사용자가 재화를 사용한다면, 값이 유실될 수도 있을 것 같아요..!&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획팀에서는 처음 이 얘기를 듣고, 비즈니스적으로 풀려고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 지급 시간을 자정이 아닌 새벽 4시같은 사용자가 적은 시간으로 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 지정된 시간에 지급하는 것이 아닌, 인앱 광고 시청시 재화 지급하도록 변경(사용자가 직접 수령하는 방식)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등등...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 일정상의 이유로 인앱 광고는 붙이지 않기로 했고...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 사용자라면 재화를 지급받는 시간에 맞추어서 해당 재화를 사용할 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 엔지니어로서, 기술적인 문제는 비즈니스로 풀기보단 기술로 풀어보고 싶었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 고민한 것들과 해결 방법을 이 글에 정리한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 상황&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 JPA를 사용중이므로, 위와 같이 구현했을때 나가는 쿼리로 생기는 엣지케이스는 다음과 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;갱신 분실 1.png&quot; data-origin-width=&quot;2630&quot; data-origin-height=&quot;1680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/npDMT/btsL3lYWDpl/cO8vCt1PqEm3A7yxeLqn0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/npDMT/btsL3lYWDpl/cO8vCt1PqEm3A7yxeLqn0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/npDMT/btsL3lYWDpl/cO8vCt1PqEm3A7yxeLqn0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnpDMT%2FbtsL3lYWDpl%2FcO8vCt1PqEm3A7yxeLqn0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;562&quot; height=&quot;359&quot; data-filename=&quot;갱신 분실 1.png&quot; data-origin-width=&quot;2630&quot; data-origin-height=&quot;1680&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 초기에 가지고있는 재화가 1이라고 가정했을 때, 스케줄러에 의해 1을 추가해준다면 기대값은 2가 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 재화를 1만큼 사용한다면, 2에서 1을 사용해 재화가 1이 되어야겠지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케줄러에 의한 변경이 커밋되기 이전의 값인 1을 읽어들여 재화를 1을 사용하기 때문에..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기대값인 1과 다르게 재화는 0이 되버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://en.wikipedia.org/wiki/Write%E2%80%93write_conflict&quot;&gt;갱신 분실(lost update)&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;문제가 발생하는 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;고민&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 어플리케이션 단 락&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글링을 하면 JPA단에서 낙관적 락, 비관적 락을 이용해 동시성 문제를 해결하는 방법이 정말 많이 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나 역시 이전에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://iamjooon2.tistory.com/60&quot;&gt;강의를 들으며 학습&lt;/a&gt;한 적이 있고, 수많은 블로그 글들을 봐왔기에 해당 방법이 먼저 떠올랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만.. 팀원분 중 spring, jpa를 처음 사용하는 분들도 계셨기에, 이후 유지보수시 수월하게 대응할 수 있을지가 고민이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 후순위로 두고, 다른 방법을 찾아보려 했다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 트랜잭션 격리레벨 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;트랜잭션과 관련된 문제니까 격리수준을 변경하면 되지 않을까? &lt;/i&gt;라고 생각했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 서비스의 DB는 MySQL이라, 기본 격리수준은 Repetable Read이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 높은 수준인 SERIALIZABLE로 올리면, 한 레코드에 한 트랜잭션만 접근할 수 있기에 부정합 문제를 방지할 수 있지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서 mysql을 띄운 후, 격리수준을 올려 실험을 해보았다&lt;/p&gt;
&lt;pre id=&quot;code_1738424180748&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;set transaction isolation level serializable; -- 전역 격리수준 변경

SELECT @@transaction_isolation; -- SERIALIZABLE
SELECT @@global.transaction_isolation; -- SERIALIZABLE

-- Tx 1
start transaction;

select * from member where id = 1; -- balance) 10
update member set balance = 11 where id = 1;

commit;

-- Tx 2
start transaction;

select * from member where id = 1; -- balance) 10
update member set balance = 9 where id = 1;

commit;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1059&quot; data-origin-height=&quot;367&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JDk5Q/btsL4Qwuvtn/xnRrctrW8dGmRxW3UeZXP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JDk5Q/btsL4Qwuvtn/xnRrctrW8dGmRxW3UeZXP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JDk5Q/btsL4Qwuvtn/xnRrctrW8dGmRxW3UeZXP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJDk5Q%2FbtsL4Qwuvtn%2FxnRrctrW8dGmRxW3UeZXP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1059&quot; height=&quot;367&quot; data-origin-width=&quot;1059&quot; data-origin-height=&quot;367&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;[40001][1213] Deadlock found when trying to get lock; try restarting transaction&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;락을 얻는데 실패해 데드락이 발생해 실패했다고 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;왜 데드락이 걸렸을까?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;격리 레벨을 Serializable로 올리게 되면, select 쿼리에도 락(공유락)이 걸리게 돼&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다른 트랜잭션에서의 조회요청은 허용하지만, 쓰기 요청은 허용되지 않는다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;(select for update와 같은 효과)&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;모식도는 아래와 같다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;913&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVid85/btsL5maLol8/2llSkAzQPGoU3T1g1rlfAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVid85/btsL5maLol8/2llSkAzQPGoU3T1g1rlfAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVid85/btsL5maLol8/2llSkAzQPGoU3T1g1rlfAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVid85%2FbtsL5maLol8%2F2llSkAzQPGoU3T1g1rlfAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;697&quot; height=&quot;313&quot; data-origin-width=&quot;913&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tx 1은 해당 레코드에 대해서 락을 얻기 위해 대기하게 되고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tx 2에서 다른 레코드가 락을 얻기 위해 대기하다가 데드락이 발생하게 된다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스는 답이 아닌걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Synchronized&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 코드레벨로 돌아와 생각한 것은, 자바 동기화에 대해 스쳐지나가듯 접한 synchronized다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; Synchronized 키워드는 &lt;b&gt;멀티스레드 환경에서 동기화(synchronization)를 보장&lt;/b&gt;하기 위해 사용되는 키워드로, 특정 코드 블록 또는 메서드가 &lt;b&gt;한 번에 하나의 스레드만 접근&lt;/b&gt;할 수 있도록 제한하는 자바의 키워드다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉, 메서드 선언할 때 그냥 붙이기만 하면 된다&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;synchronized는 서버가 여러대인 상황에서는 무의미해지니 실무에서 잘 사용하지 않는다고 했다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 단일 서버니까, 이걸 적용하면 되지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 서비스에서는 @Transactional 어노테이션을 이용해 논리적 트랜잭션 범위를 명시해주고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional은 AOP 기반으로 동작하고, AOP는 클래스를 상속하여 프록시 객체를 만든다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/44205710/why-is-java-synchronized-not-working-as-expected&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;하지만 synchronized는 메서드 시그니처에 포함되지 않아, 상속받은 클래스에서 사용할 수 없다&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(AOP에 관한 게시글은 아니기에, 자세한 설명 생략)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, synchronized는 @Transactional을 떼지 않는 이상 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당장 @Transactional을 떼더라도, 이후 어떻게 변경될 지 모르고... 또 어떤 어노테이션을 쓸 지 모른다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 지양하기로 했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. Atmoic Query&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://youtu.be/poyjLx-LOEU?si=oV1D9nduF1qv_0n8&amp;amp;t=1040&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;최범균님의 영상에서&lt;/a&gt;&amp;nbsp;갈피를 잡게 됐다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에 쿼리 날릴 때 원자적인 연산을 보장하는 방식이 있었다&lt;/p&gt;
&lt;pre id=&quot;code_1738492166629&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- before
update member set balance = 1 where id = 1

-- after
update member set balance = balance + 1 where id = 1;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점은 ORM 사용중이지만 직접 날쿼리를 날린다는 것, 객체지향이 조금 깨진다는 것 외에는.. 잘 모르겠다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향 문파에 있었지만(ㅋㅋ), 제일 중요한건 데이터 정합성 아닐까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 지금 위 방식으로 코드가 올라가있고... 아직까지 문제없이 잘 버텨줬다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마치며&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시성 제어는 신입 개발자로서 필수적인 개념이라고 생각하는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 진행하면서 마주쳐 고민할 수 있게 되어 다행이라고 생각한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼른 출시하고 사용자들 만나고싶다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간고사 준비해야지....&lt;/p&gt;</description>
      <category>트러블슈팅</category>
      <author>iamjooon2</author>
      <guid isPermaLink="true">https://iamjooon2.tistory.com/66</guid>
      <comments>https://iamjooon2.tistory.com/66#entry66comment</comments>
      <pubDate>Wed, 27 Mar 2024 23:08:42 +0900</pubDate>
    </item>
    <item>
      <title>Lombok is- 필드 직렬화 버그 트러블슈팅</title>
      <link>https://iamjooon2.tistory.com/63</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 진행중인 프로젝트에서는 API 응답이 성공했는지, 혹은 실패했는지를 포함한 기본 응답 포맷을 사용한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;ResponseBodyAdvice&quot; href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ResponseBodyAdvice&lt;/a&gt; 인터페이스를 구현하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러의 ResponseEntity가 생성된 후, HttpMessageConverter를 거쳐 직렬화 되기 전에 공통된 응답을 감싸 보내는 형식이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;게시글 적으면서 알게 된건데, 이런걸 &lt;a href=&quot;https://programmer93.tistory.com/62&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;봉투 패턴(Envelope Pattern)&lt;/a&gt; 이라 하더라&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 각설하고, 이 기본 응답엔 isSuccess 필드가 하나 있어서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공하면 isSuccess : true, 실패하면 isSuccess : false를 기본으로 반환하려 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감싸 보내는 코드는 다음과 같다&lt;/p&gt;
&lt;pre id=&quot;code_1707573823904&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
public class BaseResponse&amp;lt;T&amp;gt; {

    private final boolean isSuccess;
    private final T result;
    
    public BaseResponse(T result) {
        this.isSuccess = true;
        this.result = result;
    }
    
}

@Getter
public class ErrorResponse&amp;lt;T&amp;gt; {

    private final boolean isSuccess;
    private final String code;
    private final String message;
    private final T result;
    
    public BaseResponse(ExceptionType exceptionType, T result) {
        this.isSuccess = true;
        this.code = exceptionType.getCode();
        this.message = exceptionType.getMessage();
        this.result = result;
    }
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별 특이한 점은 없어 보이지만, 요걸 테스트하기위해 스웨거로 API 콜 때려보면 아주 신기한 일이 발생한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;191&quot; data-origin-height=&quot;103&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBRFC2/btsFUcZ4Mqs/cThlKL349kbkyDDRA6AJt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBRFC2/btsFUcZ4Mqs/cThlKL349kbkyDDRA6AJt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBRFC2/btsFUcZ4Mqs/cThlKL349kbkyDDRA6AJt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBRFC2%2FbtsFUcZ4Mqs%2FcThlKL349kbkyDDRA6AJt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;191&quot; height=&quot;103&quot; data-origin-width=&quot;191&quot; data-origin-height=&quot;103&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prefix인 is-가 없어져서 success가 나온다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔, @JsonProperty를 사용해서 임의의 값을 설정해줬다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;210&quot; data-origin-height=&quot;133&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rtIUU/btsFRZG8Nry/yhKSl2mv4kSm0Mqe0AA3VK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rtIUU/btsFRZG8Nry/yhKSl2mv4kSm0Mqe0AA3VK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rtIUU/btsFRZG8Nry/yhKSl2mv4kSm0Mqe0AA3VK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrtIUU%2FbtsFRZG8Nry%2FyhKSl2mv4kSm0Mqe0AA3VK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;210&quot; height=&quot;133&quot; data-origin-width=&quot;210&quot; data-origin-height=&quot;133&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그랬더니 isSuccess는 생겼지만, success도 그대로 있었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 후 인텔리제이의 classes 파일을 확인했다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u7DFn/btsFRc06Kad/kjYeUgOGJH1vV2nDA3V2aK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u7DFn/btsFRc06Kad/kjYeUgOGJH1vV2nDA3V2aK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u7DFn/btsFRc06Kad/kjYeUgOGJH1vV2nDA3V2aK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu7DFn%2FbtsFRc06Kad%2FkjYeUgOGJH1vV2nDA3V2aK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;738&quot; height=&quot;628&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;result 필드의 게터는 getResult()로 잘 만들어졌는데, isSuccess는 isSuccess()로 만들어졌다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 응답 보내면 success도 오고, isSuccess도 오잖아....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수상하다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@JsonProperty를 지우고 다시 빌드를 돌려봤다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;528&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVYKrI/btsFTcFZWWw/fem3NKjX4s1cvBVXyLIK61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVYKrI/btsFTcFZWWw/fem3NKjX4s1cvBVXyLIK61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVYKrI/btsFTcFZWWw/fem3NKjX4s1cvBVXyLIK61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVYKrI%2FbtsFTcFZWWw%2Ffem3NKjX4s1cvBVXyLIK61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;528&quot; height=&quot;498&quot; data-origin-width=&quot;528&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isSuccess()가 그대로 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이친구... @JsonProperty 때문에 나온 친구가 아니었다?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보니 @JsonProperty는 json에 매핑해주는 놈이니, 빌드된 파일과는 상관없다..ㅋㅋㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 롬복의 @Getter 요녀석이 getIsSuccess()로 안만들고, isSuccess()로 만들었다는 얘기다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클린코드에서는 boolean 데이터 타입을 반환하는 함수의 경우, is-, can-, has-의 접두사를 붙이라고 말한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련이 있으려나 ?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isSuccess의 데이터 타입을 int로 바꾸고 다시 빌드를 해봤다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;473&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czyXYw/btsFUHemid4/0yvad1Q0fktjG7di9S7OR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czyXYw/btsFUHemid4/0yvad1Q0fktjG7di9S7OR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czyXYw/btsFUHemid4/0yvad1Q0fktjG7di9S7OR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczyXYw%2FbtsFUHemid4%2F0yvad1Q0fktjG7di9S7OR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;473&quot; height=&quot;508&quot; data-origin-width=&quot;473&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getIsSuccess()로 생성된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 데이터타입을 boolean으로 바꿔주고, is- 접두사를 빼보고 빌드를 돌려봤다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dP8xtD/btsFUm9peDb/kZI4KwGqon8kp0MswHth3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dP8xtD/btsFUm9peDb/kZI4KwGqon8kp0MswHth3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dP8xtD/btsFUm9peDb/kZI4KwGqon8kp0MswHth3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdP8xtD%2FbtsFUm9peDb%2FkZI4KwGqon8kp0MswHth3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;503&quot; height=&quot;530&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;530&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;boolean이라 그런가 is- 가 붙어서 나온다....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국.. &lt;b&gt;롬복이 boolean 필드의 경우 게터를 is-로 컴파일 한다는 얘기였다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 롬복을 안쓰고 그냥 Getter를 만들어주면 될려나?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;롬복 안쓰고 직접 get-으로 시작하는 게터들을 만들어주고 API 콜 때려봤다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;156&quot; data-origin-height=&quot;45&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GakJQ/btsFS0TjiHb/24K6867zEmYkKbLHxcCWj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GakJQ/btsFS0TjiHb/24K6867zEmYkKbLHxcCWj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GakJQ/btsFS0TjiHb/24K6867zEmYkKbLHxcCWj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGakJQ%2FbtsFS0TjiHb%2F24K6867zEmYkKbLHxcCWj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;156&quot; height=&quot;45&quot; data-origin-width=&quot;156&quot; data-origin-height=&quot;45&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘.. 나온다....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 해결했는데, 소소한 고민이 생겼다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BaseResponse는 필드가 두 개밖에 없어서 그렇다 쳐도, ExceptionResponse는 필드가 여러개인데 이거 다 게터 만들어줘야할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾아보니 &lt;a href=&quot;https://www.baeldung.com/lombok-omit-getter-setter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;@Getter(AccessLevel.NONE)을 통해 특정 필드의 접근을 막을 수 있다고 한다&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트러블슈팅 다 했으니.. 빠르게 글을 마무리 지어본다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해결 &amp;amp; 결론&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;롬복은 boolean 필드는 getIs-가 아닌 is- 식으로 메서드를 만든다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isSuccess는 롬복을 사용하지 않고, 직접 getter를 만들어줘서 해결했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 코드는 아래와 같다&lt;/p&gt;
&lt;pre id=&quot;code_1710775093856&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class BaseResponse&amp;lt;T&amp;gt; {

    private final boolean isSuccess;
    private final T result;
    
    public BaseResponse(T result) {
        this.isSuccess = true;
        this.result = result;
    }
    
    public boolean getIsSuccess() {
        return isSuceess;
    }
    
    public T getResult() {
        return result;
    }
    
}

@Getter
public class ErrorResponse&amp;lt;T&amp;gt; {

    @Getter(AccessLevel.NONE)
    private final boolean isSuccess;
    private final String code;
    private final String message;
    private final T result;
    
    public BaseResponse(ExceptionType exceptionType, T result) {
        this.isSuccess = false;
        this.code = exceptionType.getCode();
        this.message = exceptionType.getMessage();
        this.result = result;
    }
    
    public boolean getIsSuccess() {
        return isSuccess;   
    }
    
}​&lt;/code&gt;&lt;/pre&gt;</description>
      <category>트러블슈팅</category>
      <author>iamjooon2</author>
      <guid isPermaLink="true">https://iamjooon2.tistory.com/63</guid>
      <comments>https://iamjooon2.tistory.com/63#entry63comment</comments>
      <pubDate>Fri, 9 Feb 2024 00:15:54 +0900</pubDate>
    </item>
    <item>
      <title>어린 시절부터 앞서가고 뒤쳐지는 것</title>
      <link>https://iamjooon2.tistory.com/61</link>
      <description>&lt;div style=&quot;background-color: #ffffff; color: #050505; text-align: left;&quot;&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;자신과 동갑이거나 어린데 벌써부터 탁월한 연주자들을 유튜브나 페이스북으로 보면서 열등감과 자괴감 등에 빠진 특히 어린 친구들이 많을 거다. 정보가 넘쳐나고 모든 것들을 확인할 수 있는 시대이기 때문에. 어쩔 수 없는 것 같다.&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #050505; text-align: left;&quot;&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;나도 20살 때 마냥 드럼을 좋아하는 일반 전문대 경영학과 학생이었는데. 나랑 동갑내기 학생들의 연주를 공연 등에서 보고 왔을 때도, 또 그때부터 유명했던 비슷한 또래 연주자들을 보면서도 나도 많은 감정을 느꼈다. 내가 10년을 쳐도 저 친구들만큼이라도 칠 수 있을까라는 감정을 꽤 자주 느끼고. 열등감을 느꼈지만 연습도 열심히 했다.&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #050505; text-align: left;&quot;&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;그런데 점점 지나고 보니 깨닫게 된 것은. 연주는 따라잡고 뒤쳐지는 게 없는 거였고, 컬러가 달라지는 거였다. 연주로 따라잡는다는 게 대체 어떻게 판단할 수 있단 말인가.ㅎ 예를 들어 어떤 곡을 저 사람이 연주할 땐 멋짐이 10인데 내가 치면 9였다가 나중에 비로소 10이 되는 건가. 그런 건 존재하지 않는다.&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #050505; text-align: left;&quot;&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;또는 쟤가 손 발이 240 까지 나오는데 난 240 에서 4연음 스트록만 되는데 쟤는 240에서 스트록과 컴비네이션까지 되면 쟤가 더 위대한 연주자인가. 그런 것도 없다.&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #050505; text-align: left;&quot;&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;단지 흔히 얘기하는 &amp;lsquo;스타일&amp;rsquo;이 중요한 것이었다. 스타일을 갖춘 연주자가 되면. 말 그대로 그 사람이 연주하는 방식이 생기면. 그리고 그게 매력적이라면. 그때부턴 멋진 가치를 지닌 연주자가 되는 것이었다.&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #050505; text-align: left;&quot;&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;예를 들어 아마도 토니 로이스터 주니어가 베니그렙보다 손은 2배 빠를 것이다. 그런데 그렇다고 해서 토니와 베니 둘 중 누가 더 높고 낮은 가를 평가하나? 아니다. 토니 나름의 연주 방식이 있고. 베니 나름의 연주 방식이 있고. 그래서 두 사람 다 존중받는 것이고. 과연 그 둘이 서로의 연주를 보며 신경이나 쓸까? 아니다. 그냥 서로 박수를 쳐줄 것이다. 와~ 멋지다! 하면서.&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #050505; text-align: left;&quot;&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;그리고 그 둘이 누가 더 나이가 많은 게 중요한가? 예를 들어 토니는 12살 때부터 세계적 스타가 되었고. 베니는 세계적으로 알려진 게 아마 30대에 들어서 정도인 듯 한데. 그렇다면 토니가 이긴건가? 그런게 어디있나. 둘 다 그냥 세계적 스타인 것이지.&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #050505; text-align: left;&quot;&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;맛동산처럼 수십년 전에 출시된 과자여서 그때부터 인기 상품으로 잘 팔렸어도 멋진 것이고. 최근에 출시된 과자인데 인기 상품이 된 것도 멋진 것이고. 나이,시점 등이 뭐가 중요한가.&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #050505; text-align: left;&quot;&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;하지만 오래 전에 출시가 되서 계속 사랑을 받는 상품은 이유가 있는 것이고. 그만큼 그렇게 오랫동안 사랑받는다는 것은 정말 어려운 일인 것이다. 그래서 우리의 전 세대의 연주자 선배님들 중 오랫동안. 또 여전히 직업 음악인으로써 살아가고 계신 분들은 정말 대단한 것이다. 음악으로도, 음악 외적으로도. 그런 분들의 이야기는 분명히 새겨 들을만 하다. 버릴 이야기가 하나도 없다.&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #050505; text-align: left;&quot;&gt;
&lt;div style=&quot;text-align: start;&quot;&gt;어쨌든 많은 연주 동영상을 접하며 열등감 등에 빠지기 보단 그들이 했을 노력을 칭찬하고, 나이가 어리든 적든 배울 점을 찾고. 쟤는 칸쵸지만 내가 더 맛있는 칸쵸가 되어야지. 보다는 나는 칸쵸보단 새우깡이 될꺼야 라는 마음으로. 그러면 훨씬 연습과 음악을 즐길 수 있다고 생각합니다. 글이 길었습니다. 화이팅!&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;from. 드러머 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;임용훈님 페이스북&lt;br /&gt;&lt;a href=&quot;https://www.facebook.com/drumyonghoon/posts/843988252431933/?_rdr&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.facebook.com/drumyonghoon/posts/843988252431933/?_rdr&lt;/a&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <author>iamjooon2</author>
      <guid isPermaLink="true">https://iamjooon2.tistory.com/61</guid>
      <comments>https://iamjooon2.tistory.com/61#entry61comment</comments>
      <pubDate>Thu, 4 Jan 2024 17:16:49 +0900</pubDate>
    </item>
    <item>
      <title>동시성 문제를 해결해보자 - RDB 분산 락</title>
      <link>https://iamjooon2.tistory.com/60</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스트에서 Java의 synchronized를 이용하여 동시성 문제를 해결해보았는데요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional 적용이 안된다는 점과, 스케일 아웃 환경에서 동시성 보장이 안된다는 문제가 있었습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 문제를 해결할 수 있는 방법으로, 분산 락을 이용하는 방법을 소개해보려 합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;분산된 환경&lt;/b&gt;&lt;/i&gt;에서 &lt;i&gt;&lt;b&gt;동일 자원&lt;/b&gt;에 접근&lt;/i&gt;하는 경우, 동시에 한 개의 프로세스만 접근 가능하도록 하기 위해 사용하는 락을 &lt;b&gt;분산락(Distribution Lock)&lt;/b&gt;이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 공부할 때 헷갈린 포인트가 X-lock이니, S-Lock등의 키워드였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 분산된 환경에서의 잠금을 구현하기 위한 방법들이고, 그 구현 방법으로 크게 낙관적 락(Optimistic Lock), 비관적 락(Pessmistic Lock), MySQL의 Named Lock 등이 있던 거였습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포스트에서 낙관적 락, 비관적 락, MySQL의 네임드 락에 대해 정리해보겠습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 비관적 락&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락은 데이터베이스의 S-Lock이나 X-Lock등을 이용한 방법입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- S(&lt;b&gt;S&lt;/b&gt;hared) Lock&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S-Lock은 데이터베이스의 특정 행을 &lt;b&gt;읽을 때&lt;/b&gt; 사용하는 락입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S-Lock이 걸린 행에는, 다른 트랜잭션은 S-Lock만이 접근 가능하고, X-Lock은 접근 불가능합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-X(e&lt;b&gt;X&lt;/b&gt;clusive) Lock&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;X-Lock은 데이터베이스의 특정 행에 &lt;b&gt;쓰기작업을 할 때&amp;nbsp;&lt;/b&gt;사용하는 락입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;X-Lock이 걸린 행은 다른 트랜잭션에서는 S-Lock 혹은 X-Lock을 통해 접근할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 JPA에서 비관적 락을 적용해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701671676332&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface StockRepository extends JpaRepository&amp;lt;Stock, Long&amp;gt; {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query(&quot;select s from Stock s where s.id = :id&quot;)
    Stock findByIdWithPessimisticXLock(Long id);

    @Lock(LockModeType.PESSIMISTIC_READ)
    @Query(&quot;select s from Stock s where s.id = :id&quot;)
    Stock findByIdWithPessimisticSLock(Long id);
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Lock 어노테이션을 통해 잠금을 설정할 수 있고, LockModeType을 통해 어떤 종류의 락을 설정할지 고를 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Query 메서드를 통해 실제 JPQL을 사용한 모습입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메서드를 사용하는 서비스와 이 서비스의 테스트 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701671907928&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class StockPessimisticLockService {

    private final StockRepository stockRepository;

    public StockPessimisticLockService(StockRepository stockRepository) {
        this.stockRepository = stockRepository;
    }

    @Transactional
    public void decrease(Long id) {
        Stock stock = stockRepository.findByIdWithPessimisticXLock(id);
        // Stock stock = stockRepository.findByIdWithPessimisticSLock(id);
        stock.decrease();

        stockRepository.save(stock);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트는 아래와 같습니다&lt;/p&gt;
&lt;pre id=&quot;code_1701671955016&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
@SuppressWarnings(&quot;NonAsciiCharacters&quot;)
class StockPessimisticLockServiceTest {

    @Autowired
    private StockPessimisticLockService stockPessimisticLockService;

    @Autowired
    private StockRepository stockRepository;

    @BeforeEach
    void setUp() {
        stockRepository.saveAndFlush(new Stock(1L, 100L));
    }

    @Test
    void 동시에_100개의_요청() throws InterruptedException {
        // given
        int threadCount = 100;
        ExecutorService executorService = Executors.newFixedThreadPool(32);
        CountDownLatch latch = new CountDownLatch(threadCount);

        // when
        for (int i = 0; i &amp;lt; threadCount; i++) {
            executorService.submit(() -&amp;gt; {
                try {
                    stockPessimisticLockService.decrease(1L);
                } finally {
                    latch.countDown();
                }
            });
        }
        latch.await();

        // then
        Stock stock = stockRepository.findById(1L).orElseThrow();
        assertThat(stock.getQuantity()).isZero();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 테스트를 실행했을 때,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;185&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJ7Pl8/btsBqzEUKYu/Fn19vp2hyJ45uX8XgoQOH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJ7Pl8/btsBqzEUKYu/Fn19vp2hyJ45uX8XgoQOH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJ7Pl8/btsBqzEUKYu/Fn19vp2hyJ45uX8XgoQOH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJ7Pl8%2FbtsBqzEUKYu%2FFn19vp2hyJ45uX8XgoQOH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;829&quot; height=&quot;185&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;185&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트가 성공하는 것을 볼 수 있고, Hibernate 내에서 실제 나가는 쿼리를 보면, for update가 잘 붙는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락(pessimistic lock)의 경우, 충돌이 빈번하지 않다면 낙관적 락(optimistic lock)보다 더 좋은 선택일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터의 정합성이 완전히 보장되나, 데드락의 위험성이 존재한다는 단점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 낙관적 락&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙관적 락은 데이터베이스에 실제 락을 사용하지 않고, &lt;b&gt;버전 칼럼을 통해 데이터베이스의 정합성을 맞추는 방법&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽어온 시점과 트랜잭션이 커밋되는 시점의 데이터의 정합성을 비교하는 방식으로 이루어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하고자 하는 테이블에 @Version 어노테이션을 추가해서 관리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1701674556774&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Stock {

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

    private Long productId;

    private Long quantity;

    @Version
    private Long version;

    protected Stock() {
    }

    public Stock(Long productId, Long quantity) {
        this.productId = productId;
        this.quantity = quantity;
    }

    public void decrease() {
        if (this.quantity &amp;lt; 0) {
            throw new IllegalArgumentException();
        }
        this.quantity -= 1;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이&amp;nbsp; @Version 어노테이션을 이용할 수 있고, 이제 이걸 사용하는 Pessimistic Lock을 적용해보겠습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1701674628659&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface StockRepository extends JpaRepository&amp;lt;Stock, Long&amp;gt; {

    @Lock(LockModeType.OPTIMISTIC)
    @Query(&quot;select s from Stock s where s.id = :id&quot;)
    Stock findByIdWithOptimisticLock(Long id);

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Lock 어노테이션을 이용하고, Optimistic 옵션을 추가해줍니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙관적 락은&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Version&lt;span&gt;&amp;nbsp;&lt;/span&gt;어노테이션이 달린 컬럼을 관리해야할 버전으로 인식 후, 자동으로 비교 및 업데이트해줍니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 데이터베이스단에 거는 잠금이 아닌, &lt;b&gt;어플리케이션 레벨에 거는 잠금&lt;/b&gt;입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;낙관적 락은 동시성 감지만 가능합니다. 성공해야한다면, 예외를 감지했을 때 재시도를 하기 위해&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Service를 사용하는 파사드를 하나 만들어, 재시도 처리를 이곳에서 처리하도록 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1701677936600&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class StockOptimisticLockService {

    private final StockRepository stockRepository;

    public StockOptimisticLockService(StockRepository stockRepository) {
        this.stockRepository = stockRepository;
    }

    @Transactional
    public void decrease(Long id) {
        Stock stock = stockRepository.findByIdWithOptimisticLock(id);
        stock.decrease();

        stockRepository.save(stock);
    }

}

@Service
public class StockOptimisticLockServiceFacade {

    private final StockOptimisticLockService stockOptimisticLockService;

    public StockOptimisticLockServiceFacade(StockOptimisticLockService stockOptimisticLockService) {
        this.stockOptimisticLockService = stockOptimisticLockService;
    }

    public void decrease(Long id) {
        while (true) {
            try {
                stockOptimisticLockService.decrease(id);

                break;
            } catch (Exception e) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }
    }
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 테스트에서 사용하는 서비스만 파사드로 변경해준 뒤, 실행해 보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;818&quot; data-origin-height=&quot;151&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbqSsy/btsBiwjfTQe/muDYY0mNiOaaomcNT2QokK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbqSsy/btsBiwjfTQe/muDYY0mNiOaaomcNT2QokK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbqSsy/btsBiwjfTQe/muDYY0mNiOaaomcNT2QokK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbqSsy%2FbtsBiwjfTQe%2FmuDYY0mNiOaaomcNT2QokK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;818&quot; height=&quot;151&quot; data-origin-width=&quot;818&quot; data-origin-height=&quot;151&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트도 성공적으로 통과하는 것을 볼 수 있고, 쿼리에도 version 행을 이용하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙관적 락은, 실제로 DB상에서 락을 잡는 것이 아닌 데이터베이스의 버전을 통해 사용하기 때문에 비관적 락보다 성능상 이점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고로 충돌이 빈번할 것이라 예상된다면, 낙관적 락이 좋은 선택지가 될 수 있겠죠?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 네임드 락&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네임드 락은 MySQL에서 지원해주는 락 중 하나의 종류인데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블, 레코드가 아닌 임의의 문자열에 대해 잠금을 획득할 수 있습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용 방법은 간단합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 MySQL에서 GET_LOCK()을 통해 잠금 획득하는 방법을 그대로 이용하는 건데요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드와 함께 보시죠!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1701679116403&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface LockRepository extends JpaRepository&amp;lt;Stock, Long&amp;gt; {

    // 락 획득
    @Query(value = &quot;select get_lock(:key, 3000)&quot;, nativeQuery = true)
    void getLock(String key);

    // 락 해제
    @Query(value = &quot;select release_lock(:key)&quot;, nativeQuery = true)
    void releaseLock(String key);

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nativeQuery 옵션을 통해, JPQL이 아닌 SQL 문법을 사용함을 명시한 다음,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;value 옵션에 쿼리를 작성해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용할 서비스에 다음과 같은 메서드를 작성해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1701679323685&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void decrease(Long id) {
    try {
        lockRepository.getLock(id.toString());

        stockService.decrease(id);
    } finally {
        lockRepository.releaseLock(id.toString());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락을 획득한 다음 재고를 감소시킨 후 최종적으로 락을 해제해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(user-level lock은 자동으로 해제되지 않기 때문에 꼭 명시적으로 해제해줘야 합니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네임드 락 역시, 분산락을 구현할 때  많이 사용하지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락은 timeout 설정이 어렵지만, 네임드 락이 timeout을 더 쉽게 구현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 트랜잭션 종료시, &lt;i&gt;&lt;b&gt;락 해제 관리를 꼭 해줘야합니다.&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네임드 락은 MySQL의 임의의 문자열에 대해 잠금을 획득하지만(변수같은 느낌인 것 같습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락은 레코드 수준에 락을 걸기 때문에, 다른 트랜잭션이 같은 레코드에 접근할 수 없다는 차이가 있는 것 같습니다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 &lt;a href=&quot;https://github.com/iamjooon2-playground/concurrency&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;이 곳&lt;/a&gt; 에서 확인할 수 있습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레퍼런스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C/dashboard&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/2631/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://techblog.woowahan.com/2631/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>iamjooon2</author>
      <guid isPermaLink="true">https://iamjooon2.tistory.com/60</guid>
      <comments>https://iamjooon2.tistory.com/60#entry60comment</comments>
      <pubDate>Mon, 4 Dec 2023 17:58:28 +0900</pubDate>
    </item>
    <item>
      <title>동시성 문제를 해결해보자 - synchronized</title>
      <link>https://iamjooon2.tistory.com/59</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서 꼭 한 번 만나고싶었던 문제가 바로 동시성문제였는데요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선착순으로 실행되는 기능이 없고, 좋아요 기능이 있지만 서비스 기획상 치명적이지 않아서, 동시성 문제를 만날 일이 없었습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 따로 찾아 공부하고 익혀 정리해보려 합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 동시성 문제란 무엇일까용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공대생의 친구 위키피디아에 &lt;i&gt;동시성 제어&lt;/i&gt;를 검색해보았습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%A0%95%EB%B3%B4%EA%B8%B0%EC%88%A0&quot;&gt;정보기술&lt;/a&gt;과&amp;nbsp;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99&quot;&gt;컴퓨터 과학&lt;/a&gt;에서, 특히&amp;nbsp;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%BB%B4%ED%93%A8%ED%84%B0_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&quot;&gt;컴퓨터 프로그래밍&lt;/a&gt;,&amp;nbsp;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%9A%B4%EC%98%81_%EC%B2%B4%EC%A0%9C&quot;&gt;운영 체제&lt;/a&gt;,&amp;nbsp;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EB%A9%80%ED%8B%B0%ED%94%84%EB%A1%9C%EC%84%B8%EC%84%9C&quot;&gt;멀티프로세서&lt;/a&gt;,&amp;nbsp;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4&quot;&gt;데이터베이스&lt;/a&gt;&amp;nbsp;분야에서&amp;nbsp;동시성 제어(concurrency control)는 가능한 빠른 조회와 동시에&amp;nbsp;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EB%B3%91%ED%96%89_%EC%BB%B4%ED%93%A8%ED%8C%85&quot;&gt;병행&lt;/a&gt;되는 동작의 정확한 결과가 발생하는 것을 보증한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 언어로 정리해보자면, &lt;b&gt;동시에 여러 사용자가 접근할 때 발생하는 문제&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 정리해볼 수 있겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 예제와 함께 보겠습니다&lt;/p&gt;
&lt;pre id=&quot;code_1701593418374&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void decrease(Long id) {
        Stock stock = stockRepository.findById(id).orElseThrow();
        stock.decrease();

        stockRepository.save(stock);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 레이어에서 id를 바탕으로 재고(Stock)을 가져와 재고를 하나 감소시킨 다음(decrease()),&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스에 변경 내역을 반영(save()) 하는 메서드입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉보기에는 문제없어보이지만, 동시에 100명의 사용자가 해당 메서드를 통해 Stock에 접근한다고 가정해봤을 때, 해당 코드는 문제가 발생합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트코드와 함께 보겠습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1701593769305&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SuppressWarnings(&quot;NonAsciiCharacters&quot;)
@SpringBootTest
class StockServiceTest {

    @Autowired
    private StockService stockService;

    @Autowired
    private StockRepository stockRepository;

    @BeforeEach
    void before() {
        stockRepository.saveAndFlush(new Stock(1L, 100L));
    }

    @Test
    void 동시에_100개의_요청을_받는다() throws InterruptedException {
        // given
        int threadCount = 100;
        ExecutorService executorService = Executors.newFixedThreadPool(32);
        CountDownLatch latch = new CountDownLatch(threadCount); // 다른 쓰레드에서 작업이 완료될 때까지 기다릴 수 있게 해주는 클래스

        // when
        for (int i = 0; i &amp;lt; threadCount; i++) {
            executorService.submit(() -&amp;gt; {
                try {
                    stockService.decrease(1L);
                } finally {
                    latch.countDown();
                }
            });
        }
        latch.await();

        // then
        Long quantity = stockRepository.findById(1L).orElseThrow().getQuantity();
        assertThat(quantity).isZero();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시에 100개의 요청이 들어옴을 가정한 이용한 위 테스트는 보기 좋게 실패합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;533&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uL5uZ/btsBh40ZRCh/W9lqWXfhGrvOAyTlFFlW6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uL5uZ/btsBh40ZRCh/W9lqWXfhGrvOAyTlFFlW6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uL5uZ/btsBh40ZRCh/W9lqWXfhGrvOAyTlFFlW6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuL5uZ%2FbtsBh40ZRCh%2FW9lqWXfhGrvOAyTlFFlW6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;499&quot; height=&quot;204&quot; data-origin-width=&quot;533&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 동시성 문제가 발생했기 때문인데요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100명의 사용자가 같은 순간 접근한다고 가정할 때, 정합성이 보장되지 않는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 동시성 문제는,&amp;nbsp;Race Condition(경쟁 상태)에 해당되기 때문에 발생하는 문제인데요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Race Condition은, 두 개 이상의 프로세스나 스레드가 하나의 공유자원에 접근하여 변경하려는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접근 순서에 따라 실행 결과가 달라지는 현상을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 여러 스레드가 동시에 Stock에 접근해서 일어나는 일임을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 이 문제를 어떻게 해결할 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시성 문제를 해결하기 위한 방법은 크게 세가지가 있습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;b&gt;Java의 synchronized 키워드를 이용하여 해결하는 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;b&gt;데이터베이스의 Lock을 이용한 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 비관적 락&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 낙관적 락&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 네임드 락&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;b&gt;Redis를 이용한 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Lettuce&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Redisson&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방법들을 하나하나 정리하고, 각각의 장단점을 비교해보려 합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 방법은, synchronized를 이용한 방법입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;synchronized를 이용한 방법은 간단합니다. 해당하는 메서드에 synchronized 키워드만 붙이면 됩니다&lt;/p&gt;
&lt;pre id=&quot;code_1701601139343&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public synchronized void decrease(Long id) {
        Stock stock = stockRepository.findById(id).orElseThrow();
        stock.decrease();

        stockRepository.save(stock);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 간단하죠?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 synchronized 키워드를 통해, 하나의 쓰레드만 접근이 가능하도록 성장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 해결할 수 있지만, 그만큼의 리스크역시 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. @Transactional 어노테이션을 사용하지 못한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 서버가 스케일-아웃된 환경일 시, 적용될 수 없다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번의 경우, @Transaction 어노테이션은 AOP 기반으로 작동되는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP는 해당 어노테이션이 붙은 클래스를 런타임시점에서 상속받은 객체를 만들어 사용하는데, 이 작동원리 때문에 다른 쓰레드에서 동일한 메서드를 호출할 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 로직이 간단하여 상관없지만 하나의 트랜잭션으로 묶어야 하는 작업이 많아지는 경우 문제가 생길 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번의 경우, synchronized 키워드는 하나의 프로세스 내에서만 보장이 되기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케일 아웃이 적용된 경우, 여러 서버가 각각의 프로세스를 띄워서 동시성이 보장이 될 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, @Transactional 어노테이션도 적용하면서, 스케일 아웃된 환경에서 동시성 보장을 위해서는 어떤걸 할 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 데이터베이스의 락을 이용해서 할 수 있습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 다음 포스트에 적어보도록 하겠습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레퍼런스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C/dashboard&lt;/a&gt;&lt;/p&gt;</description>
      <author>iamjooon2</author>
      <guid isPermaLink="true">https://iamjooon2.tistory.com/59</guid>
      <comments>https://iamjooon2.tistory.com/59#entry59comment</comments>
      <pubDate>Sun, 3 Dec 2023 23:58:01 +0900</pubDate>
    </item>
    <item>
      <title>로컬 캐싱을 이용해서 성능을 높이자</title>
      <link>https://iamjooon2.tistory.com/57</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;집사의고민 프로젝트의 기능 개발은 완료되었고, 이제 리팩터링 및 성능 개선을 고민하고 있는 상황입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러던 중, 쉽게 바뀌지 않는 API를 캐싱하는 것은 어떨까라는 생각이 들었는데요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;적용 과정과 선택한 기술에 대한 근거를 정리하고자 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 전에, 캐시는 무엇일까요&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;공대생의 친구, 위키백과에게 물어봤습니다&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;캐시(cache,&amp;nbsp;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://ko.wikipedia.org/wiki/%EB%AC%B8%ED%99%94%EC%96%B4&quot;&gt;문화어&lt;/a&gt;:&amp;nbsp;캐쉬, 고속완충기, 고속완충기억기)는&amp;nbsp;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://ko.wikipedia.org/wiki/%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99&quot;&gt;컴퓨터 과학&lt;/a&gt;에서 데이터나 값을 미리 복사해 놓는 임시 장소를 가리킨다. 캐시는 캐시의 접근 시간에 비해 원래 데이터를 접근하는 시간이 오래 걸리는 경우나 값을 다시 계산하는 시간을 절약하고 싶은 경우에 사용한다. 캐시에 데이터를 미리 복사해 놓으면 계산이나 접근 시간없이 더 빠른 속도로 데이터에 접근할 수 있다.&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;라고 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제 언어로 정리해보자면, 미리 쟁여둔 값을 사용하는 것입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;풀링도 그렇고, 컴퓨터공학에서는 이렇게 쟁여두는 게 참 많은 것 같습니다&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;로컬 캐시?&amp;nbsp; 글로벌 캐시?&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;캐시는 크게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;로컬 캐시&lt;/b&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;글로벌 캐시&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;두 종류가 존재합니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로컬 캐시는  서버의 내장 캐시를 사용하는 방법입니다. 로컬 서버의 메모리를 사용하여 캐싱처리를 하고,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따로 네트워크를 타지 않아도 된다는 장점이 있어요&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;글로벌 캐시는 여러 서버에서 캐시 서버에 접근하여 참조하는 캐시입니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;별도의 캐시 서버를 이용하여 서버간 데이터 공유가 쉽고, 네트워크 트래픽을 사용해야해서&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로컬 캐시보다 속도는 느린 점이 있어요&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저는 이 둘중&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;로컬 캐시를 선택&lt;/b&gt;했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;크게 두가지 이유였는데요&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;첫번째로는, 글로벌 캐시를 사용하기에는 비용이 너무 크다고 생각했기 때문입니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;캐싱하려는 API는 두 개이고, 더 자세히는 ORM에서의 메서드 한 개입니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 메서드 하나를 위해 부가적인 인프라를 구축하는 것은 배보다 배꼽이 더 크다고 생각했어요&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한 현재 서버는  EC2 하나 내의 단일 WAS로 구성되어있습니다. 여러대의 서버로 scale-out되지 않은 상태라서, 여러 서버간의 정합성을 필요성도 없었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두번째로는, 로컬 캐시가 글로벌 캐시보다 더 빠르기 때문입니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로컬캐시를 사용한다면&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;별도의 인프라에서 정보를 가져오기위한 네트워크 비용이 필요없기 때문입니다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용 라이브러리&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로컬 캐싱 라이브러리에 크게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://github.com/ben-manes/caffeine&quot;&gt;Caffeine&lt;/a&gt;과 Ehcache 두 개가 대표주자격으로 있는 것 같더라구요&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저는 Caffeine을 선택했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Ehcache는 xml 방식으로 적용해야해서 불편하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://github.com/ben-manes/caffeine/wiki/Benchmarks&quot;&gt;Caffeine이 성능면에서 더 빠르더라구요&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;적용 방법&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래의 두 의존성을 추가했습니다&lt;/p&gt;
&lt;pre id=&quot;code_1699941211568&quot; class=&quot;clean&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.github.ben-manes.caffeine:caffeine'&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 추가한 `spring-boot-starter-cache`는&amp;nbsp; 스프링에서 cache를 구현할 수 있도록, 인터페이스를 제공합니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;`spring-boot-starter-web`에서 기본적으로 제공해주는 @EnableCaching이 있긴 하지만,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;직접적으로 캐싱을 사용하기위한 기능은 포함되어 있지 않습니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 아래 caffeine은, 캐싱에 선택한 라이브러리입니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;적용한 코드는 아래와 같습니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1699941211569&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@EnableCaching
@Configuration
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(caches());
        return cacheManager;
    }

    private List&amp;lt;CaffeineCache&amp;gt; caches() {
        return Arrays.stream(CacheType.values())
                .map(cacheType -&amp;gt; new CaffeineCache(cacheType.getName(), cache(cacheType)))
                .toList();
    }

    private Cache&amp;lt;Object, Object&amp;gt; cache(CacheType cacheType) {
        return Caffeine.newBuilder()
                .maximumSize(cacheType.getMaxSize())
                .build();
    }

}

@Getter
@AllArgsConstructor
public enum CacheType {

    BREEDS(&quot;breeds&quot;, 1),
    ;

    private final String name;
    private final int maxSize;

}

public interface BreedRepository extends JpaRepository&amp;lt;Breed, Long&amp;gt; {

    @Cacheable(cacheNames = &quot;breeds&quot;, key = &quot;'allBreeds'&quot;)
    default List&amp;lt;Breed&amp;gt; findAllBreeds() {
        return findAll();
    }
}

@Service
public class AdminService {

    @CacheEvict(cacheNames = &quot;breeds&quot;, key = &quot;'allBreeds'&quot;)
    public Long createBreed(Long petSizeId, String breedName) {
            PetSize petSize = petSizeRepository.getReferenceById(petSizeId);
            Breed breed = breedRepository.save(Breed.builder().name(breedName).petSize(petSize).build());
            return breed.getId();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클래스 하나하나씩 설명해보자면,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 CacheType은, 우리가 어플리케이션에서 임의로 등록해줄 캐시 타입입니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이름과 최대 캐싱 개수를 Enum으로 만들어 담아두었고, 현재는 견종 목록만 만들어두었습니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CacheConfig에서 이 Enum을 하나씩 말아올려,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;캐시 매니저 빈을 등록하고, @EnableCaching을 통해 spring-mvc에서 캐싱을 적용하도록 해줍니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;캐시 적용할 메서드는 @Cacheable 어노테이션을 통해 적용해줄 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 어노테이션이 붙은 메서드가 처음 호출되고 난 뒤, 결과를 was단에서 캐싱해두어 다음 요청부터는 캐싱해둔 값이 나가게 됩니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;select * from breeds; 쿼리인 findAll() 메서드가 선언되어있는 Repository는 인터페이스기 때문에&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;default 접근제어자를 붙인 findAllBreeds() 메서드로 감싸주었습니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;캐시 미스는 @CacheEvict 어노테이션을 통해 적용해줄 수 있습니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 어노테이션이 붙은 메서드가 호출되면 같은 key를 가진 메서드들의 적용된 캐시가 무효화됩니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(추가로, cacheNames는 앞서 적용해둔 임의의 cacheType의 설정값을 가져옵니다)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;야무지게 캐싱을 적용해보았는데, 과연 성능상 이점은 생겼을까요?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dozaxP/btsAjR1BsGw/j1Pc5Sqkl67QA0OtnUJZEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dozaxP/btsAjR1BsGw/j1Pc5Sqkl67QA0OtnUJZEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dozaxP/btsAjR1BsGw/j1Pc5Sqkl67QA0OtnUJZEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdozaxP%2FbtsAjR1BsGw%2Fj1Pc5Sqkl67QA0OtnUJZEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;642&quot; height=&quot;301&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oQKev/btsAkrBB9ky/Ap9tS2AgLKa3oQlmIVaoy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oQKev/btsAkrBB9ky/Ap9tS2AgLKa3oQlmIVaoy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oQKev/btsAkrBB9ky/Ap9tS2AgLKa3oQlmIVaoy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoQKev%2FbtsAkrBB9ky%2FAp9tS2AgLKa3oQlmIVaoy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;631&quot; height=&quot;234&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;475&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로컬기준 각각&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;194ms -&amp;gt; 24ms&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;241ms -&amp;gt; 23ms&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로 유의미한 차이를 확인할 수 있었습니다 :D&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 배포 환경에서는 어떤 차이가 있었을까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;463&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRgF5K/btsAEnsqZsO/4MnHQ73rN2bn4Wu3I7e7t1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRgF5K/btsAEnsqZsO/4MnHQ73rN2bn4Wu3I7e7t1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRgF5K/btsAEnsqZsO/4MnHQ73rN2bn4Wu3I7e7t1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRgF5K%2FbtsAEnsqZsO%2F4MnHQ73rN2bn4Wu3I7e7t1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;463&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;463&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;747ms -&amp;gt; 26ms&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여러번 테스트 해본 결과 네트워크 환경 따라 20~40ms 왔다갔다 하는데요&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;약 2~30배로 성능을 개선할 수 있었습니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;나도 성능개선 해볼거야! 하면서 DB 인덱스 걸 것 찾아보고 그랬었는데요&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;성능개선의 가장 빠른 길은 그리 먼 곳에 있지 않았다는 사실을 새삼 깨닫게 되었습니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;드라마틱한 변화는 아니지만, 보는 시야가 넓어졌다고 생각하며 나아가보도록 하겠습니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;레퍼런스&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://loosie.tistory.com/806&quot;&gt;https://loosie.tistory.com/806&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@injoon2019/Caffeine-Cache-%EB%A1%9C%EC%BB%AC-%EC%BA%90%EC%8B%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0&quot;&gt;https://velog.io/@injoon2019/Caffeine-Cache-%EB%A1%9C%EC%BB%AC-%EC%BA%90%EC%8B%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gngsn.tistory.com/170&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://gngsn.tistory.com/170&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우아한테크코스 5기 후추&lt;/p&gt;</description>
      <category>공부기록/Spring</category>
      <author>iamjooon2</author>
      <guid isPermaLink="true">https://iamjooon2.tistory.com/57</guid>
      <comments>https://iamjooon2.tistory.com/57#entry57comment</comments>
      <pubDate>Mon, 13 Nov 2023 01:37:20 +0900</pubDate>
    </item>
    <item>
      <title>비관리 의존성을 트랜잭션에서 제외하기(feat 파사드 패턴)</title>
      <link>https://iamjooon2.tistory.com/55</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;집사의고민 프로젝트에서는 소셜 로그인을 사용하고는데요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 경험 측면에서 회원가입 과정이 간소해지고, 우리 사이트에서 관리해야할 사용자 정보 역시 줄어들어, 보안 측면에서도 이점이 있다고 생각했기 때문이었습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 소셜로그인 과정에서 문제가 되는 점이 하나 있었는데요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드와 함께 보자면&lt;/p&gt;
&lt;pre id=&quot;code_1696849113354&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public TokenDto login(String authCode) {
    // 외부 인프라 호출
    String accessToken = oAuthClient.getAccessToken(authCode);
    OAuthMemberResponse oAuthMemberResponse = oAuthClient.getMember(accessToken);

    Member member = memberRepository.findByEmail(oAuthMemberResponse.getEmail())
            .orElseGet(() -&amp;gt; memberRepository.save(oAuthMemberResponse.toMember()));

    return createTokens(member.getId());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 oAuthClinet 객체와 협력하는 두 부분입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;oAuthClinet는 외부 인증서버 + 자원서버를 추상화한 객체로, 해당 객체를 통해 외부 자원서버의 엑세스 토큰을 발급한 다음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 받아온 엑세스 토큰을 통해 사용자의 정보를 받아오는 역할을 합니다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이 부분이 우리 트랜잭션 안에 묶여있다는 점이 고민이었습니다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RealMySQL에는 이런 내용이 나옵니다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;메일전송이나 FTP 파일 전송 작업 또는 네트워크를 통해 원격 서버와 통신하는 등과 같은 작업은 어떻게 해서든 DBMS의 트랜잭션 내에서 제거하는 것이 좋다. 프로그램이 실행되는 동안 메일 서버와 통신할 수 없는 상황이 발생한다면 웹 서버뿐 아니라 DBMS서버까지 위험해지는 상황이 발생할 것이다 [Real MySQL 8.0, p159]&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 말하는 원격 서버와 통신하는 일 == 카카오 API 호출이었습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 카카오 API를 호출하는 부분은 우리 트랜잭션을 내부에 잡아둘 필요가 없습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API와 통신하는 동안 불필요하게 우리 트랜잭션을 불필요하게 점유할 뿐더러,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹여라도 외부 API에 장애가 발생하여 호출 시간이 길어진다면 불필요한 성능 저하를 일으킬 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원과 고민을 나누었는데, HTTP 요청의 영역이라고 볼 수도 있으니.. Controller 계층으로 옮기는 것은 어떻냐는 의견을 받았지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 생각한 Controller의 역할은 사용자의 입출력을 처리하고, 위는 로그인에 대한 비즈니스로직이라 Serivce의 영역이라 생각했습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국&amp;nbsp;&lt;i&gt;&lt;b&gt;파사드 패턴&lt;/b&gt;&lt;/i&gt;을 적용하여 해결하였습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파사드 패턴은, 하위 객체를 복잡도를 숨기기위해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용처에서 인터페이스를 하나 두는 디자인 패턴입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 전후 의존관계는 다음과 같아요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3501&quot; data-origin-height=&quot;2900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nXXn5/btsxp78dmuZ/bsXec1T9KCqVOqmqXmZBn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nXXn5/btsxp78dmuZ/bsXec1T9KCqVOqmqXmZBn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nXXn5/btsxp78dmuZ/bsXec1T9KCqVOqmqXmZBn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnXXn5%2Fbtsxp78dmuZ%2FbsXec1T9KCqVOqmqXmZBn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;687&quot; height=&quot;569&quot; data-origin-width=&quot;3501&quot; data-origin-height=&quot;2900&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 처음에는 실제 외부 RESTApi를 사용하는 RestTemplate에 i/o timeout를 거는 방식을 사용했습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 현실적으로 외부 인프라에 timeout을 얼마나 걸어야할지.. 잘 감이 오지 않았고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임의로 시간을 정해 timeout을 설정하였습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불확실한 기준에 의존하던 이전 방법보다, 확실하게 트랜잭션에서 제외시킨 지금이 더 좋은 방법이라는 확신이 듭니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용하고 난 코드는 다음과 같습니다&lt;/p&gt;
&lt;pre id=&quot;code_1696852251465&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AuthServiceFacade {

    private final AuthService authService;
    private final OAuthClient oAuthClient;

    public TokenDto login(String authCode) {
        String accessToken = oAuthClient.getAccessToken(authCode);
        OAuthMemberResponse oAuthMemberResponse = oAuthClient.getMember(accessToken);
        return authService.login(oAuthMemberResponse);
    }
    
 }
 
@Transactional 
public class AuthService {
   
    private final JwtProvider jwtProvider;
    private final MemberRepository memberRepository;
    private final RefreshTokenRepository refreshTokenRepository;
  
    public TokenDto login(OAuthMemberResponse oAuthMemberResponse) {
        Member member = memberRepository.findByEmail(oAuthMemberResponse.getEmail())
                .orElseGet(() -&amp;gt; memberRepository.save(oAuthMemberResponse.toMember()));
        return createTokens(member.getId());
    }
    
    private TokenDto createTokens(Long memberId) {
        String accessToken = jwtProvider.createAccessToken(memberId);
        String refreshToken = jwtProvider.createRefreshToken();

        refreshTokenRepository.deleteByMemberId(memberId);
        refreshTokenRepository.save(new RefreshToken(memberId, refreshToken));

        return TokenDto.of(accessToken, refreshToken);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파사드 패턴의 본래 사용 목적과는 다르게 사용하긴 했다..만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션을 필요한 부분에서만 적용되도록 하는 데에 성공했기에 만족합니다!&lt;/p&gt;</description>
      <author>iamjooon2</author>
      <guid isPermaLink="true">https://iamjooon2.tistory.com/55</guid>
      <comments>https://iamjooon2.tistory.com/55#entry55comment</comments>
      <pubDate>Mon, 9 Oct 2023 20:52:17 +0900</pubDate>
    </item>
    <item>
      <title>Java optional - OrElse, OrElseGet 알고쓰자</title>
      <link>https://iamjooon2.tistory.com/52</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;우아한테크코스 프로젝트를 진행하면서&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;java Optional의 orElse, orElseGet을 아주 제대로 배울 기회가 생겨 정리해본다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;후딱 계기가 된 코드부터 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1694858903216&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public String createToken(String authCode) {
        String accessToken = oAuthClient.getAccessToken(authCode);
        OAuthMemberResponse oAuthMemberResponse = oAuthClient.getMember(accessToken);

        Member member = memberRepository.findByEmail(oAuthMemberResponse.getEmail())
                .orElse(memberRepository.save(oAuthMemberResponse.toMember()));

        return jwtProvider.create(String.valueOf(member.getId()));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로그인하여 토큰을 발급하는 코드다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;간략하게 설명해보자면, 클라이언트로부터 받아온 인가코드를 바탕으로&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;oAuthClient 객체에게 자원서버로부터 accessToken을 받아오고,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다시 받아온 accessToken을 바탕으로 회원 정보를 얻어온다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;받아온 회원 정보중 이메일을 바탕으로 이미 존재하는 회원인지 확인하여&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;존재한다면 기존 회원 정보를 가져오고, 존재하지 않는다면 새로 가입한 후&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;회원의 식별자를 바탕으로 토큰을 발급한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;문제가 생겼던 부분은 기존 가입자가 다시 로그인이 안되는 것이었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로그인이 안되던 이유는, 데이터베이스의 이메일에 unique 조약을 위반한다는 이유....&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ni6UT/btsuk12Daq6/c6f3k2PHkTWCiavC2q7xG0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ni6UT/btsuk12Daq6/c6f3k2PHkTWCiavC2q7xG0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ni6UT/btsuk12Daq6/c6f3k2PHkTWCiavC2q7xG0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNi6UT%2Fbtsuk12Daq6%2Fc6f3k2PHkTWCiavC2q7xG0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;493&quot; height=&quot;317&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이유는 orElse를 잘 모르고 썼던 것이었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ci56r3/btsudZY7Bwl/4ZEIHdBNr4mIKmH26ugvdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ci56r3/btsudZY7Bwl/4ZEIHdBNr4mIKmH26ugvdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ci56r3/btsudZY7Bwl/4ZEIHdBNr4mIKmH26ugvdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fci56r3%2FbtsudZY7Bwl%2F4ZEIHdBNr4mIKmH26ugvdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;749&quot; height=&quot;270&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;378&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;orElse()의 경우, 매개변수로 T를 받아, 리턴값으로 T를 반환한다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;메서드 구현을 보면, 값이 null이 아니면 값을 반환하고, 아니라면 매개변수를 반환한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;나는 orElse()의 인자로 메서드를 넣어주었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때, 먼저 실행한 memberRepository.findByEmail()과는 상관없이&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;orElse에 넣었던 메서드가 먼저 실행이 되고(매개변수가 값인지라, 값을 구하기위해 메서드가 실행됨)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 email Unique 제약조건이 걸렸던 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그래서 어떻게 해결했냐고?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;orElse() 대신 orElseGet()을 사용했다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1694858903218&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public String createToken(String authCode) {
        String accessToken = oAuthClient.getAccessToken(authCode);
        OAuthMemberResponse oAuthMemberResponse = oAuthClient.getMember(accessToken);

        Member member = memberRepository.findByEmail(oAuthMemberResponse.getEmail())
                .orElseGet(() -&amp;gt; memberRepository.save(oAuthMemberResponse.toMember()));

        return jwtProvider.create(String.valueOf(member.getId()));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure style=&quot;color: #333333; text-align: center;&quot; data-ke-type=&quot;image&quot; data-ke-style=&quot;alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1262&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PKaq2/btsuemzOPLe/bwhKzqaBix0WRKs2WoulmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PKaq2/btsuemzOPLe/bwhKzqaBix0WRKs2WoulmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PKaq2/btsuemzOPLe/bwhKzqaBix0WRKs2WoulmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPKaq2%2FbtsuemzOPLe%2FbwhKzqaBix0WRKs2WoulmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1262&quot; height=&quot;354&quot; data-origin-width=&quot;1262&quot; data-origin-height=&quot;354&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;orElseGet()는 orElse() 와 달리 값이 아닌 supplier(매개변수를 받지 않고 리턴값을 반환)을 인자로 받는다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;값이 null이 아니면 값을 반환하고, null이면 Supplier의 get을 호출한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이로써, 앞선 memberRepository.findByEmail()의 결과에 따라&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이후 orElseGet을 실행시킬 수 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Optional의 orElse와 orElseGet을 잘 알고 쓰게 된 계기가 되었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;정리&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;orElse는 Optional이 값이 무엇이든, 항상 로직이 실행되고&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;orElseGet은 Optional의 값이 null일 때만 로직이 실행된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;잘 알고 쓰자&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고자료&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/java-optional-or-else-vs-or-else-get&quot;&gt;https://www.baeldung.com/java-optional-or-else-vs-or-else-get&lt;/a&gt;&lt;/p&gt;</description>
      <category>트러블슈팅</category>
      <author>iamjooon2</author>
      <guid isPermaLink="true">https://iamjooon2.tistory.com/52</guid>
      <comments>https://iamjooon2.tistory.com/52#entry52comment</comments>
      <pubDate>Sun, 20 Aug 2023 01:55:31 +0900</pubDate>
    </item>
    <item>
      <title>모킹 관련 어노테이션 정리 - @Mockito @Mock @MockBean @InjectMock</title>
      <link>https://iamjooon2.tistory.com/50</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;우아한테크코스 레벨 3 프로젝트를 진행하면서, 테스트코드 작성을 새로 배우고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 크루들과 테스트에 관한 이야기를 나눌 때 마다 스스로를 클래시스트라고 말했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API 사용 같이 '실제 객체를 테스트 할 수 없는 상황이 아니면, 연관된 모든 객체를 직접 테스트해야 그것이 진짜 테스트다'라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일일히 메서드들을 모킹할 때마다 가독성이 떨어지고, 테스트 해야하는 메서드 하나를 위해, 연관된 수많은 메서드를 짜맞추는 것이 말이 안맞는다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 난 레벨2 까지 Controller단은 &lt;a href=&quot;https://rest-assured.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RestAssured,&lt;/a&gt; Service와 Repository는 @SpringBootTest 어노테이션을 이용하여 테스트를 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이런 내 신조는 레벨 3가 되어 아주 보기좋게 깨지고 말았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서 소셜 로그인을 담당하게 되었고, '실제 객체를 테스트 할 수 없는 상황'이 나에게 벌어진 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드로부터 넘겨받은 카카오의 인가 코드를 받아 accessToken을 받아오는 메서드였는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 이 인가코드를 받는 과정이 웹에서 일일히 동의항목을 체크하고 로그인 버튼을 클릭한 후 redirectUri로 리다이렉트되는 것이었고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 인가코드는 한 번밖에 사용이 불가능하여... 임의로 받아둔 인가 코드를 테스트에 사용할 수도 없던 상황이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;즉, 모킹을 이용할 수 밖에 없는 상황이었다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 배우게 된 스프링에서 사용할 수 있는 모킹 관련 어노테이션을 정리하고자 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@Mock&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;mock 객체를 만든다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;실제 인스턴스 없이 가상의 Mock 인스턴스를 만들어 직접 사용할 수 있으며,&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mockito의 어노테이션이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@MockBean&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;Spring의 ApplicationContext에 실제 객체의 Bean이 아닌 MockBean을 등록시킬 수 있다&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@SpringBootTest을 사용하게 되면 ApplicationContext을 모두 로드하는데&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 @MockBean 어노테이션을 붙여두면, 실제 Bean이 아닌 Mock 객체를 ApplicationContext에 주입할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스프링 부트의 어노테이션이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;@InjectMocks&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mock 객체를 주입한다.&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mockito의 어노테이션이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@SpringBootTest 어노테이션을 통해 작성한 코드는 다음과 같다&lt;/p&gt;
&lt;pre id=&quot;code_1690987071265&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
@SpringBootTest
@SuppressWarnings(&quot;NonAsciiCharacters&quot;)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class AuthServiceTest {

    @MockBean
    private KakaoOAuthClient oAuthClient;

    @MockBean
    private JwtProvider jwtProvider;

    @MockBean
    private MemberRepository memberRepository;

    @Autowired
    private AuthService authService;

    @Test
    void 기존_멤버의_토큰을_발급한다() {
        // given
        카카오_토큰_받기_성공();
        Member 저장된_멤버 = 식별자_있는_멤버();
        when(memberRepository.findByEmail(&quot;이메일&quot;))
                .thenReturn(Optional.of(저장된_멤버));
        when(jwtProvider.create(String.valueOf(저장된_멤버.getId())))
                .thenReturn(&quot;생성된 토큰&quot;);

        // when
        String 토큰 = authService.createToken(&quot;코드&quot;);

        // then
        assertThat(토큰).isEqualTo(&quot;생성된 토큰&quot;);
    }

    @Test
    void 새로_가입한_멤버의_토큰을_발급한다() {
        // given
        카카오_토큰_받기_성공();
        when(memberRepository.findByEmail(&quot;이메일&quot;))
                .thenReturn(Optional.empty());
        when(memberRepository.save(식별자_없는_멤버()))
                .thenReturn(식별자_있는_멤버());

        when(jwtProvider.create(&quot;1&quot;))
                .thenReturn(&quot;생성된 토큰&quot;);

        // when
        String 토큰 = authService.createToken(&quot;코드&quot;);

        // then
        assertThat(토큰).isEqualTo(&quot;생성된 토큰&quot;);
    }

    private OAuthMemberResponse 카카오_토큰_받기_성공() {
        when(oAuthClient.getAccessToken(&quot;코드&quot;))
                .thenReturn(&quot;토큰&quot;);

        OAuthMemberResponse oAuthMemberResponse = 카카오_응답();
        when(oAuthClient.getMember(&quot;토큰&quot;))
                .thenReturn(카카오_응답());

        return oAuthMemberResponse;
    }

    private KakaoMemberResponse 카카오_응답() {
        return KakaoMemberResponse.builder().kakaoAccount(KakaoMemberResponse.KakaoAccount.builder()
                .email(&quot;이메일&quot;)
                .profile(KakaoMemberResponse.Profile.builder()
                        .nickname(&quot;이름&quot;)
                        .picture(&quot;사진&quot;)
                        .build())
                .build()).build();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시보니 코드가 정말 못났다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 @MockBean 어노테이션을 붙여준 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;i&gt;jwtProvider&lt;/i&gt;, &lt;i&gt;memberRepository&lt;/i&gt;, &lt;i&gt;oAuthClient&lt;/i&gt;는&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;실제 테스트시 authService와 협력할 객체들이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;@MockBean을 이용해 가짜 빈을 등록해두지 않으면, &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;실제 스프링 빈에 등록된 객체가 호출되기 때문에&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;/span&gt;&lt;/span&gt;모킹을 이용해 가짜 객체를 주입해준 후,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 메서드 내부에서 해당 객체들의 행동을 미리 가정을 해두는 방식으로 테스트를 진행할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1696509415430&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 메서드 호출과 반환값을 가정
when(targetClass.doSomething()).thenReturn(&quot;반환할 값&quot;);

// 메서드 호출시 예외를 가정
when(targetClass.doSomething()).thenThrow(new SomeException());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와같이 행동을 가정할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 when 메서드는 모두 Mockito를 static import한 메서드이고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출할 메서드와 반환할 값을 임의로 지정해둘 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10월 5일 추가...!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@SpringBootTest를 사용하지 않고는 다음과 같이 사용할 수 있다&lt;/p&gt;
&lt;pre id=&quot;code_1696510953042&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension.class)
@SuppressWarnings(&quot;NonAsciiCharacters&quot;)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class AuthServiceTest {

    @Mock
    private OAuthClient oAuthClient;

    @Mock
    private JwtProvider jwtProvider;

    @InjectMocks
    private AuthService authService;

    @Test
    void 기존_멤버의_토큰을_발급한다() {
        // given
        카카오_토큰_받기_성공();
        Member 저장된_멤버 = 식별자_있는_멤버();
        when(memberRepository.findByEmail(&quot;이메일&quot;))
                .thenReturn(Optional.of(저장된_멤버));
        when(jwtProvider.create(String.valueOf(저장된_멤버.getId())))
                .thenReturn(&quot;생성된 토큰&quot;);

        // when
        String 토큰 = authService.createToken(&quot;코드&quot;);

        // then
        assertThat(토큰).isEqualTo(&quot;생성된 토큰&quot;);
    }

    @Test
    void 새로_가입한_멤버의_토큰을_발급한다() {
        // given
        카카오_토큰_받기_성공();
        when(memberRepository.findByEmail(&quot;이메일&quot;))
                .thenReturn(Optional.empty());
        when(memberRepository.save(식별자_없는_멤버()))
                .thenReturn(식별자_있는_멤버());
                
        when(jwtProvider.create(&quot;1&quot;))
                .thenReturn(&quot;생성된 토큰&quot;);

        // when
        String 토큰 = authService.createToken(&quot;코드&quot;);

        // then
        assertThat(토큰).isEqualTo(&quot;생성된 토큰&quot;);
    }

    private OAuthMemberResponse 카카오_토큰_받기_성공() {
        when(oAuthClient.getAccessToken(&quot;코드&quot;))
                .thenReturn(&quot;토큰&quot;);

        OAuthMemberResponse oAuthMemberResponse = 카카오_응답();
        when(oAuthClient.getMember(&quot;토큰&quot;))
                .thenReturn(카카오_응답());

        return oAuthMemberResponse;
    }

    private KakaoMemberResponse 카카오_응답() {
        return KakaoMemberResponse.builder().kakaoAccount(KakaoMemberResponse.KakaoAccount.builder()
                .email(&quot;이메일&quot;)
                .profile(KakaoMemberResponse.Profile.builder()
                        .nickname(&quot;이름&quot;)
                        .picture(&quot;사진&quot;)
                        .build())
                .build()).build();
    }
 
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ExtendWith(MockitoExtension.class) 어노테이션을 클래스 상단에 붙여준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@MockBean은 스프링 어노테이션이지만, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;@Mock과 @InjectMock은&lt;/span&gt; Mockito의 어노테이션이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영어단어 그대로, MockitoExtension을 통해 확장시켜 사용하겠다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationContext를 띄울 이유가 없어서.. 현재는 위와 같은 방식으로 리팩터링 했다..!&lt;/p&gt;</description>
      <author>iamjooon2</author>
      <guid isPermaLink="true">https://iamjooon2.tistory.com/50</guid>
      <comments>https://iamjooon2.tistory.com/50#entry50comment</comments>
      <pubDate>Sun, 23 Jul 2023 23:27:39 +0900</pubDate>
    </item>
    <item>
      <title>집고팀 OAuth 로그인 적용기</title>
      <link>https://iamjooon2.tistory.com/49</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;집사의고민 프로젝트를 진행하며, 카카오 로그인 기능을 맡게 되었습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 접한 OAuth 관련 개념과 용어들이 블로그마다 혼재되어있는 경우가 많아서, 많이 헷갈렸습니다 &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 관련 개념을 제 언어로 정리하고, 구현하면서 가졌던 고민들을 정리해보려 합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 공대생의 절친 위키백과에서 OAuth라는 단어의 뜻부터 한번 알아봅시다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;OAuth(&quot;Open&amp;nbsp;Authorization&quot;)는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한&amp;nbsp;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EA%B0%9C%EB%B0%A9%ED%98%95_%ED%91%9C%EC%A4%80&quot;&gt;개방형 표준&lt;/a&gt;이다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네. 그렇다고 하네요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 언어로 정리해보면, 우리 서비스에서 다른 서비스의 정보를 사용할 수 있는 권한을 위임받는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 우리는 집사의고민에서 카카오의 사용자 정보를 쓸 수 있는 권한을 위임받아 로그인을 적용할겁니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 팀이 구성한 로그인 플로우는 다음과 같습니다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2369&quot; data-origin-height=&quot;2417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHQ6nw/btsuelt7gj2/soF0cLBDVCQPPDkc4ZEum0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHQ6nw/btsuelt7gj2/soF0cLBDVCQPPDkc4ZEum0/img.png&quot; data-alt=&quot;나는 확신의 백엔드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHQ6nw/btsuelt7gj2/soF0cLBDVCQPPDkc4ZEum0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHQ6nw%2Fbtsuelt7gj2%2FsoF0cLBDVCQPPDkc4ZEum0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;656&quot; height=&quot;669&quot; data-origin-width=&quot;2369&quot; data-origin-height=&quot;2417&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;나는 확신의 백엔드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공부하는 과정에서 OAuth 관련 포스트를 보면 플로우차트가 빠짐없이 나와서, 다른 방법으로 설명할 순 없을까 생각했는데요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;막상 정리하려고보니, 플로우차트만한게 없네요 &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최대한 쉽게 풀어서 설명해보겠습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Resource Owner&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Resource Owner는 사용자입니다. (이하 사용자로 작성)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Resource는 외부 플랫폼의 자원인데요, 즉 외부 서비스에 자신의 정보를 가지고 있는 사용자를 말해요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FrontEnd와 Backend는 우리 서비스입니다. 이 부분은 설명을 생략할게요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 포스트에서 Client라는 표현을 쓰기도 하는데, Resource 서버 입장에서 우리 서비스도 결국 고객이라 client라는 표현을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분 client라고 퉁쳐놓는 블로그와 달리, 우리 프로젝트의 로그인 적용기므로 frontend와 backend로 나누어보았습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Auth Server&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Autorization Server는 인증서버입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자로부터 받아온 아이디/비밀번호가 적합한지 검증하여 인가 코드를 발급하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인가코드를 받아 Resource server에 접근할 수 있는 토큰을 발급하는 역할을 해요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만일 적합하지 않다면, 401등의 에러를 반환하겠죠?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 카카오에 저장되어있는 사용자 정보를 가져오기때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Resource Owner는 결국 카카오 인증서버입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Resource Server&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Resource Server는 사용자의 정보를 가지고 있는 서버입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발급받은 토큰을 받아 검증한 다음, 사용자의 정보를 반환하는 역할을 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기도 결국 카카오인 셈입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 헷갈렸던 부분이 5~9의 플로우인데요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthServer에서 인가코드를 발급하고, 받은 인가코드를 발급받아 왜 다시 토큰을 발급받게 했을지?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 로그인 성공시 바로 토큰을 발급하게 해버리면 안될지?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 조금만 더 생각해봅시다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰이 클라이언트로 바로 발급됐고, 탈취당했다고 가정해볼게요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰을 탈취한 사용자는 자원서버로부터 사용자 정보를 계속 뽑아서 사용하겠죠?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 카카오 로그인의 경우 한번 발급받은 인가 코드로 최대 2번까지 토큰 발급 요청을 할 수 있었습니다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 이렇게 플로우를 구성하기까지 생긴 고민과, 그 선택 이유를 정리해보려 해요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 블로그의 OAuth 포스트에서 미세하게 플로우가 다른 점들이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대체로 아래 두가지였는데요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 2~3 흐름에 해당하는 사용자 로그인 요청을 각각 프론트엔드에서 백엔드 서버로 보낼지, Auth Server 직접 보낼지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 12~13에 해당하는 redirectURI를 프론트가 받을지, 백엔드가 받을지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번의 경우, &lt;b&gt;카카오 인증 바로 서버로 보내는 것을 선택했고&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번의 경우, &lt;b&gt;프론트엔드에서 redirectURI를 받는 것을 선택했습니다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 선택의 이유 모두, &lt;b&gt;사용자가 직접 인증 서버와 협력하는 데에는, 우리 백엔드 서버를 거칠 필요가 없다고 생각했어요&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 서버의 역할이 아닌, 사용자가 인증 서버로부터 실제 자원을 소유하고있는 사용자인지 검증해야 하는지라, 불필요하게 우리 서버와 통신할 필요는 없다고 생각했기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제 코드와 함께 살펴보겠습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 컨트롤러 부분입니다&lt;/p&gt;
&lt;pre id=&quot;code_1694857894625&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/auth&quot;)
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;
    private final JwtProvider jwtProvider;
    private final MemberQueryService memberQueryService;
    
    @PostMapping(&quot;/login&quot;)
    public ResponseEntity&amp;lt;TokenResponse&amp;gt; login(@RequestParam(&quot;code&quot;) String authCode) {
        String token = authService.createToken(authCode);
        String memberId = jwtProvider.getPayload(token);
        Member member = memberQueryService.findById(Long.valueOf(memberId));
        return ResponseEntity.ok(TokenResponse.of(token, member));
    }

    @GetMapping
    public ResponseEntity&amp;lt;AuthResponse&amp;gt; getMemberDetail(@Auth AuthCredentials authCredentials) {
        Member member = memberQueryService.findById(authCredentials.id());
        return ResponseEntity.ok(AuthResponse.from(member));
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드로부터 받아온 인가 코드를 받아 authService를 통해 토큰을 생성합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이후 토큰에 있는 정보로부터 추가로 사용자 정보를 찾아 반환합니다 (프론트엔드 요청건)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 Service 부분입니다&lt;/p&gt;
&lt;pre id=&quot;code_1694858011312&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@Transactional
@RequiredArgsConstructor
public class AuthService {

    private final OAuthClient oAuthClient;
    private final JwtProvider jwtProvider;
    private final MemberRepository memberRepository;

    public String createToken(String authCode) {
        String accessToken = oAuthClient.getAccessToken(authCode);
        OAuthMemberResponse oAuthMemberResponse = oAuthClient.getMember(accessToken);

        Member member = memberRepository.findByEmail(oAuthMemberResponse.getEmail())
                .orElseGet(() -&amp;gt; memberRepository.save(oAuthMemberResponse.toMember()));

        return jwtProvider.create(String.valueOf(member.getId()));
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;받은 인가코드를 바탕으로, 자원서버로부터 토큰을 발급받습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 받은 토큰을 바탕으로, 사용자 정보를 반환받습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 받은 사용자 정보를 바탕으로 이미 존재하는 회원인지 이메일로 찾은 후&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;존재한다면 조회하여 회원을 찾고, 존재하지 않는다면 저장 후 회원 정보를 가져오는 방입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이후, 사용자 식별자를 바탕으로 토큰을 발급하여 프론트엔드로 반환합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 OAuthClient입니다&lt;/p&gt;
&lt;pre id=&quot;code_1694858186197&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class KakaoOAuthClient implements OAuthClient {

    private static final String ACCESS_TOKEN_URI = &quot;https://kauth.kakao.com/oauth/token&quot;;
    private static final String USER_INFO_URI = &quot;https://kapi.kakao.com/v2/user/me&quot;;
    private static final String GRANT_TYPE = &quot;authorization_code&quot;;

    private final KakaoCredentials kakaoCredentials;
    private final RestTemplate restTemplate;

    @Override
    public String getAccessToken(String authCode) {
        HttpHeaders header = createRequestHeader();
        MultiValueMap&amp;lt;String, String&amp;gt; body = createRequestBodyWithAuthCode(authCode);
        HttpEntity&amp;lt;MultiValueMap&amp;lt;String, String&amp;gt;&amp;gt; request = new HttpEntity&amp;lt;&amp;gt;(body, header);

        ResponseEntity&amp;lt;KakaoTokenResponse&amp;gt; kakaoTokenResponse = getKakaoToken(request);

        return requireNonNull(requireNonNull(kakaoTokenResponse.getBody())).accessToken();
    }

    private HttpHeaders createRequestHeader() {
        HttpHeaders header = new HttpHeaders();
        header.setContentType(APPLICATION_FORM_URLENCODED);
        return header;
    }

    private MultiValueMap&amp;lt;String, String&amp;gt; createRequestBodyWithAuthCode(String authCode) {
        MultiValueMap&amp;lt;String, String&amp;gt; body = new LinkedMultiValueMap&amp;lt;&amp;gt;();
        body.add(&quot;grant_type&quot;, GRANT_TYPE);
        body.add(&quot;client_id&quot;, kakaoCredentials.getClientId());
        body.add(&quot;redirect_uri&quot;, kakaoCredentials.getRedirectUri());
        body.add(&quot;client_secret&quot;, kakaoCredentials.getClientSecret());
        body.add(&quot;code&quot;, authCode);
        return body;
    }

    private ResponseEntity&amp;lt;KakaoTokenResponse&amp;gt; getKakaoToken(HttpEntity&amp;lt;MultiValueMap&amp;lt;String, String&amp;gt;&amp;gt; request) {
            try {
                return restTemplate.exchange(
                    ACCESS_TOKEN_URI,
                    POST,
                    request,
                    KakaoTokenResponse.class
            );
        } catch (HttpClientErrorException e) {
            throw new AuthException.ResourceNotFound(&quot;카카오 토큰을 가져오는 데 실패했습니다.&quot;, e);
        }
    }

    @Override
    public OAuthMemberResponse getMember(String accessToken) {
        HttpEntity&amp;lt;HttpHeaders&amp;gt; request = createRequest(accessToken);

        ResponseEntity&amp;lt;KakaoMemberResponse&amp;gt; response = getKakaoMember(request);

        return response.getBody();
    }

    private HttpEntity&amp;lt;HttpHeaders&amp;gt; createRequest(String accessToken) {
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(accessToken);
        return new HttpEntity&amp;lt;&amp;gt;(headers);
    }

    private ResponseEntity&amp;lt;KakaoMemberResponse&amp;gt; getKakaoMember(HttpEntity&amp;lt;HttpHeaders&amp;gt; request) {
            try {
                return restTemplate.exchange(
                    USER_INFO_URI,
                    GET,
                    request,
                    KakaoMemberResponse.class
            );
        } catch (HttpClientErrorException e) {
            throw new AuthException.ResourceNotFound(&quot;카카오 사용자 정보를 가져오는 데 실패했습니다.&quot;, e);
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 기네요  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오 외의 네이버와 구글과 같은 다른 인증서버가 추가될수도 있다는 생각에 인터페이스화 했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체적인 OAuth 흐름이 토큰을 가져오는 것 한번, 사용자 정보를 가져오는 것 한번&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 두 번을 외부 서버에 요청을 보내야 하기에, getToken 메서드와 getMember 메서드를 만들어두었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 메서드의 KakaoOAuthclient에서의 구현은 위와 같아요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API 호출시 RestTemplate과 HttpClient라는 두개의 선택지 중 고민했는데요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpClient는 추가로 의존성을 설치해야 한다는 점과, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;비동기 처리가 가능하다는 점이 있지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;동기적으로 실행되야하는 플로우인지라&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이점을 별로 느끼지 못했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 조금 더 깔끔해진다고는 하는데, 추가로 의존성을 설치해야 될 정도는 아닌 것 같더라구요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestTemplate은 Bean으로 등록해두어, 자동으로 주입받을 수 있고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KakaoCredentials는 카카오 로그인 관련 정보들을 모아둔 놈입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1694858424691&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@RequiredArgsConstructor
@ConfigurationProperties(prefix = &quot;oauth.kakao&quot;)
public class KakaoCredentials {

    private final String clientId;
    private final String redirectUri;
    private final String clientSecret;

}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1694858470524&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// application.yml
oauth:
  kakao:
    client-id: ${CLIENT_ID}
    redirect-uri: ${REDIRECT_URI}
    client-secret: ${CLIENT_SECRET}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1694858529960&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootApplication
@EnableConfigurationProperties({KakaoCredentials.class, JwtCredentials.class})
public class ZipgoApplication {

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

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EnableConfigurationProperties 어노테이션을 통해,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yml 파일에 작성해둔 친구를 코드레벨로 편리하게 가지고 올 수 있습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 어렵게만 보이던 OAuth 인증을 적용해 본 결과를 정리해보았습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 처음 보고 플로우에 지레 겁먹던 저를 위한 글인데요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹여 보는 분들에게 도움이 되셧으면 좋겠습니다&lt;/p&gt;</description>
      <author>iamjooon2</author>
      <guid isPermaLink="true">https://iamjooon2.tistory.com/49</guid>
      <comments>https://iamjooon2.tistory.com/49#entry49comment</comments>
      <pubDate>Sun, 16 Jul 2023 23:12:14 +0900</pubDate>
    </item>
  </channel>
</rss>