<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>아둥바둥 버텨라</title>
    <link>https://mo-greene.tistory.com/</link>
    <description>그후에 날아가자</description>
    <language>ko</language>
    <pubDate>Fri, 10 Apr 2026 01:46:27 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Mo'Greene</managingEditor>
    <image>
      <title>아둥바둥 버텨라</title>
      <url>https://tistory1.daumcdn.net/tistory/5349579/attach/4338f0f2b6fc4a0b914efdd430c4c5b9</url>
      <link>https://mo-greene.tistory.com</link>
    </image>
    <item>
      <title>좋은 API Response 만들기 리뷰</title>
      <link>https://mo-greene.tistory.com/118</link>
      <description>&lt;p&gt;&lt;a href=&quot;https://jojoldu.tistory.com/720&quot;&gt;좋은 API Response 만들기&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;그래야하지 않을까? 싶었지만 하지 않았던 습관들에 대해 지적해주는 글이 아닌가 싶다.&lt;/p&gt;
&lt;h2&gt;최소 스펙 유지하기&lt;/h2&gt;
&lt;p&gt;가능한 &lt;code&gt;API 크기를 최소화&lt;/code&gt;  한다.&lt;br&gt;즉, DTO를 중복적으로 사용하지 않는게 중요한 듯 하다.&lt;br&gt;회사에서 레거시한 프로젝트들을 관리하다 보면 하나의 통합적인 DTO(예를들면 상점 도메인에 사용하는 모든 데이터(!!)를 적어둔 ShopDTO)를 만들어 놓고&lt;br&gt;중복적으로 사용하며 Response에는 null 값이 숭숭 뚫려있는 프로젝트들이 있었다.&lt;/p&gt;
&lt;p&gt;이게 참 백엔드 개발자로써는 중복된 코드를 지양하고(!) 객체지향인것(!) 싶지만&lt;br&gt;단순히 레이어 간의 파라미터의 역할을 하는 객체를 객체지향 코드로 격상시켜 버린 것이 아닐까 싶다.(그리고 이런 response api를 받으면 프론트 개발자 분들이 쌍욕한다ㅋㅋ)&lt;/p&gt;
&lt;p&gt;나는 메서드 단위의 ResponseDTO를 만들어 사용하고 있다.&lt;br&gt;아무리 중복되는 인자가 있어도 DTO를 2개 이상의 메서드에 반환하는 일은 최소한으로 해둔다.(뭐 정말 똑같은 경우는 어쩔 수 없지만)&lt;br&gt;DTO 내부의 innerClass 도 지양하고 있다. 하나의 클래스가 너무 커지게 되고 DTO를 부를때도 한줄에 너무 길게 적히다 보니 가독성이 안좋아지는 코드를 많이 보게 되었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        MemberDTO.MemberXXXResponse response = MemberRepository.findBy....&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;h2&gt;camelCase 사용하기&lt;/h2&gt;
&lt;p&gt;문제없다.&lt;/p&gt;
&lt;br&gt;

&lt;h2&gt;표준 포맷 사용하기&lt;/h2&gt;
&lt;p&gt;사용자에 노출되는 데이터에는 날짜데이터를 포맷팅 하거나 Date 클래스로 전달하였지만&lt;br&gt;사용자에게 노출이 안되는 데이터들(jwt 토큰의 만료시간 등등)에는 UNIX 타임스탬프를 사용한 적이 있었던 것 같다.&lt;br&gt;이 점도 고치도록 하자.&lt;/p&gt;
&lt;br&gt;

&lt;h2&gt;Boolean 타입에는 &lt;code&gt;null&lt;/code&gt; 이 있으면 안된다.&lt;/h2&gt;
&lt;p&gt;정말 부끄럽지만 Boolean의 null 값을 하나의 상태값으로 사용한 적이 있다.&lt;br&gt;참과 거짓의 열거형인데 이것을 &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt; or &lt;code&gt;null&lt;/code&gt; 으로 사용하였다.&lt;br&gt;블로그에서도 각각 의미가 있다면 YES, NO, UNKNOWN 으로 표현하는 것을 권장한다.&lt;/p&gt;
&lt;br&gt;

&lt;h2&gt;제한된 문자열 값은 열거형 (Enum) 으로 표현한다.&lt;/h2&gt;
&lt;p&gt;간혹 시간에 쫓겨 열거형을 사용해야함에도 불구하고 String 으로 DB를 설계하는 경우가 있는데 이런 부분도 신경쓰면서 사용하자.&lt;/p&gt;
&lt;br&gt;

&lt;h2&gt;열거형 (Enum)은 문자열로 표현되어야 한다.&lt;/h2&gt;
&lt;p&gt;Entity를 설계할 때 @Enumerated(EnumType.STRING) 어노테이션을 사용하여 관리하고 있다.&lt;/p&gt;
&lt;br&gt;

&lt;h2&gt;복수형 빈 값은 빈 배열로&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;단일 객체에 Null Object Pattern을 대응 하는 것은 기존 시스템의 상황에 따라 기본값을 구현하기 어렵거나 Null 자체가 유의미한 경우가 많아 상황에 따라 선택한다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;복수형 객체를 반환할때 null 일 경우 빈 배열로 전달하도록 하자. 생각도 못해봤다.&lt;/p&gt;
&lt;br&gt;

&lt;h2&gt;일관성 유지하기&lt;/h2&gt;
&lt;p&gt;결국 네이밍의 중요성인데, 아직까지 네이밍을 구분하기 쉽게 잘(?) 짓는다고 할 수 없어서 항상 생각하면서 네이밍을 짓도록 하자.&lt;/p&gt;
&lt;br&gt;

&lt;h2&gt;필드명 축약 금지, 타입에 맞는 필드명&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;필드명을 축약하는 것이 버릇이 된다면 나중가면 본인만 알고 전혀 의미를 알 수 없기 때문에 고치도록 하자&lt;/li&gt;
&lt;li&gt;Boolean 타입이라면 &lt;code&gt;is&lt;/code&gt;, &lt;code&gt;has&lt;/code&gt; 등을 prefix로&lt;/li&gt;
&lt;li&gt;일시 타입이라면 &lt;code&gt;At&lt;/code&gt; 을 suffix로&lt;/li&gt;
&lt;li&gt;복수형은 복수형으로&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;우리 회사의 경우 At을 사용하지 않고 date를 사용하고 있다.(At은 올드해보인다는 말 때문에..)&lt;br&gt;복수형의 경우는 복수형도 있지만 xxxList 등의 방식으로 반환하고 있다.&lt;/p&gt;</description>
      <category>사견/노트</category>
      <author>Mo'Greene</author>
      <guid isPermaLink="true">https://mo-greene.tistory.com/118</guid>
      <comments>https://mo-greene.tistory.com/118#entry118comment</comments>
      <pubDate>Tue, 23 Jan 2024 09:57:42 +0900</pubDate>
    </item>
    <item>
      <title>MoGreene Code Convention</title>
      <link>https://mo-greene.tistory.com/117</link>
      <description>&lt;ol&gt;
&lt;li&gt;&lt;p&gt;서비스 코드에 Response 등 응답객체 반환 금지&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;테스트 코드 작성시 진행하기 어려운 부분이 있음&lt;/li&gt;
&lt;li&gt;반환타입에 따라 Controller에서 응답객체로 감싸기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;테스트 코드 작성 시 Controller는 Mock테스트, 그 외 Junit 단위테스트로&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;패키지 구조는 &amp;#39;api&amp;#39;와 &amp;#39;global&amp;#39;로 나눈다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;api의 패키지는 도메인별로 나눈다.(기능별 x)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;exception의 경우 global 패키지 내의 도메인으로 나눈다.(팀 컨벤션에 따라 변경)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;디자인패턴을 적용시켜야 할 경우 Service 패키지를 작성하여 그안에 보관한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;api 패키지 내에 특정 도메인만 사용하게 되는 util 패키지를 만들지 않는다.(생길 경우 디자인패턴으로 보관)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;util의 경우 특정 도메인의 컴포넌트를 DI 하지 않는다. (ex: private final MemberRepository(X))&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;dto패키지 안에는 request, response을 패키지로 나누고 어플리케이션 내부에서 사용할 dto의 경우 dto패키지 안에 넣도록 하자&lt;br&gt;ex)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dto
ㄴ request
     ㄴ XXXRequest
ㄴ response
     ㄴ XXXResponse
ㄴ XXXDTO&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;메서드 명으로 get, set 등의 prefix를 사용하지 않는다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;노출되어선 안될 내용이 없다면 Entity로 반환해도 문제없다.(ex: 약관 등 간략한 도메인)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;JPA&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;join 및 단방향관계, 양방향관계는 매우 신중하게 결정(특히 삭제되는 데이터를 주의하면서 진행) 애지간하면 join을 걸지 않고 queryDsl을 사용해서 사용한다.&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.inflearn.com/questions/39769/%EB%B6%80%EB%AA%A8-%EC%9E%90%EC%8B%9D%EA%B4%80%EA%B3%84%EC%97%90%EC%84%9C-%EB%B6%80%EB%AA%A8-%EC%82%AD%EC%A0%9C%EC%8B%9C-set-null%EB%B0%A9%EB%B2%95%EC%97%90-%EB%8C%80%ED%95%B4%EA%B6%81%EA%B8%88%ED%95%A9%EB%8B%88%EB%8B%A4&quot;&gt;유용한 답변&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;@OneToOne, @ManyToMany는 사용금지&lt;/li&gt;
&lt;li&gt;양방향관계 금지&lt;/li&gt;
&lt;li&gt;단방향일 경우&lt;br&gt;연관된 두 객체의 라이프사이클이 동일할 경우에만 @ManyToOne&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>사견/노트</category>
      <category>컨벤션</category>
      <author>Mo'Greene</author>
      <guid isPermaLink="true">https://mo-greene.tistory.com/117</guid>
      <comments>https://mo-greene.tistory.com/117#entry117comment</comments>
      <pubDate>Tue, 2 Jan 2024 11:28:08 +0900</pubDate>
    </item>
    <item>
      <title>자바 플레이그라운드 with TDD, 클린코드 - 문자열계산기</title>
      <link>https://mo-greene.tistory.com/116</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;- 천천히 하더라도 꼭 완주&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 기존 구성에 따라, 내 힘으로 구현 -&amp;gt; 피드백 확인 -&amp;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;객체지향 생활체조원칙&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #555555; text-align: left;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;한 메서드에 오직 한 단계의 들여쓰기만 한다.&lt;/li&gt;
&lt;li&gt;else 예약어를 쓰지 않는다.&lt;/li&gt;
&lt;li&gt;모든 원시 값과 문자열을 포장한다.&lt;/li&gt;
&lt;li&gt;한 줄에 점을 하나만 찍는다.&lt;/li&gt;
&lt;li&gt;줄여 쓰지 않는다(축약 금지).&lt;/li&gt;
&lt;li&gt;모든 엔티티를 작게 유지한다.&lt;/li&gt;
&lt;li&gt;3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.&lt;/li&gt;
&lt;li&gt;일급 컬렉션을 쓴다.&lt;/li&gt;
&lt;li&gt;getter/setter/프로퍼티를 쓰지 않는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TEST 코드&lt;/p&gt;
&lt;pre id=&quot;code_1703405643946&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;..test코드
public class StringCalcTest {  
  
