<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>갱미니의 코딩일지</title>
    <link>https://gengminy.tistory.com/</link>
    <description>코딩</description>
    <language>ko</language>
    <pubDate>Sat, 30 May 2026 14:01:56 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>gengminy</managingEditor>
    <image>
      <title>갱미니의 코딩일지</title>
      <url>https://tistory1.daumcdn.net/tistory/5185881/attach/299b3a9482d74a168a92e522a715777d</url>
      <link>https://gengminy.tistory.com</link>
    </image>
    <item>
      <title>[Spring] x-www-form-urlencoded 요청 JSON 으로 변환하여 받기</title>
      <link>https://gengminy.tistory.com/63</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링을 사용한 프로젝트에서 NHN KCP 결제 시스템을 연동하는 요구사항이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 서버에서는 객체 필드명에 카멜 케이스를 사용하는데, 결제 서버에서는 스네이크 케이스로 보내주는 상황.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 @JsonProperty 로 매핑시키려고 했으나&lt;br /&gt;해당 결제 서버에서 보내는 콜백 요청은 x-www-form-urlencded 를 사용 중이었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Jackson 이 아닌 다른 컨버터가 사용되어 @JsonProperty 가 먹히지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 커스텀 컨버터를 등록하여 JSON 으로 매핑시키도록 하여 해결해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;인코딩&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;application/x-www-form-urlencoded &lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML 의 form 태그에서 서버에 전송할 때 주로 사용되는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 데이터를 서버에 전송하기 전에 URL 인코딩하여 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공백은 '+', 특수문자는 '%x' 형태로 치환되고 키 값 쌍이 '&amp;amp;' 으로 구분되는 형태이다.&lt;/p&gt;
&lt;pre id=&quot;code_1711946943971&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(name=John Doe, age=23)

=&amp;gt; name=John+Doe&amp;amp;age=23&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;application/json&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 형식으로 된 데이터를 전송하는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드와 백엔드를 나누어 API 로 통신할 때 가장 익숙한 형태일 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1711947048352&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;name&quot;: &quot;John Doe&quot;,
    &quot;age&quot;: 23
}&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring 의 요청 파싱 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 은 여러 요청 방법을 Java 코드로 올바르게 파싱할 수 있도록 표준적인 컨버터를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1711948242249&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface HttpMessageConverter&amp;lt;T&amp;gt; {
    boolean canRead(Class&amp;lt;?&amp;gt; clazz, @Nullable MediaType mediaType);

    boolean canWrite(Class&amp;lt;?&amp;gt; clazz, @Nullable MediaType mediaType);

    List&amp;lt;MediaType&amp;gt; getSupportedMediaTypes();

    default List&amp;lt;MediaType&amp;gt; getSupportedMediaTypes(Class&amp;lt;?&amp;gt; clazz) {
        return !this.canRead(clazz, (MediaType)null) &amp;amp;&amp;amp; !this.canWrite(clazz, (MediaType)null) ? Collections.emptyList() : this.getSupportedMediaTypes();
    }

    T read(Class&amp;lt;? extends T&amp;gt; clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;

    void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC 는 HttpMessageConverter 인터페이스를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;canRead, canWrite 메서드를 통해 해당 객체에 적용 가능한지 여부를 파악하는데&lt;br /&gt;여기에서 HTTP Header 의 Content-Type, Accept 를 기반으로 적절한 구현체를 선택하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 인터페이스이기 때문에 WebMvcConfigurer 를 구현하여 얼마든지 확장 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1711948828564&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class FormHttpMessageConverter implements HttpMessageConverter&amp;lt;MultiValueMap&amp;lt;String, ?&amp;gt;&amp;gt; {
    ...
    
    public FormHttpMessageConverter() {
        this.charset = DEFAULT_CHARSET;
        this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
        this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
        this.supportedMediaTypes.add(MediaType.MULTIPART_MIXED);
        this.supportedMediaTypes.add(MediaType.MULTIPART_RELATED);
        this.partConverters.add(new ByteArrayHttpMessageConverter());
        this.partConverters.add(new StringHttpMessageConverter());
        this.partConverters.add(new ResourceHttpMessageConverter());
        this.applyDefaultCharset();
    }
    
    public MultiValueMap&amp;lt;String, String&amp;gt; read(@Nullable Class&amp;lt;? extends MultiValueMap&amp;lt;String, ?&amp;gt;&amp;gt; clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        MediaType contentType = inputMessage.getHeaders().getContentType();
        Charset charset = contentType != null &amp;amp;&amp;amp; contentType.getCharset() != null ? contentType.getCharset() : this.charset;
        String body = StreamUtils.copyToString(inputMessage.getBody(), charset);
        String[] pairs = StringUtils.tokenizeToStringArray(body, &quot;&amp;amp;&quot;);
        MultiValueMap&amp;lt;String, String&amp;gt; result = new LinkedMultiValueMap(pairs.length);
        
        ...

        return result;
    }
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application/x-www-form-urlencoded 요청은 &lt;a href=&quot;https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;FormHttpMessageConverter&lt;/a&gt; 를 사용하여 파싱된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 요청의 데이터는 MultiValueMap 형태로 반환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jackson 관련 코드를 전혀 사용하지 않기 때문에 @JsonProperty 가 동작하지 않았던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1711949049796&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
    ...
    
    public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
        super(objectMapper, new MediaType[]{MediaType.APPLICATION_JSON, new MediaType(&quot;application&quot;, &quot;*+json&quot;)});
    }
}

public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter&amp;lt;Object&amp;gt; {
   ...

    protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType supportedMediaType) {
        this(objectMapper);
        this.setSupportedMediaTypes(Collections.singletonList(supportedMediaType));
    }
    
    public Object read(Type type, @Nullable Class&amp;lt;?&amp;gt; contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        JavaType javaType = this.getJavaType(type, contextClass);
        return this.readJavaType(javaType, inputMessage);
    }
    
    private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
        MediaType contentType = inputMessage.getHeaders().getContentType();
        Charset charset = this.getCharset(contentType);
        ObjectMapper objectMapper = this.selectObjectMapper(javaType.getRawClass(), contentType);
        
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application/json 은 &lt;a href=&quot;https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MappingJackson2HttpMessageConverter&lt;/a&gt; 을 통해 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 ObjectMapper 를 사용하기 때문에 Jackson 관련 어노테이션을 모두 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 form 요청에 Jackson 관련 어노테이션을 사용하려면 Jackson Converter 를 사용하도록 해야 한다.&lt;/p&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Custom FormHttpMessageConverter&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 스프링의 Http 요청 파싱 방법을 알았으니 이를 응용하여 커스텀 컨버터를 구현해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1711953116720&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonFormData {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 모든 form 요청에 대해 적용하는 것이 아닌 특정 요청에 대해서만 적용할 것이기 때문에&lt;br /&gt;이를 지정하기 위한 커스텀 어노테이션을 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1711953132838&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SuppressWarnings(&quot;all&quot;)
public class JsonFormUrlEncodedConverter&amp;lt;T&amp;gt; extends AbstractHttpMessageConverter&amp;lt;T&amp;gt; {

    /** JSON 매핑 안된 속성 오류 무시 */
    private static final ObjectMapper objectMapper =
            new ObjectMapper().configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

    private static final FormHttpMessageConverter converter = new FormHttpMessageConverter();

    @Override
    protected boolean supports(Class&amp;lt;?&amp;gt; clazz) {
        return clazz.isAnnotationPresent(JsonFormData.class);
    }

    @Override
    protected T readInternal(Class&amp;lt;? extends T&amp;gt; clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        Map&amp;lt;String, String&amp;gt; vals = converter.read(null, inputMessage).toSingleValueMap();

        return objectMapper.convertValue(vals, clazz);
    }

    @Override
    protected void writeInternal(T clazz, HttpOutputMessage outputMessage)
            throws HttpMessageNotWritableException {}
}&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;AbstractHttpMessageConverter 를 상속하고 FormHttpMessageConverter 를 내부적으로 주입한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;supports 메소드를 통해 convert 대상을&amp;nbsp;&lt;b&gt;JsonFormData&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;그리고 Map 으로 변환된 폼 요청 데이터를 objectMapper 를 통해 json 으로 변환하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1711953348623&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class JsonFormUrlEncodedConverterConfiguration implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List&amp;lt;HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt; converters) {
        JsonFormUrlEncodedConverter&amp;lt;?&amp;gt; converter = new JsonFormUrlEncodedConverter&amp;lt;&amp;gt;();
        MediaType mediaType = new MediaType(APPLICATION_FORM_URLENCODED, UTF_8);
        converter.setSupportedMediaTypes(List.of(mediaType));
        converters.add(converter);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 Spring MVC 에 해당 컨버터를 등록해주면 끝이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebMvcConfigurer 구현체를 만들어 MessageConverter 를 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1711953297194&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@NoArgsConstructor
@JsonFormData
public class UserRequest {
    @JsonProperty(&quot;user_name&quot;)
    @NotNull
    private String userName;

    @JsonProperty(&quot;user_phone&quot;)
    private String userPhone;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@JsonFormData 를 적용하고자 하는 클래스의 메타 어노테이션으로 지정해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1711953317875&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/user&quot;)
public class UserController {
    @PostMapping(value = &quot;/callback&quot;, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public ResponseEntity&amp;lt;?&amp;gt; callback(@Valid @RequestBody UserRequest userRequest) {
        return ResponseEntity.ok(UserResponse.builder()
                .userName(userRequest.getUserName())
                .userPhone(userRequest.getUserPhone())
                .build());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러 단에서는 consumes 와 @RequestBody 를 지정해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 구현할 경우 java validation 의 @Valid 동작도 올바르게 수행되어 요청 검증이 쉬워진다.&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-04-01 15.39.07.png&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;930&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/150tB/btsGeKpHpE7/vN3K7QXMezNEWOHh7bBYdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/150tB/btsGeKpHpE7/vN3K7QXMezNEWOHh7bBYdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/150tB/btsGeKpHpE7/vN3K7QXMezNEWOHh7bBYdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F150tB%2FbtsGeKpHpE7%2FvN3K7QXMezNEWOHh7bBYdK%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;1328&quot; height=&quot;930&quot; data-filename=&quot;스크린샷 2024-04-01 15.39.07.png&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;930&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application/x-www-form-urlencoded + @RequestBody 를 사용했음에도 요청이 올바르게 파싱된 모습&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-04-01 15.39.21.png&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;984&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6rsIA/btsGfil24o6/6FJDnKN11tvJj2VjU940MK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6rsIA/btsGfil24o6/6FJDnKN11tvJj2VjU940MK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6rsIA/btsGfil24o6/6FJDnKN11tvJj2VjU940MK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6rsIA%2FbtsGfil24o6%2F6FJDnKN11tvJj2VjU940MK%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;1324&quot; height=&quot;984&quot; data-filename=&quot;스크린샷 2024-04-01 15.39.21.png&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;984&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-04-01 15.41.12.png&quot; data-origin-width=&quot;2338&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOqiDP/btsGh4UoLb5/TUiFYF7goPyrdRmqufpiW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOqiDP/btsGh4UoLb5/TUiFYF7goPyrdRmqufpiW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOqiDP/btsGh4UoLb5/TUiFYF7goPyrdRmqufpiW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOqiDP%2FbtsGh4UoLb5%2FTUiFYF7goPyrdRmqufpiW1%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;2338&quot; height=&quot;410&quot; data-filename=&quot;스크린샷 2024-04-01 15.41.12.png&quot; data-origin-width=&quot;2338&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Validation 의 @NotNull 도 올바르게 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;코드 컨벤션이 다른&amp;nbsp;&lt;/span&gt;외부 API 를 연동하며 내부적인 컨벤션을 지키기 위해 고민했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 과정에서 Spring 이 HttpMessageConverter 를 사용하고 확장하는 방식에 대해 알 수 있었다.&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;</description>
      <category>  백엔드/  Spring Boot</category>
      <category>application/json</category>
      <category>Converter</category>
      <category>HttpMessageConverter</category>
      <category>Jackson</category>
      <category>JSON</category>
      <category>MediaType</category>
      <category>Spring</category>
      <category>x-www-form-urlencoded</category>
      <category>스프링</category>
      <category>컨버터</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/63</guid>
      <comments>https://gengminy.tistory.com/63#entry63comment</comments>
      <pubDate>Mon, 1 Apr 2024 15:48:55 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Jackson Module 을 이용한 Jackson 확장</title>
      <link>https://gengminy.tistory.com/62</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Custom Serializer / Deserializer 를 만들어 Jackson Module 에 등록하고,&lt;br /&gt;이를 통해 Jackson 을 확장하여 기본 JSON 처리 방식을 변경하는 방법에 대한 글입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java Spring 서버를 개발하다보면 Jackson 이라는 라이브러리를 많이 들어봤을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jackson 은 Java 진영에서 JSON 처리를 담당하는 라이브러리이다.&lt;br /&gt;Java 객체를 JSON 으로 직렬화, 혹은 반대로 역직렬화할 수 있는 데이터 바인딩 기능을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 REST API 처리 방식에 JSON 요청, 응답을 가장 많이 사용하기 때문에 중요한 라이브러리라 할 수 있다.&lt;br /&gt;그래서인지 현재 스프링 프레임워크에도 Jackson 이 기본적으로 탑재되어 있기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Jackson Module&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jackson Module 은 Jackson 의 확장을 위한 추상 클래스로&lt;br /&gt;커스텀 직렬화, 역직렬화, 타입 변환기를 Jackson 에 등록하여 확장할 수 있도록 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1711368586173&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract class Module implements Versioned {
	
    ...
    
    public abstract void setupModule(SetupContext var1);

    
    public interface SetupContext {
        void addDeserializers(Deserializers var1);

        void addKeyDeserializers(KeyDeserializers var1);

        void addSerializers(Serializers var1);
    
        void addKeySerializers(Serializers var1);
        
        void setNamingStrategy(PropertyNamingStrategy var1);
        
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jackson 은 이전 버전의 Java 에서 구현되었기 때문에&lt;br /&gt;Java 8 에서 도입된 java.time 패키지를 자동으로 처리하지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위한 jackson-datatype-jsr310 모듈의 JavaTimeModule 도 Jackson Module 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Kotlin 에서 자주 사용하는 jackson-module-kotlin 도&lt;br /&gt;자주 사용되는 Jackson Module 중 하나이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 Module 을 구현하고 ObjectMapper 에 등록하면 사용할 수 있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jackson 2.10 이상에서는 Jackson Module 을 Bean 으로 등록하기만 하면&lt;br /&gt;ObjectMapper 에서 ServiceLoader 를 사용하여 classpath 상 모든 Module 구현체를 자동으로 탐색하고 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 ObjectMapper 는 JSON 을 바인딩하는 과정에서&lt;br /&gt;Context 에 등록된 Module 을 탐색하고 적절한 모듈을 찾아 바인딩하는데 사용하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1711370198672&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public JsonDeserializer&amp;lt;?&amp;gt; createEnumDeserializer(DeserializationContext ctxt, JavaType type, BeanDescription beanDesc) throws JsonMappingException {
    DeserializationConfig config = ctxt.getConfig();
    Class&amp;lt;?&amp;gt; enumClass = type.getRawClass();
    JsonDeserializer&amp;lt;?&amp;gt; deser = this._findCustomEnumDeserializer(enumClass, config, beanDesc);
    if (deser == null) {
        if (enumClass == Enum.class) {
            return AbstractDeserializer.constructForNonPOJO(beanDesc);
        }

        ...
    }

    ...

    return (JsonDeserializer)deser;
}

protected JsonDeserializer&amp;lt;?&amp;gt; _findCustomEnumDeserializer(Class&amp;lt;?&amp;gt; type, DeserializationConfig config, BeanDescription beanDesc) throws JsonMappingException {
    Iterator var4 = this._factoryConfig.deserializers().iterator();

    JsonDeserializer deser;
    do {
        if (!var4.hasNext()) {
            return null;
        }

        Deserializers d = (Deserializers)var4.next();
        deser = d.findEnumDeserializer(type, config, beanDesc);
    } while(deser == null);

    return deser;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 &lt;a href=&quot;https://github.com/FasterXML/jackson-databind/blob/2.13/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java#L1645-L1653&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BasicDeserializerFactory.class&lt;/a&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;의 내부 구현 중 EnumDeserializer 에 대한 코드 일부이다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;사용자 정의 모듈을 최우선으로 선택하며 없을 경우 기본 Enum 처리 모듈을 사용하는 것을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1711370537104&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public &amp;lt;T&amp;gt; MappingIterator&amp;lt;T&amp;gt; readValues(JsonParser p, JavaType valueType) throws IOException {
    this._assertNotNull(&quot;p&quot;, p);
    DeserializationConfig config = this.getDeserializationConfig();
    DeserializationContext ctxt = this.createDeserializationContext(p, config);
    JsonDeserializer&amp;lt;?&amp;gt; deser = this._findRootDeserializer(ctxt, valueType);
    return new MappingIterator(valueType, p, ctxt, deser, false, (Object)null);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ObjectMapper 는 Context 를 가져와 현재 존재하는 Module 을 탐색하고&lt;br /&gt;가장 적절한 모듈을 가져와 JSON 매핑을 처리하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 눈여겨봐야 할 점은 Deserializers 클래스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1711370826584&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Deserializers {
    JsonDeserializer&amp;lt;?&amp;gt; findEnumDeserializer(Class&amp;lt;?&amp;gt; var1, DeserializationConfig var2, BeanDescription var3) throws JsonMappingException;
    
    ...
    
    public abstract static class Base implements Deserializers {
        public Base() {
        }

        public JsonDeserializer&amp;lt;?&amp;gt; findEnumDeserializer(Class&amp;lt;?&amp;gt; type, DeserializationConfig config, BeanDescription beanDesc) throws JsonMappingException {
            return null;
        }
    }
}&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;a href=&quot;https://github.com/FasterXML/jackson-databind/blob/2.13/src/main/java/com/fasterxml/jackson/databind/deser/Deserializers.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Deserializers.class&lt;/a&gt; 는 JsonDeserializer 를 리턴하여&lt;br /&gt;해당 클래스 타입에 맞는 Deserializaer 를 반환하거나&lt;br /&gt;null 을 리턴하여 처리를 해당 처리기가 아닌 다른 Deserializer 에게 넘길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 내부 추상 메서드인 Base.class 를 구현하도록 강력하게 권장하고 있는데&lt;br /&gt;나머지 타입에 대해 기본 처리기 구현체가 이미 만들어져 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Deserializers.Base 를 구현하는 것을 통해&lt;br /&gt;커스텀 Deserializer 를 구현하여 Jackson Module 로 등록할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이를 구현해서 실제 Module 로 등록해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Deserializer 등록&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1711371567239&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class EnumDeserializer extends StdDeserializer&amp;lt;Enum&amp;gt; implements ContextualDeserializer {

    public EnumDeserializer() {
        this(null);
    }

    protected EnumDeserializer(Class&amp;lt;?&amp;gt; vc) {
        super(vc);
    }

    @SuppressWarnings(&quot;unchecked&quot;)
    @Override
    public Enum deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
        JsonNode jsonNode = jp.getCodec().readTree(jp);
        String text = jsonNode.asText();
        Class&amp;lt;? extends Enum&amp;gt; enumType = (Class&amp;lt;? extends Enum&amp;gt;) this._valueClass;
        return Arrays.stream(enumType.getEnumConstants()).filter(e -&amp;gt; {
            return e.name().equals(text);
        }).findAny().map(it -&amp;gt; Enum.valueOf(enumType, it.name())).orElse(null);
    }

    @Override
    public JsonDeserializer&amp;lt;?&amp;gt; createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
        return new EnumDeserializer(property.getType().getRawClass());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 Custom Enum Deserializer 를 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 역직렬화기는 기대했던 Enum 타입 내부에 존재하는 값이 아닐 경우&lt;br /&gt;JsonParseError 를 던지지 않고 null 값을 리턴하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Request Body 의 Enum 오류 처리를 위한 역직렬화기이고 프로젝트의 전역 에러 핸들링 설정으로 인해 사용하게 되었다.&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;a href=&quot;https://gengminy.tistory.com/49&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://gengminy.tistory.com/49&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1711371729579&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;[Spring] 스프링 Custom Enum Deserializer 구현으로 JSON Enum null 로 파싱하기&quot; data-og-description=&quot;https://gengminy.tistory.com/48 [Spring] 스프링 Enum Validator Reflection 으로 개선 및 구현하기 https://gengminy.tistory.com/47 [Spring] 스프링에서 Enum 클래스 Validation 추가하기 (Enum JSON parse error 해결) 스프링에서 일&quot; data-og-host=&quot;gengminy.tistory.com&quot; data-og-source-url=&quot;https://gengminy.tistory.com/49&quot; data-og-url=&quot;https://gengminy.tistory.com/49&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eWFLk/hyVDtlu8FC/L5KSC14oKyBpTkibEdlIw0/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/bYdiF8/hyVDFM0ybe/I5CcKLLStQzEYc8Ee63ai0/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/e5Bgd/hyVGNii1q0/YEbSKbTIUoktZNHMU1g5wK/img.png?width=2172&amp;amp;height=378&amp;amp;face=0_0_2172_378&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/49&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gengminy.tistory.com/49&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eWFLk/hyVDtlu8FC/L5KSC14oKyBpTkibEdlIw0/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/bYdiF8/hyVDFM0ybe/I5CcKLLStQzEYc8Ee63ai0/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/e5Bgd/hyVGNii1q0/YEbSKbTIUoktZNHMU1g5wK/img.png?width=2172&amp;amp;height=378&amp;amp;face=0_0_2172_378');&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;[Spring] 스프링 Custom Enum Deserializer 구현으로 JSON Enum null 로 파싱하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://gengminy.tistory.com/48 [Spring] 스프링 Enum Validator Reflection 으로 개선 및 구현하기 https://gengminy.tistory.com/47 [Spring] 스프링에서 Enum 클래스 Validation 추가하기 (Enum JSON parse error 해결) 스프링에서 일&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gengminy.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;이제 이 Deserializer 를 Module 안에 포함시킨다.&lt;/p&gt;
&lt;pre id=&quot;code_1711371821607&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class JacksonConfig {
    @Bean
    public Module customEnumModule() {
        return new Module() {
            @Override
            public String getModuleName() {
                return &quot;customEnumModule&quot;;
            }

            @Override
            public Version version() {
                return Version.unknownVersion();
            }

            @Override
            public void setupModule(SetupContext context) {
                context.addDeserializers(new Deserializers.Base() {
                    final Map&amp;lt;ClassKey, JsonDeserializer&amp;lt;?&amp;gt;&amp;gt; cache = new ConcurrentHashMap&amp;lt;&amp;gt;();

                    @Override
                    public JsonDeserializer&amp;lt;?&amp;gt; findEnumDeserializer(Class&amp;lt;?&amp;gt; type, DeserializationConfig config, BeanDescription beanDesc) {
                        if (Enum.class.isAssignableFrom(type)) {
                            JsonDeserializer&amp;lt;?&amp;gt; enumDeserializer = new EnumDeserializer(type);
                            addDeserializer(type, enumDeserializer);
                            return enumDeserializer;
                        }
                        return null;
                    }

                    @Override
                    public boolean hasDeserializerFor(DeserializationConfig config, Class&amp;lt;?&amp;gt; valueType) {
                        return cache.containsKey(new ClassKey(valueType));
                    }

                    public void addDeserializer(Class&amp;lt;?&amp;gt; forClass, JsonDeserializer&amp;lt;?&amp;gt; deserializer) {
                        ClassKey key = new ClassKey(forClass);
                        cache.put(key, deserializer);
                    }
                });
            }
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Module 을 생성하고 필요한 추상 메서드를 구현한다.&lt;br /&gt;이후 SetupContext 에서 Deserializers.Base() 를 인스턴스화 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부에는 findEnumDeserializer 를 오버라이딩하여 원하는 타입에 대해 Custom Deserializer 를 반환하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 Deserialzer 는 Lazy 하게 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시를 두어 매번 역직렬화기를 생성하지 않고 이미 존재하는 Deserializer 를 가져와 처리하도록 할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jackson Module 을 알아보며 Custom Deserializer 를 Jackson Module 을 통해 등록하는 방식이&lt;br /&gt;Jackson 이 의도하는 확장 방식에 가깝다고 확실히 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 @JsonCreator, @JsonDeserialize 등의 어노테이션을 사용하는 것 보다는&lt;br /&gt;이처럼 전역적인 모듈 설정을 등록하는 것으로 중복을 없애고 깔끔하게 POJO 를 구성할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝&lt;/p&gt;</description>
      <category>  백엔드/  Spring Boot</category>
      <category>deserializer</category>
      <category>Jackson</category>
      <category>jackson-module</category>
      <category>JSON</category>
      <category>module</category>
      <category>Serializer</category>
      <category>Spring</category>
      <category>스프링</category>
      <category>역직렬화</category>
      <category>파싱</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/62</guid>
      <comments>https://gengminy.tistory.com/62#entry62comment</comments>
      <pubDate>Mon, 25 Mar 2024 22:19:42 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Swagger 공통 응답 예시 커스터마이징</title>
      <link>https://gengminy.tistory.com/61</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;SuccessResponse 를 @RestControllerAdvice 를 이용해 전역 처리 중이며&lt;br /&gt;Swagger Example 커스터마이징을 하고 싶은 경우&lt;br /&gt;이 글을 읽어주시면 도움이 될 것 입니다.&lt;span style=&quot;color: #000000; font-size: 1.62em; letter-spacing: -1px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Swagger 를 사용중이며, @RestControllerAdvice 를 사용해 전역 응답을 설정중인 경우&lt;br /&gt;Swagger 응답 예시에서 wrapping 시켜주는 객체가 나타나지 않는다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;최근에는 springfox 공식 업데이트가 멈췄고 springdocs 를 많이 사용중이다.&lt;br /&gt;springfox 같은 경우 기본 설정으로 제네릭 클래스를 응답으로 선택할 수 있는데&lt;br /&gt;springdocs 에서는 이를 지원해주지 않아 다른 방법을 사용해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;SuccessResponseAdvice 예시&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Getter
@NoArgsConstructor
public class SuccessResponse&amp;lt;T&amp;gt; {

    private final boolean success = true;
    private final LocalDateTime timeStamp = LocalDateTime.now();
    private int status;
    private T data;

    public SuccessResponse(T data) {
        this.status = 200;
        this.data = data;
    }

    public SuccessResponse(T data, int status) {
        this.status = status;
        this.data = data;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;zephir&quot;&gt;&lt;code&gt;@RestControllerAdvice(basePackages = &quot;kr.co.swagger&quot;)
public class SuccessResponseAdvice implements ResponseBodyAdvice&amp;lt;Object&amp;gt; {
    @Override
    public boolean supports(MethodParameter returnType, Class&amp;lt;? extends HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt; converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(
            Object body,
            MethodParameter returnType,
            MediaType selectedContentType,
            Class&amp;lt;? extends HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt; selectedConverterType,
            ServerHttpRequest request,
            ServerHttpResponse response) {
        final HttpServletResponse servletResponse =
                ((ServletServerHttpResponse) response).getServletResponse();

        int status = servletResponse.getStatus();
        final HttpStatus resolve = HttpStatus.resolve(status);

        if (resolve == null) {
            return body;
        }

        if (resolve.is2xxSuccessful()) {
            return new SuccessResponse&amp;lt;&amp;gt;(body, status);
        }

        return body;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시 응답&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2592&quot; data-origin-height=&quot;1296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbkSna/btsFQgWCEf1/3lx5ciDcg0kRp16SqH4bK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbkSna/btsFQgWCEf1/3lx5ciDcg0kRp16SqH4bK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbkSna/btsFQgWCEf1/3lx5ciDcg0kRp16SqH4bK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbkSna%2FbtsFQgWCEf1%2F3lx5ciDcg0kRp16SqH4bK1%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;2592&quot; height=&quot;1296&quot; data-origin-width=&quot;2592&quot; data-origin-height=&quot;1296&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제 응답&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2538&quot; data-origin-height=&quot;1214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ngfHK/btsFP3wuYlI/1oOU0YKAYdJGFGIRhVL7nK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ngfHK/btsFP3wuYlI/1oOU0YKAYdJGFGIRhVL7nK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ngfHK/btsFP3wuYlI/1oOU0YKAYdJGFGIRhVL7nK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FngfHK%2FbtsFP3wuYlI%2F1oOU0YKAYdJGFGIRhVL7nK%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;2538&quot; height=&quot;1214&quot; data-origin-width=&quot;2538&quot; data-origin-height=&quot;1214&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이럴 경우 swagger 상에서 설정을 바꿔줘서 기본 예시를 wrapping 할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;Swagger 동작 원리&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;{host}/v3/api-docs&lt;/b&gt; 로 접속할 경우 아래와 같이 swagger-ui 로 변환되기 이전의 JSON 데이터가 노출된다.&lt;br /&gt;이를 바탕으로 스웨거는 화면에 API 문서를 띄우게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1464&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AsJFE/btsFPI0mYaY/NsvBCkqiWEgWe9X5L1s1T0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AsJFE/btsFPI0mYaY/NsvBCkqiWEgWe9X5L1s1T0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AsJFE/btsFPI0mYaY/NsvBCkqiWEgWe9X5L1s1T0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAsJFE%2FbtsFPI0mYaY%2FNsvBCkqiWEgWe9X5L1s1T0%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;1464&quot; height=&quot;408&quot; data-origin-width=&quot;1464&quot; data-origin-height=&quot;408&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;즉 이 JSON 데이터를 설정해준다면 스웨거가 그리는 화면을 수정할 수 있다는 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;Swagger Customizer&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;1406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZjhIb/btsFSZeKviD/T38v8zSucbKl2jNxP6TUDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZjhIb/btsFSZeKviD/T38v8zSucbKl2jNxP6TUDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZjhIb/btsFSZeKviD/T38v8zSucbKl2jNxP6TUDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZjhIb%2FbtsFSZeKviD%2FT38v8zSucbKl2jNxP6TUDk%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;1316&quot; height=&quot;1406&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;1406&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;스웨거에서는 customizer 라는 함수형 인터페이스를 제공하는데&lt;br /&gt;이를 빈으로 등록하게 될 경우 커스터마이징을 할 수 있다&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 중에서 컨트롤러 메소드를 처리하는 OperationCustomizer 를 사용할 것이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface OperationCustomizer {
    Operation customize(Operation operation, HandlerMethod handlerMethod);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;공통 응답 랩핑 처리&lt;/h2&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;@Configuration
public class SwaggerConfig {
    ...

    @Bean
    public OperationCustomizer operationCustomizer() {
        return (operation, handlerMethod) -&amp;gt; {
            this.addResponseBodyWrapperSchemaExample(operation);
            return operation;
        };
    }

    private void addResponseBodyWrapperSchemaExample(Operation operation) {
        final Content content = operation.getResponses().get(&quot;200&quot;).getContent();
        if (content != null) {
            content.forEach((mediaTypeKey, mediaType) -&amp;gt; {
                Schema&amp;lt;?&amp;gt; originalSchema = mediaType.getSchema();
                Schema&amp;lt;?&amp;gt; wrappedSchema = wrapSchema(originalSchema);
                mediaType.setSchema(wrappedSchema);
            });
        }
    }

    private Schema&amp;lt;?&amp;gt; wrapSchema(Schema&amp;lt;?&amp;gt; originalSchema) {
        final Schema&amp;lt;?&amp;gt; wrapperSchema = new Schema&amp;lt;&amp;gt;();

        wrapperSchema.addProperty(&quot;success&quot;, new Schema&amp;lt;&amp;gt;().type(&quot;boolean&quot;).example(true));
        wrapperSchema.addProperty(&quot;timeStamp&quot;, new Schema&amp;lt;&amp;gt;().type(&quot;string&quot;).format(&quot;date-time&quot;).example(
                LocalDateTime.now().toString()));
        wrapperSchema.addProperty(&quot;status&quot;, new Schema&amp;lt;&amp;gt;().type(&quot;integer&quot;).example(200));
        wrapperSchema.addProperty(&quot;data&quot;, originalSchema);

        return wrapperSchema;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;operation 의 getResponses() 를 통해 예시 응답 리스트를 꺼내고 그 중에서 200 성공 응답에 대해 가져온다.&lt;br /&gt;JSON 에서 각 컨트롤러 메소드의 예시는 응답 코드 쌍으로 그룹핑된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그 후 내부에서는 각 mediaType 별로 그룹핑되어 있는데, 이 중에서 Schema 를 꺼내면 원본 예시가 나오게 된다.&lt;br /&gt;이를 새로운 응답으로 랩핑해주면 원하는 결과가 나올 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;기존 JSON&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;...        
        &quot;/user&quot;: {
            &quot;get&quot;: {
                &quot;tags&quot;: [
                    &quot;사용자 API&quot;
                ],
                &quot;summary&quot;: &quot;사용자 정보 조회&quot;,
                &quot;description&quot;: &quot;사용자 정보를 조회합니다.&quot;,
                &quot;operationId&quot;: &quot;getUser&quot;,
                &quot;responses&quot;: {
                    &quot;200&quot;: {
                        &quot;description&quot;: &quot;OK&quot;,
                        &quot;content&quot;: {
                            &quot;application/json;charset=UTF-8&quot;: {
                                &quot;schema&quot;: {
                                    &quot;$ref&quot;: &quot;#/components/schemas/UserResponse&quot;
                                }
                            }
                        }
                    }
                }
            }
        }
...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;새로운 JSON&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;...
			&quot;/user&quot;: {
            &quot;get&quot;: {
                &quot;tags&quot;: [
                    &quot;사용자 API&quot;
                ],
                &quot;summary&quot;: &quot;사용자 정보 조회&quot;,
                &quot;description&quot;: &quot;사용자 정보를 조회합니다.&quot;,
                &quot;operationId&quot;: &quot;getUser&quot;,
                &quot;responses&quot;: {
                    &quot;200&quot;: {
                        &quot;description&quot;: &quot;OK&quot;,
                        &quot;content&quot;: {
                            &quot;application/json;charset=UTF-8&quot;: {
                                &quot;schema&quot;: {
                                    &quot;properties&quot;: {
                                        &quot;success&quot;: {
                                            &quot;type&quot;: &quot;boolean&quot;,
                                            &quot;example&quot;: true
                                        },
                                        &quot;timeStamp&quot;: {
                                            &quot;type&quot;: &quot;string&quot;,
                                            &quot;format&quot;: &quot;date-time&quot;,
                                            &quot;example&quot;: &quot;2024-03-17T18:30:50.334396&quot;
                                        },
                                        &quot;status&quot;: {
                                            &quot;type&quot;: &quot;integer&quot;,
                                            &quot;example&quot;: 200
                                        },
                                        &quot;data&quot;: {
                                            &quot;$ref&quot;: &quot;#/components/schemas/UserResponse&quot;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-03-17 19.46.03.png&quot; data-origin-width=&quot;2862&quot; data-origin-height=&quot;1528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/diRB7O/btsFPGVLjYg/SziF0QQ2ASQK7fGrTJEA31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/diRB7O/btsFPGVLjYg/SziF0QQ2ASQK7fGrTJEA31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/diRB7O/btsFPGVLjYg/SziF0QQ2ASQK7fGrTJEA31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdiRB7O%2FbtsFPGVLjYg%2FSziF0QQ2ASQK7fGrTJEA31%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;2862&quot; height=&quot;1528&quot; data-filename=&quot;스크린샷 2024-03-17 19.46.03.png&quot; data-origin-width=&quot;2862&quot; data-origin-height=&quot;1528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;각 스키마에 property 가 지정되며 응답이 성공적으로 wrapping 되었다.&lt;br /&gt;다만 각 필드를 삽입하는 과정에서 텍스트를 하드코딩하게 되는 문제가 있는데 리플렉션으로 해결해보았다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@Configuration
public class SwaggerConfig {
    ...
    
    @Bean
    public OperationCustomizer operationCustomizer() {
        return (operation, handlerMethod) -&amp;gt; {
            this.addResponseBodyWrapperSchemaExample(operation, SuccessResponse.class, &quot;data&quot;);
            return operation;
        };
    }
    
    private void addResponseBodyWrapperSchemaExample(Operation operation, Class&amp;lt;?&amp;gt; type, String wrapFieldName) {
        final Content content = operation.getResponses().get(&quot;200&quot;).getContent();
        if (content != null) {
            content.keySet()
                    .forEach(mediaTypeKey -&amp;gt; {
                        final MediaType mediaType = content.get(mediaTypeKey);
                        mediaType.schema(wrapSchema(mediaType.getSchema(), type, wrapFieldName));
                    });
        }
    }
    
    @SneakyThrows
    private &amp;lt;T&amp;gt; Schema&amp;lt;T&amp;gt; wrapSchema(Schema&amp;lt;?&amp;gt; originalSchema, Class&amp;lt;T&amp;gt; type, String wrapFieldName) {
        final Schema&amp;lt;T&amp;gt; wrapperSchema = new Schema&amp;lt;&amp;gt;();
        final T instance = type.getDeclaredConstructor().newInstance();
        
        for (Field field : type.getDeclaredFields()) {
            field.setAccessible(true);
            wrapperSchema.addProperty(field.getName(), new Schema&amp;lt;&amp;gt;().example(field.get(instance)));
            field.setAccessible(false);
        }
        wrapperSchema.addProperty(wrapFieldName, originalSchema);
        return wrapperSchema;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 되면 SuccessResponse 필드 코드가 바뀌었을 때에도 조치가 가능하다.&lt;br /&gt;이 부분은 최초 Swagger 로딩시에만 리플렉션이 동작하고 이후에는 캐싱되기 때문에 어느정도 감수할만 하다고 생각해서 적용해보았다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;스웨거 커스터마이징에 익숙해진다면 더 좋은 방법이 있을 수도 있을 것 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;스웨거 응답을 커스터마이징하여 API 문서의 공통 응답을 변경하는 방법을 알아보았다.&lt;br /&gt;이외에도 예외 처리에 대한 문서 자동화, 복잡한 처리 등이 가능하니 잘 찾아본다면 더 깔끔하고 정확한 스웨거 문서를 작성할 수 있겠다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  백엔드/  Spring Boot</category>
      <category>APIdocs</category>
      <category>API명세서</category>
      <category>api문서</category>
      <category>Spring</category>
      <category>swagger</category>
      <category>명세자동화</category>
      <category>스웨거</category>
      <category>스웨거설정</category>
      <category>스프링</category>
      <category>스프링스웨거</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/61</guid>
      <comments>https://gengminy.tistory.com/61#entry61comment</comments>
      <pubDate>Sun, 17 Mar 2024 18:46:49 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] spring-data-envers 를 이용한 엔티티 변경 이력 관리</title>
      <link>https://gengminy.tistory.com/60</link>
      <description>&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;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c76onv/btsFF7dONWz/E6ueSwHz7ELXNKg1Pxkru1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c76onv/btsFF7dONWz/E6ueSwHz7ELXNKg1Pxkru1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c76onv/btsFF7dONWz/E6ueSwHz7ELXNKg1Pxkru1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc76onv%2FbtsFF7dONWz%2FE6ueSwHz7ELXNKg1Pxkru1%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;500&quot; height=&quot;286&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&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;보통 history 테이블을 따로 만들어 이것들을 관리하곤 하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 를 사용중이라면 spring-data-envers 를 통해 이를 편리하게 설정하고 관리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring-data-envers 는 hibernate-envers 의 wrapping 프로젝트로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;envers 를 편리하게 사용할 수 있는 기능(RevisionRepository, 메타데이터 조회)을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Dependency (gradle)&lt;/h2&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;implementation(&quot;org.springframework.data:spring-data-envers&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Auditing 활성화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사를 진행할 엔티티에 &lt;code&gt;@Audited&lt;/code&gt; 를 추가한다&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
@Entity
@Audited
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;
}&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;code&gt;@Audited&lt;/code&gt; 를 붙이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정해진 필드만 관리하고 싶다면 각 필드에 대해 &lt;code&gt;@Audited&lt;/code&gt; 어노테이션을 붙이면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 전체 필드 중 제외하고 싶은 필드가 있다면 &lt;code&gt;@NotAudited&lt;/code&gt; 를 붙이면 감사 대상에서 제외된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;@Audited
private String name;

@NotAudited
private String email;&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;이력 조회 대상에 대해 변경되었는지 여부를 flag 를 통해 관리할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 flag 를 조회해서 해당 버전에서 어떤 엔티티가 바뀌었는지 boolean 값으로 관리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Audited(withModifiedFlag = true, modifiedColumnName = &quot;nameChanged&quot;)
private String 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;&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;원래는 연관관계 테이블도 반드시 @Audited 를 통해 이력 관리 대상으로 추가해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 @NotAudited&lt;span style=&quot;text-align: start;&quot;&gt; 로 아예 제외시키거나&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;targetAuditMode 를 NOT_AUDITED 로 설정하여 fk 값만 저장하도록 할 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1710069207749&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// FK 만 이력 조회 대상으로 한다
@Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
@ManyToOne(fetch=FetchType.LAZY)
private Department department;

// 이력 관리 대상에서 제외한다
@NotAudited
private Department department;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. RevInfo 테이블&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2266&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7ebJO/btsFGpFh7tk/IQJwXR3AQQRmRiQUL6uuRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7ebJO/btsFGpFh7tk/IQJwXR3AQQRmRiQUL6uuRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7ebJO/btsFGpFh7tk/IQJwXR3AQQRmRiQUL6uuRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7ebJO%2FbtsFGpFh7tk%2FIQJwXR3AQQRmRiQUL6uuRk%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;2266&quot; height=&quot;190&quot; data-origin-width=&quot;2266&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hibernate.ddl-auto 옵션이 create 또는 update 일 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;envers 는 자동으로 REVINFO 테이블과 ***_AUD 테이블을 생성한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPkUHk/btsFHs9rsZT/1VVg8xtXw6BFMdbsjavpu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPkUHk/btsFHs9rsZT/1VVg8xtXw6BFMdbsjavpu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPkUHk/btsFHs9rsZT/1VVg8xtXw6BFMdbsjavpu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPkUHk%2FbtsFHs9rsZT%2F1VVg8xtXw6BFMdbsjavpu0%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;510&quot; height=&quot;178&quot; data-origin-width=&quot;510&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;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;create table REVINFO (
    REV integer not null auto_increment, 
    REVTSTMP bigint, 
    primary key (REV)
) engine=InnoDB

create table User_AUD (
    REV integer not null, 
    REVTYPE tinyint, 
    nameChanged bit, 
    id bigint not null, 
    name varchar(255), 
    primary key (REV, id)
) engine=InnoDB

alter table User_AUD 
    add constraint FKilft2rdosb65jocpcoan7xnjq 
        foreign key (REV) references REVINFO (REV)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REVINFO 는 envers 의 버전 관리 정보로 버전 번호인 REV, 그리고 수정 시간을 REVSTMP 으로 기록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이력 관리에 대한 테이블은 ****_AUD 로 생성되는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REVINFO 에 대한 FK 로 REV 를 가지고 있으며 원래 엔티티의 PK 와 복합키 쌍으로 관리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REVTYPE 은 수정 타입으로 다음과 같이 관리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;public enum RevisionType {
    ADD((byte)0), // 추가
    MOD((byte)1), // 수정
    DEL((byte)2); // 삭제

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 네이밍 방식을 바꾸고 싶거나 REVINFO 엔티티를 변경하고 싶다면 커스텀이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;envers 를 사용한다면 REVINFO 엔티티의 커스텀은 거의 필수적인데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세히 보면 REVINFO 엔티티의 PK 인 REV 의 타입은 Integer 이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이력 조회에 경우 실제 운영단계에서는 잦은 추가가 발생하기 때문에 금방 INT_MAX 에 도달하게 되고,&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 class=&quot;less&quot;&gt;&lt;code&gt;@Getter
@Entity
@RevisionEntity
@Table(name = &quot;RevisionHistory&quot;)
public class CustomRevisionEntity {
    @Id
    @Column
    @RevisionNumber
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long revisionId;

    @Column @RevisionTimestamp
    private Long updatedAt;

    public LocalDateTime getUpdatedAt() {
        return LocalDateTime.ofInstant(Instant.ofEpochMilli(updatedAt), ZoneId.systemDefault());
    }
}&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;REVINFO 를 &lt;code&gt;@RevisionEntity&lt;/code&gt; 를 통해 재정의할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REV 는 &lt;code&gt;@RevisionNumber&lt;/code&gt; , REVTSTMP 는 &lt;code&gt;@RevisionTimestamp&lt;/code&gt; 로 재정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# custom revision entity 로 변경된 쿼리

create table RevisionHistory (
    revisionId bigint not null auto_increment, 
    updatedAt bigint, primary key (revisionId)
) engine=InnoDB

create table User_AUD (
    REVTYPE tinyint, 
    REV bigint not null, 
    id bigint not null, 
    email varchar(255), 
    name varchar(255), 
primary key (REV, id)) engine=InnoDB

alter table User_AUD 
    add constraint FK7gw0sgfolgrr3a6xju5tvqxxp 
        foreign key (REV) references RevisionHistory (revisionId)&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;다만 생성 쿼리를 보면 알 수 있다시피 User_AUD 쪽의 정보는 변경되지 않았는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 application.yml 의 추가 설정을 통해 변경할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
    jpa:
    properties:
      org.hibernate.envers:
        audit_table_suffix: History # 이력 테이블 이름의 _AUD 를 History 로 변경
        revision_field_name: revisionId # 이력 테이블 REV 컬럼명
        revision_type_field_name: revisionType # 이력 테이블 REVTYPE 컬럼명
        store_data_at_delete: true # 삭제시 저장 여부, 기본은 false 이다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;revision_field_name&lt;/code&gt; 을 변경하면 REV &amp;rArr; 해당 이름의 컬럼명으로 변경된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 유용한 추가 설정이 있는데 검색 또는 envers 패키지를 찾아보면 좋을 듯 싶다.&lt;/p&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://docs.jboss.org/hibernate/envers/3.6/reference/en-US/html/configuration.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.jboss.org/hibernate/envers/3.6/reference/en-US/html/configuration.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1710068645266&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;Chapter&amp;nbsp;3.&amp;nbsp;Configuration&quot; data-og-description=&quot;Important The following configuration options have been added recently and should be regarded as experimental: org.hibernate.envers.audit_strategy org.hibernate.envers.audit_strategy_validity_end_rev_field_name org.hibernate.envers.audit_strategy_validity_&quot; data-og-host=&quot;docs.jboss.org&quot; data-og-source-url=&quot;https://docs.jboss.org/hibernate/envers/3.6/reference/en-US/html/configuration.html&quot; data-og-url=&quot;https://docs.jboss.org/hibernate/envers/3.6/reference/en-US/html/configuration.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.jboss.org/hibernate/envers/3.6/reference/en-US/html/configuration.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.jboss.org/hibernate/envers/3.6/reference/en-US/html/configuration.html&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;Chapter&amp;nbsp;3.&amp;nbsp;Configuration&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Important The following configuration options have been added recently and should be regarded as experimental: org.hibernate.envers.audit_strategy org.hibernate.envers.audit_strategy_validity_end_rev_field_name org.hibernate.envers.audit_strategy_validity_&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.jboss.org&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;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# yml 설정으로 변경된 쿼리

create table RevisionHistory (
    revisionId bigint not null auto_increment, 
    updatedAt bigint, primary key (revisionId)
) engine=InnoDB

create table UserHistory (
    revisionType tinyint, 
    id bigint not null, 
    revisionId bigint not null, 
    email varchar(255), 
    name varchar(255), 
    primary key (id, revisionId)
) engine=InnoDB

alter table UserHistory 
    add constraint FKtqxax72rt5cgosfu35jtcghu7 
        foreign key (revisionId) references RevisionHistory (revisionId)&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;code&gt;RevisionListener&lt;/code&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REVINFO 생성 단계에서 추가적인 정보를 부여하고 싶을 때 사용하는 기능이다.&lt;/p&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;수정한 사람을 기록하기 위해 RevisionListener 와 interceptor 를 적절하게 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;public class CustomRevisionEntityListener implements RevisionListener {
    @Override
    public void newRevision(Object o) {
        final Admin currentAdmin = AuditThreadContext.getCurrentAdmin();
        CustomRevisionEntity customRevisionEntity = (CustomRevisionEntity) o;
        if (currentAdmin != null) {
            customRevisionEntity.setAdminId(currentAdmin.getAdminId());
        }
    }
}&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;인터셉터에서 AuditThreadContext 라는 임의의 ThreadLocal 에 어드민 정보를 저장하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Revision 엔티티를 생성할 때 이를 꺼내와서 저장하는 방식으로 수정한 어드민 정보를 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 데이터를 수정한 사람이 누구인지 History 생성 단계에서 지정해줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Getter
@Entity
@Table(name = &quot;RevisionHistory&quot;)
@RevisionEntity(CustomRevisionEntityListener.class)
public class CustomRevisionEntity {
    @Id
    @Column
    @RevisionNumber
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long revisionId;

    @Column @RevisionTimestamp
    private Long updatedAt;

    @Column private Long adminId;

    public void setAdminId(Long adminId) {
        this.adminId = adminId;
    }

    public LocalDateTime getUpdatedAt() {
        return LocalDateTime.ofInstant(Instant.ofEpochMilli(updatedAt), ZoneId.systemDefault());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@RevisionEntity&lt;/code&gt; 에 새로운 컬럼을 지정한 후, 리스너 클래스를 등록하는 방식으로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. RevisionRepository&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@NoRepositoryBean
public interface RevisionRepository&amp;lt;T, ID, N extends Number &amp;amp; Comparable&amp;lt;N&amp;gt;&amp;gt; extends Repository&amp;lt;T, ID&amp;gt; {
    Optional&amp;lt;Revision&amp;lt;N, T&amp;gt;&amp;gt; findLastChangeRevision(ID id);

    Revisions&amp;lt;N, T&amp;gt; findRevisions(ID id);

    Page&amp;lt;Revision&amp;lt;N, T&amp;gt;&amp;gt; findRevisions(ID id, Pageable pageable);

    Optional&amp;lt;Revision&amp;lt;N, T&amp;gt;&amp;gt; findRevision(ID id, N revisionNumber);
}&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;spring-data-envers 는 기본적인 RevisionRepository 인터페이스를 제공하며 이를 상속받아 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입 인자는 &lt;code&gt;&amp;lt;엔티티 클래스, 엔티티 ID 타입, REV 타입&amp;gt;&lt;/code&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public interface UserRepository
    extends CrudRepository&amp;lt;User, Long&amp;gt;, RevisionRepository&amp;lt;User, Long, Long&amp;gt; {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최신 버전 단건 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public User findLatestUserRevisionById(Long id) {
    Revision&amp;lt;Long, User&amp;gt; latestRevision 
        = userRepository.findRevisions(id).getLatestRevision();

    User user = latestRevision.getEntity(); // 감사 엔티티
    Optional&amp;lt;Long&amp;gt; revisionNumber = latestRevision.getRevisionNumber(); // 버전번호
    Optional&amp;lt;Instant&amp;gt; revisionInstant = latestRevision.getRevisionInstant(); // 수정일시

    return user;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Jpa Pageable 을 이용한 페이징 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public Page&amp;lt;User&amp;gt; findUserRevisionPage(Long id, Pageable pageable) {
    Page&amp;lt;Revision&amp;lt;Long, User&amp;gt;&amp;gt; revisionPage = userRepository.findRevisions(id, pageable);

    return revisionPage.map(Revision::getEntity);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 동적 쿼리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RevisionRepository 로 해결할 수 없는 복잡한 동적 쿼리는 &lt;code&gt;AuditReader&lt;/code&gt; 를 사용하여 구현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class AuditReaderConfig {
    private final EntityManagerFactory entityManagerFactory;

    @Bean
    public AuditReader auditReader() {
        return AuditReaderFactory.get(entityManagerFactory.createEntityManager());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface UserHistoryRepositoryCustom {
    Optional&amp;lt;User&amp;gt; findLatestById(Long userId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class UserHistoryRepositoryCustomImpl implements UserHistoryRepositoryCustom {
    private final AuditReader auditReader;

    public Optional&amp;lt;User&amp;gt; findLatestById(Long userId) {
        List&amp;lt;?&amp;gt; results =
                    auditReader
                            .createQuery()
                            .forRevisionsOfEntity(User.class, true, true)
                            .add(AuditEntity.id().eq(userId))
                            .addOrder(AuditEntity.revisionNumber().desc())
                            .setMaxResults(1)
                            .getResultList();
        return (results.isEmpty()) ? Optional.empty() : Optional.of((User) results.get(0));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuditReader 의 forRevisionsOfEntity 의 파라미터에 따라 여러 옵션이 오버로딩 되어있는데 필요에 따라 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예시에서는 추가적인 Revision 메타데이터 없이 유저 엔티티만 가져오는 옵션을 주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;where 절 조건은 &lt;code&gt;.add()&lt;/code&gt; 체이닝으로 추가할 수 있고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;and 는 &lt;code&gt;AuditConjunction&lt;/code&gt; or 은 &lt;code&gt;AuditDisjunction&lt;/code&gt; 이라는 타입을 제공해서 추가할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private AuditConjunction allMatch(AuditCriterion... criterion) {
    final AuditConjunction auditConjunction = new AuditConjunction();
    Arrays.stream(criterion).forEach(auditConjunction::add);
    return auditConjunction;
}

private AuditDisjunction anyMatch(AuditCriterion... criterion) {
    final AuditDisjunction auditDisjunction = new AuditDisjunction();
    Arrays.stream(criterion).forEach(auditDisjunction::add);
    return auditDisjunction;
}&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;AuditReader 를 사용해서 1개 결과를 조회할 때 List 로 먼저 가져온 후에 처리하는 것이 좋다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;getSingleResult()&lt;/code&gt; 는 결과가 없을 경우 NoResultException 이 발생하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패치된 결과는 Object[] 형태인데 프로젝션 갯수가 늘어날 때 마다 원소의 개수가 증가한다.&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;code&gt;@Audited&lt;/code&gt; 에 수정 플래그를 사용 중인데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 플래그도 같이 가져오려면 &lt;code&gt;forRevisionsOfEntityWithChanges&lt;/code&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;이를 Object[] 의 3번 인덱스 원소로 수정된 엔티티 컬럼명을 Set 으로 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(modifiedFlag 모드를 사용하고 있지 않다면 오류가 발생한다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Getter
@AllArgsConstructor
public class AuditReaderResult&amp;lt;T&amp;gt; {
    private final T entity;
    private final CustomRevisionEntity revisionEntity;
    private final RevisionType revisionType;
    private final Set&amp;lt;String&amp;gt; revisionFields;

    @SuppressWarnings(&quot;unchecked&quot;)
    public static &amp;lt;T&amp;gt; AuditReaderResult&amp;lt;T&amp;gt; from(Object[] result) {
        return new AuditReaderResult&amp;lt;&amp;gt;(
                (T) result[0],
                (CustomRevisionEntity) result[1],
                (RevisionType) result[2],
                (Set&amp;lt;String&amp;gt;) result[3]);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class UserHistoryRepositoryCustomImpl implements UserHistoryRepositoryCustom {
    private final AuditReader auditReader;

    public Optional&amp;lt;User&amp;gt; findLatestById(Long userId) {
        final AuditCriterion predicate = this.allMatch(userIdEq(userId));
        final Optional&amp;lt;AuditReaderResult&amp;lt;User&amp;gt;&amp;gt; user = this.queryOne(predicate);

        return user.map(AuditReaderResult::getEntity);
    }

    private AuditCriterion userIdEq(Long userId) {
        return AuditEntity.property(&quot;id&quot;).eq(userId);
    }

    private &amp;lt;T&amp;gt; Optional&amp;lt;AuditReaderResult&amp;lt;T&amp;gt;&amp;gt; queryOne(AuditCriterion... auditCriterion) {
        List&amp;lt;?&amp;gt; results;
        synchronized (this) {
            results =
                    auditReader
                            .createQuery()
                            .forRevisionsOfEntityWithChanges(User.class, true)
                            .add(this.allMatch(auditCriterion))
                            .addOrder(AuditEntity.revisionNumber().desc())
                            .setMaxResults(1)
                            .getResultList();
        }
        final List&amp;lt;AuditReaderResult&amp;lt;T&amp;gt;&amp;gt; histories = auditReaderResultOf(results);
        return (histories.isEmpty()) ? Optional.empty() : Optional.of(histories.get(0));
    }

    private &amp;lt;T&amp;gt; List&amp;lt;AuditReaderResult&amp;lt;T&amp;gt;&amp;gt; auditReaderResultOf(List&amp;lt;?&amp;gt; results) {
        return results.stream()
                .map(Object[].class::cast)
                .map(AuditReaderResult::&amp;lt;T&amp;gt;from)
                .collect(Collectors.toList());
    }

    private AuditConjunction allMatch(AuditCriterion... criterion) {
        final AuditConjunction auditConjunction = new AuditConjunction();
        Arrays.stream(criterion).forEach(auditConjunction::add);
        return auditConjunction;
    }
}
&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;Pageable 을 이용한 페이지네이션도 &lt;code&gt;setFirstResult()&lt;/code&gt; 와 &lt;code&gt;setMaxResults()&lt;/code&gt; 를 적절하게 섞어 구현할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@SuppressWarnings(&quot;unchecked&quot;)
private &amp;lt;T&amp;gt; Page&amp;lt;AuditReaderResult&amp;lt;T&amp;gt;&amp;gt; queryAll(
        Pageable pageable, AuditCriterion... auditCriterion) {
    final List&amp;lt;Long&amp;gt; coveringIndexes = (List&amp;lt;Long&amp;gt;)
            auditReader
                    .createQuery()
                    .forRevisionsOfEntity(User.class, false, true)
                    .add(this.allMatch(auditCriterion))
                    .addProjection(AuditEntity.revisionNumber())
                    .getResultList();

    final List&amp;lt;?&amp;gt; results =
                auditReader
                        .createQuery()
                        .forRevisionsOfEntityWithChanges(User.class, true)
                        .add(revisionIdIn(coveringIndexes))
                        .addOrder(AuditEntity.revisionNumber().desc())
                        .setFirstResult((int) pageable.getOffset())
                        .setMaxResults(pageable.getPageSize())
                        .getResultList();
    final List&amp;lt;AuditReaderResult&amp;lt;T&amp;gt;&amp;gt; histories = auditReaderResultOf(results);
    return PageableExecutionUtils.getPage(histories, pageable, coveringIndexes::size);
}

private AuditCriterion revisionIdIn(List&amp;lt;Long&amp;gt; ids) {
    return AuditEntity.revisionNumber().in(ids);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 spring-data-envers 를 이용해서 이력 조회를 간편하게 설정하는 방법을 알아보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;envers 를 이용하면 History 테이블을 직접 구현하지 않고도 이력 관리가 가능하기 때문에&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;또 생각보다 hibernate 에 많은 기능이 구현되어있고 지원해주니 잘 찾아보면 수고를 덜 수 있을 것 같다.&lt;/p&gt;</description>
      <category>  백엔드/  Spring Boot</category>
      <category>envers</category>
      <category>Hibernate</category>
      <category>Spring</category>
      <category>spring-data-envers</category>
      <category>스프링</category>
      <category>이력관리</category>
      <category>이력테이블</category>
      <category>히스토리</category>
      <category>히스토리테이블</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/60</guid>
      <comments>https://gengminy.tistory.com/60#entry60comment</comments>
      <pubDate>Sun, 10 Mar 2024 20:16:14 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Redisson 분산락 AOP로 동시성 문제 해결하기 (트랜잭션 전파속성 NEVER 사용)</title>
      <link>https://gengminy.tistory.com/59</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  들어가며&lt;/b&gt;&lt;/h2&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선착순, 이벤트 등의 상황에서 다수의 트래픽이 동시에 몰리는 상황&lt;/li&gt;
&lt;li&gt;확인 버튼을 실수로 두 번 클릭을 해(따닥) 서버에 동시에 동일한 요청이 두 번  전송된 상황&lt;/li&gt;
&lt;li&gt;어떤 사용자가 추천과 비추천, 취소를 순식간에 계속 눌렀는데&lt;br /&gt;서버에 마침 부하가 걸려 이 요청들이 동시에 들어온 상황&lt;/li&gt;
&lt;/ul&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;b&gt;경쟁상태(Race Condition)&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;Redis&lt;/b&gt; 의 구현체 중 하나인 &lt;b&gt;Redisson Client&lt;/b&gt; 를 활용하여 이 문제를 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 그 과정에 대해 서술해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;❓ 그거 프론트에서 더블 클릭 막으면 되는 거 아닌가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트에서 동시에 여러 번 클릭하는 상황을 막는 기법들이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디바운스나 쓰로틀링 같은 기법으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트로부터 일정 주기안에 들어오는 동시 요청 중 단 하나만 수행하도록 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 방식은 허점이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 클라이언트가 여러 개의 브라우저로 동시 요청을 보내면?&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  Java Synchronized&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 진영에서는 기본적으로 &lt;b&gt;synchronized&lt;/b&gt; 키워드를 통해 &lt;b&gt;상호 배제(Mutual Exclusion)&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Synchronized 를 통한 멀티 쓰레드의 동시성 제어가 가능할 것으로 예상된다.&lt;/p&gt;
&lt;pre id=&quot;code_1687534863883&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
class ReviewService {

    @Transactional
    @Synchronized
    fun update(productId: Long) {
        // do something
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 Synchronized 키워드는 몇 가지 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. Synchronized 키워드는 @Transactional 에서 처리되지 않는다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP는 @Transactional 이 붙은 메소드를&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션의 시작, 커밋, 종료를 담당하는 프록시 객체로 감싸 처리해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 이 부분은 Synchronized 메소드에 포함되어 있지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 트랜잭션의 커밋을 처리하고 DB에 반영되기 직전,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 스레드가 이 구역에 진입해 반영되지 않은 값을 읽어도 오류가 발생하지 않는다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;2. 멀티 프로세스 환경에서는 동작하지 않는다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Synchronized 는 싱글 프로세스 내부의 멀티 쓰레드를 제어할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 멀티 프로세스 환경에서는 다른 프로세스 내부의 쓰레드를 제어할 수 없기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 확장된다면 동시성 제어가 불가능하다.&lt;/p&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  MySQL을 이용한 분산락&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 이 분산락을 적용하려는 서버도 MySQL 을 사용하고 있기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 에 내장되어 있는 Lock 을 고려할 수도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 의 Lock은 Lock Table 을 생성하여 별도 관리하는데&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;별도의 커넥션 풀을 관리해야 하고, DB에 락과 관련된 부하를 일으켜 속도가 저하될 수 있으며 dead lock 의 가능성도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 우리 서버는 Redis 를 사용하고 있는 상황이기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL을 이용한 Lock은 사용하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우형에서는 MySQL을 이용한 분산락을 구현한 글을 적어두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/2631/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://techblog.woowahan.com/2631/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1687533378950&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;MySQL을 이용한 분산락으로 여러 서버에 걸친 동시성 관리 | 우아한형제들 기술블로그&quot; data-og-description=&quot;{{item.name}} 안녕하세요. 비즈인프라개발팀 권순규입니다. 현재 광고시스템에서 사용하고 있는 MySQL을 이용한 분산락에 대해 설명드리고자 합니다. 분산락을 적용하게된 원인 현재 테이블은 아래&quot; data-og-host=&quot;techblog.woowahan.com&quot; data-og-source-url=&quot;https://techblog.woowahan.com/2631/&quot; data-og-url=&quot;https://techblog.woowahan.com/2631/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/beEtLG/hyS5rKsVDq/DAnrRZqJjKi2KOqf9IkkUK/img.jpg?width=1640&amp;amp;height=856&amp;amp;face=0_0_1640_856,https://scrap.kakaocdn.net/dn/ULYrC/hyS6EuCR8E/zB6sdhjTMNYqjzn2RNXEuk/img.jpg?width=1640&amp;amp;height=856&amp;amp;face=0_0_1640_856,https://scrap.kakaocdn.net/dn/hOOXw/hyS6JbDbrO/uAywZiXK01HjK8ikHPDGDK/img.png?width=964&amp;amp;height=374&amp;amp;face=0_0_964_374&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/2631/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://techblog.woowahan.com/2631/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/beEtLG/hyS5rKsVDq/DAnrRZqJjKi2KOqf9IkkUK/img.jpg?width=1640&amp;amp;height=856&amp;amp;face=0_0_1640_856,https://scrap.kakaocdn.net/dn/ULYrC/hyS6EuCR8E/zB6sdhjTMNYqjzn2RNXEuk/img.jpg?width=1640&amp;amp;height=856&amp;amp;face=0_0_1640_856,https://scrap.kakaocdn.net/dn/hOOXw/hyS6JbDbrO/uAywZiXK01HjK8ikHPDGDK/img.png?width=964&amp;amp;height=374&amp;amp;face=0_0_964_374');&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;MySQL을 이용한 분산락으로 여러 서버에 걸친 동시성 관리 | 우아한형제들 기술블로그&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;{{item.name}} 안녕하세요. 비즈인프라개발팀 권순규입니다. 현재 광고시스템에서 사용하고 있는 MySQL을 이용한 분산락에 대해 설명드리고자 합니다. 분산락을 적용하게된 원인 현재 테이블은 아래&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;techblog.woowahan.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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  Redis를 이용한 분산락&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zookeeper 를 통한 예제도 있지만 오버 테크놀로지라 생각했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 현재 서버에서 토큰 및 캐싱 관리에 Redis 를 사용중이기 때문에 이를 채택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 싱글 쓰레드 특성을 지녀 한 번에 하나의 명령을 처리하기 때문에 분산락 구현에 많이 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Client 중 기본 설정인 &lt;b&gt;Lettuce&lt;/b&gt; 와 다른 클라이언트인 &lt;b&gt;Redisson&lt;/b&gt; 중 하나를 선택해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. Lettuce&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1687535075705&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class RedisLockRepository(private val redisTemplate: RedisTemplate&amp;lt;String, String&amp;gt;) {

    fun lock(key: Long): Boolean {
        return redisTemplate.opsForValue()
            .setIfAbsent(key.toString(), &quot;lock&quot;, Duration.ofMillis(3000))
    }

    fun unlock(key: Long): Boolean {
        return redisTemplate.delete(key.toString())
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lettuce 는 setnx, setex 같은 명령어를 통해 분산락을 구현해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 이 명령어들은 Spin Lock 방식으로 동작하기 때문에 Redis 에 엄청난 부하를 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 단순하게 무한 while 루프를 통해 대기하는 방식이다.&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. Redisson&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1687535123567&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
class ReviewService(private val redissonClient: RedissonClient) {

    @Transactional
    fun update(productId: Long) {
        val lock = redissonClient.getLock(&quot;key&quot;)

        try {
            val available = lock.tryLock(2, 3, TimeUnit.SECONDS)
            if (!available) {
                throw RuntimeExcetpion(&quot;Lock Not Avaliable&quot;)
            }

            // do something

        } finally {
            lock.unlock()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson 을 이용한 lock 예시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 우리 서버에서 사용 중인 클라이언트이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pub/Sub 방식으로 동작하며 락이 해제되면 해당 락을 Subscribe 중인 클라이언트들이 신호를 받고 락에 진입한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson 은 Lock에 타임아웃을 명시해야 하며, 덕분에 무한 루프에 빠지지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 마켓컬리 팀에서 작성한  분산락 방식이 최상단에 뜨는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색하면 나오는 대부분의 결과도 Redisson 으로 비슷하게 구현되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://helloworld.kurly.com/blog/distributed-redisson-lock/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://helloworld.kurly.com/blog/distributed-redisson-lock/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1687534796707&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;풀필먼트 입고 서비스팀에서 분산락을 사용하는 방법 - Spring Redisson&quot; data-og-description=&quot;어노테이션 기반으로 분산락을 사용하는 방법에 대해 소개합니다.&quot; data-og-host=&quot;helloworld.kurly.com&quot; data-og-source-url=&quot;https://helloworld.kurly.com/blog/distributed-redisson-lock/&quot; data-og-url=&quot;http://thefarmersfront.github.io/blog/distributed-redisson-lock/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cx9Hb9/hyS5qxx8VF/VDeBikxBlwJSAnZxWAzWkK/img.png?width=1452&amp;amp;height=1452&amp;amp;face=0_0_1452_1452,https://scrap.kakaocdn.net/dn/tSVrA/hyS5EWRZFh/X130FA0efekk83rrkgulM0/img.png?width=1670&amp;amp;height=1610&amp;amp;face=0_0_1670_1610,https://scrap.kakaocdn.net/dn/befNLp/hyS5BeKTj5/jNsO3I2CuZOIY7Kiq7MCNk/img.png?width=1656&amp;amp;height=1550&amp;amp;face=0_0_1656_1550&quot;&gt;&lt;a href=&quot;https://helloworld.kurly.com/blog/distributed-redisson-lock/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://helloworld.kurly.com/blog/distributed-redisson-lock/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cx9Hb9/hyS5qxx8VF/VDeBikxBlwJSAnZxWAzWkK/img.png?width=1452&amp;amp;height=1452&amp;amp;face=0_0_1452_1452,https://scrap.kakaocdn.net/dn/tSVrA/hyS5EWRZFh/X130FA0efekk83rrkgulM0/img.png?width=1670&amp;amp;height=1610&amp;amp;face=0_0_1670_1610,https://scrap.kakaocdn.net/dn/befNLp/hyS5BeKTj5/jNsO3I2CuZOIY7Kiq7MCNk/img.png?width=1656&amp;amp;height=1550&amp;amp;face=0_0_1656_1550');&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;풀필먼트 입고 서비스팀에서 분산락을 사용하는 방법 - Spring Redisson&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;helloworld.kurly.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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  Redisson Lock AOP 구현&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 Redisson Lock 을 매번 필요한 비즈니스 로직마다 사용하면 비즈니스 로직이 오염된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 스프링의 AOP를 이용하여 분리시키고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색하면 나오는 대부분의 예제는 AOP에 너무 많은 처리 로직이 몰려있어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 LockManager를 통해 분리시켰다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  DistributedLock&lt;/h3&gt;
&lt;pre id=&quot;code_1687535736662&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class DistributedLock(
    val type: DistributedLockType,
    val keys: Array&amp;lt;String&amp;gt;,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DistributedLock 을 정의하는 어노테이션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lock name 을 enum 으로 관리하고 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;우리 프로젝트에서는 DDD 를 사용하고 있기 때문에 Object Key 에 대해서는 고려하지 않았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 key 값에는 분산락의 키로 사용하는 각 엔티티 id가 메소드의 인자로 들어온다.&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;  DistributedLockAop&lt;/h3&gt;
&lt;pre id=&quot;code_1687535440469&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Aspect
@Component
class DistributedLockAop(
        private val lockManager: LockManager,
) {
    @Around(&quot;@annotation(distributedLock)&quot;)
    fun lock(joinPoint: ProceedingJoinPoint, distributedLock: DistributedLock): Any? {
        val dynamicKey = createDynamicKey(joinPoint, distributedLock.keys)
        val lockName = &quot;${distributedLock.type.lockName}:$dynamicKey&quot;
        return lockManager.lock(lockName) { joinPoint.proceed() }
    }

    private fun createDynamicKey(joinPoint: ProceedingJoinPoint, keys: Array&amp;lt;String&amp;gt;): String {
        val methodSignature = joinPoint.signature as MethodSignature
        val methodParameterNames = methodSignature.parameterNames
        val methodArgs = joinPoint.args

        val dynamicKey = keys.joinToString(separator = &quot;:&quot;) { key -&amp;gt;
            val indexOfKey = methodParameterNames.indexOf(key)
            methodArgs.getOrNull(indexOfKey)?.toString() ?: throw InvalidLockException(&quot;Invalid Lock Key&quot;)
        }
        return dynamicKey
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Client 가 바뀌거나 Lock 을 처리하는 방법 자체가 바뀔 상황을 대비하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LockManager 를 별도 인터페이스로 정의하여 빼주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 AOP 에서 Lock 을 처리하지 않고 LockManager 에 위임했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 각 key 를 메소드 파라미터에서 찾아와 콜론으로 분리하여 키를 설정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후 확장한다면 Object 에 대해서도 리플렉션으로 가져와 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;&lt;b&gt;@DistributedLock&lt;/b&gt; 과 관련된 오류는 &lt;b&gt;InvalidLockException&lt;/b&gt; 이라는 커스텀 예외를 만들어 던졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  LockManager&lt;/h3&gt;
&lt;pre id=&quot;code_1687535800007&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
interface LockManager {
    fun lock(lockName: String, operation: () -&amp;gt; Any?): Any?
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락을 담당하는 인터페이스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작하려는 함수 하나와 락 이름을 지정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  RedissonLockManager&lt;/h3&gt;
&lt;pre id=&quot;code_1687535878808&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class RedissonLockManager(
    private val redissonClient: RedissonClient,
    private val aopForNewTransaction: AopForNewTransaction
): LockManager {

    override fun lock(operation: () -&amp;gt; Any?, lockName: String): Any? {
        val rLock = redissonClient.getLock(lockName)

        try {
            val available = rLock.tryLock(waitTime, leaseTime, timeUnit)
            if (!available) {
                throw InvalidLockException(&quot;Redisson Lock Not Available&quot;)
            }
            return aopForNewTransaction.callNewTransaction { operation() }
        } finally {
            rLock.unlock()
        }
    }

    companion object {
        private const val waitTime = 5L
        private const val leaseTime = 3L
        private val timeUnit = TimeUnit.SECONDS
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson 을 이용한 분산락을 처리하는 LockManager의 구현체이다.&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;waitTime 과 leaseTime 등의 각 속성은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 LockManager 구현체마다 동일하고 바꿀 일이 거의 없을 것이라 예상하고&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  AopForNewTransaction&lt;/h3&gt;
&lt;pre id=&quot;code_1687536540351&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class AopForNewTransaction {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    fun callNewTransaction(operation: () -&amp;gt; Any?): Any? {
        return operation()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주목할 점은 이 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락으로 자원을 잠그고, 해제하는 그 사이에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 전파속성을 통해 완전히 새로운 트랜잭션을 열고 로직을 처리하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 트랜잭션이 락 내부에 있기 때문에 &lt;b&gt;트랜잭션이 완전히 커밋된 후에야 락이 해제된다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이렇게 구현해야 할까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1186&quot; data-origin-height=&quot;1301&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1AC22/btsmAMAshGh/6u6h4GXvqyvxbU1n9Fodmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1AC22/btsmAMAshGh/6u6h4GXvqyvxbU1n9Fodmk/img.png&quot; data-alt=&quot;REQUIRES_NEW 를 사용하지 않은 경우 트랜잭션 플로우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1AC22/btsmAMAshGh/6u6h4GXvqyvxbU1n9Fodmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1AC22%2FbtsmAMAshGh%2F6u6h4GXvqyvxbU1n9Fodmk%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;500&quot; height=&quot;548&quot; data-origin-width=&quot;1186&quot; data-origin-height=&quot;1301&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;REQUIRES_NEW 를 사용하지 않은 경우 트랜잭션 플로우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 락이 트랜잭션의 내부에 있다면&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;락이 해제되고 커밋되는 그 사이에 다른 트랜잭션이 끼어들 여지&lt;/b&gt;가 생긴다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 되면 동시성 문제가 똑같이 발생한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;1385&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vFI2y/btsmCat0jOx/2KKOmao8GkyPwoBgrnMEqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vFI2y/btsmCat0jOx/2KKOmao8GkyPwoBgrnMEqk/img.png&quot; data-alt=&quot;REQUIRES_NEW 를 사용한 경우 트랜잭션 플로우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vFI2y/btsmCat0jOx/2KKOmao8GkyPwoBgrnMEqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvFI2y%2FbtsmCat0jOx%2F2KKOmao8GkyPwoBgrnMEqk%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;500&quot; height=&quot;585&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;1385&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;REQUIRES_NEW 를 사용한 경우 트랜잭션 플로우&lt;/figcaption&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;다른 트랜잭션이 커밋 이전에 끼어들 여지가 없어져 데이터의 정합성이 보장된다.&lt;/p&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  트랜잭션 전파속성 REQUIRES_NEW 의 단점&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1688622904782&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// (1)
@Transactional
@DistributedLock(DistributedLockType.LIKE, [&quot;productId&quot;, &quot;memberId&quot;])
fun like(productId: Long): ProductLikeVO {
    // do something
}

// (2)
@DistributedLock(DistributedLockType.LIKE, [&quot;productId&quot;, &quot;memberId&quot;])
fun like(productId: Long): ProductLikeVO {
    // do something
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 어떠한 메소드에&amp;nbsp;&lt;b&gt;@Transactional&lt;/b&gt;&amp;nbsp;과&amp;nbsp;&lt;b&gt;@DistributedLock&lt;/b&gt;&amp;nbsp;을 모두 사용했다고 하자.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Transactional&lt;/b&gt;&amp;nbsp;의 우선순위가 기본적으로 높기 때문에&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하나의 트랜잭션 내부에서 다시&amp;nbsp;&lt;b&gt;@DistributedLock&lt;/b&gt;&amp;nbsp;의&amp;nbsp;&lt;b&gt;새로운 트랜잭션 호출 로직(REQUIRES_NEW)이 동작&lt;/b&gt;한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러면 총 두 개의 트랜잭션이 하나의 메소드에서 호출된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1119&quot; data-origin-height=&quot;1245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biprXO/btsmHHX2SUS/VLLs1IVjsTNNwM13pAObIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biprXO/btsmHHX2SUS/VLLs1IVjsTNNwM13pAObIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biprXO/btsmHHX2SUS/VLLs1IVjsTNNwM13pAObIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiprXO%2FbtsmHHX2SUS%2FVLLs1IVjsTNNwM13pAObIk%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;500&quot; height=&quot;556&quot; data-origin-width=&quot;1119&quot; data-origin-height=&quot;1245&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서&lt;b&gt;&amp;nbsp;@DistributedLock&amp;nbsp;&lt;/b&gt;관련 트랜잭션인 Transaction 2는 커밋이 되었는데&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 메소드를 호출한 &lt;b&gt;Transaction 1 &lt;/b&gt;은 오류가 발생했다고 하자.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러면 &lt;b&gt;Transaction 1&lt;/b&gt;에서&amp;nbsp;&lt;b&gt;@Transactional&lt;/b&gt;&amp;nbsp;을 사용했기 때문에&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두 트랜잭션이 모두 롤백 되기를 기대했지만&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하나의 트랜잭션은 커밋되고 하나의 트랜잭션은 커밋이 된 상황이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두 트랜잭션은 독립적이기 때문이다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 방지하려면&lt;b&gt;&amp;nbsp;(2)번&lt;/b&gt;처럼 &lt;b&gt;@DistributedLock&lt;/b&gt;&amp;nbsp;하나의 어노테이션만 사용해야 하는데&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;그러면 이 메소드가 트랜잭션을 포함하는지 안하는지, 코드의 동작 결과를 한눈에 예상하기 어려워질 것이다.&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;그래서 이를 해결하기 위해 전파속성을 바꿔서 처리하고자 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  Propagation 속성 변경을 통한 aspect 개선&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 구현한 분산락 코드를 개선하고자 하는 이유는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떠한 메소드에 &lt;b&gt;@Transactional&lt;/b&gt;&amp;nbsp;과&amp;nbsp;&lt;b&gt;@DistributedLock&lt;/b&gt; 을 모두 사용하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 동작 결과를 예상하기 쉽게 하여 가독성을 높이기 위함이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 상황은 &lt;b&gt;@DistributedLock&amp;nbsp;&lt;/b&gt;이 다른 트랜잭션에서 호출될 때 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 다른 트랜잭션에서 절대 호출할 수 없도록 하면 해결되지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  트랜잭션 전파속성 NEVER&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 전파속성 중 &lt;b&gt;NEVER&lt;/b&gt; 은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션을 사용하지 않음과 동시에 선행 트랜잭션이 존재하면 오류를 발생시키는 옵션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 선행 트랜잭션이 존재하는지 검사하기 위해 사용하는 옵션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  RedissonLockManager&lt;/h3&gt;
&lt;pre id=&quot;code_1688625991390&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class RedissonLockManager(
    private val redissonClient: RedissonClient,
): LockManager {
    // 트랜잭션이 모두 Lock 내부에서 실행됨을 보장한다
    @Transactional(propagation = Propagation.NEVER)
    override fun lock(lockName: String, operation: () -&amp;gt; Any?): Any? {
        val rLock = redissonClient.getLock(lockName)

        try {
            val available = rLock.tryLock(waitTime, leaseTime, timeUnit)
            if (!available) {
                throw InvalidLockException(&quot;Redisson Lock Not Available&quot;)
            }
            return operation()
        } finally {
            rLock.unlock()
        }
    }

    companion object {
        private const val waitTime = 5L
        private const val leaseTime = 3L
        private val timeUnit = TimeUnit.SECONDS
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AopForNewTransaction 을 없애고 Lock 에 대한 연산만 남겨두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이렇게 될 경우 &lt;b&gt;@Transaction 의 기본 우선순위&lt;/b&gt;가 높기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transaction 과 @DistributedLock 가 메소드에 동시에 붙게 되면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transaction 내부에서 @DistributedLock 이 호출되고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Propagation=NEVER&lt;/b&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;그렇기 때문에 @DistributedLock 의 우선순위를 높혀&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional 보다 먼저 처리될 수 있도록 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  DistributedLockAop&lt;/h3&gt;
&lt;pre id=&quot;code_1688626229531&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
class DistributedLockAop(
        private val lockManager: LockManager,
) {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서는&amp;nbsp;&lt;b&gt;@Order&lt;/b&gt; 라는 어노테이션을 통해 어노테이션의 적용 우선순위를 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선순위는 &lt;b&gt;INT_MIN&lt;/b&gt; 부터 &lt;b&gt;INT_MAX&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;Ordered.HIGHEST_PRECEDENCE&lt;/b&gt; 라는 상수는 &lt;b&gt;INT_MIN&lt;/b&gt; 으로 설정되어 있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 사용해 &lt;b&gt;@DistributedLock&lt;/b&gt; 의 적용 우선순위를 &lt;b&gt;@Transactional&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;@Transactional&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;은&lt;/span&gt; 락이 설정된 이후에 호출되기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메소드 선언에서 같이 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 외부 메소드에서 &lt;b&gt;@DistributedLock&amp;nbsp;&lt;/b&gt;을 호출할 수 없기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시처럼 예상치 못한 롤백 같은 상황이 생기지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  마치며&lt;/b&gt;&lt;/h2&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;</description>
      <category>  백엔드/  Spring Boot</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/59</guid>
      <comments>https://gengminy.tistory.com/59#entry59comment</comments>
      <pubDate>Thu, 6 Jul 2023 16:01:44 +0900</pubDate>
    </item>
    <item>
      <title>[YAPP] IT 연합동아리 YAPP 22기 백엔드 서류 / 면접 합격 후기</title>
      <link>https://gengminy.tistory.com/58</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;yapp.jpeg&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bp2gD0/btscxcgQYmQ/tC1y3wRGm81cagBwU6qObK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bp2gD0/btscxcgQYmQ/tC1y3wRGm81cagBwU6qObK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bp2gD0/btscxcgQYmQ/tC1y3wRGm81cagBwU6qObK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbp2gD0%2FbtscxcgQYmQ%2FtC1y3wRGm81cagBwU6qObK%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;1280&quot; height=&quot;720&quot; data-filename=&quot;yapp.jpeg&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✨ YAPP&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연합 개발 동아리들 중 하나로, &lt;b&gt;기업형 IT 연합동아리&lt;/b&gt;라고 소개하고 있네요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대학생과 현업자가 모두 참여할 수 있는 것이 특징 중 하나입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PM(기획자), 디자이너, 프론트엔드, 백엔드 네 개의 파트로 나누어 모집하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YAPP은 22기 기준 매주 토요일 정기 세션을 진행하고 기수 당 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;p data-ke-size=&quot;size16&quot;&gt;YAPP 에는 실력자 분들도 많고 네트워킹도 활발하다고 알고 있어서 지원할까 고민 중이었는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결정적으로 지인이 YAPP 한 번 지원해보는 게 어떻냐고 꼬셔서&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; 서류 준비해서 지원했네요  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.yapp.co.kr/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.yapp.co.kr/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1682386649425&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;YAPP&quot; data-og-description=&quot;작은 아이디어로 세상을 크게 변화시키는 IT동아리, YAPP&quot; data-og-host=&quot;www.yapp.co.kr&quot; data-og-source-url=&quot;https://www.yapp.co.kr/&quot; data-og-url=&quot;https://yapp.co.kr&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.yapp.co.kr/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.yapp.co.kr/&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;YAPP&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;작은 아이디어로 세상을 크게 변화시키는 IT동아리, YAPP&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.yapp.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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  서류 문항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글 폼을 사용하는 대부분의 다른 동아리들과는 다르게&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'&lt;b&gt;그리팅&lt;/b&gt;'이라는 채용 사이트를 별도로 사용해서 지원서를 받는 점이 신기했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 서류 문항은 다음과 같았습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;YAPP에 지원하게 된 동기를 포함하여 자유롭게 자기소개를 해주세요. (최대 500자)&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot; data-token-index=&quot;0&quot;&gt;프로젝트를 진행할 때 어떤 방식으로 팀원과 소통하는지 경험을 바탕으로 작성해 주세요.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;(최대 500자)&lt;/li&gt;
&lt;li&gt;개발 경력 혹은 경험을 알려주세요. 프로젝트 이름, 기간, 본인의 역할, 성과를 포함하여 작성해 주세요. (최대 5000자, 개괄식)&lt;/li&gt;
&lt;li&gt;위 프로젝트 중 기억에 남는 것을 하나 선정해 기술적인 어려움을 겪었던 부분과 어떠한 과정으로 해당 문제를 해결했는지 작성해 주세요. (최대 700자)&lt;/li&gt;
&lt;/ol&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;아무래도&lt;span&gt; &lt;/span&gt;&lt;/span&gt;읽는 사람의 이목을 끌도록 적는게 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(그렇다고 절대 거짓말을 적으면 안됩니다... 어차피 면접 때 다 들통나요. 깔끔하고 보기 좋게 적으라는 의미!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 답변을 두괄식으로 작성하고 가독성 좋게 하려고 노력했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 같은 경우는 사람마다 다를테니 넘어가고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번과 4번은 최근에 프로젝트에서 경험한 것들을 &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;3번은 여태 했던 프로젝트들에서 어떤 기능을 담당하고 구현했는지, 이것으로 어떤 성장을 이루어냈는지 적었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-25 11.02.02.png&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;1182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v9IIw/btscxmDI0TM/tJ03Kf6XSpUkAqtQAvz0Gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v9IIw/btscxmDI0TM/tJ03Kf6XSpUkAqtQAvz0Gk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v9IIw/btscxmDI0TM/tJ03Kf6XSpUkAqtQAvz0Gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv9IIw%2FbtscxmDI0TM%2FtJ03Kf6XSpUkAqtQAvz0Gk%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;1470&quot; height=&quot;1182&quot; data-filename=&quot;스크린샷 2023-04-25 11.02.02.png&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;1182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서류 같은 경우는 여러 개발 동아리들 많이 써보기도 했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트폴리오도 정리 많이 해놔서 자신이 있었는데 다행스럽게도 합격 했네요!&lt;/p&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;⚡️ 면접&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접 같은 경우 &lt;b&gt;서류 결과 발표 주 토요일&lt;/b&gt;에 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서류 합격 메일에 포함된 링크로 면접 시간을 선택할 수 있는데 &lt;b&gt;선착순&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 요상하게도 면접 날 아침에 네이버 코테, 오후에 중간고사가 겹쳐서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호다닥 들어가서 오후 5시 반으로 정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당일 날 준비할 시간이 모자라서 서류 합격 날 부터 맨날 면접 준비했던 거 같아요ㅜ ㅜ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넥스터즈와 디프만도 서류는 합격했으나 면접 때 기술 질문에 탈탈 털리고 광탈해버렸는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 진짜 붙고 싶어서 며칠 동안 하루 종일 CS 질문과 프로젝트 관련 예상 질문 쫙 뽑아서 대비했습니다...!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접은 2:2 로 진행했고 같이 본 분은 현업자셨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문은 기억 나는 거 몇 가지만 간단하게 적을게요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1분 자기소개 및 지원동기&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;얍 활동으로 얻어가고 싶은 것이 있는지?&lt;/li&gt;
&lt;li&gt;얍에서는 네이티브 앱과 웹 앱 중 어떤 환경에서 개발해보고 싶은지&lt;/li&gt;
&lt;/ul&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;총 45분 정도 소요&lt;/b&gt;되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특이한 점은 제 면접 방에서는 기술 질문 없이 모두 인성 질문만 여쭤보셨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 활동하면서 어떤 플랫폼에서(웹 또는 앱) 개발을 해보고 싶은지 팀 빌딩 관련된 질문을 주셨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  결과&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-04-25 11.42.23.png&quot; data-origin-width=&quot;1498&quot; data-origin-height=&quot;910&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKxgyH/btscwoB4YeI/Wd1PmBKp9aTKt8yS1FZIrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKxgyH/btscwoB4YeI/Wd1PmBKp9aTKt8yS1FZIrK/img.png&quot; data-alt=&quot;CMC 이후로 오랜만에 받아보는 합격 메일&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKxgyH/btscwoB4YeI/Wd1PmBKp9aTKt8yS1FZIrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKxgyH%2FbtscwoB4YeI%2FWd1PmBKp9aTKt8yS1FZIrK%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;1498&quot; height=&quot;910&quot; data-filename=&quot;스크린샷 2023-04-25 11.42.23.png&quot; data-origin-width=&quot;1498&quot; data-origin-height=&quot;910&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CMC 이후로 오랜만에 받아보는 합격 메일&lt;/figcaption&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;다행스럽게도 포폴 적은 것과 답변 내용을 좋게 봐주신 거 같아요.  &lt;/p&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;백엔드 경쟁률이 18.5 : 1&lt;/b&gt; 이더라구요....????&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기분이 좋으면서도 뭔가 많은 것을 느끼게 해주는 숫자네요  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;2048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPDKc4/btscjmd9bpI/NcbH1BCpWNbacuIxmOe5R0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPDKc4/btscjmd9bpI/NcbH1BCpWNbacuIxmOe5R0/img.jpg&quot; data-alt=&quot;장난 아닌 경쟁률 디자이너는 25.3 : 1 이라는데..... 후달달&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPDKc4/btscjmd9bpI/NcbH1BCpWNbacuIxmOe5R0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPDKc4%2Fbtscjmd9bpI%2FNcbH1BCpWNbacuIxmOe5R0%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;400&quot; height=&quot;400&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;2048&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;장난 아닌 경쟁률 디자이너는 25.3 : 1 이라는데..... 후달달&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;확실히 실력자 분들이 모인다는 느낌을 받은게&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;</description>
      <category>  주저리</category>
      <category>22기</category>
      <category>backend</category>
      <category>IT동아리</category>
      <category>yapp</category>
      <category>yapp22기</category>
      <category>개발동아리</category>
      <category>백엔드</category>
      <category>얍</category>
      <category>연합it동아리</category>
      <category>연합동아리</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/58</guid>
      <comments>https://gengminy.tistory.com/58#entry58comment</comments>
      <pubDate>Tue, 25 Apr 2023 13:01:11 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링 소셜 로그인 OIDC 방식으로 구현하기 (OAuth with OpenID Connect)</title>
      <link>https://gengminy.tistory.com/57</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;628&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBJ5Vt/btr5Tn9UfVp/srMkizpUQP4HKc68nb2O1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBJ5Vt/btr5Tn9UfVp/srMkizpUQP4HKc68nb2O1k/img.png&quot; data-alt=&quot;OpenID Connect (OIDC) Flow Chart&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBJ5Vt/btr5Tn9UfVp/srMkizpUQP4HKc68nb2O1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBJ5Vt%2Fbtr5Tn9UfVp%2FsrMkizpUQP4HKc68nb2O1k%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;500&quot; height=&quot;444&quot; data-origin-width=&quot;628&quot; data-origin-height=&quot;558&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;OpenID Connect (OIDC) Flow Chart&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  OpenID Connect (OIDC) 는 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0 프로토콜을 기반으로 한 사용자 인증 프로토콜&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;accessToken 이외에도 &lt;b&gt;id_token&lt;/b&gt;을 사용하여 토큰으로 사용자가 누구인지 확인할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OIDC는 표준 프로토콜(스펙)이기 때문에 다른 OAuth 공급자와 호환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 소셜 로그인 요청 시 scope 에 `open id` 를 추가해주면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엑세스 토큰과 리프레시 토큰 이외에 id token 을 추가로 응답해준다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  공개 키 가져오기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 OAuth Provider 들은 공개키 목록의 JSON 파일을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글, 카카오, 애플의 공개키 목록 url 은 다음과 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Google&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;https://www.googleapis.com/oauth2/v3/certs&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Kakao&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;https://kauth.kakao.com/.well-known/jwks.json&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Apple&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;https://appleid.apple.com/auth/keys&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;아래는 애플의 공개키를 가져오는 FeignClient 예시이다&lt;/p&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;  AppleOIDCClient&lt;/h3&gt;
&lt;pre id=&quot;code_1679749772862&quot; class=&quot;kotlin&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@FeignClient(
        name = &quot;AppleOIDCClient&quot;,
        url = &quot;https://appleid.apple.com&quot;,
        configuration = AppleOAuthConfig.class)
public interface AppleOIDCClient {
    @GetMapping(&quot;/auth/keys&quot;)
    OIDCPublicKeysResponse getAppleOIDCOpenKeys();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플의 OIDC 정보들을 가져오는 Feign Client&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 url 로 들어가면 Apple 이 제공하는 공개키 리스트가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 DTO에 저장시켰다.&lt;/p&gt;
&lt;pre id=&quot;code_1679749772863&quot; class=&quot;typescript&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@NoArgsConstructor
public class OIDCPublicKeysResponse {
    List&amp;lt;OIDCPublicKeyDto&amp;gt; keys;
}

@Getter
@NoArgsConstructor
public class OIDCPublicKeyDto {
    private String kid;
    private String alg;
    private String use;
    private String n;
    private String e;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  id_token 검증하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OIDC 방식 로그인에서는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 decode 했을 때 나오는 정보들을 검증할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 글을 많이 참고했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://devnm.tistory.com/35&quot;&gt;&amp;gt;&amp;gt; [스프링]&amp;nbsp;spring&amp;nbsp;oauth&amp;nbsp;Open&amp;nbsp;ID&amp;nbsp;Connect&amp;nbsp;with&amp;nbsp;kakao&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;OIDC 는 앞서 말했다시피 SPEC 이기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 구현해두면&lt;span&gt;&amp;nbsp;&lt;/span&gt;OAuth Provider 별로 동일하게 사용 가능하다!&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;  JwtOIDCProvider&lt;/h3&gt;
&lt;pre id=&quot;code_1679749772863&quot; class=&quot;arduino&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private String getUnsignedToken(String token) {
    String[] splitToken = token.split(&quot;\\.&quot;);
    if (splitToken.length != 3) throw new BaseException(INVALID_TOKEN);
    return splitToken[0] + &quot;.&quot; + splitToken[1] + &quot;.&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Header, Payload, Signature 를 분리하고 검증한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Header 와 Payload 값만 가져옴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1679749772864&quot; class=&quot;reasonml&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private Jwt&amp;lt;Header, Claims&amp;gt; getUnsignedTokenClaims(String token, String iss, String aud) {
    try {
        return Jwts.parserBuilder()
                .requireAudience(aud)
                .requireIssuer(iss)
                .build()
                .parseClaimsJwt(getUnsignedToken(token));
    } catch (ExpiredJwtException e) {
        throw new BaseException(ACCESS_TOKEN_EXPIRED);
    } catch (Exception e) {
        log.error(e.toString());
        throw new BaseException(INVALID_TOKEN);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 iss와 aud 및 토큰 만료를 검증한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1679749772864&quot; class=&quot;arduino&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public String getKidFromUnsignedTokenHeader(String token, String iss, String aud) {
    return (String) getUnsignedTokenClaims(token, iss, aud).getHeader().get(&quot;kid&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 공개키 목록에서 사용할 kid 를 알아내기 위해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헤더에서 key id 를 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이후 OAuthOIDCHelper 에서 이 메소드를 사용할 것이다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1679749772864&quot; class=&quot;haxe&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public OIDCDecodePayload getOIDCTokenBody(String token, String modulus, String exponent) {
    Claims body = getOIDCTokenJws(token, modulus, exponent).getBody();
    return new OIDCDecodePayload(
            body.getIssuer(),
            body.getAudience(),
            body.getSubject(),
            body.get(&quot;email&quot;, String.class));
}

public Jws&amp;lt;Claims&amp;gt; getOIDCTokenJws(String token, String modulus, String exponent) {
    try {
        return Jwts.parserBuilder()
                .setSigningKey(getRSAPublicKey(modulus, exponent))
                .build()
                .parseClaimsJws(token);
    } catch (ExpiredJwtException e) {
        throw new BaseException(ACCESS_TOKEN_EXPIRED);
    } catch (Exception e) {
        log.error(e.toString());
        throw new BaseException(INVALID_TOKEN);
    }
}

private Key getRSAPublicKey(String modulus, String exponent)
        throws NoSuchAlgorithmException, InvalidKeySpecException {
    KeyFactory keyFactory = KeyFactory.getInstance(&quot;RSA&quot;);
    byte[] decodeN = Base64.getUrlDecoder().decode(modulus);
    byte[] decodeE = Base64.getUrlDecoder().decode(exponent);
    BigInteger n = new BigInteger(1, decodeN);
    BigInteger e = new BigInteger(1, decodeE);

    RSAPublicKeySpec keySpec = new RSAPublicKeySpec(n, e);
    return keyFactory.generatePublic(keySpec);
}&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;kid 로 가져온 공개키에서 n 과 e 를 조합하여 직접 공개키를 새로 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n과 e는 base64 로 인코딩된 값이 넘어오기 때문에 디코딩이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 n과 e는 값이 큰 수이기 때문에 BigInteger 를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 과정으로 새로 만든 공개키로 서명하여 id token 의 body 를 검증할 수 있다.&lt;/p&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;  OAuthOIDCHelper&lt;/h3&gt;
&lt;pre id=&quot;code_1679749772866&quot; class=&quot;arduino&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class OAuthOIDCHelper {
    private final JwtOIDCProvider jwtOIDCProvider;

    public OIDCDecodePayload getPayloadFromIdToken(
            String token, String iss, String aud, OIDCPublicKeysResponse oidcPublicKeysResponse) {
        String kid = getKidFromUnsignedIdToken(token, iss, aud);

        OIDCPublicKeyDto oidcPublicKeyDto =
                oidcPublicKeysResponse.getKeys().stream()
                        .filter(o -&amp;gt; o.getKid().equals(kid))
                        .findFirst()
                        .orElseThrow(() -&amp;gt; new BaseException(INVALID_TOKEN));

        return jwtOIDCProvider.getOIDCTokenBody(
                token, oidcPublicKeyDto.getN(), oidcPublicKeyDto.getE());
    }

    private String getKidFromUnsignedIdToken(String token, String iss, String aud) {
        return jwtOIDCProvider.getKidFromUnsignedTokenHeader(token, iss, aud);
    }
}&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;헤더에서 kid 를 뽑아 공개키 리스트 중에서 선택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 n 과 e 정보를 가지고 공개키를 구하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;oauth 에 인증된 사용자인지 토큰을 검증한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  OIDCDecodePayload&lt;/h3&gt;
&lt;pre id=&quot;code_1679749772866&quot; class=&quot;typescript&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@AllArgsConstructor
public class OIDCDecodePayload {
    private String iss;
    private String aud;
    private String sub;
    private String email;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id token 를 decode 했을 때 나오는 필드들&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로써 id token 을 검증하고 body 정보를 가져올 수 있다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  백엔드/  Spring Boot</category>
      <category>idtoken</category>
      <category>OAuth</category>
      <category>oauth2</category>
      <category>OIDC</category>
      <category>openIdconnect</category>
      <category>Spring</category>
      <category>소셜로그인</category>
      <category>스프링</category>
      <category>스프링소셜로그인</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/57</guid>
      <comments>https://gengminy.tistory.com/57#entry57comment</comments>
      <pubDate>Sat, 25 Mar 2023 22:21:28 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링 애플 로그인 구현하기 (Sign in with Apple OIDC)</title>
      <link>https://gengminy.tistory.com/56</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;852&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buY63A/btr5TnvgClE/3BtBqEV93X3PFSmmW6OLj1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buY63A/btr5TnvgClE/3BtBqEV93X3PFSmmW6OLj1/img.jpg&quot; data-alt=&quot;이 단순한 흑백 버튼 하나에 개발자의 무한한 피와 땀이.... 사과놈들&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buY63A/btr5TnvgClE/3BtBqEV93X3PFSmmW6OLj1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuY63A%2Fbtr5TnvgClE%2F3BtBqEV93X3PFSmmW6OLj1%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;500&quot; height=&quot;333&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;852&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이 단순한 흑백 버튼 하나에 개발자의 무한한 피와 땀이.... 사과놈들&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;공식 Docs 도 너무 불친절하고 자료도 별로 없어서 애먹었지만&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;OIDC(Open ID Connect)의 id_token 방식을 사용하여 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id_token 방식의 경우 이슈어와 앱키 등의 정보가 들어있어&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;OAuth 에서 제공하지 않는 정보의 추가 기입이 필요할 때 유용하게 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Dependency&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;implementation 'com.nimbusds:nimbus-jose-jwt:3.10'&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client secret 을 생성하기 위한 jwt 관련 라이브러리&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  Apple Developers 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플 로그인은 준비해야 할 것들이 조금 많다....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;13만원인가 하는 Apple Developer 서비스를 매년 결제해야 하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;private key 를 발급받은 후 저장한 .p8 파일을 이용하여 시크릿 키를 생성해줘야 하고&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;근데 이게 IOS 앱 등록 시&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;s&gt;아니면 리젝 먹인다.&lt;/s&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;a title=&quot;스프링 프로젝트에 애플 로그인 API 연동을 위한 Apple Developer 설정&quot; href=&quot;https://whitepaek.tistory.com/60&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&amp;gt;&amp;gt; 스프링 프로젝트에 애플 로그인 API 연동을 위한 Apple Developer 설정&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  애플 로그인 링크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Query Param 은 다음과 같다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;client_id : Services Id -&amp;gt; identifier 값&lt;/li&gt;
&lt;li&gt;redirect_uri : Return URLs 값, 로그인 직후 리다이렉션 되는 링크&lt;/li&gt;
&lt;li&gt;scope : 전달받을 정보 (name, email, openid)&lt;/li&gt;
&lt;li&gt;response_type : code 로 하면 인가 코드 반환 (고정)&lt;/li&gt;
&lt;li&gt;response_mode : (공백) -&amp;gt; GET, form_post -&amp;gt; 리다이렉션 링크에 POST 요청을 날림&lt;/li&gt;
&lt;li&gt;state : csrf 공격 방지 위한 검증 값&lt;/li&gt;
&lt;li&gt;nonce : 랜덤 값, oauth 인증 토큰 무결성 보호를 위한 것&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;  로그인 요청 시 주의할 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초 로그인 요청 링크에서 주의할 점이 몇 가지 있는데&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;response_mode 가 form_post 일 때만 scope 를 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;scope 의 name 은 최초 가입시에만 제공되는 필드이다 (이후 요청 시 값이 안나옴)&lt;/li&gt;
&lt;li&gt;redirect_uri 에는 localhost 를 사용할 수 없다. 정책적으로 막혀있다.&lt;br /&gt;ngrok 같은 포워딩 툴로 우회해야 로컬에서 테스트 가능!&lt;/li&gt;
&lt;/ol&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_1679744439117&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;https://appleid.apple.com/auth/authorize?client_id=%s&amp;amp;redirect_uri=%s&amp;amp;response_type=code&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;h2 data-ke-size=&quot;size26&quot;&gt;  토큰 요청 Feign Client&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Feign Client 를 사용중이며 관련 설정은 다음 게시글을 참고하면 되겠다.&lt;/p&gt;
&lt;figure id=&quot;og_1679744557845&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;[Spring] 스프링 Feign Client 적용하기 (Spring Cloud OpenFeign)&quot; data-og-description=&quot;  Feign Client 란? 본래 Netflix (그 넷플릭스 맞음) 에서 오픈 소스 일부로 개발되어 사용중인 경량 REST 클라이언트 현재는 Spring Cloud 프레임워크의 일부가 되었다. 인터페이스로 정의된 API를 기반&quot; data-og-host=&quot;gengminy.tistory.com&quot; data-og-source-url=&quot;https://gengminy.tistory.com/55&quot; data-og-url=&quot;https://gengminy.tistory.com/55&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pOnXe/hyR2HUwPCK/OjTfFq5g2kMksXPLw52Tek/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/cszx2o/hyR2P51B7g/NkrVAgaJXMWB2tmnUnMqU0/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/IOAJQ/hyR2RQizHK/Kj4S43djY9F9KXlquxtoD0/img.png?width=750&amp;amp;height=995&amp;amp;face=0_0_750_995&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/55&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gengminy.tistory.com/55&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pOnXe/hyR2HUwPCK/OjTfFq5g2kMksXPLw52Tek/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/cszx2o/hyR2P51B7g/NkrVAgaJXMWB2tmnUnMqU0/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/IOAJQ/hyR2RQizHK/Kj4S43djY9F9KXlquxtoD0/img.png?width=750&amp;amp;height=995&amp;amp;face=0_0_750_995');&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;[Spring] 스프링 Feign Client 적용하기 (Spring Cloud OpenFeign)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  Feign Client 란? 본래 Netflix (그 넷플릭스 맞음) 에서 오픈 소스 일부로 개발되어 사용중인 경량 REST 클라이언트 현재는 Spring Cloud 프레임워크의 일부가 되었다. 인터페이스로 정의된 API를 기반&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gengminy.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;h3 data-ke-size=&quot;size23&quot;&gt;  AppleOAuthClient&lt;/h3&gt;
&lt;pre id=&quot;code_1679744288931&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@FeignClient(
        name = &quot;AppleOAuthClient&quot;,
        url = &quot;https://appleid.apple.com&quot;,
        configuration = AppleOAuthConfig.class)
public interface AppleOAuthClient {

    @PostMapping(&quot;/auth/token?grant_type=authorization_code&quot;)
    AppleTokenResponse appleAuth(
            @RequestParam(&quot;client_id&quot;) String clientId,
            @RequestParam(&quot;redirect_uri&quot;) String redirectUri,
            @RequestParam(&quot;code&quot;) String code,
            @RequestParam(&quot;client_secret&quot;) String clientSecret);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플 로그인을 통해 받은 인가 코드를 받아 처리하는 url&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 client_secret 파라미터 인데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;p8 파일을 가지고 jwt 인코딩을 해줘서 여기에 집어넣어야 한다.&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;b&gt;무한 invalid_client 오류에 빠지게 될 것이다......&lt;/b&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;  AppleOAuthHelper&lt;/h3&gt;
&lt;pre id=&quot;code_1679744693508&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 AppleOAuthHelper {
    private final AppleOAuthProperties appleOAuthProperties;
    private final AppleOAuthClient appleOAuthClient;
    private final AppleOIDCClient appleOIDCClient;
    private final OAuthOIDCHelper oAuthOIDCHelper;
    
    //...(생락)
    
    public AppleTokenResponse getOAuthToken(String code, String referer) {
        return appleOAuthClient.appleAuth(
                appleOAuthProperties.getClientId(),
                referer + &quot;callback/apple&quot;,
                code,
                getClientSecret());
    }
    
    private String getClientSecret() {
        return AppleLoginUtil.createClientSecret(
                appleOAuthProperties.getTeamId(),
                appleOAuthProperties.getClientId(),
                appleOAuthProperties.getKeyId(),
                appleOAuthProperties.getKeyPath(),
                appleOAuthProperties.getBaseUrl());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 Feign Client 를 호출하기 전에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client secret 을 만드는 로직이 필요하다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  Client Secret&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client secret 을 만드는 방법은 다음 게시글을 기본적으로 참고하였으나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529;&quot;&gt;&lt;b&gt;ECPrivateKeyImpl&lt;/b&gt;&lt;span&gt; 과 &lt;b&gt;:classpath&lt;/b&gt; 읽어오는데 문제가 있어 &lt;/span&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;a href=&quot;https://whitepaek.tistory.com/61&quot;&gt;&amp;gt;&amp;gt; 스프링 프로젝트에 애플 로그인 API 연동하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  AppleLoginUtil&lt;/h3&gt;
&lt;pre id=&quot;code_1679744765608&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AppleLoginUtil {
    /**
     * client_secret 생성 Apple Document URL ‣
     * https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
     *
     * @return client_secret(jwt)
     */
    public static String createClientSecret(
            String teamId, String clientId, String keyId, String keyPath, String authUrl) {

        JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(keyId).build();
        JWTClaimsSet claimsSet = new JWTClaimsSet();
        Date now = new Date();

        claimsSet.setIssuer(teamId);
        claimsSet.setIssueTime(now);
        claimsSet.setExpirationTime(new Date(now.getTime() + 3600000));
        claimsSet.setAudience(authUrl);
        claimsSet.setSubject(clientId);

        SignedJWT jwt = new SignedJWT(header, claimsSet);

        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(readPrivateKey(keyPath));
        try {
            KeyFactory kf = KeyFactory.getInstance(&quot;EC&quot;);
            ECPrivateKey ecPrivateKey = (ECPrivateKey) kf.generatePrivate(spec);
            JWSSigner jwsSigner = new ECDSASigner(ecPrivateKey.getS());
            jwt.sign(jwsSigner);
        } catch (InvalidKeyException | JOSEException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new RuntimeException(e);
        }

        return jwt.serialize();
    }

    /**
     * 파일에서 private key 획득
     *
     * @return Private Key
     */
    private static byte[] readPrivateKey(String keyPath) {

        Resource resource = new ClassPathResource(keyPath);
        byte[] content = null;

        try (InputStream keyInputStream = resource.getInputStream();
                InputStreamReader keyReader = new InputStreamReader(keyInputStream);
                PemReader pemReader = new PemReader(keyReader)) {
            PemObject pemObject = pemReader.readPemObject();
            content = pemObject.getContent();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return content;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apple Developer 설정할 때 받아왔던 정보를 모두 기억해야 하는 이유이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;teamId, clientId, keyId 를 모두 요구한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 keyPath 는 .p8 파일이 들어있는 폴더이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같이 resources 디렉토리에 p8 파일을 넣어두었다면,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;607&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC2OcQ/btr5QoaB4bi/psrEP8O3L596RdTrFe1w0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC2OcQ/btr5QoaB4bi/psrEP8O3L596RdTrFe1w0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC2OcQ/btr5QoaB4bi/psrEP8O3L596RdTrFe1w0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC2OcQ%2Fbtr5QoaB4bi%2FpsrEP8O3L596RdTrFe1w0K%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;272&quot; data-origin-width=&quot;607&quot; data-origin-height=&quot;272&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;keyPath 에 대한 설정 디렉토리는 다음과 같아진다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;APPLE_KEY_PATH=static/apple/AuthKey_{keyId}.p8&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;b&gt;ECPrivateKeyImpl&lt;/b&gt;&lt;span style=&quot;color: #212529;&quot;&gt;&lt;span&gt; 같은 경우&lt;/span&gt;&lt;/span&gt; java.security 에 기본 포함된 것으로 아는데&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_1679745282172&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(readPrivateKey(keyPath));
try {
    KeyFactory kf = KeyFactory.getInstance(&quot;EC&quot;);
    ECPrivateKey ecPrivateKey = (ECPrivateKey) kf.generatePrivate(spec);
    JWSSigner jwsSigner = new ECDSASigner(ecPrivateKey.getS());
    jwt.sign(jwsSigner);
} catch (InvalidKeyException | JOSEException e) {
    e.printStackTrace();
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
    throw new RuntimeException(e);
}&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;그리고 빌드된 jar 파일 내부에서 :classpath 를 제대로 찾지 못해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FileNotFoundException 이 터지는 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getFile 같은 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Resource 객체의 참조 리소스가 classpath 에 있는 경우 오류가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 getInputStream 은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Resource 객체의 모든 리소스 내용을 바이트 단위로 읽을 수 있고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FileNotFoundException 이 터지지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1679745552711&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static byte[] readPrivateKey(String keyPath) {

    Resource resource = new ClassPathResource(keyPath);
    byte[] content = null;

    try (InputStream keyInputStream = resource.getInputStream();
            InputStreamReader keyReader = new InputStreamReader(keyInputStream);
            PemReader pemReader = new PemReader(keyReader)) {
        PemObject pemObject = pemReader.readPemObject();
        content = pemObject.getContent();
    } catch (IOException e) {
        e.printStackTrace();
    }

    return 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;이러한 과정으로 jwt 로 감싸주면 client secret 이 생성된다.&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;span&gt;다른 OAuth 에 비해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;상당히 난해한 부분이 바로 여기이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1679745707851&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;access_token&quot;:&quot;a08c1600e80f84d4484...&quot;,
  &quot;expires_in&quot;:3600,
  &quot;id_token&quot;:&quot;eyJraWQiOiJlWGF1bm...&quot;,
  &quot;refresh_token&quot;:&quot;r8e88bc9f62bc49...&quot;,
  &quot;token_type&quot;:&quot;Bearer&quot;
}&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;h3 data-ke-size=&quot;size23&quot;&gt;  AppleTokenResponse&lt;/h3&gt;
&lt;pre id=&quot;code_1679745731417&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@NoArgsConstructor
@JsonNaming(SnakeCaseStrategy.class)
public class AppleTokenResponse {
    private String accessToken;
    private String refreshToken;
    private String idToken;
}&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;이제 idToken 을 다음 요청에 사용하면 된다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  id_token 검증하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메소드와 DTO 등 자세한 것은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용이 길어져서 게시글을 분리시켰다&lt;/p&gt;
&lt;figure id=&quot;og_1679750517435&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;[Spring] 스프링 소셜 로그인에 OIDC 사용하기 (OAuth with OpenID Connect)&quot; data-og-description=&quot;  OpenID Connect (OIDC) 는 무엇인가? OAuth 2.0 프로토콜을 기반으로 한 사용자 인증 프로토콜 accessToken 이외에도 id_token을 사용하여 토큰으로 사용자가 누구인지 확인할 수도 있다. OIDC는 표준 프로토&quot; data-og-host=&quot;gengminy.tistory.com&quot; data-og-source-url=&quot;https://gengminy.tistory.com/57&quot; data-og-url=&quot;https://gengminy.tistory.com/57&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/E1UJL/hyR2N1v3lJ/XHQe5D23OzakcnwCRSdVDK/img.png?width=628&amp;amp;height=558&amp;amp;face=0_0_628_558,https://scrap.kakaocdn.net/dn/cp4AqB/hyR2WdbZ1J/KWg9YKi3sKpvdOzdIQKns0/img.png?width=628&amp;amp;height=558&amp;amp;face=0_0_628_558,https://scrap.kakaocdn.net/dn/TCVh6/hyR2NtGgnG/u5OtYKfoKo1puL2uhyqj20/img.jpg?width=750&amp;amp;height=499&amp;amp;face=0_0_750_499&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/57&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gengminy.tistory.com/57&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/E1UJL/hyR2N1v3lJ/XHQe5D23OzakcnwCRSdVDK/img.png?width=628&amp;amp;height=558&amp;amp;face=0_0_628_558,https://scrap.kakaocdn.net/dn/cp4AqB/hyR2WdbZ1J/KWg9YKi3sKpvdOzdIQKns0/img.png?width=628&amp;amp;height=558&amp;amp;face=0_0_628_558,https://scrap.kakaocdn.net/dn/TCVh6/hyR2NtGgnG/u5OtYKfoKo1puL2uhyqj20/img.jpg?width=750&amp;amp;height=499&amp;amp;face=0_0_750_499');&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;[Spring] 스프링 소셜 로그인에 OIDC 사용하기 (OAuth with OpenID Connect)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  OpenID Connect (OIDC) 는 무엇인가? OAuth 2.0 프로토콜을 기반으로 한 사용자 인증 프로토콜 accessToken 이외에도 id_token을 사용하여 토큰으로 사용자가 누구인지 확인할 수도 있다. OIDC는 표준 프로토&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gengminy.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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  id_token 으로 회원가입하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  AppleController&lt;/h3&gt;
&lt;pre id=&quot;code_1679745860845&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;/register&quot;)
public TokenResponse appleOAuthRegistration(
        @RequestParam(&quot;id_token&quot;) String token,
        @Valid @RequestBody UserRegistrationRequest userRegistrationRequest) {
    return appleService.registerUserByOCIDToken(token, userRegistrationRequest);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발급받은 id token 을 통해 회원가입을 진행해보자.&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;  AppleService&lt;/h3&gt;
&lt;pre id=&quot;code_1679745931300&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public TokenAndUserResponse registerUserByOCIDToken(
			String idToken, UserRegistrationRequest userRegistrationRequest) {
    final OAuthInfo oAuthInfo = this.getOAuthInfoByIdToken(idToken);
    final UserProfileDto userProfileDto = userRegistrationRequest.toProfile();

    final User user = userService.register(userProfileDto, oAuthInfo);
    return tokenGenerateHelper.generate(user);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apple OAuth 에서는 유저 정보에 대한 스코프가 현저하게 적기 때문에 &lt;b&gt;(이름, 이메일로 끝)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저 회원가입 정보를 반드시 추가로 받아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 받아서 회원가입을 진행하고 토큰을 발급해주는 로직이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Apple OAuth 에 대한 글이기 때문에 자세한 로직은 생략.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public OAuthInfo getOAuthInfoByIdToken(String idToken) {
    OIDCDecodePayload oidcDecodePayload = getOIDCDecodePayload(idToken);
    return OAuthInfo.builder().provider(Provider.APPLE).oid(oidcDecodePayload.getSub()).build();
}

public OIDCDecodePayload getOIDCDecodePayload(String token) {
    OIDCPublicKeysResponse oidcPublicKeysResponse = appleOIDCClient.getAppleOIDCOpenKeys();
    return oAuthOIDCHelper.getPayloadFromIdToken(
            token,
            appleOAuthProperties.getBaseUrl(),
            appleOAuthProperties.getClientId(),
            oidcPublicKeysResponse);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id token 을 decode 하여 정보를 가져오는 로직&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 구현한 id token 검증 로직으로 검증해주면 decode 가 완료된다.&lt;/p&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;  OAuthInfo / Provider&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
@AllArgsConstructor
@Builder
public class OAuthInfo {
    private Provider provider;
    private String oid;

    public static OAuthInfo from(OAuthUserInfoDto oAuthUserInfoDto) {
        return new OAuthInfo(oAuthUserInfoDto.getOAuthProvider(), oAuthUserInfoDto.getOauthId());
    }
}

@Getter
@AllArgsConstructor
public enum Provider {
    GOOGLE(&quot;GOOGLE&quot;),
    KAKAO(&quot;KAKAO&quot;),
    APPLE(&quot;APPLE&quot;);

    private String value;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth provider 와 유저 고유 아이디인 oid 값을 담는 객체&lt;/p&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;  TokenGenerationHelper&lt;/h3&gt;
&lt;pre id=&quot;code_1679745967618&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 TokenGenerationHelper {

    private final JwtTokenProvider jwtTokenProvider;
    private final UserService userService;

    @Transactional
    public TokenAndUserResponse generate(User user) {
        final String newAccessToken =
                jwtTokenProvider.generateAccessToken(user.getId(), user.getRole().getValue());
        final String newRefreshToken = jwtTokenProvider.generateRefreshToken(user.getId());

        user.setRefreshToken(newRefreshToken);
        userService.upsert(user);

        return TokenAndUserResponse.builder()
                .accessToken(newAccessToken)
                .accessTokenAge(jwtTokenProvider.getAccessTokenTTLSecond())
                .refreshTokenAge(jwtTokenProvider.getRefreshTokenTTLSecond())
                .refreshToken(newRefreshToken)
                .build();
    }
}&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  id_token 으로 로그인 하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  AppleController&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@PostMapping(&quot;/login&quot;)
public TokenResponse appleOAuthUserLogin(@RequestParam(&quot;id_token&quot;) String token) {
    return appleService.login(token);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 id token 으로 우리 서버 로그인을 해보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발급받은 id token 을 가져온다.&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;  AppleService&lt;/h3&gt;
&lt;pre id=&quot;code_1679748746887&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public TokenResponse login(String idToken) {
    OAuthInfo oAuthInfo = this.getOAuthInfoByIdToken(idToken);
    User user = userService.login(oAuthInfo);
    return tokenGenerationHelper.generate(user);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;oAuthInfo 를 가지고 유저 정보를 찾음&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;  UserService&lt;/h3&gt;
&lt;pre id=&quot;code_1679748876700&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public User login(OAuthInfo oAuthInfo) {
    User user = userAdaptor.queryByOAuthInfo(oAuthInfo);
    user.login();
    return user;
}

//User
public void login() {
    if (!this.state.equals(ACTIVE)) throw new BaseException(INACTIVE_USER);
}&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;OIDC 방식과 Apple login 모두를 적용하는게 상당히 난이도 있지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 관련 지식이 더욱 상승하는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 OAuth 로그인 대응 시에도 동일하게 사용 가능하니까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한번 적용하면 편리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자는 실제론 이런 식으로 Provider 별로 Switch 하여 대응 중이다.&lt;/p&gt;
&lt;pre id=&quot;code_1679749087764&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private OAuthInfo getOAuthInfoByProviderAndIdToken(Provider provider, String idToken) {
    switch (provider) {
        case GOOGLE:
            return googleOAuthHelper.getOAuthInfoByIdToken(idToken);
        case KAKAO:
            return kakaoOAuthHelper.getOAuthInfoByIdToken(idToken);
        case APPLE:
            return appleOAuthHelper.getOAuthInfoByIdToken(idToken);
        default:
            throw new BaseException(INVALID_OAUTH_PROVIDER);
    }
}&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;</description>
      <category>  백엔드/  Spring Boot</category>
      <category>applelogin</category>
      <category>appleoauth</category>
      <category>OAuth</category>
      <category>OIDC</category>
      <category>signinwithapple</category>
      <category>Spring</category>
      <category>스프링</category>
      <category>스프링부트</category>
      <category>스프링애플로그인</category>
      <category>애플로그인</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/56</guid>
      <comments>https://gengminy.tistory.com/56#entry56comment</comments>
      <pubDate>Sat, 25 Mar 2023 21:58:55 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링 Feign Client 적용하기 (Spring Cloud OpenFeign)</title>
      <link>https://gengminy.tistory.com/55</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btwQlP/btr5OQyURYp/GKBdTffp1HpzcW8kIXSSIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btwQlP/btr5OQyURYp/GKBdTffp1HpzcW8kIXSSIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btwQlP/btr5OQyURYp/GKBdTffp1HpzcW8kIXSSIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtwQlP%2Fbtr5OQyURYp%2FGKBdTffp1HpzcW8kIXSSIk%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;400&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Feign Client 란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본래 &lt;b&gt;Netflix&lt;/b&gt; (그 넷플릭스 맞음) 에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈 소스 일부로 개발되어 사용중인 경량 REST 클라이언트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 Spring Cloud 프레임워크의 일부가 되었다.&lt;/p&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를 기반으로 RESTful 서비스를 호출한다.&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;h2 data-ke-size=&quot;size26&quot;&gt;  Dependency&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-openfeign', version: '3.1.1'&lt;br /&gt;implementation 'io.github.openfeign:feign-jackson:12.1'&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;h2 data-ke-size=&quot;size26&quot;&gt;  @EnableFeignClient&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Feign 을 사용하기 위한 어노테이션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;class 를 가져오는 방식과 basePackage 를 기반으로 scan 하는 방식이 있다.&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;  FeignCommonConfig&lt;/h3&gt;
&lt;pre id=&quot;code_1679740862983&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableFeignClients(basePackageClasses = BaseFeignClientPackage.class)
public class FeignCommonConfig {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트에서는 basePackage 를 스캔하는 방식을 사용했다.&lt;/p&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;  BaseFeignClientPackage&lt;/h3&gt;
&lt;pre id=&quot;code_1679740887456&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface BaseFeignClientPackage {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Feign Client 들이 있는 패키지에 이 인터페이스 파일을 넣어주면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 패키지 하위의 모든 FeignClient 들을 스캔해준다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  @FeignClient&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Feign 에서 본격적으로 외부 서버와 통신하게 해주는 Client&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;  RestTemplete 예시&lt;/h3&gt;
&lt;pre id=&quot;code_1679741513742&quot; class=&quot;processing&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public CommonResponse addUser() {
  RestTemplate restTemplate = new RestTemplate();

  HttpHeaders headers = new HttpHeaders();
  headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

  MultiValueMap&amp;lt;String, String&amp;gt; map = new LinkedMultiValueMap&amp;lt;&amp;gt;();
  map.add(&quot;userId&quot;, &quot;lim&quot;);
  map.add(&quot;userName&quot;, &quot;sua&quot;);
  map.add(&quot;age&quot;, &quot;21&quot;);
  
  HttpEntity&amp;lt;MultiValueMap&amp;lt;String, String&amp;gt;&amp;gt; request = new HttpEntity&amp;lt;&amp;gt;(map, headers);
  ResponseEntity&amp;lt;CommonResponse&amp;gt; response = 
    restTemplate.postForEntity(USER_URL + &quot;/form&quot;, request, CommonResponse.class);

  return response.getBody();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;만약 RestTemplete 이나 Webclient 로 구현되었으면&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 서비스 레이어에서 http 요청이 수행됐을 거다&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;  AppleOAuthClient&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@FeignClient(
        name = &quot;AppleOAuthClient&quot;,
        url = &quot;https://appleid.apple.com&quot;,
        configuration = AppleOAuthConfig.class)
public interface AppleOAuthClient {

    @PostMapping(&quot;/auth/token?grant_type=authorization_code&quot;)
    AppleTokenResponse appleAuth(
            @RequestParam(&quot;client_id&quot;) String clientId,
            @RequestParam(&quot;redirect_uri&quot;) String redirectUri,
            @RequestParam(&quot;code&quot;) String code,
            @RequestParam(&quot;client_secret&quot;) String clientSecret);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;애플 oauth 서버에서 받은 인가 코드를 넘겨서 인증하는 Client 예시&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;반면 Feign Client 같은 경우는&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;의견 차이가 있을 수 있겠지만 Feign Client 가 가독성이 더 높아 보인다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  추가 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 파일을 통해 Feign Client 에 대한 추가 설정을 해줄 수 있다.&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;  AppleOAuthConfig&lt;/h3&gt;
&lt;pre id=&quot;code_1679741570813&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Import(AppleOAuthErrorDecoder.class)
public class AppleOAuthConfig {

    @Bean
    @ConditionalOnMissingBean(value = ErrorDecoder.class)
    public AppleOAuthErrorDecoder commonFeignErrorDecoder() {
        return new AppleOAuthErrorDecoder();
    }

    @Bean
    Encoder formEncoder() {
        return new feign.form.FormEncoder();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@ConditionalOnMissingBean&lt;/b&gt; 어노테이션은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 ErrorDecoder 가 존재하면 무시,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;존재하지 않을 경우 &lt;b&gt;AppleOAuthErrorDecoder&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;FeignClient 같은 경우 200 이외 값에 대해서 에러를 처리 해주는데&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;Encoder&lt;/b&gt; 는 객체를 Key - Value 의 폼 데이터로 변환시켜준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 Java 객체를 HTTP POST 요청에 대한 폼 데이터로 보낼 수 있도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Content-Type 헤더는 application/x-www-form-urlencoded 로 설정된다.&lt;/p&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;  AppleOAuthDecoder&lt;/h3&gt;
&lt;pre id=&quot;code_1679741981840&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AppleOAuthErrorDecoder implements ErrorDecoder {
    @Override
    @SneakyThrows
    public Exception decode(String methodKey, Response response) {
        InputStream inputStream = response.body().asInputStream();
        byte[] byteArray = IOUtils.toByteArray(inputStream);
        String responseBody = new String(byteArray);
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode jsonNode = objectMapper.readTree(responseBody);

        String error = jsonNode.get(&quot;error&quot;) == null ? null : jsonNode.get(&quot;error&quot;).asText();
        String errorDescription =
                jsonNode.get(&quot;error_description&quot;) == null
                        ? null
                        : jsonNode.get(&quot;error_description&quot;).asText();

        System.out.println(jsonNode);
        throw new BaseDynamicException(response.status(), error, errorDescription);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ErrorDecoder 를 implements 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 에러 코드에 대한 처리를 커스터마이징 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 인증을 예시로 들면&lt;/p&gt;
&lt;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;  KauthErrorDecoder&lt;/h3&gt;
&lt;pre id=&quot;code_1679742099330&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class KauthErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {
        KakaoKauthErrorResponse body = KakaoKauthErrorResponse.from(response);

        try {
            KakaoKauthErrorCode kakaoKauthErrorCode =
                    KakaoKauthErrorCode.valueOf(body.getErrorCode());
            throw kakaoKauthErrorCode.getException();
        } catch (IllegalArgumentException e) {
            KakaoKauthErrorCode koeInvalidRequest = KakaoKauthErrorCode.KOE_INVALID_REQUEST;
            throw koeInvalidRequest.getException();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시로 카카오 oauth 같은 경우에는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KOE303 과 같이 카카오 자체 에러코드를 제공해준다.&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;h2 data-ke-size=&quot;size26&quot;&gt;  그렇다면 FeignClient 호출은?&lt;/h2&gt;
&lt;pre id=&quot;code_1679742276398&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public AppleTokenResponse getOAuthTokenTest(String code) {
    return appleOAuthClient.appleAuth(
            appleOAuthProperties.getClientId(),
            appleOAuthProperties.getRedirectUrl(),
            code,
            getClientSecret());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마치 메소드를 호출하는 것 처럼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 레이어에서 파라미터를 넣어줌으로써 호출할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로직 분리에 정말 탁월하고 편리하다.&lt;/p&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;특히 외부 서버의 API 를 호출하는 코드가 많다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Feign Client 의 도입을 적극적으로 고려해봐도 좋을 것 같다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  백엔드/  Spring Boot</category>
      <category>feign</category>
      <category>feignclient</category>
      <category>openfeign</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>SpringCloud</category>
      <category>springcloudfeign</category>
      <category>스프링</category>
      <category>스프링부트</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/55</guid>
      <comments>https://gengminy.tistory.com/55#entry55comment</comments>
      <pubDate>Sat, 25 Mar 2023 20:07:58 +0900</pubDate>
    </item>
    <item>
      <title>[DuDoong] 두둥 프로젝트 관련 글 정리</title>
      <link>https://gengminy.tistory.com/54</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cb6JDi/btr3Xy8jLji/ekfgRl0FjM89DHAMs0JGe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cb6JDi/btr3Xy8jLji/ekfgRl0FjM89DHAMs0JGe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cb6JDi/btr3Xy8jLji/ekfgRl0FjM89DHAMs0JGe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcb6JDi%2Fbtr3Xy8jLji%2FekfgRl0FjM89DHAMs0JGe1%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;500&quot; height=&quot;500&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  사용 언어 / 프레임워크&lt;/h2&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;Java + Spring Boot&lt;br /&gt;MySQL&lt;br /&gt;Redis&lt;br /&gt;Docker&lt;br /&gt;JUnit5&lt;br /&gt;Spring Batch&lt;br /&gt;Spring Cloud Feign&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  관련 포스팅&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://gengminy.tistory.com/47&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Custom Enum Validator 구현하기 (Enum 값 JSON Parse Error 해결)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gengminy.tistory.com/48&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Reflection 을 이용하여 Enum Validator 개선하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gengminy.tistory.com/49&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Custom&amp;nbsp;Enum&amp;nbsp;Deserializer&amp;nbsp;구현하여&amp;nbsp;Enum&amp;nbsp;에&amp;nbsp;없는&amp;nbsp;값&amp;nbsp;null&amp;nbsp;로&amp;nbsp;파싱하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gengminy.tistory.com/50&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링&amp;nbsp;날짜&amp;nbsp;타입&amp;nbsp;JSON&amp;nbsp;변환&amp;nbsp;및&amp;nbsp;포맷팅하기&amp;nbsp;-&amp;nbsp;@JsonFormat,&amp;nbsp;@JacksonAnnotationsInside&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gengminy.tistory.com/51&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Incoming WebHooks 로 슬랙봇 생성 및 슬랙 메세지 전송하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gengminy.tistory.com/53&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;500 내부 에러 발생 시 Slack 알림 전송하기 (슬랙 봇)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;✨ 웹 사이트 주소&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dudoong.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://dudoong.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1678891327853&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;dudoong.com&quot; data-og-source-url=&quot;https://dudoong.com/&quot; data-og-url=&quot;https://dudoong.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/kDAJ6/hyRWrjWSLy/kfbtdtQN9gt2ttvGtvUZp0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://dudoong.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dudoong.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/kDAJ6/hyRWrjWSLy/kfbtdtQN9gt2ttvGtvUZp0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&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;dudoong.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;</description>
      <category>  프로젝트/  두둥</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/54</guid>
      <comments>https://gengminy.tistory.com/54#entry54comment</comments>
      <pubDate>Wed, 15 Mar 2023 23:42:11 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링 500 에러 발생 시 Slack 알림 전송하기 (슬랙 봇)</title>
      <link>https://gengminy.tistory.com/53</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1041&quot; data-origin-height=&quot;659&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chCIAz/btr34cjfkfr/31Z6tfhioTRVOPRZQdL1p0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chCIAz/btr34cjfkfr/31Z6tfhioTRVOPRZQdL1p0/img.png&quot; data-alt=&quot;500 에러 발생시 메세지를 보내주는 슬랙 봇 2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chCIAz/btr34cjfkfr/31Z6tfhioTRVOPRZQdL1p0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchCIAz%2Fbtr34cjfkfr%2F31Z6tfhioTRVOPRZQdL1p0%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;600&quot; height=&quot;380&quot; data-origin-width=&quot;1041&quot; data-origin-height=&quot;659&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;500 에러 발생시 메세지를 보내주는 슬랙 봇 2&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;지난 글에 이은,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본격적으로 500 내부 서버 에러 발생 시&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;Slack 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;h2 id=&quot;%E-%-A%--%EF%B-%-F%--Dependency&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;⚙️ Dependency&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;implementation 'com.slack.api:slack-api-client:1.27.2'&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 의존성을 build.gradle 에 추가해준다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  Slack API 사용하기 위한 설정&lt;/h2&gt;
&lt;h3 id=&quot;%F-%-F%--%-D%--HostController&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;  SlackApiConfig&lt;/h3&gt;
&lt;pre id=&quot;code_1678887392836&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class SlackApiConfig {

    @Value(&quot;${slack.webhook.token}&quot;)
    private String token;

    @Bean
    public MethodsClient getClient() {
        Slack slackClient = Slack.getInstance();
        return slackClient.methods(token);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 지속적으로 알림을 보내는 봇으로 만들고자 슬랙 봇을 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬랙 봇을 사용할 때는 슬랙 관련 설정이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MethodClient 빈에 슬랙 웹훅 토큰을 등록하는 과정이 필요한데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬랙 웹훅 토큰(OAuth Token)은 &lt;b&gt;xoxb- 로 시작하는 문자열&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/52&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://gengminy.tistory.com/52&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1678888906938&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;[Slack] 슬랙 봇 설정 및 슬랙 OAuth Token 발급받기&quot; data-og-description=&quot;스프링에서 Slack API 를 사용하기 위해서는 슬랙 웹훅 토큰이 필요하다. 따라서 슬랙 토큰을 발급받고 슬랙 봇을 등록하는 과정을 알아보자. 1. 워크스페이스 및 슬랙 APP 생성 https://api.slack.com/apps &quot; data-og-host=&quot;gengminy.tistory.com&quot; data-og-source-url=&quot;https://gengminy.tistory.com/52&quot; data-og-url=&quot;https://gengminy.tistory.com/52&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dRUl5e/hyRWm31AVK/UujetCI9OknyiP3D4lABS1/img.png?width=800&amp;amp;height=555&amp;amp;face=0_0_800_555,https://scrap.kakaocdn.net/dn/bcpsp5/hyRWqeePgD/tazzuFjFx3dVMwDTxE7DSk/img.png?width=800&amp;amp;height=555&amp;amp;face=0_0_800_555,https://scrap.kakaocdn.net/dn/eVIrt/hyRWo1MsMl/kbuLkHFWOogxFhuK8eGzoK/img.png?width=1446&amp;amp;height=1188&amp;amp;face=0_0_1446_1188&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/52&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gengminy.tistory.com/52&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dRUl5e/hyRWm31AVK/UujetCI9OknyiP3D4lABS1/img.png?width=800&amp;amp;height=555&amp;amp;face=0_0_800_555,https://scrap.kakaocdn.net/dn/bcpsp5/hyRWqeePgD/tazzuFjFx3dVMwDTxE7DSk/img.png?width=800&amp;amp;height=555&amp;amp;face=0_0_800_555,https://scrap.kakaocdn.net/dn/eVIrt/hyRWo1MsMl/kbuLkHFWOogxFhuK8eGzoK/img.png?width=1446&amp;amp;height=1188&amp;amp;face=0_0_1446_1188');&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;[Slack] 슬랙 봇 설정 및 슬랙 OAuth Token 발급받기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;스프링에서 Slack API 를 사용하기 위해서는 슬랙 웹훅 토큰이 필요하다. 따라서 슬랙 토큰을 발급받고 슬랙 봇을 등록하는 과정을 알아보자. 1. 워크스페이스 및 슬랙 APP 생성 https://api.slack.com/apps&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gengminy.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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;  슬랙 알림 구현하기&lt;/h2&gt;
&lt;h3 id=&quot;%F-%-F%--%-D%--HostController&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;  SlackHelper&lt;/h3&gt;
&lt;pre id=&quot;code_1678889127194&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
@Slf4j
public class SlackHelper {
    private final SpringEnvironmentHelper springEnvironmentHelper;

    private final MethodsClient methodsClient;

    public void sendNotification(String CHANNEL_ID, List&amp;lt;LayoutBlock&amp;gt; layoutBlocks) {
        if (!springEnvironmentHelper.isProdAndStagingProfile()) {
            return;
        }
        ChatPostMessageRequest chatPostMessageRequest =
                ChatPostMessageRequest.builder()
                        .channel(CHANNEL_ID)
                        .text(&quot;&quot;)
                        .blocks(layoutBlocks)
                        .build();
        try {
            methodsClient.chatPostMessage(chatPostMessageRequest);
        } catch (SlackApiException | IOException slackApiException) {
            log.error(slackApiException.toString());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringEnvironmentHelper 는 현재 프로파일을 가져오도록 만든&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 유틸 클래스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prod 환경일 때만 Slack 알림을 보여주기 위해 도입했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클래스에서는 실제 Slack 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;ChatPostMessageRequest 인스턴스를 생성하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;methodsClient.chatPostMessage 의 인자로 넣고 호출하면&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 id=&quot;%F-%-F%--%-D%--HostController&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;  SlackErrorNotificationProvider&lt;/h3&gt;
&lt;pre id=&quot;code_1678889368403&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
@Slf4j
public class SlackErrorNotificationProvider {

    private final SlackHelper slackHelper;

    private final int MAX_LEN = 500;

    @Value(&quot;${slack.webhook.id}&quot;)
    private String CHANNEL_ID;

    public String getErrorStack(Throwable throwable) {
        String exceptionAsString = Arrays.toString(throwable.getStackTrace());
        int cutLength = Math.min(exceptionAsString.length(), MAX_LEN);
        return exceptionAsString.substring(0, cutLength);
    }

    @Async
    public void sendNotification(List&amp;lt;LayoutBlock&amp;gt; layoutBlocks) {
        slackHelper.sendNotification(CHANNEL_ID, layoutBlocks);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크스페이스의 채널 ID를 설정하고 전송 메소드를 호출하는 클래스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getErrorStack 는 해당 에러의 StackTrace 를 꺼내어&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 id=&quot;%F-%-F%--%-D%--HostController&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;  SlackInternalErrorSender&lt;/h3&gt;
&lt;pre id=&quot;code_1678889530592&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
@Slf4j
public class SlackInternalErrorSender {
    private final ObjectMapper objectMapper;

    private final SlackErrorNotificationProvider slackProvider;

    public void execute(ContentCachingRequestWrapper cachingRequest, Exception e, Long userId)
            throws IOException {
        final String url = cachingRequest.getRequestURL().toString();
        final String method = cachingRequest.getMethod();
        final String body =
                objectMapper.readTree(cachingRequest.getContentAsByteArray()).toString();

        final String errorMessage = e.getMessage();
        String errorStack = slackProvider.getErrorStack(e);
        final String errorUserIP = cachingRequest.getRemoteAddr();

        List&amp;lt;LayoutBlock&amp;gt; layoutBlocks = new ArrayList&amp;lt;&amp;gt;();
        layoutBlocks.add(
                Blocks.header(
                        headerBlockBuilder -&amp;gt;
                                headerBlockBuilder.text(plainText(&quot;Error Detection&quot;))));
        layoutBlocks.add(divider());

        MarkdownTextObject errorUserIdMarkdown =
                MarkdownTextObject.builder().text(&quot;* User Id :*\n&quot; + userId).build();
        MarkdownTextObject errorUserIpMarkdown =
                MarkdownTextObject.builder().text(&quot;* User IP :*\n&quot; + errorUserIP).build();
        layoutBlocks.add(
                section(
                        section -&amp;gt;
                                section.fields(List.of(errorUserIdMarkdown, errorUserIpMarkdown))));

        MarkdownTextObject methodMarkdown =
                MarkdownTextObject.builder()
                        .text(&quot;* Request Addr :*\n&quot; + method + &quot; : &quot; + url)
                        .build();
        MarkdownTextObject bodyMarkdown =
                MarkdownTextObject.builder().text(&quot;* Request Body :*\n&quot; + body).build();
        List&amp;lt;TextObject&amp;gt; fields = List.of(methodMarkdown, bodyMarkdown);
        layoutBlocks.add(section(section -&amp;gt; section.fields(fields)));

        layoutBlocks.add(divider());

        MarkdownTextObject errorNameMarkdown =
                MarkdownTextObject.builder().text(&quot;* Message :*\n&quot; + errorMessage).build();
        MarkdownTextObject errorStackMarkdown =
                MarkdownTextObject.builder().text(&quot;* Stack Trace :*\n&quot; + errorStack).build();
        layoutBlocks.add(
                section(section -&amp;gt; section.fields(List.of(errorNameMarkdown, errorStackMarkdown))));

        slackProvider.sendNotification(layoutBlocks);
    }
}&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;커스텀하여 메세지 형식을 바꿔서 예쁘게 보여줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마크다운 오브젝트는 Slack 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;여기서 주목해야 할 점은 ContentCachingRequestWrapper 인데 후술하겠다.&lt;/p&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 id=&quot;%F-%-F%--%-D%--HostController&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;  GlobalExceptionHandler&lt;/h3&gt;
&lt;pre id=&quot;code_1678889722958&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestControllerAdvice
@Slf4j
@RequiredArgsConstructor
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    private final SlackInternalErrorSender slackInternalErrorSender;
    
    //...(생략)
    
    @ExceptionHandler(Exception.class)
    protected ResponseEntity&amp;lt;ErrorResponse&amp;gt; handleException(Exception e, HttpServletRequest request)
            throws IOException {
        final ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper) request;
        final Long userId = SecurityUtils.getCurrentUserId();
        String url =
                UriComponentsBuilder.fromHttpRequest(new ServletServerHttpRequest(request))
                        .build()
                        .toUriString();

        log.error(&quot;INTERNAL_SERVER_ERROR&quot;, e);
        GlobalErrorCode internalServerError = GlobalErrorCode.INTERNAL_SERVER_ERROR;
        ErrorResponse errorResponse =
                new ErrorResponse(
                        internalServerError.getStatus(),
                        internalServerError.getCode(),
                        internalServerError.getReason(),
                        url);

        slackInternalErrorSender.execute(cachingRequest, e, userId);
        return ResponseEntity.status(HttpStatus.valueOf(internalServerError.getStatus()))
                .body(errorResponse);
    }
}&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;GlobalExceptionHandler 이다.&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;여기서 HttpServletRequest 를 즉시 사용하지 않고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ContentCachingRequestWrapper&lt;span&gt; 로 감싸주는 형태를 사용하게 된다.&lt;/span&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;color: #333333;&quot;&gt;httpServletRequest의 내용을 가져오기 위해 getInputStream() 을 사용해야하는데,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;한번 사용하게 될 경우 내용이 사라지게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;그래서 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;java&lt;/span&gt;.&lt;span style=&quot;color: #000000;&quot;&gt;lang&lt;/span&gt;.IllegalStateException:&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;getReader&lt;/span&gt;()&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;has already been called&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;for&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;this&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;request 에러가 발생한다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그렇기 때문에 ContentCachingRequestWrapper&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 로 감싸주어 전달하는 것이다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 되면 원하는 처리가 이루어지게 된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  프로젝트/  두둥</category>
      <category>slack</category>
      <category>SlackAPI</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>스프링</category>
      <category>스프링부트</category>
      <category>슬랙</category>
      <category>슬랙API</category>
      <category>슬랙알림</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/53</guid>
      <comments>https://gengminy.tistory.com/53#entry53comment</comments>
      <pubDate>Wed, 15 Mar 2023 23:21:08 +0900</pubDate>
    </item>
    <item>
      <title>[Slack] 슬랙 봇 설정 및 슬랙 OAuth Token 발급받기</title>
      <link>https://gengminy.tistory.com/52</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 Slack 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;따라서 슬랙 토큰을 발급받고 슬랙 봇을 등록하는 과정을 알아보자.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 워크스페이스 및 슬랙 APP 생성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://api.slack.com/apps&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://api.slack.com/apps&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1678888352976&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;Slack API: Applications | Slack&quot; data-og-description=&quot;Your Apps Don't see an app you're looking for? Sign in to another workspace.&quot; data-og-host=&quot;api.slack.com&quot; data-og-source-url=&quot;https://api.slack.com/apps&quot; data-og-url=&quot;https://api.slack.com/apps&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://api.slack.com/apps&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://api.slack.com/apps&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;Slack API: Applications | Slack&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Your Apps Don't see an app you're looking for? Sign in to another workspace.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;api.slack.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 작업 이전에 Slack 워크스페이스를 생성해두어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 슬랙 APP 을 생성하기 위해 Slack API 사이트에 접속합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1781&quot; data-origin-height=&quot;821&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mEK64/btr37AC4I1e/4BsHOPc8l5VdQQjbletd81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mEK64/btr37AC4I1e/4BsHOPc8l5VdQQjbletd81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mEK64/btr37AC4I1e/4BsHOPc8l5VdQQjbletd81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmEK64%2Fbtr37AC4I1e%2F4BsHOPc8l5VdQQjbletd81%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;1781&quot; height=&quot;821&quot; data-origin-width=&quot;1781&quot; data-origin-height=&quot;821&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽 상단&lt;b&gt; Create New App&lt;/b&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1KT4q/btr34Nja1KQ/PGX0Rtx3F5T15saaya3v81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1KT4q/btr34Nja1KQ/PGX0Rtx3F5T15saaya3v81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1KT4q/btr34Nja1KQ/PGX0Rtx3F5T15saaya3v81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1KT4q%2Fbtr34Nja1KQ%2FPGX0Rtx3F5T15saaya3v81%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;1091&quot; height=&quot;920&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;920&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App 이름 지어주고 이 앱을 등록할 워크스페이스를 지정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Create App 버튼 클릭&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Bot 유저 추가&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1446&quot; data-origin-height=&quot;1188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SQhPq/btr35ciLe1y/ukznkoIYC6HK1pXUk2KHBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SQhPq/btr35ciLe1y/ukznkoIYC6HK1pXUk2KHBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SQhPq/btr35ciLe1y/ukznkoIYC6HK1pXUk2KHBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSQhPq%2Fbtr35ciLe1y%2FukznkoIYC6HK1pXUk2KHBk%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;1446&quot; height=&quot;1188&quot; data-origin-width=&quot;1446&quot; data-origin-height=&quot;1188&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 생성 후 Bots 를 선택해줍니다.&lt;/p&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Scopes 지정&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1480&quot; data-origin-height=&quot;1157&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0fm9U/btr34WfTG0R/FvMjaecfwjYSQOfJBZpdsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0fm9U/btr34WfTG0R/FvMjaecfwjYSQOfJBZpdsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0fm9U/btr34WfTG0R/FvMjaecfwjYSQOfJBZpdsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0fm9U%2Fbtr34WfTG0R%2FFvMjaecfwjYSQOfJBZpdsK%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;1480&quot; height=&quot;1157&quot; data-origin-width=&quot;1480&quot; data-origin-height=&quot;1157&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth &amp;amp; Permissions 탭에서 Scopes 를 찾습니다.&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;769&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgAJmb/btr32tFCrBi/ppKhQp22WhoYlD1MnZdFqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgAJmb/btr32tFCrBi/ppKhQp22WhoYlD1MnZdFqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgAJmb/btr32tFCrBi/ppKhQp22WhoYlD1MnZdFqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgAJmb%2Fbtr32tFCrBi%2FppKhQp22WhoYlD1MnZdFqK%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;1145&quot; height=&quot;769&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;769&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;메세지 전송 권한을 위해 chat:write 는 필수겠죠?&lt;/p&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. OAuth Token 발급&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1524&quot; data-origin-height=&quot;703&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bl6Mqq/btr34BQzc9A/brMY6yuffDbFDFfk6QuAUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bl6Mqq/btr34BQzc9A/brMY6yuffDbFDFfk6QuAUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bl6Mqq/btr34BQzc9A/brMY6yuffDbFDFfk6QuAUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbl6Mqq%2Fbtr34BQzc9A%2FbrMY6yuffDbFDFfk6QuAUk%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;1524&quot; height=&quot;703&quot; data-origin-width=&quot;1524&quot; data-origin-height=&quot;703&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Install to Workspace 버튼을 통해 워크스페이스에 설치.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1563&quot; data-origin-height=&quot;810&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dpnf4/btr3Xy8ih0b/jOKLF0xLr6rH3DabVU6KUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dpnf4/btr3Xy8ih0b/jOKLF0xLr6rH3DabVU6KUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dpnf4/btr3Xy8ih0b/jOKLF0xLr6rH3DabVU6KUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDpnf4%2Fbtr3Xy8ih0b%2FjOKLF0xLr6rH3DabVU6KUK%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;1563&quot; height=&quot;810&quot; data-origin-width=&quot;1563&quot; data-origin-height=&quot;810&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 원하는 OAuth Token 을 획득했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slack API 토큰은 xoxb- 로 시작하는 문자열입니다.&lt;/p&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 워크스페이스에 봇 유저 추가&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;406&quot; data-origin-height=&quot;213&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/moAQa/btr35ciLaBf/Nd7ibgP9jHNmw1tgCBSIA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/moAQa/btr35ciLaBf/Nd7ibgP9jHNmw1tgCBSIA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/moAQa/btr35ciLaBf/Nd7ibgP9jHNmw1tgCBSIA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmoAQa%2Fbtr35ciLaBf%2FNd7ibgP9jHNmw1tgCBSIA1%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;406&quot; height=&quot;213&quot; data-origin-width=&quot;406&quot; data-origin-height=&quot;213&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크스페이스로 돌아와서 원하는 채널 오른쪽 클릭 후 &quot;앱 추가&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;735&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkjDSo/btr33y0THyg/fiA58Y9sC7JqYO5ZXKkfAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkjDSo/btr33y0THyg/fiA58Y9sC7JqYO5ZXKkfAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkjDSo/btr33y0THyg/fiA58Y9sC7JqYO5ZXKkfAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkjDSo%2Fbtr33y0THyg%2FfiA58Y9sC7JqYO5ZXKkfAK%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;1052&quot; height=&quot;735&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;735&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통합 -&amp;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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;793&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bArCsc/btr34BXkPZG/ckjRtGvsau1XssQVTB8lh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bArCsc/btr34BXkPZG/ckjRtGvsau1XssQVTB8lh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bArCsc/btr34BXkPZG/ckjRtGvsau1XssQVTB8lh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbArCsc%2Fbtr34BXkPZG%2FckjRtGvsau1XssQVTB8lh0%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;1143&quot; height=&quot;793&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;793&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;추가 해줍니다.&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;133&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Of1xX/btr38oWLf87/CpA41SaWVBfQ2E4r5DJAAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Of1xX/btr38oWLf87/CpA41SaWVBfQ2E4r5DJAAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Of1xX/btr38oWLf87/CpA41SaWVBfQ2E4r5DJAAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOf1xX%2Fbtr38oWLf87%2FCpA41SaWVBfQ2E4r5DJAAk%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;1074&quot; height=&quot;133&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;133&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 채널에 이렇게 뜬다면 성공입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 발급받은 봇 유저 토큰으로 Slack API 를 사용하시면 됩니다.&lt;/p&gt;</description>
      <category>  Infra/⚙ 준비를 위한 준비</category>
      <category>slack</category>
      <category>SlackAPI</category>
      <category>SlackToken</category>
      <category>Spring</category>
      <category>token</category>
      <category>슬랙</category>
      <category>슬랙API</category>
      <category>슬랙봇</category>
      <category>슬랙알림</category>
      <category>슬랙토큰</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/52</guid>
      <comments>https://gengminy.tistory.com/52#entry52comment</comments>
      <pubDate>Wed, 15 Mar 2023 23:01:20 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링 Slack 메세지 전송하기 (Incoming WebHooks 활용하여 슬랙봇 만들기)</title>
      <link>https://gengminy.tistory.com/51</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-02 17.20.13.png&quot; data-origin-width=&quot;1292&quot; data-origin-height=&quot;764&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5xfLP/btr1nCZkqzR/3RP3lad2qT4OLZL4yznoA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5xfLP/btr1nCZkqzR/3RP3lad2qT4OLZL4yznoA0/img.png&quot; data-alt=&quot;500 에러 발생 시 메세지 보내주는 슬랙봇&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5xfLP/btr1nCZkqzR/3RP3lad2qT4OLZL4yznoA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5xfLP%2Fbtr1nCZkqzR%2F3RP3lad2qT4OLZL4yznoA0%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;1292&quot; height=&quot;764&quot; data-filename=&quot;스크린샷 2023-03-02 17.20.13.png&quot; data-origin-width=&quot;1292&quot; data-origin-height=&quot;764&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;500 에러 발생 시 메세지 보내주는 슬랙봇&lt;/figcaption&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;배포 서버에서 실행 중일 때 발생한 에러는 바로 탐지가 어렵다.&lt;/p&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;Slack 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;&lt;b&gt;다만 이 파트에서는 에러 자동 탐지 알림 대신&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용자 편의를 의한 단순 메세지 발송에 대해서만 적었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;500 서버 에러 탐지 알림은 아래 다른 게시글에 올려두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/53&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://gengminy.tistory.com/53&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1678890880066&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;[Spring] 스프링 500 에러 발생 시 Slack 알림 전송하기 (슬랙 봇)&quot; data-og-description=&quot;지난 글에 이은, 본격적으로 500 내부 서버 에러 발생 시 슬랙으로 알림을 전송해주는 봇을 만들어보는 글이다. Slack API 를 활용하여 이러한 알림 서비스를 구현할 수 있다. ⚙️ Dependency implementati&quot; data-og-host=&quot;gengminy.tistory.com&quot; data-og-source-url=&quot;https://gengminy.tistory.com/53&quot; data-og-url=&quot;https://gengminy.tistory.com/53&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/yJPgD/hyRWt9TyGF/KCDmvoRFc4WQkSOBHK3ExK/img.png?width=800&amp;amp;height=506&amp;amp;face=0_0_800_506,https://scrap.kakaocdn.net/dn/cU8W1q/hyRWzvwX4a/5EN3SEha280TqZWKK7rJD1/img.png?width=800&amp;amp;height=506&amp;amp;face=0_0_800_506,https://scrap.kakaocdn.net/dn/biHBsK/hyRWnhzBQG/Wkop4rII7NbIRckAIznFb0/img.png?width=750&amp;amp;height=443&amp;amp;face=0_0_750_443&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/53&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gengminy.tistory.com/53&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/yJPgD/hyRWt9TyGF/KCDmvoRFc4WQkSOBHK3ExK/img.png?width=800&amp;amp;height=506&amp;amp;face=0_0_800_506,https://scrap.kakaocdn.net/dn/cU8W1q/hyRWzvwX4a/5EN3SEha280TqZWKK7rJD1/img.png?width=800&amp;amp;height=506&amp;amp;face=0_0_800_506,https://scrap.kakaocdn.net/dn/biHBsK/hyRWnhzBQG/Wkop4rII7NbIRckAIznFb0/img.png?width=750&amp;amp;height=443&amp;amp;face=0_0_750_443');&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;[Spring] 스프링 500 에러 발생 시 Slack 알림 전송하기 (슬랙 봇)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;지난 글에 이은, 본격적으로 500 내부 서버 에러 발생 시 슬랙으로 알림을 전송해주는 봇을 만들어보는 글이다. Slack API 를 활용하여 이러한 알림 서비스를 구현할 수 있다. ⚙️ Dependency implementati&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gengminy.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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⚙️ Dependency&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;implementation 'com.slack.api:slack-api-client:1.27.2'&lt;br /&gt;implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Slack API 사용을 위한 slack api client 추가&lt;/li&gt;
&lt;li&gt;StringUtils 를 사용하기 위한 apache 모듈 추가 (이건 반드시 필요한 것은 아님)&lt;/li&gt;
&lt;/ul&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;h2 data-ke-size=&quot;size26&quot;&gt;  Incoming Webhooks URL 발급받기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰을 가져와 Slack API Bean 을 만들어 등록 및 사용하는 방식도 있지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 Incoming Webhooks 라는 서드파티 앱을 추가한 후&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전용 URL 을 발급받아 메세지를 전송하는 방식을 택했다.&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;  Incoming Webhooks URL 발급받는 방법&lt;/h3&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 채널 세부정보 보기&lt;/b&gt;&lt;/h4&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-02 17.28.35.png&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r67MS/btr1IKuzSVE/UHNgxFC45edTG5NzJknRT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r67MS/btr1IKuzSVE/UHNgxFC45edTG5NzJknRT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r67MS/btr1IKuzSVE/UHNgxFC45edTG5NzJknRT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr67MS%2Fbtr1IKuzSVE%2FUHNgxFC45edTG5NzJknRT1%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;500&quot; height=&quot;164&quot; data-filename=&quot;스크린샷 2023-03-02 17.28.35.png&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;Slack 워크스페이스 생성 -&amp;gt; 채널 오른쪽 클릭 -&amp;gt; 채널 세부정보 보기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fafafa;&quot;&gt;&amp;nbsp;2. 앱 추가 들어가기&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-02 17.30.40.png&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QGx3l/btr1lAtXYdb/VAlLsKExh5ms5yPajwCbd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QGx3l/btr1lAtXYdb/VAlLsKExh5ms5yPajwCbd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QGx3l/btr1lAtXYdb/VAlLsKExh5ms5yPajwCbd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQGx3l%2Fbtr1lAtXYdb%2FVAlLsKExh5ms5yPajwCbd0%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;500&quot; height=&quot;403&quot; data-filename=&quot;스크린샷 2023-03-02 17.30.40.png&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;/figure&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. Incoming Webhooks 앱 추가&lt;/b&gt;&lt;/h4&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-02 17.31.45.png&quot; data-origin-width=&quot;1402&quot; data-origin-height=&quot;1204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZPueG/btr1udEHWWp/i8YAsB8xk3vlvZMupSnos0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZPueG/btr1udEHWWp/i8YAsB8xk3vlvZMupSnos0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZPueG/btr1udEHWWp/i8YAsB8xk3vlvZMupSnos0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZPueG%2Fbtr1udEHWWp%2Fi8YAsB8xk3vlvZMupSnos0%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;500&quot; height=&quot;429&quot; data-filename=&quot;스크린샷 2023-03-02 17.31.45.png&quot; data-origin-width=&quot;1402&quot; data-origin-height=&quot;1204&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;Incoming WebHooks 검색 -&amp;gt; 앱 디렉터리에서 -&amp;gt; 설치&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-02 17.32.47.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc5CC6/btr1CNemjra/HYTPVb6SlOfonUIFGbJlsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc5CC6/btr1CNemjra/HYTPVb6SlOfonUIFGbJlsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc5CC6/btr1CNemjra/HYTPVb6SlOfonUIFGbJlsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc5CC6%2Fbtr1CNemjra%2FHYTPVb6SlOfonUIFGbJlsK%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;500&quot; height=&quot;391&quot; data-filename=&quot;스크린샷 2023-03-02 17.32.47.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;Slack에 추가 클릭&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-02 17.33.20.png&quot; data-origin-width=&quot;1942&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmJSQU/btr1KQOCibI/b2kSndB7IUqpGqkElmKcpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmJSQU/btr1KQOCibI/b2kSndB7IUqpGqkElmKcpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmJSQU/btr1KQOCibI/b2kSndB7IUqpGqkElmKcpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmJSQU%2Fbtr1KQOCibI%2Fb2kSndB7IUqpGqkElmKcpK%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;500&quot; height=&quot;165&quot; data-filename=&quot;스크린샷 2023-03-02 17.33.20.png&quot; data-origin-width=&quot;1942&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;메세지를 받기 원하는 채널 선택 -&amp;gt; 수신 웹후크 통합 앱 추가 클릭&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-02 17.34.13.png&quot; data-origin-width=&quot;1956&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kDG8e/btr1KvRhA2G/DUdSAhxaFIxsgwi1qcVoY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kDG8e/btr1KvRhA2G/DUdSAhxaFIxsgwi1qcVoY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kDG8e/btr1KvRhA2G/DUdSAhxaFIxsgwi1qcVoY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkDG8e%2Fbtr1KvRhA2G%2FDUdSAhxaFIxsgwi1qcVoY0%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;500&quot; height=&quot;256&quot; data-filename=&quot;스크린샷 2023-03-02 17.34.13.png&quot; data-origin-width=&quot;1956&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 웹후크 URL 이 발급된다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 슬랙 URL 로 등록하고, 메세지를 보내면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Incoming Webhooks URL 작동 테스트 하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-02 17.36.18.png&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8vij5/btr1nvTleZ5/MiIhJpCaZCAO0UN7kfuTO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8vij5/btr1nvTleZ5/MiIhJpCaZCAO0UN7kfuTO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8vij5/btr1nvTleZ5/MiIhJpCaZCAO0UN7kfuTO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8vij5%2Fbtr1nvTleZ5%2FMiIhJpCaZCAO0UN7kfuTO0%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;500&quot; height=&quot;217&quot; data-filename=&quot;스크린샷 2023-03-02 17.36.18.png&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;752&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹훅 URL 발급받은 창에서 아래로 내리면&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;style3&quot;&gt;curl -X POST --data-urlencode &quot;payload={\&quot;channel\&quot;: \&quot;#webhook\&quot;, \&quot;username\&quot;: \&quot;webhookbot\&quot;, \&quot;text\&quot;: \&quot;이 항목은 #개의 webhook에 포스트되며 webhookbot이라는 봇에서 제공됩니다.\&quot;, \&quot;icon_emoji\&quot;: \&quot;:ghost:\&quot;}&quot; https://hooks.slack.com/services/{고유 문자열}&lt;/blockquote&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;스크린샷 2023-03-02 17.38.22.png&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DgE6R/btr1pWJ4sHK/grUE9gJ5YSnc0j2HWwyKOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DgE6R/btr1pWJ4sHK/grUE9gJ5YSnc0j2HWwyKOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DgE6R/btr1pWJ4sHK/grUE9gJ5YSnc0j2HWwyKOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDgE6R%2Fbtr1pWJ4sHK%2FgrUE9gJ5YSnc0j2HWwyKOk%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;500&quot; height=&quot;74&quot; data-filename=&quot;스크린샷 2023-03-02 17.38.22.png&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 있는 요청을 보냈을 때 응답으로 ok 가 온다면&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;스크린샷 2023-03-03 13.57.07.png&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXRx1h/btr1PNSkUy8/dzokXYG1bp4OlrTHdblxa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXRx1h/btr1PNSkUy8/dzokXYG1bp4OlrTHdblxa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXRx1h/btr1PNSkUy8/dzokXYG1bp4OlrTHdblxa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXRx1h%2Fbtr1PNSkUy8%2FdzokXYG1bp4OlrTHdblxa1%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;500&quot; height=&quot;73&quot; data-filename=&quot;스크린샷 2023-03-03 13.57.07.png&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;168&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Slack URL 등록 비즈니스 로직&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 채널 URL 발급받는 방법을 알았으니&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;  HostController&lt;/h3&gt;
&lt;pre id=&quot;code_1677746532078&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/v1/hosts&quot;)
@RequiredArgsConstructor
public class HostController {    
    private final UpdateHostSlackUrlUseCase updateHostSlackUrlUseCase;
    
    //... 생략

    @PatchMapping(&quot;/{hostId}/slack&quot;)
    public HostDetailResponse patchHostSlackUrlById(
            @PathVariable Long hostId,
            @RequestBody @Valid UpdateHostSlackRequest updateHostSlackRequest) {
        return updateHostSlackUrlUseCase.execute(hostId, updateHostSlackRequest);
    }
}&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;h3 data-ke-size=&quot;size23&quot;&gt;  UpdateHostSlackRequest&lt;/h3&gt;
&lt;pre id=&quot;code_1677746970835&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UpdateHostSlackRequest {
    @NotBlank(message = &quot;올바른 슬랙 URL 을 입력해주세요&quot;)
    @URL(message = &quot;올바른 슬랙 URL 을 입력해주세요&quot;)
    private String slackUrl;
}&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;단순하게 String 만 하나 가져오고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO 내부에서 Validation 한다.&lt;/p&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;  UpdateHostSlackUrlUseCase&lt;/h3&gt;
&lt;pre id=&quot;code_1677746593287&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@UseCase
@RequiredArgsConstructor
public class UpdateHostSlackUrlUseCase {
    private final HostService hostService;
    private final HostAdaptor hostAdaptor;
    private final HostMapper hostMapper;

    private final SlackMessageProvider slackMessageProvider;

    @Transactional
    @HostRolesAllowed(role = MANAGER, findHostFrom = HOST_ID)
    public HostDetailResponse execute(Long hostId, UpdateHostSlackRequest updateHostSlackRequest) {
        final Host host = hostAdaptor.findById(hostId);
        final String slackUrl = updateHostSlackRequest.getSlackUrl();
        hostService.validateDuplicatedSlackUrl(host, slackUrl);

        try {
            slackMessageProvider.register(slackUrl);
            return hostMapper.toHostDetailResponse(hostService.updateHostSlackUrl(host, slackUrl));
        } catch (UnknownHostException e) {
            throw InvalidSlackUrlException.EXCEPTION;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두둥 프로젝트에서는 Service 끼리의 참조에서 생기는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순환 참조와 복잡도 증가 등을 막기 위해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UseCase 라는 레이어를 하나 더 도입했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 UseCase 에서 종합적인 비즈니스 로직이 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SlackUrl 의 중복 주입을 검증하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레포지토리에서 Host 를 가져와 주입하는 로직이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;try catch 로 감싼 이유는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 내부에서 반환되는 UnknownHostException 을 깔끔하게 처리하기 위함이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환값은 Setter 호출 및 Response DTO 생성이니&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;  SlackMessageProvider&lt;/h3&gt;
&lt;pre id=&quot;code_1677819820607&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
@Slf4j
public class SlackMessageProvider {

    @Value(&quot;${slack.webhook.username}&quot;)
    private String username;

    @Value(&quot;${slack.webhook.icon-url}&quot;)
    private String iconUrl;

    /** 이벤트 핸들러 자체에서 비동기로 실행하기 때문에 @Async 어노테이션 지움 */
    public void sendMessage(String url, String text) {
        // 슬랙 url 이 null 일경우 안보냄.
        if (Objects.isNull(url)) return;
        try {
            doSend(url, text);
        } catch (Exception ignored) {
        }
    }

    /** 호스트가 존재하는 지 확인하기 위해 동기로 처리 */
    public void register(String url) throws UnknownHostException {
        final String text = &quot;두둥 슬랙 알림이 성공적으로 등록되었습니다!&quot;;
        doSend(url, text);
    }

    private void doSend(String url, String text) throws UnknownHostException {
        final Slack slack = Slack.getInstance();
        final Payload payload =
                Payload.builder().text(text).username(username).iconUrl(iconUrl).build();
        try {
            String responseBody = slack.send(url, payload).getBody();
            if (!StringUtils.equals(responseBody, &quot;ok&quot;)) {
                throw new UnknownHostException(&quot;올바른 슬랙 URL이 아닙니다.&quot;);
            }
        } catch (UnknownHostException error) {
            // 호스트가 존재하지 않을 경우 abort
            throw error;
        } catch (IOException e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException(e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 슬랙 API 를 본격적으로 사용하는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;doSend 라는 private method 에서는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 URL 에 텍스트를 보내는 로직을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 전에 해당 URL 에 Payload 를 담아 post 요청을 보내면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 값으로 ok 가 반환되는 것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청에 실패하면 슬랙 API 자체에서 UnknownErrorException 가 던져지는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 응용하여 &quot;ok&quot; 가 반환되면 통과시키고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않으면 UnknownErrorException 을 임의로 던져주도록 변형했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IOException 은 슬랙 메세지 전송 실패에 대해 처리하기 위해 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slack 인스턴스를 가져와서 Payload 에 원하는 값을 넣고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;slack.send() 메소드를 호출하면 메세지 전송이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 슬랙 알림 같은 경우는 @Async 를 통해 비동기로 처리했으나,&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;스크린샷 2023-03-03 14.14.53.png&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tfH26/btr1IrbHiHp/e2HWDlK2PVckunX665qXe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tfH26/btr1IrbHiHp/e2HWDlK2PVckunX665qXe0/img.png&quot; data-alt=&quot;이상한 url 을 입력하여 메세지 전송에 실패했을 때 오류&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tfH26/btr1IrbHiHp/e2HWDlK2PVckunX665qXe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtfH26%2Fbtr1IrbHiHp%2Fe2HWDlK2PVckunX665qXe0%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;500&quot; height=&quot;215&quot; data-filename=&quot;스크린샷 2023-03-03 14.14.53.png&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이상한 url 을 입력하여 메세지 전송에 실패했을 때 오류&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-03 14.15.45.png&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXAphR/btr1PNrmUDa/5S8RUtnc3ZIK96HZgDhFCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXAphR/btr1PNrmUDa/5S8RUtnc3ZIK96HZgDhFCk/img.png&quot; data-alt=&quot;슬랙 url 등록 성공 시 메세지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXAphR/btr1PNrmUDa/5S8RUtnc3ZIK96HZgDhFCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXAphR%2Fbtr1PNrmUDa%2F5S8RUtnc3ZIK96HZgDhFCk%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;500&quot; height=&quot;118&quot; data-filename=&quot;스크린샷 2023-03-03 14.15.45.png&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;186&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;슬랙 url 등록 성공 시 메세지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Slack 비동기 메세지 전송 비즈니스 로직&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트를 간편하게 구현하기 위해 &lt;b&gt;커스텀 객체들이 많으니 감안하고 봐주시길&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;  Host&lt;/h3&gt;
&lt;pre id=&quot;code_1677821092695&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
public class Host extends BaseTimeEntity {
	
    //...생략
    
    public void setSlackUrl(String slackUrl) {
        if (StringUtils.equals(this.slackUrl, slackUrl)) {
            throw DuplicateSlackUrlException.EXCEPTION;
        }
        Events.raise(HostRegisterSlackEvent.of(this));
        this.slackUrl = slackUrl;
    }
    
	//...생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Events 오브젝트는 ApplicationEventPublisher 를 감싸서 임의로 구현한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;publishEvent 의 의미라고 생각하면 되겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Host 내부에서 setter 를 호출할 때 호스트 등록 이벤트를 발생시켰다.&lt;/p&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;  HostRegisterSlackEvent&lt;/h3&gt;
&lt;pre id=&quot;code_1677821264437&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Builder
@ToString
public class HostRegisterSlackEvent extends DomainEvent {
    private final Long hostId;
    private final String hostName;

    public static HostRegisterSlackEvent of(Host host) {
        return HostRegisterSlackEvent.builder()
                .hostId(host.getId())
                .hostName(host.toHostProfileVo().getName())
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Events 에서 DomainEvent 라는 객체로 통합 처리하기 위해 이를 상속받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발행시킬 이벤트는 POJO 로 구성한다.&lt;/p&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;  DomainEvent&lt;/h3&gt;
&lt;pre id=&quot;code_1677821516468&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
public class DomainEvent {
    private final LocalDateTime publishAt;

    public DomainEvent() {
        this.publishAt = LocalDateTime.now();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DomainEvent 객체는 그냥 이렇게 생겼다.&lt;/p&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;  HostRegisterSlackEventHandler&lt;/h3&gt;
&lt;pre id=&quot;code_1677821347719&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
@Slf4j
public class HostRegisterSlackEventHandler {
    private final HostAdaptor hostAdaptor;
    private final SlackMessageProvider slackMessageProvider;

    @Async
    @TransactionalEventListener(
            classes = HostRegisterSlackEvent.class,
            phase = TransactionPhase.AFTER_COMMIT)
    public void handle(HostRegisterSlackEvent hostRegisterSlackEvent) {
        final Host host = hostAdaptor.findById(hostRegisterSlackEvent.getHostId());
        final String message = HostSlackAlarm.slackRegistrationOf(host);

        slackMessageProvider.sendMessage(host.getSlackUrl(), message);
    }
}&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;@Async 를 통해 비동기로 처리하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@TransactionalEventListener 의 phase 를 AFTER_COMMIT 으로 설정해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 종료되어 성공적으로 Host 엔티티에 값이 들어간 이후에&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;스크린샷 2023-03-03 14.15.45.png&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXAphR/btr1PNrmUDa/5S8RUtnc3ZIK96HZgDhFCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXAphR/btr1PNrmUDa/5S8RUtnc3ZIK96HZgDhFCk/img.png&quot; data-alt=&quot;슬랙 url 등록 성공 시 메세지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXAphR/btr1PNrmUDa/5S8RUtnc3ZIK96HZgDhFCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXAphR%2Fbtr1PNrmUDa%2F5S8RUtnc3ZIK96HZgDhFCk%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;500&quot; height=&quot;118&quot; data-filename=&quot;스크린샷 2023-03-03 14.15.45.png&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;186&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;슬랙 url 등록 성공 시 메세지&lt;/figcaption&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;이를 활용하여 각종 호스트 유저 가입, 티켓 판매 알림 등등&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;</description>
      <category>  프로젝트/  두둥</category>
      <category>slack</category>
      <category>SlackAPI</category>
      <category>Spring</category>
      <category>두둥</category>
      <category>스프링</category>
      <category>스프링부트</category>
      <category>슬랙</category>
      <category>슬랙봇</category>
      <category>슬랙알림</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/51</guid>
      <comments>https://gengminy.tistory.com/51#entry51comment</comments>
      <pubDate>Fri, 3 Mar 2023 14:35:24 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링 날짜 타입 JSON 변환 및 포맷팅하기 - @JsonFormat, @JacksonAnnotationsInside</title>
      <link>https://gengminy.tistory.com/50</link>
      <description>&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;개발을 하다보면&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;좋든 싫든 항상 날짜에 대한 포맷팅을 마주하게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서는 그 과정에서 공부했던,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;@JsonFormat 을 활용하여 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;LocalDateTime 등 날짜에 대한 JSON 직렬화하기와&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;또 날짜 포맷팅, Jackson 을 활용한 커스텀 어노테이션까지 적어보았다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;  LocalDateTime 형식&lt;/h2&gt;
&lt;pre id=&quot;code_1677424362979&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;hello&quot;,
  &quot;startAt&quot;: &quot;2023-02-26T15:12:17.536Z&quot;,
  &quot;endAt&quot;: &quot;2023-02-26T15:12:17.536Z&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LocalDateTime 같은 경우에는&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;pre id=&quot;code_1677424540663&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;name&quot;: &quot;hello&quot;,
    &quot;startAt&quot;: &quot;2023.03.20 12:00&quot;,
    &quot;endAt&quot;: &quot;2023.03.20 13:30&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답값에 포맷팅을 걸어서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 요청 및 응답하게 할 순 없을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  @JsonFormat&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jackson 에서 제공하는 @JsonFormat 을 사용하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Request 나 Response 필드의 날짜 형식을 JSON 으로 변환시킬 수 있다.&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;  MyRequest&lt;/h3&gt;
&lt;pre id=&quot;code_1677424658188&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@RequiredArgsConstructor
public class MyRequest {
    private String name;
    
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = &quot;yyyy.MM.dd HH:mm&quot;, timezone = &quot;Asia/Seoul&quot;)
    private LocalDateTime startAt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@JsonFormat 의 pattern 필드에 내가 원하는 날짜 형식을 입력하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1677424837379&quot; class=&quot;json&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;HI&quot;,
  &quot;startAt&quot;: &quot;2023.03.20 12:00&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Request DTO 필드에 @JsonFormat 을 쓸 경우&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;이런 식으로 하게 되면 Controller 에서 String 형식으로 가져오거나 해서&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;  BasicResponse&lt;/h3&gt;
&lt;pre id=&quot;code_1677424755556&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Builder
public class BasicResponse {
    private String name;
    
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = &quot;yyyy.MM.dd HH:mm&quot;, timezone = &quot;Asia/Seoul&quot;)
    private LocalDateTime startAt;
    
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = &quot;yyyy.MM.dd HH:mm&quot;, timezone = &quot;Asia/Seoul&quot;)
    private LocalDateTime endAt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Response DTO 도 마찬가지이다.&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;h2 data-ke-size=&quot;size26&quot;&gt;  @DateTimeFormat&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 지원하는 어노테이션인 @DateTimeFormat 도 있지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 Spring 의 기본 JSON 컨버터는 Jackson 이기 때문에 우선순위에서 밀린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 @DateTimeFormat 을 사용해야 하는 곳이 있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 직렬화를 하지 않는 Request Parameter 나 ModelAttribute 등에서는&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;JSON 은 @JsonFormat 을 사용하자!&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  커스텀 어노테이션 @DateFormat&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;119&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mrzck/btr01TyKUkO/fsr4m1lTRKkKbA7kcIckJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mrzck/btr01TyKUkO/fsr4m1lTRKkKbA7kcIckJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mrzck/btr01TyKUkO/fsr4m1lTRKkKbA7kcIckJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMrzck%2Fbtr01TyKUkO%2Ffsr4m1lTRKkKbA7kcIckJk%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;1230&quot; height=&quot;119&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;119&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;모든 필드에 이와 같이 붙여줘야 하는데,&lt;/p&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;파라미터가 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;  DateFormat&lt;/h3&gt;
&lt;pre id=&quot;code_1677426160427&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@JacksonAnnotationsInside
@Retention(RetentionPolicy.RUNTIME)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = &quot;yyyy.MM.dd HH:mm&quot;, timezone = &quot;Asia/Seoul&quot;)
public @interface DateFormat {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 어노테이션인 @DateFormat 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 목적은 @JsonFormat 을 전부 통일시키고&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;중요한 점은 @JacksonAnnotationsInside 를 반드시 붙여줘야 하는데,&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 어노테이션이 Jackson 관련 어노테이션을 상속하여 사용하겠다는 의미이다.&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;90&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvR94M/btr0RJXPwNO/Z0KNcaXoWCmbhgXXmgeLH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvR94M/btr0RJXPwNO/Z0KNcaXoWCmbhgXXmgeLH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvR94M/btr0RJXPwNO/Z0KNcaXoWCmbhgXXmgeLH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvR94M%2Fbtr0RJXPwNO%2FZ0KNcaXoWCmbhgXXmgeLH0%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;938&quot; height=&quot;90&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;90&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;가독성이 올라가게 된다!&lt;/p&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>  프로젝트/  두둥</category>
      <category>Date</category>
      <category>DateFormat</category>
      <category>Formatting</category>
      <category>Jackson</category>
      <category>JsonFormat</category>
      <category>LocalDateTime</category>
      <category>Spring</category>
      <category>날짜형식</category>
      <category>스프링</category>
      <category>스프링부트</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/50</guid>
      <comments>https://gengminy.tistory.com/50#entry50comment</comments>
      <pubDate>Mon, 27 Feb 2023 00:46:03 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링 Custom Enum Deserializer 구현으로 JSON Enum null 로 파싱하기</title>
      <link>https://gengminy.tistory.com/49</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/48&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://gengminy.tistory.com/48&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1676965189890&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;[Spring] 스프링 Enum Validator Reflection 으로 개선 및 구현하기&quot; data-og-description=&quot;https://gengminy.tistory.com/47 [Spring] 스프링에서 Enum 클래스 Validation 추가하기 (Enum JSON parse error 해결) 스프링에서 일반적으로 RequestBody 의 값을 Validation 하는 방법 스프링 MVC 에서 @Valid 어노테이션을 &quot; data-og-host=&quot;gengminy.tistory.com&quot; data-og-source-url=&quot;https://gengminy.tistory.com/48&quot; data-og-url=&quot;https://gengminy.tistory.com/48&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/nPeGt/hyRIBlOueg/nYB09kKw9RIyzwgKkEfiyk/img.png?width=800&amp;amp;height=136&amp;amp;face=0_0_800_136,https://scrap.kakaocdn.net/dn/7CIfb/hyRICkJbk8/Q2qsj94rggxPdq2tLfcysK/img.png?width=800&amp;amp;height=136&amp;amp;face=0_0_800_136,https://scrap.kakaocdn.net/dn/c9h8FK/hyRHxZEEUU/ZAkNlaSl02Vm1KCLTN5Bd0/img.png?width=2172&amp;amp;height=370&amp;amp;face=0_0_2172_370&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/48&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gengminy.tistory.com/48&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/nPeGt/hyRIBlOueg/nYB09kKw9RIyzwgKkEfiyk/img.png?width=800&amp;amp;height=136&amp;amp;face=0_0_800_136,https://scrap.kakaocdn.net/dn/7CIfb/hyRICkJbk8/Q2qsj94rggxPdq2tLfcysK/img.png?width=800&amp;amp;height=136&amp;amp;face=0_0_800_136,https://scrap.kakaocdn.net/dn/c9h8FK/hyRHxZEEUU/ZAkNlaSl02Vm1KCLTN5Bd0/img.png?width=2172&amp;amp;height=370&amp;amp;face=0_0_2172_370');&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;[Spring] 스프링 Enum Validator Reflection 으로 개선 및 구현하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://gengminy.tistory.com/47 [Spring] 스프링에서 Enum 클래스 Validation 추가하기 (Enum JSON parse error 해결) 스프링에서 일반적으로 RequestBody 의 값을 Validation 하는 방법 스프링 MVC 에서 @Valid 어노테이션을&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gengminy.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;앞서 스프링에서 Custom Enum Constraint Validator 를 Reflection 으로까지 구현했다.&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;pre id=&quot;code_1676965254150&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Enum Validation 을 위한 코드, enum 에 속하지 않으면 null 리턴
@JsonCreator
public static EventStatus fromEventStatus(String val) {
    return Arrays.stream(values())
            .filter(type -&amp;gt; type.getName().equals(val))
            .findAny()
            .orElse(null);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Request 에서 제공한 Enum 필드에 대해 역직렬화 해주는 @JsonCreator&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@JsonCreator 를 구현하지 않는다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;null 값을 대입하는 대신 JSON Parse Error 를 발생시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Enum Validation 을 위해선&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Enum 에 속하지 않는 Constant 에 대해서 null 을 반환하도록 할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Enum 마다 반복적으로 구현해야 해서 이를 줄이고 싶었다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  원하고자 하는 목표&lt;/h2&gt;
&lt;pre id=&quot;code_1676965467088&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@AllArgsConstructor
@EnumClass
public enum EventStatus {
    PREPARING(&quot;PREPARING&quot;, &quot;준비중&quot;),
    OPEN(&quot;OPEN&quot;, &quot;진행중&quot;),
    CALCULATING(&quot;CALCULATING&quot;, &quot;정산중&quot;),
    CLOSED(&quot;CLOSED&quot;, &quot;지난공연&quot;),
    DELETED(&quot;DELETED&quot;, &quot;삭제된공연&quot;);

    private final String name;
    @JsonValue private final String value;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 처럼 @EnumClass 라는 커스텀 어노테이션 하나로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@JsonCreator 에서 실행하는 로직을 축약하고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;%F-%-F%--%-D%--EventStatus&quot; data-ke-size=&quot;size23&quot;&gt;  EnumClass&lt;/h3&gt;
&lt;pre id=&quot;code_1676965511998&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonDeserialize(using = CustomEnumDeserializer.class)
public @interface EnumClass {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 어노테이션 @EnumClass 를 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@JsonDeserialize 라는 어노테이션을 통해 사용하고 싶은 Deserializer 를 지정할 수 있다.&lt;/p&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;Jackson 관련 어노테이션을 상속시킬 때&lt;/b&gt;는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반드시 &lt;b&gt;@JacksonAnnotationsInside&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;그렇지 않으면 이 어노테이션 자체에 Jackson 관련 코드가 적용되어서&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 id=&quot;%F-%-F%--%-D%--EventStatus&quot; data-ke-size=&quot;size23&quot;&gt;  CustomEnumDeserializer&lt;/h3&gt;
&lt;pre id=&quot;code_1676965635807&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CustomEnumDeserializer extends StdDeserializer&amp;lt;Enum&amp;lt;?&amp;gt;&amp;gt;
        implements ContextualDeserializer {

    public CustomEnumDeserializer() {
        this(null);
    }

    protected CustomEnumDeserializer(Class&amp;lt;?&amp;gt; vc) {
        super(vc);
    }

    @SuppressWarnings(&quot;unchecked&quot;)
    @Override
    public Enum&amp;lt;?&amp;gt; deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
        JsonNode jsonNode = jp.getCodec().readTree(jp);
        JsonNode nameNode = jsonNode.get(&quot;name&quot;);
        if (nameNode == null) return null;
        String text = jsonNode.asText();
        Class&amp;lt;? extends Enum&amp;gt; enumType = (Class&amp;lt;? extends Enum&amp;gt;) this._valueClass;
        return Arrays.stream(enumType.getEnumConstants())
                .filter(constant -&amp;gt; constant.name().equals(text))
                .findAny()
                .orElse(null);
    }

    @Override
    public JsonDeserializer&amp;lt;?&amp;gt; createContextual(DeserializationContext ctxt, BeanProperty property)
            throws JsonMappingException {
        return new CustomEnumDeserializer(property.getType().getRawClass());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Enum 인스턴스에 대해 적용하기 위해 제네릭으로 &lt;b&gt;Enum&amp;lt;?&amp;gt;&lt;/b&gt; 을 적용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;또 주목할만한 점은 &lt;b&gt;ContextualDeserializer&lt;/b&gt; 를 &lt;b&gt;implementation&lt;/b&gt; 하는 점인데&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;현재 Context 의 Target 클래스 타입을 통해 &lt;b&gt;Deserializer 를 재정의&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;b&gt;this._valueClass 호출 시 NPE&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;Enum 타입이 고정되어 있다면 직접 Status.class 같이 생성자에 넣어주면 되지만,&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;deserialize 구현부에서는 jsonNode 의 &quot;name&quot; 을 가져와&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Enum Constant 의 name 으로 비교를 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nameNode 로 굳이 한 번 더 나누어준 이유는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;asText() 메소드가 null 에 대해 수행할 수 있는 가능성이 있기 때문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-02-21 16.55.39.png&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cn9qc1/btrZ9HtZe7F/3kASkJxKNXMz3zn4UkXxr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cn9qc1/btrZ9HtZe7F/3kASkJxKNXMz3zn4UkXxr1/img.png&quot; data-alt=&quot;아주 깔끔해졌다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cn9qc1/btrZ9HtZe7F/3kASkJxKNXMz3zn4UkXxr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcn9qc1%2FbtrZ9HtZe7F%2F3kASkJxKNXMz3zn4UkXxr1%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;500&quot; height=&quot;318&quot; data-filename=&quot;스크린샷 2023-02-21 16.55.39.png&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;538&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아주 깔끔해졌다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-02-21 16.54.55.png&quot; data-origin-width=&quot;2172&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6sD3C/btr0eLPXuDm/5DpJxLJuPkxAAJpfGqMgE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6sD3C/btr0eLPXuDm/5DpJxLJuPkxAAJpfGqMgE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6sD3C/btr0eLPXuDm/5DpJxLJuPkxAAJpfGqMgE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6sD3C%2Fbtr0eLPXuDm%2F5DpJxLJuPkxAAJpfGqMgE1%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;2172&quot; height=&quot;378&quot; data-filename=&quot;스크린샷 2023-02-21 16.54.55.png&quot; data-origin-width=&quot;2172&quot; data-origin-height=&quot;378&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;없는 Enum Constant 에 대해 정상적으로 null 을 반환하고 Validation 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로써 Enum 관련 코드가 아주 깔끔해졌다!&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  레퍼런스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/0473330&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://d2.naver.com/helloworld/0473330&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 글이 많은 도움이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  구현 과정 관련 게시글&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Custom Enum Validator 구현하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;a href=&quot;https://gengminy.tistory.com/47&quot;&gt;https://gengminy.tistory.com/47&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Reflection 을 이용하여 Enum Validator 개선하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/48&quot;&gt;https://gengminy.tistory.com/48&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Custom Enum Deserializer 구현하여 Enum 에 없는 값 null 로 파싱하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/49&quot;&gt;https://gengminy.tistory.com/49&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  프로젝트/  두둥</category>
      <category>deserializer</category>
      <category>enum</category>
      <category>JSON</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>validation</category>
      <category>VALIDATOR</category>
      <category>스프링</category>
      <category>역직렬화</category>
      <category>열거형</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/49</guid>
      <comments>https://gengminy.tistory.com/49#entry49comment</comments>
      <pubDate>Tue, 21 Feb 2023 16:57:31 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링 Enum Validator Reflection 으로 개선 및 구현하기</title>
      <link>https://gengminy.tistory.com/48</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/47&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://gengminy.tistory.com/47&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1676954303475&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;[Spring] 스프링에서 Enum 클래스 Validation 추가하기 (Enum JSON parse error 해결)&quot; data-og-description=&quot;스프링에서 일반적으로 RequestBody 의 값을 Validation 하는 방법 스프링 MVC 에서 @Valid 어노테이션을 명시해주면 컨트롤러에서 해당 값에 대해 미리 Validation 해줘서 값이 들어오게 된다. JAVAX 에서 @Not&quot; data-og-host=&quot;gengminy.tistory.com&quot; data-og-source-url=&quot;https://gengminy.tistory.com/47&quot; data-og-url=&quot;https://gengminy.tistory.com/47&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/xOJ1j/hyRHjtskbR/IDjfDdsRB4FjjQpDQcJ8l1/img.png?width=800&amp;amp;height=169&amp;amp;face=0_0_800_169,https://scrap.kakaocdn.net/dn/jEJW7/hyRImvji1v/CVCQvXGnz66GkoUnh10u9K/img.png?width=800&amp;amp;height=169&amp;amp;face=0_0_800_169,https://scrap.kakaocdn.net/dn/qCzuu/hyRIqkaYq2/Bkrc3NGCuX98ivFboVDGkk/img.png?width=2188&amp;amp;height=666&amp;amp;face=0_0_2188_666&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/47&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gengminy.tistory.com/47&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/xOJ1j/hyRHjtskbR/IDjfDdsRB4FjjQpDQcJ8l1/img.png?width=800&amp;amp;height=169&amp;amp;face=0_0_800_169,https://scrap.kakaocdn.net/dn/jEJW7/hyRImvji1v/CVCQvXGnz66GkoUnh10u9K/img.png?width=800&amp;amp;height=169&amp;amp;face=0_0_800_169,https://scrap.kakaocdn.net/dn/qCzuu/hyRIqkaYq2/Bkrc3NGCuX98ivFboVDGkk/img.png?width=2188&amp;amp;height=666&amp;amp;face=0_0_2188_666');&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;[Spring] 스프링에서 Enum 클래스 Validation 추가하기 (Enum JSON parse error 해결)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;스프링에서 일반적으로 RequestBody 의 값을 Validation 하는 방법 스프링 MVC 에서 @Valid 어노테이션을 명시해주면 컨트롤러에서 해당 값에 대해 미리 Validation 해줘서 값이 들어오게 된다. JAVAX 에서 @Not&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gengminy.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;앞서 작성한 스프링에서 Custom Enum Constraint Validator 구현하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서 @Enum 이라는 커스텀 어노테이션을 만들었고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Request 필드에서 적용해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1676954376277&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Schema(defaultValue = &quot;OPEN&quot;, description = &quot;오픈 상태&quot;)
@Enum(target = EventStatus.class, message = &quot;올바른 값을 입력해주세요.&quot;)
private EventStatus status;&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;매번 검증하고자 하는 Enum 클래스를 위처럼 명시해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 보기 싫기 때문에 Reflection 을 통해 동적으로 Enum 클래스의 value 를 가져와 검증하려고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;리플렉션(Reflection) : 실행 중인 프로그램의 클래스, 메서드, 필드 등의 정보를 동적으로 조사하고 사용하는 기능&lt;br /&gt;클래스의 이름을 문자열로 받아와 그 클래스를 동적으로 로딩하거나, 인스턴스의 메소드나 필드를 호출하거나, 어떤 클래스가 어떤 인터페이스를 구현하고 있는지, 어떤 클래스가 어떤 클래스를 상속받았는지 등의 정보를 런타임에 조사할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  원하고자 하는 목표&lt;/h2&gt;
&lt;pre id=&quot;code_1676954590743&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Schema(defaultValue = &quot;OPEN&quot;, description = &quot;오픈 상태&quot;)
@Enum(message = &quot;올바른 값을 입력해주세요.&quot;)
private EventStatus status;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 처럼 @Enum 사용 시 조사할 Enum 클래스를 생략하고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;%F-%-F%--%-D%--EventStatus&quot; data-ke-size=&quot;size23&quot;&gt;  Enum&lt;/h3&gt;
&lt;pre id=&quot;code_1676954649269&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Constraint(validatedBy = {EnumValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Enum {
    String message() default &quot;Invalid Enum Value.&quot;;

    Class&amp;lt;?&amp;gt;[] groups() default {};

    Class&amp;lt;? extends Payload&amp;gt;[] payload() default {};
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;target 필드를 제외하여 재정의한다.&lt;/p&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 id=&quot;%F-%-F%--%-D%--EventStatus&quot; data-ke-size=&quot;size23&quot;&gt;  EnumValidator&lt;/h3&gt;
&lt;pre id=&quot;code_1676954680369&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class EnumValidator implements ConstraintValidator&amp;lt;Enum, java.lang.Enum&amp;gt; {
    @Override
    public boolean isValid(java.lang.Enum value, ConstraintValidatorContext context) {
        if (value == null) {
            return false; //null 값 허용 여부
        }

        Class&amp;lt;?&amp;gt; reflectionEnumClass = value.getDeclaringClass();
        return Arrays.asList(reflectionEnumClass.getEnumConstants()).contains(value);
    }
}&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;우선 해당 Enum 값이 null 일 경우를 검사한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 null 에 해당하는 값은 검증에 실패하도록 정책을 설정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 이 Enum 값이 null 이 아님이 보장되었기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;value.getDeclaringClass&lt;/b&gt; 를 통해 &lt;b&gt;이 value 가 속한 Enum 클래스 정보&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;이제 Enum Class 를 알아냈으니 이 Enum 에 속해있는 Constant 들을 가져와 비교하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교는 Collection 의 contains 메소드를 이용하여 축약시켰다.&lt;/p&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;이제 target 을 지정하지 않아도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적으로 Enum Class 정보를 가져와 Validation 할 수 있게 되었다!&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-02-21 14.04.03.png&quot; data-origin-width=&quot;2172&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clSh6e/btr0eLhzBs4/u4qez5LXj55x1yXKAsrhe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clSh6e/btr0eLhzBs4/u4qez5LXj55x1yXKAsrhe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clSh6e/btr0eLhzBs4/u4qez5LXj55x1yXKAsrhe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclSh6e%2Fbtr0eLhzBs4%2Fu4qez5LXj55x1yXKAsrhe1%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;2172&quot; height=&quot;370&quot; data-filename=&quot;스크린샷 2023-02-21 14.04.03.png&quot; data-origin-width=&quot;2172&quot; data-origin-height=&quot;370&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;h2 data-ke-size=&quot;size26&quot;&gt;  구현 과정 관련 게시글&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Custom Enum Validator 구현하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;a href=&quot;https://gengminy.tistory.com/47&quot;&gt;https://gengminy.tistory.com/47&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Reflection 을 이용하여 Enum Validator 개선하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/48&quot;&gt;https://gengminy.tistory.com/48&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Custom Enum Deserializer 구현하여 Enum 에 없는 값 null 로 파싱하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/49&quot;&gt;https://gengminy.tistory.com/49&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  프로젝트/  두둥</category>
      <category>enum</category>
      <category>Reflection</category>
      <category>Spring</category>
      <category>validation</category>
      <category>VALIDATOR</category>
      <category>리플렉션</category>
      <category>스프링</category>
      <category>스프링부트</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/48</guid>
      <comments>https://gengminy.tistory.com/48#entry48comment</comments>
      <pubDate>Tue, 21 Feb 2023 14:05:51 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링에서 Enum 클래스 Validation 구현하기 (Enum JSON parse error 해결)</title>
      <link>https://gengminy.tistory.com/47</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/veObx/btr0aUSTNxf/KyK0gA7F6gKnFtVqhQXkk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/veObx/btr0aUSTNxf/KyK0gA7F6gKnFtVqhQXkk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/veObx/btr0aUSTNxf/KyK0gA7F6gKnFtVqhQXkk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FveObx%2Fbtr0aUSTNxf%2FKyK0gA7F6gKnFtVqhQXkk0%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;500&quot; height=&quot;81&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;246&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;1120&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXHtDT/btrZ7aCE4xh/KkiBfmzgstXcKuBWKEWHz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXHtDT/btrZ7aCE4xh/KkiBfmzgstXcKuBWKEWHz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXHtDT/btrZ7aCE4xh/KkiBfmzgstXcKuBWKEWHz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXHtDT%2FbtrZ7aCE4xh%2FKkiBfmzgstXcKuBWKEWHz0%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;500&quot; height=&quot;88&quot; data-origin-width=&quot;1120&quot; data-origin-height=&quot;198&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;스프링에서 일반적으로 RequestBody 의 값을 Validation 하는 방법&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;스프링 MVC 에서 @Valid 어노테이션을 명시해주면&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;컨트롤러에서 해당 값에 대해 미리 Validation 해줘서 값이 들어오게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;JAVAX 에서 @NotBlank, @NotNull, @Positive 등등&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;원시 타입에 대해서는 여러 가지 기본 검증을 지원한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 Enum 클래스에 대한 Validation 은 기본으로 지원하지 않는다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그러면 어떤 식으로 처리해야 할까?&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;  Enum Validation 처리를 하지 않았을 경우&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-02-21 11.22.53.png&quot; data-origin-width=&quot;2186&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5ZTuR/btr0dkDPRaO/NRzwHM6flTezbKioXLZDek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5ZTuR/btr0dkDPRaO/NRzwHM6flTezbKioXLZDek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5ZTuR/btr0dkDPRaO/NRzwHM6flTezbKioXLZDek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5ZTuR%2Fbtr0dkDPRaO%2FNRzwHM6flTezbKioXLZDek%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;2186&quot; height=&quot;464&quot; data-filename=&quot;스크린샷 2023-02-21 11.22.53.png&quot; data-origin-width=&quot;2186&quot; data-origin-height=&quot;464&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Enum 필드에 대해 Validation 처리하지 않는다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Enum 클래스에 속하지 않은 값이 Request 필드에 포함되면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 값을 Enum 으로 올바르게 파싱하지 못하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 처럼 JSON parse error 가 응답으로 오게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 백엔드의 입장에서도 그렇고 500 에러와 더불어 이런 에러는 상당히 보기가 싫다&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;h2 data-ke-size=&quot;size26&quot;&gt;  Enum Validation 적용 과정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  EventStatus&lt;/h3&gt;
&lt;pre id=&quot;code_1676947541824&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@AllArgsConstructor
public enum EventStatus {
    // 준비중
    PREPARING(&quot;PREPARING&quot;, &quot;준비중&quot;),
    // 진행중
    OPEN(&quot;OPEN&quot;, &quot;진행중&quot;),
    // 정산중
    CALCULATING(&quot;CALCULATING&quot;, &quot;정산중&quot;),
    // 지난 공연
    CLOSED(&quot;CLOSED&quot;, &quot;지난공연&quot;),
    // 삭제된 공연
    DELETED(&quot;DELETED&quot;, &quot;삭제된공연&quot;);

    private final String name;
    @JsonValue private final String value;

    // Enum Validation 을 위한 코드, enum 에 속하지 않으면 null 리턴
    @JsonCreator
    public static EventStatus fromEventStatus(String val) {
        return Arrays.stream(values())
                .filter(type -&amp;gt; type.getName().equals(val))
                .findAny()
                .orElse(null);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Validation 을 사용하고 싶은 Enum Class 에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 @JsonCreator 어노테이션을 명시한 정적 메소드를 추가하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@JsonCreator 는 페이로드에서 JSON -&amp;gt; Enum 필드로 파싱하는 방법을 직접 명시해주게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fromEventStatus 메소드에서는 Enum Class 에 속하지 않는 값이 들어올 때 null 을 리턴하게 되서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON parsing error 가 발생하지 않도록 해줬다.&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baIL45/btrZ6KdfLpN/uyWkdF1EeKaVcoXCdybUfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baIL45/btrZ6KdfLpN/uyWkdF1EeKaVcoXCdybUfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baIL45/btrZ6KdfLpN/uyWkdF1EeKaVcoXCdybUfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaIL45%2FbtrZ6KdfLpN%2FuyWkdF1EeKaVcoXCdybUfk%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;2188&quot; height=&quot;666&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게만 해도 JSON parsing error 는 응답으로 오지 않지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘못된 Enum 값에 대한 예외 처리도 해주고 싶다.&lt;/p&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;  Enum&lt;/h3&gt;
&lt;pre id=&quot;code_1676947828575&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Constraint(validatedBy = {EnumValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Enum {
    String message() default &quot;Invalid Enum Value.&quot;;

    Class&amp;lt;?&amp;gt;[] groups() default {};

    Class&amp;lt;? extends Payload&amp;gt;[] payload() default {};
    
    Class&amp;lt;? extends java.lang.Enum&amp;lt;?&amp;gt;&amp;gt; target();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 어노테이션 인터페이스를 작성해준다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Constraint 는 Validation 하려는 메소드가 정의된 클래스&lt;/li&gt;
&lt;li&gt;@Target 은 이 어노테이션이 붙을 수 있는 범위&lt;/li&gt;
&lt;li&gt;@Retention 은 이 어노테이션의 생명주기를 지정해준다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Custom Constraint Annotation 을 지정할 때는 다음 3개를 꼭 정의해야 한다&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;message 는 예외 발생 시 응답 메세지 지정&lt;/li&gt;
&lt;li&gt;group 은 Validation 그룹 지정&lt;/li&gt;
&lt;li&gt;payload 는 추가 정보를 위해 지정할 수 있으며 주로 심각도를 나타내는 필드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 target 은 이 Validator 가 적용해줄 범위를 지정한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Class&amp;lt;? extends Enum&amp;gt; 은 java.lang.Enum 클래스를 상속받는 모든 인자라는 뜻을 제네릭으로 표현한 것이다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이 인자에는 Enum 클래스만 올 수 있다는 뜻이 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;target 필드는 Request 에 무조건 Enum 타입만 넣는다고 가정하면 없어도 되지만,&lt;br /&gt;다른 타입의 값을 Request 로 받아 Enum 으로 넣어주고 싶은 상황이 생길 수도 있으니&lt;br /&gt;범용성을 위해 추가하였다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  EnumValidator&lt;/h3&gt;
&lt;pre id=&quot;code_1676953259533&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class EnumValidator implements ConstraintValidator&amp;lt;Enum, java.lang.Enum&amp;gt; {
    private Enum annotation;

    @Override
    public void initialize(Enum constraintAnnotation) {
        this.annotation = constraintAnnotation;
    }

    @Override
    public boolean isValid(java.lang.Enum value, ConstraintValidatorContext context) {
        Object[] enumValues = this.annotation.target().getEnumConstants();
        if (enumValues != null) {
            for (Object enumValue : enumValues) {
                if (value.equals(enumValue.toString())) {
                    return true;
                }
            }
        }
        return false;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EnumValidator 에서는 Enum 의 value 값들을 가져오기 위해 멤버 변수로 지정하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;initailize 메소드를 오버라이딩 하여&lt;span&gt; 이 값을 할당한다.&lt;/span&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&gt;&lt;span&gt;이후 isValid 메소드를 오버라이딩 하여 검증 로직을 실행한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;target 클래스에서 Enum 값들을 가져와 검증하려는 value 와 비교하게 된다.&lt;/span&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&gt;&lt;span&gt;무조건 Enum 값이 들어온다고 가정하고 작성하면 아래와 같이 줄일 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1676953472086&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class EnumValidator implements ConstraintValidator&amp;lt;Enum, java.lang.Enum&amp;gt; {
    @Override
    public boolean isValid(java.lang.Enum value, ConstraintValidatorContext context) {
        return value != null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@JsonCreator 에서 해당 Enum 클래스에 없는 값이 들어오면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NULL 로 파싱해주기 때문에 이 로직을 적용할 수 있다&lt;/p&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;  UpdateEventStatusRequest&lt;/h3&gt;
&lt;pre id=&quot;code_1676953524886&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@RequiredArgsConstructor
public class UpdateEventStatusRequest {
    @Schema(defaultValue = &quot;OPEN&quot;, description = &quot;오픈 상태&quot;)
    @Enum(target = EventStatus.class, message = &quot;올바른 값을 입력해주세요.&quot;)
    private EventStatus status;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Request 에서 원하는 Enum 필드에 적용해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;target 에는 내가 검증하고자 하는 Enum 클래스를 지정하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2186&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AnrGK/btr0eTGochK/GqsXfgW2c9Mw6gWJiuTTC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AnrGK/btr0eTGochK/GqsXfgW2c9Mw6gWJiuTTC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AnrGK/btr0eTGochK/GqsXfgW2c9Mw6gWJiuTTC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAnrGK%2Fbtr0eTGochK%2FGqsXfgW2c9Mw6gWJiuTTC1%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;2186&quot; height=&quot;478&quot; data-origin-width=&quot;2186&quot; data-origin-height=&quot;478&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;&lt;b&gt;@RestControllerAdvice&lt;/b&gt; 에서 &lt;b&gt;MethodArgumentNotValidException&lt;/b&gt; 핸들링을 해줘&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적절하게 에러 메세지를 가공하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-02-21 13.26.17.png&quot; data-origin-width=&quot;2174&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkkxEi/btr0dmB46WM/EUbgUhXluWxpkLSN67RkAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkkxEi/btr0dmB46WM/EUbgUhXluWxpkLSN67RkAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkkxEi/btr0dmB46WM/EUbgUhXluWxpkLSN67RkAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkkxEi%2Fbtr0dmB46WM%2FEUbgUhXluWxpkLSN67RkAK%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;2174&quot; height=&quot;376&quot; data-filename=&quot;스크린샷 2023-02-21 13.26.17.png&quot; data-origin-width=&quot;2174&quot; data-origin-height=&quot;376&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  구현 과정 관련 게시글&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Custom Enum Validator 구현하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;a href=&quot;https://gengminy.tistory.com/47&quot;&gt;https://gengminy.tistory.com/47&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Reflection 을 이용하여 Enum Validator 개선하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/48&quot;&gt;https://gengminy.tistory.com/48&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Custom Enum Deserializer 구현하여 Enum 에 없는 값 null 로 파싱하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/49&quot;&gt;https://gengminy.tistory.com/49&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  프로젝트/  두둥</category>
      <category>enum</category>
      <category>JSON</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>validation</category>
      <category>VALIDATOR</category>
      <category>스프링</category>
      <category>스프링MVC</category>
      <category>스프링부트</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/47</guid>
      <comments>https://gengminy.tistory.com/47#entry47comment</comments>
      <pubDate>Tue, 21 Feb 2023 13:37:08 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] RefreshToken + Redis 사용해서 자동 로그인 + 로그아웃 구현하기</title>
      <link>https://gengminy.tistory.com/46</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;512x512_app_ico.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/soey2/btrRaElO3VK/cDtSYBGNXpcltsKYXEJ7g1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/soey2/btrRaElO3VK/cDtSYBGNXpcltsKYXEJ7g1/img.png&quot; data-alt=&quot;현재 진행중인 프로젝트 내친소 캐릭터입니다 귀엽죠&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/soey2/btrRaElO3VK/cDtSYBGNXpcltsKYXEJ7g1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsoey2%2FbtrRaElO3VK%2FcDtSYBGNXpcltsKYXEJ7g1%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;400&quot; data-filename=&quot;512x512_app_ico.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;현재 진행중인 프로젝트 내친소 캐릭터입니다 귀엽죠&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모바일 어플리케이션 또는 일부 웹 앱을 사용하다 보면 보통 자동 로그인 체크 옵션이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 이런 옵션이 없어도 앱을 키면 이미 로그인 되어있는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오톡, 토스 등등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과연 어떻게 구현하는 것일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 방식과 토큰 방식에서 물론 차이가 있지만&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;span style=&quot;color: #ee2323;&quot;&gt;&lt;u&gt;&lt;b&gt;이 포스팅은 Spring Security, JWT, Redis 관련 세팅이&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;u&gt;&lt;b&gt;이미 되어있다고 가정하고 코드를 작성했습니다.&lt;/b&gt;&lt;/u&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;  자동 로그인 인증 과정&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1) 유저가 성공적으로 로그인 했을 경우 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리프레시 토큰과 엑세스 토큰을 응답받는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2) 유저는 이를 보관하고 사용하다가, 엑세스 토큰이 만료되는 시점이 올 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3) 유저의 서버 API 요청시 401 응답을 받으면 reissue 요청을 시도해야 한다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4) 로그인 시 응답받았던 리프레시 토큰과 만료된 엑세스 토큰을 통해 재발급 요청을 서버에 보낸다.&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;5) 유효한 토큰일 경우 새로운 엑세스 토큰과 새로운 리프레시 토큰을 응답받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;  구현&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  JwtTokenProvider&lt;/h3&gt;
&lt;pre id=&quot;code_1668424468575&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
	private static final long JWT_EXPIRATION_MS = 1000L * 60 * 40; //40분
	private static final long REFRESH_TOKEN_EXPIRATION_MS = 1000L * 60 * 60 * 24 * 7; //7일


	... 생략


    public String generateRefreshToken(JwtDTO jwtDTO) {
        final String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes());
        final Date now = new Date();
        final Date refreshTokenExpiresIn = new Date(now.getTime() + REFRESH_TOKEN_EXPIRATION_MS);

        final String refreshToken = Jwts.builder()
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
                .setIssuer(&quot;naechinso&quot;)
                .setExpiration(refreshTokenExpiresIn)
                .signWith(SignatureAlgorithm.HS512, encodedKey)
                .compact();

        //redis에 해당 phone number 의 리프레시 토큰 등록
        redisService.setValues(
                jwtDTO.getPhoneNumber(),
                refreshToken,
                Duration.ofMillis(REFRESH_TOKEN_EXPIRATION_MS)
        );

        return refreshToken;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 TokenProvider에서 레지스터 토큰을 만드는 로직이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엑세스 토큰의(JWT) 만료 시간은 40분,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리프레시 토큰의 만료 시간은 7일로 잡아놨다.&lt;/p&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;리프레시 토큰은 브라우저의 쿠키나 LocalStorage 에 저장하기도 하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저 DB에 저장하기도 하고 여러 가지 방법이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;리프레시 토큰도 같이 Redis 에 저장하는 방법을 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 유저의 ID를 통해 해당 리프레시 토큰에 접근할 수 있다.&lt;/p&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; MemberController&lt;/h3&gt;
&lt;pre id=&quot;code_1668424323979&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@RestController
@RequestMapping(&quot;/member&quot;)
@RequiredArgsConstructor
public class MemberController {    
    private final MemberService memberService;
    private final JwtTokenProvider jwtTokenService;

    @PostMapping(&quot;/reissue&quot;)
    @ApiOperation(value = &quot;리프레시 토큰을 통해 새로운 토큰을 발급받는다 (RefreshToken)&quot;)
    public CommonApiResponse&amp;lt;MemberReissueResponseDTO&amp;gt; reissue(
            @RequestHeader(&quot;Authorization&quot;) String accessToken,
            @RequestHeader(&quot;Refresh&quot;) String refreshToken
    ) {
        return CommonApiResponse.of(memberService.reissue(accessToken, refreshToken));
    }
    
    ... 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러에는 reissue 라는 API를 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 현재 유효한 로그인 상태를 가지고 있을 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;accessToken 과 refreshToken 을 모두 가지고 있을 것이다.&lt;/p&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;&quot;Refresh&quot;&lt;/b&gt; 라는 헤더를 통해 리프레시 토큰 값을 가져오도록 프론트와 말했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 &lt;b&gt;&quot;Authorization&quot;&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;h3 data-ke-size=&quot;size23&quot;&gt;  MemberService&lt;/h3&gt;
&lt;pre id=&quot;code_1668424894244&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
@Transactional
public class MemberService {
    private final JwtTokenProvider jwtTokenProvider;
    private final MemberRepository memberRepository;
    
    
    (...생략)
    
    
    public MemberReissueResponseDTO reissue(String accessToken, String refreshToken) {
        String phone;

        if (!jwtTokenProvider.validateTokenExceptExpiration(accessToken)){
            throw new BadRequestException(ErrorCode.INVALID_ACCESS_TOKEN);
        }

        try {
            phone = jwtTokenProvider.parseClaims(accessToken).getSubject();
        } catch (Exception e) {
            throw new BadRequestException(ErrorCode.INVALID_REFRESH_TOKEN);
        }

        Member authMember = findByPhone(phone);

   		(...생략)


        jwtTokenProvider.validateRefreshToken(phone, refreshToken);

        TokenResponseDTO tokenResponseDTO = jwtTokenProvider.generateToken(new JwtDTO(phone, authMember.getRole().toString()));

        return MemberReissueResponseDTO.builder()
                .accessToken(tokenResponseDTO.getAccessToken())
                .refreshToken(tokenResponseDTO.getRefreshToken())
                (...생략)
                .build();
    }
}&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;처음에 jwtTokenProvider 에서 다시 토큰 검증이 발생하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰의 유효성 중 만료 시간만 검증하는 validateTokenExceptExpiration 메소드를 만들었다.&lt;/p&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_1668425267549&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//jwtTokenProvider
...
	/**
     * 토큰 예외 중 만료 상황만 검증 함수
     * @param token 검사하려는 JWT 토큰
     * @returns boolean
     * */
    public boolean validateTokenExceptExpiration(String token) {
        final String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes());
        try {
            Jwts.parser().setSigningKey(encodedKey).parseClaimsJws(token);
            return true;
        } catch(ExpiredJwtException e) {
            return true;
        } catch (Exception e) {
            return false;
        }
    }
   
...&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;토큰이 정상이거나 토큰 만료 시 던져주는 ExpiredJwtException 만 catch 해서 true 로 처리해준다.&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;이후 parseClaims 를 호출하면 엑세스 토큰으로 부터&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저의 ID (이 프로젝트에서는 phone) 를 뽑아낼 수 있다.&lt;/p&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_1668425464549&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; //JwtTokenProvider
 ...
 	/** Redis Memory 의 RefreshToken 과
     * User 의 RefreshToken 이 일치하는지 확인
     * @param phone 검증하려는 유저 휴대전화
     * @param refreshToken 검증하려는 리프레시 토큰
     */
    public void validateRefreshToken(String phone, String refreshToken) {
        String redisRefreshToken = redisService.getValues(phone);
        if (!refreshToken.equals(redisRefreshToken)) {
            throw new BadRequestException(ErrorCode.EXPIRED_REFRESH_TOKEN);
        }
    }
...&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;lt;Key, Value&amp;gt; 값으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;Phone, RefreshToken&amp;gt; 쌍을 Redis 에 삽입했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추출할 때는 phone 을 참조하여 RefreshToken 값을 가져올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 요청한 리프레시 토큰과 Redis 의 값을 비교해서 일치하면 검증 성공&lt;/p&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;h3 data-ke-size=&quot;size23&quot;&gt;  MemberReissueResponseDTO&lt;/h3&gt;
&lt;pre id=&quot;code_1668425608326&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 토큰 재발급을 담당하는 응답 DTO
 * */
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@ToString
@JsonInclude(JsonInclude.Include.NON_NULL) //NULL 필드 가림
public class MemberReissueResponseDTO {
    private String accessToken;
    private String refreshToken;
    
    (생략)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 DTO는 별 거 없이 accessToken 과 refreshToken 필드만 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;967&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLorWB/btrRddgsaZv/1YsHXNDuVVo6nYihd7P3qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLorWB/btrRddgsaZv/1YsHXNDuVVo6nYihd7P3qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLorWB/btrRddgsaZv/1YsHXNDuVVo6nYihd7P3qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLorWB%2FbtrRddgsaZv%2F1YsHXNDuVVo6nYihd7P3qk%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;1152&quot; height=&quot;967&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;967&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@RequestHeader 사용했기 때문에 스웨거에서 테스트도 가능하다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;  로그아웃은?&lt;/span&gt;&lt;/h2&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;pre id=&quot;code_1668425906041&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//MemberService
	(...생략)
    
    /**
     * 로그아웃 -&amp;gt; Register Token 삭제
     * */
    public MemberLoginResponseDTO logout(Member authMember) {
        Member member = findByMember(authMember);

        //redis 에서 registerToken 삭제
        jwtTokenProvider.deleteRegisterToken(member.getPhone());
        
        return new MemberLoginResponseDTO(&quot;&quot;);
    }&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;Redis 에 저장되어 있는 리프레시 토큰과 비교한다고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 Redis 에 저장된 리프레시 토큰을 없애주면 될 것이다.&lt;/p&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;pre id=&quot;code_1668426032542&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//JwtTokenProvider

	(...생략)

    /** Redis 에서 RegistrerToken 을 제거
     * @param phone 로그아웃 요청 유저
     * @return true if redis 서버에 토큰이 있었을 경우
     * false if 토큰이 없었을 경우
     */
    public boolean deleteRegisterToken(String phone) {
        try {
            if (redisService.hasKey(phone)) {
                redisService.deleteValues(phone);
                return true;
            }
        } catch (Exception e) {
            log.error(&quot;Redis 로그아웃 요청을 실패했습니다&quot;);
        }
        return false;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 에서 유저 키 값을 참고해 삭제한다.&lt;/p&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;</description>
      <category>  프로젝트/  내친소</category>
      <category>JWT</category>
      <category>RefreshToken</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>리프레시토큰</category>
      <category>스프링</category>
      <category>자동로그인</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/46</guid>
      <comments>https://gengminy.tistory.com/46#entry46comment</comments>
      <pubDate>Mon, 14 Nov 2022 20:41:51 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링 스케쥴러 사용해서 일정 주기로 메소드 실행하기 (Scheduler)</title>
      <link>https://gengminy.tistory.com/45</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 개발 중&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;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL 에서는 내장 스케쥴러가 없다는 말도 있고&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;h2 data-ke-size=&quot;size26&quot;&gt;⚙️ Dependency&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;@Scheduler 는 Spring Boot Starter 에서 기본으로 제공하는 어노테이션이다&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;nbsp;전역 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  MyApplication&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@EnableScheduling //scheduler 사용
@EnableJpaAuditing //jpa entity 자동 감시
@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 Application 의 main 메소드가 들어있는 클래스에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@EnableScheduling&lt;/code&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;h2 data-ke-size=&quot;size26&quot;&gt;  사용법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케쥴링을 원하는 메소드에 @Scheduled 어노테이션을 추가한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 스케쥴링이 가능한 메소드는 다음과 같은 조건이 있다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;반환형이 void 이어야함&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;매개변수가 없는 메소드&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  fixedDelay&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 메소드가 끝나는 시점을 기준으로 ms 단위로 실행&lt;/p&gt;
&lt;pre id=&quot;code_1667650887915&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Scheduled(fixedDelay = 1000)
protected void scheduleTask() {
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 1초마다 실행하는 스케쥴링 메소드&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;  fixedRate&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 메소드가 시작하는 시점을 기준으로 ms 단위로 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬로 실행을 원한다면&lt;b&gt; @Async&lt;/b&gt; 어노테이션 가능&lt;/p&gt;
&lt;pre id=&quot;code_1667650967325&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Async
@Scheduled(fixedRate = 1000)
protected void scheduleTask() {
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  cron&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;cron 표현식은 다음 사이트에서 도움을 받을 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단 스프링 스케쥴러에서는 6자리 표기식을 사용하기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7번째 글자를 지우고 맞는지 확인해보아야 함&lt;/p&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;http://www.cronmaker.com/?0&quot;&gt;http://www.cronmaker.com/?0&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1667651053524&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;CronMaker&quot; data-og-description=&quot;&quot; data-og-host=&quot;www.cronmaker.com&quot; data-og-source-url=&quot;http://www.cronmaker.com/?0&quot; data-og-url=&quot;http://www.cronmaker.com/;jsessionid=node086govosysuiydvpjmw6w5vi945113.node0?0&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;http://www.cronmaker.com/?0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;http://www.cronmaker.com/?0&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;CronMaker&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.cronmaker.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-11-05 21.32.14.png&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;1374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PjUwx/btrQrGKMWVP/VKGU6WlY3MFJ2iBQOplK51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PjUwx/btrQrGKMWVP/VKGU6WlY3MFJ2iBQOplK51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PjUwx/btrQrGKMWVP/VKGU6WlY3MFJ2iBQOplK51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPjUwx%2FbtrQrGKMWVP%2FVKGU6WlY3MFJ2iBQOplK51%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;500&quot; height=&quot;1374&quot; data-filename=&quot;스크린샷 2022-11-05 21.32.14.png&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;1374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식로 원하는 정보를 기입하면 Cron 표현식을 만들어준다&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-11-05 21.32.32.png&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;830&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwF38Y/btrQqME2lRG/KKBj3gqlNoTZDHHSpQyzi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwF38Y/btrQqME2lRG/KKBj3gqlNoTZDHHSpQyzi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwF38Y/btrQqME2lRG/KKBj3gqlNoTZDHHSpQyzi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwF38Y%2FbtrQqME2lRG%2FKKBj3gqlNoTZDHHSpQyzi1%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;500&quot; height=&quot;830&quot; data-filename=&quot;스크린샷 2022-11-05 21.32.32.png&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;830&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 적은 Cron 표현식에 따른 다음 스케쥴링도 알려주기 때문에&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_1667651092798&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Scheduled(cron = &quot;0 0 0 * * ?&quot;) //daily at 00:00
protected void scheduleTask() {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 위 식은 매일 00시 자정마다 아래 메소드를 실행하는 어노테이션이다&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 내가 원하는 것은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매일 00시 자정마다 생성한지 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;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;pre id=&quot;code_1667651126042&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 매일 00시00분 마다 기간이 지난 카드 자동 만료
 */
@Scheduled(cron = &quot;0 0 0 * * ?&quot;) //daily at 00:00
protected void scheduleTask() {
    //현재 시간으로 부터 3일 전의 00시 00분 이전 시각
    LocalDateTime compareDateTime = LocalDateTime.of(LocalDate.now().minusDays(EXPIRY_DATE_DAY), LocalTime.of(0,0,0));
    //그 이전의 카드는 만료되었으므로 모두 가져옴
    List&amp;lt;Match&amp;gt; matchList = matchRepository.findAllByIsExpiredFalseAndCreatedAtBefore(compareDateTime);
    //만료
    matchList.forEach(Match::expire);
    log.info(&quot;Scheduling System Automatically Expire Matches :: Affected Matches Count - {}&quot;, matchList.size());
    matchRepository.saveAll(matchList);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성 시간의 비교는 JpaRepository 의 표기 방법 중 CreatedAtBefore 을 통해 비교했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만료는 boolean 형의 isExpired 필드를 가져와서 비교했다&lt;/p&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;expire 메소드를 통해 isExpired 를 true 로 변경했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 비즈니스 로직에서는 isExpired 값을 비교하여 수행하도록 바꿨다&lt;/p&gt;</description>
      <category>  프로젝트/  내친소</category>
      <category>cron</category>
      <category>Scheduled</category>
      <category>Scheduling</category>
      <category>Spring</category>
      <category>스케쥴링</category>
      <category>스프링</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/45</guid>
      <comments>https://gengminy.tistory.com/45#entry45comment</comments>
      <pubDate>Sat, 5 Nov 2022 21:30:43 +0900</pubDate>
    </item>
    <item>
      <title>[Spring/Nginx] MultipartFile 최대 용량 설정하기 (MaxUploadSizeExceededException / Request Entity Too Large)</title>
      <link>https://gengminy.tistory.com/44</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;❌ 에러 상황&lt;/h2&gt;
&lt;pre id=&quot;code_1667276179634&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.springframework.web.multipart.MaxUploadSizeExceededException:
Maximum upload size exceeded;
nested exception is java.lang.IllegalStateException:
org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException:
The field multipartFiles exceeds its maximum permitted size of 1048576 bytes.&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;AWS S3 파일 업로드 중 발생한 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MaxUploadSizeExceededException&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;AWS S3 와 연동해서 파일을 업로드 할 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http 413 이 뜨면서 허용된 파일 용량을 초과했다고 하는 에러이다&lt;/p&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;스프링의 내장 톰캣 서버에서 초기 설정 값은&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;1048576 bytes =&amp;gt; 1MB 이다&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;h2 data-ke-size=&quot;size26&quot;&gt;  Trouble Shooting 1&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 설정 파일에서 최대 허용 용량을 설정해주면 된다&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;  application.yml&lt;/h3&gt;
&lt;pre id=&quot;code_1667276370682&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  servlet:
    multipart:
      #file upload size
      max-file-size: 3MB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring.servlet.multipart.max-file-size: {원하는 크기} 로 설정해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 서버에서는 3MB 를 최대 크기로 하도록 설정했다&lt;/p&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;  GlobalExceptionHandler.java&lt;/h3&gt;
&lt;pre id=&quot;code_1667276471683&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    /**
     * 업로드 최대 용량을 초과했을 경우
     */
    @ExceptionHandler({MaxUploadSizeExceededException.class})
    protected ResponseEntity&amp;lt;ErrorResponse&amp;gt; handleMultipartException(MaxUploadSizeExceededException e) {
        log.error(&quot;handleMaxUploadSizeExceededException&quot;, e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.MAX_FILE_SIZE_EXCEEDED);
        return new ResponseEntity&amp;lt;&amp;gt;(response, HttpStatus.BAD_REQUEST);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보너스로 글로벌 에러 핸들러에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 예외를 처리해주는 코드도 작성했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 413 에러가 아닌 400 에러로 처리된다&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;❌ 또 다른 에러&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-11-01 13.03.38.png&quot; data-origin-width=&quot;2092&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LS2MX/btrP4waY4wb/4i0aQJhr3ZgDW3Pqtshph1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LS2MX/btrP4waY4wb/4i0aQJhr3ZgDW3Pqtshph1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LS2MX/btrP4waY4wb/4i0aQJhr3ZgDW3Pqtshph1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLS2MX%2FbtrP4waY4wb%2F4i0aQJhr3ZgDW3Pqtshph1%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;2092&quot; height=&quot;832&quot; data-filename=&quot;스크린샷 2022-11-01 13.03.38.png&quot; data-origin-width=&quot;2092&quot; data-origin-height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 json 예외 메세지가 아니라 html 응답이 뜨면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여전히 http 413 에러가 발생한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 우리 서버에서 사용하고 있는 Nginx 에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 최대 body size 를 제한하고 있기 때문이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 Nginx 설정 파일도 바꿔줘야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Trouble Shooting 2&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  nginx.conf&lt;/h3&gt;
&lt;pre id=&quot;code_1667276685014&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http {
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;

  client_max_body_size  4M;
  ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx 설정 파일에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client_max_body_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;spring 내장 tomcat 에서 3MB 를 최대로 하기로 했으니까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx 에서는 넉넉하게 4MB 로 설정해주었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시 Nginx 에서도 초기 값은 1MB 이다&lt;/p&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-11-01 13.27.12.png&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQeoLp/btrP3MeitYn/MFzJKHndo6JqhlZKTKShD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQeoLp/btrP3MeitYn/MFzJKHndo6JqhlZKTKShD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQeoLp/btrP3MeitYn/MFzJKHndo6JqhlZKTKShD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQeoLp%2FbtrP3MeitYn%2FMFzJKHndo6JqhlZKTKShD1%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;1170&quot; height=&quot;354&quot; data-filename=&quot;스크린샷 2022-11-01 13.27.12.png&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;354&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;로컬이랑 AWS 개발 서버에서 모두 동작하는 것을 확인했따&lt;/p&gt;</description>
      <category>  프로젝트/  내친소</category>
      <category>AWS</category>
      <category>nginx</category>
      <category>S3</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>Troubleshooting</category>
      <category>스프링</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/44</guid>
      <comments>https://gengminy.tistory.com/44#entry44comment</comments>
      <pubDate>Tue, 1 Nov 2022 13:28:35 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] JPA + Lombok 사용할 때 @OneToOne 에서 발생하는 StackOverflowError 해결</title>
      <link>https://gengminy.tistory.com/43</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 와 Lombok 을 같이 사용중이라면 발생할 수 있는 에러&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;❌ 원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@OneToOne 또는 @OneToMany 를 통해 연관관계를 정의했고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 데이터를 꺼내올 때 hashCode 또는 toString 을 호출하면서&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_1664009076529&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java.lang.StackOverflowError: null
	at com.tikitaka.naechinso.domain.member.entity.Member.toString(Member.java:21) ~[main/:na]
	at java.base/java.lang.StringConcatHelper.stringOf(StringConcatHelper.java:453) ~[na:na]
	at com.tikitaka.naechinso.domain.member.entity.MemberDetail.toString(MemberDetail.java:18) ~[main/:na]
	at java.base/java.lang.StringConcatHelper.stringOf(StringConcatHelper.java:453) ~[na:na]
	at com.tikitaka.naechinso.domain.member.entity.Member.toString(Member.java:21) ~[main/:na]
	at java.base/java.lang.StringConcatHelper.stringOf(StringConcatHelper.java:453) ~[na:na]
    ...&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;pre id=&quot;code_1664009202437&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Caused by: java.lang.StackOverflowError: null
	at com.tikitaka.naechinso.domain.member.entity.Member.hashCode(Member.java:22) ~[main/:na]
	at com.tikitaka.naechinso.domain.member.entity.MemberDetail.hashCode(MemberDetail.java:19) ~[main/:na]
	at com.tikitaka.naechinso.domain.member.entity.Member.hashCode(Member.java:22) ~[main/:na]
	at com.tikitaka.naechinso.domain.member.entity.MemberDetail.hashCode(MemberDetail.java:19) ~[main/:na]
	at com.tikitaka.naechinso.domain.member.entity.Member.hashCode(Member.java:22) ~[main/:na]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Lombok 어노테이션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EqualsAndHashCode 는 equals 와 hashCode 를 자동 생성해준다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;eqauls : 두 객체의 내용이 같은지 비교 (동일성)&lt;/li&gt;
&lt;li&gt;hashCode : 두 객체가 같은 객체인지 존재를 비교 (동등성)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ToString 은 toString 을 자동 생성하여 멤버 데이터를 문자열로 만들어준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Data는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를&lt;span&gt;&amp;nbsp;&lt;/span&gt;한번에 정의해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  엔티티 정의&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Member.java&lt;/h3&gt;
&lt;pre id=&quot;code_1664009320960&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;member&quot;)
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
@EqualsAndHashCode
public class Member extends BaseEntity {

    @Id
    @Column(name = &quot;mem_id&quot;)
    @GeneratedValue
    private Long id;

    @OneToOne(mappedBy = &quot;member&quot;)
    @JoinColumn(name = &quot;mem_detail&quot;)
    private MemberDetail detail;
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  MemberDetail.java&lt;/h3&gt;
&lt;pre id=&quot;code_1664009387466&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;member_detail&quot;)
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
@EqualsAndHashCode
public class MemberDetail extends BaseEntity {

    @Id
    @Column(name = &quot;mem_id&quot;)
    private Long id;

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @MapsId
    @JoinColumn(name = &quot;mem_id&quot;)
    private Member member;
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Member 와 멤버의 디테일을 저장하는 MemberDetail 을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1대1 연관관계를 맺었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberRepository.findAll 을 통해 Member 정보를 가져올 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hashCode 를 무한으로 호출하면서 순환 참조에 빠졌다&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  해결 방안&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  MemberDetail.java&lt;/h3&gt;
&lt;pre id=&quot;code_1664009544645&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;member_detail&quot;)
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = {&quot;member&quot;})
@EqualsAndHashCode(exclude = {&quot;member&quot;})
public class MemberDetail extends BaseEntity {

    @Id
    @Column(name = &quot;mem_id&quot;)
    private Long id;

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @MapsId
    @JoinColumn(name = &quot;mem_id&quot;)
    private Member member;
    
}&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;@ToString 과 @EqualsAndHashCode 의 옵션 중에 exclude를 추가해주면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;exclude = { &quot;원하는 필드명&quot; } 을 추가하고&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;이렇게 되면 ToString 과 EqualsAndHashCode 를 생성할 때&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;스크린샷 2022-09-24 17.55.16.png&quot; data-origin-width=&quot;2280&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rvyh5/btrMTFCUtvM/W6DIIA7QH9f6igCgs4stGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rvyh5/btrMTFCUtvM/W6DIIA7QH9f6igCgs4stGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rvyh5/btrMTFCUtvM/W6DIIA7QH9f6igCgs4stGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frvyh5%2FbtrMTFCUtvM%2FW6DIIA7QH9f6igCgs4stGK%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;2280&quot; height=&quot;370&quot; data-filename=&quot;스크린샷 2022-09-24 17.55.16.png&quot; data-origin-width=&quot;2280&quot; data-origin-height=&quot;370&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 완료!!&lt;/p&gt;</description>
      <category>  프로젝트/  내친소</category>
      <category>circularref</category>
      <category>hashcode</category>
      <category>JPA</category>
      <category>lombok</category>
      <category>Spring</category>
      <category>StackOverflowError</category>
      <category>toString</category>
      <category>스프링</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/43</guid>
      <comments>https://gengminy.tistory.com/43#entry43comment</comments>
      <pubDate>Sat, 24 Sep 2022 17:56:09 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] Elastic Beanstalk + Github Actions 사용해서 Spring boot CI/CD 파이프라인 구축하기</title>
      <link>https://gengminy.tistory.com/42</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;동아리 스터디의 일환으로 시작된&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Beanstalk 으로 CI/CD 파이프라인 구축하기&lt;/p&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;EC2 생성하고 RDS 생성하고 https 설정해주고... 하던 시절과 비교하면&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;하나의 EC2 환경에서 배포하고 싶어서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JAR 환경에서 Docker 환경으로 이동할 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JAR 이나 WAR 로 배포하는 것은 물론 좋지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 EC2 인스턴스를 사용하기엔 돈이 모자란 ^^.... 가난한 대학생에겐&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 EC2에서 멀티 컨테이너로 돌리는게 더 나을 것이라는 판단이다&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  Elastic Beanstalk 환경 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elastic Beanstalk 은 EC2 를 한번 더 추상화 한 개념이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 환경 내에 여러 EC2 인스턴스를 두고 Auto Scaling 으로 관리도 할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3나 RDS, ELB 등 여러 요소를 하나의 환경에서 관리할 수 있다&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1818&quot; data-origin-height=&quot;878&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCA8n6/btrMHI0MvsR/Apsn0tkFLn7d0WjnhcKtck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCA8n6/btrMHI0MvsR/Apsn0tkFLn7d0WjnhcKtck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCA8n6/btrMHI0MvsR/Apsn0tkFLn7d0WjnhcKtck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCA8n6%2FbtrMHI0MvsR%2FApsn0tkFLn7d0WjnhcKtck%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;1818&quot; height=&quot;878&quot; data-origin-width=&quot;1818&quot; data-origin-height=&quot;878&quot;/&gt;&lt;/span&gt;&lt;/figure&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;1570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ajp8U/btrMK1467qg/HYAih6jXm8v3n8orJ8KIBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ajp8U/btrMK1467qg/HYAih6jXm8v3n8orJ8KIBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ajp8U/btrMK1467qg/HYAih6jXm8v3n8orJ8KIBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAjp8U%2FbtrMK1467qg%2FHYAih6jXm8v3n8orJ8KIBK%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;1824&quot; height=&quot;1570&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;1570&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;환경 이름은 첫 문자를 대문자로 바꾼 것에 -env 가 붙어서 자동으로 만들어진다&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1391&quot; data-origin-height=&quot;1158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdwYDJ/btrMK2iDfpy/KwDVbKbyAk8h5gAYmzKGbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdwYDJ/btrMK2iDfpy/KwDVbKbyAk8h5gAYmzKGbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdwYDJ/btrMK2iDfpy/KwDVbKbyAk8h5gAYmzKGbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdwYDJ%2FbtrMK2iDfpy%2FKwDVbKbyAk8h5gAYmzKGbk%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;1391&quot; height=&quot;1158&quot; data-origin-width=&quot;1391&quot; data-origin-height=&quot;1158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리형 플랫폼 -&amp;gt; Java -&amp;gt; 자바 버전 선택 -&amp;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;JAR 파일을 그대로 사용하려면 Java 플랫폼을 선택해주면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker, Go, Node.js, Python 등등 웹서버로 많이 활용하는 플랫폼도 당연히 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 는 8, 11, 17이 지원되는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 8이나 11버전을 많이 사용하더라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 간지나게 새삥 17을 선택했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 중요한 &lt;b&gt;추가 옵션 구성&lt;/b&gt;으로 들어가준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1487&quot; data-origin-height=&quot;1226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dVqtOs/btrMHHHyMRK/xizeKCQIE9U8TT9ugHUqpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dVqtOs/btrMHHHyMRK/xizeKCQIE9U8TT9ugHUqpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dVqtOs/btrMHHHyMRK/xizeKCQIE9U8TT9ugHUqpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdVqtOs%2FbtrMHHHyMRK%2FxizeKCQIE9U8TT9ugHUqpK%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;1487&quot; height=&quot;1226&quot; data-origin-width=&quot;1487&quot; data-origin-height=&quot;1226&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 인스턴스를 생성하고 삭제하는 것을 통해 &lt;span&gt;CI 환경을 만들기 때문에&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 인스턴스 대신 사용자 지정 구성을 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 EC2 프리티어 기준이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한달 EC2의 총합 사용 시간이 750시간을 안넘기면 되기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 EC2 인스턴스가 존재하는 것은 사실 상관이 없다&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1351&quot; data-origin-height=&quot;1181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zvCBm/btrMIPrhBZG/ZGNWQ85A9abmD05rT33K70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zvCBm/btrMIPrhBZG/ZGNWQ85A9abmD05rT33K70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zvCBm/btrMIPrhBZG/ZGNWQ85A9abmD05rT33K70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzvCBm%2FbtrMIPrhBZG%2FZGNWQ85A9abmD05rT33K70%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;1351&quot; height=&quot;1181&quot; data-origin-width=&quot;1351&quot; data-origin-height=&quot;1181&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 EC2 보안 그룹을 설정해야 하는데&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;그러니까 EC2 서비스 -&amp;gt; 보안 그룹으로 가서 설정해야한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 탭 열어서 HTTP, HTTPS 포트 여는 등 설정을 끝내고 다시 오자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/by97Zb/btrMKWpdkc3/PV1OyAbNK4EQMPFtOS62T1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/by97Zb/btrMKWpdkc3/PV1OyAbNK4EQMPFtOS62T1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/by97Zb/btrMKWpdkc3/PV1OyAbNK4EQMPFtOS62T1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fby97Zb%2FbtrMKWpdkc3%2FPV1OyAbNK4EQMPFtOS62T1%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;1406&quot; height=&quot;1000&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;용량 설정에서는 Auto Scaling 을 중요하게 보면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐면 돈과 직결되는 부분이기 때문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최소 최대 인스턴스를 모두 1로 설정해주면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Scaling 이 동작하지 않아서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 EC2 인스턴스가 생기지 않기 때문에 과금 우려가 없어진다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;979&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tYZwi/btrMIWjqMjE/1E6qoVyeOY02OPOZ1BckY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tYZwi/btrMIWjqMjE/1E6qoVyeOY02OPOZ1BckY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tYZwi/btrMIWjqMjE/1E6qoVyeOY02OPOZ1BckY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtYZwi%2FbtrMIWjqMjE%2F1E6qoVyeOY02OPOZ1BckY0%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;1378&quot; height=&quot;979&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;979&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스 유형에서 t2.small 기본 선택이 되어있을텐데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프리티어인 t2.micro 만 빼고 없애버린다&lt;/p&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;1208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ba9Hfh/btrMIOePM1Y/1ILfJplFhZcKIweNalmxXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ba9Hfh/btrMIOePM1Y/1ILfJplFhZcKIweNalmxXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ba9Hfh/btrMIOePM1Y/1ILfJplFhZcKIweNalmxXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fba9Hfh%2FbtrMIOePM1Y%2F1ILfJplFhZcKIweNalmxXk%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;1404&quot; height=&quot;1208&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;1208&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPS 설정이 필요하다면 리스너 -&amp;gt; 리스너 추가에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPS 와 SSL 인증서를 달아주면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증서가 없다면 Route 53 서비스를 이용하는 조건으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무료 인증서를 발급받을 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(무료라면서 Route 53 에 도메인 하나 연결 당 매달 500원 정도 나온다)&lt;/p&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1405&quot; data-origin-height=&quot;967&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ztCf1/btrMIV5TXcJ/iSHpjeXG4ogIM3tHwi5vL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ztCf1/btrMIV5TXcJ/iSHpjeXG4ogIM3tHwi5vL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ztCf1/btrMIV5TXcJ/iSHpjeXG4ogIM3tHwi5vL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FztCf1%2FbtrMIV5TXcJ%2FiSHpjeXG4ogIM3tHwi5vL0%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;1405&quot; height=&quot;967&quot; data-origin-width=&quot;1405&quot; data-origin-height=&quot;967&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;배포 방식을 추가 배치를 사용한 롤링으로 바꿔준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포할 때마다 새 EC2 인스턴스를 생성한 후 배포 작업을 진행하며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 작업이 끝나면 로드밸런서가 새 EC2 인스턴스를 가리키게 하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI 환경을 구성하는 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 구 버전 EC2 인스턴스를 삭제하여 배포 작업을 끝낸다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;843&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lzN7I/btrMIcN78hZ/PeFeXKxzo8UzkrUaKICULK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lzN7I/btrMIcN78hZ/PeFeXKxzo8UzkrUaKICULK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lzN7I/btrMIcN78hZ/PeFeXKxzo8UzkrUaKICULK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlzN7I%2FbtrMIcN78hZ%2FPeFeXKxzo8UzkrUaKICULK%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;1414&quot; height=&quot;843&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;843&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;서비스 역할과 IAM 인스턴스 프로파일은 IAM 서비스에서 정의하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 키 페어는 EC2 서비스로 들어가서 만들어야 한다&lt;/p&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;없다면 IAM 서비스로 진입&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1193&quot; data-origin-height=&quot;1149&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMW1aO/btrMKtALXyc/aOej7VEu7MrKV5LtUbgb2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMW1aO/btrMKtALXyc/aOej7VEu7MrKV5LtUbgb2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMW1aO/btrMKtALXyc/aOej7VEu7MrKV5LtUbgb2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMW1aO%2FbtrMKtALXyc%2FaOej7VEu7MrKV5LtUbgb2k%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;1193&quot; height=&quot;1149&quot; data-origin-width=&quot;1193&quot; data-origin-height=&quot;1149&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAM -&amp;gt; 엑세스 관리 -&amp;gt; 역할 -&amp;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;AdministratorAccess 만 추가했을 때는&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1485&quot; data-origin-height=&quot;947&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjkvcC/btrMKsWcW6a/hedq7S3GWtupO8GkkrHsl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjkvcC/btrMKsWcW6a/hedq7S3GWtupO8GkkrHsl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjkvcC/btrMKsWcW6a/hedq7S3GWtupO8GkkrHsl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjkvcC%2FbtrMKsWcW6a%2Fhedq7S3GWtupO8GkkrHsl0%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;1485&quot; height=&quot;947&quot; data-origin-width=&quot;1485&quot; data-origin-height=&quot;947&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;엑세스 키를 선택하고 이름 지정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;523&quot; data-origin-height=&quot;103&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFMICe/btrMIWRfLqs/KKx3yhzKU9uUWUmDveEwo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFMICe/btrMIWRfLqs/KKx3yhzKU9uUWUmDveEwo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFMICe/btrMIWRfLqs/KKx3yhzKU9uUWUmDveEwo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFMICe%2FbtrMIWRfLqs%2FKKx3yhzKU9uUWUmDveEwo0%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;523&quot; height=&quot;103&quot; data-origin-width=&quot;523&quot; data-origin-height=&quot;103&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 이 2가지의 권한을 주었다&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;837&quot; data-origin-height=&quot;375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dpwbs/btrMKVw6Pj0/2up2sH7W4Nqu8OXVaeReI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dpwbs/btrMKVw6Pj0/2up2sH7W4Nqu8OXVaeReI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dpwbs/btrMKVw6Pj0/2up2sH7W4Nqu8OXVaeReI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDpwbs%2FbtrMKVw6Pj0%2F2up2sH7W4Nqu8OXVaeReI1%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;837&quot; height=&quot;375&quot; data-origin-width=&quot;837&quot; data-origin-height=&quot;375&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다 만들면 이렇게 엑세스 키 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;Github Actions 정의할 때 필요하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 하고 환경 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1678&quot; data-origin-height=&quot;1175&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IKgs6/btrMJlpJyic/0rELzXPcNJziY22yCMTtrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IKgs6/btrMJlpJyic/0rELzXPcNJziY22yCMTtrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IKgs6/btrMJlpJyic/0rELzXPcNJziY22yCMTtrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIKgs6%2FbtrMJlpJyic%2F0rELzXPcNJziY22yCMTtrK%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;1678&quot; height=&quot;1175&quot; data-origin-width=&quot;1678&quot; data-origin-height=&quot;1175&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;보통 처음에 안되는건 업로드한 서버 파일 문제 또는 권한 문제&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  Beanstalk 배포 세팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Beanstalk 배포 설정을 위해서 내 프로젝트에 파일을 추가해야 한다&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;  .ebextensions/00-makeFile.config&lt;/h3&gt;
&lt;pre id=&quot;code_1663770377329&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;files:
    &quot;/sbin/appstart&quot; :
        mode: &quot;000755&quot;
        owner: webapp
        group: webapp
        content: |
            #!/usr/bin/env bash
            JAR_PATH=/var/app/current/application.jar

            # run app
            killall java
            java -Dfile.encoding=UTF-8 -Dspring.profiles.active=$SPRING_PROFILE -jar $JAR_PATH&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Beanstalk 내부에서 배포 작업 중 실행할 명령어를 정의했다&lt;/p&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1379&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UkQYr/btrMKU50Ynv/UiJGZyWEwrw4PQK6Q3um7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UkQYr/btrMKU50Ynv/UiJGZyWEwrw4PQK6Q3um7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UkQYr/btrMKU50Ynv/UiJGZyWEwrw4PQK6Q3um7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUkQYr%2FbtrMKU50Ynv%2FUiJGZyWEwrw4PQK6Q3um7K%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;1379&quot; height=&quot;675&quot; data-origin-width=&quot;1379&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPRING_PROFILE 이라는 환경 변수는 Beanstalk 의 소프트웨이 -&amp;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;  ./Procfile&lt;/h3&gt;
&lt;pre id=&quot;code_1663770493003&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;web: appstart&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;아까 .ebextensions 에서 정의한 명령어를 배포 작업이 종료되면 즉시 실행한다&lt;/p&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;  .platform/nginx/nginx.conf&lt;/h3&gt;
&lt;pre id=&quot;code_1663770548783&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;user                    nginx;
error_log               /var/log/nginx/error.log warn;
pid                     /var/run/nginx.pid;
worker_processes        auto;
worker_rlimit_nofile    33282;

events {
    use epoll;
    worker_connections  1024;
    multi_accept on;
}

http {
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;

  log_format  main  '$remote_addr - $remote_user [$time_local] &quot;$request&quot; '
                    '$status $body_bytes_sent &quot;$http_referer&quot; '
                    '&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;';

  include       conf.d/*.conf;

  map $http_upgrade $connection_upgrade {
      default     &quot;upgrade&quot;;
  }

  upstream springboot {
    server localhost:8080;
    keepalive 1024;
  }

  server {
      listen        80 default_server;
      listen        [::]:80 default_server;

      location / {
          proxy_pass          http://springboot;
          proxy_http_version  1.1;
          proxy_set_header    Connection          $connection_upgrade;
          proxy_set_header    Upgrade             $http_upgrade;

          proxy_set_header    Host                $host;
          proxy_set_header    X-Real-IP           $remote_addr;
          proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
      }

      access_log    /var/log/nginx/access.log main;

      client_header_timeout 60;
      client_body_timeout   60;
      keepalive_timeout     60;
      gzip                  off;
      gzip_comp_level       4;

      # Include the Elastic Beanstalk generated locations
      include conf.d/elasticbeanstalk/healthd.conf;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx 설정 정의 파일&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elastic Beanstalk은 기본적으로 Nginx 를 통해 호스팅 해주는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx 의 특장점중 하나인 리버스 프록시를 통해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;80번 포트 -&amp;gt; 8080 포트로 전달해주는 설정이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Beanstalk 에 내장된 Nginx 는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 설정으로 localhost:5000 포트로 전달해주기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재정의를 통해 8080 포트로 전달해주도록 하는 것이다&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Github Actions 정의&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  .github/workflows/deploy.yml&lt;/h3&gt;
&lt;pre id=&quot;code_1663770031471&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: beanstalk-springboot-deploy

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      #JDK Setting
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'

      #Create dotenv file
      - name: Make env properties
        run: |
          touch ./src/main/resources/application-dev.yml
          touch ./src/main/resources/application-prod.yml
          echo &quot;$ENV_PROPERTIES_DEV&quot; &amp;gt; ./src/main/resources/application-dev.yml
          echo &quot;$ENV_PROPERTIES_PROD&quot; &amp;gt; ./src/main/resources/application-prod.yml
        env:
          ENV_PROPERTIES_DEV: ${{ secrets.ENV_PROPERTIES_DEV }}
          ENV_PROPERTIES_PROD: ${{ secrets.ENV_PROPERTIES_PROD }}

      #Grant gradlew Permission
      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew
        shell: bash

      - name: Build with Gradle
        run: ./gradlew clean build
        shell: bash

      - name: Get current time
        uses: 1466587594/get-current-time@v2
        id: current-time
        with:
          format: YYYY-MM-DDTHH-mm-ss
          utcOffset: &quot;+09:00&quot;

      - name: Show Current Time
        run: echo &quot;CurrentTime=$&quot;
        shell: bash

      - name: Generate deployment package
        run: |
          mkdir -p deploy
          cp build/libs/*.jar deploy/application.jar
          cp Procfile deploy/Procfile
          cp -r .ebextensions deploy/.ebextensions
          cp -r .platform deploy/.platform
          cd deploy &amp;amp;&amp;amp; zip -r deploy.zip .

      - name: Beanstalk Deploy
        uses: einaregilsson/beanstalk-deploy@v20
        with:
          aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          application_name: naechinso
          environment_name: Naechinso-env-1
          version_label: github-action-${{ steps.current-time.outputs.formattedTime }}
          region: ap-northeast-2
          deployment_package: deploy/deploy.zip&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github actions 는 반드시 루트 폴더의 .github/workflows 내부에 정의해야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 민감한 변수들을 application.yml 을 나누어 관리하기 때문에&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;마지막 application_name 과 environment_name 은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 beanstalk 에서 구성한 어플리케이션 이름과 환경 이름을 넣어주면 된다&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;1124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGi2rP/btrMJl4mgzf/Yt6KaRz8bWnmi0eZrvN8m0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGi2rP/btrMJl4mgzf/Yt6KaRz8bWnmi0eZrvN8m0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGi2rP/btrMJl4mgzf/Yt6KaRz8bWnmi0eZrvN8m0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGi2rP%2FbtrMJl4mgzf%2FYt6KaRz8bWnmi0eZrvN8m0%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;1674&quot; height=&quot;1124&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;1124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;secrets 변수들은 내 레포지토리 -&amp;gt; setting -&amp;gt; secrets -&amp;gt; actions 에서 관리한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 아까 만든 IAM 프로파일 값을 넣어주고 사용한다&lt;/p&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1694&quot; data-origin-height=&quot;979&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7ahBT/btrMJumHOe7/VNDMqbcYeh4AqNCFZMtjkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7ahBT/btrMJumHOe7/VNDMqbcYeh4AqNCFZMtjkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7ahBT/btrMJumHOe7/VNDMqbcYeh4AqNCFZMtjkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7ahBT%2FbtrMJumHOe7%2FVNDMqbcYeh4AqNCFZMtjkk%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;1694&quot; height=&quot;979&quot; data-origin-width=&quot;1694&quot; data-origin-height=&quot;979&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정에 따라 main 브랜치에 푸시를 하면 액션 실행&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;h2 data-ke-size=&quot;size26&quot;&gt;✨ Log 보는 법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elastic Beanstalk 이 이렇게 설정하는 것이 간단하지만&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;449&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vaNa7/btrMJPxoR28/Vb7gKRiAyqOp84kwodRGJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vaNa7/btrMJPxoR28/Vb7gKRiAyqOp84kwodRGJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vaNa7/btrMJPxoR28/Vb7gKRiAyqOp84kwodRGJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvaNa7%2FbtrMJPxoR28%2FVb7gKRiAyqOp84kwodRGJ1%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;1406&quot; height=&quot;449&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;449&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그는 환경 -&amp;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Docker 로 새 환경 구성하러 가야겠다&lt;/p&gt;</description>
      <category>  Infra/  AWS</category>
      <category>AWS</category>
      <category>beanstalk</category>
      <category>EB</category>
      <category>elasticbeanstalk</category>
      <category>Spring</category>
      <category>배포</category>
      <category>스프링</category>
      <category>클라우드</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/42</guid>
      <comments>https://gengminy.tistory.com/42#entry42comment</comments>
      <pubDate>Wed, 21 Sep 2022 23:34:01 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 문자 인증 구현하기 (Redis + 네이버 클라우드 플랫폼 SMS API)</title>
      <link>https://gengminy.tistory.com/41</link>
      <description>&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;지금 하는 중인 프로젝트가 원래는 아이디 + 비밀번호 기반 로그인이여서&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;메일 기반 인증과 OAuth 를 준비중이었는데&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;다시 문자 인증 기반 로그인으로 기획이 변경되었다&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;문자 인증 같은 경우는 예전 프로젝트에서&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;네이버 SMS API를 통해 구현해 본 적이 있어서 그다지 어렵지는 않지만&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;마침 만드는 김에 복습하는 차원으로 구현하고 나서 글을 적어보기로 했다&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;⚙️ Dependency&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;//webflux&lt;br /&gt;implementation 'org.springframework.boot:spring-boot-starter-webflux'&lt;br /&gt;//redis&lt;br /&gt;implementation 'org.springframework.boot:spring-boot-starter-data-redis'&lt;br /&gt;&lt;br /&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;서버 상에서 http 요청을 보내고 응답받기 위한 WebClient를 사용하기 위해 webflux를 추가,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;휴대폰 번호와 인증번호를 만료기간을 두고 관리하기 위한 인메모리 DB인 Redis 를 추가해준다&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;  네이버 클라우드 플랫폼 SMS API 가입&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; 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_1662869484982&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/Mumh4/hyPJ0o8kxm/B3UTmGM6flvwledZR3RQkk/img.jpg?width=526&amp;amp;height=274&amp;amp;face=0_0_526_274&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/Mumh4/hyPJ0o8kxm/B3UTmGM6flvwledZR3RQkk/img.jpg?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;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;&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;먼저 우리가 사용할 서비스는 SMS API 서비스이고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름은 SENS(&lt;span&gt;Simple &amp;amp; Easy Notification Service) 이다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&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;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_1662869544853&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/f06PS/hyPJ3lPPQF/WNxo3pKu1IrSfmOyysZS50/img.jpg?width=526&amp;amp;height=274&amp;amp;face=0_0_526_274&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/f06PS/hyPJ3lPPQF/WNxo3pKu1IrSfmOyysZS50/img.jpg?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;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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2354&quot; data-origin-height=&quot;1094&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qpFys/btrLLySa7d8/tKkT1cNKDEkKRw8Q3p8GfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qpFys/btrLLySa7d8/tKkT1cNKDEkKRw8Q3p8GfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qpFys/btrLLySa7d8/tKkT1cNKDEkKRw8Q3p8GfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqpFys%2FbtrLLySa7d8%2FtKkT1cNKDEkKRw8Q3p8GfK%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;2354&quot; height=&quot;1094&quot; data-origin-width=&quot;2354&quot; data-origin-height=&quot;1094&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자 인증에는 당연히 80자 이하의 SMS 를 사용할 것이고&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;테스트로 몇 번씩 보내보면서 진행하기에 딱이다&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ct9FJ7/btrLOna5oBP/Sx7ZNdF3iGsrDBzo1S6kjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ct9FJ7/btrLOna5oBP/Sx7ZNdF3iGsrDBzo1S6kjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ct9FJ7/btrLOna5oBP/Sx7ZNdF3iGsrDBzo1S6kjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fct9FJ7%2FbtrLOna5oBP%2FSx7ZNdF3iGsrDBzo1S6kjk%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;600&quot; height=&quot;268&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;430&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;gt; 서비스 -&amp;gt; 검색어로 Simple &amp;amp; Easy Notification Service 진입&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1012&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnphTh/btrLR7L9qz3/J1UcllKraQ8odKtvTckkbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnphTh/btrLR7L9qz3/J1UcllKraQ8odKtvTckkbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnphTh/btrLR7L9qz3/J1UcllKraQ8odKtvTckkbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnphTh%2FbtrLR7L9qz3%2FJ1UcllKraQ8odKtvTckkbK%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;600&quot; height=&quot;121&quot; data-origin-width=&quot;1012&quot; data-origin-height=&quot;204&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;옆에 OPEN 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;개발자는 공식 Docs 도 잘 봐야한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pp5AH/btrLTB7hTE7/IXX6P7kivKQZu89rxlTAO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pp5AH/btrLTB7hTE7/IXX6P7kivKQZu89rxlTAO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pp5AH/btrLTB7hTE7/IXX6P7kivKQZu89rxlTAO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpp5AH%2FbtrLTB7hTE7%2FIXX6P7kivKQZu89rxlTAO1%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;600&quot; height=&quot;415&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자 인증이니 당연히 SMS 체크 후 이름과 설명 대충 만들어주기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;492&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8VOXy/btrLMxkWMMK/ZIEw5AGwKVyh5fiSHBboe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8VOXy/btrLMxkWMMK/ZIEw5AGwKVyh5fiSHBboe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8VOXy/btrLMxkWMMK/ZIEw5AGwKVyh5fiSHBboe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8VOXy%2FbtrLMxkWMMK%2FZIEw5AGwKVyh5fiSHBboe1%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;2290&quot; height=&quot;492&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;492&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 왼쪽 대시보드에서 Projects 선택 후에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방금 만든 프로젝트 행에서 오른쪽 서비스 ID의 열쇠모양 클릭&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1372&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BODMA/btrLTBzq5NO/LuDUeerDcbTGmX92CWmWmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BODMA/btrLTBzq5NO/LuDUeerDcbTGmX92CWmWmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BODMA/btrLTBzq5NO/LuDUeerDcbTGmX92CWmWmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBODMA%2FbtrLTBzq5NO%2FLuDUeerDcbTGmX92CWmWmk%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;600&quot; height=&quot;367&quot; data-origin-width=&quot;1372&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ncp: 로 시작하는 API Service ID Key 가 나오는데&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2824&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9kFTv/btrLLYpC1cp/8KmYGhLzI9UpP0ixl31dRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9kFTv/btrLLYpC1cp/8KmYGhLzI9UpP0ixl31dRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9kFTv/btrLLYpC1cp/8KmYGhLzI9UpP0ixl31dRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9kFTv%2FbtrLLYpC1cp%2F8KmYGhLzI9UpP0ixl31dRK%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;2824&quot; height=&quot;622&quot; data-origin-width=&quot;2824&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 Simple &amp;amp; Easy Notification Service -&amp;gt; Solutions -&amp;gt; SMS -&amp;gt; Calling Number 들어와서 발신번호 등록해주기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개인 회원은 개인 명의의 휴대폰밖에 등록 못하니 주의&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 번호 등록하거나 사업자 같은 경우 따로 서류를 준비해서 여차저차 하면 된다고 설명되어 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2370&quot; data-origin-height=&quot;864&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHsDYA/btrLSugdq3C/i34KPS2kOoPtI1USEMlCN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHsDYA/btrLSugdq3C/i34KPS2kOoPtI1USEMlCN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHsDYA/btrLSugdq3C/i34KPS2kOoPtI1USEMlCN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHsDYA%2FbtrLSugdq3C%2Fi34KPS2kOoPtI1USEMlCN1%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;2370&quot; height=&quot;864&quot; data-origin-width=&quot;2370&quot; data-origin-height=&quot;864&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;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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2094&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDJd6Y/btrLLzDANdk/73KXCaF6SicwqKii1LlNa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDJd6Y/btrLLzDANdk/73KXCaF6SicwqKii1LlNa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDJd6Y/btrLLzDANdk/73KXCaF6SicwqKii1LlNa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDJd6Y%2FbtrLLzDANdk%2F73KXCaF6SicwqKii1LlNa1%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;2094&quot; height=&quot;486&quot; data-origin-width=&quot;2094&quot; data-origin-height=&quot;486&quot;/&gt;&lt;/span&gt;&lt;/figure&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;Access Key 와 Secret Key 를 잘 기억해두자&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;  .env&lt;/h3&gt;
&lt;pre id=&quot;code_1662870311397&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;NAVER_SMS_ID={네이버sms 서비스ID}
NAVER_SMS_PHONE_NUMBER={발신전화번호}
NAVER_ACCESS_KEY={네이버API 엑세스키}
NAVER_SECRET_KEY={네이버API 비밀키}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dotenv 나 application.properties 파일등에 비밀스럽게 잘 두면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 git 에 이 정보가 올라가면 큰일나니 주의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘엔 워낙 서비스가 잘 되어있어서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;git 같은데 올라가는 거 감지되면 서비스를 강제 종료시켜버리던가 그러지만&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;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;  문자 인증 절차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://api.ncloud-docs.com/docs/ko/ai-application-service-sens-smsv2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://api.ncloud-docs.com/docs/ko/ai-application-service-sens-smsv2&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1662870583616&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;SMS API&quot; data-og-description=&quot; &quot; data-og-host=&quot;api.ncloud-docs.com&quot; data-og-source-url=&quot;https://api.ncloud-docs.com/docs/ko/ai-application-service-sens-smsv2&quot; data-og-url=&quot;https://api.ncloud-docs.com/docs/ko/ai-application-service-sens-smsv2&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://api.ncloud-docs.com/docs/ko/ai-application-service-sens-smsv2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://api.ncloud-docs.com/docs/ko/ai-application-service-sens-smsv2&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;SMS API&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;api.ncloud-docs.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;/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;  API Header 정의&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1678&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPxCYS/btrLLj15q7U/40KI8yZw4OtI8boKFotDE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPxCYS/btrLLj15q7U/40KI8yZw4OtI8boKFotDE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPxCYS/btrLLj15q7U/40KI8yZw4OtI8boKFotDE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPxCYS%2FbtrLLj15q7U%2F40KI8yZw4OtI8boKFotDE0%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;1678&quot; height=&quot;502&quot; data-origin-width=&quot;1678&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 클라우드 API 를 사용하기 위해서는 위 3개의 헤더를 끼워서 요청을 보내야한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 밀리초 단위의 타임스탬프&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 아까 받은 계정 Access Key&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 1번과 2번을 암호화 알고리즘으로 서명한 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번과 2번은 여차저차 하면 되지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번은 조금 헷갈릴 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  서명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://api.ncloud-docs.com/docs/common-ncpapi&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://api.ncloud-docs.com/docs/common-ncpapi&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1662870784267&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;Ncloud API&quot; data-og-description=&quot; &quot; data-og-host=&quot;api.ncloud-docs.com&quot; data-og-source-url=&quot;https://api.ncloud-docs.com/docs/common-ncpapi&quot; data-og-url=&quot;https://api.ncloud-docs.com/docs/common-ncpapi&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://api.ncloud-docs.com/docs/common-ncpapi&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://api.ncloud-docs.com/docs/common-ncpapi&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;Ncloud API&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;api.ncloud-docs.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;h4 data-ke-size=&quot;size20&quot;&gt;  SmsCertificationServiceImpl.java&lt;/h4&gt;
&lt;pre id=&quot;code_1662870851126&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Service
@RequiredArgsConstructor
public class SmsCertificationServiceImpl implements SmsCertificationService {

    private final WebClient webClient;
    private final RedisService redisService;

    private final String VERIFICATION_PREFIX = &quot;sms:&quot;;
    private final int VERIFICATION_TIME_LIMIT = 3 * 60;

    @Value(&quot;${SPRING_PROFILE}&quot;)
    private String springProfile;
    @Value(&quot;${NAVER_ACCESS_KEY}&quot;)
    private String accessKey;
    @Value(&quot;${NAVER_SECRET_KEY}&quot;)
    private String secretKey;
    @Value(&quot;${NAVER_SMS_ID}&quot;)
    private String serviceId;
    @Value(&quot;${NAVER_SMS_PHONE_NUMBER}&quot;)
    private String senderNumber;

	/**
     * sms 전송을 위한 서명을 추가한다
     * @param currentTime 현재 시간
     * @return 서명
     */
    @Override
    public String makeSignature(Long currentTime) {
        String space = &quot; &quot;;
        String newLine = &quot;\n&quot;;
        String method = &quot;POST&quot;;
        String url = &quot;/sms/v2/services/&quot; + this.serviceId + &quot;/messages&quot;;
        String timestamp = currentTime.toString();
        String accessKey = this.accessKey;
        String secretKey = this.secretKey;

        try {

            String message = method +
                    space +
                    url +
                    newLine +
                    timestamp +
                    newLine +
                    accessKey;

            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;));
            String encodeBase64String = Base64.encodeBase64String(rawHmac);

            return encodeBase64String;
        } catch (Exception e) {
            throw new BadRequestException(ErrorCode._BAD_REQUEST, e.getMessage());
        }
    }
    ...
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제와 비슷하지만 매개변수로 Long 형의 현재 시간을 받아온다&lt;/p&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;  DTO 정의&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  NaverSmsMessageDTO.java&lt;/h4&gt;
&lt;pre id=&quot;code_1662871457699&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@ToString
public class NaverSmsMessageDTO {
    String to;
    String content;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt; &amp;nbsp; NaverSmsRequestDTO.java&lt;/h4&gt;
&lt;pre id=&quot;code_1662871478932&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@ToString
public class NaverSmsRequestDTO {
    String type;
    String contentType;
    String countryCode;
    String from;
    String content;
    List&amp;lt;NaverSmsMessageDTO&amp;gt; messages;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  NaverSmsResponseDTO.java&lt;/h4&gt;
&lt;pre id=&quot;code_1662871468933&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@ToString
public class NaverSmsResponseDTO {
    String requestId;
    LocalDateTime requestTime;
    String statusCode;
    String statusName;
}&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;꼭 정의해야하는 DTO는 위의 세 가지로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 SMS API의 메세지 응답, 처리에 필요하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름 마음대로 바꿨다가 DTO &amp;lt;-&amp;gt; JSON 매칭이 안돼서 삽질좀 했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왠만하면 그대로 사용하는 게 좋다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request body 의 각 필드는 optional 이라서 필요한 것만 추가해주면 된다&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;  SmsCertificationServiceImpl.java&lt;/h4&gt;
&lt;pre id=&quot;code_1662871713637&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    ...
    ...
    /**
     * 인증번호가 담긴 메세지를 전송한다
     * @param to 수신자
     * @return 네이버 api 서버 메세지 응답
     */
    @Override
    public String sendVerificationMessage(String to) {
        final String smsURL = &quot;https://sens.apigw.ntruss.com/sms/v2/services/&quot;+ serviceId +&quot;/messages&quot;;
        final Long time = System.currentTimeMillis();
        //랜덤 6자리 인증번호
        final String verificationCode = generateVerificationCode();
        //3분 제한시간
        final Duration verificationTimeLimit = Duration.ofSeconds(VERIFICATION_TIME_LIMIT);

        //[local, dev] 배포 환경이 아닐때는 fake service 를 제공합니다
        if (!springProfile.equals(&quot;prod&quot;)) {
            log.info(&quot;스프링 프로파일(&quot; + springProfile + &quot;) 따라 fake 서비스를 제공합니다&quot;);
            String message = generateMessageWithCode(verificationCode);
            log.info(message);
            redisService.setValues(VERIFICATION_PREFIX + to, verificationCode, verificationTimeLimit);
            return message;
        }

        //[prod] 실 배포 환경에서는 문자를 전송합니다
        try {
            //네이버 sms 메세지 dto
            final NaverSmsMessageDTO naverSmsMessageDTO = new NaverSmsMessageDTO(to, generateMessageWithCode(verificationCode));
            List&amp;lt;NaverSmsMessageDTO&amp;gt; messages = new ArrayList&amp;lt;&amp;gt;();
            messages.add(naverSmsMessageDTO);
            final NaverSmsRequestDTO naverSmsRequestDTO = NaverSmsRequestDTO.builder()
                    .type(&quot;SMS&quot;)
                    .contentType(&quot;COMM&quot;)
                    .countryCode(&quot;82&quot;)
                    .from(senderNumber)
                    .content(naverSmsMessageDTO.getContent())
                    .messages(messages)
                    .build();
            final String body = new ObjectMapper().writeValueAsString(naverSmsRequestDTO);

//            final ResponseMessageDTO responseMessageDTO =
            webClient.post().uri(smsURL)
                    .contentType(MediaType.APPLICATION_JSON)
                    .header(&quot;x-ncp-apigw-timestamp&quot;, time.toString())
                    .header(&quot;x-ncp-iam-access-key&quot;, accessKey)
                    .header(&quot;x-ncp-apigw-signature-v2&quot;, makeSignature(time))
                    .accept(MediaType.APPLICATION_JSON)
                    .body(BodyInserters.fromValue(body))
                    .retrieve()
                    .bodyToMono(NaverSmsResponseDTO.class).block();

            //redis 에 3분 제한의 인증번호 토큰 저장
            redisService.setValues(VERIFICATION_PREFIX + to, verificationCode, verificationTimeLimit);

            return &quot;메세지 전송 성공&quot;;
//            return responseMessageDTO;
        } catch (Exception e) {
            e.printStackTrace();
            throw new BadRequestException(ErrorCode._BAD_REQUEST, &quot;메세지 발송에 실패하였습니다&quot;);
        }
    }
    
        /**
     * 랜덤 인증번호를 생성한다
     * @return 인증번호 6자리
     */
    private String generateVerificationCode() {
        Random random = new Random();
        int verificationCode = random.nextInt(888888) + 111111;
        return Integer.toString(verificationCode);
    }

    /**
     * 인증번호가 포함된 메세지를 생성한다
     * @param code 인증번호 6자리
     * @return 인증번호 6자리가 포함된 메세지
     */
    private String generateMessageWithCode(String code) {
        final String provider = &quot;내친소&quot;;
        return &quot;[&quot; + provider + &quot;] 인증번호 [&quot; + code + &quot;] 를 입력해주세요 :)&quot;;
    }
    
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증번호 전송은 fake 와 real 서비스로 나누었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 프로파일을 local / dev / prod 총 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;p data-ke-size=&quot;size16&quot;&gt;프로파일이 prod가 아닐 경우에는 문자를 전송하지 않고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;response 로 바로 인증번호를 반환시켜버렸다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로파일이 prod 이면은 실제 문자를 전송시키는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Webflux 의 WebClient를 사용해서 네이버 SMS API 서버와 통신했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증번호가 생성되고 문자를 보내면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3분 기한을 두고 Redis 에 {Key: Value} 형태로 저장해둔다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Key는 사용자의 번호로 두었고 인증 과정에서 뽑아올 때 번호를 통해 처리한다&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;  Redis 설정&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  RedisConfig.java&lt;/h4&gt;
&lt;pre id=&quot;code_1662872111972&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class RedisConfig {
    @Value(&quot;${REDIS_HOST}&quot;)
    private String host;

    @Value(&quot;${REDIS_PORT}&quot;)
    private int port;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate() {
        // redisTemplate를 받아와서 set, get, delete를 사용
        RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate = new RedisTemplate&amp;lt;&amp;gt;();
        /**
         * setKeySerializer, setValueSerializer 설정
         * redis-cli을 통해 직접 데이터를 조회 시 알아볼 수 없는 형태로 출력되는 것을 방지
         */
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        return redisTemplate;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스 설정 파일이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스 서버는 도커 컴포즈를 통해 로컬에서 관리중이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  RedisServiceImpl.java&lt;/h4&gt;
&lt;pre id=&quot;code_1662872163118&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Service
@RequiredArgsConstructor
public class RedisServiceImpl implements RedisService {
    private final RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate;

    @Override
    public void setValues(String key, String data) {
        ValueOperations&amp;lt;String, String&amp;gt; values = redisTemplate.opsForValue();
        values.set(key, data);
    }

    @Override
    public void setValues(String key, String data, Duration duration) {
        ValueOperations&amp;lt;String, String&amp;gt; values = redisTemplate.opsForValue();
        values.set(key, data, duration);
    }

    @Override
    public String getValues(String key) {
        ValueOperations&amp;lt;String, String&amp;gt; values = redisTemplate.opsForValue();
        return values.get(key);
    }

    @Override
    public void deleteValues(String key) {
        redisTemplate.delete(key);
    }

    @Override
    public boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스 서비스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키 저장, 삭제, 뽑아오기 등을 담당하고 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  인증번호 검증&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  SmsCertificationServiceImpl.java&lt;/h4&gt;
&lt;pre id=&quot;code_1662872234205&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
...
	@Override
    public String verifyCode(SmsCertificationRequestDTO smsCertificationRequestDto) {
        String phoneNumber = smsCertificationRequestDto.getPhoneNumber();
        String code = smsCertificationRequestDto.getCode();
        String key = VERIFICATION_PREFIX + phoneNumber;

        //redis 에 해당 번호의 키가 없는 경우는 인증번호(3분) 만료로 처리
        if (!redisService.hasKey(key)) {
            throw new UnauthorizedException(ErrorCode.EXPIRED_VERIFICATION_CODE);
        }

        //redis 에 해당 번호의 키와 인증번호가 일치하지 않는 경우
        if (!redisService.getValues(key).equals(code)) {
            throw new UnauthorizedException(ErrorCode.MISMATCH_VERIFICATION_CODE);
        }

        //필터를 모두 통과, 인증이 완료되었으니 redis 에서 전화번호 삭제
        redisService.deleteValues(key);
        return &quot;인증에 성공하였습니다&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;만약 사용자의 번호로 된 Key값이 없으면 만료된 것으로 처리&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;모든 필터를 통과하면 Redis 상에서 번호로 된 키 값을 제거(만료)시키고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 성공 처리를 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리턴 값을 String 메세지 형태로 주었는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증에 성공할 경우를 제외하면 모두 4xx 에러로 리턴되기 때문이다&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;  SmsCertificationController.java&lt;/h4&gt;
&lt;pre id=&quot;code_1662872325705&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@RestController
@RequestMapping(&quot;/sms&quot;)
@RequiredArgsConstructor
public class SmsCertificationController {
    private final SmsCertificationService smsCertificationService;

    @PostMapping(&quot;/send&quot;)
    public CommonApiResponse&amp;lt;String&amp;gt; sendMessageWithVerificationCode(@RequestBody @Valid SmsVerificationCodeRequestDTO dto) {
        String result = smsCertificationService.sendVerificationMessage(dto.getPhoneNumber());
        return CommonApiResponse.of(result);
    }

    @PostMapping(&quot;/verify&quot;)
    public CommonApiResponse&amp;lt;String&amp;gt; verifyCode(@RequestBody @Valid SmsCertificationRequestDTO dto) {
        String result = smsCertificationService.verifyCode(dto);
        return CommonApiResponse.of(result);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증번호 전송과 검증 처리 관련 url 을 추가했다&lt;/p&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;  SmsVerificationCodeRequestDTO.java&lt;/h4&gt;
&lt;pre id=&quot;code_1662872415471&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@ToString
public class SmsVerificationCodeRequestDTO {
    @NotBlank(message = &quot;전화번호를 입력해주세요&quot;)
    @Pattern(regexp = &quot;[0-9]{10,11}&quot;, message = &quot;하이픈 없는 10~11자리 숫자를 입력해주세요&quot;)
    String phoneNumber;
}&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;Spring Validation 을 통해 인자를 검증한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;01000000000 형태의 붙어있는 번호를 받을 것이기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규식으로 숫자만 있는 문자열이 오도록 했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  SmsCertificationRequestDTO.java&lt;/h4&gt;
&lt;pre id=&quot;code_1662872424970&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@ToString
public class SmsCertificationRequestDTO {
    @NotBlank(message = &quot;전화번호를 입력해주세요&quot;)
    @Pattern(regexp = &quot;[0-9]{10,11}&quot;, message = &quot;하이픈 없는 10~11자리 숫자를 입력해주세요&quot;)
    String phoneNumber;

    @NotBlank(message = &quot;인증번호를 입력해주세요&quot;)
    @Pattern(regexp = &quot;[0-9]{6}&quot;, message = &quot;인증번호 6자리를 입력해주세요&quot;)
    String code;
}&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;마찬가지로 휴대폰번호와 인증번호를 검증했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Validation 을 통과하지 못하면 MethodArgumentNotValidException 을 내뿜어서&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;h2 data-ke-size=&quot;size26&quot;&gt;  문자인증 테스트&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1950&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q0vBk/btrLLxsdAnX/kCKnCrZENzVelnzjGPYkC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q0vBk/btrLLxsdAnX/kCKnCrZENzVelnzjGPYkC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q0vBk/btrLLxsdAnX/kCKnCrZENzVelnzjGPYkC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq0vBk%2FbtrLLxsdAnX%2FkCKnCrZENzVelnzjGPYkC0%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;1950&quot; height=&quot;600&quot; data-origin-width=&quot;1950&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로파일이 local 일 경우 이렇게 데이터로 바로 넘어온다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1860&quot; data-origin-height=&quot;798&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBonsh/btrLLAo0JYp/kddDpKJAXKKdYH6vd9Oazk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBonsh/btrLLAo0JYp/kddDpKJAXKKdYH6vd9Oazk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBonsh/btrLLAo0JYp/kddDpKJAXKKdYH6vd9Oazk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBonsh%2FbtrLLAo0JYp%2FkddDpKJAXKKdYH6vd9Oazk%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;1860&quot; height=&quot;798&quot; data-origin-width=&quot;1860&quot; data-origin-height=&quot;798&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;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2010&quot; data-origin-height=&quot;586&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXSrGL/btrLTDcYTtD/QJW97va8RUZTLvIOTRxeyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXSrGL/btrLTDcYTtD/QJW97va8RUZTLvIOTRxeyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXSrGL/btrLTDcYTtD/QJW97va8RUZTLvIOTRxeyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXSrGL%2FbtrLTDcYTtD%2FQJW97va8RUZTLvIOTRxeyK%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;2010&quot; height=&quot;586&quot; data-origin-width=&quot;2010&quot; data-origin-height=&quot;586&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;이후 똑같은 인증번호로 인증 요청을 하게 되면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 상에서 삭제했기 때문에 만료되었다고 뜬다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;42&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JmFc2/btrLTVLqrKt/UZoaWyCxqXyNLyfwBNy9A1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JmFc2/btrLTVLqrKt/UZoaWyCxqXyNLyfwBNy9A1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JmFc2/btrLTVLqrKt/UZoaWyCxqXyNLyfwBNy9A1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJmFc2%2FbtrLTVLqrKt%2FUZoaWyCxqXyNLyfwBNy9A1%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;24&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;42&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1900&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vQkSc/btrLQxxv65k/mxpcM5ZhoRiKKkyjhPwrbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vQkSc/btrLQxxv65k/mxpcM5ZhoRiKKkyjhPwrbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vQkSc/btrLQxxv65k/mxpcM5ZhoRiKKkyjhPwrbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvQkSc%2FbtrLQxxv65k%2FmxpcM5ZhoRiKKkyjhPwrbk%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;1900&quot; height=&quot;590&quot; data-origin-width=&quot;1900&quot; data-origin-height=&quot;590&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A38TQ/btrLLVGpdw1/WkVk2u5kY7tUZ5PmPkUIz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A38TQ/btrLLVGpdw1/WkVk2u5kY7tUZ5PmPkUIz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A38TQ/btrLLVGpdw1/WkVk2u5kY7tUZ5PmPkUIz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA38TQ%2FbtrLLVGpdw1%2FWkVk2u5kY7tUZ5PmPkUIz0%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;199&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;582&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;첫 프로젝트에서 담당했던게 Node.js + 문자인증이었는데&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;</description>
      <category>  프로젝트/  내친소</category>
      <category>Sens</category>
      <category>smsapi</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>네이버SMS</category>
      <category>문자인증API</category>
      <category>스프링</category>
      <category>스프링부트</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/41</guid>
      <comments>https://gengminy.tistory.com/41#entry41comment</comments>
      <pubDate>Sun, 11 Sep 2022 14:15:57 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링 시큐리티 + JWT로 카카오 로그인 구현하기, 프론트엔드와 연결</title>
      <link>https://gengminy.tistory.com/40</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 번에 DefaultOAuth2Service 를 상속하여&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;그래서 이번에는 OAuth 인증 과정의 본질을 뜯어보면서 하나하나 구현해보았다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  이전 글 (DefaultOAuth2Service 이용)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/39&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://gengminy.tistory.com/39&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1662277886096&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;[Spring] OAuth2Service + 스프링 시큐리티 + JWT로 카카오 로그인 구현하기&quot; data-og-description=&quot;  OAuth &amp;quot;OpenID Authorization&amp;quot;의 약자 비밀번호를 제공하지 않으면서 웹사이트나 어플리케이션 접근 권한을 부여할 수 있는 로그인 방식 기존 아이디와 비밀번호를 통한 로그인 방식은 보안상 취약&quot; data-og-host=&quot;gengminy.tistory.com&quot; data-og-source-url=&quot;https://gengminy.tistory.com/39&quot; data-og-url=&quot;https://gengminy.tistory.com/39&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/QOEMj/hyPHiPdkIZ/IpkEKqolnG9jyxknKt9JsK/img.png?width=800&amp;amp;height=446&amp;amp;face=0_0_800_446,https://scrap.kakaocdn.net/dn/csDEIt/hyPHfLIT7o/lQk89EZ08owMjqeDW8yuJk/img.png?width=800&amp;amp;height=446&amp;amp;face=0_0_800_446,https://scrap.kakaocdn.net/dn/XHRAL/hyPFZwW8aA/y7iC7jKPteEjhbByI3xC7K/img.png?width=1812&amp;amp;height=1342&amp;amp;face=0_0_1812_1342&quot;&gt;&lt;a href=&quot;https://gengminy.tistory.com/39&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gengminy.tistory.com/39&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/QOEMj/hyPHiPdkIZ/IpkEKqolnG9jyxknKt9JsK/img.png?width=800&amp;amp;height=446&amp;amp;face=0_0_800_446,https://scrap.kakaocdn.net/dn/csDEIt/hyPHfLIT7o/lQk89EZ08owMjqeDW8yuJk/img.png?width=800&amp;amp;height=446&amp;amp;face=0_0_800_446,https://scrap.kakaocdn.net/dn/XHRAL/hyPFZwW8aA/y7iC7jKPteEjhbByI3xC7K/img.png?width=1812&amp;amp;height=1342&amp;amp;face=0_0_1812_1342');&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;[Spring] OAuth2Service + 스프링 시큐리티 + JWT로 카카오 로그인 구현하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  OAuth &quot;OpenID Authorization&quot;의 약자 비밀번호를 제공하지 않으면서 웹사이트나 어플리케이션 접근 권한을 부여할 수 있는 로그인 방식 기존 아이디와 비밀번호를 통한 로그인 방식은 보안상 취약&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gengminy.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;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;%F-%-F%-A%--%--Kakao%--Developers%--%EC%--%A-%EC%A-%--&quot; data-ke-size=&quot;size26&quot;&gt;  Kakao Developers 설정&lt;/h2&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.kakao.com/&quot;&gt;https://developers.kakao.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1662278055350&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;Kakao Developers&quot; data-og-description=&quot;카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&quot; data-og-host=&quot;developers.kakao.com&quot; data-og-source-url=&quot;https://developers.kakao.com/&quot; data-og-url=&quot;https://developers.kakao.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/sLy03/hyPAoii3aV/V3TvkkCCfTiZ0N1GGZbcLk/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/Q1XK6/hyPzrHI0ga/7nhM4pk0DH2G3RBkS2KNqK/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000,https://scrap.kakaocdn.net/dn/ml4Eq/hyPzvDnry6/Gvdx3bKEsi8iocw6Z28k2k/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000&quot;&gt;&lt;a href=&quot;https://developers.kakao.com/&quot; data-source-url=&quot;https://developers.kakao.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/sLy03/hyPAoii3aV/V3TvkkCCfTiZ0N1GGZbcLk/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/Q1XK6/hyPzrHI0ga/7nhM4pk0DH2G3RBkS2KNqK/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000,https://scrap.kakaocdn.net/dn/ml4Eq/hyPzvDnry6/Gvdx3bKEsi8iocw6Z28k2k/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000');&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;Kakao Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.kakao.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;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;715&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nnzzz/btrLhJfqQ0J/dtlTQuYM19U5sACzMz9Wm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nnzzz/btrLhJfqQ0J/dtlTQuYM19U5sACzMz9Wm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nnzzz/btrLhJfqQ0J/dtlTQuYM19U5sACzMz9Wm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnnzzz%2FbtrLhJfqQ0J%2FdtlTQuYM19U5sACzMz9Wm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;715&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;715&quot;/&gt;&lt;/span&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;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;966&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nv0U3/btrLiUN1C2F/Efus3A3wUepOsRXO9Qs4P0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nv0U3/btrLiUN1C2F/Efus3A3wUepOsRXO9Qs4P0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nv0U3/btrLiUN1C2F/Efus3A3wUepOsRXO9Qs4P0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnv0U3%2FbtrLiUN1C2F%2FEfus3A3wUepOsRXO9Qs4P0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;966&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;966&quot;/&gt;&lt;/span&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;이후 만든 어플리케이션 클릭해서 이동&lt;/p&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;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1007&quot; data-origin-height=&quot;695&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BnHy7/btrLiVTG9l3/6zCivLCoAz33UDvhoK7jYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BnHy7/btrLiVTG9l3/6zCivLCoAz33UDvhoK7jYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BnHy7/btrLiVTG9l3/6zCivLCoAz33UDvhoK7jYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBnHy7%2FbtrLiVTG9l3%2F6zCivLCoAz33UDvhoK7jYk%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;1007&quot; height=&quot;695&quot; data-origin-width=&quot;1007&quot; data-origin-height=&quot;695&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;REST API 키 나중에 사용해야 하니까 이 페이지 기억해두기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;947&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bU4PxG/btrLa3xRgxY/MKDvuMakNN0MNe9zV9bpb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bU4PxG/btrLa3xRgxY/MKDvuMakNN0MNe9zV9bpb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bU4PxG/btrLa3xRgxY/MKDvuMakNN0MNe9zV9bpb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbU4PxG%2FbtrLa3xRgxY%2FMKDvuMakNN0MNe9zV9bpb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;947&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;947&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;좌측 메뉴 카카오 로그인 -&amp;gt; 활성화 설정 상태 ON 으로 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 아래 Redirect URI 설정으로 진입&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1828&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPXEoF/btrLiAITdoQ/jN4oy9EdWA9kXra35wW000/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPXEoF/btrLiAITdoQ/jN4oy9EdWA9kXra35wW000/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPXEoF/btrLiAITdoQ/jN4oy9EdWA9kXra35wW000/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPXEoF%2FbtrLiAITdoQ%2FjN4oy9EdWA9kXra35wW000%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;1828&quot; height=&quot;382&quot; data-origin-width=&quot;1828&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;일단 로컬 환경에서 테스트를 진행해야 하기 때문에 로컬호스트로 설정&lt;br /&gt;리액트에서 localhost:3000 사용하니까 그 쪽으로 등록해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 localhost:8000 사용해서 서버 포트로 들어갔는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 리액트랑도 완벽하게 연결하기 위해 바꾸었다&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 시도할 때 이 URI 를 통해 인가 코드가 반환이 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 URI 은 클라이언트 쪽에서 사용해야 하니까 잘 기억해두기&lt;/p&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;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGhkaV/btrLhuCO9dJ/3bmWvptQKbzpD03i147750/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGhkaV/btrLhuCO9dJ/3bmWvptQKbzpD03i147750/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGhkaV/btrLhuCO9dJ/3bmWvptQKbzpD03i147750/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGhkaV%2FbtrLhuCO9dJ%2F3bmWvptQKbzpD03i147750%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;740&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;740&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;카카오 로그인 -&amp;gt; 동의항목 에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 를 통한 회원가입 진행 시 받아올 정보를 설정&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;%F-%-F%-A%--%--Kakao%--Developers%--%EC%--%A-%EC%A-%--&quot; data-ke-size=&quot;size26&quot;&gt;  인증 구조&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1699&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lrr8x/btrLiAIS8OU/NO1XzMC3Wk9HBboBwLo351/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lrr8x/btrLiAIS8OU/NO1XzMC3Wk9HBboBwLo351/img.png&quot; data-alt=&quot;이미지 출처 :&amp;amp;nbsp;https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lrr8x/btrLiAIS8OU/NO1XzMC3Wk9HBboBwLo351/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flrr8x%2FbtrLiAIS8OU%2FNO1XzMC3Wk9HBboBwLo351%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;600&quot; height=&quot;796&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1699&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미지 출처 :&amp;nbsp;https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api&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;클라이언트 &amp;lt;-&amp;gt; 백엔드 서버 &amp;lt;-&amp;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;본질은 같아서 하나만 구현해보면 나머지는 쉽게 할 수 있을 거 같다&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  프론트엔드 (React)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  KakaoLogin.js&lt;/h3&gt;
&lt;pre id=&quot;code_1662283432050&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const KakaoLogin = () =&amp;gt; {
  const location = useLocation();
  const param = useParams();

  const KAKAO_BASEURL = process.env.REACT_APP_KAKAO_BASEURL;
  const KAKAO_RESTAPI_KEY = process.env.REACT_APP_KAKAO_RESTAPI_KEY;
  const KAKAO_REDIRECT_URI = process.env.REACT_APP_KAKAO_REDIRECT_URI;

  const loginUri =
    'https://kauth.kakao.com/oauth/authorize?response_type=code&amp;amp;client_id=' +
    KAKAO_RESTAPI_KEY +
    '&amp;amp;redirect_uri=' +
    KAKAO_BASEURL +
    KAKAO_REDIRECT_URI;

  return (
    &amp;lt;&amp;gt;
      &amp;lt;NavBar /&amp;gt;
      &amp;lt;h1&amp;gt;카카오 로그인&amp;lt;/h1&amp;gt;
      &amp;lt;a href={loginUri}&amp;gt;카카오 로그인&amp;lt;/a&amp;gt;
    &amp;lt;/&amp;gt;
  );
};

export default KakaoLogin;&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;dotenv 파일에 다 담아두고 사용 중이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정보를 이용해서 카카오 로그인 요청 시 해당 url로 넘어가게 하면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 url 로 넘어가면 유저에게 카카오 계정 로그인을 요청하고&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;  KakaoProcess.js&lt;/h3&gt;
&lt;pre id=&quot;code_1662283625049&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const KakaoProcess = () =&amp;gt; {
  const params = new URL(document.location).searchParams;
  const code = params.get('code'); //쿼리파라미터 code 가져오기
  const setAccessToken = useCookies(['accessToken'])[1];

  useEffect(() =&amp;gt; {
    async function dispatchLogin() {
      try {
        AuthApi.requestKakaoLogin(code).then(res =&amp;gt; {
          if (res.status === 200) {
            const date = new Date();
            const { accessToken } = res.data.data;
            setAccessToken('accessToken', accessToken, {
              expires: new Date(date.setDate(date.getDate() + 3)),
              path: '/',
              secure: true,
            });
            console.log(res.data.data);
          }
        });
      } catch (error) {
        console.error(error);
      }
    }
    dispatchLogin();
  }, []);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;NavBar /&amp;gt;
      &amp;lt;h1&amp;gt;로그인 처리중입니다&amp;lt;/h1&amp;gt;
    &amp;lt;/&amp;gt;
  );
};

export default KakaoProcess;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오 계정 로그인이 완료되면 query parameter 로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 유저의 계정 코드가 같이 넘어오는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 캐치해서 백엔드 서버에 axios post 하는 방식으로 진행했다&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;88&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4qK1L/btrLkweWRj3/UHJHIlSyPTkVdTrVnmOQm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4qK1L/btrLkweWRj3/UHJHIlSyPTkVdTrVnmOQm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4qK1L/btrLkweWRj3/UHJHIlSyPTkVdTrVnmOQm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4qK1L%2FbtrLkweWRj3%2FUHJHIlSyPTkVdTrVnmOQm1%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;1210&quot; height=&quot;88&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;88&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 설정한 redirect url + code 가 넘어온다&lt;/p&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;  AuthApi.js&lt;/h3&gt;
&lt;pre id=&quot;code_1662283924529&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const AuthApi = {
  ...
  ...
  requestKakaoLogin: async body =&amp;gt; {
    const data = await axios.post(
      'http://localhost:8000/auth/login/kakao',
      body,
      { headers: { 'Content-Type': 'application/json' } }
    );
    return data;
  },
};

export default AuthApi;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 테스트 중이니 localhost:8000으로 날리는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서는 /auth/login/kakao 를 컨트롤러에서 받아서 처리한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간혹 content-type 이 urlencoded 방식이라고 안되는 경우가 있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헤더에 application/json 방식을 정의해주면 해결된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h2 data-ke-size=&quot;size26&quot;&gt;  백엔드 (Spring)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  KakaoController.java&lt;/h3&gt;
&lt;pre id=&quot;code_1662283858230&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Controller
@RequestMapping(&quot;/auth/login&quot;)
@RequiredArgsConstructor
public class KakaoController {

    private final KakaoService kakaoService;
    private final AuthService authService;
    private final UserRepository userRepository;
    private final JwtTokenProvider jwtTokenProvider;

    @ApiOperation(value = &quot;카카오 코드를 포함한 로그인 요청&quot;)
    @PostMapping(value = &quot;/kakao&quot;, produces = &quot;application/json; charset=utf-8&quot;)
    @ResponseBody
    public CommonApiResponse&amp;lt;TokenResponseDto&amp;gt; postKakaoLoginWithCode(
            @RequestBody KakaoRequest kakaoRequest,
            HttpServletResponse response
    ) {
        KakaoTokenDto kakaoTokenDto = kakaoService.getKakaoAccessToken(kakaoRequest.getCode());
        KakaoUserDto kakaoUserDto = kakaoService.getKakaoUser(kakaoTokenDto.getAccessToken());
        TokenResponseDto tokenResponseDto = authService.loginKakao(kakaoUserDto);
        return CommonApiResponse.of(tokenResponseDto);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리하는 엔드포인트는 /auth/login/kakao&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KakaoRequest 라는 Dto를 받아서 처리한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별건 없고 String code 를 가지고 있는 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;나머지 로직은 서비스 단에서 구현했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환값은 JWT 관련 Dto 인데&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;  KakaoRequest.java&lt;/h3&gt;
&lt;pre id=&quot;code_1662284102163&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class KakaoRequest {
    private String code;
}&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;프론트의 유저 code 를 가지고 있다&lt;/p&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;  KakaoService.java&lt;/h3&gt;
&lt;pre id=&quot;code_1662284217609&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
@Transactional(readOnly = false)
@Slf4j
public class KakaoService {

    private final WebClient webClient;
    private final UserRepository userRepository;

    @Value(&quot;${KAKAO_BASEURL}&quot;)
    private String KAKAO_BASEURL;
    @Value(&quot;${KAKAO_RESTAPI_KEY}&quot;)
    private String KAKAO_RESTPAPI_KEY;
    @Value(&quot;${KAKAO_REDIRECT_URI}&quot;)
    private String KAKAO_REDIRECT_URL;

    public KakaoTokenDto getKakaoAccessToken(String code) {
        String getTokenURL =
                &quot;https://kauth.kakao.com/oauth/token?grant_type=authorization_code&amp;amp;client_id=&quot;
                        + KAKAO_RESTPAPI_KEY + &quot;&amp;amp;redirect_uri=&quot; + KAKAO_BASEURL + KAKAO_REDIRECT_URL + &quot;&amp;amp;code=&quot;
                        + code;

        try {
            KakaoTokenDto kakaoTokenDto =
                    webClient.post()
                            .uri(getTokenURL)
                            .retrieve()
                            .bodyToMono(KakaoTokenDto.class).block();
            return kakaoTokenDto;
        } catch (Exception e) {
            e.printStackTrace();
            throw new BadRequestException(ErrorCode.KAKAO_BAD_REQUEST);
        }
    }

    public KakaoUserDto getKakaoUser(String kakaoAccessToken) {
        String getUserURL = &quot;https://kapi.kakao.com/v2/user/me&quot;;

        try {
            KakaoUserDto kakaoUserDto =
                    webClient.post()
                            .uri(getUserURL)
                            .header(&quot;Authorization&quot;, &quot;Bearer &quot; + kakaoAccessToken)
                            .retrieve()
                            .bodyToMono(KakaoUserDto.class)
                            .block();
            
            return kakaoUserDto;
        } catch (Exception e) {
            e.printStackTrace();
            throw new BadRequestException(ErrorCode.KAKAO_BAD_REQUEST);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getKakaoAccessToken 에서는&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 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;여기서는 서버에서도 프론트의 axios 와 같이&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;이 때 사용하는게 RestTemplete 과 WebClient 인데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestTemplete 은 지원이 끊기니 사용하지 않는게 좋다고 들어서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Webflux의 WebClient 를 사용했다&lt;/p&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;  WebClientConfig.java&lt;/h3&gt;
&lt;pre id=&quot;code_1662284505725&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient() {
        HttpClient httpClient = HttpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                .responseTimeout(Duration.ofMillis(5000))
                .doOnConnected(connection -&amp;gt; {
                    connection.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
                            .addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS));
                });

        WebClient webClient = WebClient.builder()
                .defaultHeader(HttpHeaders.CONTENT_TYPE, String.valueOf(MediaType.APPLICATION_JSON))
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();

        httpClient.warmup().block();

        return webClient;
    }
}&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;여러가지 WebClient 생성 관련 설정을 해줄 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성은 다음을 build.gradle 에 추가해주면 된다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;implementation 'org.springframework.boot:spring-boot-starter-webflux'&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 kakaoUserDto 에 저장했다&lt;/p&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;  KakaoUserDto.java&lt;/h3&gt;
&lt;pre id=&quot;code_1662284651908&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class KakaoUserDto {

    @JsonProperty(&quot;id&quot;)
    private String authenticationCode;

    @JsonProperty(&quot;connected_at&quot;)
    private Timestamp connectedAt;

    @JsonProperty(&quot;kakao_account&quot;)
    private KakaoAccount kakaoAccount;

    @JsonProperty(&quot;properties&quot;)
    private Properties properties;

    @Getter
    @ToString
    public static class KakaoAccount {
        private String email;
    }

    @Getter
    @ToString
    public static class Properties {
        private String nickname;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오 계정 로그인한 유저가 정보 제공에 동의했다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 정보들이 다 넘어와 각 필드에 저장된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;   AuthService.java&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1662284720209&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
...
  @Override
    @Transactional
    public TokenResponseDto loginKakao(KakaoUserDto kakaoUserDto) {
        String provider = &quot;kakao&quot;;

        log.info(&quot;로그인 시도&quot;);

        Optional&amp;lt;User&amp;gt; user = userRepository.findByEmailAndProvider(
                kakaoUserDto.getKakaoAccount().getEmail(),
                provider
        );

        if (user.isPresent()) {
            log.info(&quot;가입된 회원&quot;);
            /* 이미 가입된 회원 */
            TokenResponseDto tokenResponseDto = jwtTokenProvider.generateSocialToken(user.get());
            return tokenResponseDto;
        } else {
            /* 새로 가입할 회원 */

            String email = kakaoUserDto.getKakaoAccount().getEmail();
            String name = kakaoUserDto.getProperties().getNickname();

            User newUser = User.builder()
                    .email(email)
                    .name(name)
                    .role(UserRole.ROLE_USER)
                    .provider(provider)
                    .authenticationCode(kakaoUserDto.getAuthenticationCode())
                    .build();
            userRepository.save(newUser);

            log.info(&quot;새로운 회원&quot;);
            

            TokenResponseDto tokenResponseDto = jwtTokenProvider.generateSocialToken(newUser);
            return tokenResponseDto;
        }
    }
...
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이전에 만든 로그인 관련 서비스인 AuthService 에 일부 로직을 추가해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KakaoUserDto 를 받아서 해당 유저의 소셜 로그인을 처리한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 DB 상 유저가 있다면 로그인을 시키고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않다면 회원가입 시킨다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 해당 유저의 정보가 담긴 JWT를 생성해서 리턴해주면 된다&lt;/p&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2136&quot; data-origin-height=&quot;932&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctUHA6/btrLhuixKYE/VkSOgg68MkYmYXrTuyOuX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctUHA6/btrLhuixKYE/VkSOgg68MkYmYXrTuyOuX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctUHA6/btrLhuixKYE/VkSOgg68MkYmYXrTuyOuX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctUHA6%2FbtrLhuixKYE%2FVkSOgg68MkYmYXrTuyOuX0%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;2136&quot; height=&quot;932&quot; data-origin-width=&quot;2136&quot; data-origin-height=&quot;932&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;이를 리다이렉션 시켜서 다시 서비스를 시작하면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 구조 이해하기가 힘들어서 일단 구현하면서 부딪혀봤는데 꽤 어려웠다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 너무 추상화된 코드 보다는 이런식으로 하나하나 뜯어서 하는게 성에 차는 거 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 다른 소셜 로그인도 구현해 봐야지&lt;/p&gt;</description>
      <category>  백엔드/  Spring Boot</category>
      <category>JWT</category>
      <category>kakao</category>
      <category>OAuth</category>
      <category>oauth2</category>
      <category>Spring</category>
      <category>스프링</category>
      <category>카카오로그인</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/40</guid>
      <comments>https://gengminy.tistory.com/40#entry40comment</comments>
      <pubDate>Sun, 4 Sep 2022 18:51:14 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] OAuth2Service + 스프링 시큐리티 + JWT로 카카오 로그인 구현하기</title>
      <link>https://gengminy.tistory.com/39</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  OAuth&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;&lt;u&gt;&lt;/u&gt;&lt;b&gt;&lt;u&gt;O&lt;/u&gt;&lt;/b&gt;penID &lt;b&gt;&lt;u&gt;Auth&lt;/u&gt;&lt;/b&gt;orization&quot;의 약자&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;그러나 OAuth 를 사용하면 특정 접근 권한만 부여할 수도 있고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강력한 보안을 제공하는 대기업에 사용자 인증과 인가를 위임하는 방식으로 안전하게 로그인할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;%F-%-F%--%BB%--Project%--Dependency&quot; data-ke-size=&quot;size26&quot;&gt;  Dependency&lt;/h2&gt;
&lt;pre id=&quot;code_1661501536437&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation group: 'org.springframework.security', name: 'spring-security-oauth2-client', version: '5.6.3'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle 에 OAuth 관련 의존성을 추가해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;%F-%-F%--%BB%--%EC%--%A-%EC%A-%--%--%ED%-C%-C%EC%-D%BC&quot; data-ke-size=&quot;size26&quot;&gt;  Kakao Developers 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.kakao.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developers.kakao.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1661502481627&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;Kakao Developers&quot; data-og-description=&quot;카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&quot; data-og-host=&quot;developers.kakao.com&quot; data-og-source-url=&quot;https://developers.kakao.com/&quot; data-og-url=&quot;https://developers.kakao.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/sLy03/hyPAoii3aV/V3TvkkCCfTiZ0N1GGZbcLk/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/Q1XK6/hyPzrHI0ga/7nhM4pk0DH2G3RBkS2KNqK/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000,https://scrap.kakaocdn.net/dn/ml4Eq/hyPzvDnry6/Gvdx3bKEsi8iocw6Z28k2k/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000&quot;&gt;&lt;a href=&quot;https://developers.kakao.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.kakao.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/sLy03/hyPAoii3aV/V3TvkkCCfTiZ0N1GGZbcLk/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/Q1XK6/hyPzrHI0ga/7nhM4pk0DH2G3RBkS2KNqK/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000,https://scrap.kakaocdn.net/dn/ml4Eq/hyPzvDnry6/Gvdx3bKEsi8iocw6Z28k2k/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000');&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;Kakao Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.kakao.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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;828&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tE7jN/btrKE5Kdkoe/WNFOAH9GaXkdWb17pzzLAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tE7jN/btrKE5Kdkoe/WNFOAH9GaXkdWb17pzzLAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tE7jN/btrKE5Kdkoe/WNFOAH9GaXkdWb17pzzLAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtE7jN%2FbtrKE5Kdkoe%2FWNFOAH9GaXkdWb17pzzLAk%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;1482&quot; height=&quot;828&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;828&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1415&quot; data-origin-height=&quot;1068&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ADWJX/btrKGoBIOUf/Lipyd9dCuKp7UZy8quTUH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ADWJX/btrKGoBIOUf/Lipyd9dCuKp7UZy8quTUH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ADWJX/btrKGoBIOUf/Lipyd9dCuKp7UZy8quTUH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FADWJX%2FbtrKGoBIOUf%2FLipyd9dCuKp7UZy8quTUH0%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;1415&quot; height=&quot;1068&quot; data-origin-width=&quot;1415&quot; data-origin-height=&quot;1068&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;이후 만든 어플리케이션 클릭해서 이동&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1007&quot; data-origin-height=&quot;695&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TZ1Yv/btrKFd9hwXS/f4Pq9WwMOXzkKuJaGrmCMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TZ1Yv/btrKFd9hwXS/f4Pq9WwMOXzkKuJaGrmCMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TZ1Yv/btrKFd9hwXS/f4Pq9WwMOXzkKuJaGrmCMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTZ1Yv%2FbtrKFd9hwXS%2Ff4Pq9WwMOXzkKuJaGrmCMK%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;1007&quot; height=&quot;695&quot; data-origin-width=&quot;1007&quot; data-origin-height=&quot;695&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST API 키 나중에 사용해야 하니까 이 페이지 기억해두기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;1342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S38gd/btrKHeeqw9x/lnLrSj6xwhxnLRh4ReQ8o1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S38gd/btrKHeeqw9x/lnLrSj6xwhxnLRh4ReQ8o1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S38gd/btrKHeeqw9x/lnLrSj6xwhxnLRh4ReQ8o1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS38gd%2FbtrKHeeqw9x%2FlnLrSj6xwhxnLRh4ReQ8o1%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;1812&quot; height=&quot;1342&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;1342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌측 메뉴 카카오 로그인 -&amp;gt; 활성화 설정 상태 ON 으로 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 아래 Redirect URI 설정으로 진입&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1343&quot; data-origin-height=&quot;1144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMHBIP/btrKHu2o9PF/lmpUJkz6o4yQWKBCGh3xb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMHBIP/btrKHu2o9PF/lmpUJkz6o4yQWKBCGh3xb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMHBIP/btrKHu2o9PF/lmpUJkz6o4yQWKBCGh3xb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMHBIP%2FbtrKHu2o9PF%2FlmpUJkz6o4yQWKBCGh3xb0%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;1343&quot; height=&quot;1144&quot; data-origin-width=&quot;1343&quot; data-origin-height=&quot;1144&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;로그인 시도할 때 이 URI 를 통해 인가 코드가 반환이 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 URI 은 클라이언트 쪽에서 사용해야 하니까 잘 기억해두기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1890&quot; data-origin-height=&quot;1094&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bK18W1/btrKF1AbeNX/gWysbMg8Z1sJK57SYaMdlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bK18W1/btrKF1AbeNX/gWysbMg8Z1sJK57SYaMdlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bK18W1/btrKF1AbeNX/gWysbMg8Z1sJK57SYaMdlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbK18W1%2FbtrKF1AbeNX%2FgWysbMg8Z1sJK57SYaMdlk%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;1890&quot; height=&quot;1094&quot; data-origin-width=&quot;1890&quot; data-origin-height=&quot;1094&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오 로그인 -&amp;gt; 동의항목 에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 를 통한 회원가입 진행 시 받아올 정보를 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;%F-%-F%--%BB%--%EC%--%A-%EC%A-%--%--%ED%-C%-C%EC%-D%BC&quot; data-ke-size=&quot;size26&quot;&gt;  application.yml 설정&lt;/h2&gt;
&lt;h3 id=&quot;%F-%-F%--%-D%--SwaggerConfig-java&quot; data-ke-size=&quot;size23&quot;&gt;  application.yml&lt;/h3&gt;
&lt;pre id=&quot;code_1661501536438&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
...
  security:
    oauth2:
      client:
        registration:
          kakao:
            client-id: ${KAKAO_RESTAPI_KEY}
            redirect-uri: ${KAKAO_REDIRECT_URI}
            authorization-grant-type: authorization_code
            client-authentication-method: POST
            client-name: Kakao
            scope:
              - profile_nickname
              - account_email
        provider:
          kakao:
            authorization-uri: https://kauth.kakao.com/oauth/authorize
            token-uri: https://kauth.kakao.com/oauth/token
            user-info-uri: https://kapi.kakao.com/v2/user/me
            user-name-attribute: id
...
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 파일에서 security 부분을 추가해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 받은 rest api key 와 redirect uri 를 넣어주면 된다&lt;/p&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;h2 id=&quot;%F-%-F%--%BB%--%EC%--%A-%EC%A-%--%--%ED%-C%-C%EC%-D%BC&quot; data-ke-size=&quot;size26&quot;&gt;  OAuth2 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  CustomOAuth2Service&lt;/h3&gt;
&lt;pre id=&quot;code_1661527751862&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
@Service
public class CustomOAuth2Service extends DefaultOAuth2UserService {

    private final UserRepository userRepository;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);

        //provider 정보 (kakao, google, naver...)
        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        String userNameAttributeName = userRequest.getClientRegistration()
                .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
        log.info(&quot;registrationId = {}&quot;, registrationId);
        log.info(&quot;userNameAttributeName = {}&quot;, userNameAttributeName);

        //provider 정보 기반 객체 생성
        OAuth2Attribute oAuth2Attribute =
                OAuth2Attribute.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());

        final String email = oAuth2Attribute.getEmail();
        final String name = oAuth2Attribute.getName();
        final String provider = oAuth2Attribute.getProvider();

        if (userRepository.existsByEmail(email)) {
            log.info(&quot;가입된 회원입니다&quot;);
        } else {
            User user = User.builder()
                    .email(email)
                    .name(name)
                    .provider(provider)
                    .role(UserRole.ROLE_USER)
                    .build();

            userRepository.save(user);

            log.info(&quot;회원가입&quot;);
        }

        var memberAttribute = oAuth2Attribute.convertToMap();

        return new DefaultOAuth2User(
                Collections.singleton(new SimpleGrantedAuthority(&quot;ROLE_USER&quot;)),
                memberAttribute, &quot;email&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DefaultOAuth2UserService 를 구현한 클래스이다&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;OAuth2UserRequest 는 서드파티 서버로부터 사용자 정보를 받아올 수 있다&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;결과로는 UserPrincipal 을 반환하는데 세션 방식에서는 이 결과가 저장이 되지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 JWT 방식을 사용하기 때문에 무시된다&lt;/p&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;  OAuth2Attribute&lt;/h3&gt;
&lt;pre id=&quot;code_1661528517442&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ToString
@Builder(access = AccessLevel.PRIVATE)
@Getter
public class OAuth2Attribute {
    private Map&amp;lt;String, Object&amp;gt; attributes;
    private String attributeKey;
    private String email;
    private String name;
    private String provider;

    static OAuth2Attribute of(String provider, String attributeKey,
                              Map&amp;lt;String, Object&amp;gt; attributes) {
        switch (provider) {
            case &quot;google&quot;:
                return ofGoogle(attributeKey, attributes);
            case &quot;kakao&quot;:
                return ofKakao(&quot;email&quot;, attributes);
            case &quot;naver&quot;:
                return ofNaver(&quot;id&quot;, attributes);
            default:
                throw new RuntimeException();
        }
    }

    private static OAuth2Attribute ofGoogle(String attributeKey,
                                            Map&amp;lt;String, Object&amp;gt; attributes) {
        return OAuth2Attribute.builder()
                .name((String) attributes.get(&quot;name&quot;))
                .email((String) attributes.get(&quot;email&quot;))
                .provider(&quot;google&quot;)
                .attributes(attributes)
                .attributeKey(attributeKey)
                .build();
    }

    private static OAuth2Attribute ofKakao(String attributeKey,
                                           Map&amp;lt;String, Object&amp;gt; attributes) {
        Map&amp;lt;String, Object&amp;gt; kakaoAccount = (Map&amp;lt;String, Object&amp;gt;) attributes.get(&quot;kakao_account&quot;);
        Map&amp;lt;String, Object&amp;gt; kakaoProfile = (Map&amp;lt;String, Object&amp;gt;) kakaoAccount.get(&quot;profile&quot;);

        return OAuth2Attribute.builder()
                .name((String) kakaoProfile.get(&quot;nickname&quot;))
                .email((String) kakaoAccount.get(&quot;email&quot;))
                .provider(&quot;kakao&quot;)
                .attributes(kakaoAccount)
                .attributeKey(attributeKey)
                .build();
    }

    private static OAuth2Attribute ofNaver(String attributeKey,
                                           Map&amp;lt;String, Object&amp;gt; attributes) {
        Map&amp;lt;String, Object&amp;gt; response = (Map&amp;lt;String, Object&amp;gt;) attributes.get(&quot;response&quot;);

        return OAuth2Attribute.builder()
                .name((String) response.get(&quot;name&quot;))
                .email((String) response.get(&quot;email&quot;))
                .provider(&quot;naver&quot;)
//                .picture((String) response.get(&quot;profile_image&quot;))
                .attributes(response)
                .attributeKey(attributeKey)
                .build();
    }

    Map&amp;lt;String, Object&amp;gt; convertToMap() {
        Map&amp;lt;String, Object&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
        map.put(&quot;id&quot;, attributeKey);
        map.put(&quot;key&quot;, attributeKey);
        map.put(&quot;email&quot;, email);
        map.put(&quot;name&quot;, name);
        map.put(&quot;provider&quot;, provider);

        return map;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 를 지원하는 플랫폼 마다 이름들이 조금씩 달라져서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통합하여 관리하기 위한 클래스이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;provider에 따라서 자동으로 이름을 변환시켜서 값을 대입해준다&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;  OAuth2AuthenticationSuccessHandler&lt;/h3&gt;
&lt;pre id=&quot;code_1661528608444&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package Backend.HIFI.auth.oauth;

import Backend.HIFI.auth.dto.TokenResponseDto;
import Backend.HIFI.auth.jwt.JwtTokenProvider;
import Backend.HIFI.auth.security.UserAuthentication;
import Backend.HIFI.user.User;
import Backend.HIFI.user.UserRepository;
import Backend.HIFI.user.UserRole;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.stream.Collectors;

@Slf4j
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final JwtTokenProvider jwtTokenProvider;
    private final UserRepository userRepository;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        //로그인 성공한 사용자
        OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();

        final String email = oAuth2User.getName();
        final String provider = oAuth2User.getAttribute(&quot;provider&quot;);
        final String name = oAuth2User.getAttribute(&quot;name&quot;);
        final String authorities = oAuth2User.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(&quot;,&quot;));

        TokenResponseDto tokenResponseDto = jwtTokenProvider.generateOAuth2Token(oAuth2User);
        log.info(&quot;토큰 발행&quot;);

        String targetUrl = UriComponentsBuilder.fromUriString(&quot;/oauth2/redirect&quot;)
                        .queryParam(&quot;token&quot;, tokenResponseDto.getAccessToken())
                                .build().toUriString();
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CustomOAuth2Service 이후로 실행되는 핸들러 메서드&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;리프레시 토큰을 Redis 에 저장하는 로직은 아직 추가 안했는데&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;http://localhost:8000/oauth2/redirect?token={accessToken}&lt;/p&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;  SecurityConfiguration&lt;/h3&gt;
&lt;pre id=&quot;code_1661528810066&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SecurityConfiguration {
    ...
    ...
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                ...
                ...
                .oauth2Login()
                .defaultSuccessUrl(&quot;/login-success&quot;)
                .successHandler(oAuth2AuthenticationSuccessHandler)
                .userInfoEndpoint()
                .userService(customOAuth2Service);

                http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
                return http.build();
    }
    ...
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 스프링 시큐리티 필터 체인에 등록시켜주면 끝난다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;defaultSuccessUrl - 로그인 성공 시 이동할 url&lt;/li&gt;
&lt;li&gt;successHanlder - 인증 절차에 따라서 사용자가 정의한 로직을 실행시킴&lt;/li&gt;
&lt;li&gt;userInfoEndpoint - OAuth 로그인 성공 이후 행동 정의&lt;/li&gt;
&lt;li&gt;userService - customOAuth2Service 에서 후처리 하겠다고 선언&lt;/li&gt;
&lt;/ul&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: #000000;&quot;&gt;http://localhost:{서버포트}/oauth2/authorization/kakao 로 접속하면 카카오 로그인 화면이 뜬다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://dobi852.tistory.com/35?category=1025162&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://dobi852.tistory.com/35?category=1025162&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ws-pace.tistory.com/100&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://ws-pace.tistory.com/100&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://deeplify.dev/back-end/spring/oauth2-social-login&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://deeplify.dev/back-end/spring/oauth2-social-login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sudo-minz.tistory.com/78&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://sudo-minz.tistory.com/78&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@jkijki12/Spring-Boot-OAuth2-JWT-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EB%A6%AC%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://velog.io/@jkijki12/Spring-Boot-OAuth2-JWT-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EB%A6%AC%EA%B8%B0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  백엔드/  Spring Boot</category>
      <category>JWT</category>
      <category>OAuth</category>
      <category>oauth2</category>
      <category>Security</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>스프링</category>
      <category>스프링시큐리티</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/39</guid>
      <comments>https://gengminy.tistory.com/39#entry39comment</comments>
      <pubDate>Sat, 27 Aug 2022 00:53:23 +0900</pubDate>
    </item>
    <item>
      <title>[Gosrock] 고스락 티켓 2.0 프로젝트 관련 글 정리</title>
      <link>https://gengminy.tistory.com/38</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;rock.png&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WrRug/btrJSXZ5eTS/uXbk4sX5tk2UglublzkL20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WrRug/btrJSXZ5eTS/uXbk4sX5tk2UglublzkL20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WrRug/btrJSXZ5eTS/uXbk4sX5tk2UglublzkL20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWrRug%2FbtrJSXZ5eTS%2FuXbk4sX5tk2UglublzkL20%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;400&quot; data-filename=&quot;rock.png&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;400&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;h2 data-ke-size=&quot;size26&quot;&gt;  사용 언어 / 프레임워크&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Nodejs + Nestjs&lt;br /&gt;PostgreSQL&lt;br /&gt;Socket.io&lt;br /&gt;Redis&lt;br /&gt;Docker&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  관련 포스팅&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://gengminy.tistory.com/23&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Socket.io 사용하여 실시간 공연 입장 시스템 구현하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gengminy.tistory.com/36&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Guard 사용중인 Controller 내부 특정 메소드에 모든 접근 허가하기 (NoAuth)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gengminy.tistory.com/37&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PageDto를 이용한 페이지네이션 구현하기 (Paging)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✨ 웹 사이트 주소&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gosrock.band/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://gosrock.band/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1660714674437&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;22번째 정기공연 [We are GOSROCK, Invites you]&quot; data-og-host=&quot;gosrock.band&quot; data-og-source-url=&quot;https://gosrock.band/&quot; data-og-url=&quot;https://gosrock.band/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/d6IKP9/hyPsLeKBwj/N99k3aG2esvrrUZD8VNqM1/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400&quot;&gt;&lt;a href=&quot;https://gosrock.band/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gosrock.band/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/d6IKP9/hyPsLeKBwj/N99k3aG2esvrrUZD8VNqM1/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_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;22번째 정기공연 [We are GOSROCK, Invites you]&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gosrock.band&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;</description>
      <category>  프로젝트/  고스락 티켓</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/38</guid>
      <comments>https://gengminy.tistory.com/38#entry38comment</comments>
      <pubDate>Wed, 17 Aug 2022 14:38:20 +0900</pubDate>
    </item>
    <item>
      <title>[Gosrock/Nestjs] PageDto를 이용한 페이지네이션 구현하기 (Paging)</title>
      <link>https://gengminy.tistory.com/37</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;고스락 티켓 예매 페이지 22th 프로젝트의 일부인&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;특정 조건에 맞게 N개 가져올 필요가 있었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 페이지네이션을 제네릭을 이용하여 구현하게 되었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언제 들었는지 기억은 안나지만 이런 말이 문득 생각난다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 힘들수록 사용자는 편리해진다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지네이션도 그렇다&lt;/p&gt;
&lt;p 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;h2 data-ke-size=&quot;size26&quot;&gt;  PageOptionsDto 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  enum.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1660711260722&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enum PageOrder {
  ASC = 'ASC',
  DESC = 'DESC'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오름차순 / 내림차순 옵션을 위한 Enum 이다&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;  page-options.dto.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1660711208533&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export class PageOptionsDto {
  @ApiPropertyOptional({ enum: PageOrder, default: PageOrder.ASC })
  @IsEnum(PageOrder)
  @IsOptional()
  @Expose()
  readonly order: PageOrder = PageOrder.ASC;

  @ApiPropertyOptional({
    minimum: 1,
    default: 1
  })
  @Type(() =&amp;gt; Number)
  @IsInt()
  @Min(1)
  @Expose()
  readonly page: number = 1;

  @ApiPropertyOptional({
    minimum: 1,
    maximum: 50,
    default: 10
  })
  @Type(() =&amp;gt; Number)
  @IsInt()
  @Min(1)
  @Max(50)
  @Expose()
  readonly take: number = 10;

  get skip(): number {
    return (this.page - 1) * this.take;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;page-options.dto 는 Request, page.dto 는 Response 용이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Request 를 먼저 보내는게 순서니까 옵션부터 알아보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 옵션에는 order, page, take, skip 이 네 가지 옵션이 들어간다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;order&lt;/b&gt; : 오름차순 / 내림차순 정렬&lt;/li&gt;
&lt;li&gt;&lt;b&gt;page&lt;/b&gt; : 현재 가져올 페이지 번호&lt;/li&gt;
&lt;li&gt;&lt;b&gt;take&lt;/b&gt; :&amp;nbsp;한 페이지 당 몇 개의 원소를 가져올지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;skip&lt;/b&gt; : 탐색을 시작하는 원소의 위치 ({현재 페이지 -1} * 가져오는 원소 개수)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 각 멤버에 대해 validation 을 해주면 끝이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 integer 조건과 최소, 최대, 기본값을 지정해주었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;skip은 해당 멤버를 불러올 때마다 값이 달라지기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;get 으로 따로 처리했다&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  TicketFindDto 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  enum.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1660711668070&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enum PerformanceDate {
  YB = 'YB',
  OB = 'OB'
}

enum TicketStatus {
  DONE = '입장완료',
  ENTERWAIT = '입금확인',
  ORDERWAIT = '확인대기',
  EXPIRE = '기한만료'
}&lt;/code&gt;&lt;/pre&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;p data-ke-size=&quot;size16&quot;&gt;티켓 상태에 대한 enum 이다&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;  ticket-find.dto.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1660711577606&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export class TicketFindDto {
  @ApiProperty({
    description: '티켓 상태',
    enum: TicketStatus,
    required: false
  })
  @IsEnum(TicketStatus)
  @IsOptional()
  @Expose()
  readonly status: TicketStatus;

  @ApiProperty({
    description: '공연 날짜',
    enum: PerformanceDate,
    required: false
  })
  @IsEnum(PerformanceDate)
  @IsOptional()
  @Expose()
  readonly date: PerformanceDate;
}&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;여기서는 티켓의 상태와 공연 날짜를 추가로 조건으로 받았다&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;b&gt;required: false&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;h2 data-ke-size=&quot;size26&quot;&gt;  Ticket 모듈에 적용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  tickets.controller.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1660711754639&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @ApiOperation({
    summary: '[어드민]해당 조건의 티켓을 모두 불러온다'
  })
  @Get('/find')
  @Roles(Role.Admin)
  getTicketsWith(
    @Query() ticketFindDto: TicketFindDto,
    @Query() pageOptionsDto: PageOptionsDto
  ) {
    return this.ticketService.findAllWith(ticketFindDto, pageOptionsDto);
  }&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;이전 검색 결과를 유지하기 위해서 GET 메소드를 사용했고&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;아까 만든 TicketFindDto와 PageOptionsDto 를 레포지토리까지 넘겨주면 된다&lt;/p&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;  tickets.repository.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1660711890482&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  /**
   * 해당 ticketStatus를 참조하여 해당하는 Ticket 엔티티를 가지고 온다 (관리자용)
   * @param ticketStatus TicketStatus Enum
   * @param pageOptionsDto 페이지네이션 메타 정보
   */
  async findAllWith(
    ticketFindDto: TicketFindDto,
    pageOptionsDto: PageOptionsDto
  ): Promise&amp;lt;PageDto&amp;lt;Ticket&amp;gt;&amp;gt; {
    const { status, date } = ticketFindDto;
    const queryBuilder = this.ticketRepository.createQueryBuilder('ticket');

    //조건부 검색
    if (status) {
      queryBuilder.andWhere({ status });
    }
    if (date) {
      queryBuilder.andWhere({ date });
    }

    queryBuilder
      .orderBy('ticket.id', pageOptionsDto.order)
      .leftJoinAndSelect('ticket.user', 'user')
      .leftJoinAndSelect('ticket.admin', 'admin')
      .skip(pageOptionsDto.skip)
      .take(pageOptionsDto.take);

    const itemCount = await queryBuilder.getCount();
    const { entities } = await queryBuilder.getRawAndEntities();

    const pageMetaDto = new PageMetaDto({ pageOptionsDto, itemCount });

    //console.log(1);
    return new PageDto(entities, pageMetaDto);
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TicketFindDto 의 멤버를 찾아서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 있을 때만 queryBuilder 의 where 절에 추가시켜준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리를 날릴 때도 order, skip, take 조건이 있기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 pageOptionsDto 에서 찾아서 넣어준다&lt;/p&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;보통은 쿼리 빌더에서 getMany 나 getOne으로 가져오는 것과 달리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;getRawAndEntities&lt;/b&gt; 를 통해 &lt;b&gt;원시 데이터(raw data)&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;이건 내가 pageDto 구현부에서 데이터를 &lt;b&gt;제네릭&lt;/b&gt;으로 구현했기 때문이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 getMany 로 들고 오면 값이 안들어 가는걸 확인할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제네릭으로 구현했기 때문에 Ticket 엔티티에 제대로 매칭이 안된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 raw data를 뽑아내서 직접 넣어주면 된다&lt;/p&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;결과인 PageDto에 넣어주면 된다&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  PageDto, PageMetaDto 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  page-meta-dto.interface.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1660712765938&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { PageOptionsDto } from './page-options.dto';

export interface PageMetaDtoParameters {
  pageOptionsDto: PageOptionsDto;
  itemCount: number;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PageOptionsDto 와 아이템 결과의 개수를 가지고 있는&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;  page-meta.dto.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1660712783039&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export class PageMetaDto {
  @ApiProperty({ description: '페이지 정보입니다.' })
  @Expose()
  readonly page: number;

  @ApiProperty({ description: '몇개를 받아가는지 한페이지 당 원소갯수' })
  @Expose()
  readonly take: number;

  @ApiProperty({ description: '총 아이템 숫자 ( 검색 조건에 맞는 )' })
  @Expose()
  readonly itemCount: number;

  @ApiProperty({ description: '총 페이지 숫자 ( 검색 조건에 맞는 )' })
  @Expose()
  readonly pageCount: number;

  @ApiProperty({ description: '이전페이지가 있는지에 대한정보' })
  @Expose()
  readonly hasPreviousPage: boolean;

  @ApiProperty({ description: '다음페이지가 있는지에 대한 정보' })
  @Expose()
  readonly hasNextPage: boolean;

  constructor({ pageOptionsDto, itemCount }: PageMetaDtoParameters) {
    this.page = pageOptionsDto.page;
    this.take = pageOptionsDto.take;
    this.itemCount = itemCount;
    this.pageCount = Math.ceil(this.itemCount / this.take);
    this.hasPreviousPage = this.page &amp;gt; 1;
    this.hasNextPage = this.page &amp;lt; this.pageCount;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출할 때 사용한 PageOptionsDto 를 그대로 넣어주고&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  page.dto.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1660712950377&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export class PageDto&amp;lt;T&amp;gt; {
  @IsArray()
  @ApiProperty({ type: 'generic', isArray: true })
  @Expose()
  readonly data: T[];

  @ApiProperty({ type: () =&amp;gt; PageMetaDto })
  @Type(() =&amp;gt; PageMetaDto)
  @Expose()
  readonly meta: PageMetaDto;

  constructor(data: T[], meta: PageMetaDto) {
    this.data = data;
    this.meta = meta;
  }
}&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;아까 raw data 로 결과 엔티티를 넣어버린 이유도 제네릭 때문&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;Order, User 에서도 이 dto 정보를 사용중이다 하 하&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9Vo14/btrJRKzH9Rs/5MX8cutPcO71DadtI76dq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9Vo14/btrJRKzH9Rs/5MX8cutPcO71DadtI76dq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9Vo14/btrJRKzH9Rs/5MX8cutPcO71DadtI76dq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9Vo14%2FbtrJRKzH9Rs%2F5MX8cutPcO71DadtI76dq1%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;1006&quot; height=&quot;768&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;768&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;pre id=&quot;code_1660713260958&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;statusCode&quot;: &quot;상태코드&quot;,
  &quot;success&quot;: &quot;성공여부&quot;,
  &quot;data&quot;: {
    &quot;data&quot;: [
      {
        &quot;id&quot;: &quot;티켓 고유 식별번호입니다.&quot;,
        &quot;uuid&quot;: &quot;티켓의 고유 아이디(uuid) 입니다.&quot;,
        &quot;date&quot;: &quot;공연일자 입니다. (YB/OB)&quot;,
        &quot;status&quot;: &quot;티켓의 상태입니다. (입장대기/입장완료)&quot;,
        &quot;admin&quot;: {
          &quot;id&quot;: &quot;유저의 고유 아이디입니다.&quot;,
          &quot;name&quot;: &quot;유저의 입금자명입니다.&quot;,
          &quot;phoneNumber&quot;: &quot;유저의 휴대전화번호 입니다.&quot;,
          &quot;role&quot;: &quot;유저의 권한입니다.&quot;
        },
        &quot;user&quot;: {
          &quot;id&quot;: &quot;유저의 고유 아이디입니다.&quot;,
          &quot;name&quot;: &quot;유저의 입금자명입니다.&quot;,
          &quot;phoneNumber&quot;: &quot;유저의 휴대전화번호 입니다.&quot;,
          &quot;role&quot;: &quot;유저의 권한입니다.&quot;
        },
        &quot;createdAt&quot;: &quot;티켓 생성 일자&quot;
      }
    ],
    &quot;meta&quot;: {
      &quot;page&quot;: &quot;페이지 정보입니다.&quot;,
      &quot;take&quot;: &quot;몇개를 받아가는지 한페이지 당 원소갯수&quot;,
      &quot;itemCount&quot;: &quot;총 아이템 숫자 ( 검색 조건에 맞는 )&quot;,
      &quot;pageCount&quot;: &quot;총 페이지 숫자 ( 검색 조건에 맞는 )&quot;,
      &quot;hasPreviousPage&quot;: &quot;이전페이지가 있는지에 대한정보&quot;,
      &quot;hasNextPage&quot;: false
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 &lt;span&gt;데이터 필드에 결괏값 배열이 채워지고&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&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;b&gt;또 중요한게 결과가 없을 때에도 null 이 아닌 빈 배열을 주어야 한다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래야 프론트에서도 처리하기가 더 수월하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1055&quot; data-origin-height=&quot;1302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bosshc/btrJUZXGZLV/ZQB9FGykgHUKxKxv9IqQ7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bosshc/btrJUZXGZLV/ZQB9FGykgHUKxKxv9IqQ7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bosshc/btrJUZXGZLV/ZQB9FGykgHUKxKxv9IqQ7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbosshc%2FbtrJUZXGZLV%2FZQB9FGykgHUKxKxv9IqQ7k%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;1055&quot; height=&quot;1302&quot; data-origin-width=&quot;1055&quot; data-origin-height=&quot;1302&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;&lt;span&gt;생각보다 ORM 동작 원리에 대해 빠삭하게 알아야하고&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리 빌더로 쿼리 날리는 것까지 열심히 공부해야했던 구현이었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 막상 구현해보니 재미있었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;실력이 쑥쑥 늘어나는 느낌 &lt;/span&gt;&lt;/p&gt;</description>
      <category>  프로젝트/  고스락 티켓</category>
      <category>nest</category>
      <category>nestjs</category>
      <category>Node</category>
      <category>pagination</category>
      <category>paging</category>
      <category>페이지네이션</category>
      <category>페이징</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/37</guid>
      <comments>https://gengminy.tistory.com/37#entry37comment</comments>
      <pubDate>Wed, 17 Aug 2022 14:16:52 +0900</pubDate>
    </item>
    <item>
      <title>[Gosrock/Nestjs] Guard 사용중인 Controller 내부 특정 메소드에 모든 접근 허가하기 (NoAuth)</title>
      <link>https://gengminy.tistory.com/36</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;고스락 티켓 예매 페이지 22th 의 일부인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@NoAuth 데코레이터 구현에 관한 글입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 이 글은 Nestjs 에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthGuard 등의 &lt;b&gt;Custom Guard 구현에 대해 알고 있다고 가정&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;h2 data-ke-size=&quot;size26&quot;&gt;  Reference&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Guards - &lt;a href=&quot;https://jakekwak.gitbook.io/nestjs/overview/guards&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://jakekwak.gitbook.io/nestjs/overview/guards&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 고스락 티켓 프로젝트에서는 &lt;b&gt;AccessTokenGuard&lt;/b&gt; 라는 커스텀 가드를 구현했고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 Role 기반으로 엑세스 토큰에서 유저와 그 권한을 뽑아와서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 요청 메소드에 접근 권한이 있는지 확인하고 접근 인가 / 불가 처리를 하는 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 것은 고티켓 팀장님의 포스팅을 참고하십쇼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://devnm.tistory.com/16?category=1258201&quot;&gt;https://devnm.tistory.com/16?category=1258201&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1660708106937&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;[고스락 티켓 2.0] nest js 유저 role 기반 api 인가&quot; data-og-description=&quot;&amp;nbsp;어노테이션과 메타데이터를 이용해서 , 유저가 Admin인지 일반 User인지에따른 api 인가를 설정해보도록 하자. 이글을 통해서 얻어갈 수 있는점 nestjs SetMetadata 를 통해서 어노테이션과, 메타데이&quot; data-og-host=&quot;devnm.tistory.com&quot; data-og-source-url=&quot;https://devnm.tistory.com/16?category=1258201&quot; data-og-url=&quot;https://devnm.tistory.com/16&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fHkBO/hyPuG3XzgG/4XRCd7CsvgBrOCuTvX0iAK/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bSp8nK/hyPuNWjxCn/AHpUMaQ6lFlGcZMPPkCUC0/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bXnKQq/hyPuB2El3H/OG47WrVwc1JNyi5qyBPE60/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675&quot;&gt;&lt;a href=&quot;https://devnm.tistory.com/16?category=1258201&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devnm.tistory.com/16?category=1258201&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fHkBO/hyPuG3XzgG/4XRCd7CsvgBrOCuTvX0iAK/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bSp8nK/hyPuNWjxCn/AHpUMaQ6lFlGcZMPPkCUC0/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bXnKQq/hyPuB2El3H/OG47WrVwc1JNyi5qyBPE60/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675');&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;[고스락 티켓 2.0] nest js 유저 role 기반 api 인가&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;어노테이션과 메타데이터를 이용해서 , 유저가 Admin인지 일반 User인지에따른 api 인가를 설정해보도록 하자. 이글을 통해서 얻어갈 수 있는점 nestjs SetMetadata 를 통해서 어노테이션과, 메타데이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;devnm.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;h3 data-ke-size=&quot;size23&quot;&gt;  tickets.controller.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1660708142125&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ApiTags('tickets')
@ApiBearerAuth('accessToken')
@Controller('tickets')
@UseGuards(AccessTokenGuard)
export class TicketsController {
  constructor(private ticketService: TicketsService) {}
  ...
  ...
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 맡았던 구현 부분 중 티켓 컨트롤러의 예시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@UseGuards&lt;/b&gt; 데코레이터를 통해 구현해둔&amp;nbsp;&lt;b&gt;AccessTokenGuard&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;/p&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;개발자가 제일 싫어하는 구조? 라고 해야되나&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  @NoAuth&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  NoAuth.guard.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1660708436823&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { SetMetadata } from '@nestjs/common';

export const NoAuth = () =&amp;gt; SetMetadata('no-auth', true);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데코레이터를 하나 만드는 방법은 어렵지 않다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인자로 받은 애들을 특정 이름의 메타데이터로 변환 시켜주면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 특정 메소드에 &lt;b&gt;@NoAuth()&lt;/b&gt; 를 붙여주게 되면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 메소드의 메타데이터로 &lt;b&gt;'no-auth': true&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;h2 data-ke-size=&quot;size26&quot;&gt;  코드 수정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  AccessToken.guard.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1660708622532&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Injectable()
export class AccessTokenGuard implements CanActivate {
  constructor(private authService: AuthService, private reflector: Reflector) {}

  canActivate(
    context: ExecutionContext
  ): boolean | Promise&amp;lt;boolean&amp;gt; | Observable&amp;lt;boolean&amp;gt; {
    //@NoAuth 사용시 해당 부분에서 AccessTokenGuard 사용 해제시킴
    const noAuth = this.reflector.get&amp;lt;boolean&amp;gt;('no-auth', context.getHandler());
    if (noAuth) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    return this.validateRequest(request, context);
  }

  private async validateRequest(request: Request, context: ExecutionContext) {
  ...
  ...
  ...
}&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;b&gt;AuthGuard&lt;/b&gt; 라던지,,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 &lt;b&gt;AccessTokenGuard&lt;/b&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;269&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NATYP/btrJPlUwODF/DsdM8dn1uc699tNOXaBK70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NATYP/btrJPlUwODF/DsdM8dn1uc699tNOXaBK70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NATYP/btrJPlUwODF/DsdM8dn1uc699tNOXaBK70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNATYP%2FbtrJPlUwODF%2FDsdM8dn1uc699tNOXaBK70%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;906&quot; height=&quot;269&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;269&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nestjs 에서 커스텀 가드는 반드시 &lt;b&gt;CanActivate&lt;/b&gt; 를 구현해야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가드는 특정 요청을 가로챈 후에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CanActivate 의 값이 true 이면 요청을 승인하고 그렇지 않으면 거부한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 ExecutionContext 에서 getHandler 로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AccessTokenGuard를 사용중인 컨트롤러에서 메소드의 참조값을 가져온다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후&amp;nbsp;리플렉터로 여기서 우리가 구현했던 메타데이터인 'no-auth' 값을 뽑아온다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getHandler 로 가져온 메소드가 &lt;b&gt;@NoAuth()&lt;/b&gt; 데코레이터를 사용중이라면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'no-auth': true 일 것이고 그렇지 않다면 undefined, 즉 false 이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'no-auth': true 이면 가드를 즉시 통과시키고&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;h2 data-ke-size=&quot;size26&quot;&gt;  NoAuth 적용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  tickets.controller.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1660709055192&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
  @ApiOperation({
    summary: '[랜딩페이지] 티켓 개수를 반환한다'
  })
  @ApiResponse({
    status: 200,
    description: '요청 성공시',
    type: TicketCountDto
  })
  @NoAuth()
  @Get('/count')
  async getTicketCount() {
    const count = await this.ticketService.countTicket();
    return { count: count };
  }
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@NoAuth() 를 필요로 하는 메소드에 적용시켰다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메소드는 GET /count 요청을 날렸을 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 내부에 몇 장의 티켓이 있는지 반환해주는 메소드이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;1242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFSeSt/btrJRsMV9fE/db58vMB7ufGIieeQoZyeRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFSeSt/btrJRsMV9fE/db58vMB7ufGIieeQoZyeRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFSeSt/btrJRsMV9fE/db58vMB7ufGIieeQoZyeRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFSeSt%2FbtrJRsMV9fE%2Fdb58vMB7ufGIieeQoZyeRk%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;1510&quot; height=&quot;1242&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;1242&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;구현은 알고보면 매우 간단하지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nestjs 의 가드와 데코레이터의 동작 원리를 이해하지 못했다면 상당히 복잡해 보일 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무쪼록 더 까먹기 전에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼른 구현하면서 공부했던 것들을 정리해야겠다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이트가 궁금하다면~~~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gosrock.band/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://gosrock.band/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1660709270397&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;22번째 정기공연 [We are GOSROCK, Invites you]&quot; data-og-host=&quot;gosrock.band&quot; data-og-source-url=&quot;https://gosrock.band/&quot; data-og-url=&quot;https://gosrock.band/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/d6IKP9/hyPsLeKBwj/N99k3aG2esvrrUZD8VNqM1/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400&quot;&gt;&lt;a href=&quot;https://gosrock.band/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gosrock.band/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/d6IKP9/hyPsLeKBwj/N99k3aG2esvrrUZD8VNqM1/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_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;22번째 정기공연 [We are GOSROCK, Invites you]&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gosrock.band&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;</description>
      <category>  프로젝트/  고스락 티켓</category>
      <category>nest</category>
      <category>nestjs</category>
      <category>NOAUTH</category>
      <category>nodejs</category>
      <category>가드</category>
      <category>데코레이터</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/36</guid>
      <comments>https://gengminy.tistory.com/36#entry36comment</comments>
      <pubDate>Wed, 17 Aug 2022 13:09:47 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Swagger 연동 시 Unable to infer base url 접속 에러 (ResponseBodyAdvice)</title>
      <link>https://gengminy.tistory.com/35</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;323&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tLlAe/btrJvQMVeKT/9K9A8bBfVQqZipfKLtcez0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tLlAe/btrJvQMVeKT/9K9A8bBfVQqZipfKLtcez0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tLlAe/btrJvQMVeKT/9K9A8bBfVQqZipfKLtcez0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtLlAe%2FbtrJvQMVeKT%2F9K9A8bBfVQqZipfKLtcez0%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;612&quot; height=&quot;323&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;323&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Unable to infer base url. This is common when using dynamic servlet registration or when the API is behind an API Gateway. The base url is the root of where all the swagger resources are served. For e.g. if the api is available at http://example.org/api/v2/api-docs then&amp;nbsp;the&amp;nbsp;base&amp;nbsp;url&amp;nbsp;is&amp;nbsp;http://example.org/api/.&lt;br /&gt;Please&amp;nbsp;enter&amp;nbsp;the&amp;nbsp;location&amp;nbsp;manually:&amp;nbsp;&lt;/blockquote&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Project Dependency&lt;/h2&gt;
&lt;pre id=&quot;code_1660219972879&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
	id 'org.springframework.boot' version '2.7.1'
...
...
	implementation 'io.springfox:springfox-boot-starter:3.0.0'
	implementation 'io.springfox:springfox-swagger-ui:3.0.0'
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 2.7.1 그리고 Spring fox 3.0 사용 중이다&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;h2 data-ke-size=&quot;size26&quot;&gt;  설정 파일&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  SwaggerConfig.java&lt;/h3&gt;
&lt;pre id=&quot;code_1660220029282&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebMvc
public class SwaggerConfig {

    private ApiInfo swaggerInfo() {
        return new ApiInfoBuilder()
                .title(&quot;HIFI Api Docs&quot;)
                .version(&quot;0.0.1&quot;)
                .description(&quot;HIFI Api 문서입니다&quot;)
                .build();
    }

    @Bean
    public Docket swaggerApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .securityContexts(Arrays.asList(securityContext()))
                .securitySchemes(Arrays.asList(apiKey()))
                .ignoredParameterTypes(AuthenticationPrincipal.class)
                .consumes(getConsumeContentTypes())
                .produces(getProduceContentTypes())
                .apiInfo(swaggerInfo()).select()
                .apis(RequestHandlerSelectors.basePackage(&quot;Backend.HIFI&quot;))
                .paths(PathSelectors.any())
                .build();
    }

    private SecurityContext securityContext() {
        return SecurityContext.builder()
                .securityReferences(defaultAuth())
                .build();
    }
    private List&amp;lt;SecurityReference&amp;gt; defaultAuth() {
        AuthorizationScope authorizationScope = new AuthorizationScope(
                &quot;global&quot;,
                &quot;accessEverything&quot;
        );
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
        authorizationScopes[0] = authorizationScope;
        return Arrays.asList(new SecurityReference(&quot;Authorization&quot;, authorizationScopes));
    }
    private ApiKey apiKey() {
        return new ApiKey(&quot;Authorization&quot;, &quot;X-AUTH-TOKEN&quot;, &quot;header&quot;);
    }
    private Set&amp;lt;String&amp;gt; getConsumeContentTypes() {
        Set&amp;lt;String&amp;gt; consumes = new HashSet&amp;lt;&amp;gt;();
        consumes.add(&quot;application/json;charset=UTF-8&quot;);
        consumes.add(&quot;application/x-www-form-urlencoded&quot;);
        return consumes;
    }

    private Set&amp;lt;String&amp;gt; getProduceContentTypes() {
        Set&amp;lt;String&amp;gt; produces = new HashSet&amp;lt;&amp;gt;();
        produces.add(&quot;application/json;charset=UTF-8&quot;);
        return produces;
    }
}&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;h2 data-ke-size=&quot;size26&quot;&gt;  해결 방안 1 - 스프링 시큐리티 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  SecurityConfiguration.java&lt;/h3&gt;
&lt;pre id=&quot;code_1660220127681&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EnableWebSecurity
@RequiredArgsConstructor
@Configuration
public class SecurityConfiguration {
    private static final String[] SwaggerPatterns = {
            &quot;/swagger-resources/**&quot;,
            &quot;/swagger-ui.html&quot;,
            &quot;/v2/api-docs&quot;,
            &quot;/webjars/**&quot;
    };
    
    ...
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                ...
                ...
                .antMatchers(SwaggerPatterns).permitAll()
                ...
                ...

                http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
                return http.build();
    }&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;스프링 시큐리티 설정 파일에서 Swagger 가 사용 중인 url에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안 설정을 끄는 방법이 해결 방안 중 하나이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 위에 있는 4가지 url 패턴을 스프링 시큐리티에서 허용해주면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&quot;/swagger-resources/**&quot;,&lt;br /&gt;&quot;/swagger-ui.html&quot;,&lt;br /&gt;&quot;/v2/api-docs&quot;,&lt;br /&gt;&quot;/webjars/**&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  해결 방안 2 - @ControllerAdvice 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/64473435/controlleradvice-does-not-allow-swagger-ui-to-be-displayed&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://stackoverflow.com/questions/64473435/controlleradvice-does-not-allow-swagger-ui-to-be-displayed&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1660220263966&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;@ControllerAdvice does not allow Swagger UI to be displayed&quot; data-og-description=&quot;I just add a GlobalExceptionHandler with @ControllerAdvice and when i try to access Swagger UI at http://localhost:8080/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config#/ a get a message...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/64473435/controlleradvice-does-not-allow-swagger-ui-to-be-displayed&quot; data-og-url=&quot;https://stackoverflow.com/questions/64473435/controlleradvice-does-not-allow-swagger-ui-to-be-displayed&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/KJ5jC/hyPpIOQPXb/Nd0bsuKPvNDSE3xQvxSE40/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/64473435/controlleradvice-does-not-allow-swagger-ui-to-be-displayed&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/64473435/controlleradvice-does-not-allow-swagger-ui-to-be-displayed&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/KJ5jC/hyPpIOQPXb/Nd0bsuKPvNDSE3xQvxSE40/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;@ControllerAdvice does not allow Swagger UI to be displayed&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I just add a GlobalExceptionHandler with @ControllerAdvice and when i try to access Swagger UI at http://localhost:8080/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config#/ a get a message...&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;h3 data-ke-size=&quot;size23&quot;&gt;  ResponceAdvice.java&lt;/h3&gt;
&lt;pre id=&quot;code_1660220416422&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestControllerAdvice                                                                   
public class ResponseAdvice implements ResponseBodyAdvice&amp;lt;Object&amp;gt; {                     
                                                                                        
    @Override                                                                           
    public boolean supports(                                                            
            MethodParameter returnType,                                                 
            Class&amp;lt;? extends HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt; converterType)                     
    {                                                                                   
        return true;                                                                    
    }                                                                                   
                                                                                        
    @Override                                                                           
    public Object beforeBodyWrite(                                                      
            Object body,                                                                
            MethodParameter returnType,                                                 
            MediaType selectedContentType,                                              
            Class&amp;lt;? extends HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt; selectedConverterType,             
            ServerHttpRequest request, ServerHttpResponse response)                     
    {                                                                                   
        if (body instanceof ErrorResponse)                                              
            return ResponseUtil.onError((ErrorResponse) body);                          
        return ResponseUtil.onSuccess(body);                                            
    }                                                                                   
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 문제는 바로 이 ResponseBodyAdvice의 구현체로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@RestControllerAdvice 를 추가해서 발생했다&lt;/p&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;br /&gt;&lt;br /&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;편하려고 구현한 controller advice 에 뒷통수를 세게 맞았다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책 중 하나는 basePackage를 설정해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러 범위를 지정해주는 것이다&lt;/p&gt;
&lt;pre id=&quot;code_1660220622229&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestControllerAdvice(basePackages = {&quot;Backend.HIFI.controller&quot;})
public class ResponseAdvice implements ResponseBodyAdvice&amp;lt;Object&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;이렇게 스웨거 설정 파일이 없는 디렉토리만 지정해주면 잘 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 나는 도메인 기반 디렉토리 구조를 사용 중이라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 좋은 해결방안을 고민해봐야겠다&lt;/p&gt;</description>
      <category>  백엔드/  Spring Boot</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>springfox</category>
      <category>swagger</category>
      <category>버그</category>
      <category>스웨거</category>
      <category>스프링</category>
      <category>스프링부트</category>
      <category>트러블슈팅</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/35</guid>
      <comments>https://gengminy.tistory.com/35#entry35comment</comments>
      <pubDate>Thu, 11 Aug 2022 21:25:27 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링 시큐리티로 CORS와 preflight 설정하기</title>
      <link>https://gengminy.tistory.com/34</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티로 CORS 설정 삽질해서 해결한 과정을 올려본다&lt;/p&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;로컬 환경에서는 너무나도 잘 되다가 EC2에 띄웠을 때만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자꾸 계속 preflight 과정에서 CORS 이슈가 생겨서&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;그 과정에서 CORS 관련 공부는 된 거 같아서 긍정적으로 생각할란다&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  문제 상황&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIQygp/btrJqneocqT/VDNG9P2GrWeyddH6QKisEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIQygp/btrJqneocqT/VDNG9P2GrWeyddH6QKisEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIQygp/btrJqneocqT/VDNG9P2GrWeyddH6QKisEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIQygp%2FbtrJqneocqT%2FVDNG9P2GrWeyddH6QKisEK%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;1334&quot; height=&quot;170&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;170&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;preflight.png&quot; data-origin-width=&quot;1153&quot; data-origin-height=&quot;653&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BWxZi/btrJrPuvl62/pdVDZpk6YNtYsvOoobVyu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BWxZi/btrJrPuvl62/pdVDZpk6YNtYsvOoobVyu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BWxZi/btrJrPuvl62/pdVDZpk6YNtYsvOoobVyu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBWxZi%2FbtrJrPuvl62%2FpdVDZpk6YNtYsvOoobVyu1%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;1153&quot; height=&quot;653&quot; data-filename=&quot;preflight.png&quot; data-origin-width=&quot;1153&quot; data-origin-height=&quot;653&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주니어 개발자들을 벌벌 떨게 만드는 무시무시한 CORS 에러&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Access to XMLHttpRequest at '{SERVER}' from origin '{ANOTHER ORIGIN}' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.&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;b&gt;Postman&lt;/b&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;나도 그랬다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것도 브라우저에서 보내는 &lt;b&gt;Preflight&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;h2 data-ke-size=&quot;size26&quot;&gt;  CORS ?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Cross Origin Resource Sharing&lt;/b&gt;&lt;span&gt;&amp;nbsp;&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;b&gt;동일 출처 정책(SOP)&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘에는 API 서버와 클라이언트(리액트나 뷰)를 분리하기 때문에 자주 발생하는 에러이다,,,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YMufo/btrJre9edWF/3v05AW0iJL2kk6bWnUK7OK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YMufo/btrJre9edWF/3v05AW0iJL2kk6bWnUK7OK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YMufo/btrJre9edWF/3v05AW0iJL2kk6bWnUK7OK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYMufo%2FbtrJre9edWF%2F3v05AW0iJL2kk6bWnUK7OK%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;1024&quot; height=&quot;417&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;417&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS 에러를 해결하기 위해서는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 서버에서 클라이언트 URL에 대한 리소스 참고를 허용하도록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헤더에 &quot;Access-Control-Allow-Origin&quot; 값을 넣어 응답을 해줘야 한다.&lt;/p&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;GET, POST, HEAD&lt;/b&gt; 를 제외한 메소드를 사용&lt;/li&gt;
&lt;li data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink,&lt;/b&gt;&lt;br /&gt;&lt;b&gt;Save-Data, Viewport-Width, Width &lt;/b&gt;를 제외한 헤더를 사용&lt;/li&gt;
&lt;li data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Content-Type&lt;/b&gt; 에서 &lt;b&gt;application/x-www-form-urlencoded, mulitpart/form-data, text/plain&lt;/b&gt; 이외의 것을 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 2번과 3번에서 문제가 되는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 방식으로 로그인을 구현할 때 X-AUTH-TOKEN 또는 Authorization 헤더를 사용하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Content-Type 으로 application/json 을 보내는 경우가 많기 때문이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Preflight&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3al3n/btrJt3simep/sQkwEWTE0MEguLznniaDU1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3al3n/btrJt3simep/sQkwEWTE0MEguLznniaDU1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3al3n/btrJt3simep/sQkwEWTE0MEguLznniaDU1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3al3n%2FbtrJt3simep%2FsQkwEWTE0MEguLznniaDU1%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;698&quot; height=&quot;400&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;400&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;&quot;Access-Control-Allow-Origin&quot; 헤더로 해결할 수 없는 경우&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;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;OPTIONS&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메서드를 먼저 보내서 판단한 후 본 요청을 보내게 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;preflight&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;요청이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 만났던 에러는 바로 이 preflight 요청을 서버에서 처리하지 못해서&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;그러니까 결론적으로는 서버에서 preflight 요청을 처리할 수 있도록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OPTIONS 메서드를 허용해주어야 한다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  스프링 시큐리티 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  SecurityConfiguration.java&lt;/h3&gt;
&lt;pre id=&quot;code_1660198816644&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EnableWebSecurity
@RequiredArgsConstructor
@Configuration
public class SecurityConfiguration {
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .cors().configurationSource(corsConfigurationSource())
                .and()
                .csrf().disable()
                //예외처리 핸들러
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(jwtAccessDeniedHandler)
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .httpBasic().disable()
                //권한이 필요한 요청에 대한 설정
                .authorizeRequests()
                .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
                .antMatchers(&quot;/admin/**&quot;).hasAuthority(&quot;ROLE_ADMIN&quot;)
                .antMatchers(&quot;/user/**&quot;).authenticated()
                .anyRequest().permitAll()
                .and()
                .headers().frameOptions().disable();

                http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
                return http.build();
    }

    /** cors 설정 bean */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        configuration.addAllowedOriginPattern(&quot;*&quot;);
        configuration.addAllowedHeader(&quot;*&quot;);
        configuration.addAllowedMethod(&quot;*&quot;);
        configuration.addExposedHeader(&quot;x-auth-token&quot;);
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);

        source.registerCorsConfiguration(&quot;/**&quot;, configuration);
        return source;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC 말고 스프링 시큐리티를 사용중이라면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS 관련 설정을 전역적으로 할 수 있다&lt;/p&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 단에서 @CrossOrigin 에노테이션 박아서 사용하는 코드를 많이 보는데&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;여기서 핵심은 시큐리티 필터의 허용 Request 에 PreFlight 요청을 추가하는 것이다&lt;/p&gt;
&lt;pre id=&quot;code_1660198982095&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
.authorizeRequests()
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 CorsUtils 클래스에서 가져왔는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpMethod.OPTIONS 로 빼서 가져와도 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 CorsConfigurationSource 빈에서 CORS 관련 설정을 해주면 끝이다&lt;/p&gt;
&lt;pre id=&quot;code_1660199021445&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
        configuration.addAllowedOriginPattern(&quot;*&quot;);
        configuration.addAllowedHeader(&quot;*&quot;);
        configuration.addAllowedMethod(&quot;*&quot;);
        configuration.addExposedHeader(&quot;x-auth-token&quot;);
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);
...&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;addAllowedOrigin 과 setAllowCredentials(true)를 같이 사용할 수 없다고 오류가 뜬다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 메세지 그대로 addAllowedOriginPattern 으로 수정해주면 된다&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1343&quot; data-origin-height=&quot;201&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/egA6Zo/btrJuDNKVXG/sEfk55XF860LX3hbgEUvVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/egA6Zo/btrJuDNKVXG/sEfk55XF860LX3hbgEUvVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/egA6Zo/btrJuDNKVXG/sEfk55XF860LX3hbgEUvVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FegA6Zo%2FbtrJuDNKVXG%2FsEfk55XF860LX3hbgEUvVk%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;1343&quot; height=&quot;201&quot; data-origin-width=&quot;1343&quot; data-origin-height=&quot;201&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 이후 프론트단 리액트앱에서 axios 요청 날리니까 정상적으로 되는 모습&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;이상 스프링 시큐리티 CORS 와 Preflight 관련한 삽질의 흔적이었습니다&lt;/p&gt;</description>
      <category>  백엔드/  Spring Boot</category>
      <category>cors</category>
      <category>preflight</category>
      <category>Security</category>
      <category>Spring</category>
      <category>스프링</category>
      <category>스프링시큐리티</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/34</guid>
      <comments>https://gengminy.tistory.com/34#entry34comment</comments>
      <pubDate>Thu, 11 Aug 2022 15:29:27 +0900</pubDate>
    </item>
    <item>
      <title>[React] 로컬 환경에서 CORS 이슈 임시로 해결하기 + 스프링 CORS 설정</title>
      <link>https://gengminy.tistory.com/33</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mYx8x/btrJq30KlKL/rEqhk8jHdGjfkHQDCuIDp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mYx8x/btrJq30KlKL/rEqhk8jHdGjfkHQDCuIDp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mYx8x/btrJq30KlKL/rEqhk8jHdGjfkHQDCuIDp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmYx8x%2FbtrJq30KlKL%2FrEqhk8jHdGjfkHQDCuIDp0%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;1024&quot; height=&quot;417&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;417&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;결국 손 놨던 리액트까지 다시 공부중이다&lt;/p&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;분명 백엔드 서버에서 CORS 설정을 해놨는데도 자꾸 팅기는 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 적어보는 프론트 단에서 CORS 이슈 해결하는 방법&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;✨ 개발 환경&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spring Boot&lt;br /&gt;React&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  CORS ?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Cross Origin Resource Sharing&lt;/b&gt; 의 약자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 도메인에서 다른 도메인의 리소스에 접근할 수 있게 해주는 보안 메커니즘&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동일 출처 정책(SOP)&lt;/b&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;71&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eEqDLf/btrJpf1i4Bm/z4OzKYuGEeu2AkbiluPQF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eEqDLf/btrJpf1i4Bm/z4OzKYuGEeu2AkbiluPQF1/img.png&quot; data-alt=&quot;웹 개발을 처음 시작하는, 특히 프론트에게 있어서 지옥을 보여주는 메세지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eEqDLf/btrJpf1i4Bm/z4OzKYuGEeu2AkbiluPQF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeEqDLf%2FbtrJpf1i4Bm%2Fz4OzKYuGEeu2AkbiluPQF1%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;1338&quot; height=&quot;71&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;71&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;웹 개발을 처음 시작하는, 특히 프론트에게 있어서 지옥을 보여주는 메세지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 백엔드 서버 쪽에서 먼저 이 CORS를 허용해둬야&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;axios나 ajax를 이용한 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;h2 data-ke-size=&quot;size26&quot;&gt;  스프링 부트 서버에서 CORS 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  SecurityConfiguration.java&lt;/h3&gt;
&lt;pre id=&quot;code_1660126826629&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SecurityConfiguration {
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .cors().and()
                ...
                ...
                return http.build();
    }

    /** cors 설정 configuration bean */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        //모든 ip에 응답 허용
        configuration.addAllowedOriginPattern(&quot;*&quot;);
        configuration.addAllowedMethod(&quot;*&quot;);
        configuration.addAllowedHeader(&quot;*&quot;);
        //내 서버의 응답 json 을 javascript에서 처리할수 있게 하는것(axios 등)
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);

        source.registerCorsConfiguration(&quot;/**&quot;, configuration);
        return source;
    }
}&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;이곳에서 HttpSecurity의 CORS 사용 설정을 허용해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 CorsConfigurationSource 빈을 하나 만들어 설정해준다&lt;/p&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;왜냐면 모든 Origin 에 대하여 리소스 공유를 허용한 것이기 때문이다&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;s&gt;addAllowedOrigin&lt;/s&gt; 에 내 프론트 서버 Origin 만 허용해주는 게 옳다&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;(스프링 업데이트 되면서 addAllowedOrigin 이랑 setAllowCredentials 를 같이 사용할 수 없다&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;addAllowedOriginPattern으로 사용하자)&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그외 백엔드에서 설정하는 자세한 방법은 &lt;b&gt;&lt;a href=&quot;https://gengminy.tistory.com/34?category=940520&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&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;h2 data-ke-size=&quot;size26&quot;&gt;  리액트에서 CORS 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 나의 경우 이렇게 설정하더라도 CORS 이슈로 Axios 요청이 계속 거부되었다&lt;/p&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;package.json 에 프록시 설정을 해주는 것이다&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;  package.json&lt;/h3&gt;
&lt;pre id=&quot;code_1660127136007&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  ...
  &quot;proxy&quot;: &quot;https://api.examplesite.net&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;proxy를 추가해주고 http 통신을 할 사이트 도메인을 적어준다&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;  api.js&lt;/h3&gt;
&lt;pre id=&quot;code_1660127747044&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import axios from 'axios';

const AuthApi = {
  requestJoin: async () =&amp;gt; {
    const data = await axios.get('/join', {});

    console.log(data);
    return data;
  },
};

export default AuthApi;&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1335&quot; data-origin-height=&quot;217&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beJSoE/btrJreGPG4Y/xQAn2afPtLgJ8HjKsvkeg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beJSoE/btrJreGPG4Y/xQAn2afPtLgJ8HjKsvkeg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beJSoE/btrJreGPG4Y/xQAn2afPtLgJ8HjKsvkeg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeJSoE%2FbtrJreGPG4Y%2FxQAn2afPtLgJ8HjKsvkeg1%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;1335&quot; height=&quot;217&quot; data-origin-width=&quot;1335&quot; data-origin-height=&quot;217&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS 이슈가 해결되었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버로부터 올바른 응답을 받았다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 헤더에 아까 스프링 서버에서 설정해둔 CORS 관련 설정도 같이 있다&lt;/p&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;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;(수정) 위 방법은 로컬 개발환경에서만 가능하다&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;실제 AWS 서버 같은데에 올리면 다시 CORS 이슈가 발생한다&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;그리고 사실 CORS는 백엔드에서 수정해주는게 거의 맞다&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>  프론트엔드/  React.js</category>
      <category>cors</category>
      <category>proxy</category>
      <category>React</category>
      <category>리액트</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/33</guid>
      <comments>https://gengminy.tistory.com/33#entry33comment</comments>
      <pubDate>Wed, 10 Aug 2022 19:38:40 +0900</pubDate>
    </item>
    <item>
      <title>[AWS/Docker/Nginx] 단일 서버에서 도메인별 서비스 제공하기</title>
      <link>https://gengminy.tistory.com/32</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;635&quot; data-origin-height=&quot;345&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MaiGx/btrJlzlBcpr/i82u7nvAHEhpagJRfBaFE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MaiGx/btrJlzlBcpr/i82u7nvAHEhpagJRfBaFE1/img.png&quot; data-alt=&quot;처음엔 뭔 괴상한 기둥이 있길래 놀랐었다 AWS는 늘 이런 이미지를 쓴다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MaiGx/btrJlzlBcpr/i82u7nvAHEhpagJRfBaFE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMaiGx%2FbtrJlzlBcpr%2Fi82u7nvAHEhpagJRfBaFE1%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;635&quot; height=&quot;345&quot; data-origin-width=&quot;635&quot; data-origin-height=&quot;345&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;처음엔 뭔 괴상한 기둥이 있길래 놀랐었다 AWS는 늘 이런 이미지를 쓴다&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;&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;docker-compose 로 이미지 빌드가 제대로 안되어서&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;h2 data-ke-size=&quot;size26&quot;&gt;  작업 환경&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;AWS EC2 + ELB&lt;br /&gt;Docker&lt;br /&gt;Nginx&lt;br /&gt;Spring boot&lt;br /&gt;React&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  프로젝트 구조&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;347&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6nwbK/btrJpd9KS86/HztY3DnK7JZurSNNuCU4r1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6nwbK/btrJpd9KS86/HztY3DnK7JZurSNNuCU4r1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6nwbK/btrJpd9KS86/HztY3DnK7JZurSNNuCU4r1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6nwbK%2FbtrJpd9KS86%2FHztY3DnK7JZurSNNuCU4r1%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;250&quot; height=&quot;362&quot; data-origin-width=&quot;347&quot; data-origin-height=&quot;362&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;h2 data-ke-size=&quot;size26&quot;&gt;  도커 컴포즈 세팅&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  docker-compose.yml&lt;/h3&gt;
&lt;pre id=&quot;code_1660114515687&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3.7&quot;
services:

# redis 설정
  redis:
    image: &quot;redis:alpine&quot;
    network_mode: &quot;host&quot;

# 서버 설정
  backend:
    image: gengminy/hifi-dev:dev
    container_name: backend
    network_mode: &quot;host&quot;
    env_file:
      - .env

# 리액트 설정
  frontend:
    image: gengminy/hififront:main
    container_name: frontend
    network_mode: &quot;host&quot;

# nginx 설정
  nginx:
    depends_on:
      - backend
      - frontend
      - redis
    network_mode: &quot;host&quot;
    restart: always
    build:
      dockerfile: Dockerfile
      context: ./nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 서버에 배포용으로 올라가있는 도커 컴포즈 파일&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 모드를 기본 브릿지가 아닌 &lt;b&gt;호스트 모드&lt;/b&gt;로 사용하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 인스턴스에서 localhost로도 각 컨테이너에 접근할 수 있다&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;예를 들어 리액트 이미지를 만들때 포트가 3000번으로 세팅되어 있다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 인스턴스에서도 localhost:3000 으로 리액트 컨테이너에 접근할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx는 배포 레포지토리에 default.conf 파일과 도커파일을 내장시켜서&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;여담으로 이 Nginx 빌드하는 과정 때문에 장장 한나절을 삽질해버렸다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 세팅했을 때, Nginx 설정 파일을 건드렸다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;docker-compose up --build&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;h2 data-ke-size=&quot;size26&quot;&gt;  Nginx 세팅&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  default.conf&lt;/h3&gt;
&lt;pre id=&quot;code_1660114974691&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server{
    listen 80;
    server_name hifihifi.site;
    location / {
        proxy_pass http://localhost:3000;
        proxy_redirect     off;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

server{
    listen 80;
    server_name api.hifihifi.site;
    access_log  /var/log/nginx/access.log  backend;

    location / {
        proxy_pass http://localhost:8000;
        proxy_redirect     off;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

server{
    listen 80;
    server_name admin.hifihifi.site;
    location / {
        proxy_pass http://localhost:3100;
        proxy_redirect     off;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 요청은 EC2 인스턴스의 80번 포트로 들어오게 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;443번 포트를 사용하는 HTTPS 요청도 ELB 앞단에서 80번 포트로 포워딩 해주었다&lt;/p&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;EC2의 각 서비스별 포트로 포워딩 시켜준다&lt;/p&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;3000, 8000, 3100번 포트와 같은 애들을 노출시키지 않아도 되어서&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;h2 data-ke-size=&quot;size26&quot;&gt;  Route 53 세팅&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1637&quot; data-origin-height=&quot;1059&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/otiYa/btrJqnqfqrf/bmPMXQYf7k1kblYH244rpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/otiYa/btrJqnqfqrf/bmPMXQYf7k1kblYH244rpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/otiYa/btrJqnqfqrf/bmPMXQYf7k1kblYH244rpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FotiYa%2FbtrJqnqfqrf%2FbmPMXQYf7k1kblYH244rpk%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;1637&quot; height=&quot;1059&quot; data-origin-width=&quot;1637&quot; data-origin-height=&quot;1059&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 Route 53에 도메인 등록을 했다고 가정하고&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;b&gt;****.com&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;api.****.com&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;admin.****.com&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 사용할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 다른 사이트로 단순 라우팅을 해주는거라면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값에다가 해당 서비스의 CNAME 이나 IP 등을 입력&lt;/p&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1633&quot; data-origin-height=&quot;978&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1Dmlf/btrJn636ojp/wF8IkriHDgt1vRwkvzfVIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1Dmlf/btrJn636ojp/wF8IkriHDgt1vRwkvzfVIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1Dmlf/btrJn636ojp/wF8IkriHDgt1vRwkvzfVIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1Dmlf%2FbtrJn636ojp%2FwF8IkriHDgt1vRwkvzfVIk%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;1633&quot; height=&quot;978&quot; data-origin-width=&quot;1633&quot; data-origin-height=&quot;978&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;레코드 생성을 눌러서 등록 완료하고 몇 분 기다리면 라우팅이 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 단일 EC2 서버에서 여러 서비스를 제공할 수 있으니&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;</description>
      <category>  Infra/  AWS</category>
      <category>AWS</category>
      <category>docker</category>
      <category>nginx</category>
      <category>Route53</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/32</guid>
      <comments>https://gengminy.tistory.com/32#entry32comment</comments>
      <pubDate>Wed, 10 Aug 2022 16:17:00 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] Elastic Load Balancer(ELB)로 https 설정하기</title>
      <link>https://gengminy.tistory.com/31</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zIW8W/btrJlh5riOt/mFXwhmLyxww6C7huuBR5lk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zIW8W/btrJlh5riOt/mFXwhmLyxww6C7huuBR5lk/img.png&quot; data-alt=&quot;웃고 있는게 조금이라도 실수하면 바로 과금을 때려버리겠다는 사악한 미소 같다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zIW8W/btrJlh5riOt/mFXwhmLyxww6C7huuBR5lk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzIW8W%2FbtrJlh5riOt%2FmFXwhmLyxww6C7huuBR5lk%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;500&quot; height=&quot;375&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;웃고 있는게 조금이라도 실수하면 바로 과금을 때려버리겠다는 사악한 미소 같다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 구매해서 백엔드 서버 연결 완료한 이후&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;자꾸 잘 안되길래 https 설정이 안되어서 그런 거 같아&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;  HTTPS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HTTP Secure&lt;/b&gt; 의 약자로 HTTP 프로토콜을 암호화한 버전이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSL이나 TLS를 이용해서 클라이언트와 서버 간의 모든 커뮤니케이션을 암호화하여&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;그래서 가끔 HTTPS 지원 안하고 HTTP만 사용하는 웹사이트들은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안오류 뜨면서 접근을 차단하는 경우가 꽤 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bg8NeO/btrJgbd4umB/QhZFCAXMKQeSKwFOdiaip1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bg8NeO/btrJgbd4umB/QhZFCAXMKQeSKwFOdiaip1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bg8NeO/btrJgbd4umB/QhZFCAXMKQeSKwFOdiaip1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbg8NeO%2FbtrJgbd4umB%2FQhZFCAXMKQeSKwFOdiaip1%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;470&quot; height=&quot;186&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;186&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPS 설정이 되어있다면 이렇게 URL 옆에 자물쇠가 뜬다 (크롬 기준)&lt;/p&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;✨ SSL 인증서 발급&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 HTTPS를 사용하기 위해 SSL(Secure Socket Layer)을 발급 받는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 공인 기업 CA(Certificate Authority)에서 발급해주며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대칭키와 공개키 방식을 동시에 사용한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트와 서버는 SSL 핸드쉐이크를 통해 HTTPS 통신을 시작한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 SSL 인증서를 발급 받아야 HTTPS를 사용할 수 있는데&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;AWS 서비스를 사용할 경우 &lt;b&gt;무료&lt;/b&gt;로 발급받을 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 &lt;u&gt;이 인증서는 AWS 내에서만 사용가능&lt;/u&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1541&quot; data-origin-height=&quot;451&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lbegO/btrJlV8IwlQ/X5FXjL3Rw1d9iWgwcc0KpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lbegO/btrJlV8IwlQ/X5FXjL3Rw1d9iWgwcc0KpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lbegO/btrJlV8IwlQ/X5FXjL3Rw1d9iWgwcc0KpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlbegO%2FbtrJlV8IwlQ%2FX5FXjL3Rw1d9iWgwcc0KpK%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;1541&quot; height=&quot;451&quot; data-origin-width=&quot;1541&quot; data-origin-height=&quot;451&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Certificate Manager (ACM) 으로 이동&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1619&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfLCnc/btrJlhYGCU4/3QBMKEqQLk3yMTfZsEhkek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfLCnc/btrJlhYGCU4/3QBMKEqQLk3yMTfZsEhkek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfLCnc/btrJlhYGCU4/3QBMKEqQLk3yMTfZsEhkek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfLCnc%2FbtrJlhYGCU4%2F3QBMKEqQLk3yMTfZsEhkek%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;1619&quot; height=&quot;393&quot; data-origin-width=&quot;1619&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1561&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhjuSJ/btrJfU4KNbO/2WqkdxjktgzzrNiJOu7UR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhjuSJ/btrJfU4KNbO/2WqkdxjktgzzrNiJOu7UR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhjuSJ/btrJfU4KNbO/2WqkdxjktgzzrNiJOu7UR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhjuSJ%2FbtrJfU4KNbO%2F2WqkdxjktgzzrNiJOu7UR0%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;1561&quot; height=&quot;622&quot; data-origin-width=&quot;1561&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1610&quot; data-origin-height=&quot;1409&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yO5c6/btrJj5YLKt1/18M56HVG19K0kF2zPbpu1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yO5c6/btrJj5YLKt1/18M56HVG19K0kF2zPbpu1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yO5c6/btrJj5YLKt1/18M56HVG19K0kF2zPbpu1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyO5c6%2FbtrJj5YLKt1%2F18M56HVG19K0kF2zPbpu1K%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;1610&quot; height=&quot;1409&quot; data-origin-width=&quot;1610&quot; data-origin-height=&quot;1409&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;*.example.com 이런식으로 와일드카드를 매치하면&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;검증 방법은 DNS 검증으로&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1571&quot; data-origin-height=&quot;1179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6ymHR/btrJmtcY2bb/lCgnjAOuvRVHCOwgTuDO81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6ymHR/btrJmtcY2bb/lCgnjAOuvRVHCOwgTuDO81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6ymHR/btrJmtcY2bb/lCgnjAOuvRVHCOwgTuDO81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6ymHR%2FbtrJmtcY2bb%2FlCgnjAOuvRVHCOwgTuDO81%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;1571&quot; height=&quot;1179&quot; data-origin-width=&quot;1571&quot; data-origin-height=&quot;1179&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;발급 받은 인증서로 들어가면 검증 보류중이라고 뜰 것이다&lt;/p&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;도메인 -&amp;gt; Route 53에서 레코드 생성&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;CNAME 을 Route 53에 등록해주면 바로 사용할 수 있다&lt;/p&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;  ELB 생성 및 설정&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1704&quot; data-origin-height=&quot;1415&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bG51pP/btrJm8fst8O/j8UK6rYWtjnCKT0UkwJMl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bG51pP/btrJm8fst8O/j8UK6rYWtjnCKT0UkwJMl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bG51pP/btrJm8fst8O/j8UK6rYWtjnCKT0UkwJMl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbG51pP%2FbtrJm8fst8O%2Fj8UK6rYWtjnCKT0UkwJMl0%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;1704&quot; height=&quot;1415&quot; data-origin-width=&quot;1704&quot; data-origin-height=&quot;1415&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 Console -&amp;gt; 로드 밸런싱 -&amp;gt; 로드밸런서 -&amp;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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1609&quot; data-origin-height=&quot;1607&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BCBPB/btrJj5Etvaj/abkaJsk8ciJ9PYvTx0oas0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BCBPB/btrJj5Etvaj/abkaJsk8ciJ9PYvTx0oas0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BCBPB/btrJj5Etvaj/abkaJsk8ciJ9PYvTx0oas0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBCBPB%2FbtrJj5Etvaj%2FabkaJsk8ciJ9PYvTx0oas0%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;1609&quot; height=&quot;1607&quot; data-origin-width=&quot;1609&quot; data-origin-height=&quot;1607&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application Load Balancer 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서는 &lt;b&gt;AWS 가입 후 1년 간 월 750시간 무료&lt;/b&gt;다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 랑 기준은 똑같다&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4z4Rh/btrJhiYLNCv/e0yH7RkshKSrkYgX287uKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4z4Rh/btrJhiYLNCv/e0yH7RkshKSrkYgX287uKk/img.png&quot; data-alt=&quot;프리티어 기준 중 ELB&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4z4Rh/btrJhiYLNCv/e0yH7RkshKSrkYgX287uKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4z4Rh%2FbtrJhiYLNCv%2Fe0yH7RkshKSrkYgX287uKk%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;1430&quot; height=&quot;294&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;294&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프리티어 기준 중 ELB&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1609&quot; data-origin-height=&quot;1144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oGSsv/btrJhys8z8J/5JPdNnoo3KTkDHllnDfbB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oGSsv/btrJhys8z8J/5JPdNnoo3KTkDHllnDfbB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oGSsv/btrJhys8z8J/5JPdNnoo3KTkDHllnDfbB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoGSsv%2FbtrJhys8z8J%2F5JPdNnoo3KTkDHllnDfbB0%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;1609&quot; height=&quot;1144&quot; data-origin-width=&quot;1609&quot; data-origin-height=&quot;1144&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;우리는 http로 들어오는 트래픽에 대해 처리를 해주어야 하니까 Internet-facing&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1550&quot; data-origin-height=&quot;1418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cg9Jrm/btrJm7gydlg/VnMP83yZEFDKbsdluQ0HKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cg9Jrm/btrJm7gydlg/VnMP83yZEFDKbsdluQ0HKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cg9Jrm/btrJm7gydlg/VnMP83yZEFDKbsdluQ0HKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcg9Jrm%2FbtrJm7gydlg%2FVnMP83yZEFDKbsdluQ0HKK%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;1550&quot; height=&quot;1418&quot; data-origin-width=&quot;1550&quot; data-origin-height=&quot;1418&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;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1556&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qcmh5/btrJlHv6aqR/ZPnq6WkBQOaSK8N67rgQyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qcmh5/btrJlHv6aqR/ZPnq6WkBQOaSK8N67rgQyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qcmh5/btrJlHv6aqR/ZPnq6WkBQOaSK8N67rgQyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqcmh5%2FbtrJlHv6aqR%2FZPnq6WkBQOaSK8N67rgQyK%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;1556&quot; height=&quot;380&quot; data-origin-width=&quot;1556&quot; data-origin-height=&quot;380&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;로드밸런서 용으로 따로 파줘도 되고&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;대신 새로 만들 경우 80번 포트와 443번 포트를 오픈해야한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1549&quot; data-origin-height=&quot;862&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K9ZKS/btrJj5Et47B/glhcyrtkDfTEUmhPUJJBxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K9ZKS/btrJj5Et47B/glhcyrtkDfTEUmhPUJJBxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K9ZKS/btrJj5Et47B/glhcyrtkDfTEUmhPUJJBxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK9ZKS%2FbtrJj5Et47B%2FglhcyrtkDfTEUmhPUJJBxk%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;1549&quot; height=&quot;862&quot; data-origin-width=&quot;1549&quot; data-origin-height=&quot;862&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 와 HTTPS 를 로드밸런서에서 오픈&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1644&quot; data-origin-height=&quot;1443&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JRXWu/btrJhkoD1ie/ndVeV195sx1Sk9ys9fYQtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JRXWu/btrJhkoD1ie/ndVeV195sx1Sk9ys9fYQtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JRXWu/btrJhkoD1ie/ndVeV195sx1Sk9ys9fYQtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJRXWu%2FbtrJhkoD1ie%2FndVeV195sx1Sk9ys9fYQtk%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;1644&quot; height=&quot;1443&quot; data-origin-width=&quot;1644&quot; data-origin-height=&quot;1443&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;이름 예쁘게 지어주고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP로 포트 지정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 이 그룹에 속한 인스턴스의 80번 포트로 요청을 처리하겠다는 의미&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1583&quot; data-origin-height=&quot;1415&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XivJN/btrJlHizAEz/RiJIxhtsbLfCLX8W6VmQrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XivJN/btrJlHizAEz/RiJIxhtsbLfCLX8W6VmQrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XivJN/btrJlHizAEz/RiJIxhtsbLfCLX8W6VmQrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXivJN%2FbtrJlHizAEz%2FRiJIxhtsbLfCLX8W6VmQrK%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;1583&quot; height=&quot;1415&quot; data-origin-width=&quot;1583&quot; data-origin-height=&quot;1415&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;pending 시켜주고 Create target group 눌러주면 설정 끝&lt;/p&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1543&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mHSsP/btrJhyfBI1L/cLPsIIgInWhG5zBack3tB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mHSsP/btrJhyfBI1L/cLPsIIgInWhG5zBack3tB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mHSsP/btrJhyfBI1L/cLPsIIgInWhG5zBack3tB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmHSsP%2FbtrJhyfBI1L%2FcLPsIIgInWhG5zBack3tB0%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;1543&quot; height=&quot;904&quot; data-origin-width=&quot;1543&quot; data-origin-height=&quot;904&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽에서 아까 만들어준 SSL 인증서를 선택할 수 있는데&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1586&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csJrWM/btrJhyfBJ1i/r2KG5rawXg4NeMPcHDTmkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csJrWM/btrJhyfBJ1i/r2KG5rawXg4NeMPcHDTmkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csJrWM/btrJhyfBJ1i/r2KG5rawXg4NeMPcHDTmkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsJrWM%2FbtrJhyfBJ1i%2Fr2KG5rawXg4NeMPcHDTmkk%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;1586&quot; height=&quot;482&quot; data-origin-width=&quot;1586&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Create Load balancer&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1740&quot; data-origin-height=&quot;575&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lyzbn/btrJlU9O8NH/sWS49ckRmBi3xsv6tWsiCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lyzbn/btrJlU9O8NH/sWS49ckRmBi3xsv6tWsiCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lyzbn/btrJlU9O8NH/sWS49ckRmBi3xsv6tWsiCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flyzbn%2FbtrJlU9O8NH%2FsWS49ckRmBi3xsv6tWsiCk%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;1740&quot; height=&quot;575&quot; data-origin-width=&quot;1740&quot; data-origin-height=&quot;575&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 로드 밸런서 메뉴에서 리스너 탭 -&amp;gt; &lt;b&gt;HTTP: 80&lt;/b&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;1107&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wezrV/btrJk28BFKl/V3ukMg66yhoCDRt5QytPG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wezrV/btrJk28BFKl/V3ukMg66yhoCDRt5QytPG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wezrV/btrJk28BFKl/V3ukMg66yhoCDRt5QytPG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwezrV%2FbtrJk28BFKl%2FV3ukMg66yhoCDRt5QytPG0%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;1728&quot; height=&quot;1107&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;1107&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPS 포트인 443으로 리디렉션 시킴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스너 규칙에서 포트포워딩, HTTP 고정 응답, 특정 IP만 허용 등등&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1724&quot; data-origin-height=&quot;1134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bshTpR/btrJljvqfLA/IbZW0P3kzSDhxEbs5JWDo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bshTpR/btrJljvqfLA/IbZW0P3kzSDhxEbs5JWDo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bshTpR/btrJljvqfLA/IbZW0P3kzSDhxEbs5JWDo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbshTpR%2FbtrJljvqfLA%2FIbZW0P3kzSDhxEbs5JWDo1%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;1724&quot; height=&quot;1134&quot; data-origin-width=&quot;1724&quot; data-origin-height=&quot;1134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 HTTP:443의 규칙으로 들어옴&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;  Route 53 에서 로드밸런서 등록&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;473&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kzzg8/btrJncWoSV0/EaOOkmxNCHGb6m1Fzyw4tK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kzzg8/btrJncWoSV0/EaOOkmxNCHGb6m1Fzyw4tK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kzzg8/btrJncWoSV0/EaOOkmxNCHGb6m1Fzyw4tK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fkzzg8%2FbtrJncWoSV0%2FEaOOkmxNCHGb6m1Fzyw4tK%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;1430&quot; height=&quot;473&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;473&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 Route 53으로 이동한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nJCB1/btrJmtjOn6j/JAEvTR7Sluqlgcf9wtJKe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nJCB1/btrJmtjOn6j/JAEvTR7Sluqlgcf9wtJKe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nJCB1/btrJmtjOn6j/JAEvTR7Sluqlgcf9wtJKe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnJCB1%2FbtrJmtjOn6j%2FJAEvTR7Sluqlgcf9wtJKe0%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;1226&quot; height=&quot;832&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;832&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1656&quot; data-origin-height=&quot;1317&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0Eu57/btrJlWzQSVF/W7LP53dWGykOCbwNyL95wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0Eu57/btrJlWzQSVF/W7LP53dWGykOCbwNyL95wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0Eu57/btrJlWzQSVF/W7LP53dWGykOCbwNyL95wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0Eu57%2FbtrJlWzQSVF%2FW7LP53dWGykOCbwNyL95wk%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;1656&quot; height=&quot;1317&quot; data-origin-width=&quot;1656&quot; data-origin-height=&quot;1317&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;유형A -&amp;gt; 별칭 -&amp;gt; Application/Classic Load Balancer에 대한 별칭 -&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;저장하면 설정이 끝난다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 내 인스턴스 IP로 오는 요청은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드밸런서가 가로채서 처리해줄 것이다&lt;/p&gt;</description>
      <category>  Infra/  AWS</category>
      <category>AWS</category>
      <category>ELB</category>
      <category>https</category>
      <category>SSL</category>
      <category>로드밸런서</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/31</guid>
      <comments>https://gengminy.tistory.com/31#entry31comment</comments>
      <pubDate>Tue, 9 Aug 2022 21:55:50 +0900</pubDate>
    </item>
    <item>
      <title>[Github] Github Actions로 CI/CD 개발 주기 자동화하기 - 3 (AWS EC2 서버에서 배포)</title>
      <link>https://gengminy.tistory.com/30</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc8XkM/btrJgeNA79h/r1otepokOO7KCx1Z3nWwO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc8XkM/btrJgeNA79h/r1otepokOO7KCx1Z3nWwO0/img.png&quot; data-alt=&quot;쉘 스크립트로 AWS까지 조종하는 깃헙 액션&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc8XkM/btrJgeNA79h/r1otepokOO7KCx1Z3nWwO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc8XkM%2FbtrJgeNA79h%2Fr1otepokOO7KCx1Z3nWwO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;720&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;쉘 스크립트로 AWS까지 조종하는 깃헙 액션&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;2편에 이어서 3편도 계속&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 와 RDS 프리티어를 구매한 후 세팅까지 완료&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;h2 data-ke-size=&quot;size26&quot;&gt;  작업 환경&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Windows + MacOS&lt;br /&gt;AWS EC2 + RDS&lt;br /&gt;Spring Boot (Gradle)&lt;br /&gt;PostgreSQL&lt;br /&gt;Redis&lt;br /&gt;Docker&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  실제 운영 서버에 배포하기&lt;/h2&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;Deploy 전용 레포지토리를 만들어주고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시를 위한 nginx와 쉘 스크립트를 셋팅해준다&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;  deploy.sh&lt;/h3&gt;
&lt;pre id=&quot;code_1659936584047&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash

# Installing docker engine if not exists
if ! type docker &amp;gt; /dev/null
then
  echo &quot;docker does not exist&quot;
  echo &quot;Start installing docker&quot;
  sudo apt-get update
  sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
  curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
  sudo add-apt-repository &quot;deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable&quot;
  sudo apt update
  apt-cache policy docker-ce
  sudo apt install -y docker-ce
fi

# Installing docker-compose if not exists
if ! type docker-compose &amp;gt; /dev/null
then
  echo &quot;docker-compose does not exist&quot;a
  echo &quot;Start installing docker-compose&quot;
  sudo curl -L &quot;https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)&quot; -o /usr/local/bin/docker-compose
  sudo chmod +x /usr/local/bin/docker-compose
fi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 깃헙 액션에서 제일 처음 실행하는 쉘 스크립트이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 액션에서 바로 명령어를 입력할 수도 있지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지저분해지니까 따로 빼놓는 게 좋다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 도커 이미지를 사용하기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커와 도커 컴포즈가 반드시 깔려 있어야한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 도커와 도커 컴포즈가 설치되어 있는지 확인하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 없으면 다운받아 실행시킨다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커를 사용하지 않는다면 자바나 파이썬, 노드, npm 등등&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;  deploy.yml&lt;/h3&gt;
&lt;pre id=&quot;code_1659936474978&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Deploy to EC2
on:
  workflow_dispatch:
  push:
    branches:
      - main

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@master

      - name: create env file
        run: |
          touch .env
          echo &quot;${{ secrets.ENV_VARS }}&quot; &amp;gt;&amp;gt; .env
      - name: create remote directory
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          password: ${{ secrets.PASSWORD }}
          script: mkdir -p ~/srv/ubuntu

      - name: copy source via ssh key
        uses: burnett01/rsync-deployments@4.1
        with:
          switches: -avzr --delete
          remote_path: ~/srv/ubuntu/
          remote_host: ${{ secrets.HOST }}
          remote_user: ${{ secrets.USERNAME }}
          remote_key: ${{ secrets.KEY }}
          
      - name: executing remote ssh commands using password
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.KEY }}
          script: |
            sh ~/srv/ubuntu/deploy.sh
            echo &quot;start docker-compose up: ubuntu&quot;
            sudo docker-compose -f ~/srv/ubuntu/docker-compose.yml pull
            sudo docker-compose -f ~/srv/ubuntu/docker-compose.yml --env-file ~/srv/ubuntu/.env up -d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;deploy/main 브랜치를 리스닝하는 액션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실 배포 환경을 위한 .env 파일을 만들어주고&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;여기서는 secrets에 여러 환경 변수를 입력해두었는데&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;HOST&lt;/b&gt; : EC2 public IP&lt;/li&gt;
&lt;li&gt;&lt;b&gt;USERNAME&lt;/b&gt; : EC2 username (ubuntu를 사용하기 때문에 기본 ubuntu)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PASSWORD&lt;/b&gt; : EC2 password (기본 패스워드는 없어서 사용하고 싶다면&lt;br /&gt;EC2 콘솔에서 password authentication 세팅해줘야함)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;KEY&lt;/b&gt; : EC2 pem 키 (windows에서는 ppk로 변환되는데 이거는 잘 안먹으니까 조심)&lt;/li&gt;
&lt;/ul&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;rsync&lt;/b&gt; 를 사용하는게 있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 github action runner에 복사된 내 deploy 레포지토리의 소스를&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;remote, 즉 내 EC2 서버로 동기화하여 복사해주는 리눅스 명령어다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 자동으로 내 레포지토리의 소스가 EC2 까지 복사된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 EC2 서버에 복사된 deploy.sh 파일을 실행하고&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;  docker-compose.yml&lt;/h3&gt;
&lt;pre id=&quot;code_1659937500999&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3.7&quot;
services:

# redis 설정
  redis:
    image: &quot;redis:alpine&quot;
    network_mode: &quot;host&quot;

# 서버 설정
  backend:
    image: gengminy/hifi-dev:dev
    container_name: backend
    network_mode: &quot;host&quot;
    env_file:
      - .env

# nginx 설정
  nginx:
    network_mode: &quot;host&quot;
    depends_on:
      - backend
    restart: always
    build:
      dockerfile: Dockerfile
      context: ./nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS 를 사용하기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인메모리 DB인 redis 를 제외한 DB관련 컨테이너는 띄우지 않는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redis는 jwt 리프레시 토큰을 편하게 사용하기 위해 도입했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx를 이용한 프록시 설정으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;80번 포트에서 서버 포트인 8000번 포트로 포워딩 시켰다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx는 도커 파일과 default.conf 파일을 따로 두어서 빌드시켰다&lt;/p&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;network_mode 를 host 로 두었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 네트워크 모드는 bridge라서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너가 독립적으로 존재하는 네트워크를 구성하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;host로 두게 되면 호스트의 네트워크를 각 컨테이너가 사용하게 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 쓰면 편한게 localhost로도 각 컨테이너에 접근이 가능하다&lt;/p&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;  default.conf&lt;/h3&gt;
&lt;pre id=&quot;code_1659937645752&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server{
    listen 80;
    server_name {내 서버 도메인};
    access_log  /var/log/nginx/access.log  backend;

    location / {
        proxy_pass http://backend:8000;
        proxy_redirect     off;

        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx에 대해서는 따로 설명을 붙이지 않겠다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;80번 포트에서 들어온 요청을 서버 8000번 포트로 포워딩 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;server_name 은 내가 구매해서 사용중인 도메인을 적어주면 된다&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  배포 확인&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1447&quot; data-origin-height=&quot;745&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7BiWi/btrI4VpcAbY/NjDZPDwqJ8LjaxuYUoVI60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7BiWi/btrI4VpcAbY/NjDZPDwqJ8LjaxuYUoVI60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7BiWi/btrI4VpcAbY/NjDZPDwqJ8LjaxuYUoVI60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7BiWi%2FbtrI4VpcAbY%2FNjDZPDwqJ8LjaxuYUoVI60%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;1447&quot; height=&quot;745&quot; data-origin-width=&quot;1447&quot; data-origin-height=&quot;745&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;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bU5ffN/btrI4VpcLMN/cipx85YU0Xbszdmo5dphp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bU5ffN/btrI4VpcLMN/cipx85YU0Xbszdmo5dphp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bU5ffN/btrI4VpcLMN/cipx85YU0Xbszdmo5dphp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbU5ffN%2FbtrI4VpcLMN%2Fcipx85YU0Xbszdmo5dphp1%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;1500&quot; height=&quot;804&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 EC2 서버에 접속해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sudo docker-compose logs 찍으면 배포가 되었는지 확인할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;928&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9az7j/btrJe5RedVR/nQy3hex66ccfi3xauV5mDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9az7j/btrJe5RedVR/nQy3hex66ccfi3xauV5mDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9az7j/btrJe5RedVR/nQy3hex66ccfi3xauV5mDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9az7j%2FbtrJe5RedVR%2FnQy3hex66ccfi3xauV5mDk%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;1266&quot; height=&quot;928&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;928&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;postman에서 리퀘스트 보내도 잘 오는 것을 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB와 redis 관련 동작까지 잘 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 깃헙 액션을 통해 CI/CD 자동화를 해보았는데&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;h2 data-ke-size=&quot;size26&quot;&gt;  Series&lt;/h2&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;color: #333333;&quot;&gt;&lt;a style=&quot;color: #333333;&quot; href=&quot;https://gengminy.tistory.com/27&quot;&gt;[Github] Github Actions로 CI/CD 개발 주기 자동화하기 - 1 (깃헙 액션 사용법)&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;a style=&quot;color: #333333;&quot; href=&quot;https://gengminy.tistory.com/29&quot;&gt;[Github] Github Actions로 CI/CD 개발 주기 자동화하기 - 2 (스프링 Gradle 빌드 + 도커 푸시)&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;a style=&quot;color: #333333;&quot; href=&quot;https://gengminy.tistory.com/30&quot;&gt;[Github] Github Actions로 CI/CD 개발 주기 자동화하기 - 3 (AWS EC2 서버에서 배포)&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Infra/⚡ Github Actions</category>
      <category>actions</category>
      <category>CD</category>
      <category>CI</category>
      <category>docker</category>
      <category>dockercompose</category>
      <category>github</category>
      <category>GithubActions</category>
      <category>깃헙액션</category>
      <category>도커</category>
      <category>자동배포</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/30</guid>
      <comments>https://gengminy.tistory.com/30#entry30comment</comments>
      <pubDate>Mon, 8 Aug 2022 14:53:59 +0900</pubDate>
    </item>
    <item>
      <title>[Github] Github Actions로 CI/CD 개발 주기 자동화하기 - 2 (스프링 Gradle 빌드 + 도커 푸시)</title>
      <link>https://gengminy.tistory.com/29</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1zYoQ/btrI2ZdmKWB/AlnjyUois5kpPNQucEu530/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1zYoQ/btrI2ZdmKWB/AlnjyUois5kpPNQucEu530/img.png&quot; data-alt=&quot;쓰면 쓸수록 생각보다 재미있는 깃헙 액션&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1zYoQ/btrI2ZdmKWB/AlnjyUois5kpPNQucEu530/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1zYoQ%2FbtrI2ZdmKWB%2FAlnjyUois5kpPNQucEu530%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;720&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;쓰면 쓸수록 생각보다 재미있는 깃헙 액션&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 어떻게 효율적으로 띄울까 하다가 여러 방식을 찾아보았는데&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;고로 프론트엔드, 백엔드, 배포 레포지토리 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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  서비스 아키텍쳐&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;hifiapp 다이어그램.png&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nb8xL/btrI9m0n55f/pK0lSj2cKOnzMoQrluZ9d1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nb8xL/btrI9m0n55f/pK0lSj2cKOnzMoQrluZ9d1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nb8xL/btrI9m0n55f/pK0lSj2cKOnzMoQrluZ9d1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnb8xL%2FbtrI9m0n55f%2FpK0lSj2cKOnzMoQrluZ9d1%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;1144&quot; height=&quot;675&quot; data-filename=&quot;hifiapp 다이어그램.png&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 프론트는 건들지 않아서 백엔드 서버와 배포 단계만 진행중이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;backend/main 브랜치에 push가 일어나면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깃헙 액션을 통해 스프링 도커 이미지를 빌드해 도커 허브에 push 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 deploy/main 브랜치에 push가 일어나거나, 직접 action 을 실행시키면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 쉘 스크립트 및 docker-compose를 통해 배포가 일어난다&lt;/p&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;여기서는 Continuous Deployment 보다는 Continuous Delivery가 맞을 거 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS S3 나 Github Runner를 이용하면 승인없는 자동배포도 가능한데&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;h2 data-ke-size=&quot;size26&quot;&gt;  Github Actions 생성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  docker-image-publish.yml&lt;/h3&gt;
&lt;pre id=&quot;code_1659933839815&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Push Image to DockerHub

on:
  push:
    branches: [main]
    # Publish semver tags as releases.
    tags: ['v*.*.*']

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      #JDK Setting
      - uses: actions/checkout@v3
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'
          
      #Gradle Caching
      - name: Cache Gradle packages
        uses: actions/cache@v2
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
          restore-keys: ${{ runner.os }}-gradle-
      
      #Grant gradlew Permission
      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      #Create dotenv file
      - name: Make env file
        run: |
          touch ./.env
          echo &quot;$ENV_PROPERTIES_DEV&quot; &amp;gt; ./.env
        env:
          ENV_PROPERTIES_DEV: ${{ secrets.ENV_PROPERTIES_DEV }}

      #Gradle Build
      - name: Build with Gradle
        run: ./gradlew build -x test

      #Image Tagging
      - name: Docker meta
        id: docker_meta
        uses: crazy-max/ghaction-docker-meta@v1
        with:
          images: ${{ secrets.DOCKER_REPOSITORY }}/hifi-dev
          tag-semver: |
            {{version}}
            {{major}}.{{minor}}
      #Docker Buildx Setup
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      #Login to DockerHub
      - name: Login to DockerHub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      #Docker Build and Push
      - name: Docker build &amp;amp; push
        uses: docker/build-push-action@v2
        with:
          context: .
          file: ./Dockerfile
          platforms: linux/amd64
          push: true
          tags: ${{ steps.docker_meta.outputs.tags }}
          labels: ${{ steps.docker_meta.outputs.labels }}&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;h3 data-ke-size=&quot;size23&quot;&gt;  실행 과정&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;JDK 11 버전으로 세팅을 시작한다&lt;/li&gt;
&lt;li&gt;Gradle Caching 이라는 과정이 있는데 반드시 필요한 것은 아니지만&lt;br /&gt;캐싱 사용 시 성능이 2~30퍼센트 정도 빨라진다&lt;/li&gt;
&lt;li&gt;gradlew 빌더를 사용하기 위해 chmod 로 권한을 부여한다&lt;/li&gt;
&lt;li&gt;환경변수 파일을 생성한다 secrets 는 레포지토리의 Settings 에서 설정 가능하다. 후술&lt;/li&gt;
&lt;li&gt;gradlew 빌드를 진행한다&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;/ol&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;h2 data-ke-size=&quot;size26&quot;&gt;  Github Actions 실행 및 로그&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1624&quot; data-origin-height=&quot;1190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nXTgK/btrI4UXW1uM/PALriMJ2xSXke84Yv0BhD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nXTgK/btrI4UXW1uM/PALriMJ2xSXke84Yv0BhD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nXTgK/btrI4UXW1uM/PALriMJ2xSXke84Yv0BhD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnXTgK%2FbtrI4UXW1uM%2FPALriMJ2xSXke84Yv0BhD1%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;1624&quot; height=&quot;1190&quot; data-origin-width=&quot;1624&quot; data-origin-height=&quot;1190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 레포지토리 -&amp;gt; Settings -&amp;gt; Security(Secrets) -&amp;gt; Actions 탭에서&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;가져다 쓸 때는 깃헙 액션 yml 에서 ${{secrets.환경변수이름}} 으로 가져와 쓸 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 EC2 서버 접속을 위한 pem 키 같은 것들을 여기다가 잘 입력해주자&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1379&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwq4x5/btrI4VihLMV/27VE7ShCeFXcGwyPseWkm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwq4x5/btrI4VihLMV/27VE7ShCeFXcGwyPseWkm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwq4x5/btrI4VihLMV/27VE7ShCeFXcGwyPseWkm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbwq4x5%2FbtrI4VihLMV%2F27VE7ShCeFXcGwyPseWkm0%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;1379&quot; height=&quot;544&quot; data-origin-width=&quot;1379&quot; data-origin-height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main 브랜치에서 변화를 감지하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 Actions 탭에서 액션이 실행되는 것을 볼 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;1310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y2sp0/btrI7LlUVZv/uxSyDwOlzVUoeimTa1Iv8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y2sp0/btrI7LlUVZv/uxSyDwOlzVUoeimTa1Iv8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y2sp0/btrI7LlUVZv/uxSyDwOlzVUoeimTa1Iv8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY2sp0%2FbtrI7LlUVZv%2FuxSyDwOlzVUoeimTa1Iv8K%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;1654&quot; height=&quot;1310&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;1310&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;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNmLch/btrI6ruwfeQ/0NDPqDLvvhdZyYIH7GA340/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNmLch/btrI6ruwfeQ/0NDPqDLvvhdZyYIH7GA340/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNmLch/btrI6ruwfeQ/0NDPqDLvvhdZyYIH7GA340/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNmLch%2FbtrI6ruwfeQ%2F0NDPqDLvvhdZyYIH7GA340%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;1072&quot; height=&quot;442&quot; data-origin-width=&quot;1072&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;깃헙 액션이 실행 도중 실패하면 에러 메세지 뜨니까 그것으로 확인 잘 하면 고치기 쉽다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 컴파일 에러, Docker hub 로그인 에러, 환경 변수 설정 안해서 생기는 에러 등등&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;h2 data-ke-size=&quot;size26&quot;&gt;  Series&lt;/h2&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;color: #333333;&quot;&gt;&lt;a style=&quot;color: #333333;&quot; href=&quot;https://gengminy.tistory.com/27&quot;&gt;[Github] Github Actions로 CI/CD 개발 주기 자동화하기 - 1 (깃헙 액션 사용법)&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;a style=&quot;color: #333333;&quot; href=&quot;https://gengminy.tistory.com/29&quot;&gt;[Github] Github Actions로 CI/CD 개발 주기 자동화하기 - 2 (스프링 Gradle 빌드 + 도커 푸시)&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;a style=&quot;color: #333333;&quot; href=&quot;https://gengminy.tistory.com/30&quot;&gt;[Github] Github Actions로 CI/CD 개발 주기 자동화하기 - 3 (AWS EC2 서버에서 배포)&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Infra/⚡ Github Actions</category>
      <category>actions</category>
      <category>CD</category>
      <category>CI</category>
      <category>CI/CD</category>
      <category>docker</category>
      <category>github</category>
      <category>GithubActions</category>
      <category>깃헙액션</category>
      <category>배포</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/29</guid>
      <comments>https://gengminy.tistory.com/29#entry29comment</comments>
      <pubDate>Mon, 8 Aug 2022 14:19:18 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] docker compose로 다중 컨테이너 연결 시 Connection to localhost:5432 refused 에러</title>
      <link>https://gengminy.tistory.com/28</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;도커 컴포즈로 Spring Boot + PostgreSQL 다중 컨테이너를 띄워서 연결하던 중 발견한 오류&lt;/p&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;장장 4시간의 삽질&lt;/p&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;blockquote data-ke-style=&quot;style3&quot;&gt;Spring Boot (Gradle)&lt;br /&gt;Redis&lt;br /&gt;PostgreSQL&lt;br /&gt;PgAdmin&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;h3 data-ke-size=&quot;size23&quot;&gt;  원인 찾기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2573&quot; data-origin-height=&quot;701&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Pmq74/btrI2dXJuVM/3VsKYOWHsKym1OTVKTmDU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Pmq74/btrI2dXJuVM/3VsKYOWHsKym1OTVKTmDU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Pmq74/btrI2dXJuVM/3VsKYOWHsKym1OTVKTmDU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPmq74%2FbtrI2dXJuVM%2F3VsKYOWHsKym1OTVKTmDU0%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;2573&quot; height=&quot;701&quot; data-origin-width=&quot;2573&quot; data-origin-height=&quot;701&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;org.postgresql.uti.PSQLException: Conntection to localhost:5432 refused.&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;Spring에서 PostgreSQL 연결 시 호스트나 포트를 잘못 찾아서 연결할 수 없는 오류이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 db 주소를 잘못 입력했다는 거다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  변경 전 파일&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  docker-compose.yml&lt;/p&gt;
&lt;pre id=&quot;code_1659872555946&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3.7'
services:
  app:
    build: .
    container_name: app
    hostname: backend
    env_file:
      - .env
    expose:
      - '8000'
    depends_on:
      - postgres
      - redis

  redis:
    image: redis:alpine
    ports:
      - '6379:6379'

  postgres:
    image: postgres:latest
    container_name: postgres
    restart: always
    ports:
      - '5432:5432'
    env_file:
      - .env
    networks:
      - postgres

  pgadmin:
    links:
      - postgres:postgres
    container_name: pgadmin
    image: dpage/pgadmin4
    ports:
      - '8080:80'
    env_file:
      - .env
    networks:
      - postgres

networks:
  postgres:
    driver: bridge&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;  .env&lt;/p&gt;
&lt;pre id=&quot;code_1659872745194&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PORT=8000
...
REDIS_HOST=localhost
REDIS_PORT=6379
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
...&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;  ./src/main/resources/application.yml&lt;/p&gt;
&lt;pre id=&quot;code_1659872766225&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  # .env import
  config:
    import: optional:file:.env[.properties]
  # Using POSTGRESQL
  datasource:
    url: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
    username: ${POSTGRES_USER}
    password: ${POSTGRES_PASSWORD}
    driver-class-name: org.postgresql.Driver
    ...&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;장장 4시간을 헤메이다 결국 구글링으로 원인을 찾아냈다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.linuxfixes.com/2021/12/solved-docker-is-server-running-on-host.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.linuxfixes.com/2021/12/solved-docker-is-server-running-on-host.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1659872841047&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;[SOLVED] Docker: Is the server running on host &amp;quot;localhost&amp;quot; (::1) and accepting TCP/IP connections on port 5432?&quot; data-og-description=&quot;Issue I am getting issues while setup and run the docker instance on my local system with ...&quot; data-og-host=&quot;www.linuxfixes.com&quot; data-og-source-url=&quot;https://www.linuxfixes.com/2021/12/solved-docker-is-server-running-on-host.html&quot; data-og-url=&quot;https://www.linuxfixes.com/2021/12/solved-docker-is-server-running-on-host.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.linuxfixes.com/2021/12/solved-docker-is-server-running-on-host.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.linuxfixes.com/2021/12/solved-docker-is-server-running-on-host.html&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;[SOLVED] Docker: Is the server running on host &quot;localhost&quot; (::1) and accepting TCP/IP connections on port 5432?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Issue I am getting issues while setup and run the docker instance on my local system with ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.linuxfixes.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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app 이라는 스프링 서버 컨테이너에서 localhost:5432 를 조회하게 되면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기 자신의 5432 포트를 조회하는 거니까 아무것도 없는 빈 곳을 조회하는 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머리가 상당히 띵했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 결국 &lt;b&gt;localhost:5432 &lt;/b&gt;가 아니라&lt;b&gt; postgres:5432&lt;/b&gt; 로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;postgres 서비스의 5432 포트를 조회해야 db에 접근할 수 있다는 것이다&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  변경 후 파일&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  docker-compose.yml&lt;/p&gt;
&lt;pre id=&quot;code_1659873008432&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3.7'
services:
  app:
    build: .
    container_name: app
    hostname: backend
    env_file:
      - .env
    expose:
      - '8000'
    depends_on:
      - postgres
      - redis
    networks:
      - postgres

  redis:
    image: redis:alpine
    ports:
      - '6379:6379'

  postgres:
    image: postgres:latest
    container_name: postgres
    restart: always
    ports:
      - '5432:5432'
    env_file:
      - .env
    networks:
      - postgres

  pgadmin:
    links:
      - postgres:postgres
    container_name: pgadmin
    image: dpage/pgadmin4
    ports:
      - '8080:80'
    env_file:
      - .env
    networks:
      - postgres

networks:
  postgres:
    driver: bridge&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;depends_on 으로 redis 와 postgres 가 먼저 켜진 후에 app을 실행한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 같은 networks 로 묶어서 통신할 수 있도록 해준다&lt;/p&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;  .env&lt;/p&gt;
&lt;pre id=&quot;code_1659873074188&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PORT=8000
...
REDIS_HOST=redis
REDIS_PORT=6379
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
...&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;도커 컴포즈 파일에서 정의한 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2167&quot; data-origin-height=&quot;405&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vx2aK/btrI6e1BRSD/kk9N4KqQShhR0XXmGrBqD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vx2aK/btrI6e1BRSD/kk9N4KqQShhR0XXmGrBqD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vx2aK/btrI6e1BRSD/kk9N4KqQShhR0XXmGrBqD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvx2aK%2FbtrI6e1BRSD%2Fkk9N4KqQShhR0XXmGrBqD0%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;2167&quot; height=&quot;405&quot; data-origin-width=&quot;2167&quot; data-origin-height=&quot;405&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;이상 도커 컴포즈 삽질의 흔적이었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;막상 삽질했더니 이렇게 하면 안될 거 같아서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;postgres docker는 테스트 용으로만 두고 RDS 추가해서 그걸로 설정했다&lt;/p&gt;</description>
      <category>  Infra/  Docker</category>
      <category>postgres</category>
      <category>PostgreSQL</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>도커</category>
      <category>도커컴포즈</category>
      <category>스프링</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/28</guid>
      <comments>https://gengminy.tistory.com/28#entry28comment</comments>
      <pubDate>Sun, 7 Aug 2022 20:54:02 +0900</pubDate>
    </item>
    <item>
      <title>[Github] Github Actions로 CI/CD 개발 주기 자동화하기 - 1 (깃헙 액션 사용법)</title>
      <link>https://gengminy.tistory.com/27</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coz0Cy/btrI2po8NIb/sxqHdeAMsTtq9xOJ5OSZ91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coz0Cy/btrI2po8NIb/sxqHdeAMsTtq9xOJ5OSZ91/img.png&quot; data-alt=&quot;CI/CD 툴 중 하나인 Github Actions 를 이용해보자&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coz0Cy/btrI2po8NIb/sxqHdeAMsTtq9xOJ5OSZ91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcoz0Cy%2FbtrI2po8NIb%2FsxqHdeAMsTtq9xOJ5OSZ91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;720&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CI/CD 툴 중 하나인 Github Actions 를 이용해보자&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; &lt;/b&gt;&amp;nbsp;CI / CD ?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m5kQl/btrI6rmCfOf/rw7CK1UTSEieUyEjbboRiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m5kQl/btrI6rmCfOf/rw7CK1UTSEieUyEjbboRiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m5kQl/btrI6rmCfOf/rw7CK1UTSEieUyEjbboRiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm5kQl%2FbtrI6rmCfOf%2Frw7CK1UTSEieUyEjbboRiK%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;600&quot; height=&quot;293&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  CI (Continuous Integration)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지속적인 통합&lt;/li&gt;
&lt;li&gt;&lt;i&gt;[코드작성 -&amp;gt; 테스트 -&amp;gt; 빌드 -&amp;gt; 배포 -&amp;gt; 버그수정]&lt;/i&gt; 이라는 일련의 사이클을 단축시키고 자동화시키는 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  CD (Continuous Delivery 또는 Continuous Deployment)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지속적인 배포&lt;/li&gt;
&lt;li&gt;어플리케이션에 적용한 변경 사항을 테스트를 거쳐 레포지토리에 업로드 되는 것&lt;/li&gt;
&lt;li&gt;&lt;u&gt;&lt;i&gt;Continuous Delivery&lt;/i&gt;&lt;/u&gt;&amp;nbsp;는 마지막 단계인 개발환경에서 운영환경으로의 배포를 개발자가 수동으로 해줌&lt;/li&gt;
&lt;li&gt;&lt;u&gt;&lt;i&gt;Continuous Deployment&lt;/i&gt;&lt;/u&gt; 는 이러한 수동 처리 없이 자동으로 운영환경 배포까지 자동으로 해줌&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Github Actions 는 이러한 CI/CD를 가능하게 해주는 툴 중 하나이고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins, Travis CI, AWS Code Deploy 등등 다양한 툴이 있다&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; &lt;/b&gt;&amp;nbsp;Github Actions 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트 폴더에서 ./.github/workflows/ 내부에 정의할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깃헙 액션 템플릿을 사용할 수도 있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레포지토리의 Actions 탭에서 만들거나 깃허브 Marketplace 탭에서 커스텀 액션을 검색할 수도 있다&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1872&quot; data-origin-height=&quot;1101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBg5Sk/btrI6rz7EMU/PGnWNws6XFuD3krpWnFMC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBg5Sk/btrI6rz7EMU/PGnWNws6XFuD3krpWnFMC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBg5Sk/btrI6rz7EMU/PGnWNws6XFuD3krpWnFMC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBg5Sk%2FbtrI6rz7EMU%2FPGnWNws6XFuD3krpWnFMC1%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;1872&quot; height=&quot;1101&quot; data-origin-width=&quot;1872&quot; data-origin-height=&quot;1101&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1942&quot; data-origin-height=&quot;1372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bug5O1/btrI2VBmGhf/OSqwK6FCxK30BUl6504e0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bug5O1/btrI2VBmGhf/OSqwK6FCxK30BUl6504e0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bug5O1/btrI2VBmGhf/OSqwK6FCxK30BUl6504e0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbug5O1%2FbtrI2VBmGhf%2FOSqwK6FCxK30BUl6504e0k%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;1942&quot; height=&quot;1372&quot; data-origin-width=&quot;1942&quot; data-origin-height=&quot;1372&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;여기다가 깃헙 액션을 언제 실행할지, 무엇을 실행할지 등을 yml 파일로 정의한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; &lt;/b&gt; Github Actions 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;435&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5dIJ6/btrI9llRKHY/PFaaSHlyXk0I8HjF7VTtbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5dIJ6/btrI9llRKHY/PFaaSHlyXk0I8HjF7VTtbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5dIJ6/btrI9llRKHY/PFaaSHlyXk0I8HjF7VTtbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5dIJ6%2FbtrI9llRKHY%2FPFaaSHlyXk0I8HjF7VTtbk%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;700&quot; height=&quot;297&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;435&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;Workflow &amp;gt; Job &amp;gt; Step &amp;gt; Action&lt;/b&gt;&lt;/i&gt;&lt;/u&gt; 순으로 정의한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 하나의 workflow 안에 여러 개의 job 을 정의할 수 있고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;job 내부에도 여러 개의 step .... 이런식으로 세부화된다&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;  ./.github/workflows/gradle-publish.yml&lt;/h4&gt;
&lt;pre id=&quot;code_1659850372441&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Gradle Package

on:
  release:
    types: [created]

jobs:
  build:
  ...&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Workflow&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나 이상의 Job 으로 구성되고 이벤트 기반으로 동작하는 절차이다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;name&lt;/b&gt; : workflow 이름을 정의한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;on&lt;/b&gt; : 어떠한 branch에 push 되었을때, 특정한 시간에만 등등.. workflow 가 실행되도록 하는 이벤트를 정의한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;jobs&lt;/b&gt; : 하나의 workflow 에서 수행시킬 job을 정의한다. 기본적으로 &lt;u&gt;병렬로 실행&lt;/u&gt;된다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Jobs&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나 이상의 step 으로 구성된 집합이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 job 은 &lt;b&gt;독립된 runner, 즉 분리되어진 가상 환경&lt;/b&gt;에서 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 workflow는 여러 개의 job을 &lt;u&gt;병렬로 실행&lt;/u&gt;시킨다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 순차적으로 실행하게 하고 싶다면 의존성을 명시해주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1659851026723&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
jobs:
  build:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        os: [ubuntu-latest, windows-2016]
        node-version: [12.x, 14,x]
        
    steps:
      - uses: actions/checkout@v2
      - name: npm install and build webpack
        run: |
          npm install
          npm run build
      #아티팩트를 업로드함
      - uses: actions/upload-artifact@master
        with:
          name: webpack artifacts
          path: public/

  test:
    #의존성 명시, build가 실행된 이후 test가 실행됨
    needs: build
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    #아티팩트를 다운받음    
    - uses: actions/download-artifact@master
      with: 
        name: webpack artifacts
        path: public
    ...&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;build 이후에 여러 개의 test를 수행해야 한다면 위 코드처럼 분리시킬 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 각 job은 독립된 &lt;b&gt;runner&lt;/b&gt;에서 돌아간다고 했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 build 라는 job 에서 사용한 빌드된 코드를 test에서 사용할 수 없다 (독립된 공간이므로)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 의존성이 있는 job 은 빌드된 아티팩트를 업로드 하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 job 에서 내려받는 식으로 사용할 수도 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 needs 를 사용하여 순서를 명시해주면 된다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;build 또는 test &lt;/b&gt;: 하나의 job과 그 이름을 정의한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;runs-on&lt;/b&gt; : job이 수행될 os 등의 환경을 정의한다. 여기서는 ubuntu 최신 버전에서 동작하도록 했다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;strategy&lt;/b&gt; : 매트리스 등을 정의하여 여러 환경에서 동시에 빌드하도록 할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;needs &lt;/b&gt;: 해당 job 이 끝나면 이 job 을 수행하도록 정의한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;steps&lt;/b&gt; : 하나의 job 에서 수행할 step의 집합을 정의한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Steps&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 안에서 명령어를 실행시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;action 또는 shell 명령어로 이루어진 집합이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 job은 같은 runner에서 실행되기 때문에 데이터를 공유할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1659851623128&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  test:
    ...
    steps:
    - uses: actions/checkout@v2
    - uses: actions/download-artifact@master
      with: 
        name: webpack artifacts
        path: public
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - name: npm install, and test
      run: |
        npm install
        npm test
      env:
        CI: true&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;uses &lt;/span&gt;&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 오픈 소스 커뮤니티에 있는 다른 &lt;/span&gt;&lt;span&gt;action를 가지고 와서 실행하는 것&lt;br /&gt;&lt;i&gt;actions/checkout@v2&lt;/i&gt; 는 내 레포지토리 환경을 runner 에 복사해서 실행하는 action이므로&lt;br /&gt;내 레포지토리의 코드를 다루는 job 에서는 반드시 사용해야함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;with&lt;/b&gt; : 각 actions에 의해 정의된 파라미터를 나열한다.&lt;br /&gt;이 변수들은 앞에 INPUT_ 이라는 prefix 가 붙고&lt;br /&gt;대문자로 바뀌어 각 action에 대한 환경변수로 선언된다. (node-version -&amp;gt; INPUT_NODE-VERSION)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;env&lt;/b&gt; : 각 actions에 필요한 환경 변수를 정의한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;name&lt;/b&gt; : 각 step 의 이름을 정의한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;run&lt;/b&gt; : runner에서 실행할 커맨드를 정의한다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Secrets 환경 변수 정의하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Secret 을 이용해 환경 변수를 전달할 수도 있다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;1025&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IgLpM/btrI4V9j2Ii/nrbtXiF7cP3di8Z6KGsz40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IgLpM/btrI4V9j2Ii/nrbtXiF7cP3di8Z6KGsz40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IgLpM/btrI4V9j2Ii/nrbtXiF7cP3di8Z6KGsz40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIgLpM%2FbtrI4V9j2Ii%2FnrbtXiF7cP3di8Z6KGsz40%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;1352&quot; height=&quot;1025&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;1025&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 레포지토리 -&amp;gt; Settings -&amp;gt; Secrets -&amp;gt; Actions 에서&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_1659852624952&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;      ...
      - name: Login to DockerHub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_TOKEN }}
      ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깃헙 액션 yml 파일에서 secrets로 접근 가능하다&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  Reference&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  &lt;b&gt;Github Actions Docs&lt;/b&gt; - &lt;a href=&quot;https://docs.github.com/en/actions&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;깃헙 액션 공식 Docs&lt;/a&gt;&lt;br /&gt;  &lt;b&gt;Github Actions로 개발 주기 자동화&lt;/b&gt; - &lt;a href=&quot;https://www.youtube.com/watch?v=MhGpFunlmMQ&amp;amp;list=PLDZRZwFT9Wkt19Ox35Ir2A7CyNIWG96Nm&amp;amp;index=1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;애저 듣고보는 잡학지식&lt;/a&gt;&lt;/blockquote&gt;
&lt;figure id=&quot;og_1659853001224&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;GitHub Actions Documentation - GitHub Docs&quot; data-og-description=&quot;Automate, customize, and execute your software development workflows right in your repository with GitHub Actions. You can discover, create, and share actions to perform any job you'd like, including CI/CD, and combine actions in a completely customized wo&quot; data-og-host=&quot;docs.github.com&quot; data-og-source-url=&quot;https://docs.github.com/en/actions&quot; data-og-url=&quot;https://ghdocs-prod.azurewebsites.net/en/actions&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/sktMX/hyPmez8O4Y/NJiuFy8wuw3eyienWzrGSk/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200&quot;&gt;&lt;a href=&quot;https://docs.github.com/en/actions&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.github.com/en/actions&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/sktMX/hyPmez8O4Y/NJiuFy8wuw3eyienWzrGSk/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_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;GitHub Actions Documentation - GitHub Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Automate, customize, and execute your software development workflows right in your repository with GitHub Actions. You can discover, create, and share actions to perform any job you'd like, including CI/CD, and combine actions in a completely customized wo&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.github.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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Series&lt;/h2&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;color: #333333;&quot;&gt;&lt;a style=&quot;color: #333333;&quot; href=&quot;https://gengminy.tistory.com/27&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Github] Github Actions로 CI/CD 개발 주기 자동화하기 - 1 (깃헙 액션 사용법)&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;a style=&quot;color: #333333;&quot; href=&quot;https://gengminy.tistory.com/29&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Github] Github Actions로 CI/CD 개발 주기 자동화하기 - 2 (스프링 Gradle 빌드 + 도커 푸시)&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;a style=&quot;color: #333333;&quot; href=&quot;https://gengminy.tistory.com/30&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Github] Github Actions로 CI/CD 개발 주기 자동화하기 - 3 (AWS EC2 서버에서 배포)&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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;</description>
      <category>  Infra/⚡ Github Actions</category>
      <category>CD</category>
      <category>CI</category>
      <category>github</category>
      <category>GithubActions</category>
      <category>깃헙</category>
      <category>깃헙액션</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/27</guid>
      <comments>https://gengminy.tistory.com/27#entry27comment</comments>
      <pubDate>Sun, 7 Aug 2022 15:29:01 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] docker compose 사용해서 다중 컨테이너 띄우기</title>
      <link>https://gengminy.tistory.com/26</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxuT9e/btrI6eG4lfH/zSCHE0le8ocBzhXha4yK6k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxuT9e/btrI6eG4lfH/zSCHE0le8ocBzhXha4yK6k/img.jpg&quot; data-alt=&quot;왜 귀여운 고래 어디가고 징징이를 데려왔을까 싶었는데 다중 컨테이너 띄우는걸 잘 표현한 거 같다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxuT9e/btrI6eG4lfH/zSCHE0le8ocBzhXha4yK6k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxuT9e%2FbtrI6eG4lfH%2FzSCHE0le8ocBzhXha4yK6k%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;1280&quot; height=&quot;614&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;왜 귀여운 고래 어디가고 징징이를 데려왔을까 싶었는데 다중 컨테이너 띄우는걸 잘 표현한 거 같다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Reference&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  docker compose 를 사용하는 이유&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너가 늘어나면 늘어날 수록 docker run 으로 실행해야 할 명령어 수가 많아지며&lt;br /&gt;특히 각각의 컨테이너에 설정할 플래그가 많아질수록 더욱 더 복잡해진다&lt;/li&gt;
&lt;li&gt;이를 하나로 묶어서 한번에 서비스를 올리고 관리할 수 있도록 해주는 도구가 &lt;b&gt;도커 컴포즈&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;각 컨테이너는 독립된 기능을 가지고 공통 네트워크로 구성되어 컨테이너 간 통신이 쉽다&lt;/li&gt;
&lt;li&gt;다만 다양한 관리 기능은 없어 실제 운영 환경에서는 쿠버네티스를 사용하는 것이 더 좋다&lt;/li&gt;
&lt;/ul&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;h2 data-ke-size=&quot;size26&quot;&gt;  docker compose 파일 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트 폴더에 docker-compose.yml 또는 docker-compose.yaml 파일을 생성한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 컴포즈 파일은 yml(야믈) 코드로 작성해야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yml 파일은 &lt;u&gt;반드시 두 칸의 들여쓰기&lt;/u&gt;를 통해 구분이 된다 &lt;u&gt;tab은 안된다&lt;/u&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;h3 data-ke-size=&quot;size23&quot;&gt;  docker-compose.yml&lt;/h3&gt;
&lt;pre id=&quot;code_1659798176998&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3.7'
services:
  redis:
    image: redis:alpine
    ports:
      - '6379:6379'

  postgres:
    image: postgres:latest
    container_name: postgres
    restart: always
    ports:
      - '5432:5432'
    env_file:
      - .env
    networks:
      - postgres

  pgadmin:
    links:
      - postgres:postgres
    container_name: pgadmin
    image: dpage/pgadmin4
    ports:
      - '8080:80'
    # volumes:
    #   - /data/pgadmin:/root/.pgadmin
    env_file:
      - .env
    networks:
      - postgres

networks:
  postgres:
    driver: bridge&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;version&lt;/b&gt; : 도커 컴포즈 버전을 명시한다, 버전에 따라 약간의 기능과 옵션의 차이가 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;service&lt;/b&gt; : 실행시킬 서비스를 정의한다. 컨테이너와 동일하다고 생각하면 편하다
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;image&lt;/b&gt; : 도커 허브의 공식 이미지를 사용할 경우 지정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;build&lt;/b&gt; : Dockerfile 의 경로를 지정한다. 도커 컴포즈 파일과 동일한 경로일 경우 점. 을 찍는다&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;context / dockerfile : 경로가 다를 경우 도커 파일 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;container_name&lt;/b&gt; : (생략가능) 컨테이너의 이름 지정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ports&lt;/b&gt; : 호스트 포트와 서비스 내부 포트 지정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;expose&lt;/b&gt; : 서비스 포트만을 노출&lt;/li&gt;
&lt;li&gt;&lt;b&gt;networks&lt;/b&gt; : 최상위 레벨의 networks에 정의된 네트워크 이름을 지정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;volumes&lt;/b&gt; : 호스트 디렉토리와 서비스 내부 디렉토리를 연결, 데이터 지속성 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;environment&lt;/b&gt; : 서비스 내부의 환경 변수 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;env_file&lt;/b&gt; : 환경변수 파일(.env)을 가져와 환경 변수 지정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;command&lt;/b&gt; : 서비스 구동 이후 실행할 명령어&lt;/li&gt;
&lt;li&gt;&lt;b&gt;restart&lt;/b&gt; : 서비스 재시작 옵션 (&lt;b&gt;always&lt;/b&gt;: 수동 종료 제외하고 항상 재시작, &lt;b&gt;on-failure&lt;/b&gt;: 오류 시 재시작)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;depends_on&lt;/b&gt; : 서비스 간 종속성 지정, 이 옵션에 지정한 서비스가 먼저 시작됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;network&lt;/b&gt; : 해당 이름의 네트워크를 정의한다. 대역은 172.x.x.x 로 할당, 기본 드라이버는 bridge&lt;/li&gt;
&lt;li&gt;&lt;b&gt;volume&lt;/b&gt; : 최상위 레벨의 볼륨 지정&lt;/li&gt;
&lt;/ul&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;h2 data-ke-size=&quot;size26&quot;&gt;  자주 사용하는 docker compose 명령어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &amp;nbsp;&lt;/b&gt;&lt;u&gt;&lt;b&gt;docker-compose&lt;/b&gt;&lt;/u&gt; 또는 &lt;u&gt;&lt;b&gt;docker compose&lt;/b&gt;&lt;/u&gt; 를 통해 도커 컴포즈를 실행할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;docker-compose up : 도커 컴포즈 실행&lt;/li&gt;
&lt;li&gt;docker-compose up -d : 백그라운드로 도커 컴포즈 실행&lt;/li&gt;
&lt;li&gt;docker-compose down : 컨테이너 서비스, 볼륨, 네트워크를 모두 정지시킨 후 삭제&lt;/li&gt;
&lt;li&gt;docker-compose down -v : 외부 볼륨까지 전체 삭제&lt;/li&gt;
&lt;li&gt;docker-compose stop {service} : 특정 컨테이너만 중지&lt;/li&gt;
&lt;li&gt;docker-compose start {service} : 특정 컨테이너만 시작&lt;/li&gt;
&lt;li&gt;docker-compose logs : 어플리케이션 로그 출력&lt;/li&gt;
&lt;li&gt;docker-compose logs -f : 실시간 로그 출력&lt;/li&gt;
&lt;li&gt;docker-compose ps : 도커 컴포즈에 정의된 모든 서비스 컨테이너 조회&lt;/li&gt;
&lt;li&gt;docker-compose config : yml 파일 설정 확인&lt;/li&gt;
&lt;li&gt;docker-compose pause : 컨테이너 일시 정지&lt;/li&gt;
&lt;li&gt;docker-compose unpause : 컨테이너 정지 해제&lt;/li&gt;
&lt;/ul&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;h2 data-ke-size=&quot;size26&quot;&gt;  docker compose 실행&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;235&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/omRxG/btrI2dv0npm/cDyjns8ol4G2DpXupkO5p1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/omRxG/btrI2dv0npm/cDyjns8ol4G2DpXupkO5p1/img.png&quot; data-alt=&quot;잘 시작된 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/omRxG/btrI2dv0npm/cDyjns8ol4G2DpXupkO5p1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FomRxG%2FbtrI2dv0npm%2FcDyjns8ol4G2DpXupkO5p1%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;1785&quot; height=&quot;235&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;235&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;잘 시작된 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널이 멈추는 것을 원치 않으면 반드시 -d 플래그를 붙여서&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;Windows 나 macOS 환경이라면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꼭 Docker Desktop 실행 후에 도커 컴포즈 명령어 입력하시구요&lt;/p&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;</description>
      <category>  Infra/  Docker</category>
      <category>docker</category>
      <category>dockercompose</category>
      <category>YAML</category>
      <category>yml</category>
      <category>도커</category>
      <category>도커컴포즈</category>
      <category>야믈</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/26</guid>
      <comments>https://gengminy.tistory.com/26#entry26comment</comments>
      <pubDate>Sun, 7 Aug 2022 00:33:33 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] Dockerfile 명령어로 스프링 프로젝트 도커 이미지 빌드 및 푸시</title>
      <link>https://gengminy.tistory.com/25</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lDuf6/btrI11unprI/5F76INnJVSaimPk2erBPMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lDuf6/btrI11unprI/5F76INnJVSaimPk2erBPMk/img.png&quot; data-alt=&quot;우영우가 좋아할 만한 귀여운 도커 고래&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lDuf6/btrI11unprI/5F76INnJVSaimPk2erBPMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlDuf6%2FbtrI11unprI%2F5F76INnJVSaimPk2erBPMk%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;694&quot; height=&quot;604&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;우영우가 좋아할 만한 귀여운 도커 고래&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 진행하면서 배포를 직접할 일이 생겼다&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;이 글은 도커와 AWS를 공부하면서 참고하기 위해 작성해보는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커를 이용한 배포 과정, 그 중에서도 &lt;b&gt;Dockerfile&lt;/b&gt; 에 대한 작성법이다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Reference&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  도커, 컨테이너 빌드 업! - 이현룡&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Dockerfile&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&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;도커 파일을 통해 특정 컨테이너를 빌드하고 배포하기 위한 과정들을 자동화 할 수 있게 된다&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  Dockerfile 작성 시 고려할 점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너 서비스의 장점인 경량 가상화 서비스를 지향하기 위해 최소한의 설정과 구성만을 사용&lt;/li&gt;
&lt;li&gt;Dockerfile 명령어 수와 도커 이미지 레이어 수는 동일하다&lt;br /&gt;레이어 수가 늘어나면 빌드 시간이 길어지고 파일 용량이 커지게 된다&lt;br /&gt;따라서 Dockerfile 명령어를 최적화 할 필요가 있다&lt;/li&gt;
&lt;li&gt;하나의 어플리케이션은 하나의 컨테이너에 담아 결합성을 낮춘다&lt;/li&gt;
&lt;li&gt;Dockerfile은 명령어 단위로 캐싱이 일어난다&lt;br /&gt;이것을 적극 활용하여 빌드 속도를 높힌다&lt;/li&gt;
&lt;li&gt;Dockerfile은 이미지 빌드를 시작하면 현재 위치 포함 하위 디렉토리의 모든 것들을 빌드한다&lt;br /&gt;따라서 별도의 작업 디렉토리를 두어 독립된 환경에서 꼭 필요한 파일만 빌드한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Dockerfile 명령어 리스트&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;FROM&lt;/b&gt; : 생성하려는 이미지의 베이스 이미지 지정, &lt;u&gt;반드시 입력해야함&lt;/u&gt;&lt;br /&gt;&lt;sub&gt; ex) FROM ubuntu:20.04&lt;br /&gt;&lt;br /&gt;&lt;/sub&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MAINTAINER&lt;/b&gt; : 보통 이미지를 빌드한 작성자의 이름 또는 이메일을 입력&lt;br /&gt;&lt;sub&gt; ex) MAINTAINER gengminy &amp;lt;gengminy@github.com&amp;gt;&lt;br /&gt;&lt;br /&gt;&lt;/sub&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LABEL&lt;/b&gt; : 버전, 타이틀, 설명, 라이센스 등 메타 데이터를 입력, &lt;u&gt;여러 개 입력 가능&lt;/u&gt;&lt;br /&gt;&lt;sub&gt; ex) LABEL description = 'my web service application'&lt;br /&gt;&lt;br /&gt;&lt;/sub&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RUN&lt;/b&gt; : 컨테이너 내부에서 실행할 명령어를 지정, &lt;u&gt;여러 개 입력 가능&lt;/u&gt;&lt;br /&gt;&lt;sub&gt; ex1 shell 방식) RUN apt update&lt;br /&gt;ex2 exec 방식) RUN [&quot;bin/bash&quot;, &quot;-c&quot;, &quot;apt -y install nginx git vim curl&quot;]&lt;br /&gt;&lt;br /&gt;&lt;/sub&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CMD&lt;/b&gt; : 컨테이너가 실행될 때 실행할 명령어, &lt;u&gt;하나만 입력 가능&lt;/u&gt;&lt;br /&gt;&lt;sub&gt; ex1 shell 방식) CMD nginx -g deamon off&lt;br /&gt;ex2 exec 방식) CMD [&quot;python&quot;, &quot;app.py&quot;]&lt;br /&gt;&lt;br /&gt;&lt;/sub&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ENTRYPOINT&lt;/b&gt; : CMD 와 마찬가지로 생성된 이미지가 컨테이너로 실행될 때 사용&lt;br /&gt;&lt;sub&gt; ex) ENTRYPOINT [&quot;npm&quot;, &quot;start&quot;]&lt;br /&gt;&lt;br /&gt;&lt;/sub&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;COPY&lt;/b&gt; : 호스트 환경의 파일이나 디렉토리를 이미지 내부로 복사할 때 사용&lt;br /&gt;빌드 작업 디렉토리 외부 파일은 복사할 수 없다&lt;br /&gt;&lt;sub&gt; ex) COPY index.html /usr/share/nginx/html&lt;br /&gt;&lt;br /&gt;&lt;/sub&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ADD&lt;/b&gt; : 호스트 환경의 파일이나 디렉토리를 이미지 내부로 복사하거나&lt;br /&gt;해당 URL에서 다운로드, 또는 압축파일을 풀어서 추가한다&lt;br /&gt;&lt;sub&gt; ex1) ADD index.html /usr/share/nginx/html&lt;br /&gt;ex2) ADD http://dockerhub.com/example.tar.gz /app&lt;br /&gt;ex3) ADD web.tar.gz /var/www/html&lt;br /&gt;&lt;br /&gt;&lt;/sub&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ENV&lt;/b&gt; : 이미지 내부에 환경 변수를 지정할 때 사용&lt;br /&gt;&lt;sub&gt; ex) ENV JAVA_HOME /usr/lib/jvm/java-8-oracle&lt;br /&gt;&lt;br /&gt;&lt;/sub&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;EXPOSE&lt;/b&gt; : 호스트 네트워크를 통해 컨테이너로 들어오는 트래픽을 listening 하기 위한 포트와 프로토콜 지정&lt;br /&gt;&lt;sub&gt; ex1) EXPOSE 80&lt;br /&gt;ex2) EXPOSE 8000/tcp&lt;br /&gt;&lt;br /&gt;&lt;/sub&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;VOLUME&lt;/b&gt; : 호스트와 공유하기 위한 컨테이너 볼륨 지정&lt;br /&gt;&lt;sub&gt; ex) VOLUME /etc/nginx&lt;br /&gt;&lt;br /&gt;&lt;/sub&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;USER&lt;/b&gt; : 컨테이너 사용자 변경, 기본은 root&lt;br /&gt;&lt;sub&gt; ex) USER gengminy&lt;br /&gt;&lt;br /&gt;&lt;/sub&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WORKDIR&lt;/b&gt; : 컨테이너에서 작업할 경로의 전환을 위한 명령어&lt;br /&gt;&lt;sub&gt; ex) WORKDIR /app&lt;br /&gt;&lt;br /&gt;&lt;/sub&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ARG&lt;/b&gt; : docker build 시점에서 인자 값을 전달하기 위한 명령어&lt;br /&gt;&lt;sub&gt; ex) ARG db_name ---&amp;gt; docker bulid --build-arg db_name=my_sql_server&lt;br /&gt;&lt;br /&gt;&lt;/sub&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ONBUILD&lt;/b&gt; : 현재 이미지가 다른 이미지의 기본 이미지(FROM)로 포함될 경우&lt;br /&gt;다른 이미지가 빌드될 때 실행시킬 명령어 현재 이미지에서는 실행되지 않는다&lt;br /&gt;&lt;sub&gt; ex) ONBUILD ADD source.tar.gz /usr/share/nginx&lt;br /&gt;&lt;br /&gt;&lt;/sub&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;STOPSIGNAL&lt;/b&gt; : docker stop 명령어 입력 시 컨테이너에 전달될 signal 지정&lt;br /&gt;&lt;sub&gt; ex) STOPSIGNAL SIGKILL&lt;br /&gt;&lt;br /&gt;&lt;/sub&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HEALTHCHECK&lt;/b&gt; : 컨테이너 프로세스 상태를 체크할 때 작성, &lt;u&gt;하나만 사용 가능&lt;/u&gt;&lt;br /&gt;&lt;sub&gt; ex) HEALTHCHECK --interval={체크간격(초)} --timeout={타임아웃(초)} --retries={타임아웃회수}&lt;br /&gt;&lt;br /&gt;&lt;/sub&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SHELL&lt;/b&gt; : Dockerfile 내부에서 사용할 기본 쉘을 지정&lt;br /&gt;&lt;sub&gt; ex) SHELL [&quot;bin/bash&quot;, &quot;-c&quot;]&lt;/sub&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Dockerfile 직접 작성해보기 (Spring Boot Project)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Dockerfile&lt;/h3&gt;
&lt;pre id=&quot;code_1659684734396&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM openjdk:11
EXPOSE 8000
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT [&quot;java&quot;,&quot;-jar&quot;,&quot;/app.jar&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FROM &lt;/b&gt;openjdk:11&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 베이스 이미지를 openjdk:11로 지정&lt;/p&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;EXPOSE&lt;/b&gt; 8000&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 8000번 포트를 listening (스프링 프로젝트의 기본 포트는 8080)&lt;/p&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;ARG&lt;/b&gt; JAR_FILE=build/libs/*.jar&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 스프링 프로젝트를 빌드할 때 나오는 결과물을 JAR_FILE 로 지정&lt;/p&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;COPY&lt;/b&gt; ${JAR_FILE} app.jar&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 빌드된 파일을을 app.jar 로 복사&lt;/p&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;ENTRYPOINT&lt;/b&gt; [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 생성된 이미지를 컨테이너로 실행하는 시점에 app.jar 실행, 즉 컨테이너 실행과 동시에 서버를 실행함&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  스프링 프로젝트 Gradle 빌드&lt;/h2&gt;
&lt;pre id=&quot;code_1659685830498&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./gradlew build -x test&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-x test : 테스트 없이 빌드하겠다는 의미&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;471&quot; data-origin-height=&quot;217&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SyjLG/btrI11Bi6Vq/AnM5VZ5jAJYvlbx6P1UMCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SyjLG/btrI11Bi6Vq/AnM5VZ5jAJYvlbx6P1UMCK/img.png&quot; data-alt=&quot;빌드 성공 시 /build/libs 에 .jar 파일이 만들어진다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SyjLG/btrI11Bi6Vq/AnM5VZ5jAJYvlbx6P1UMCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSyjLG%2FbtrI11Bi6Vq%2FAnM5VZ5jAJYvlbx6P1UMCK%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;471&quot; height=&quot;217&quot; data-origin-width=&quot;471&quot; data-origin-height=&quot;217&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;빌드 성공 시 /build/libs 에 .jar 파일이 만들어진다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Dockerfile 빌드&lt;/h2&gt;
&lt;pre id=&quot;code_1659686793605&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker  build  [플래그]  이미지명:[태그]  경로|URL|압축파일&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;[플래그]&lt;/b&gt;&lt;br /&gt;-t : tag를 지정하는 옵션&lt;br /&gt;-f : Dockerfile 이외의 다른 파일명을 지정하는 옵션&lt;br /&gt;--platform : 애플 M1의 경우 플랫폼을 linux/amd64 로 지정해주어야 빌드가 됨&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이미지명:[태그]&lt;/b&gt;&lt;br /&gt;생성할 이미지 이름 및 태그를 지정, 태그는 보통 버전 관리용으로 지정&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경로 | URL | 압축파일&lt;/b&gt;&lt;br /&gt;디렉토리 경로나 Dokerfile 이 포함된 깃허브 url, 또는 압축파일을 지정한다&lt;/li&gt;
&lt;/ul&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;pre id=&quot;code_1659687261618&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker build -t {username}/{imagename} .&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;반드시 마지막에 Dockerfile 의 경로를 표시해주어야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 현재 디렉토리에 Dockerfile 이 있기 때문에 점을 찍는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Docker hub 에 이미지를 올릴 것이라면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반드시 {username} 에 본인의 유저 이름이 들어가야 한다&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;blob&quot; data-origin-width=&quot;2175&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dE3CtK/btrI1hq7ZmR/EW5iheVseSU8ZmUw8mD77k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dE3CtK/btrI1hq7ZmR/EW5iheVseSU8ZmUw8mD77k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dE3CtK/btrI1hq7ZmR/EW5iheVseSU8ZmUw8mD77k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdE3CtK%2FbtrI1hq7ZmR%2FEW5iheVseSU8ZmUw8mD77k%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;2175&quot; height=&quot;106&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2175&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시라도 docker deamon is not runnig 오류가 나온다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker desktop 을 실행하고 빌드하면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2189&quot; data-origin-height=&quot;275&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ky2pk/btrI2vPIahW/edQ6r1obZl8I2VBagbIDeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ky2pk/btrI2vPIahW/edQ6r1obZl8I2VBagbIDeK/img.png&quot; data-alt=&quot;빌드 실행&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ky2pk/btrI2vPIahW/edQ6r1obZl8I2VBagbIDeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fky2pk%2FbtrI2vPIahW%2FedQ6r1obZl8I2VBagbIDeK%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;2189&quot; height=&quot;275&quot; data-origin-width=&quot;2189&quot; data-origin-height=&quot;275&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;빌드 실행&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2156&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TUH07/btrI1iKlk57/PgdHkOugz2fIWUtBPkaqOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TUH07/btrI1iKlk57/PgdHkOugz2fIWUtBPkaqOk/img.png&quot; data-alt=&quot;빌드 완료&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TUH07/btrI1iKlk57/PgdHkOugz2fIWUtBPkaqOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTUH07%2FbtrI1iKlk57%2FPgdHkOugz2fIWUtBPkaqOk%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;2156&quot; height=&quot;305&quot; data-origin-width=&quot;2156&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;빌드 완료&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Docker image push&lt;/h2&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;a href=&quot;https://hub.docker.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://hub.docker.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1659687525859&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;Docker Hub Container Image Library | App Containerization&quot; data-og-description=&quot;We and third parties use cookies or similar technologies (&amp;quot;Cookies&amp;quot;) as described below to collect and process personal data, such as your IP address or browser information. You can learn more about how this site uses Cookies by reading our privacy policy &quot; data-og-host=&quot;hub.docker.com&quot; data-og-source-url=&quot;https://hub.docker.com/&quot; data-og-url=&quot;https://hub.docker.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/AG59O/hyPkJAvplC/BEzHLH3BgNvDquqKgg9tRk/img.png?width=416&amp;amp;height=250&amp;amp;face=0_0_416_250&quot;&gt;&lt;a href=&quot;https://hub.docker.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hub.docker.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/AG59O/hyPkJAvplC/BEzHLH3BgNvDquqKgg9tRk/img.png?width=416&amp;amp;height=250&amp;amp;face=0_0_416_250');&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;Docker Hub Container Image Library | App Containerization&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;We and third parties use cookies or similar technologies (&quot;Cookies&quot;) as described below to collect and process personal data, such as your IP address or browser information. You can learn more about how this site uses Cookies by reading our privacy policy&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hub.docker.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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1659687464564&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker login&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker login 명령어 실행 후&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker hub 에 로그인 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1659688048360&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker push {username}/{imagename}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2130&quot; data-origin-height=&quot;369&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfZR6b/btrIXQH8RIP/AlX3QESYprPZpJtK1MyfK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfZR6b/btrIXQH8RIP/AlX3QESYprPZpJtK1MyfK1/img.png&quot; data-alt=&quot;푸시 완료&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfZR6b/btrIXQH8RIP/AlX3QESYprPZpJtK1MyfK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfZR6b%2FbtrIXQH8RIP%2FAlX3QESYprPZpJtK1MyfK1%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;2130&quot; height=&quot;369&quot; data-origin-width=&quot;2130&quot; data-origin-height=&quot;369&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;푸시 완료&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;553&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIYT8Z/btrI1OCfJ0N/IfDu4CkhvamRikZFuOB1g0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIYT8Z/btrI1OCfJ0N/IfDu4CkhvamRikZFuOB1g0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIYT8Z/btrI1OCfJ0N/IfDu4CkhvamRikZFuOB1g0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIYT8Z%2FbtrI1OCfJ0N%2FIfDu4CkhvamRikZFuOB1g0%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;1406&quot; height=&quot;553&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;553&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;나중에 배포 서버에서 도커 허브에 접근하여&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;나중에 Github Action 이랑 연동하면&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;</description>
      <category>  Infra/  Docker</category>
      <category>docker</category>
      <category>Dockerfile</category>
      <category>dockerhub</category>
      <category>도커</category>
      <category>도커파일</category>
      <category>도커허브</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/25</guid>
      <comments>https://gengminy.tistory.com/25#entry25comment</comments>
      <pubDate>Fri, 5 Aug 2022 17:32:08 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링 부트 프로젝트에서 dotenv 환경변수 파일 사용하기</title>
      <link>https://gengminy.tistory.com/24</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 프레임워크를 사용하는 웹앱 프로젝트를 진행하면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경변수를 저장하기 위한 방법을 검색해보았다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 보통 .env.properties 파일을 만들어서&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;.gitignore 에다가 등록을 하면 되지만&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;.env 파일을 사용해서 환경변수를 불러오는 방법을 도입시켜보았다&lt;/p&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;  /.env&lt;/p&gt;
&lt;pre id=&quot;code_1659502868687&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PORT=8000
JWT_SECRET_KEY=&quot;MYSUPERSECRETJWTKEY&quot;
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 프로젝트 루트 디렉토리에 .env 파일을 만들었다&lt;/p&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;  /.gitignore&lt;/p&gt;
&lt;pre id=&quot;code_1659502898708&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
.env*&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;git을 사용중이라면 반드시 이 설정 파일을 gitignore에 등록해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;git에 딸려서 리모트 레포지토리에 올라가지 않도록 하자&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;  /src/main/resources/application.yml&lt;/p&gt;
&lt;pre id=&quot;code_1659503008155&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server:
  port: ${PORT:8000}

spring:
  # .env import
  config:
    import: optional:file:.env[.properties]
  # Using POSTGRESQL
  datasource:
    url: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
    username: ${POSTGRES_USER}
    password: ${POSTGRES_PASSWORD}
    driver-class-name: org.postgresql.Driver

  jpa:
    database: postgresql
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        database-platform: org.hibernate.dialect.PostgreSQLDialect

  thymeleaf:
    cache: false
    prefix: classpath:/templates/
    suffix: .html

logging.level:
    org.hibernate.SQL: debug
    org.hibernate.type: trace&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 프로젝트의 설정 파일이다 yml 형식을 사용중이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 부분은 저 spring:config:import: 에다 .env 파일을 import 해주어야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1659503080356&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  config:
    import: optional:file:.env[.properties]&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;.env를 .env.properties 로 해석하여 가져오겠다는 뜻이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;optional을 붙여서 .env 파일이 있을 때만 들고오며&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;또한 동일 yml 파일에서 환경변수 값을 읽을 때는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;${변수명}&lt;/b&gt; 으로 가져오면 된다&lt;/p&gt;
&lt;pre id=&quot;code_1659503158355&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  datasource:
    url: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
    username: ${POSTGRES_USER}
    password: ${POSTGRES_PASSWORD}
    driver-class-name: org.postgresql.Driver&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본값을 지정하고 싶다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;${변수명:기본값}&lt;/b&gt; 이런식으로 콜론을 붙여주면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 환경변수가 없을 때 자동으로 기본 값으로 들어가게 된다&lt;/p&gt;
&lt;pre id=&quot;code_1659503496237&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server:
  port: ${PORT:8000}&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;  .../JwtTokenProvider&lt;/p&gt;
&lt;pre id=&quot;code_1659503258885&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Component
public class JwtTokenProvider {
    /** 토큰 비밀 키 */
    @Value(&quot;${JWT_SECRET_KEY}&quot;)
    private String JWT_SECRET;

	...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 클래스 내에서 읽을 때는 클래스의 제일 바깥 블록 안에다가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Value&lt;/b&gt; 에노테이션을 사용해서 가져온다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래에 해당 값을 주입시킬 변수도 써주면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1227&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pPPWR/btrIPX7avcP/Kz7tSJ5sUOUpxhdSM73xN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pPPWR/btrIPX7avcP/Kz7tSJ5sUOUpxhdSM73xN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pPPWR/btrIPX7avcP/Kz7tSJ5sUOUpxhdSM73xN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpPPWR%2FbtrIPX7avcP%2FKz7tSJ5sUOUpxhdSM73xN0%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;1227&quot; height=&quot;304&quot; data-origin-width=&quot;1227&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 프레임워크 빈 팩토리의 것을 import 해주면 된다&lt;/p&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;</description>
      <category>  백엔드/  Spring Boot</category>
      <category>.env</category>
      <category>dotenv</category>
      <category>Java</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>스프링</category>
      <category>스프링부트</category>
      <category>자바</category>
      <category>환경변수</category>
      <category>환경변수파일</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/24</guid>
      <comments>https://gengminy.tistory.com/24#entry24comment</comments>
      <pubDate>Wed, 3 Aug 2022 14:12:37 +0900</pubDate>
    </item>
    <item>
      <title>[Gosrock/Nestjs] Socket.io 사용하여 실시간 공연 입장 시스템 구현하기</title>
      <link>https://gengminy.tistory.com/23</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;학교 컴공 밴드 동아리 고스락 여름방학 프로젝트인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고스락 티켓 예매 페이지 22th 의 일부인 socket 구현에 대한 글입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nestjs + socket.io 를 사용하여 구현하였습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Reference&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nestjs + socket.io(EventsGateway) -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=gkJ1N6PDCEc&amp;amp;t=690s&quot;&gt;https://www.youtube.com/watch?v=gkJ1N6PDCEc&amp;amp;t=690s&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;chat app with nestjs -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=7xpLYk4q0Sg&amp;amp;t=722s&quot;&gt;https://www.youtube.com/watch?v=7xpLYk4q0Sg&amp;amp;t=722s&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docs nest js (gateway) -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.nestjs.com/websockets/gateways&quot;&gt;https://docs.nestjs.com/websockets/gateways&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  socket.io 모듈 설치&lt;/h2&gt;
&lt;pre id=&quot;code_1659174272185&quot; class=&quot;moonscript&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i @nestjs/websockets @nestjs/platform-socket.io --save&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  gateway 생성&lt;/h2&gt;
&lt;pre id=&quot;code_1659174272186&quot; class=&quot;ebnf&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;nest g ga socket&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;socket 모듈 아래에 게이트웨이를 생성한다&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  socket 모듈 구조&lt;/h2&gt;
&lt;pre id=&quot;code_1659174272186&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;backend
...
└───socket
│   │   socket-admin.gateway.ts
│   │   socket-user.gateway.ts
│   │   socket.guard.ts
│   │   socket-module.ts
│   │   socket-service.ts
└───tickets
│   │   tickets-service.ts
│   │   ...
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt; &lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Gateway 구현&lt;/span&gt;&lt;/span&gt;&lt;/h2&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;SocketUserGateway 와 SocketAdminGateway&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘솔에 찍어서 확인해본 결과 게이트웨이를 나누어 init 해줘도&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;  socket-user.gateway.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1659174272186&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// @UseGuards(SocketGuard)
@WebSocketGateway({
  cors: {
    origin: '*'
  },
  namespace: '/socket/user' //socket/admin or socket/user
})
export class SocketUserGateway
  implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
  private readonly logger = new Logger(SocketUserGateway.name);
  constructor(
    @Inject(forwardRef(() =&amp;gt; TicketsService))
    private ticketsService: TicketsService
  ) {}
  @WebSocketServer() public io: Namespace;

  //interface 구현부

  afterInit(server: Server) {
    this.logger.log('SocketUserGateway Init');
  }

  //티켓 uuid로 존재하는 티켓인지 검사 후 연결
  async handleConnection(@ConnectedSocket() client: Socket) {
    try {
      const ticketUuid =
        process.env.NODE_ENV == 'dev'
          ? client.handshake.headers.authorization
          : client.handshake.auth?.ticketUuid;

      if (!ticketUuid) {
        throw new UnauthorizedException('잘못된 헤더 요청');
      }

      const ticket = await this.ticketsService.findByUuidSocket(ticketUuid);
      if (!ticket) {
        throw new UnauthorizedException('없는 유저입니다.');
      }
      this.logger.log(`${client.id} connected`);

      //room: uuid로 강제 연결
      client.join(ticketUuid);
    } catch (e) {
      this.logger.error(
        `${client.id} 연결 강제 종료, status: ${e.status}, ${e.message}`
      );
      client.disconnect();
    }
  }

  handleDisconnect(@ConnectedSocket() client: Socket) {
    this.logger.log(`${client.id} disconnected`);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 정의 위에 @WebSocketGateway 데코레이터를 달아준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에서 네임스페이스 이름과 cors 정책을 변경할 수 있다&lt;/p&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;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;네임스페이스는 채널, 룸은 채팅방&lt;/b&gt;이라고 생각하면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게이트웨이는 interface 상속해서 세 가지 메서드를 구현해야 한다&lt;span&gt;&amp;nbsp;&lt;/span&gt;구현하는 것이 반드시 좋다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;afterInit, handleConnection, handleDisconnect&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 socket.on 사용해서 connection, disconnection 구현했던 거와 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  afterinit - 초기 생성 시 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  handleConnection - 유저가 연결 시도할 때 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  handleConnection - 유저가 연결 해제 시도할 때 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;constructor 대신 @WebSocketServer() 로 의존성을 주입 받는다(DI)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DI 오브젝트에 Server와 Namespace 를 적을 수 있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 외부 네임스페이스를 가져오기 위해 Namespace 형태로 주입시켜주었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜인지는 모르겠지만 Server로 주입하면 server.of('admin') 형태로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 네임스페이스와 연결할 수가 없었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;of 메서드가 정의되지 않았다면서 말이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@SubscribeMessage('') 를 통해 해당 룸에 대한 리스너를 달아줄 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;node 같은데서 했던 sockett.on('message', () =&amp;gt; {}) 이런 구문과 같음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매개변수로 @ConnectedSocket 은 현재 연결중인 소켓 정보를 가져올 수 있고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@MessageBody로 메세지 정보를 가져올 수 있음&lt;/p&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;socket 구조.png&quot; data-origin-width=&quot;1998&quot; data-origin-height=&quot;1168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/by5bKn/btrIyZxEU62/uQZZBxVLrAkl8f6Gk6GaN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/by5bKn/btrIyZxEU62/uQZZBxVLrAkl8f6Gk6GaN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/by5bKn/btrIyZxEU62/uQZZBxVLrAkl8f6Gk6GaN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fby5bKn%2FbtrIyZxEU62%2FuQZZBxVLrAkl8f6Gk6GaN1%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;1998&quot; height=&quot;1168&quot; data-filename=&quot;socket 구조.png&quot; data-origin-width=&quot;1998&quot; data-origin-height=&quot;1168&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;결국 구현하고자 하는 것은 실시간 티켓 입장 확인 이벤트 처리를 소켓으로 하고자 하는 거였는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 유저가 /tickets/{ticketUuid} 라는 url에 접근&lt;br /&gt;티켓 uuid 가 담긴 QR 코드를 띄움과 동시에 socket 채널에 입장시켜야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 프론트 단에서 socket 요청을 날릴 때 header.authorization 에 담아서 보내도록 말해놨다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;여기서 티켓의 uuid 를 뽑아와 유효성 검사를 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유효성 검사를 마치면 uuid 룸에 강제로 join 시켜버렸다&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;&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;  socket-admin.gateway.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1659174272188&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// @UseGuards(SocketGuard)
// @Roles(Role.Admin)
@WebSocketGateway({
  cors: {
    origin: '*'
  },
  namespace: '/socket/admin' //socket/admin or socket/user
})
export class SocketAdminGateway
  implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
  private readonly logger = new Logger(SocketAdminGateway.name);
  constructor(private authService: AuthService) {}

  @WebSocketServer() public io: Namespace;

  //interface 구현부

  afterInit(server: Server) {
    // remove the namespace
    this.io.server._nsps.delete('/');
    this.logger.log('SocketAdminGateway Init');
  }

  //소켓 헤더에서 엑세스토큰 검사
  async handleConnection(@ConnectedSocket() client: Socket) {
    try {
      const accessToken =
        process.env.NODE_ENV == 'dev'
          ? client.handshake.headers.authorization
          : client.handshake.auth?.token;

      if (!accessToken) {
        throw new UnauthorizedException('잘못된 헤더 요청');
      }
      const payload = this.authService.verifyAccessJWT(accessToken);

      const user = await this.authService.findUserById(payload.id);
      if (!user) {
        throw new UnauthorizedException('없는 유저입니다.');
      }
      if (user.role !== Role.Admin) {
        throw new UnauthorizedException('권한이 없습니다');
      }
      this.logger.log(`${client.id} connected`);
    } catch (e) {
      this.logger.error(
        `${client.id} 연결 강제 종료, status: ${e.status}, ${e.message}`
      );
      client.disconnect();
    }
  }

  handleDisconnect(@ConnectedSocket() client: Socket) {
    this.logger.log(`${client.id} disconnected`);
  }
}&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;3. 어드민은 티켓을 QR 리더로 찍었을때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/tickets/{ticketUuid}/enter 라는 url 로 이동하게되고&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;  socket.service.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1659174272190&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Injectable()
export class SocketService {
  constructor(
    @Inject(forwardRef(() =&amp;gt; SocketUserGateway))
    private userGateway: SocketUserGateway,
    @Inject(forwardRef(() =&amp;gt; SocketAdminGateway))
    private adminGateway: SocketAdminGateway
  ) {}

  async emitToUser(ticketEntryResponseDto: TicketEntryResponseDto) {
    try {
      const { uuid } = ticketEntryResponseDto;
      this.userGateway.io.emit(uuid, ticketEntryResponseDto);
    } catch (error) {
      console.log(error);
      throw new GatewayTimeoutException('소켓 서버에 연결할 수 없습니다');
    }
  }

  async emitToAdmin(ticketEntryResponseDto: TicketEntryResponseDto) {
    try {
      this.adminGateway.io.emit('enter', ticketEntryResponseDto);
    } catch (error) {
      console.log(error);
      throw new GatewayTimeoutException('소켓 서버에 연결할 수 없습니다');
    }
  }

  //양쪽
  async emitToAll(ticketEntryResponseDto: TicketEntryResponseDto) {
    await this.emitToUser(ticketEntryResponseDto);
    await this.emitToAdmin(ticketEntryResponseDto);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 해당 url 에 접근하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저의 ticketUuid 룸과 어드민의 enter 룸에 메세지를 보낸다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트 단에서 admin 은 enter 메세지를 구독중이니까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 입장 확인은 admin 도 가능하다&lt;/p&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;  tickets.service.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1659174947489&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
   * 어드민이 티켓을 찍었을때 연결할 url에서 검증을 완료한 후 소켓 메세지 전송
   * @param uuid TicketValidationDto -&amp;gt; uuid
   * @param admin 현재 로그인 중인 어드민
   */
  async entryValidation(
    ticketEntryDateValidationDto: TicketEntryDateValidationDto,
    uuid: string,
    admin: User
  ): Promise&amp;lt;TicketEntryResponseDto&amp;gt; {
    this.logger.log('TicketEntryValidation');

    const queryRunner = this.dataSource.createQueryRunner();

    await queryRunner.connect();
    await queryRunner.startTransaction();

    const connectedRepository = getConnectedRepository(
      TicketRepository,
      queryRunner,
      Ticket
    );

    try {
      const { date } = ticketEntryDateValidationDto;
      const ticket = await connectedRepository.findByUuid(uuid);

      // 티켓 날짜 오류(공연 날짜가 일치하지 않음)
      if (ticket.date !== date) {
        const failureResponse = new TicketEntryResponseDto(
          ticket,
          admin.name,
          false,
          '[입장실패] 공연 날짜가 일치하지 않습니다'
        );
        this.socketService.emitToAll(failureResponse);
        throw new BadRequestException('공연 날짜가 일치하지 않습니다');
      }

      // 티켓 상태 오류('입장대기'가 아님)
      if (ticket.status !== TicketStatus.ENTERWAIT) {
        const failureResponse = new TicketEntryResponseDto(
          ticket,
          admin.name,
          false,
          '[입장실패] 이미 입장 완료된 티켓입니다'
        );
        this.socketService.emitToAll(failureResponse);
        throw new BadRequestException('이미 입장 완료된 티켓입니다');
      }

      //성공 시
      ticket.status = TicketStatus.DONE;
      ticket.admin = admin;

      await connectedRepository.saveTicket(ticket);

      await queryRunner.commitTransaction();

      const successResponse = new TicketEntryResponseDto(
        ticket,
        admin.name,
        true,
        `[입장성공] ${ticket.user?.name}님이 입장하셨습니다`
      );
      this.logger.log(`${ticket.user?.name}님이 입장하셨습니다`);
      this.socketService.emitToAll(successResponse);
      return successResponse;
    } catch (e) {
      await queryRunner.rollbackTransaction();
      this.logger.error(`티켓 상태 오류 - ${e.message}`);
      // 내부 예외 그대로 던짐
      throw e;
    } finally {
      await queryRunner.release();
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/tickets/{ticektUuid}/enter 접근 시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TicketsController 에서 호출하는 비즈니스 로직&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티켓 입장 상태와 티켓 건드린 어드민을 넣어줘야 해서 트랜잭션으로&amp;nbsp; 처리햇다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 조건을 통과하면 socketService 의 emit 로직을 호출했다&lt;/p&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;  socket.guard.ts&lt;/h3&gt;
&lt;pre id=&quot;code_1659174272190&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Injectable()
export class SocketGuard implements CanActivate {
  private readonly logger = new Logger(SocketGuard.name);
  constructor(private authService: AuthService, private reflector: Reflector) {}

  canActivate(
    context: ExecutionContext
  ): boolean | Promise&amp;lt;boolean&amp;gt; | Observable&amp;lt;boolean&amp;gt; {
    const client: Socket = context.switchToWs().getClient&amp;lt;Socket&amp;gt;();
    return this.validateHeader(client, context);
  }

  public async validateHeader(client: Socket, context: ExecutionContext) {
    //가드에 걸리면 에러 리턴 + 소켓 강제 연결 종료
    try {
      const accessToken =
        process.env.NODE_ENV == 'dev'
          ? client.handshake.headers.authorization
          : client.handshake.auth?.accessToken;

      if (!accessToken) {
        throw new UnauthorizedException('잘못된 헤더 요청');
      }
      if (Array.isArray(accessToken)) {
        throw new UnauthorizedException('잘못된 헤더 요청');
      }
      const payload = this.authService.verifyAccessJWT(accessToken);

      const roles = this.reflector.getAllAndOverride&amp;lt;string[]&amp;gt;('roles', [
        context.getHandler(),
        context.getClass()
      ]);

      const user = await this.authService.findUserById(payload.id);
      if (!user) {
        throw new UnauthorizedException('없는 유저입니다.');
      }
      const newObj: any = client;
      newObj.user = user;
      context.switchToWs().getData().user = user;

      // 롤기반 체크
      if (!roles) {
        return true;
      }
      if (!roles.length) {
        return true;
      } else {
        if (roles.includes(user.role) === true) {
          return true;
        } else if (user.role === Role.Admin) {
          return true;
        } else {
          throw new UnauthorizedException('권한이 없습니다.');
        }
      }
    } catch (e) {
      this.logger.error(
        `${client.id} 연결 강제 종료, status: ${e.status}, ${e.message}`
      );
      client.disconnect();
      throw new WsException(e.message);
    }
  }
}&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;그 중의 일부인 SocketGuard 이다&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;Nestjs 공식 레퍼런스 페이지에서는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소켓에서도 Guard를 사용할 수 있다 해서 SocketGuard를 구현했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 이건&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;소켓의 이벤트,&lt;span&gt;&amp;nbsp;&lt;/span&gt;예를 들면 @SubscribeMessage 같은 거만 막을 수 있다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소켓 커넥션 자체를 막을 수 있는게 아니였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 구현해야 하는 건&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;handleConnection 에서 접근 자체를 막는 것으로 바뀌었다&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;✨ Postman 으로 소켓 연결 테스트&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1599&quot; data-origin-height=&quot;1187&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kCKe6/btrIC0bd8MY/CdYtQ6qFTY2uaOYPIY1Lz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kCKe6/btrIC0bd8MY/CdYtQ6qFTY2uaOYPIY1Lz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kCKe6/btrIC0bd8MY/CdYtQ6qFTY2uaOYPIY1Lz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkCKe6%2FbtrIC0bd8MY%2FCdYtQ6qFTY2uaOYPIY1Lz1%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;1599&quot; height=&quot;1187&quot; data-origin-width=&quot;1599&quot; data-origin-height=&quot;1187&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트는 postman 에 소켓 연결 테스트가 있길래 그것으로 했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저 테스트는 headers -&amp;gt; authorization 에 ticketUuid 를 끼우고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Listeners 에 해당 ticketUuid 를 끼우면 구독이 된다 (수동으로 해주어야됨)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1587&quot; data-origin-height=&quot;1121&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rUtHi/btrIyxafQmT/whLfm4AxdFkgXAKAU4Xuw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rUtHi/btrIyxafQmT/whLfm4AxdFkgXAKAU4Xuw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rUtHi/btrIyxafQmT/whLfm4AxdFkgXAKAU4Xuw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrUtHi%2FbtrIyxafQmT%2FwhLfm4AxdFkgXAKAU4Xuw1%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;1587&quot; height=&quot;1121&quot; data-origin-width=&quot;1587&quot; data-origin-height=&quot;1121&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어드민은 헤더에 엑세스 토큰을 끼우고 db에서 권한을 admin 으로 바꿔준 다음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;listener에 enter 등록해주면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러고 tickets/{ticketUuid}/enter 에 접근해서 돌리면 테스트가 된다&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2185&quot; data-origin-height=&quot;93&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3rKi6/btrIvSfonFq/0g9k2jA15Vw9k2qPRkE9C1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3rKi6/btrIvSfonFq/0g9k2jA15Vw9k2qPRkE9C1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3rKi6/btrIvSfonFq/0g9k2jA15Vw9k2qPRkE9C1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3rKi6%2FbtrIvSfonFq%2F0g9k2jA15Vw9k2qPRkE9C1%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;2185&quot; height=&quot;93&quot; data-origin-width=&quot;2185&quot; data-origin-height=&quot;93&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1223&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2b7lA/btrIxTYt3ks/x4hTwb0YKWcNuEBANH8gAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2b7lA/btrIxTYt3ks/x4hTwb0YKWcNuEBANH8gAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2b7lA/btrIxTYt3ks/x4hTwb0YKWcNuEBANH8gAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2b7lA%2FbtrIxTYt3ks%2Fx4hTwb0YKWcNuEBANH8gAK%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;1223&quot; height=&quot;640&quot; data-origin-width=&quot;1223&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소켓 둘다 연결시킨 다음에 swagger로 요청 테스트&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;999&quot; data-origin-height=&quot;419&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9pDOy/btrIyvQ6adk/mV4tKnCzqRhbkle15ADxw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9pDOy/btrIyvQ6adk/mV4tKnCzqRhbkle15ADxw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9pDOy/btrIyvQ6adk/mV4tKnCzqRhbkle15ADxw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9pDOy%2FbtrIyvQ6adk%2FmV4tKnCzqRhbkle15ADxw0%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;999&quot; height=&quot;419&quot; data-origin-width=&quot;999&quot; data-origin-height=&quot;419&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;어드민이랑 유저랑 동일함&lt;/p&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;</description>
      <category>  프로젝트/  고스락 티켓</category>
      <category>nestjs</category>
      <category>nodejs</category>
      <category>socketio</category>
      <category>소켓</category>
      <category>소켓통신</category>
      <category>실시간</category>
      <category>티켓</category>
      <category>프로젝트</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/23</guid>
      <comments>https://gengminy.tistory.com/23#entry23comment</comments>
      <pubDate>Sat, 30 Jul 2022 18:46:42 +0900</pubDate>
    </item>
    <item>
      <title>[Github] 이슈 템플릿, PR 템플릿 등록 (Issue Template, Pull Request Template)</title>
      <link>https://gengminy.tistory.com/22</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;매번 개발 뛰어나게 잘하는 친구의 도움을 받아&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체계적이게 깃허브를 사용하는 법을 배우는 중이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이드 프로젝트로 내가 조금? 주도적인 프로젝트를 하게 되었는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 누가 편하게 쓰라고 만들어놨던 템플릿들을 쓰기만 했다면&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;이슈 템플릿과 PR 템플릿을 만드는 과정을 써보았다&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  이슈 템플릿 생성하기&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 해당 프로젝트 레포 -&amp;gt; Settings&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YiI3r/btrH74GyGPZ/HsKAzN3HHXWwF1H8B7Pc4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YiI3r/btrH74GyGPZ/HsKAzN3HHXWwF1H8B7Pc4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YiI3r/btrH74GyGPZ/HsKAzN3HHXWwF1H8B7Pc4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYiI3r%2FbtrH74GyGPZ%2FHsKAzN3HHXWwF1H8B7Pc4k%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;1310&quot; height=&quot;254&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;254&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;2. 내리다 보면 Features -&amp;gt; Issue -&amp;gt; Set up templates&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;1150&quot; data-origin-height=&quot;571&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dWvTUF/btrIcHwRuoz/pwnSKc8fCKNBIlIjeDuFIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dWvTUF/btrIcHwRuoz/pwnSKc8fCKNBIlIjeDuFIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dWvTUF/btrIcHwRuoz/pwnSKc8fCKNBIlIjeDuFIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdWvTUF%2FbtrIcHwRuoz%2FpwnSKc8fCKNBIlIjeDuFIk%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;1150&quot; height=&quot;571&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;1150&quot; data-origin-height=&quot;571&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;3. 템플릿 종류를 하나 선택한다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;541&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9Xzho/btrH9IDd5iG/FcaenA39W9hp4pM0IFwFkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9Xzho/btrH9IDd5iG/FcaenA39W9hp4pM0IFwFkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9Xzho/btrH9IDd5iG/FcaenA39W9hp4pM0IFwFkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9Xzho%2FbtrH9IDd5iG%2FFcaenA39W9hp4pM0IFwFkk%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;1166&quot; height=&quot;541&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;541&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;4. 그러면 템플릿이 하나 만들어지는데 Preview and edit 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;4.png&quot; data-origin-width=&quot;1155&quot; data-origin-height=&quot;282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1ooaq/btrIaPPFlEv/gULcDos49w4KV5FTW5Nt90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1ooaq/btrIaPPFlEv/gULcDos49w4KV5FTW5Nt90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1ooaq/btrIaPPFlEv/gULcDos49w4KV5FTW5Nt90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1ooaq%2FbtrIaPPFlEv%2FgULcDos49w4KV5FTW5Nt90%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;1155&quot; height=&quot;282&quot; data-filename=&quot;4.png&quot; data-origin-width=&quot;1155&quot; data-origin-height=&quot;282&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;5. 연필 아이콘 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;5.png&quot; data-origin-width=&quot;1167&quot; data-origin-height=&quot;1001&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JBqNh/btrIcIbqqIb/KRZZjN7zYsB0giyi0cXOQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JBqNh/btrIcIbqqIb/KRZZjN7zYsB0giyi0cXOQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JBqNh/btrIcIbqqIb/KRZZjN7zYsB0giyi0cXOQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJBqNh%2FbtrIcIbqqIb%2FKRZZjN7zYsB0giyi0cXOQ1%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;1167&quot; height=&quot;1001&quot; data-filename=&quot;5.png&quot; data-origin-width=&quot;1167&quot; data-origin-height=&quot;1001&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;6. 내용과 제목, 설명 등을 적어주면 된다 마크다운으로 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다 만들면 꼭 Close Preview 눌러주자 아니면 사라짐&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;6.png&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;1180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckvaS3/btrH5faL2AT/kZ3P5VV8HMN1NttELnNndK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckvaS3/btrH5faL2AT/kZ3P5VV8HMN1NttELnNndK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckvaS3/btrH5faL2AT/kZ3P5VV8HMN1NttELnNndK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckvaS3%2FbtrH5faL2AT%2FkZ3P5VV8HMN1NttELnNndK%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;1128&quot; height=&quot;1180&quot; data-filename=&quot;6.png&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;1180&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;7. 커밋 날리면 이렇게 레포 루트 폴더에 .github 하위에 템플릿들이 만들어짐&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;7.png&quot; data-origin-width=&quot;823&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bw45uF/btrH6IX1x8t/sWdUdG05stFfcPfJEkHcKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bw45uF/btrH6IX1x8t/sWdUdG05stFfcPfJEkHcKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bw45uF/btrH6IX1x8t/sWdUdG05stFfcPfJEkHcKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbw45uF%2FbtrH6IX1x8t%2FsWdUdG05stFfcPfJEkHcKk%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;823&quot; height=&quot;406&quot; data-filename=&quot;7.png&quot; data-origin-width=&quot;823&quot; data-origin-height=&quot;406&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;8. 이제 Issue 카테고리에 들어가서 이슈를 생성하면 이렇게 템플릿을 사용할 수 있다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;8.png&quot; data-origin-width=&quot;1881&quot; data-origin-height=&quot;395&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0EQsk/btrH6IqeCf4/hVFxPlup4soSJhW3W1HADk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0EQsk/btrH6IqeCf4/hVFxPlup4soSJhW3W1HADk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0EQsk/btrH6IqeCf4/hVFxPlup4soSJhW3W1HADk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0EQsk%2FbtrH6IqeCf4%2FhVFxPlup4soSJhW3W1HADk%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;1881&quot; height=&quot;395&quot; data-filename=&quot;8.png&quot; data-origin-width=&quot;1881&quot; data-origin-height=&quot;395&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;9.png&quot; data-origin-width=&quot;1405&quot; data-origin-height=&quot;1186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uUvqa/btrIbNjNkV4/1EKkfccsGv1P98K9UYlpHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uUvqa/btrIbNjNkV4/1EKkfccsGv1P98K9UYlpHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uUvqa/btrIbNjNkV4/1EKkfccsGv1P98K9UYlpHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuUvqa%2FbtrIbNjNkV4%2F1EKkfccsGv1P98K9UYlpHK%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;1405&quot; height=&quot;1186&quot; data-filename=&quot;9.png&quot; data-origin-width=&quot;1405&quot; data-origin-height=&quot;1186&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;컨벤션이 지켜지니 체계적으로 협업을 하기 쉬워진다&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;  PR 템플릿 생성하기&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 해당 프로젝트 레포 -&amp;gt; .github 폴더로 이동&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;10.png&quot; data-origin-width=&quot;877&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rkMCd/btrH3b7IdWW/qK3V7NTJi2thCCtlOlkdbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rkMCd/btrH3b7IdWW/qK3V7NTJi2thCCtlOlkdbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rkMCd/btrH3b7IdWW/qK3V7NTJi2thCCtlOlkdbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrkMCd%2FbtrH3b7IdWW%2FqK3V7NTJi2thCCtlOlkdbk%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;877&quot; height=&quot;328&quot; data-filename=&quot;10.png&quot; data-origin-width=&quot;877&quot; data-origin-height=&quot;328&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;나는 {root}/.github 에 PULL_REQUEST_TEMPLATE.md 파일을 생성해서&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;Pull Request 날리기 전에 양식으로 해당 글이 잘 나온다&lt;/p&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;</description>
      <category>  Infra/⚙ 준비를 위한 준비</category>
      <category>GIT</category>
      <category>github</category>
      <category>Issue</category>
      <category>PR</category>
      <category>pullrequest</category>
      <category>template</category>
      <category>깃허브</category>
      <category>이슈</category>
      <category>이슈템플릿</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/22</guid>
      <comments>https://gengminy.tistory.com/22#entry22comment</comments>
      <pubDate>Mon, 25 Jul 2022 14:20:12 +0900</pubDate>
    </item>
    <item>
      <title>[NestJs] 따라하면서 배우는 NestJs - 9 (로그, 설정)</title>
      <link>https://gengminy.tistory.com/20</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bl58Rq/btrGRiMuXTa/Z8vkKEdkQVveHqXNUTPwK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bl58Rq/btrGRiMuXTa/Z8vkKEdkQVveHqXNUTPwK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bl58Rq/btrGRiMuXTa/Z8vkKEdkQVveHqXNUTPwK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbl58Rq%2FbtrGRiMuXTa%2FZ8vkKEdkQVveHqXNUTPwK0%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;300&quot; height=&quot;299&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ Logger 모듈 추가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; ./src/main.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657342100728&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
    const logger = new Logger();
    const port = 3000;
    const app = await NestFactory.create(AppModule);
    await app.listen(port);
    logger.log(`Application running on port ${port}`);
}
bootstrap();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;expressjs 에서는 Winston 모듈을 주로 사용한다고 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nestjs 에서는 내장 Logger 클래스가 존재한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;27&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jx9Ox/btrGS5ZYFbx/iRanOKKItpBLB0XCGYUPLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jx9Ox/btrGS5ZYFbx/iRanOKKItpBLB0XCGYUPLk/img.png&quot; data-alt=&quot;간단하게 서버 돌아가는 포트 번호 알려주는 로그 찍기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jx9Ox/btrGS5ZYFbx/iRanOKKItpBLB0XCGYUPLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJx9Ox%2FbtrGS5ZYFbx%2FiRanOKKItpBLB0XCGYUPLk%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;600&quot; height=&quot;16&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;27&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;간단하게 서버 돌아가는 포트 번호 알려주는 로그 찍기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Log&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Warning&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Error&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Debug&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Verbose&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 다섯 가지 레벨로 사용 가능하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로덕션 단계에서는 Log 와 Error 만 사용 가능&lt;/p&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; ./src/boards/boards.controller.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657342305348&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export class BoardsController {
    private logger = new Logger('BoardsController');
    
    				...
                    
    @Get()
    getAllBoard(@GetUser() user: User): Promise&amp;lt;Board[]&amp;gt; {
        this.logger.verbose(`User ${user.username} trying to get all boards`);
        return this.boardsService.getAllBoards(user);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1313&quot; data-origin-height=&quot;36&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eBOmjl/btrGTwQyK47/9lrOVvR4MBxjAsJNqyG1DK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eBOmjl/btrGTwQyK47/9lrOVvR4MBxjAsJNqyG1DK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eBOmjl/btrGTwQyK47/9lrOVvR4MBxjAsJNqyG1DK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeBOmjl%2FbtrGTwQyK47%2F9lrOVvR4MBxjAsJNqyG1DK%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;600&quot; height=&quot;16&quot; data-origin-width=&quot;1313&quot; data-origin-height=&quot;36&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;Logger 생성자 인자로 string 전달해주면 그게 로그 찍을 때 정보로 나온다&lt;/p&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; ./src/boards/boards.controller.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657342450323&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Post()
@UsePipes(ValidationPipe)
createBoard(
    @Body() createBoardDto: CreateBoardDto,
    @GetUser() user: User,
): Promise&amp;lt;Board&amp;gt; {
    this.logger.verbose(
        `User ${
            user.username
        } creating a new board. Payload: ${JSON.stringify(createBoardDto)}`,
    );
    return this.boardsService.createBoard(createBoardDto, user);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1423&quot; data-origin-height=&quot;53&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E1hAm/btrGQEoYHAk/dkDnkeGEprWtW5pCFrgJpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E1hAm/btrGQEoYHAk/dkDnkeGEprWtW5pCFrgJpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E1hAm/btrGQEoYHAk/dkDnkeGEprWtW5pCFrgJpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE1hAm%2FbtrGQEoYHAk%2FdkDnkeGEprWtW5pCFrgJpk%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;600&quot; height=&quot;22&quot; data-origin-width=&quot;1423&quot; data-origin-height=&quot;53&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;h4 data-ke-size=&quot;size20&quot;&gt;✅ 설정 (Configuration)&lt;/h4&gt;
&lt;pre id=&quot;code_1657342929203&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i config --save&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;config 모듈을 추가한다&lt;/p&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; ./config/default.yml&lt;/p&gt;
&lt;pre id=&quot;code_1657342947087&quot; class=&quot;ruby&quot; data-ke-language=&quot;ruby&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server:
  port: 3000

db:
  type: 'postgres'
  port: 5432
  database: 'board-app'
  
jwt:
  expiresIn: 3600&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; ./config/development.yml&lt;/p&gt;
&lt;pre id=&quot;code_1657343018945&quot; class=&quot;ruby&quot; data-ke-language=&quot;ruby&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;db:
  host: 'localhost'
  username: 'postgres'
  password: 'postgres'
  synchronize: true

jwt:
  secret: 'MySecretKey1234'&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; ./config/development.yml&lt;/p&gt;
&lt;pre id=&quot;code_1657343027126&quot; class=&quot;ruby&quot; data-ke-language=&quot;ruby&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;db:
  synchronize: false&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;예제에서는 야믈 파일로 생성해준다 json 형식도 가능하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;expressjs 할 때는 dotenv 를 썼었던 거 같은데 yml 파일 쓰는 건 첨이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; ./src/main.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657343179300&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as config from 'config';

async function bootstrap() {
    const logger = new Logger();

    const app = await NestFactory.create(AppModule);

    const serverConfig = config.get('server');
    const port = serverConfig.port;

    await app.listen(port);
    logger.log(`Application running on port ${port}`);
}
bootstrap();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;config를 import 해서 사용 가능하다&lt;/p&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; ./src/configs/typeorm.config.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657343812492&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import * as config from 'config';

const dbConfig = config.get('db');

export const typeORMConfig: TypeOrmModuleOptions = {
    type: dbConfig.type,
    host: process.env.RDS_HOSTNAME || dbConfig.host,
    port: process.env.RDS_PORT || dbConfig.port,
    username: process.env.RDS_USERNAME || dbConfig.username,
    password: process.env.RDS_PASSWORD || dbConfig.password,
    database: process.env.RDS_DB_NAME || dbConfig.database,
    entities: [__dirname + '/../**/*.entity.{js,ts}'],
    synchronize: dbConfig.synchronize,
};&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;AWS 같은 클라우드에선 환경 변수 설정이 가능하기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;process.env 에서 가져오거나 yml 파일에서 가져오도록 || 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일주일 정도 들어서 완강했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 nestjs 활용한 프로젝트 곧 시작하니까 나머지도 잘 공부해야겠다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  백엔드/  Nest.js</category>
      <category>Configuration</category>
      <category>env</category>
      <category>log</category>
      <category>Logger</category>
      <category>nestjs</category>
      <category>nodejs</category>
      <category>네스트</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/20</guid>
      <comments>https://gengminy.tistory.com/20#entry20comment</comments>
      <pubDate>Sat, 9 Jul 2022 14:19:46 +0900</pubDate>
    </item>
    <item>
      <title>[NestJs] 따라하면서 배우는 NestJs - 8 (권한 부여, 유저와 게시글 관계 부여)</title>
      <link>https://gengminy.tistory.com/19</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MpBi8/btrGRjdwUZ6/3QXp8ZWrbobhlsWQgLapcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MpBi8/btrGRjdwUZ6/3QXp8ZWrbobhlsWQgLapcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MpBi8/btrGRjdwUZ6/3QXp8ZWrbobhlsWQgLapcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMpBi8%2FbtrGRjdwUZ6%2F3QXp8ZWrbobhlsWQgLapcK%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;300&quot; height=&quot;299&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ Boards 모듈에서 AuthGuard 사용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; ./src/boards/boards.module.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657339834588&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from 'src/auth/auth.module';
import { BoardRepository } from './board.repository';
import { BoardsController } from './boards.controller';
import { BoardsService } from './boards.service';

@Module({
    imports: [
        ConfigModule.forRoot({ isGlobal: true }),
        TypeOrmModule.forFeature([BoardRepository]),
        AuthModule,
    ],
    controllers: [BoardsController],
    providers: [BoardsService],
})
export class BoardsModule {}&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; ./src/boards/boards.controller.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657339863018&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller('boards')
@UseGuards(AuthGuard())
export class BoardsController {
			...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러 레벨에 UseGuards 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;937&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpCZeR/btrGRIRG9lU/i0oFYzKwTuWeIH45PHaTvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpCZeR/btrGRIRG9lU/i0oFYzKwTuWeIH45PHaTvk/img.png&quot; data-alt=&quot;JWT 없이 GET 요청을 보내면 권한 없음 오류 발생&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpCZeR/btrGRIRG9lU/i0oFYzKwTuWeIH45PHaTvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpCZeR%2FbtrGRIRG9lU%2Fi0oFYzKwTuWeIH45PHaTvk%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;1281&quot; height=&quot;937&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;937&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JWT 없이 GET 요청을 보내면 권한 없음 오류 발생&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;1195&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ppf09/btrGPqStcSn/oLgSJKTl6XL6SvSnXrIK9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ppf09/btrGPqStcSn/oLgSJKTl6XL6SvSnXrIK9k/img.png&quot; data-alt=&quot;Signin 으로 얻은 JWT을 request 에 실어보내면 권한을 정상적으로 얻는다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ppf09/btrGPqStcSn/oLgSJKTl6XL6SvSnXrIK9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPpf09%2FbtrGPqStcSn%2FoLgSJKTl6XL6SvSnXrIK9k%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;1278&quot; height=&quot;1195&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;1195&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Signin 으로 얻은 JWT을 request 에 실어보내면 권한을 정상적으로 얻는다&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 처리도 Guard 관련 코드 몇개만 뚝딱 했는데 되니까 상당히 편하다&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;✅ User와 Board 사이의 관계 정의&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; ./src/auth/user.entity.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657340600465&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity()
@Unique(['username'])
export class User extends BaseEntity {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    username: string;

    @Column()
    password: string;

    @OneToMany((type) =&amp;gt; Board, (board) =&amp;gt; board.user, { eager: true })
    boards: Board[];

    async validatePassword(password: string): Promise&amp;lt;boolean&amp;gt; {
        const isValid = await bcrypt.compare(password, this.password);
        return isValid;
    }
}&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; ./src/boards/board.entity.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657340621026&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity()
export class Board extends BaseEntity {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    title: string;

    @Column()
    description: string;

    @Column()
    status: BoardStatus;

    @ManyToOne((type) =&amp;gt; User, (user) =&amp;gt; user.boards, { eager: false })
    user: User;
}&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;여기서는 User 가 Board 에 대해서 OneToMany 의 관계이고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로는 ManyToOne 의 관계이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;eager 옵션을 한쪽은 true, 한쪽은 false로 주었는데&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;h4 data-ke-size=&quot;size20&quot;&gt;✅ 게시글 생성할 때 유저 정보 전달&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; ./src/boards/boards.controller.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657340871640&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Post()
@UsePipes(ValidationPipe)
createBoard(
    @Body() createBoardDto: CreateBoardDto,
    @GetUser() user: User,
): Promise&amp;lt;Board&amp;gt; {
    return this.boardsService.createBoard(createBoardDto, user);
}&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; ./src/boards/boards.service.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657340880301&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;createBoard(createBoardDto: CreateBoardDto, user: User): Promise&amp;lt;Board&amp;gt; {
    return this.boardRepository.createBoard(createBoardDto, user);
}&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; ./src/boards/board.repository.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657340890309&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EntityRepository(Board)
export class BoardRepository extends Repository&amp;lt;Board&amp;gt; {
    async createBoard(
        createBoardDto: CreateBoardDto,
        user: User,
    ): Promise&amp;lt;Board&amp;gt; {
        const { title, description } = createBoardDto;

        const board = this.create({
            title,
            description,
            status: BoardStatus.PUBLIC,
            user,
        });

        await this.save(board);
        return board;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인자로 User 전달을 추가해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@GetUser 은 저번 시간에 만들었던 커스텀 데코레이터&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;req.user 를 즉시 가져오게 하는 데코레이터다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;board 엔티티에 user 정보를 정의해놓았기 때문에 바로 인자 전달해서 생성하면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1271&quot; data-origin-height=&quot;1122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccSoHf/btrGSxbk1Od/RtOqVaU82z9yXJSm6exnbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccSoHf/btrGSxbk1Od/RtOqVaU82z9yXJSm6exnbK/img.png&quot; data-alt=&quot;이제 게시글 생성하면 유저 정보가 같이 전달된다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccSoHf/btrGSxbk1Od/RtOqVaU82z9yXJSm6exnbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccSoHf%2FbtrGSxbk1Od%2FRtOqVaU82z9yXJSm6exnbK%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;1271&quot; height=&quot;1122&quot; data-origin-width=&quot;1271&quot; data-origin-height=&quot;1122&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이제 게시글 생성하면 유저 정보가 같이 전달된다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 특정 유저의 게시글만 가져오기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; ./src/boards/boards.controller.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657341372285&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Get()
getAllBoard(@GetUser() user: User): Promise&amp;lt;Board[]&amp;gt; {
    return this.boardsService.getAllBoards(user);
}&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; ./src/boards/boards.service.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657341416105&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async getAllBoards(user: User): Promise&amp;lt;Board[]&amp;gt; {
    const query = this.boardRepository.createQueryBuilder('board');

    query.where('board.userId = :userId', { userId: user.id });

    const boards = await query.getMany();
    return boards;
}&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;이번 예제에서는 repository API 대신에 queryBuilder를 사용했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 SQL 쿼리를 조작할 수 있도록 TypeORM 에서 제공해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getMany 는 해당하는 결과를 전부 가져오는 메소드다&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;✅ 자신이 생성한 게시글 지우기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; ./src/boards/boards.controller.ts&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1657341740840&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Delete('/:id')
deleteBoard(
    @Param('id', ParseIntPipe) id: number,
    @GetUser() user: User,
): Promise&amp;lt;void&amp;gt; {
    return this.boardsService.deleteBoard(id, user);
}&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; ./src/boards/boards.service.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657341749942&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async deleteBoard(id: number, user: User): Promise&amp;lt;void&amp;gt; {
    const result = await this.boardRepository.delete({ id, user });

    if (result.affected === 0) {
        throw new NotFoundException(`Can't find Board with id ${id}`);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인자로 user 정보까지 전달해주면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한이 있는 user만 그 게시글을 지울 수 있게 된다&lt;/p&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;</description>
      <category>  백엔드/  Nest.js</category>
      <category>Auth</category>
      <category>JWT</category>
      <category>nest</category>
      <category>nestjs</category>
      <category>nodejs</category>
      <category>Passport</category>
      <category>권한</category>
      <category>네스트</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/19</guid>
      <comments>https://gengminy.tistory.com/19#entry19comment</comments>
      <pubDate>Sat, 9 Jul 2022 13:43:45 +0900</pubDate>
    </item>
    <item>
      <title>[NestJs] 따라하면서 배우는 NestJs - 7 (JWT, passport 이용한 인증 구현)</title>
      <link>https://gengminy.tistory.com/18</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xjndf/btrGLANZ0kx/UCu5oGcaYAm6gHXnJ70kT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xjndf/btrGLANZ0kx/UCu5oGcaYAm6gHXnJ70kT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xjndf/btrGLANZ0kx/UCu5oGcaYAm6gHXnJ70kT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxjndf%2FbtrGLANZ0kx%2FUCu5oGcaYAm6gHXnJ70kT0%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;300&quot; height=&quot;299&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ JWT, passport 모듈 추가&lt;/h4&gt;
&lt;pre id=&quot;code_1657256613502&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i @nestjs/jwt @nestjs/passport passport passport-jwt --save&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔ @nestjs/jwt - nest에서 jwt를 사용하기 위한 모듈&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔ @nestjs/passport - nest에서 passport를 사용하기 위한 모듈&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔ passport - passport 모듈&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔ passport-jwt - passport와 jwt를 연동하기 위한 모듈&lt;/p&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; ./src/auth/auth.module.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657256796205&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UserRepository } from './user.repository';

@Module({
    imports: [
        PassportModule.register({ defaultStrategy: 'jwt' }),
        JwtModule.register({
            secret: 'MySecretKey1234',
            signOptions: {
                expiresIn: 3600,
            },
        }),
        TypeOrmModule.forFeature([UserRepository]),
    ],
    controllers: [AuthController],
    providers: [AuthService],
})
export class AuthModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;passport는 PassportModule.register로 등록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔ defaultStrategy - 전략 지정, 여기서는 jwt를 이용한 로그인이니까 jwt로 지정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jwt는 JwtModule.register 로 등록하면 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔ secret - secret key를 지정한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔ signOptions.expiresIn - 토큰 만료 기간을 지정, 3600은 한 시간&lt;/p&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; ./src/auth/auth.service.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657257150520&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Injectable, UnauthorizedException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { AuthCredentialDto } from './dto/auth-credential.dto';
import { UserRepository } from './user.repository';
import * as bcrypt from 'bcryptjs';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
    constructor(
        @InjectRepository(UserRepository)
        private userRepository: UserRepository,
        private jwtService: JwtService,
    ) {}

    async signUp(authCredentialDto: AuthCredentialDto): Promise&amp;lt;void&amp;gt; {
        return this.userRepository.createUser(authCredentialDto);
    }

    async signIn(
        authCredentialDto: AuthCredentialDto,
    ): Promise&amp;lt;{ accessToken: string }&amp;gt; {
        const { username, password } = authCredentialDto;
        const user = await this.userRepository.findOne({ username });

        if (user &amp;amp;&amp;amp; (await bcrypt.compare(password, user.password))) {
            //유저 토큰 생성 (Secret + Payload 필요)
            const payload = { username };
            const accessToken = await this.jwtService.sign(payload);

            return { accessToken };
        } else {
            throw new UnauthorizedException('Login Failed');
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JwtService에 대한 의존성 주입 이후&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리턴 값으로 accessToken을 객체로 감싸서 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 payload 에 간단한 정보를 담아서 서명할 수 있다&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;501&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nOrlX/btrGObT9gdL/pW9ZpRKa40kVhWzeKKKL6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nOrlX/btrGObT9gdL/pW9ZpRKa40kVhWzeKKKL6k/img.png&quot; data-alt=&quot;post 요청 보내면 엑세스 토큰이 날라온다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nOrlX/btrGObT9gdL/pW9ZpRKa40kVhWzeKKKL6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnOrlX%2FbtrGObT9gdL%2FpW9ZpRKa40kVhWzeKKKL6k%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;676&quot; height=&quot;501&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;501&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;post 요청 보내면 엑세스 토큰이 날라온다&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 토큰 인증 기능 구현&lt;/h4&gt;
&lt;pre id=&quot;code_1657257799916&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i @types/passport-jwt --save&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트에서 passport, jwt 를 위한 타입을 사용하기 위한 모듈&lt;/p&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; ./src/auth/jwt.strategy.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657258471214&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { User } from './user.entity';
import { UserRepository } from './user.repository';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
    constructor(
        @InjectRepository(UserRepository)
        private userRepository: UserRepository,
    ) {
        super({
            secretOrKey: 'MySecretKey1234',
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
        });
    }

    async validate(payload) {
        const { username } = payload;
        const user: User = await this.userRepository.findOne({ username });

        if (!user) {
            throw new UnauthorizedException();
        }

        return user;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT Strategy 는&amp;nbsp;PassportStrategy 를 상속받아서 구현한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자에서는 DB와의 연동을 위해 userRepository 주입&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 부모인 PassportStrategy 의 생성자를 통해 비밀 키와 토큰 타입을 지정해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 생성할때 사용한 비밀키와 똑같이 넣고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 BearerToken 을 사용하기 떄문에 그렇게 주입&lt;/p&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; ./src/auth/auth.module.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657258582015&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { UserRepository } from './user.repository';

@Module({
    imports: [
        PassportModule.register({ defaultStrategy: 'jwt' }),
        JwtModule.register({
            secret: 'MySecretKey1234',
            signOptions: {
                expiresIn: 3600,
            },
        }),
        TypeOrmModule.forFeature([UserRepository]),
    ],
    controllers: [AuthController],
    providers: [AuthService, JwtStrategy],
    exports: [JwtStrategy, PassportModule],
})
export class AuthModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;auth module 외부에서도 사용할 수 있게 하기 위해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JwtStrategy 와 PassportModule 을 export 해줌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;provider 에 JwtStrategy 도 등록해준다&lt;/p&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; ./src/auth/auth.controller.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657258631574&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Post('/test')
@UseGuards(AuthGuard())
test(@Req() req) {
    console.log('req', req);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@UseGuards 를 통해 인증 처리를 해줄 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pipe 와 마찬가지 처럼 Guard 도 미들웨어로서 동작한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증과 관련된 것이기 때문에 AuthGuard() 를 주입해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Guard를 주입하지 않으면 인증 처리가 제대로 동작하지 않음&lt;/p&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;✅ req.user 바로 가져오기 - 커스텀 데코레이터 생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; ./src/auth/get-user.decorator.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657259152140&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { User } from './user.entity';

export const GetUser = createParamDecorator((data, ctx: ExecutionContext): User =&amp;gt; {
        const req = ctx.switchToHttp().getRequest();
        return req.user;
    },
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;createParamDecorator를 통해 파라미터 레벨의 데코레이터를 만들 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인자로 함수를 넣는데 ctx 부분을 ExecutionContext 로 지정해주면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request를 받아서 반환하면 req.user를 즉시 반환 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠ 단, request 에 user가 있다는 전제 하에만 사용해야 한다&lt;/p&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; ./src/auth/auth.controller.ts&lt;/p&gt;
&lt;pre id=&quot;code_1657259276821&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Post('/test')
@UseGuards(AuthGuard())
test(@GetUser() user: User) {
    console.log('user', user);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실히 req.user 를 가져오는 것 보다 깔끔해지긴 한다&lt;/p&gt;</description>
      <category>  백엔드/  Nest.js</category>
      <category>JWT</category>
      <category>nest</category>
      <category>nestjs</category>
      <category>nodejs</category>
      <category>Passport</category>
      <category>네스트</category>
      <category>로그인</category>
      <category>인증</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/18</guid>
      <comments>https://gengminy.tistory.com/18#entry18comment</comments>
      <pubDate>Fri, 8 Jul 2022 14:48:41 +0900</pubDate>
    </item>
    <item>
      <title>[NestJs] 따라하면서 배우는 NestJs - 6 (auth 모듈 구현)</title>
      <link>https://gengminy.tistory.com/17</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u35Vg/btrGEK4o8ka/feBzpHKS7JXMRDGf13FWk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u35Vg/btrGEK4o8ka/feBzpHKS7JXMRDGf13FWk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u35Vg/btrGEK4o8ka/feBzpHKS7JXMRDGf13FWk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu35Vg%2FbtrGEK4o8ka%2FfeBzpHKS7JXMRDGf13FWk0%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;300&quot; height=&quot;299&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;299&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;h4 data-ke-size=&quot;size20&quot;&gt;✅ 인증을 위한 auth 모듈 생성&lt;/h4&gt;
&lt;pre id=&quot;code_1657166337361&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;nest g module auth
nest g controller auth --no-spec
nest g service auth --no-spec&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;✅ User Entity &amp;amp; Repository 구현 및 등록&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/auth/user.entity.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657166367805&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User extends BaseEntity {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    username: string;

    @Column()
    password: string;
}&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;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/auth/user.repository.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657166580922&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { EntityRepository, Repository } from 'typeorm';
import { User } from './user.entity';

@EntityRepository(User)
export class UserRepository extends Repository&amp;lt;User&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;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/auth/auth.module.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657166595863&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UserRepository } from './user.repository';

@Module({
    imports: [TypeOrmModule.forFeature([UserRepository])],
    controllers: [AuthController],
    providers: [AuthService],
})
export class AuthModule {}&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;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/auth/auth.service.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657166626101&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserRepository } from './user.repository';

@Injectable()
export class AuthService {
    constructor(
        @InjectRepository(UserRepository)
        private userRepository: UserRepository,
    ) {}
}&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;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/auth/auth.controller.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657166714768&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Controller } from '@nestjs/common';
import { AuthService } from './auth.service';

@Controller('auth')
export class AuthController {
    constructor(private authService: AuthService) {}
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 회원가입 기능 추가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/auth/dto/auth-credential.dto.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657167110749&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export class AuthCredentialDto {
    username: string;
    password: string;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User 정보를 넘기기 위한 DTO(Data Transfer Object)도 추가해준다&lt;/p&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;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/auth/user.repository.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657167050446&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async createUser(authCredentialDto: AuthCredentialDto) {
    const { username, password } = authCredentialDto;
    const user = this.create({ username, password });

    await this.save(user);
}&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;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/auth/auth.service.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657167057500&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async signUp(authCredentialDto: AuthCredentialDto): Promise&amp;lt;void&amp;gt; {
    return this.userRepository.createUser(authCredentialDto);
}&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;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/auth/auth.controller.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657167065099&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Post('/signup')
signUp(@Body() authCredentialDto: AuthCredentialDto): Promise&amp;lt;void&amp;gt; {
    return this.authService.signUp(authCredentialDto);
}&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 유저 유효성 체크&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/auth/dto/auth-credential.dto.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657167338937&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { IsString, Matches, MaxLength, MinLength } from 'class-validator';

export class AuthCredentialDto {
    @IsString()
    @MinLength(4)
    @MaxLength(20)
    username: string;

    @IsString()
    @MinLength(4)
    @MaxLength(20)
    //영어랑 숫자만 가능하도록 하는 정규식
    @Matches(/^[a-zA-Z0-9]*$/, {
        message: 'password only accepts English and number',
    })
    password: string;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;class-validator 에서 데코레이터 끌어와서 사용한다&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;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/auth/auth.controller.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657167426595&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Post('/signup')
signUp(
    @Body(ValidationPipe) authCredentialDto: AuthCredentialDto,
): Promise&amp;lt;void&amp;gt; {
    return this.authService.signUp(authCredentialDto);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;authController에서 @Body 부분에 ValidationPipe 집어넣어주면 동작한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 유저에 유니크한 이름 부여하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/auth/user.entity.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657167610110&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity()
@Unique(['username'])
export class User extends BaseEntity {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    username: string;

    @Column()
    password: string;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Unique 데코레이터로 특정 값이 DB에서 처리될 때 unique 한지 검사할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 username 에 유니크한 값을 주었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 이렇게만 처리하면 오류 발생 시 500 에러(Internal Server Error)를 내보내는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어디선가 듣기로 500 에러는 절대 나와서는 안되는 것이라고 했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 try catch로 따로 잡아서 처리를 해주어야 한다&lt;/p&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;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/auth/auth.controller.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657167807555&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async createUser(authCredentialDto: AuthCredentialDto) {
    const { username, password } = authCredentialDto;
    const user = this.create({ username, password });

    try {
        await this.save(user);
    } catch (error) {
        if (error.code === '23505') {
            throw new ConflictException('Existring username');
        } else {
            throw new InternalServerErrorException();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;23505(500)에 해당하는 에러를 잡아서 따로 처리&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;✅ 비밀번호 암호와 (bcryptjs)&lt;/h4&gt;
&lt;pre id=&quot;code_1657167958518&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i bcryptjs --save
import * as bcrypt from 'bcryptjs';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bcrypt 모듈 추가&lt;/p&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;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/auth/auth.controller.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657168315390&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async createUser(authCredentialDto: AuthCredentialDto) {
    const { username, password } = authCredentialDto;

    const salt = await bcrypt.genSalt();
    const hashedPassword = await bcrypt.hash(password, salt);

    const user = this.create({ username, password: hashedPassword });

    try {
        await this.save(user);
    } catch (error) {
        if (error.code === '23505') {
            throw new ConflictException('Existring username');
        } else {
            throw new InternalServerErrorException();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호를 sha256 hash 를 사용하여 변환해 저장하는 것은 동일하지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;salt 를 덧붙이게 되면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 다른 유저가 동일한 비밀번호를 사용할 때 암호가 뚫리는 문제를 방지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;salt 라는 유니크한 값을 추가하여 각기 다르게 해싱된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ 로그인 기능 구현&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/auth/auth.service.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657168538376&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async signIn(authCredentialDto: AuthCredentialDto): Promise&amp;lt;string&amp;gt; {
    const { username, password } = authCredentialDto;
    const user = await this.userRepository.findOne({ username });

    if (user &amp;amp;&amp;amp; (await bcrypt.compare(password, user.password))) {
        return 'Login Success';
    } else {
        throw new UnauthorizedException('Login Failed');
    }
}&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;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/auth/auth.controller.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657168621629&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Post('/signin')
signIn(@Body(ValidationPipe) authCredentialDto: AuthCredentialDto) {
    return this.authService.signIn(authCredentialDto);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bcrypt.compare(pw1, pw2)로 두 비밀번호를 비교함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하다&lt;/p&gt;</description>
      <category>  백엔드/  Nest.js</category>
      <category>nest</category>
      <category>nestjs</category>
      <category>nodejs</category>
      <category>네스트</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/17</guid>
      <comments>https://gengminy.tistory.com/17#entry17comment</comments>
      <pubDate>Thu, 7 Jul 2022 13:38:23 +0900</pubDate>
    </item>
    <item>
      <title>[NestJs] 따라하면서 배우는 NestJs - 5 (레포지토리 구현 및 DB 이용 CRUD)</title>
      <link>https://gengminy.tistory.com/16</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cA6fAG/btrGHluVtbJ/l8INpEIK6KPCaHxFrLmwvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cA6fAG/btrGHluVtbJ/l8INpEIK6KPCaHxFrLmwvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cA6fAG/btrGHluVtbJ/l8INpEIK6KPCaHxFrLmwvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcA6fAG%2FbtrGHluVtbJ%2Fl8INpEIK6KPCaHxFrLmwvK%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;300&quot; height=&quot;299&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/boards/boards.service.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657091665605&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { BoardRepository } from './board.repository';

@Injectable()
export class BoardsService {
    constructor(
        @InjectRepository(BoardRepository)
        private boardRepository: BoardRepository,
    ) {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러에 서비스를 주입한 것 처럼&lt;span&gt;&amp;nbsp;&lt;/span&gt;서비스에 레포지토리를 주입해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 역시 생성자 주입 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데코레이터 @InjectRepository 를 사용하면서&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;size18&quot;&gt;✅ DB에서 하나의 id로 검색&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/boards/boards.service.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657092072206&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Board } from './board.entity';
import { BoardRepository } from './board.repository';

@Injectable()
export class BoardsService {
    constructor(
        @InjectRepository(BoardRepository)
        private boardRepository: BoardRepository,
    ) {}

    async getBoardById(id: number): Promise&amp;lt;Board&amp;gt; {
        const found = await this.boardRepository.findOne(id);

        if (!found) {
            throw new NotFoundException(`Can't find Board with id ${id}`);
        }

        return found;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서 값을 불러오는 것을 기다려야 하기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async await 사용해서 반드시 비동기 작업으로 처리해주어야함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 처리이기 때문에 리턴값은 Promise&amp;lt;T&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;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/boards/boards.controller.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657092160736&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Controller, Get, Param } from '@nestjs/common';
import { Board } from './board.entity';
import { BoardsService } from './boards.service';

@Controller('boards')
export class BoardsController {
    constructor(private boardsService: BoardsService) {}

    @Get('/:id')
    getBoardById(@Param('id') id: number): Promise&amp;lt;Board&amp;gt; {
        return this.boardsService.getBoardById(id);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;✅ 새 board 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/boards/boards.service.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657092337335&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async createBoard(createBoardDto: CreateBoardDto): Promise&amp;lt;Board&amp;gt; {
    const { title, description } = createBoardDto;

    const board = this.boardRepository.create({
        title: title,
        description: description,
        status: BoardStatus.PUBLIC,
    });

    await this.boardRepository.save(board);
    return board;
}&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;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/boards/boards.controller.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657092427654&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Post()
@UsePipes(ValidationPipe)
createBoard(@Body() createBoardDto: CreateBoardDto): Promise&amp;lt;Board&amp;gt; {
    return this.boardsService.createBoard(createBoardDto);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;boardRepository.create 로 dto 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 boardRepository.save로 DB에 값을 전달해서 저장해주면 된다&lt;/p&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;❌ RepositoryNotFoundError&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 역시나 한가지 문제가 더 생겼는데.... 바로&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;795&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xhamP/btrGEx4G7hd/kOhCZJCzr1EyAAEJYmyqa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xhamP/btrGEx4G7hd/kOhCZJCzr1EyAAEJYmyqa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xhamP/btrGEx4G7hd/kOhCZJCzr1EyAAEJYmyqa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxhamP%2FbtrGEx4G7hd%2FkOhCZJCzr1EyAAEJYmyqa0%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;1294&quot; height=&quot;795&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;795&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;현재 최신 버전은 TypeORM 은 3.0, @nestjs/typeorm 은 8.4.5 이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 레포지토리를 사용하기 위해 TypeORM 버전은 2.0으로 낮췄으나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 nestjs/typeorm 의 버전은 최신 버전과 연동되었기 때문에 발생하는 버그인 것 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@nestjs/typeorm 도 강제로 버전을 8.0.1로 낮춰주니까 잘 동작했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2시간 동안 삽질의 결과 하&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1657098147982&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i @nestjs/typeorm@8.0.1 --save&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-origin-width=&quot;1287&quot; data-origin-height=&quot;405&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KJ2Ns/btrGFJDi1mO/NWTrcWkpYurXh1eN3SYTn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KJ2Ns/btrGFJDi1mO/NWTrcWkpYurXh1eN3SYTn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KJ2Ns/btrGFJDi1mO/NWTrcWkpYurXh1eN3SYTn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKJ2Ns%2FbtrGFJDi1mO%2FNWTrcWkpYurXh1eN3SYTn1%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;1287&quot; height=&quot;405&quot; data-origin-width=&quot;1287&quot; data-origin-height=&quot;405&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;634&quot; data-origin-height=&quot;546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6DX7W/btrGEGz5JbF/jE2p3QIgbtYoHjra23NSp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6DX7W/btrGEGz5JbF/jE2p3QIgbtYoHjra23NSp0/img.png&quot; data-alt=&quot;postman 으로 post 보내기도 잘 됨&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6DX7W/btrGEGz5JbF/jE2p3QIgbtYoHjra23NSp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6DX7W%2FbtrGEGz5JbF%2FjE2p3QIgbtYoHjra23NSp0%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;634&quot; height=&quot;546&quot; data-origin-width=&quot;634&quot; data-origin-height=&quot;546&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;postman 으로 post 보내기도 잘 됨&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;1370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dCYzPA/btrGF654LTM/csnEa9ToOM87o00phl4IPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dCYzPA/btrGF654LTM/csnEa9ToOM87o00phl4IPK/img.png&quot; data-alt=&quot;pgAdmin에서 Schemas -&amp;amp;gt; Table 들어가서 빨간 아이콘 누르면 확인 가능&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dCYzPA/btrGF654LTM/csnEa9ToOM87o00phl4IPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdCYzPA%2FbtrGF654LTM%2FcsnEa9ToOM87o00phl4IPK%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;1540&quot; height=&quot;1370&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;1370&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;pgAdmin에서 Schemas -&amp;gt; Table 들어가서 빨간 아이콘 누르면 확인 가능&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;✅ DB 관련 로직 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;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/boards/board.repository.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657120215519&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { EntityRepository, Repository } from 'typeorm';
import { BoardStatus } from './board-status.enum';
import { Board } from './board.entity';
import { CreateBoardDto } from './dto/create-board.dto';

@EntityRepository(Board)
export class BoardRepository extends Repository&amp;lt;Board&amp;gt; {
    async createBoard(createBoardDto: CreateBoardDto): Promise&amp;lt;Board&amp;gt; {
        const { title, description } = createBoardDto;

        const board = this.create({
            title,
            description,
            status: BoardStatus.PUBLIC,
        });

        await this.save(board);
        return board;
    }
}&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;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/boards/boards.service.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657120229434&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;createBoard(createBoardDto: CreateBoardDto): Promise&amp;lt;Board&amp;gt; {
    return this.boardRepository.createBoard(createBoardDto);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스에 만들었던 create 로직을 repository 로 옮겨준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스에 db 관련 로직까지 포함시키면 코드가 너무 복잡해질 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;size18&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;DELETE 메서드를 사용할 때 remove와 delete 를 사용할 수 있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;remove는 해당 id 값이 무조건 존재해야 하며 (존재하지 않으면 404 error)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;delete는 해당 id 값이 있으면 지우고 없으면 아무 영향을 끼치지 않음 (오류 발생 x)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 remove는 DB에 2번 접근해야 해서 비효율적&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 1번만 접근해도 되는 delete 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/boards/boards.service.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657120711887&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async deleteBoard(id: number): Promise&amp;lt;void&amp;gt; {
    const result = await this.boardRepository.delete(id);

    if (result.affected === 0) {
        throw new NotFoundException(`Can't find Board with id ${id}`);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/boards/boards.controller.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657120729479&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Delete('/:id')
deleteBoard(@Param('id', ParseIntPipe) id: number): Promise&amp;lt;void&amp;gt; {
    return this.boardsService.deleteBoard(id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/boards/boards.service.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657120890310&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async updateBoardStatus(id: number, status: BoardStatus): Promise&amp;lt;Board&amp;gt; {
    const board = await this.getBoardById(id);

    board.status = status;
    await this.boardRepository.save(board);

    return board;
}&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;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/boards/boards.controller.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657121018398&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Patch('/:id/status')
updateBoardStatus(
    @Param('id') id: number,
    @Body('status', BoardStatusValidationPipe) status: BoardStatus,
): Promise&amp;lt;Board&amp;gt; {
    return this.boardsService.updateBoardStatus(id, status);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;613&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nFQbr/btrGEsB6N80/4YTKa3C6j5AWD8DBvTfl2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nFQbr/btrGEsB6N80/4YTKa3C6j5AWD8DBvTfl2K/img.png&quot; data-alt=&quot;request body에 status 정보를 같이 보내면 성공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nFQbr/btrGEsB6N80/4YTKa3C6j5AWD8DBvTfl2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnFQbr%2FbtrGEsB6N80%2F4YTKa3C6j5AWD8DBvTfl2K%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;613&quot; height=&quot;538&quot; data-origin-width=&quot;613&quot; data-origin-height=&quot;538&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;request body에 status 정보를 같이 보내면 성공&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;&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;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/boards/boards.service.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657121272655&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async getAllBoards(): Promise&amp;lt;Board[]&amp;gt; {
    return this.boardRepository.find();
}&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;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/boards/boards.controller.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657121261707&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Get()
getAllBoard(): Promise&amp;lt;Board[]&amp;gt; {
    return this.boardsService.getAllBoards();
}&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;레포지토리의 find 를 사용하면 모든 데이터를 가져옴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 구현&lt;/p&gt;</description>
      <category>  백엔드/  Nest.js</category>
      <category>nest</category>
      <category>nestCRUD</category>
      <category>nestjs</category>
      <category>Node</category>
      <category>nodejs</category>
      <category>네스트</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/16</guid>
      <comments>https://gengminy.tistory.com/16#entry16comment</comments>
      <pubDate>Thu, 7 Jul 2022 00:29:00 +0900</pubDate>
    </item>
    <item>
      <title>[NestJs] TypeORM 사용 시 RepositoryNotFoundError 해결하기</title>
      <link>https://gengminy.tistory.com/15</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sHnvD/btrGB3CFI9X/kPaP2AtAYXfRE7CypNkRGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sHnvD/btrGB3CFI9X/kPaP2AtAYXfRE7CypNkRGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sHnvD/btrGB3CFI9X/kPaP2AtAYXfRE7CypNkRGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsHnvD%2FbtrGB3CFI9X%2FkPaP2AtAYXfRE7CypNkRGK%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;300&quot; height=&quot;299&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;299&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;h4 data-ke-size=&quot;size20&quot;&gt;❌ 난관 봉착&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nestjs의 TypeORM 은 3.0부터 EntityRepository 가 deprecated 되어서 사용할 수 없다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 최신의 TypeORM 에서 이 커스텀 레포지토리를 사용하려면&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;하지만 일단은 예제를 따라 공부하는 중이기 때문에 TypeORM을 강제로 2.0으로 낮췄다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1657097846391&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i --save typeorm@0.2&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;  package.json&lt;/p&gt;
&lt;pre id=&quot;code_1657097868224&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  &quot;dependencies&quot;: {
    &quot;@nestjs/common&quot;: &quot;^8.0.0&quot;,
    &quot;@nestjs/config&quot;: &quot;^2.1.0&quot;,
    &quot;@nestjs/core&quot;: &quot;^8.0.0&quot;,
    &quot;@nestjs/platform-express&quot;: &quot;^8.0.0&quot;,
    &quot;@nestjs/typeorm&quot;: &quot;^8.0.1&quot;,
    &quot;class-transformer&quot;: &quot;^0.5.1&quot;,
    &quot;class-validator&quot;: &quot;^0.13.2&quot;,
    &quot;dotenv&quot;: &quot;^16.0.1&quot;,
    &quot;pg&quot;: &quot;^8.7.3&quot;,
    &quot;reflect-metadata&quot;: &quot;^0.1.13&quot;,
    &quot;rimraf&quot;: &quot;^3.0.2&quot;,
    &quot;rxjs&quot;: &quot;^7.2.0&quot;,
    &quot;typeorm&quot;: &quot;^0.2.45&quot;,
    &quot;uuid&quot;: &quot;^8.3.2&quot;
  },&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;typeorm 버전이 0.2.45로 강제로 다운그레이드 되었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 @EntityRepository 데코레이터 사용 가능하다&lt;/p&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;바로 RepositoryNotFoundError 가 뜨는 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;795&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bePyhn/btrGF7XIjkt/4ShkiA9DD7KoQdmFMKjzX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bePyhn/btrGF7XIjkt/4ShkiA9DD7KoQdmFMKjzX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bePyhn/btrGF7XIjkt/4ShkiA9DD7KoQdmFMKjzX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbePyhn%2FbtrGF7XIjkt%2F4ShkiA9DD7KoQdmFMKjzX1%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;1294&quot; height=&quot;795&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;795&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;ERROR [ExceptionHandler] No repository for &quot;BoardRepository&quot; was&amp;nbsp;found.&amp;nbsp;Looks&amp;nbsp;like&amp;nbsp;this&amp;nbsp;entity&amp;nbsp;is&amp;nbsp;not&amp;nbsp;registered&amp;nbsp;in&amp;nbsp;current&amp;nbsp;&quot;default&quot;&amp;nbsp;connection? &lt;br /&gt;RepositoryNotFoundError:&amp;nbsp;No&amp;nbsp;repository&amp;nbsp;for&amp;nbsp;&quot;BoardRepository&quot;&amp;nbsp;was&amp;nbsp;found.&amp;nbsp;Looks&amp;nbsp;like&amp;nbsp;this&amp;nbsp;entity&amp;nbsp;is&amp;nbsp;not&amp;nbsp;registered&amp;nbsp;in&amp;nbsp;current&amp;nbsp;&quot;default&quot;&amp;nbsp;connection?&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;바로 Entity가 등록이 안되었다고 하면서 연동이 안되는데...&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 문제로 스택 오버플로우 겁나 뒤져가면서 찾아도 엔티티 등록하는 것 밖에 안알려줬다&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그러다가 인프런 검색하니까 바로 해결했다&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;바로바로 typeorm 과 같이 사용하는 @nestjs/typeorm 버전도 낮춰야 한다는 것이다...&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이걸 몰라서 2시간 넘게 삽질했다 ㅠ&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1657098039668&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i @nestjs/typeorm@8.0.1 --save&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강제로 8.0.1로 다운그레이드 시켜주었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1287&quot; data-origin-height=&quot;405&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ur0xS/btrGF7pUTmV/54d5JGc1rx09nMHxUIPGSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ur0xS/btrGF7pUTmV/54d5JGc1rx09nMHxUIPGSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ur0xS/btrGF7pUTmV/54d5JGc1rx09nMHxUIPGSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fur0xS%2FbtrGF7pUTmV%2F54d5JGc1rx09nMHxUIPGSk%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;1287&quot; height=&quot;405&quot; data-origin-width=&quot;1287&quot; data-origin-height=&quot;405&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;열어분들은 삽질하지 마시길&lt;/p&gt;</description>
      <category>  백엔드/  Nest.js</category>
      <category>Entity</category>
      <category>nest</category>
      <category>nestentity</category>
      <category>nestjs</category>
      <category>RepositoryNotFoundError</category>
      <category>typeorm</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/15</guid>
      <comments>https://gengminy.tistory.com/15#entry15comment</comments>
      <pubDate>Wed, 6 Jul 2022 18:02:11 +0900</pubDate>
    </item>
    <item>
      <title>[NestJs] 따라하면서 배우는 NestJs - 4 (Postgres, TypeORM 적용)</title>
      <link>https://gengminy.tistory.com/14</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I5lCt/btrGv5tNWdd/Vr0WR7fCoWxCQWQfY42yvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I5lCt/btrGv5tNWdd/Vr0WR7fCoWxCQWQfY42yvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I5lCt/btrGv5tNWdd/Vr0WR7fCoWxCQWQfY42yvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI5lCt%2FbtrGv5tNWdd%2FVr0WR7fCoWxCQWQfY42yvk%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;300&quot; height=&quot;299&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;299&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;size18&quot;&gt;✅ ORM(Object Relational Mapping)&lt;/p&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;객체 클래스 vs 관계형 db 테이블간 불일치를 해소&lt;/p&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;size18&quot;&gt;✅ Postgres, TypeORM 설치 및 적용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1657003722756&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i pg typeorm @nestjs/typeorm --save&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pg : postgres 모듈&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;typeorm : TypeORM 모듈&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@nestjs/typeorm : nest와 TypeORM 간의 연동&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제에서는 postgresSQL과 pgAdmin4 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  postgresSQL for MAC -&amp;nbsp;&lt;a href=&quot;https://postgresapp.com/downloads.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://postgresapp.com/downloads.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  pgAdmin4 for MAC -&amp;nbsp;&lt;a href=&quot;https://www.pgadmin.org/download/pgadmin-4-macos/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.pgadmin.org/download/pgadmin-4-macos/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;  &lt;/span&gt;docs - &lt;a href=&quot;https://docs.nestjs.com/techniques/database&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.nestjs.com/techniques/database&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;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;span&gt; ./src/configs/typeorm.config.ts&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657004246440&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { TypeOrmModuleOptions } from '@nestjs/typeorm';

export const typeORMConfig: TypeOrmModuleOptions = {
    type: 'postgres',
    host: 'localhost',
    port: 5432,
    username: 'postgres',
    password: 'postgres',
    database: 'board-app',
    entities: [__dirname + '/../**/*.entity.{js,ts}'],
    synchronize: true,
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;entites 옵션에 저렇게 적으면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 엔티티 파일들을 모두 긁어와서 테이블을 자동 생성해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;synchronize 옵션은 어플리케이션 재실행시 테이블을 drop 후 재생성 해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/app.module.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657004355640&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BoardsModule } from './boards/boards.module';
import { typeORMConfig } from './configs/typeorm.config';

@Module({
    imports: [TypeOrmModule.forRoot(typeORMConfig), BoardsModule],
})
export class AppModule {}&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;p data-ke-size=&quot;size18&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;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/boards/board.entity.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657004697260&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { BaseEntity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { BoardStatus } from './board.model';

@Entity()
export class Board extends BaseEntity {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    title: string;

    @Column()
    description: string;

    @Column()
    status: BoardStatus;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;BaseEntity&lt;/b&gt;&lt;/u&gt; 를 상속받아서 정의한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;@PrimaryGeneratedColumn()&lt;/b&gt;&lt;/u&gt; 데코레이터를 사용하면  unique id 를 자동으로 생성해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에는 &lt;u&gt;&lt;b&gt;@Column()&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;span&gt; Repository &lt;/span&gt;&lt;/span&gt;&lt;span&gt;docs&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://typeorm.delightful.studio/classes/_repository_repository_.repository.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;http://typeorm.delightful.studio/classes/_repository_repository_.repository.html&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;하다가 보니까 EntityRepository 는 TypeORM@0.3 부터 사용하지 않는다(deprecated)해서 찾아보니까&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;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;  TypeORM@0.3 CustomRepository&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;docs&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://typeorm.io/custom-repository#how-to-create-custom-repository&quot;&gt;https://typeorm.io/custom-repository#how-to-create-custom-repository&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;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;  EntityRepository 돌려줘! 데코레이터 뿌수기 + CustomRepository 커스텀&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://prod.velog.io/@pk3669/typeorm-0.3.x-EntityRepository-%EB%8F%8C%EB%A0%A4%EC%A4%98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://prod.velog.io/@pk3669/typeorm-0.3.x-EntityRepository-%EB%8F%8C%EB%A0%A4%EC%A4%98&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;일단은 예제 따라가는게 목적이니 TypeORM@0.2로 다운그레이드해서 해버림&lt;/p&gt;
&lt;pre id=&quot;code_1657006884013&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i typeorm@0.2&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;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/boards/board.repository.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657007112923&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { EntityRepository, Repository } from 'typeorm';
import { Board } from './board.entity';

@EntityRepository(Board)
export class BoardRepository extends Repository&amp;lt;Board&amp;gt; {
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeORM 다운그레이드 하니까 찍찍이 사라짐&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;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;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;./src/boards/boards.module.ts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1657007040976&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BoardRepository } from './board.repository';
import { BoardsController } from './boards.controller';
import { BoardsService } from './boards.service';

@Module({
    imports: [TypeOrmModule.forFeature([BoardRepository])],
    controllers: [BoardsController],
    providers: [BoardsService],
})
export class BoardsModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레포지토리 import 해줌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 여기까지 하면 TypeORM 적용은 끝&lt;/p&gt;</description>
      <category>  백엔드/  Nest.js</category>
      <category>entityrepository</category>
      <category>nestjs</category>
      <category>nodejs</category>
      <category>typeorm</category>
      <category>네스트</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/14</guid>
      <comments>https://gengminy.tistory.com/14#entry14comment</comments>
      <pubDate>Tue, 5 Jul 2022 16:49:57 +0900</pubDate>
    </item>
    <item>
      <title>[NestJs] 따라하면서 배우는 NestJs - 3 (pipe와 validation)</title>
      <link>https://gengminy.tistory.com/13</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oe5uw/btrGuqi7YDK/aLPMT5JwiUeP6ibKb8sEqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oe5uw/btrGuqi7YDK/aLPMT5JwiUeP6ibKb8sEqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oe5uw/btrGuqi7YDK/aLPMT5JwiUeP6ibKb8sEqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Foe5uw%2FbtrGuqi7YDK%2FaLPMT5JwiUeP6ibKb8sEqK%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;300&quot; height=&quot;299&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;299&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;size18&quot;&gt;✅ Pipe&lt;/p&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;핸들러 레벨 / 파라미터 레벨 / 글로벌 레벨의 3가지 레벨에서 사용할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;/p&gt;
&lt;pre id=&quot;code_1656923367440&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i class-validator class-transformer --save&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;  class-validator docs&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/typestack/class-validator#manual-validation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/typestack/class-validator#manual-validation&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;✅ class-validator 적용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  ./boards/dto/create-board.dto.ts&lt;/p&gt;
&lt;pre id=&quot;code_1656923548492&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { IsNotEmpty } from 'class-validator';

export class CreateBoardDto {
    @IsNotEmpty()
    title: string;

    @IsNotEmpty()
    description: string;
}&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;  ./boards/boards.controller.ts&lt;/p&gt;
&lt;pre id=&quot;code_1656923742201&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Post()
@UsePipes(ValidationPipe)
createBoard(@Body() createBoardDto: CreateBoardDto): Board {
    return this.boardsService.createBoard(createBoardDto);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핸들러 레벨 파이프는 @UsePipes 를 적고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유효성 검사와 관련된 파이프를 사용하기 때문에 인자로 ValidationPipe를 넣어준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 빌트인 파이프는 6가지로 NestJs가 기본적으로 제공한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ValidationPipe&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ParseIntPipe&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ParseBoolPipe&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ParseArrayPipe&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ParseUUIDPipe&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- DefaultValuePipe&lt;/p&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;&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;✅ READ - 없는 board를 가져오려 할 때 예외 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  ./boards/boards.service.ts&lt;/p&gt;
&lt;pre id=&quot;code_1656924250948&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;getBoardById(id: string): Board {
    const found = this.boards.find((board) =&amp;gt; board.id === id);

    if (!found) {
        throw new NotFoundException(`Can't find Board with id ${id}`);
    }

    return found;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NotFoundException 또한 Nest가 기본적으로 제공하는 인스턴스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인자로 문자열을 주면 response에 해당 message가 실려 나간다&lt;/p&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;size18&quot;&gt;✅ DELETE - 없는 board를 지우려 할 때 예외 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  ./boards/boards.service.ts&lt;/p&gt;
&lt;pre id=&quot;code_1656924400919&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;deleteBoard(id: string): void {
    const found = this.getBoardById(id);
    this.boards = this.boards.filter((board) =&amp;gt; board.id !== found.id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getBoardById에 이미 예외처리 구문을 설정했으므로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 찾아서 일치하는지 확인해주는 라인만 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 해당 id에 해당하는 board가 없다면 getBoardById 메서드에서 예외 처리 진행&lt;/p&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;size18&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;  ./boards/pipes/board-status-validation.pipe.ts&lt;/p&gt;
&lt;pre id=&quot;code_1656926126967&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {
    ArgumentMetadata,
    BadRequestException,
    PipeTransform,
} from '@nestjs/common';
import { BoardStatus } from '../board.model';

export class BoardStatusValidationPipe implements PipeTransform {
    readonly StatusOptions = [BoardStatus.PRIVATE, BoardStatus.PUBLIC];

    transform(value: any, metadata: ArgumentMetadata) {
        value = value.toUpperCase();

        if (!this.isStatusValid(value)) {
            throw new BadRequestException(
                `${value} isn't in the status options`,
            );
        }

        return value;
    }

    private isStatusValid(status: any) {
        const index = this.StatusOptions.indexOf(status);
        return index !== -1;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 파이프를 구현하기 위해서는 반드시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. PipeTransform 인터페이스를 가져와서&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. transform 메서드를 오버라이딩 해주어야 한다&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;PipeTransform 인터페이스 또한 Nest가 기본적으로 가지고 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 BoardStatus 의 두가지 상태를 가져와서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 상태가 아닌 인자가 전달이 되면 400 에러를 응답하도록 했다&lt;/p&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;</description>
      <category>  백엔드/  Nest.js</category>
      <category>nestjs</category>
      <category>nodejs</category>
      <category>네스트</category>
      <author>gengminy</author>
      <guid isPermaLink="true">https://gengminy.tistory.com/13</guid>
      <comments>https://gengminy.tistory.com/13#entry13comment</comments>
      <pubDate>Mon, 4 Jul 2022 18:17:51 +0900</pubDate>
    </item>
  </channel>
</rss>