    @BeforeEach  
    public void setUp() {  
       String polynomial = &quot;2 / 2 * 4 - 100 + 207&quot;;  
       System.setIn(new ByteArrayInputStream(polynomial.getBytes()));  
    }  
    @Test  
    @DisplayName(&quot;문자열 계산기&quot;)  
    public void stringCalcTest() {  
       String output = inputString();  
       String[] values = output.split(&quot; &quot;);  
       StringCalculator stringCalculator = new StringCalculator(values);  
       int result = stringCalculator.calculate();  
  
       assertThat(result).isEqualTo(111);  
    }  
    private String inputString() {  
       Scanner sc = new Scanner(System.in);  
       return sc.nextLine();  
    }}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초 TDD 구현&lt;/p&gt;
&lt;pre id=&quot;code_1703405701345&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;..초기 testcode -&amp;gt; 구현만 성공
public class StringCalculator {  
    private String[] values;  
  
    public StringCalculator(String[] values) {  
       this.values = values;  
    }  
    public int calculate() {  
       return 5;  
    }}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리팩토링 후 구현&lt;/p&gt;
&lt;pre id=&quot;code_1703405761311&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class StringCalculator {
	private String[] values;

	public StringCalculator(String[] values) {
		this.values = values;
	}

	/**
	 * 문자열 계산
	 * @return int
	 */
	public int calculate() {
		int result = Integer.parseInt(values[0]);
		for (int i = 1; i &amp;lt; values.length; i += 2) {
			int nextNum = Integer.parseInt(values[i + 1]);
			result = switchCalculation(values[i], result, nextNum);
		}
		return result;
	}

	/**
	 * 계산식
	 * @param value 기호
	 * @param result 결과
	 * @param nextNum 뒤에 번호
	 * @return int
	 */
	private int switchCalculation(String value, int result, int nextNum) {
		switch (value) {
			case &quot;+&quot; : return add(result, nextNum);
			case &quot;-&quot; : return subtract(result, nextNum);
			case &quot;*&quot; : return multiply(result, nextNum);
			case &quot;/&quot; : return divide(result, nextNum);
			default: throw new RuntimeException(&quot;잘못된 기호를 적었습니다.&quot;);
		}
	}

	/**
	 * 덧셈
	 * @param num 기존 값
	 * @param plusNum 더할 값
	 * @return int
	 */
	private int add(int num, int plusNum) {
		return num + plusNum;
	}

	/**
	 * 뺄셈
	 * @param num 기존 값
	 * @param minusNum 뺄 값
	 * @return int
	 */
	private int subtract(int num, int minusNum) {
		return num - minusNum;
	}

	/**
	 * 곱셈
	 * @param num 기존 값
	 * @param multiplyNum 곱할 값
	 * @return int
	 */
	private int multiply(int num, int multiplyNum) {
		return num * multiplyNum;
	}

	/**
	 * 자바 나눗셈
	 * @param num 기존 값
	 * @param divideNum 나눌 값
	 * @return int
	 */
	private int divide(int num, int divideNum) {
		return num / divideNum;
	}
}&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;&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-filename=&quot;스크린샷 2023-12-24 오후 5.17.05.png&quot; data-origin-width=&quot;1924&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LPHGj/btsCAhXR85v/9EN9SX0gbEyhpESVbCNHtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LPHGj/btsCAhXR85v/9EN9SX0gbEyhpESVbCNHtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LPHGj/btsCAhXR85v/9EN9SX0gbEyhpESVbCNHtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLPHGj%2FbtsCAhXR85v%2F9EN9SX0gbEyhpESVbCNHtK%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;1924&quot; height=&quot;408&quot; data-filename=&quot;스크린샷 2023-12-24 오후 5.17.05.png&quot; data-origin-width=&quot;1924&quot; data-origin-height=&quot;408&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;&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;switchCalculation의 파라미터가 3개이상인 부분..&lt;/p&gt;</description>
      <category>사견/노트</category>
      <category>java</category>
      <category>java 플레이그라운드</category>
      <category>NextSTEP</category>
      <author>Mo'Greene</author>
      <guid isPermaLink="true">https://mo-greene.tistory.com/116</guid>
      <comments>https://mo-greene.tistory.com/116#entry116comment</comments>
      <pubDate>Sun, 24 Dec 2023 17:21:11 +0900</pubDate>
    </item>
    <item>
      <title>디자인패턴에 대해서</title>
      <link>https://mo-greene.tistory.com/115</link>
      <description>&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;평소에 나는 디자인패턴에 대해서 회의적인 느낌을 받았다.&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;/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;u&gt;&lt;b&gt; &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;@ControllerAdvice, @RestControllerAdvice&lt;/span&gt; &lt;/b&gt;&lt;/u&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;스프링부트에서 사용하는 'MVC패턴'이나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller-Service-Repository의 레이어가 위의 역할을 대신해서 동작할 것이다.라고 생각해 왔다.&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;&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;코드를 작성하다 보면 분명히 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복된 코드 혹은 비슷한 기능을 가지고 있는데 파라미터만 다르게 구현된 메서드,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service 코드에서 비즈니스 로직을 전부 구현하다 보니 가독성이 심히 안 좋아진 코드,&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;만약 그렇다면 패키지 내에 Util 클래스를 분리해서 작성해 볼까 싶었지만&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;style2&quot;&gt;&amp;nbsp;Util 클래스가 특정 Service 혹은 Repository를 주입받아야 된다면 그건 Util 클래스가 아니게 된다.&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;이것에 관해 고민하다 보니 구현하게 된 게 '디자인패턴'이다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;전략패턴 유튜브&quot; href=&quot;https://www.youtube.com/watch?v=31wI-c7ExiQ&amp;amp;ab_channel=%ED%95%98%EC%B0%AE%EC%9D%80%EC%98%A4%ED%9B%84&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.youtube.com/watch?v=31wI-c7ExiQ&amp;amp;ab_channel=%ED%95%98%EC%B0%AE%EC%9D%80%EC%98%A4%ED%9B%84&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=31wI-c7ExiQ&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/bniT8L/hyUL3mdvXu/B0YajIvyHSaFov1uPkRsXk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=922_362_1070_522&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/31wI-c7ExiQ&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&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;스프링부트에서 구현할 수 있는 디자인패턴을 생각하다가 위 유튜버를 보고 내 코드에도 적용해 보자 싶었다.&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;Untitled Diagram.png&quot; data-origin-width=&quot;481&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mFNVz/btsBOiJm580/xt4fsxNNP8io9HMYEtq9H0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mFNVz/btsBOiJm580/xt4fsxNNP8io9HMYEtq9H0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mFNVz/btsBOiJm580/xt4fsxNNP8io9HMYEtq9H0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmFNVz%2FbtsBOiJm580%2Fxt4fsxNNP8io9HMYEtq9H0%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;481&quot; height=&quot;178&quot; data-filename=&quot;Untitled Diagram.png&quot; data-origin-width=&quot;481&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&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;일반결제와 결제연장의 로직은 PaymentStatus라는 Enum으로 분류된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 부분을 PaymentStrategy 인터페이스를 사용하여 분리해 주었다.&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;PaymentStatus&lt;/p&gt;
&lt;pre id=&quot;code_1702252802065&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@RequiredArgsConstructor
public enum PaymentStatus {

    CONFIRM(&quot;confirmStrategy&quot;),
    EXTENDS(&quot;extendStrategy&quot;);
    
    //value값의 부분은 전략패턴 구현체의 이름을 camelCase로 적어준다.
    private final String value;

}&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;pre id=&quot;code_1702253033355&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//전략패턴 핸들러 인터페이스
public interface PaymentStrategy {

    /**
     * 결제 승인 핸들러
     */
    void paymentHandler(PaymentTemporal paymentTemporal, PaymentSuccessResponse paymentSuccessResponse);

}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1702253116455&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 ConfirmStrategy implements PaymentStrategy {

	...

    /**
     * 결제 승인 핸들러
     * @param paymentTemporal 임시 결제 테이블
     * @param paymentSuccessResponse 결제 성공 response
     */
    @Override
    public void paymentHandler(PaymentTemporal paymentTemporal, PaymentSuccessResponse paymentSuccessResponse) {
    
    	...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1702253176577&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 ExtendStrategy implements PaymentStrategy {

	...
        
    /**
     * 연장 결제 승인 핸들러
     * @param paymentTemporal 임시 결제 테이블
     * @param paymentSuccessResponse 결제 성공 response
     */
    @Override
    public void paymentHandler(PaymentTemporal paymentTemporal, PaymentSuccessResponse paymentSuccessResponse) {
        
        ...
    }
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 코드에선&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1702252570398&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class PaymentService {

	private final Map&amp;lt;String, PaymentStrategy&amp;gt; paymentStrategyMap;
    	...

	/**
	 * 결제 성공시 상점 테이블 저장 후 리다이렉트
	 */
	private void paymentSuccessRedirectView(HttpURLConnection connection, PaymentTemporal paymentTemporal, RedirectView redirectView, PaymentStatus paymentStatus) throws IOException {
    
    		...
        
        	//strategy Payment
		PaymentStrategy paymentStrategy = paymentStrategyMap.get(paymentStatus.getValue());
		paymentStrategy.paymentHandler(paymentTemporal, paymentResponse);
        
        	...
	}
    
    ...
}&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;전략패턴을 구현하는 paymentStrategyMap을 구현해 주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 재밌는 게 PaymentStrategy를 구현한 구현체를 @Component로 컨테이너에 등록시키고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 방식으로 paymentStatus.getValue()를 통해 Componenet를 불러주니 정상적으로 동작하였다.&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;만약 이 방식이 아니었다면 if/else 문으로 분리해 가면서 어떤 구현체를 가져올지 정해야 하는데&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;그렇게 if문이 생기게 되면 전략패턴을 사용하는 이유가 없어지게 된다.&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;만약 결제에 관한 추가적인 로직이 필요하다면 PaymentService에서 코드를 늘려 가독성을 해치는 것이 아닌&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;(OCP 원칙을 지키게 된 코드가 아닐까 싶다?)&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;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;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;굳이&lt;span&gt;&amp;nbsp;&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;충분히 깔끔한 코드를 객체지향적으로 구현하게 되지 않을까 싶다.&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;span style=&quot;background-color: #ffc1c8;&quot;&gt;확장가능성과 성능의 고민하는 경계선&lt;/span&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;&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;&lt;a href=&quot;https://lsj31404.tistory.com/entry/Spring-%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%98-DI%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%A0%84%EB%9E%B5%ED%8C%A8%ED%84%B4-%EB%8F%84%EC%9E%85&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://lsj31404.tistory.com/entry/Spring-%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%98-DI%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%A0%84%EB%9E%B5%ED%8C%A8%ED%84%B4-%EB%8F%84%EC%9E%85&lt;/a&gt;&lt;/p&gt;</description>
      <category>Backend/SpringBoot</category>
      <category>디자인패턴</category>
      <category>스프링부트</category>
      <category>전략패턴</category>
      <author>Mo'Greene</author>
      <guid isPermaLink="true">https://mo-greene.tistory.com/115</guid>
      <comments>https://mo-greene.tistory.com/115#entry115comment</comments>
      <pubDate>Mon, 11 Dec 2023 09:22:34 +0900</pubDate>
    </item>
    <item>
      <title>토스페이먼츠 결제모듈 구현</title>
      <link>https://mo-greene.tistory.com/114</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;당연하지만 남의 돈을 다룬다고 하는 것에 작은 죄책감(?) 때문인지도 모르겠다.&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;PG 결제모듈은 여러 회사가 있지만 그 중에 토스페이먼츠를 지정해주었다.&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://developers.tosspayments.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developers.tosspayments.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1701368853979&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;토스페이먼츠 개발자센터&quot; data-og-description=&quot;토스페이먼츠 결제 연동 문서, API, 키, 테스트 내역, 웹훅 등록 등 개발에 필요한 정보와 기능을 확인해 보세요. 결제 연동에 필요한 모든 개발자 도구를 제공해 드립니다.&quot; data-og-host=&quot;developers.tosspayments.com&quot; data-og-source-url=&quot;https://developers.tosspayments.com/&quot; data-og-url=&quot;https://developers.tosspayments.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dojEXz/hyUE4l61w9/JwiPPvwoQNXhogETj3p7C0/img.png?width=780&amp;amp;height=390&amp;amp;face=0_0_780_390,https://scrap.kakaocdn.net/dn/g3uGk/hyUE047Ho8/nGS7Meh54nVKCrKuCXu7P1/img.png?width=1860&amp;amp;height=1200&amp;amp;face=0_0_1860_1200&quot;&gt;&lt;a href=&quot;https://developers.tosspayments.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.tosspayments.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dojEXz/hyUE4l61w9/JwiPPvwoQNXhogETj3p7C0/img.png?width=780&amp;amp;height=390&amp;amp;face=0_0_780_390,https://scrap.kakaocdn.net/dn/g3uGk/hyUE047Ho8/nGS7Meh54nVKCrKuCXu7P1/img.png?width=1860&amp;amp;height=1200&amp;amp;face=0_0_1860_1200');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;토스페이먼츠 개발자센터&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;토스페이먼츠 결제 연동 문서, API, 키, 테스트 내역, 웹훅 등록 등 개발에 필요한 정보와 기능을 확인해 보세요. 결제 연동에 필요한 모든 개발자 도구를 제공해 드립니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.tosspayments.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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분 연동 가이드만 보아도 굉장히 쉽게 구현되어있었고 api나 샘플 코드들이 참고하기 쉬웠다는게 마음에 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;토스페이먼츠 SampleCode github&quot; href=&quot;https://github.com/tosspayments/payment-samples/tree/main/payment-window&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/tosspayments/payment-samples/tree/main/payment-window&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://tosspayments.discourse.group/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://tosspayments.discourse.group/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1701410574334&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;토스페이먼츠 디스코드 Repo&quot; data-og-description=&quot;토스페이먼츠 디스코드 채널에 남겨진 많은 질문을 손쉽게 검색 및 확인하실 수 있습니다.&quot; data-og-host=&quot;tosspayments.discourse.group&quot; data-og-source-url=&quot;https://tosspayments.discourse.group/&quot; data-og-url=&quot;https://tosspayments.discourse.group/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/oCpT9/hyUE2hH25N/a8omwcz9OTitkSLWFnPwK0/img.png?width=640&amp;amp;height=640&amp;amp;face=0_0_640_640,https://scrap.kakaocdn.net/dn/pTDKK/hyUE5FvcsD/1TknbgPSFN4NYQI9lEz8w1/img.png?width=2773&amp;amp;height=1561&amp;amp;face=0_0_2773_1561&quot;&gt;&lt;a href=&quot;https://tosspayments.discourse.group/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tosspayments.discourse.group/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/oCpT9/hyUE2hH25N/a8omwcz9OTitkSLWFnPwK0/img.png?width=640&amp;amp;height=640&amp;amp;face=0_0_640_640,https://scrap.kakaocdn.net/dn/pTDKK/hyUE5FvcsD/1TknbgPSFN4NYQI9lEz8w1/img.png?width=2773&amp;amp;height=1561&amp;amp;face=0_0_2773_1561');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;토스페이먼츠 디스코드 Repo&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;토스페이먼츠 디스코드 채널에 남겨진 많은 질문을 손쉽게 검색 및 확인하실 수 있습니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tosspayments.discourse.group&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;&lt;b&gt;결제 승인처리 flow&lt;/b&gt;&lt;/i&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;결제 트랜젝션.png&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7ntYA/btsA8O5eKlu/t0lCvJ77lmwbkl85Y6ZgB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7ntYA/btsA8O5eKlu/t0lCvJ77lmwbkl85Y6ZgB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7ntYA/btsA8O5eKlu/t0lCvJ77lmwbkl85Y6ZgB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7ntYA%2FbtsA8O5eKlu%2Ft0lCvJ77lmwbkl85Y6ZgB0%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;795&quot; height=&quot;442&quot; data-filename=&quot;결제 트랜젝션.png&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&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;blockquote data-ke-style=&quot;style2&quot;&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;결제 상품 테이블은&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 35.3489%; height: 168px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;id&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;amount&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;item_name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: right; height: 17px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: right; height: 17px;&quot;&gt;5000&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: right; height: 17px;&quot;&gt;상품1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: right; height: 17px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: right; height: 17px;&quot;&gt;10000&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: right; height: 17px;&quot;&gt;상품2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: right; height: 17px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: right; height: 17px;&quot;&gt;15000&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: right; height: 17px;&quot;&gt;상품3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: right; height: 17px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: right; height: 17px;&quot;&gt;100&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: right; height: 17px;&quot;&gt;테스트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;&amp;nbsp;&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;결제 상품, 사용자 정보&amp;nbsp; '임시 결제 테이블' 저장&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;그래서 아무런 정보가 들어가도 상관없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 필수적으로 결제 승인에 필요하고 동일한 상품인지 검증하는 'orderId' 를 만들어 줄 필요가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkb7vN/btsBiXyZ8oC/yVtfKTQezUY71ZuaSXbcDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkb7vN/btsBiXyZ8oC/yVtfKTQezUY71ZuaSXbcDK/img.png&quot; data-alt=&quot;결제승인 요청시 필수값 orderId&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkb7vN/btsBiXyZ8oC/yVtfKTQezUY71ZuaSXbcDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbkb7vN%2FbtsBiXyZ8oC%2FyVtfKTQezUY71ZuaSXbcDK%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;426&quot; height=&quot;660&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;660&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;결제승인 요청시 필수값 orderId&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;어떤 값을 넣어야 하나 싶다가 UUID가 생각났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맨처음엔 UUID가 128자인 줄 알아서 고려하지 않았는데 36자 문자열에 128비트 값이라고 하더라ㅋ&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;무튼, 임시결제테이블엔 orderId를 시작으로 추후 결제테이블에서 검증할 회원 정보, 상품정보 등을 넣게 되었다.&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&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;Front에선 결제모듈을 호출하고 결제를 진행하고(은행앱의 결제과정)&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_1701412342653&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    let tossPayments = TossPayments(&quot;test_ck_OEP59LybZ8Bdv6A1JxkV6GYo7pRe&quot;);

    function pay(method, requestJson) {
        console.log(requestJson);
        tossPayments.requestPayment(method, requestJson)
            .catch(function (error) {

                if (error.code === &quot;USER_CANCEL&quot;) {
                    alert('유저가 취소했습니다.');
                } else {
                    alert(error.message);
                }

            });
    }

    let path = &quot;/&quot;;
    let successUrl = window.location.origin + path + &quot;success&quot;;
    let failUrl = window.location.origin + path + &quot;fail&quot;;
    let callbackUrl = window.location.origin + path + &quot;va_callback&quot;;
    let orderId = new Date().getTime();

    let jsons = {
        &quot;card&quot;: {
            &quot;amount&quot;: amount,
            &quot;orderId&quot;: &quot;sample-&quot; + orderId,
            &quot;orderName&quot;: &quot;토스 티셔츠 외 2건&quot;,
            &quot;successUrl&quot;: successUrl,
            &quot;failUrl&quot;: failUrl,
            &quot;cardCompany&quot;: null,
            &quot;cardInstallmentPlan&quot;: null,
            &quot;maxCardInstallmentPlan&quot;: null,
            &quot;useCardPoint&quot;: false,
            &quot;customerName&quot;: &quot;박토스&quot;,
            &quot;customerEmail&quot;: null,
            &quot;customerMobilePhone&quot;: null,
            &quot;taxFreeAmount&quot;: null,
            &quot;useInternationalCardOnly&quot;: false,
            &quot;flowMode&quot;: &quot;DEFAULT&quot;,
            &quot;discountCode&quot;: null,
            &quot;appScheme&quot;: null
        }

    }
&amp;lt;/script&amp;gt;&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;위의 javascript 코드가 sample 결제모듈 되겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;successUrl 을 서버의 결제승인 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;&amp;nbsp;&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;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1701412545518&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(value = &quot;success&quot;)
    public String paymentResult(
            Model model,
            @RequestParam(value = &quot;orderId&quot;) String orderId,
            @RequestParam(value = &quot;amount&quot;) Integer amount,
            @RequestParam(value = &quot;paymentKey&quot;) String paymentKey) throws Exception {

        String secretKey = &quot;test_ak_ZORzdMaqN3wQd5k6ygr5AkYXQGwy:&quot;;

        Base64.Encoder encoder = Base64.getEncoder();
        byte[] encodedBytes = encoder.encode(secretKey.getBytes(&quot;UTF-8&quot;));
        String authorizations = &quot;Basic &quot; + new String(encodedBytes, 0, encodedBytes.length);

        URL url = new URL(&quot;https://api.tosspayments.com/v1/payments/&quot; + paymentKey);

        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestProperty(&quot;Authorization&quot;, authorizations);
        connection.setRequestProperty(&quot;Content-Type&quot;, &quot;application/json&quot;);
        connection.setRequestMethod(&quot;POST&quot;);
        connection.setDoOutput(true);
        JSONObject obj = new JSONObject();
        obj.put(&quot;orderId&quot;, orderId);
        obj.put(&quot;amount&quot;, amount);

        OutputStream outputStream = connection.getOutputStream();
        outputStream.write(obj.toString().getBytes(&quot;UTF-8&quot;));

        int code = connection.getResponseCode();
        boolean isSuccess = code == 200 ? true : false;
        model.addAttribute(&quot;isSuccess&quot;, isSuccess);

        InputStream responseStream = isSuccess ? connection.getInputStream() : connection.getErrorStream();

        Reader reader = new InputStreamReader(responseStream, StandardCharsets.UTF_8);
        JSONParser parser = new JSONParser();
        JSONObject jsonObject = (JSONObject) parser.parse(reader);
        responseStream.close();
        model.addAttribute(&quot;responseStr&quot;, jsonObject.toJSONString());
        System.out.println(jsonObject.toJSONString());

        model.addAttribute(&quot;method&quot;, (String) jsonObject.get(&quot;method&quot;));
        model.addAttribute(&quot;orderName&quot;, (String) jsonObject.get(&quot;orderName&quot;));

        if (((String) jsonObject.get(&quot;method&quot;)) != null) {
            if (((String) jsonObject.get(&quot;method&quot;)).equals(&quot;카드&quot;)) {
                model.addAttribute(&quot;cardNumber&quot;, (String) ((JSONObject) jsonObject.get(&quot;card&quot;)).get(&quot;number&quot;));
            } else if (((String) jsonObject.get(&quot;method&quot;)).equals(&quot;가상계좌&quot;)) {
                model.addAttribute(&quot;accountNumber&quot;, (String) ((JSONObject) jsonObject.get(&quot;virtualAccount&quot;)).get(&quot;accountNumber&quot;));
            } else if (((String) jsonObject.get(&quot;method&quot;)).equals(&quot;계좌이체&quot;)) {
                model.addAttribute(&quot;bank&quot;, (String) ((JSONObject) jsonObject.get(&quot;transfer&quot;)).get(&quot;bank&quot;));
            } else if (((String) jsonObject.get(&quot;method&quot;)).equals(&quot;휴대폰&quot;)) {
                model.addAttribute(&quot;customerMobilePhone&quot;, (String) ((JSONObject) jsonObject.get(&quot;mobilePhone&quot;)).get(&quot;customerMobilePhone&quot;));
            }
        } else {
            model.addAttribute(&quot;code&quot;, (String) jsonObject.get(&quot;code&quot;));
            model.addAttribute(&quot;message&quot;, (String) jsonObject.get(&quot;message&quot;));
        }

        return &quot;success&quot;;
    }&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;적당히 분리해서 진행하면 되고 나의 경우엔 MVC 패턴이 아니다보니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 redirectView를 호출하여 결제 성공 페이지, 결제 실패 페이지로 연결해 주었다.&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;'임시 결제 테이블'에 저장한 orderId 와 비교해 실제로 같은 사람이 결제한 상품인지 검증하는 부분과&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;디스코드에 물어보니 만약 위 결제승인 과정 도중 예외처리가 되어 승인 api가 실패되고 보내지지 않는다면 금액이 지불되지 않는다는 답변을 받았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7mQjc/btsBeTSsYNE/LxY5Kl1Xjgc6IFJz9aw200/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7mQjc/btsBeTSsYNE/LxY5Kl1Xjgc6IFJz9aw200/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7mQjc/btsBeTSsYNE/LxY5Kl1Xjgc6IFJz9aw200/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7mQjc%2FbtsBeTSsYNE%2FLxY5Kl1Xjgc6IFJz9aw200%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;541&quot; height=&quot;292&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;378&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;485&quot; data-origin-height=&quot;127&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TfmeE/btsBfiLfWf8/GgagjOzUkpEoEeZfZQNdQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TfmeE/btsBfiLfWf8/GgagjOzUkpEoEeZfZQNdQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TfmeE/btsBfiLfWf8/GgagjOzUkpEoEeZfZQNdQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTfmeE%2FbtsBfiLfWf8%2FGgagjOzUkpEoEeZfZQNdQ1%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;485&quot; height=&quot;127&quot; data-origin-width=&quot;485&quot; data-origin-height=&quot;127&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;그러니 혹시 이글을 보는 여러분들도 위의 코드를 Controller에서 동작하지 말고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service 코드에서 @Transactional 어노테이션을 사용해서 예외처리시 rollback 해주시길 바란다.&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&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;별다른 어려운 점 없이 토스페이먼츠 api 만을 분석해서 결제 과정을 구현해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참. api 문서라는게 볼 때는 정말 귀찮다가도 구현하게 된다면 그만큼 후련해지는것 같다.&lt;/p&gt;</description>
      <category>Backend/SpringBoot</category>
      <category>springboot</category>
      <category>결제모듈</category>
      <category>토스페이먼츠</category>
      <author>Mo'Greene</author>
      <guid isPermaLink="true">https://mo-greene.tistory.com/114</guid>
      <comments>https://mo-greene.tistory.com/114#entry114comment</comments>
      <pubDate>Fri, 1 Dec 2023 15:59:29 +0900</pubDate>
    </item>
    <item>
      <title>QueryDsl] (SpringBoot + JPA) Offset/Limit과 transform</title>
      <link>https://mo-greene.tistory.com/113</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 QueryDsl로 회사 프로젝트를 진행하면서 까다로운 상황을 잘 해결해왔다고 생각했는데&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_1698579194018&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter @Setter
@Entity
public class Movie {
   
    @Id @GeneratedValue
    private Long id;

    @Column(name = &quot;title&quot;)
    private String title;
}

@Getter @Setter
@Entity
public class Actor {

    @Id @GeneratedValue
    private Long id;

    @Column(name = &quot;name&quot;)
    private String name;
    
    @ManyToOne
    @JoinColumn(name = &quot;movie&quot;)
    private Movie movie;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사의 코드를 들고올 수는 없으니 나와 비슷한 상황을 겪었던 StackOverFlow 질문이 있어 비슷하게 변경해 보았다.(답변이 없다..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;a href=&quot;https://stackoverflow.com/questions/70813314/querydsl-springjpa-limit-offset-when-using-transform&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stackoverflow.com/questions/70813314/querydsl-springjpa-limit-offset-when-using-transform&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1698579526887&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;QueryDSL (Spring+JPA) Limit/Offset when using Transform&quot; data-og-description=&quot;I am trying to use QueryDSL to transform a Many to Many select into a projection while also applying pagination using JPA. I have the following entities: @Getter @Setter @Entity public class Movie ...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/70813314/querydsl-springjpa-limit-offset-when-using-transform&quot; data-og-url=&quot;https://stackoverflow.com/questions/70813314/querydsl-springjpa-limit-offset-when-using-transform&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bXhlmq/hyUnQ8yRA6/URfchDAMGPAgzy1Pre5RSK/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/70813314/querydsl-springjpa-limit-offset-when-using-transform&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/70813314/querydsl-springjpa-limit-offset-when-using-transform&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bXhlmq/hyUnQ8yRA6/URfchDAMGPAgzy1Pre5RSK/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;QueryDSL (Spring+JPA) Limit/Offset when using Transform&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I am trying to use QueryDSL to transform a Many to Many select into a projection while also applying pagination using JPA. I have the following entities: @Getter @Setter @Entity public class Movie ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Actor의 N: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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Projections을 사용해 반환하려는 DTO는&lt;/p&gt;
&lt;pre id=&quot;code_1698579922409&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//롬복 생략
public class MovieDto {
    String title;
    List&amp;lt;ActorDto&amp;gt; actors;
}

public class ActorDto {
    String name;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식을 사용해 Movie를 조회할 때 Dto안에 List를 반환하려했다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;방법 1.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1698580685787&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;MovieDto&amp;gt; all = queryFactory
        .selectFrom(movie)
        .leftJoin(actor)
        .on(movie.id.eq(actor.movie.id))
        .offset(pageable.getPageOffset())
        .limit(pageable.getPageSize())
        .transform(groupBy(movie.id).list(Projections.constructor(MovieDto.class,
                            movie.title,
                            list(Projections.constructor(ActorDto.class, 
                            	actor.name))
)));&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;문제의 상황은 페이지네이션을 적용하려고 할때 나오게 되는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Movie 1, Actor N 의 관계에서 위의 QueryDsl을 사용해 size를 10개로 준다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Actor의 데이터 수에 따라 결과가&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;Page = 0, Size = 10&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 212px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ID&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;영화 제목&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배우 이름&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px; text-align: center;&quot;&gt;영화 제목 1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px; text-align: center;&quot;&gt;영화 1 배우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px; text-align: center;&quot;&gt;영화 제목 1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px; text-align: center;&quot;&gt;영화 1 배우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;영화 제목 2&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;영화 2 배우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;영화 제목 2&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;영화 2 배우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;영화 제목 3&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;영화 3 배우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;영화 제목 3&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;영화 3 배우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;영화 제목 3&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;영화 3 배우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;영화 제목 4&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;영화 4 배우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;영화 제목 4&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: center;&quot;&gt;영화 4 배우&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;영화 제목 4&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: center;&quot;&gt;영화 4 배우&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 데이터가 나오게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GroupBy로 MovieDto를 반환할 때 Actor 컬럼이 MovieDto내의 List 형태로 들어가는게 아닌 중복된 row로 추가되어 나온다는 얘기다.&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개의 Movie id가 필요한데 전혀 다른 데이터가 나오고 있기 때문이다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;방법 2.&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1698582078273&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Movie&amp;gt; query = queryFactory
                .select(Projections.constructor(MovieDto.class,
                	movie.id
                ))
                .from(movie)
                .leftJoin(actor)
                .on(movie.id.eq(actor.member.id))
                .where()
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(member.id.desc())
                .groupBy(	//GroupBy 중복 제거
                        movie
                )
                .fetchJoin().fetch()
                .stream()	//stream 시작
                .peek(p -&amp;gt; {
                    Long movieId = p.getId();
                    List&amp;lt;ActorDto&amp;gt; actorList = queryFactory
                            .selectFrom(actor)
                            .where(actor.movie.id.eq(movieId))
                            .transform(
                                    groupBy(actor.id).list(Projections.constructor(ActorDto.class,
                                            actor.name
                                    ))
                            );
                    p.setActors(actorList);
                })
                .collect(Collectors.toList());&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;성능과 효율도 굉장히 안좋은 코드가 아닌가 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략적으로 Movie와 Actor를 설명하자면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GroupBy를 통해 Movie 객체의 중복컬럼 없는 데이터를 뽑아내고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Movie id값을 for문 혹은 stream으로 하나 하나 찾아내어 MovieDto안에 actor 이름을 List로 만들어 넣어주는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 된다면 쿼리문이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N+1 이 아니라 N+size 의 쿼리문 개수가 나가게 된다ㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;size 의 수만큼 for 문을 돌면서 Actor 테이블을 조회할 텐데 Actor의 수가 늘면 늘수록 아주 재밌는 현상이 벌어질 것이다ㅋㅋ&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;이 Entity관계에 대해 고민해서 나온 해결방법은 하나였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;방법 3.&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;양방향 관계 설정&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1698582967914&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter @Setter
@Entity
public class Movie {
   
    @Id @GeneratedValue
    private Long id;

    @Column(name = &quot;title&quot;)
    private String title;
    
    //추가부분
    @OneToMany(mappedBy = &quot;movie&quot;)
    private List&amp;lt;Actor&amp;gt; actors;
}

@Getter @Setter
@Entity
public class Actor {

    @Id @GeneratedValue
    private Long id;

    @Column(name = &quot;name&quot;)
    private String name;
    
    @ManyToOne
    @JoinColumn(name = &quot;movie&quot;)
    private Movie movie;
}&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;주인은&amp;nbsp;Actor로&amp;nbsp;설정하여&amp;nbsp;Actor&amp;nbsp;테이블에&amp;nbsp;FK&amp;nbsp;키가&amp;nbsp;매핑되도록&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;pre id=&quot;code_1698583173853&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Page&amp;lt;MovieDto&amp;gt; findMovie(Pageable pageable, SearchRequest request) {

        List&amp;lt;Movie&amp;gt; query = queryFactory
                .selectFrom(movie)
                .leftJoin(actor)
                .on(movie.id.eq(actor.movie.id))
                .where(
                	//동적쿼리 작성부분
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .groupBy(movie)
                .orderBy(movie.id.desc())
                .fetchJoin().fetch();

        //MovieDto 데이터 삽입
        List&amp;lt;MovieDto&amp;gt; dtoList = query.stream().map(m -&amp;gt; {

            Movie dto = Movie.builder()
                    .title(m.getTitle)
                    .build();

            List&amp;lt;Actor&amp;gt; actorList = m.getActors().stream()
                    .map(a -&amp;gt;
                            ActorDto.builder()
                                    .name(a.getName())
                                    .build()
                    ).toList();

            dto.setActors(actorList);
            return dto;
        }).toList();

        JPAQuery&amp;lt;Long&amp;gt; count = queryFactory
                .select(movie.count())
                .from(movie)
                .where(
                	//동적쿼리부분
                );

        return PageableExecutionUtils.getPage(dtoList, pageable, count::fetchOne);
    }&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;성능과 효율의 측면에서도 N+1의 문제를 예방했다고 생각하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법2. 에서 나온 size만큼의 N+size 도 없이 한번의 쿼리를 사용하여&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결론.&lt;/b&gt;&lt;/h3&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;하지만 MyBatis와 다르게 서브쿼리를 지양하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드적으로 테이블이 아닌 객체를 다루는 것에 대해 일주일 가까히 생각하다보니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data Jpa를 사용하게 된다면 모든 테이블을 단방향 관계로 설계하는 게 아닌&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>Backend/JPA</category>
      <category>groupby</category>
      <category>JPA</category>
      <category>QueryDSL</category>
      <category>springboot</category>
      <category>SpringDataJPA</category>
      <category>Transform</category>
      <author>Mo'Greene</author>
      <guid isPermaLink="true">https://mo-greene.tistory.com/113</guid>
      <comments>https://mo-greene.tistory.com/113#entry113comment</comments>
      <pubDate>Sun, 29 Oct 2023 21:51:56 +0900</pubDate>
    </item>
    <item>
      <title>JPA] 자연키 vs 대리키</title>
      <link>https://mo-greene.tistory.com/112</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;자바 ORM 표준 JPA 프로그래밍&amp;gt; p.143 발췌&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 style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;데이터베이스 기본 키는 다음 3가지 조건을 모두 만족해야 한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. null값은 허용하지 않는다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 유일해야 한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3.변해선안된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테이블의 기본 키를 선택하는 전략은 크게 2가지가 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자연 키(natural key)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-비즈니스에의미가있는키&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 예: 주민등록번호, 이메일, 전화번호&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대리 키(surrogate key)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 비즈니스와 관련 없는 임의로 만들어진 키, 대체 키로도 불린다. - 예: 오라클 시퀀스, auto_increment, 키생성 테이블 사용&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자연 키보다는 대리 키를 권장한다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자연 키와 대리 키는 일장 일단이 있지만 될 수 있으면 대리 키의 사용을 권장한다. 예를 들어 자연 키인 전화번호를 기본 키로 선택한다면 그 번호가 유일할 수는 있지만, 전화번호가 없을 수도 있고 전화번호가 변경될 수도 있다. 따라서 기본 키로 적당하지 않다. 문제는 주민등록번호처럼 그럴듯 하게 보이는 값이다. 이 값은 null이 아니고 유일하며 변하지 않는다는 3가지 조건을 모두 만족하 는 것 같다. 하지만 현실과 비즈니스 규칙은 생각보다 쉽게 변한다. 주민등록번호조차도 여러 가지 이유로 변경될 수 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비즈니스 환경은 언젠가 변한다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;나의 경험을 하나 이야기하겠다. 레거시 시스템을 유지보수할 일이 있었는데, 분석해보니 회원 테 이블에 주민등록번호가 기본 키로 잡혀 있었다. 회원과 관련된 수많은 테이블에서 조인을 위해 주 민등록번호를 외래 키로 가지고 있었고 심지어 자식 테이블의 자식 테이블까지 주민등록번호가 내려가 있었다. 문제는 정부 정책이 변경되면서 법적으로 주민등록번호를 저장할 수 없게 되면서 발생했다. 결국 데이터베이스 테이블은 물론이고 수많은 애플리케이션 로직을 수정했다. 만약 데 이터베이스를 처음 설계할 때부터 자연 키인 주민등록번호 대신에 비즈니스와 관련 없는 대리 키 를 사용했다면 수정할 부분이 많지는 않았을 것이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기본 키의 조건을 현재는 물론이고 미래까지 충족하는 자연 키를 찾기는 쉽지 않다. 대리 키는 비 즈니스와 무관한 임의의 값이므로 요구사항이 변경되어도 기본 키가 변경되는 일은 드물다. 대리 키를 기본 키로 사용하되 주민등록번호나 이메일처럼 자연 키의 후보가 되는 컬럼들은 필요에 따 라 유니크 인덱스를 설정해서 사용하는 것을 권장한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JPA는 모든 엔티티에 일관된 방식으로 대리 키 사용을 권장한다&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;비즈니스 요구사항은 계속해서 변하는데 테이블은 한 번 정의하면 변경하기 어렵다. 그런 면에서 외부 풍파에 쉽게 흔들리지 않는 대리 키가 일반적으로 좋은 선택이라 생각한다.&lt;/p&gt;</description>
      <category>Backend/JPA</category>
      <author>Mo'Greene</author>
      <guid isPermaLink="true">https://mo-greene.tistory.com/112</guid>
      <comments>https://mo-greene.tistory.com/112#entry112comment</comments>
      <pubDate>Wed, 11 Oct 2023 22:07:02 +0900</pubDate>
    </item>
    <item>
      <title>네이버 문자(sms) 서비스 구현</title>
      <link>https://mo-greene.tistory.com/111</link>
      <description>&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;&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;coolsms&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://coolsms.co.kr/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://coolsms.co.kr/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1694524037686&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;세상에서 가장 안정적이고 빠른 메시지 발송 플랫폼 - 쿨에스엠에스&quot; data-og-description=&quot;손쉬운 결제 전용계좌, 신용카드, 계좌이체 등 국내 결제 뿐만 아니라 해용신용카드로 한번의 카드번호 등록으로 자동충전까지 지원합니다. 전용계좌, 신용카드, 계좌이체 등 다양한 결제 방식 &quot; data-og-host=&quot;coolsms.co.kr&quot; data-og-source-url=&quot;https://coolsms.co.kr/&quot; data-og-url=&quot;https://coolsms.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cdQ4oQ/hyTV1CNW57/r7JASuxqvgURcTnKtkMxj0/img.png?width=2880&amp;amp;height=1578&amp;amp;face=0_0_2880_1578,https://scrap.kakaocdn.net/dn/dqVokG/hyTVRtoZT2/EU86dKYJI3pf1fXNAju0h0/img.png?width=2880&amp;amp;height=1578&amp;amp;face=0_0_2880_1578,https://scrap.kakaocdn.net/dn/bmukuy/hyTVUDFFjy/LVmxS90ueOtlsRMUkQUbq0/img.png?width=642&amp;amp;height=400&amp;amp;face=0_0_642_400&quot;&gt;&lt;a href=&quot;https://coolsms.co.kr/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://coolsms.co.kr/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cdQ4oQ/hyTV1CNW57/r7JASuxqvgURcTnKtkMxj0/img.png?width=2880&amp;amp;height=1578&amp;amp;face=0_0_2880_1578,https://scrap.kakaocdn.net/dn/dqVokG/hyTVRtoZT2/EU86dKYJI3pf1fXNAju0h0/img.png?width=2880&amp;amp;height=1578&amp;amp;face=0_0_2880_1578,https://scrap.kakaocdn.net/dn/bmukuy/hyTVUDFFjy/LVmxS90ueOtlsRMUkQUbq0/img.png?width=642&amp;amp;height=400&amp;amp;face=0_0_642_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;세상에서 가장 안정적이고 빠른 메시지 발송 플랫폼 - 쿨에스엠에스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;손쉬운 결제 전용계좌, 신용카드, 계좌이체 등 국내 결제 뿐만 아니라 해용신용카드로 한번의 카드번호 등록으로 자동충전까지 지원합니다. 전용계좌, 신용카드, 계좌이체 등 다양한 결제 방식&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;coolsms.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;NHN Cloud&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.nhncloud.com/ko/Notification/SMS/ko/api-guide/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.nhncloud.com/ko/Notification/SMS/ko/api-guide/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1694524137450&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;API v3.0 가이드 - NHN Cloud 사용자 가이드&quot; data-og-description=&quot;Notification &amp;gt; SMS &amp;gt; API v3.0 Guide v3.0 API 소개 v2.4와 달라진 사항 시크릿 키가 도입되었습니다. v3.0 API 호출 시에는 헤더에 시크릿 키를 추가해야 성공하게 됩니다. 대량 발송 요청을 조회할 수 있는 API &quot; data-og-host=&quot;docs.nhncloud.com&quot; data-og-source-url=&quot;https://docs.nhncloud.com/ko/Notification/SMS/ko/api-guide/&quot; data-og-url=&quot;https://docs.nhncloud.com/ko/Notification/SMS/ko/api-guide/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dZf9r0/hyTSosR2N8/Bg6GRmm49FhbHaCLil3KXK/img.png?width=425&amp;amp;height=756&amp;amp;face=0_0_425_756&quot;&gt;&lt;a href=&quot;https://docs.nhncloud.com/ko/Notification/SMS/ko/api-guide/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.nhncloud.com/ko/Notification/SMS/ko/api-guide/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dZf9r0/hyTSosR2N8/Bg6GRmm49FhbHaCLil3KXK/img.png?width=425&amp;amp;height=756&amp;amp;face=0_0_425_756');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;API v3.0 가이드 - NHN Cloud 사용자 가이드&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Notification &amp;gt; SMS &amp;gt; API v3.0 Guide v3.0 API 소개 v2.4와 달라진 사항 시크릿 키가 도입되었습니다. v3.0 API 호출 시에는 헤더에 시크릿 키를 추가해야 성공하게 됩니다. 대량 발송 요청을 조회할 수 있는 API&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.nhncloud.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;Naver Cloud&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.ncloud.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.ncloud.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1694524216107&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;NAVER CLOUD PLATFORM&quot; data-og-description=&quot;cloud computing services for corporations, IaaS, PaaS, SaaS, with Global region and Security Technology Certification&quot; data-og-host=&quot;www.ncloud.com&quot; data-og-source-url=&quot;https://www.ncloud.com/&quot; data-og-url=&quot;https://www.ncloud.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/xCm99/hyTVRAa4Hp/4tCs9CI6JHIRCko2uZqTkk/img.jpg?width=526&amp;amp;height=274&amp;amp;face=0_0_526_274,https://scrap.kakaocdn.net/dn/reLaL/hyTVO4wEHT/6tj51kId1xdCt0aaVvyZx1/img.png?width=3200&amp;amp;height=1440&amp;amp;face=0_0_3200_1440,https://scrap.kakaocdn.net/dn/bAAP7W/hyTSxJ8dXl/JGTh5ZdmTRuICuAeflek81/img.png?width=3200&amp;amp;height=1440&amp;amp;face=0_0_3200_1440&quot;&gt;&lt;a href=&quot;https://www.ncloud.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.ncloud.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/xCm99/hyTVRAa4Hp/4tCs9CI6JHIRCko2uZqTkk/img.jpg?width=526&amp;amp;height=274&amp;amp;face=0_0_526_274,https://scrap.kakaocdn.net/dn/reLaL/hyTVO4wEHT/6tj51kId1xdCt0aaVvyZx1/img.png?width=3200&amp;amp;height=1440&amp;amp;face=0_0_3200_1440,https://scrap.kakaocdn.net/dn/bAAP7W/hyTSxJ8dXl/JGTh5ZdmTRuICuAeflek81/img.png?width=3200&amp;amp;height=1440&amp;amp;face=0_0_3200_1440');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;NAVER CLOUD PLATFORM&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;cloud computing services for corporations, IaaS, PaaS, SaaS, with Global region and Security Technology Certification&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.ncloud.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;코드를 구현하는 것 자체는 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;그렇다면 개발자로써 무엇을 생각하여 3개의 라이브러리 중 하나를 골라야할까&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;h3 data-ke-size=&quot;size23&quot;&gt;가격,&amp;nbsp; 사용법,&amp;nbsp; 추가과정&lt;/h3&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;coolsms 가격&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://coolsms.zendesk.com/hc/ko/articles/115001071551-%EB%AC%B8%EC%9E%90%ED%83%80%EC%9E%85%EB%B3%84-%EB%B9%84%EC%9A%A9&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://coolsms.zendesk.com/hc/ko/articles/115001071551-%EB%AC%B8%EC%9E%90%ED%83%80%EC%9E%85%EB%B3%84-%EB%B9%84%EC%9A%A9&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;기본적으로 일반문자(sms)를 사용한다는 가정하에 건당 13.67 ~ 18.18원이 나온다.&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;nhn cloud&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.nhncloud.com/kr/pricing/m-content?c=Notification&amp;amp;s=SMS&quot;&gt;https://www.nhncloud.com/kr/pricing/m-content?c=Notification&amp;amp;s=SMS&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1694525120283&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;NHN Cloud : 유연하게 안전하게 비즈니스의 힘이 되는 통합 클라우드 서비스&quot; data-og-description=&quot;안정적이고 유연한 기업용 클라우드 컴퓨팅 서비스, 오픈스택 기반의 개방성과 신뢰로 고객사의 비즈니스에 힘이 되는 NHN Cloud&quot; data-og-host=&quot;www.nhncloud.com&quot; data-og-source-url=&quot;https://www.nhncloud.com/kr/pricing/m-content?c=Notification&amp;amp;s=SMS&quot; data-og-url=&quot;https://www.nhncloud.com/kr&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bp5Cp6/hyTV1pgHUg/SOXXByDQmeFz6hTeILCEXK/img.png?width=526&amp;amp;height=274&amp;amp;face=0_0_526_274&quot;&gt;&lt;a href=&quot;https://www.nhncloud.com/kr/pricing/m-content?c=Notification&amp;amp;s=SMS&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.nhncloud.com/kr/pricing/m-content?c=Notification&amp;amp;s=SMS&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bp5Cp6/hyTV1pgHUg/SOXXByDQmeFz6hTeILCEXK/img.png?width=526&amp;amp;height=274&amp;amp;face=0_0_526_274');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;NHN Cloud : 유연하게 안전하게 비즈니스의 힘이 되는 통합 클라우드 서비스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안정적이고 유연한 기업용 클라우드 컴퓨팅 서비스, 오픈스택 기반의 개방성과 신뢰로 고객사의 비즈니스에 힘이 되는 NHN Cloud&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.nhncloud.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수신 1건당 9.9원이 나온다.&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;naver cloud platform&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.ncloud.com/product/applicationService/sens&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.ncloud.com/product/applicationService/sens&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1694525235971&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;NAVER CLOUD PLATFORM&quot; data-og-description=&quot;cloud computing services for corporations, IaaS, PaaS, SaaS, with Global region and Security Technology Certification&quot; data-og-host=&quot;www.ncloud.com&quot; data-og-source-url=&quot;https://www.ncloud.com/product/applicationService/sens&quot; data-og-url=&quot;https://www.ncloud.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ALDB8/hyTSxcjcfE/re4CWyQuxGbds61CvpbLy1/img.jpg?width=526&amp;amp;height=274&amp;amp;face=0_0_526_274,https://scrap.kakaocdn.net/dn/cvArEK/hyTSwRZN3G/GA92GKkWcvVMLdj0x0LED1/img.png?width=3200&amp;amp;height=1440&amp;amp;face=0_0_3200_1440,https://scrap.kakaocdn.net/dn/bnOpjg/hyTSCEFtMH/A6id6ddtob6Hr2e3wVaMf1/img.png?width=3200&amp;amp;height=1440&amp;amp;face=0_0_3200_1440&quot;&gt;&lt;a href=&quot;https://www.ncloud.com/product/applicationService/sens&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.ncloud.com/product/applicationService/sens&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ALDB8/hyTSxcjcfE/re4CWyQuxGbds61CvpbLy1/img.jpg?width=526&amp;amp;height=274&amp;amp;face=0_0_526_274,https://scrap.kakaocdn.net/dn/cvArEK/hyTSwRZN3G/GA92GKkWcvVMLdj0x0LED1/img.png?width=3200&amp;amp;height=1440&amp;amp;face=0_0_3200_1440,https://scrap.kakaocdn.net/dn/bnOpjg/hyTSCEFtMH/A6id6ddtob6Hr2e3wVaMf1/img.png?width=3200&amp;amp;height=1440&amp;amp;face=0_0_3200_1440');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;NAVER CLOUD PLATFORM&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;cloud computing services for corporations, IaaS, PaaS, SaaS, with Global region and Security Technology Certification&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.ncloud.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;50건 이하 건당 무료! 50건 이상 9원이 나오게 된다.&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;테스트로 사용해도 충분할 50건의 무료문자도 주는걸 보니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가격적으로는 naver를 사용하는게 옳다고 본다.&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;실제로 내가 회사에 들어온 후 추가개발을 맡게 된 레거시 프로젝트들은 전부 coolsms로 구현되어 있었고&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;왜 coolsms 사용하셨어요? 가격이 비싸지 않나요?&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;i&gt;&lt;b&gt;사용하기 되게 쉬워서요.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&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/coolsms/coolsms-java-examples/tree/main&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/coolsms/coolsms-java-examples/tree/main&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;coolsms를 보면 별다른 설정 없이 build.gradle 추가하고 깃허브에 있는 코드(단일 메시지 발송 예제)를 그대로 사용하면 끝이다.&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;아 물론 다른 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;&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 추가과정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별다른건 아니다. 추가적인 확장성? 등을 생각해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 다니는 SI 업체로써 주어진 상황에만 맞추면 되는게 아닌&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;&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;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;h3 data-ke-size=&quot;size23&quot;&gt;환경설정&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java 17&lt;/li&gt;
&lt;li&gt;SpringBoot 3.1.3&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버에서 준비해야 할건 3개다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;accessKey, secretKey, serviceId 어렵지 않고 구글로 충분히 찾아볼 수 있으니 생략하려 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1694526597893&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class NaverApi {

	public static final String ACCESS_KEY = &quot;발급받은 accessKey&quot;;
	public static final String SECRET_KEY = &quot;발급받은 secretKey&quot;;
	public static final String SERVICE_ID = &quot;발급받은 serviceId&quot;;
	public static final String SENDER_PHONE = &quot;보내려는 번호&quot;;
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Value를 통해 application.yml에서 가져오는 방법이 있겠지만 constant 패키지 내부에 보관하는 방식으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api에서 사용할 key 들을 보관하기로 했다.&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;Controller&lt;/h4&gt;
&lt;pre id=&quot;code_1694526694134&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class MessageController {

	private final MessageService messageService;

	@PostMapping(&quot;/api/sms&quot;)
	public String testMethod(@RequestBody MessageRequest request) throws UnsupportedEncodingException, NoSuchAlgorithmException, URISyntaxException, InvalidKeyException, JsonProcessingException {

		return messageService.sendSms(request);
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MessageService를 주입받아 sendSms 메서드를 호출하기만 하면 된다.&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;MessageRequest의 경우&lt;/p&gt;
&lt;pre id=&quot;code_1694526742578&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class MessageRequest {

	private String to;

}&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Service&lt;/h4&gt;
&lt;pre id=&quot;code_1694526793262&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Service
public class MessageService {

	/**
	 * 인증번호 구현
	 * @param request
	 * @return
	 */
	public String sendSms(MessageRequest request) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException, JsonProcessingException, URISyntaxException {

		String sixRandomNumber = makeRandomNumber();
		Long time = System.currentTimeMillis();

		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_JSON);
		headers.set(&quot;x-ncp-apigw-timestamp&quot;, time.toString());
		headers.set(&quot;x-ncp-iam-access-key&quot;, NaverApi.ACCESS_KEY);
		headers.set(&quot;x-ncp-apigw-signature-v2&quot;, makeSignature(time));

		List&amp;lt;MessageRequest&amp;gt; messages = new ArrayList&amp;lt;&amp;gt;();
		messages.add(request);

		SmsRequest smsRequest = SmsRequest.builder()
			.type(&quot;SMS&quot;)
			.contentType(&quot;COMM&quot;)
			.countryCode(&quot;82&quot;)
			.from(NaverApi.SENDER_PHONE)
			.content(&quot;인증번호는 [&quot; + sixRandomNumber + &quot;] 입니다.&quot;)
			.messages(messages)
			.build();

		ObjectMapper objectMapper = new ObjectMapper();
		String body = objectMapper.writeValueAsString(smsRequest);

		HttpEntity&amp;lt;String&amp;gt; httpBody = new HttpEntity&amp;lt;&amp;gt;(body, headers);
		RestTemplate restTemplate = new RestTemplate();
		restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());

		SmsResponse response =  restTemplate.postForObject(new URI(&quot;https://sens.apigw.ntruss.com/sms/v2/services/&quot; + NaverApi.SERVICE_ID + &quot;/messages&quot;), httpBody, SmsResponse.class);

		if (Objects.requireNonNull(response).getStatusCode().equals(&quot;202&quot;)) {
			return sixRandomNumber;
		}
		if (response.getStatusName().equals(&quot;fail&quot;)) {
			return response.getStatusName();
		}
		return sixRandomNumber;
	}

	/**
	 * 난수 생성 메서드
	 * @return
	 */
	private String makeRandomNumber() {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i &amp;lt; 6; i++) {
			sb.append((int) (Math.random() * 9));
		}
		return sb.toString();
	}

	/**
	 * sms signature 생성
	 * @param time
	 * @return
	 */
	public String makeSignature(Long time) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException {
		String space = &quot; &quot;;					// one space
		String newLine = &quot;\n&quot;;					// new line
		String method = &quot;POST&quot;;					// method
		String url = &quot;/sms/v2/services/&quot; + NaverApi.SERVICE_ID + &quot;/messages&quot;;	// url (include query string)
		String timestamp = time.toString();			// current timestamp (epoch)
		String accessKey = NaverApi.ACCESS_KEY;			// access key id (from portal or Sub Account)
		String secretKey = NaverApi.SECRET_KEY;

		String message = new StringBuilder()
			.append(method)
			.append(space)
			.append(url)
			.append(newLine)
			.append(timestamp)
			.append(newLine)
			.append(accessKey)
			.toString();

		SecretKeySpec signingKey = new SecretKeySpec(secretKey.getBytes(&quot;UTF-8&quot;), &quot;HmacSHA256&quot;);
		Mac mac = Mac.getInstance(&quot;HmacSHA256&quot;);
		mac.init(signingKey);

		byte[] rawHmac = mac.doFinal(message.getBytes(&quot;UTF-8&quot;));

		return Base64.encodeBase64String(rawHmac);
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 부분은 sms를 보낼때 http 호출하기 위해서 헤더에 signature값을 만들어 보내준다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 부분이 makeSignature라는 메서드가 되고 그 반환값을 헤더에 넣어주어 RestTemplate으로 네이버 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;그 안에 makeRandomNumber은 인증번호를 보낼 때 받게 되는 6개의 무작위 숫자 알고리즘이다.&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;SmsRequest, SmsResponse&lt;/h4&gt;
&lt;pre id=&quot;code_1694527472853&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SmsRequest {

	private String type;
	private String contentType;
	private String countryCode;
	private String from;
	private String content;
	private List&amp;lt;MessageRequest&amp;gt; messages;

}


@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SmsResponse {

	private String requestId;
	private LocalDateTime requestTime;
	private String statusCode;
	private String statusName;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용할 DTO 클래스&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;Test&lt;/h4&gt;
&lt;pre id=&quot;code_1694527023074&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
class MessageServiceTest {

	@Autowired
	MessageService messageService;

	@Test
	@DisplayName(&quot;sendSms 단위 테스트&quot;)
	void sendSms() throws UnsupportedEncodingException, NoSuchAlgorithmException, URISyntaxException, InvalidKeyException, JsonProcessingException {
		//given
		MessageRequest request = MessageRequest.builder()
			.to(&quot;01022745320&quot;).
			build();

		//when
		String response = messageService.sendSms(request);

		//then
		assertNotEquals(&quot;fail&quot;, response);
	}
}&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2023-09-12-22-59-10.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;293&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B3CMT/btstMjRJ1TO/Sf88SKF85HWZrKND3SR0CK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B3CMT/btstMjRJ1TO/Sf88SKF85HWZrKND3SR0CK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B3CMT/btstMjRJ1TO/Sf88SKF85HWZrKND3SR0CK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB3CMT%2FbtstMjRJ1TO%2FSf88SKF85HWZrKND3SR0CK%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;532&quot; height=&quot;144&quot; data-filename=&quot;KakaoTalk_Photo_2023-09-12-22-59-10.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;293&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;사실 api 대로 진행하기만 하면 문제될 부분이 없어 난이도 자체는 어렵지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외처리를 추가해주면서 프론트엔드와 소통하면 될 듯 하다.&lt;/p&gt;</description>
      <category>Backend/SpringBoot</category>
      <category>문자</category>
      <category>스프링부트</category>
      <author>Mo'Greene</author>
      <guid isPermaLink="true">https://mo-greene.tistory.com/111</guid>
      <comments>https://mo-greene.tistory.com/111#entry111comment</comments>
      <pubDate>Tue, 12 Sep 2023 23:01:10 +0900</pubDate>
    </item>
    <item>
      <title>Swagger Opendoc API</title>
      <link>https://mo-greene.tistory.com/110</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Swagger..&lt;/span&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;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://swagger.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://swagger.io/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1689687450021&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;API Documentation &amp;amp; Design Tools for Teams | Swagger&quot; data-og-description=&quot;Loved by all &amp;bull; Big &amp;amp; Small Thousands of teams worldwide trust Swagger to deliver better products, faster.&quot; data-og-host=&quot;swagger.io&quot; data-og-source-url=&quot;https://swagger.io/&quot; data-og-url=&quot;https://swagger.io/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://swagger.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://swagger.io/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;API Documentation &amp;amp; Design Tools for Teams | Swagger&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Loved by all &amp;bull; Big &amp;amp; Small Thousands of teams worldwide trust Swagger to deliver better products, faster.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;swagger.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;font-family: 'Noto Serif KR';&quot;&gt;개인적으로 Postman을 사용했었을 때 굳이 Postman 말고 다른 걸 사용할 필요가 있을까 하는 생각이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 회사에 들어가고 나서 선임이 말해준 이유는 충분히 Postman이 아닌 다른 API플랫폼을 찾기엔 충분했다.&lt;/span&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;font-family: 'Noto Serif KR';&quot;&gt;Postman의 경우 기업이 운영하기에 사용량이 많아지면 '유료'로 가격이 책정된다는 것이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;맨날 혼자 사이드 프로젝트를 하던 사람에겐 '아 맞다' 한 이유였다.&lt;/span&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;font-family: 'Noto Serif KR';&quot;&gt;Swagger의 경우에도 크게 SpringFox와 SpringDoc으로 나뉘어 있지만 SpringFox의 경우 업데이트가 2020년 이후로 멈춰있어&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SpringDoc을 사용해서 개발하는 게 옳다고 본다.&lt;/span&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;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://github.com/springdoc/springdoc-openapi&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/springdoc/springdoc-openapi&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1689687893227&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - springdoc/springdoc-openapi: Library for OpenAPI 3 with spring-boot&quot; data-og-description=&quot;Library for OpenAPI 3 with spring-boot. Contribute to springdoc/springdoc-openapi development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/springdoc/springdoc-openapi&quot; data-og-url=&quot;https://github.com/springdoc/springdoc-openapi&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bfnSwi/hyTmpENCew/qtMt69AhGlmOd1k8ON97M1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/springdoc/springdoc-openapi&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/springdoc/springdoc-openapi&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bfnSwi/hyTmpENCew/qtMt69AhGlmOd1k8ON97M1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - springdoc/springdoc-openapi: Library for OpenAPI 3 with spring-boot&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Library for OpenAPI 3 with spring-boot. Contribute to springdoc/springdoc-openapi development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;요즘은 굳이 다른 블로그를 안 보고 직접 문서를 번역기로 돌려 이해하는 것에 재미 들렸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;환경설정&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Java 11&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SpringBoot 2.6.8&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Spring Web&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Lombok&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Java 17 + SpringBoot 3.x.x 버전은 이미 Github에 잘 정리되어 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;나의 경우 아직 Java 11과 스프링부트 3 이하 버전의 프로젝트에 추가개발을 맡게 되어 이렇게 진행하려고 한다.&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;application.yml&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1689688450562&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;springdoc:
  swagger-ui:
    path: /swagger-ui.html&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;build.gradle&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1689688214813&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    // Swagger OpenDoc Api
    implementation group: 'org.springdoc', name: 'springdoc-openapi-ui', version: '1.6.9'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&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 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Controller&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1689688264279&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.swaggertemplate.controller;

import com.example.swaggertemplate.dto.TestDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @name : TestController
 * @author : Mo-Greene
 * @date : 2023/07/18
 * 스웨거 테스트
 */
@RestController
public class TestController {

    @GetMapping(&quot;/api&quot;)
    @Operation(summary = &quot;Swagger 테스트&quot;, description = &quot;스웨거 테스트&quot;)
    @ApiResponses({
            @ApiResponse(responseCode = &quot;200&quot;, description = &quot;Success&quot;),
            @ApiResponse(responseCode = &quot;400&quot;, description = &quot;Fail&quot;)
    })
    public void api(TestDTO testDTO) {
        System.out.println(&quot;testDTO = &quot; + testDTO);
    }
}&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;TestDTO&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1689688296194&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.swaggertemplate.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

@Data
public class TestDTO {

    @Schema(description = &quot;Swagger Content&quot;, example = &quot;swagger example&quot;)
    private String content;
}&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;따로 SwaggerConfig를 사용해 설정을 잡아주지 않아도 되는 점이 좋은 것 같다.&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결과화면&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-18 오후 10.55.11.png&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;965&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lt6uH/btsn6M6eHYD/A4XmIEAiMqxsunG857aGoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lt6uH/btsn6M6eHYD/A4XmIEAiMqxsunG857aGoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lt6uH/btsn6M6eHYD/A4XmIEAiMqxsunG857aGoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flt6uH%2Fbtsn6M6eHYD%2FA4XmIEAiMqxsunG857aGoK%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;1258&quot; height=&quot;965&quot; data-filename=&quot;스크린샷 2023-07-18 오후 10.55.11.png&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;965&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;API 문서화 작업은 그만큼 시간이 들어가는 작업이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;혼자서 작업한다면 상관없지만 분명 클라이언트 혹은 이 코드를 참고하게 될 다른 개발자들에게 전해줄 수 있는 문서가 될 수 있기에&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;시간을 할애해서라도 작업해 두는 것을 권한다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Backend/SpringBoot</category>
      <category>springboot</category>
      <category>SWAGGER</category>
      <author>Mo'Greene</author>
      <guid isPermaLink="true">https://mo-greene.tistory.com/110</guid>
      <comments>https://mo-greene.tistory.com/110#entry110comment</comments>
      <pubDate>Tue, 18 Jul 2023 22:58:13 +0900</pubDate>
    </item>
    <item>
      <title>Vue.js3 Login + JWT 토큰</title>
      <link>https://mo-greene.tistory.com/109</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;서론.&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;프론트엔드는 하나하나 잘 정리하지 않은 채 작업을 한다면 코드의 양이나 복잡도가 너무나도 높아지는 느낌을 받았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그로 인해 상태관리 라이브러리 (Pinia, Vuex..)를 굉장히 효율적으로 사용해야 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;중복되고 복잡한 코드의 양을 줄일수 있다는 것을 느끼고 있다.&lt;/span&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;font-family: 'Noto Serif KR';&quot;&gt;당장에 로그인만 하더라도 JWT토큰을 백엔드에서 구현하는 것과 함께  Vue.js 내에서 관리해 줘야 되는 게여간 까다로운 게 아니란 것을 알게 되었다.&lt;/span&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;font-family: 'Noto Serif KR';&quot;&gt;그렇기에 정리를 해두고 계속 고쳐가는 방향으로 진행하려고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;JWT 토큰&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://mo-greene.tistory.com/108&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://mo-greene.tistory.com/108&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1689079895809&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Jwt + SpringSecurity + Mybatis 구현&quot; data-og-description=&quot;Vue.js 와 SpringBoot를 사용해 게시판 토이프로젝트를 구현하고 있다. CRUD과정은 전부 끝났고 Vue.js와 씨름하며 시간을 보내다가 드디어 로그인 파트로 넘어왔다. https://www.inflearn.com/course/%EC%8A%A4%ED%94&quot; data-og-host=&quot;mo-greene.tistory.com&quot; data-og-source-url=&quot;https://mo-greene.tistory.com/108&quot; data-og-url=&quot;https://mo-greene.tistory.com/108&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b1cXOL/hyTgWKytL5/LwENq6uZ0xZbC6UDC7TVGK/img.png?width=800&amp;amp;height=536&amp;amp;face=0_0_800_536,https://scrap.kakaocdn.net/dn/dtY9Jh/hyTiqiX4Jp/9KS8Q4cC6Q9Ndf0WO4s6cK/img.png?width=800&amp;amp;height=536&amp;amp;face=0_0_800_536,https://scrap.kakaocdn.net/dn/cucTWF/hyTg16cMJh/iQ3CVv7BBGxnsj9ROJbqkK/img.png?width=2312&amp;amp;height=1084&amp;amp;face=0_0_2312_1084&quot;&gt;&lt;a href=&quot;https://mo-greene.tistory.com/108&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mo-greene.tistory.com/108&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b1cXOL/hyTgWKytL5/LwENq6uZ0xZbC6UDC7TVGK/img.png?width=800&amp;amp;height=536&amp;amp;face=0_0_800_536,https://scrap.kakaocdn.net/dn/dtY9Jh/hyTiqiX4Jp/9KS8Q4cC6Q9Ndf0WO4s6cK/img.png?width=800&amp;amp;height=536&amp;amp;face=0_0_800_536,https://scrap.kakaocdn.net/dn/cucTWF/hyTg16cMJh/iQ3CVv7BBGxnsj9ROJbqkK/img.png?width=2312&amp;amp;height=1084&amp;amp;face=0_0_2312_1084');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Jwt + SpringSecurity + Mybatis 구현&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Vue.js 와 SpringBoot를 사용해 게시판 토이프로젝트를 구현하고 있다. CRUD과정은 전부 끝났고 Vue.js와 씨름하며 시간을 보내다가 드디어 로그인 파트로 넘어왔다. https://www.inflearn.com/course/%EC%8A%A4%ED%94&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mo-greene.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;font-family: 'Noto Serif KR';&quot;&gt;백엔드에서 구현한 JWT토큰은 위 블로그에 포스팅해놨다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;특이점은 기존에 JPA로 구현하는 토큰과는 다르게 Mybatis로 회원테이블을 구현했다는 점이고&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그 외에는 특이사항이 없을 듯하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Login.vue&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1689080007168&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    &amp;lt;v-card&amp;gt;
        &amp;lt;div class=&quot;d-flex align-center justify-center&quot; style=&quot;height: 100vh&quot;&amp;gt;
            &amp;lt;v-sheet width=&quot;400&quot; class=&quot;mx-auto&quot;&amp;gt;
                &amp;lt;v-form fast-fail ref=&quot;loginForm&quot;&amp;gt;
                    &amp;lt;v-text-field
                        variant=&quot;outlined&quot;
                        v-model=&quot;username&quot;
                        label=&quot;username&quot;
                        :rules=&quot;usernameRules&quot;
                    &amp;gt;&amp;lt;/v-text-field&amp;gt;

                    &amp;lt;v-text-field
                        variant=&quot;outlined&quot;
                        v-model=&quot;password&quot;
                        label=&quot;password&quot;
                        :rules=&quot;passwordRules&quot;
                    &amp;gt;&amp;lt;/v-text-field&amp;gt;

                    &amp;lt;v-btn
                        color=&quot;primary&quot;
                        block
                        class=&quot;mt-2&quot;
                        @click=&quot;validation&quot;&amp;gt;Log in&amp;lt;/v-btn&amp;gt;
                &amp;lt;/v-form&amp;gt;
                &amp;lt;div class=&quot;mt-2&quot;&amp;gt;
                    &amp;lt;p class=&quot;text-body-2&quot;&amp;gt;Don't have an account? &amp;lt;a href=&quot;#&quot;&amp;gt;Sign Up&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/v-sheet&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/v-card&amp;gt;
&amp;lt;/template&amp;gt;


&amp;lt;script setup&amp;gt;
import * as loginApi from '@/api/login/login'
import {ref} from &quot;vue&quot;;
import {useLoginStore} from &quot;@/store/login&quot;;
import { useRouter } from &quot;vue-router&quot;;

const username = ref();
const password = ref();
const loginForm = ref();
const loginStore = useLoginStore();
const router = useRouter();

//login Logic
const login = async () =&amp;gt; {
    try {
        // Your login logic here
        const response = await loginApi.login(username.value, password.value);

        if (response.status === 200) {
            await loginStore.setToken(response.data.token);
            localStorage.setItem('access_token', response.data.token)
            await loginStore.getUserInfo();

            await router.push({path: '/'})
        }

    } catch (e) {
        alert(e.response.data.message)
    }
}

//Validation loginForm
async function validation (){
    const { valid } = await loginForm.value.validate();

    if (valid) await login();
    else {
        alert('로그인 양식에 맞지 않습니다.')
    }
}
const usernameRules = ref([
    value =&amp;gt; {
        if (value) return true
        return '아이디를 적어주세요.'
    },
])
const passwordRules = ref([
    value =&amp;gt; {
        if (value) return true
        return '비밀번호를 적어주세요.'
    },
])
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-11 오후 9.54.12.png&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6ewHW/btsngrvfA5O/vfWitPG3W9M7G3Ow48xXI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6ewHW/btsngrvfA5O/vfWitPG3W9M7G3Ow48xXI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6ewHW/btsngrvfA5O/vfWitPG3W9M7G3Ow48xXI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6ewHW%2FbtsngrvfA5O%2FvfWitPG3W9M7G3Ow48xXI0%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;400&quot; height=&quot;236&quot; data-filename=&quot;스크린샷 2023-07-11 오후 9.54.12.png&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;테스트용 아이디와 비밀번호다.&lt;/span&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;font-family: 'Noto Serif KR';&quot;&gt;중요한 부분이라고 생각되는 코드는&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;if&amp;nbsp;(response.status&amp;nbsp;===&amp;nbsp;200)&amp;nbsp;{&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await&amp;nbsp;loginStore.setToken(response.data.token);&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;localStorage.setItem('access_token',&amp;nbsp;response.data.token)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await&amp;nbsp;loginStore.getUserInfo();&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await&amp;nbsp;router.push({path:&amp;nbsp;'/'})&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 부분으로&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Pinia로 작성한 loginStore 모듈에 setToken과 getUserInfo를 호출할 것이고&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결론적으로 사용자의 로컬스토리지에 'access_token'의 키값으로 토큰을 저장할 것이다.&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;로그인 전&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-11 오후 10.01.19.png&quot; data-origin-width=&quot;1892&quot; data-origin-height=&quot;766&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Gk8xE/btsnj8U2r0p/Z3rgVcl4fBfr1jziH3SsnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Gk8xE/btsnj8U2r0p/Z3rgVcl4fBfr1jziH3SsnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Gk8xE/btsnj8U2r0p/Z3rgVcl4fBfr1jziH3SsnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGk8xE%2Fbtsnj8U2r0p%2FZ3rgVcl4fBfr1jziH3SsnK%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;650&quot; height=&quot;263&quot; data-filename=&quot;스크린샷 2023-07-11 오후 10.01.19.png&quot; data-origin-width=&quot;1892&quot; data-origin-height=&quot;766&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;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;로그인 후&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-11 오후 10.02.54.png&quot; data-origin-width=&quot;1912&quot; data-origin-height=&quot;764&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p4Q1Y/btsngTx2kkO/h4ohRxybSCkp6KLd3LYGpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p4Q1Y/btsngTx2kkO/h4ohRxybSCkp6KLd3LYGpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p4Q1Y/btsngTx2kkO/h4ohRxybSCkp6KLd3LYGpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp4Q1Y%2FbtsngTx2kkO%2Fh4ohRxybSCkp6KLd3LYGpK%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;650&quot; height=&quot;260&quot; data-filename=&quot;스크린샷 2023-07-11 오후 10.02.54.png&quot; data-origin-width=&quot;1912&quot; data-origin-height=&quot;764&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;현업에선 어떻게 사용하는지 모르지만 로컬스토리지에는 기본적으로 사용자의 정보가 담겨서는 안 된다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그렇기에 토큰을 담아주고 이 토큰을 사용하여 사용자정보를 가져오는 방식으로 사용하려고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;store/login.js&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1689080791443&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {defineStore} from &quot;pinia&quot;;
import {ref} from &quot;vue&quot;;
import * as loginApi from '@/api/login/login'

export const useLoginStore = defineStore(&quot;login&quot;, () =&amp;gt; {
    //state
    const token = ref(null);
    const userNo = ref(null);
    const username = ref(null);
    const role = ref(null);

    //actions
    async function setToken(newToken) {
        token.value = newToken;
    }

    //유저 정보
    async function getUserInfo() {
        try {
            const localToken = localStorage.getItem('access_token');
            const response = await loginApi.getUserInfo(localToken);

            token.value = localToken;
            userNo.value = response.data.data.userNo;
            username.value = response.data.data.username;
            role.value = response.data.data.role;
        } catch (e) {
            localStorage.removeItem('access_token');
        }
    }

    return {
        token,
        userNo,
        username,
        role,
        setToken,
        getUserInfo,
    }
})&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Pinia의 사용방법에 빨리 익숙해지기 위해 사용하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;개인적으로 Vuex보다 훨씬 쉽게 사용할 수 있다고 생각하고 CompositionApi에 맞는 라이브러리가 아닌가 생각이 든다.&lt;/span&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;font-family: 'Noto Serif KR';&quot;&gt;localStorage에서 'access_token' 값을 가져와 loginApi(토큰을 통해 유저정보 반환 Api)를 호출한다.&lt;/span&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;font-family: 'Noto Serif KR';&quot;&gt;그 후 State값에 추가해 주면 된다.&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;※ catch의 경우&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;catch의 에러 값은 보통 '만료된 jwt' 혹은 '유효하지 않은 jwt'에러를 뱉는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그 이유로 에러를 뱉어준다면 localStorage에 있는 토큰을 지워주는 것으로 상태값을 초기화해준다고 생각하였다.(개인적으론 정답은 아닌 코드라고 생각한다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;@/api/login.js&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1689081196945&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import http from &quot;@/api/http&quot;;

/**
 * 로그인 api
 * @param username
 * @param password
 * @returns {Promise&amp;lt;axios.AxiosResponse&amp;lt;any&amp;gt;&amp;gt;}
 */
export async function login(username, password) {

    const loginDto = {
        username: username,
        password: password
    }
    return await http.post('/api/login', loginDto);
}

/**
 * 토큰을 사용해 유저 정보 api
 * @param token
 * @returns {Promise&amp;lt;void&amp;gt;}
 */
export async function getUserInfo(token) {
    return await http.get('/api/users', {
        headers: {
            Authorization: `Bearer ${token}`
        }
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;axios 통신이다. 프런트엔드에서 토큰값을 전해줄 때 headers에다가 전달해 주는 것을 잊지 않아야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;App.vue&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1689081298097&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    &amp;lt;v-app&amp;gt;
        &amp;lt;NavBar/&amp;gt;
        &amp;lt;AppBar/&amp;gt;
        &amp;lt;v-main&amp;gt;
            &amp;lt;v-container&amp;gt;
                &amp;lt;router-view/&amp;gt;
            &amp;lt;/v-container&amp;gt;
        &amp;lt;/v-main&amp;gt;
    &amp;lt;/v-app&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup&amp;gt;
import NavBar from &quot;@/components/NavBar.vue&quot;;
import AppBar from &quot;@/components/AppBar.vue&quot;;
import {useLoginStore} from &quot;@/store/login&quot;;

const loginStore = useLoginStore();

loginStore.getUserInfo();

&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.v-application {
    background-color: lightgray;
}
&amp;lt;/style&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마지막으로 App.vue에 pinia에서 만든 getUserInfo를 사용하여 토큰값이 존재할 경우 계속해서 사용자의 정보를 유지하는 방식을 만들었다.&lt;/span&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;font-family: 'Noto Serif KR';&quot;&gt;이것으로 새로고침이나 state가 초기화되는 상황에서 만약 사용자의 localStorage에 토큰값이 있다면 유저정보를 가져와 자동으로 로그인할 수 있는 방식으로 만들 수 있게 되었다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Frontend/Vue.js</category>
      <category>JWT</category>
      <category>pinia</category>
      <category>Token</category>
      <category>Vue.js3</category>
      <category>로그인</category>
      <author>Mo'Greene</author>
      <guid isPermaLink="true">https://mo-greene.tistory.com/109</guid>
      <comments>https://mo-greene.tistory.com/109#entry109comment</comments>
      <pubDate>Tue, 11 Jul 2023 22:17:42 +0900</pubDate>
    </item>
  </channel>
</rss>