<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>생각 저장소</title>
    <link>https://curiosity-storage.tistory.com/</link>
    <description>앞으로 넘어지기</description>
    <language>ko</language>
    <pubDate>Wed, 8 Apr 2026 12:29:38 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>RoyceWon</managingEditor>
    <image>
      <title>생각 저장소</title>
      <url>https://tistory1.daumcdn.net/tistory/5729372/attach/9a9865b9217a40c58d8b595042fc4e2a</url>
      <link>https://curiosity-storage.tistory.com</link>
    </image>
    <item>
      <title>Enum 타입 변환 전역으로 설정 하기 (Converter, ConverterFactory)</title>
      <link>https://curiosity-storage.tistory.com/18</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;요청에 대한 파라미터 혹은 데이터로 Enum 타입을 직접 사용하는 경우가 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713143189876&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record SearchLibraryBookCond(
        int page,
        int size,
        BookCategory bookCategory
) {
}

//---

@GetMapping(&quot;/books&quot;)
public ApiResponse&amp;lt;SearchLibraryBookResult&amp;gt; searchLibraryBooks(
        final SearchLibraryBookCond searchCond
) {
    final SearchLibraryBookResult result = libraryService.getLibraryBook(searchCond);
    return ApiResponse.from(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;위 핸들러는 아래와 같은 http 요청을 처리할 수 있는데요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;GET &quot;/library/books?page=1&amp;amp;size=30&amp;amp;bookCategory=FICTION&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;queryParameter로 전달되는 Enum타입의 값은 일반적으로 대문자로 전달되어야 정상 처리 됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Jackson라이브러리를 기본으로 사용중이기 때문에, 해당 직렬화 정책을 따라가게 됨&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 진행중인 프로젝트에서는 url로 오는 value에 대해선 대소문자 구분을 하지 않기로 정의하였는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;GET &quot;/&lt;/span&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;library/books?page=1&amp;amp;size=30&amp;amp;bookCategory=FICTION&quot;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;GET &quot;/&lt;/span&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;library/books?page=1&amp;amp;size=30&amp;amp;bookCategory=fiction&quot;&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;h2 data-ke-size=&quot;size26&quot;&gt;String으로 받아 Enum 요청 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단하게 생각한 방식은, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;String&lt;/span&gt; 값을 직접 받아 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Enum&lt;/span&gt; 타입을 생성하는 것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sample&lt;/p&gt;
&lt;pre id=&quot;code_1713146691813&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public record SearchLibraryBookCond(
	int page,
	int size,
	String bookCategory
) {

	public SearchLibraryBookQuery() {
		return new SearchLibraryBookQuery(
			page,
			size,
			BookCategory.from(bookCategory)
		);
	}
}



@GetMapping(&quot;/books&quot;)
public String searchLibraryBooks(
	final SearchLibraryBookCond searchCond
) {
	final SearchLibraryBookQuery searchQuery = searchCond.toSearchQuery();
	libraryService.getLibraryBook(searchQuery);
	return ApiResponse.from(result);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;String&lt;/span&gt; 타입으로 요청을 받고, 지정된 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;Enum&lt;/span&gt; 타입(BookCategory)로 변환하는 로직이 추가됬습니다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 위 코드처럼 변환 로직이 모든 요청에 대해 필요하게 됩니다.&lt;br /&gt;String -&amp;gt; Enum 으로 타입만 다른 또 하나의 데이터 객체가 생성되기도 하죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 웹 요청이 반영된 코드가 침투 할 수도 있습니다.&lt;br /&gt;String -&amp;gt; Enum을 변환하는 과정에서 아래 코드를 사용하였는데요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;BookCategory.from(bookCategory)&lt;/blockquote&gt;
&lt;pre id=&quot;code_1713147177824&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public enum BookCategory {
    FICTION,
    NON_FICTION,
    // ...
    TECHNOLOGY,
    PROGRAMMING,
    SOFTWARE,
    ;

    public static BookCategory from(final String source) {
        return BookCategory.valueOf(source.toUpperCase());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Enum에 외부 요청에 대한 로직(대소문자 구분 X)이 침투하게 됩니다.&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;Conversion 사용 하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 타입을 Enum으로 정의해도 성공적으로 바인딩 된 이유는, Spring core에서 String-&amp;gt;Enum Converter 가 등록되어 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 요청이 들어오면, handler가 지정되고, handler 요청에 필요한 타입으로 변환해 주는 작업이 이루어집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-04-15 at 11.32.15.png&quot; data-origin-width=&quot;2952&quot; data-origin-height=&quot;898&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nV2dU/btsGBQiDn2W/V2k5owArTWNZZm9SKH3QuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nV2dU/btsGBQiDn2W/V2k5owArTWNZZm9SKH3QuK/img.png&quot; data-alt=&quot;기본으로 등록된 conveters.StringToEnumConverterFactory&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nV2dU/btsGBQiDn2W/V2k5owArTWNZZm9SKH3QuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnV2dU%2FbtsGBQiDn2W%2FV2k5owArTWNZZm9SKH3QuK%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;2952&quot; height=&quot;898&quot; data-filename=&quot;Screenshot 2024-04-15 at 11.32.15.png&quot; data-origin-width=&quot;2952&quot; data-origin-height=&quot;898&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기본으로 등록된 conveters.StringToEnumConverterFactory&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1713148489789&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.springframework.core.convert.support;

final class StringToEnumConverterFactory implements ConverterFactory&amp;lt;String, Enum&amp;gt; {

	@Override
	public &amp;lt;T extends Enum&amp;gt; Converter&amp;lt;String, T&amp;gt; getConverter(Class&amp;lt;T&amp;gt; targetType) {
		return new StringToEnum(ConversionUtils.getEnumType(targetType));
	}


	private static class StringToEnum&amp;lt;T extends Enum&amp;gt; implements Converter&amp;lt;String, T&amp;gt; {

		private final Class&amp;lt;T&amp;gt; enumType;

		StringToEnum(Class&amp;lt;T&amp;gt; enumType) {
			this.enumType = enumType;
		}

		@Override
		@Nullable
		public T convert(String source) {
			if (source.isEmpty()) {
				// It's an empty enum identifier: reset the enum value to null.
				return null;
			}
			return (T) Enum.valueOf(this.enumType, source.trim());
		}
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Override한 메서드, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;T convert()&lt;/span&gt;에서 타입 전환이 이루어지기 때문에 따로 정의하지 않아도 알아서 Enum타입으로 변환되고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서는 Converter와 관련한 SPI(&lt;b&gt;S&lt;/b&gt;ervice &lt;b&gt;P&lt;/b&gt;rovider &lt;b&gt;I&lt;/b&gt;nterface)를 제공하여 확장성 있게 저만의 정책을 반영하여 구현할 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring conversion doc -&amp;nbsp;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/blob/v6.1.6/framework-docs/modules/ROOT/pages/core/validation/convert.adoc&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/spring-projects/spring-framework/blob/v6.1.6/framework-docs/modules/ROOT/pages/core/validation/convert.adoc&lt;/a&gt;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Converter로 구현 하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에 제공 되는 것처럼, Converter를 직접 구현해서 타입 변환 로직을 담아낼 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Converter&amp;lt;S, T&amp;gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1713149402376&quot; class=&quot;css&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package org.springframework.core.convert.converter;

@FunctionalInterface
public interface Converter&amp;lt;S, T&amp;gt; {
	@Nullable
	T convert(S source);
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;BookCategoryConveter&lt;/blockquote&gt;
&lt;pre id=&quot;code_1713149521843&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class BookCategoryConverter implements Converter&amp;lt;String, BookCategory&amp;gt; {

    @Override
    public BookCategory convert(final String source) {
        return Arrays.stream(BookCategory.values())
                .filter(bookCategory -&amp;gt; bookCategory.name().equalsIgnoreCase(source))
                .findAny()
                .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;Invalid book category&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;이후, WebMvcConfiguter를 통해 구현한 Converter를 등록합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;WebConverterConfig&lt;/blockquote&gt;
&lt;pre id=&quot;code_1713149942571&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class WebConverterConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(final FormatterRegistry registry) {
        registry.addConverter(new BookCategoryConverter());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 로직을 검증하는 테스트도 잘 통과하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-04-15 at 11.55.51.png&quot; data-origin-width=&quot;2818&quot; data-origin-height=&quot;1708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9PXdj/btsGA1yf65X/KoYbjE6H64qzeVnh867RK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9PXdj/btsGA1yf65X/KoYbjE6H64qzeVnh867RK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9PXdj/btsGA1yf65X/KoYbjE6H64qzeVnh867RK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9PXdj%2FbtsGA1yf65X%2FKoYbjE6H64qzeVnh867RK1%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;2818&quot; height=&quot;1708&quot; data-filename=&quot;Screenshot 2024-04-15 at 11.55.51.png&quot; data-origin-width=&quot;2818&quot; data-origin-height=&quot;1708&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;blockquote data-ke-style=&quot;style2&quot;&gt;ConversionService에도 구현한 Converter가 등록되었고, 해당 converter의 구현 메서드가 실행됨&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-04-15 at 10.54.10.png&quot; data-origin-width=&quot;2952&quot; data-origin-height=&quot;898&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yqysG/btsGCbzNSAO/1qhuafpNXckes2hK6KNtaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yqysG/btsGCbzNSAO/1qhuafpNXckes2hK6KNtaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yqysG/btsGCbzNSAO/1qhuafpNXckes2hK6KNtaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyqysG%2FbtsGCbzNSAO%2F1qhuafpNXckes2hK6KNtaK%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;2952&quot; height=&quot;898&quot; data-filename=&quot;Screenshot 2024-04-15 at 10.54.10.png&quot; data-origin-width=&quot;2952&quot; data-origin-height=&quot;898&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;position: absolute;&quot; data-ke-size=&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;만약, 동일한 로직의 변환이 필요한 타입이 10개라면 모두 구현해주어야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713150075479&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class WebConverterConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(final FormatterRegistry registry) {
        registry.addConverter(new BookCategoryConverter());
        registry.addConverter(new AuthorCategoryConverter());
        registry.addConverter(new PublisherCategoryConverter());
        registry.addConverter(new InteresetCategoryConverter());
	// ...
    }
}&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;공통적인 변환 로직이 있다면, ConverterFactory를 활용할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ConverterFactory로 구현하기&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Using&amp;nbsp;ConverterFactory&lt;br /&gt;When you need to centralize the conversion logic for an entire class hierarchy (for example, when converting from&amp;nbsp;String&amp;nbsp;to&amp;nbsp;Enum&amp;nbsp;objects), you can implement&amp;nbsp;ConverterFactory, as the following example shows:&lt;br /&gt;&lt;br /&gt;- https://docs.spring.io/spring-framework/reference/core/validation/convert.html&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에 따르면, 전역적(중앙 집중적)으로 변환 로직을 사용해야 한다면 ConverterFactory를 사용할 수 있다고 하는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(주어진 예시도 String -&amp;gt; Enum 입니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 활용해서 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;StringToEnumCustomConverterFactory&lt;/span&gt;를 구현해보았습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713151377084&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class StringToEnumCustomConverterFactory implements ConverterFactory&amp;lt;String, Enum&amp;lt;?&amp;gt;&amp;gt; {

    public &amp;lt;T extends Enum&amp;lt;?&amp;gt;&amp;gt; Converter&amp;lt;String, T&amp;gt; getConverter(final Class&amp;lt;T&amp;gt; targetType) {
        return new StringToEnumCustomConverter&amp;lt;&amp;gt;(targetType);
    }

    private record StringToEnumCustomConverter&amp;lt;T extends Enum&amp;lt;?&amp;gt;&amp;gt;(
            Class&amp;lt;T&amp;gt; targetType
    ) implements Converter&amp;lt;String, T&amp;gt; {

        @Override
        public T convert(String source) {
            return Arrays.stream(targetType.getEnumConstants())
                    .filter(enumConstant -&amp;gt; enumConstant.name().equalsIgnoreCase(source)).findAny()
                    .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;Invalid value&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;ConvertFactory는 각각에 맞는 Converter를 반환하여, 변환하려는 타입에 맞는 convert를 사용할 수 있는데요.&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;먼저 ConverterFactory&amp;lt;S, T&amp;gt; 를 구현합니다. Enum 전역을 대상으로 할 예정이기에 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&amp;lt;String, Enum&amp;lt;?&amp;gt;&amp;gt;&lt;/span&gt;를 사용했습니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;변환하려는 타입, 제네릭 타입인 T(targetType)에 동적으로 결정됩니다.&lt;/li&gt;
&lt;li&gt;T 타입에 해당하는 converter를 구현합니다. (&lt;span style=&quot;background-color: #f6e199;&quot;&gt;record StringToEnumCustomConverter&amp;lt;T extends Enum&amp;lt;?&amp;gt;&amp;gt;&lt;/span&gt;)&lt;/li&gt;
&lt;li&gt;getConverter가 호출되면 변환하려는 타입에 맞게 동적으로 정의되는 converter가 반환 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 모든 Enum에 대해 적용되고 있는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Interface를 통해 로직이 반영될 Enum에 구현하고, wildcard의 타입을 제한하여 로직을 집중시킬 수도 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Sample&lt;/blockquote&gt;
&lt;pre id=&quot;code_1713151924496&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface InsensitiveEnum {}

public enum BookCategory implements InsensitiveEnum {}

public class StringToEnumCustomConverterFactory implements ConverterFactory&amp;lt;String, Enum&amp;lt;? extends InsensitiveEnum&amp;gt;&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;pre id=&quot;code_1713151985938&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class WebConverterConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(final FormatterRegistry registry) {
        final StringToEnumCustomConverterFactory converterFactory = new StringToEnumCustomConverterFactory();
        registry.addConverterFactory(converterFactory);
    }
}&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;테스트를 위해 Enum을 하나 추가하고 진행해보았습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713152253941&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum TestEnum {
    PRIVATE,
    PUBLIC,
    ;
}

public record SearchLibraryBookCond(
        int page,
        int size,
        BookCategory bookCategory,
        TestEnum testEnum
) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-04-15 at 12.37.32.png&quot; data-origin-width=&quot;2818&quot; data-origin-height=&quot;1708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sN0No/btsGCZ6LFpr/67E5B7CGYC0ZamE8kWOAEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sN0No/btsGCZ6LFpr/67E5B7CGYC0ZamE8kWOAEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sN0No/btsGCZ6LFpr/67E5B7CGYC0ZamE8kWOAEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsN0No%2FbtsGCZ6LFpr%2F67E5B7CGYC0ZamE8kWOAEk%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;2818&quot; height=&quot;1708&quot; data-filename=&quot;Screenshot 2024-04-15 at 12.37.32.png&quot; data-origin-width=&quot;2818&quot; data-origin-height=&quot;1708&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 타겟 타입에 대한 Converter를 등록하지 않아도, 정의한 Enum에 대해 대소문자 구분 없이 변환하는 로직을 적용 할 수 있었습니다.&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;ConverterFactory를 통해 공통적으로 변환 로직을 적용할 수 있었습니다.&lt;/p&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;이를 고려해서 InsensitiveEnum과 같은 선택적으로 적용하는 방법도 고안했지만, 웹 요청과 관련한 로직이 도메인으로 정의한 Enum에 침범하는 경우도 있을 수 있다는 점을 고려해야 합니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>converter</category>
      <category>Spring</category>
      <author>RoyceWon</author>
      <guid isPermaLink="true">https://curiosity-storage.tistory.com/18</guid>
      <comments>https://curiosity-storage.tistory.com/18#entry18comment</comments>
      <pubDate>Mon, 22 Apr 2024 14:32:48 +0900</pubDate>
    </item>
    <item>
      <title>Redis - 사용 패턴 (분산락-Redlock)</title>
      <link>https://curiosity-storage.tistory.com/19</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis programming patterns&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 개발 패턴&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Bulk Loading&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 프로토콜을 이용하여 대량으로 쓰기 작업을 하는 패턴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 명령을 차례로 전송하여 연산을 하는 경우, 모든 명령의 RTT가 포함되어 있기 때문에 속도가 느리기 때문에 일반적인 Redis Client를 통해 대량으로 쓰기 작업(load)을 하는 것을 권장하지 않는다.&lt;br /&gt;&lt;code&gt;Pipelining&lt;/code&gt;을 통해 다량의 명령을 빠르게 수행할 수 있지만 대량의 데이터 쓰기 작업과 응답을 읽는 작업이 동시에 진행되어야 한다.&lt;br /&gt;또, 적은 비율의 클라이언트에서만 non-blocking I/O를 지원하기 때문에 모든 클라이언트에서 bulk쓰기 처리량을 최대화 하기 위해선 Redis 프로토콜이 포함된 text 파일을 생성 후 전송하는 방법이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 수십억 개의 데이터를 추가하고자 할 때, 해당 명령이 포함된 파일을 Redis 프로토콜 형식으로 생성 하고 Redis에 전송하면 된다.&lt;br /&gt;redis-cli의 &lt;code&gt;pipe mode&lt;/code&gt;를 통해 전달 가능하다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SET Key0 Value0
SET Key1 Value1
...
SET Key100000000 Value100000000&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;&amp;gt; cat data-insert.txt | redis-cli --pipe

All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 1000000&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;netcat을 통해 전달도 가능하지만, 응답 여부나 오류 발생 여부를 확인 할 수 없어서 &lt;code&gt;--pipe&lt;/code&gt;를 통해 전달하는 것을 권장한다&lt;/i&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis protocol은 아래 예시처럼 작성 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;*&amp;lt;args&amp;gt;&amp;lt;cr&amp;gt;&amp;lt;lf&amp;gt;
$&amp;lt;len&amp;gt;&amp;lt;cr&amp;gt;&amp;lt;lf&amp;gt;
&amp;lt;arg0&amp;gt;&amp;lt;cr&amp;gt;&amp;lt;lf&amp;gt;
&amp;lt;arg1&amp;gt;&amp;lt;cr&amp;gt;&amp;lt;lf&amp;gt;
...
&amp;lt;argN&amp;gt;&amp;lt;cr&amp;gt;&amp;lt;lf&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SET keyA valueA&lt;/code&gt; 를 프로토콜로 작성하면 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;*3&amp;lt;cr&amp;gt;&amp;lt;lf&amp;gt;         # *3은 인자의 개수가 3개 
$3&amp;lt;cr&amp;gt;&amp;lt;lf&amp;gt;
SET&amp;lt;cr&amp;gt;&amp;lt;lf&amp;gt;
$4&amp;lt;cr&amp;gt;&amp;lt;lf&amp;gt;         # &quot;keyA&quot;의 길이
keyㅁ&amp;lt;cr&amp;gt;&amp;lt;lf&amp;gt;
$6&amp;lt;cr&amp;gt;&amp;lt;lf&amp;gt;
valueA&amp;lt;cr&amp;gt;&amp;lt;lf&amp;gt;  # &quot;valueA&quot;의 길이&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 사용하면 아래와 같이 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;public String generateRedisProtocol(String... cmd) {
    StringBuilder proto = new StringBuilder();
    proto.append(&quot;*&quot;).append(cmd.length).append(&quot;\r\n&quot;);

    for (String arg : cmd) {
        proto.append(&quot;$&quot;).append(arg.getBytes().length).append(&quot;\r\n&quot;);
        proto.append(arg).append(&quot;\r\n&quot;);
    }
    return proto.toString();
}

generateRedisProtocol(&quot;SET&quot;, &quot;keyA&quot;, &quot;valueA&quot;, &quot;SET&quot;, &quot;key&quot;, &quot;value);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Distributed locks&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산락으로 사용하기&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;분산락이란 서로 다른 프로세스에서 공유 자원에 접근할 때, 원자성을 보장하기 위해 사용하는 방법&lt;/i&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis로 DLM(Distributed Lock Manager)을 구현한 여러 라이브러리가 존재하고, Java의 구현체는 &lt;a href=&quot;https://github.com/redisson/redisson&quot;&gt;Redisson&lt;/a&gt;이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docs에서는 Redis로 분산 잠금을 어떻게 구현하는 지 보단 표준적인 알고리즘에 대해 설명하고 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Safety and Liveness Guarantees&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에선 분산락을 구현 할 때, 최소한 세가지 속성을 보장하며 설계한다고 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;Safety property&lt;/code&gt;: 하나의 클라이언트만 락 획득이 가능 하다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Liveness property A&lt;/code&gt;: Deadlock free. 리소스가 잠겨있거나 클라이언트간 충돌이 발생해도 결과적으로 항상 락을 획득 가능 하다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Liveness property B&lt;/code&gt;: 내결함성. Redis 노드가 가동 중인 한, 클라이언트는 잠금을 획득하고 해제할 수 있다.&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Safety_and_liveness_properties&quot;&gt;https://en.wikipedia.org/wiki/Safety_and_liveness_properties&lt;/a&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Why Failover-based Implementations Are Not Enough&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애조치 기반 구현이 부족한 이유.&lt;br /&gt;일반적은 Redis 기반 분산락 라이브러리의 현재 상황에 대해 살펴보자.&lt;br /&gt;일반적으로 키를 생성하여 락을 구현하고, 만료 기능(expire)를 통해 유효 기간을 설정하여 락을 해제하는 식으로 구현된다.&lt;br /&gt;이러한 구현 방식은 일반적으로 잘 동작하지만, SPOF가 존재한다. 만약 master redis다 다운되는 상황에서 비동기로 replica되기 때문에 안전성 보장이 깨질 수 있다.&lt;br /&gt;아래 예시를 보면,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ClientA 가 마스터에서 잠금을 획득한다.&lt;/li&gt;
&lt;li&gt;특정키에 대한 쓰기가 복제본으로 relica 되기 전에 다운 된다.&lt;/li&gt;
&lt;li&gt;복제본이 승격 된다.&lt;/li&gt;
&lt;li&gt;ClientB가 특정키에 대해 접근하며 락을 획득한다.&lt;br /&gt;=&amp;gt; ClientA와 ClientB가 모두 락을 획득하는 안전성 보장이 깨지게 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에선 이러한 상황을 개선하는 방식을 제안한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Correct Implementation with a Single Instance&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 Redis 환경에서 구현하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 간단하게 락을 구현하는 방법은 아래와 같이 &lt;code&gt;NX&lt;/code&gt;와 &lt;code&gt;PX&lt;/code&gt;옵션을 통해 키를 생성하는 것이다.&lt;br /&gt;&lt;code&gt;SET resource_name random_value NX PX 30000&lt;/code&gt;&lt;br /&gt;위 명령을 통해 key가 존재하지 않은 경우에만 생성하며, 30초의 유효시간을 가진다. 이때 값은 유니크한 값이어야 한다. 유니크한 랜덤값으로 락을 점유한 client와 락을 해제하려는 client가 동일하게 보장하기 위함이다.&lt;br /&gt;기본적으로 Lua를 통해 락을 생성한 client만이 해당 락을 해제하는 로직을 작성 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;if redis.call(&quot;get&quot;,KEYS[1]) == ARGV[1] then
    return redis.call(&quot;del&quot;,KEYS[1])
else
    return 0
end&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;StandAlone&lt;/code&gt; 환경에서는 비교적 간단하게 lock을 구현 할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;The Redlock Algorithm&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;StandAlone&lt;/code&gt;이 아닌 한개의 여러 Redis가 운영되는 환경에서는 RedLock 알고리즘을 통해 분산락을 구현 할 수 있다. 여러 Redis master로 운영되는 환경에서는 각각의 노드들은 독립적이며 복제나 조정 시스템은 없다고 가정한다.&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;모든 Redis 인스턴스에 순차적으로 잠금을 획득 요청을 보낸다.&lt;/li&gt;
&lt;li&gt;1번 단계에서 얻은 시간과 락 획득 요청 응답이 온 시간으의 차이를 통해 경과한 시간을 구한다.&lt;/li&gt;
&lt;li&gt;Client가 Redis노드로부터 특정 수치 이상의 잠금을 획득 하고, 락을 획득 하기까지 경과한 총 시간이 키 유효시간보다 적은 경우 잠금을 획득한 것으로 간주한다.&lt;/li&gt;
&lt;li&gt;잠금을 획득한 경우, 잠금의 유효 시간은 3단계에서 계산한 대로 초기 유효 시간에서 경과 시간을 뺀 값으로 간주 한다.&lt;/li&gt;
&lt;li&gt;만약 Client가 잠금을 획득하지 못한 경우, 클라이언트는 모든 인스턴스의 잠금을 해제한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 시간&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락 획득을 위해 모든 Redis 노드에 락 획득 요청을 보낸다.&lt;/p&gt;
&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/299c06b7-f95a-484d-8713-7fb5942c0462/image.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;락 획득 요청 결과&lt;/p&gt;
&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/8c46df53-f788-405e-b80e-487709706579/image.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위 그림처럼, 5개의 노드중에 3개의 노드로부터 락을 획득하고 지정한 키 만료 시간보다 획득하는데 적은 시간이 경과된 경우 이 클라이언트는 락을 획득 한 것으로 간주한다.&lt;/p&gt;
&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/4eb34755-4231-4750-bcf1-d6607768dc8f/image.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;기존 락 유효시간(10,000ms) + 락을 얻는데 걸린 시간인 (4ms) 을 합하여 락 유효시간을 설정합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redlock 알고리즘은 각 클라이언트와 Redis 노드간의 시계가 동일하게 작동하는 것을 가정하고 동작하므로, 실제 환경에서는 동기화된 시계에 대해서도 고려하여야 한다.&lt;br /&gt;클라이언트가 락을 획득하지 못한 경우 무작위 지연 시간 후에 다시 시도한다. 또, 클라이언트들은 여러 노드에 대한락 획득 시도가 빠를 수록 락을 획득할 가능성이 높아지므로 멀티플렉싱을 통해 N개의 Redis에 SET명령을 수행하는 것이 이상적이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Performance, Crash Recovery and fsync&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 락 획득 요청을 멀티플렉싕하여 지연 시간을 줄여 성능을 높힐 수 있다. 이 과정에서 분산 Redis 환경에서 장애가 발생하면 고려해야 할 상황이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5개의 Redis 노드에서 ClientA가 3개의 노드(A, B, C)로부터 Lock을 얻어 분산락을 획득했다고 해보자.&lt;br /&gt;이 상황에서 Redis 노드 3번이 종료되어 key가 휘발되어 사라졌고, 다시 복구되어 실행 되었다.&lt;br /&gt;이때, ClientB가 2개의 노드(D, E)로부터 락을 얻었고 방금 재부팅된 C에서도 락을 획득하여 (총 3개) 분산락을 획득 할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ClientA&lt;/code&gt;와 &lt;code&gt;ClientB&lt;/code&gt;가 분산 Redis환경에서 분산락 획득을 시도한다.&lt;/p&gt;
&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/36008c1c-9481-402f-842a-eb851a826777/image.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;5개중에 3개를 획득한 A가 결과적으로 분산락을 획득했고, B는 재시도 과정을 수행중이다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;code&gt;ClientA&lt;/code&gt;가 락을 가지고 작업을 수행하는 중, Redis node4번이 다운 된다. A가 락으로 설정한 KeyA가 휘발된다.&lt;/p&gt;
&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/866734fd-328d-4a2a-8ec3-f4f2edb789a3/image.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;node4번의 장애가 복구 되었다. 이때, &lt;code&gt;ClientB&lt;/code&gt;가 복구된 node4번으로부터 락을 획득하였고, 총 3개의 락을 얻은 B도 분산락을 얻게되는 락 안전성이 깨지게 된다.&lt;/p&gt;
&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/eb7b8d04-fb65-4371-aeea-ccdfc985b309/image.png&quot; alt=&quot;&quot; /&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우 &lt;code&gt;AOF&lt;/code&gt;를 통해 개선하거나 &lt;code&gt;fsync=always&lt;/code&gt;옵션을 통해 락에 대한 안전성을 보장할 수 있다. 이 과정에서 성능에 영향을 미칠 수 있다.&lt;br /&gt;위 그림에서 Redis node4번이 복구 될 때, 쓰기 작업이 수행된 키도 휘발되지 않고 복구하며 실행 된다면 &lt;code&gt;ClientB&lt;/code&gt;는 락을 획득 하지 못하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 방법으로는 다운된 Redis노드를 일반적인 키 만료 시간보다 더 늦게 사용할 수 있도록 구성하면 된다.&lt;br /&gt;키의 만료시간이 10초라면, Redis node4의 복구를 10초 이후에 하여 락이 해제 되는 시간을 보장하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우, 안전성을 위해 성능 저하가 발생하게 되는데 이 부분을 고려하여 운영 환경에 맞게 구성하면 될 것 같다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Secondary indexing&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 정확하게 key-value 형태의 스토리지라기 보단 더 복잡한 데이터 구조를 가지고 있으며 API 수준에서는 Key를 기준으로 주소가 지정된다. 기본적으로는 Key를 통한 데이터 접근만 제공하긴 하지만, 복합인덱스를 비롯한 여러 Secondary index를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에서 인덱스를 구현하고 유지하는 것을 고려하기 전에 복잡한 쿼리가 필요한 경우 관계형DB가 더 나은지 고려할 필요가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Simple numeric indexes with sorted sets&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 쉽게 Secondary index를 구현하는 방법은 각 요소에 점수(숫자)를 할당하고, 해당 점수로 정렬된 데이터를 사용하는 것이다. =&amp;gt; (Sorted set)&lt;br /&gt;&lt;code&gt;ZADD&lt;/code&gt;, &lt;code&gt;ZRANGE&lt;/code&gt;, &lt;code&gt;BYSCORE&lt;/code&gt;의 키워드를 통해 이러한 종류의 인덱스를 구축하고 검색 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 나이와 이름을 저장한 데이터에서 나이를 통해 조회를 하고 싶은 경우 아래와 같이 구성 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;&amp;gt; ZADD myindex 25 Manuel
&amp;gt; ZADD myindex 18 Anna
&amp;gt; ZADD myindex 35 Jon
&amp;gt; ZADD myindex 67 Helen

&amp;gt; ZRANGE myindex 20 40 BYSCORE # 나이를 통해 조회
1) &quot;Manuel&quot;
2) &quot;Jon&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;REV&lt;/code&gt;를 통해 역순으로 범위 검색도 가능하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Using objects IDs as associated values&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sorted Set에서 제공하는 score numberic이 아닌 필드를 인덱싱 하려면 아래처럼 사용 가능하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;gt; HMSET user:1 id 1 username antirez ctime 1444809424 age 38
&amp;gt; HMSET user:2 id 2 username maria ctime 1444808132 age 42
&amp;gt; HMSET user:3 id 3 username jballard ctime 1443246218 age 33

&amp;gt; ZADD user.age.index 38 1
&amp;gt; ZADD user.age.index 42 2
&amp;gt; ZADD user.age.index 33 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;user.age.index&lt;/code&gt;를 통해 찾고자 하는 &lt;code&gt;userId&lt;/code&gt;를 조회 한 뒤, 해당 아이디로 Set에서 데이터를 검색 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값을 변경해야 하는 경우, 두 key에 모두 반영해야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;HSET user:1 age 39
ZADD user.age.index 39 1&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lexicographical indexes&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자 뿐만 아니라 사전식 정렬을 통해서도 구현 할 수 있다. Redis는 내부적으로 문자열을 &lt;code&gt;memcmp()&lt;/code&gt;을 통해 바이너리 데이터를 비교한다. &lt;code&gt;foo&lt;/code&gt; &amp;lt; &lt;code&gt;foobar&lt;/code&gt;가 더 크다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;gt; ZADD myindex 0 baaa
&amp;gt; ZADD myindex 0 abbb
&amp;gt; ZADD myindex 0 aaaa
&amp;gt; ZADD myindex 0 bbbb&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sorted Set에서는 score가 동일하면 value의 값으로 정렬된다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;&amp;gt; ZRANGE myindex 0 -1
1) &quot;aaaa&quot;
2) &quot;abbb&quot;
3) &quot;baaa&quot;
4) &quot;bbbb&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BYSCORE&lt;/code&gt; 대신, &lt;code&gt;BYLEX&lt;/code&gt;를 통해 사전순 범위 검색이 가능하다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;&amp;gt; ZRANGE myindex [a (b BYLEX
1) &quot;aaaa&quot;
2) &quot;abbb&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;[&lt;/code&gt;는 inclusive, &lt;code&gt;(&lt;/code&gt;는 exclusive 이다.&lt;br /&gt;위 구문은 &lt;code&gt;a &amp;lt;= value &amp;lt; b&lt;/code&gt; 의 값을 조회하는 것으로 볼 수 있다.&lt;br /&gt;&lt;code&gt;+&lt;/code&gt; 와 &lt;code&gt;-&lt;/code&gt; 도 제공되는 특수 문자이다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;ZRANGE myindex [b + BYLEX
1) &quot;baaa&quot;
2) &quot;bbbb&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 속성을 활용해서 다양한 방법으로 인덱스를 사용할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Composite indexes&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 RDBMS에서 그렇듯, 여러 필드에 대한 인덱스도 구성 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방의 가격과 방의 상품 검색을 위해 인덱스를 구성한다면 아래와 같이 구성 할 수 있다.&lt;br /&gt;만약 &lt;code&gt;room:price:product_id&lt;/code&gt;를&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;gt; ZADD myindex 0 0056:0028.44:90
&amp;gt; ZADD myindex 0 0034:0011.00:832

&amp;gt; ZRANGE myindex [0056:0010.00 [0056:0030.00 BYLEX&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ZRANGE&lt;/code&gt;를 통해 56번 방에 있는 모든 제품의 가격이 10달러에서 30달러 사이인 제품을 매우 쉽게 얻을 수 있다.&lt;/p&gt;</description>
      <category>Redis</category>
      <category>docs</category>
      <category>redis</category>
      <author>RoyceWon</author>
      <guid isPermaLink="true">https://curiosity-storage.tistory.com/19</guid>
      <comments>https://curiosity-storage.tistory.com/19#entry19comment</comments>
      <pubDate>Tue, 16 Apr 2024 16:45:40 +0900</pubDate>
    </item>
    <item>
      <title>Docker network로 모니터링 시스템 내부망 사용</title>
      <link>https://curiosity-storage.tistory.com/16</link>
      <description>&lt;h1&gt;모니터링 시스템 구축기(3)&lt;/h1&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;code&gt;Spring Actuator&lt;/code&gt;, &lt;code&gt;Prometheus&lt;/code&gt; 는 외부에서 누구든지 접속 할 수 있습니다. &lt;code&gt;Granfana&lt;/code&gt;도 접근할 수 있지만, 자체적으로 제공하는 &lt;code&gt;ID/PW&lt;/code&gt; 인증이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Spring Actuator&lt;/code&gt; 같은 경우에는 Spring application 단에서 해당 경로 (&lt;code&gt;/actuator&lt;/code&gt;) 자체에 대한 보안을 추가할 수 있습니다. &lt;code&gt;Intercetpor&lt;/code&gt;나 &lt;code&gt;Filter&lt;/code&gt;에서 해당 url에 대한 인가 정보를 검증하는 식으로요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 &lt;code&gt;Prometheus&lt;/code&gt;에서 Scrape 요청시 인증 정보를 추가하여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 &lt;code&gt;Prometheus&lt;/code&gt;에 대한 자체적인 접근은 어떻게 막을 수 있을까요? 이 또한 보안을 둔다면, Grafana에서의 요청에서도 인증 정보를 추가적으로 설정해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/6587d618-293b-442a-baf2-e225d0b3b2a8/image.png&quot; alt=&quot;img&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;그래서 저는 모니터링 컨테이너들 간의 통신은 가능하고, 외부에서는 Grafana를 통해서만 모니터링을 할 수 있도록 구성해보려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적인 모습은 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/045422a1-f57b-44ef-87f1-91565c1c30bb/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡하긴 하지만, 결국 Grafana를 통해서만 모니터링 정보가 노출 될 수 있도록 관리하려고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Actuator 포트 및 경로 분리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Spring actuator의 prometheus endpoints를 분리해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Actuator는 기본적으로 ``/actuator/{endpoints}` 의 형태를 가지기 때문에 비교적 쉽게 예측하고 접근 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 아래 설정을 통해 해당 매트릭 지표의 url을 분리할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# application.yml

management:
  endpoints:
    web:
      exposure:
        include: &quot;prometheus&quot;
  server:
    port: ${SECRET PORT}
    base-path: ${SECRET PATH}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;localhost:8080/actuator/prometheus&lt;/code&gt; 는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;localhost:{secret_port}/{secret_path}/actuator/prometheus&lt;/code&gt; 경로로 노출되게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/93b9f47a-a3b4-4d8a-b68c-c5f63ad5e9ae/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;(port: 20808, base-path: secret 인 경우)&lt;/i&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8080를 사용하는 톰캣 context와 분리되어 이전 server.servlet.contextPath=&quot;/api&quot; 는 적용되지 않습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 달라진 endpoint에 맞게 prometheus에서 설정을 변경해주겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Docker network&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Docker Network&lt;/code&gt;란 실행된 각각의 컨테이너끼리 연결하기 위한 논리적인 네트워크입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Network를 구성하면 같은 네트워크 안에서는 각각의 IP를 지정할 필요 없이 &lt;code&gt;name&lt;/code&gt;만으로 네트워크 연결이 가능하다는 장점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 아웃바운드 포트를 오픈하지 않고도 내부적으로만 통신하게 구성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일종의 private network를 구현 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, Sping application과 monitoring 툴들이 사용할 network를 하나 생성하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt; docker network create celuveat_network&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 &lt;code&gt;docker network ls&lt;/code&gt;를 통해 생성된 네트워크를 확인 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/333c7d30-ee22-4e5f-9f14-fa304ca59a51/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Network에는 3가지의 드라이버가 있는데, 기본적으로 &lt;code&gt;bridge&lt;/code&gt;로 생성됩니다.&lt;br /&gt;(참고: &lt;a href=&quot;https://docs.docker.com/network/drivers/&quot;&gt;https://docs.docker.com/network/drivers/&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 동일한 호스트(ec2)에서 도커 이미지간의 통신을 위해 사용되므로 &lt;code&gt;bridge&lt;/code&gt; driver로 구성하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker network와 함께 각각의 컨테이너들을 구성하는 &lt;code&gt;docker-compose-monitoring.yml&lt;/code&gt; 파일을 수정하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# docker-compose-monitoring.yml

version: '3'

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    networks:
      - celuveat_network
    #ports:
    #  - &quot;9090:9090&quot; 외부 포트 바인딩 제거
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    depends_on:
      - celuveatdev

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    networks:
      - celuveat_network
    user: &quot;$UID:$GID&quot;
    ports:
      - &quot;3000:3000&quot;
    volumes:
      - ./grafana-data:/var/lib/grafana
    depends_on:
      - prometheus

  loki:
    image: grafana/loki:latest
    networks:
      - celuveat_network
    # ports:
    #  - &quot;3100:3100&quot; 외부 포트 바인딩 제거
    command: -config.file=/etc/loki/local-config.yaml

  promtail:
    image: grafana/promtail:latest
    networks:
      - celuveat_network
    volumes:
      - ./logs:/logs
      - ./promtail-config.yml:/etc/promtail/config.yml
    command: -config.file=/etc/promtail/config.yml

networks:
  celuveat_network:
    external: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 서비스 마다 networks 옵션을 추가하여 network에 구성 시켰고,&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;    networks:
      - celuveat_network&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;compose 파일 하단에 아래 옵션을 통해 이번 compose파일에서 구성하는 것이 아닌 존재하는 network임을 명시하였습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;networks:
  celuveat_network:
    external: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 외부 포트 바인딩 없이 private network를 통해서 접근하려고 하는 prometheus의 포트 바인딩 옵션도 제거하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 맞게 Spring application을 구성하는 docker-compose.yml 파일도 수정하였습니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# docker-compose.yml

version: '3'

services:

  celuveatdev:
    image: roycewon/celuvedev:0.0.1
    networks: # 추가된 옵션
      - celuveat_network 
    container_name: celuveatdev
    ports:
      - &quot;8080:8080&quot;
    environment:
      - SPRING_PROFILES_ACTIVE=prod
    volumes:
      - /logs:/logs

networks:
  celuveat_network:
    external: true&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설정 수정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 private 망으로 숨어버린 prometheus와 loki에 접근할 수 있도록 설정 값들을 변경하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 docker network를 통해 이제 ip가 아닌, 컨테이너 이름으로 접근할 수 있게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 prometheus -&amp;gt; spring application에 접근하여 메트릭을 수집하는 설정을 수정해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;scrape_configs:
  - job_name: 'celuveatdev'
    metrics_path: '/{secret_base_path}/actuator/prometheus'
    scrape_interval: 5s
    static_configs:
      - targets: ['celuveatdev:{secret_port}'] # 변경&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 ip, localhost가 아닌 &lt;code&gt;celuveatdev&lt;/code&gt;라는 컨테이너 이름으로 지정하도록 수정하였습니다. 또, 앞서 변경해두었던 port와 base-path에 맞게 수정하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 log파일을 수집하여 loki로 보내는 promtail의 값을 변경해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 &lt;code&gt;clients.url&lt;/code&gt; 옵션만 localhost(ip) -&amp;gt; loki 라는 컨테이너 이름으로 지정하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;clients:
  - url: http://loki:3100/loki/api/v1/push&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 모니터링이 가능한 grafana입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 했던 적용방식과 동일하게 ip 대신 container 이름으로 변경합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/616549b3-eb92-420f-91dd-e495a7e44ba2/image.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/bdd6522d-3418-4704-a54d-355fcf649bae/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 docker-compose를 재실행 하면 대시보드가 잘 작동하는 것을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/39fa4a5a-292e-4bd7-aa96-3526f58c126c/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Prometheus의 기본 포트였던 9090으로는 접근이 불가합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/94eb2145-8b35-497c-9666-b4dac668bbec/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8080포트에서 분리했던 Spring actuator 역시 8080외의 포트에 할당되었기 때문에 외부에서 접근할 수 없게 되었습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 이름만으로 가능했던 이유는 도커에서 지원해주는 내장 DNS서버가 있기 때문입니다. 먼저 docker network에 연결되어있는 dns를 탐색하게 되기 때문이죠.&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;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번의 글에 걸쳐 모니터링 시스템을 구축하였습니다.&lt;br /&gt;아래와 같은 아키텍쳐로 구성하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/045422a1-f57b-44ef-87f1-91565c1c30bb/image.png&quot; alt=&quot;&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;</description>
      <category>인프라</category>
      <category>docker</category>
      <category>Monitoring</category>
      <category>Spring</category>
      <author>RoyceWon</author>
      <guid isPermaLink="true">https://curiosity-storage.tistory.com/16</guid>
      <comments>https://curiosity-storage.tistory.com/16#entry16comment</comments>
      <pubDate>Sat, 13 Apr 2024 09:03:12 +0900</pubDate>
    </item>
    <item>
      <title>Promtail, Loki를 사용한 Logback 모니터링</title>
      <link>https://curiosity-storage.tistory.com/15</link>
      <description>&lt;h1&gt;모니터링 시스템 구축기(2)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에 이어서 메트릭 지표 뿐만 아니라 logback으로 남기는 로그 파일을 모니터링 할 수 있도록 구성해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 셀럽잇에서는 log level에 나누어 다음과 같이 파일을 정리하고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;INFO&lt;/code&gt; -&amp;gt; &lt;code&gt;logs/backend/info&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WARN&lt;/code&gt; -&amp;gt; &lt;code&gt;logs/backend/warn&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ERROR&lt;/code&gt; -&amp;gt; &lt;code&gt;logs/backend/error&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레벨에 따라 각각 다른 폴더에 날짜별, 사이즈별로 분리되어 저장되고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt;# info.log

01:09:19.973 [http-nio-8080-exec-2] INFO  c.c.c.l.r.RequestLogInterceptor - [Web Request START] : [
#  ...

    [Method] = [GET]
    [URL] = [http://localhost:8080/api/celebs]
    [QueryString] = [null]
    [Params] = [

    ]
    [Body] = []
    [Controller Method] = [CelebController.findAll()]
]
01:09:19.982 [http-nio-8080-exec-2] INFO  com.celuveat.common.log.Logger                        - [f28a3f61(anonymous)]  |---&amp;gt;CelebController.findAll()                                                  
01:09:19.984 [http-nio-8080-exec-2] INFO  com.celuveat.common.log.Logger                        - [f28a3f61(anonymous)]  |    |---&amp;gt;CelebRepository.findAll()                                             
01:09:20.044 [http-nio-8080-exec-2] INFO  com.celuveat.common.log.Logger                        - [f28a3f61(anonymous)]  |    |&amp;lt;---CelebRepository.findAll()                                                time=74ms  
01:09:20.046 [http-nio-8080-exec-2] INFO  com.celuveat.common.log.Logger                        - [f28a3f61(anonymous)]  |&amp;lt;---CelebController.findAll()                                                     time=76ms  
01:09:20.077 [http-nio-8080-exec-2] INFO  c.c.c.l.r.RequestLogInterceptor                       - [Web Request END] : [
    [ID] = [f28a3f61(anonymous)]
    [Status] = [200]
    [Headers] = [
        [Vary] = [[Origin, Access-Control-Request-Method, Access-Control-Request-Headers]]
    ]
    [Body] = [
        [ ]
    ]
    [Query Count] = [1]
    [Total Time] = [107ms]
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Logback&lt;/code&gt;을 통해 유용한 정보들을 남기지만, 매번 서버에 직접 들어가서 확인해야 한다는 불편함이 있었습니다.&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;code&gt;Promtail&lt;/code&gt;과 &lt;code&gt;Loki&lt;/code&gt;를 도입하기로 하였습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Promtail과 Loki&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Promatil &lt;a href=&quot;https://grafana.com/docs/loki/latest/clients/promtail/&quot;&gt;공식문서&lt;/a&gt;에서는 다음과 같이 설명하고 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Promtail is an agent which ships the contents of &lt;b&gt;local logs to a private Grafana Loki instance&lt;/b&gt; or Grafana Cloud. It is usually deployed to every machine that has applications needed to be monitored.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;It primarily:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Discovers targets&lt;/li&gt;
&lt;li&gt;Attaches labels to log streams&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Pushes them to the Loki instance.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 추려보면, Promtail은 모니터링이 필요한 로컬 머신에서 &lt;code&gt;Loki instance&lt;/code&gt;에 로컬 로그를 푸시하는 agent라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Loki &lt;a href=&quot;https://grafana.com/docs/loki/latest/fundamentals/overview/&quot;&gt;공식문서&lt;/a&gt;에서는 다음과 같이 설명하네요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Grafana Loki is a log aggregation tool, and it is the core of a fully-featured logging stack.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로깅에 특화된 집계 도구로 이해하면 좋을 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/073c3659-39d7-4bc8-88d3-857cc7668106/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(&lt;a href=&quot;https://grafana.com/docs/loki/latest/fundamentals/overview/&quot;&gt;https://grafana.com/docs/loki/latest/fundamentals/overview/&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에서 제공하는 overview 입니다.&lt;br /&gt;Promtail과 같은 agent가 app에서 발생하는 로그들을 수집하여 HTTP API를 통해 &lt;code&gt;Loki&lt;/code&gt;에 전송하는 구조를 띄고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;code&gt;docker-compose-monitoring.yml&lt;/code&gt;에 구성하도록 하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# docker-compose-monitoring.yml

version: '3'

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - &quot;9090:9090&quot;
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    user: &quot;$UID:$GID&quot;
    ports:
      - &quot;3000:3000&quot;
    volumes:
      - ./grafana-data:/var/lib/grafana
    depends_on:
      - prometheus

# 추가됨
  loki:
    image: grafana/loki:latest
    ports:
      - &quot;3100:3100&quot;
    command: -config.file=/etc/loki/local-config.yaml

  promtail:
    image: grafana/promtail:latest
    volumes:
      - ./logs/backend:/logs
      - ./promtail-config.yml:/etc/promtail/config.yml
    command: -config.file=/etc/promtail/config.yml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &lt;code&gt;Loki&lt;/code&gt;에 대해 살펴보겠습니다. Loki 이미지를 다운 받고, 포트를 바인딩 하는건 별로 어색하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;command&lt;/code&gt; 옵션은 도커 컨테이너가 실행된 후 실행하는 명령을 정의할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-config.file=/etc/loki/local-config.yaml&lt;/code&gt;은 Loki 가 실행될 때 사용할 설정 파일을 지정하는 것 인데, 건들일 필요가 없어서 기본값으로 설정해두었습니다.&lt;/p&gt;
&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;promtail&lt;/code&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 접근해야할 &lt;code&gt;log&lt;/code&gt; 파일들이 저장되어있는 호스트의 &lt;code&gt;./logs/backend&lt;/code&gt;를 불륨 마운트 시켜줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 &lt;code&gt;promtail&lt;/code&gt;이 Log를 수집하는 job을 정의하는 &lt;code&gt;/etc/promtail/config.yml&lt;/code&gt;를 직접 정의할 수 있도록 이 역시도 마운트 시켜줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Loki와 마찬가지로 사용할 설절 파일을 기본값으로 설정하여 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼, Log를 수집하는 job을 &lt;code&gt;promtail-config.yml&lt;/code&gt;에서 정의해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;positions:
  filename: /tmp/positions.yaml # 동기화 작업을 이루기 위해 promtail이 읽은 마지막 로그 정보를 저장하는 곳

clients:
  - url: http://localhost:3100/loki/api/v1/push # push할 Loki의 주소

scrape_configs:
  - job_name: info
    static_configs:
      - targets:
          - localhost
        labels:
          job: info_logs
          __path__: ./logs/info/*.log # info 폴더 내에 log 파일들 모두 수집

  - job_name: warn
    static_configs:
      - targets:
          - localhost
        labels:
          job: warn_logs
          __path__: ./logs/warn/*.log # warn 폴더 내에 log 파일들 모두 수집

  - job_name: error
    static_configs:
      - targets:
          - localhost
        labels:
          job: error_logs
          __path__: ./logs/error/*.log # error 폴더 내에 log 파일들 모두 수집
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구성이 완료되면, docker을 다시 실행한 뒤 Grafana로 들어갑니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Loki Datasource를 추가해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/0c0d72ee-0825-4020-9344-b899676a6b23/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택을 한 뒤,&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/ba5fb7d1-46aa-4341-9173-7c5335224999/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Loki의 주소를 입력하여 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 새로운 대시보드를 작성합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/1108c924-9fba-4be7-add0-49531a6bd338/image.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/d93dc8bc-ab5c-4644-a163-046d9acf0168/image.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/a981db41-7b89-43c1-8830-d4a7c2b98329/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 job을 설정하면 해당 job이 수집한 로그들을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/f638e4c2-81be-41b8-ba85-b6035244c2ff/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;log는 기본적으로 한 줄 단위로 분리되어 작성되는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 celuveat의 로깅 방식에 맞게 &lt;code&gt;multiline&lt;/code&gt; 옵션을 추가하여 한 블럭의 단위를 조정해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;tex&quot;&gt;&lt;code&gt;pipeline_stages:
- multiline:
    firstline: '^\d{2}:\d{2}:\d{2}\.\d{3} \[.*\] INFO  .* \[Web Request (START|END)\]'
    max_wait_time: 1s
    max_lines: 500&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블럭의 단위가 [Web Request START/END]를 기준으로 나눠 볼 수 있게 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/be9c08d0-7d8b-4813-a5d7-7b437f5601c7/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, error 로그의 경우는 다음과 같이 추가하였습니다.&lt;/p&gt;
&lt;pre class=&quot;tex&quot;&gt;&lt;code&gt;pipeline_stages:
      - multiline:
          firstline: '^\d{2}:\d{2}:\d{2}\.\d{3} \[.*\] ERROR'
          max_wait_time: 1s
          max_lines: 500&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/77eaa59c-4bff-44db-ae5d-bf5cc5114210/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 형태&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# promtail-confing.yml

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://localhost:3100/loki/api/v1/push

scrape_configs:
  - job_name: info
    static_configs:
      - targets:
          - localhost
        labels:
          job: info_logs
          __path__: ./logs/info/*.log
    pipeline_stages:
      - multiline:
          firstline: '^\d{2}:\d{2}:\d{2}\.\d{3} \[.*\] INFO  .* \[Web Request (START|END)\]'
          max_wait_time: 1s
          max_lines: 500

  - job_name: warn
    static_configs:
      - targets:
          - localhost
        labels:
          job: warn_logs
          __path__: ./logs/warn/*.log

  - job_name: error
    static_configs:
      - targets:
          - localhost
        labels:
          job: error_logs
          __path__: ./logs/error/*.log
    pipeline_stages:
      - multiline:
          firstline: '^\d{2}:\d{2}:\d{2}\.\d{3} \[.*\] ERROR'
          max_wait_time: 1s
          max_lines: 500&lt;/code&gt;&lt;/pre&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;Docker를 사용하여 비교적 간단하게 설치하여 구성할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 직접 설치해도 되지만, 현재 인스턴스 상황에 따라 모니터링 서버를 어디에 구축하여야 하는 지 확정이 되지 않아 빠르게 이식할 수 있도록 구성해보게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 다음 포스팅이에서 도커 네트워크를 통해서 비교적 안전하게 내부 네트워크를 통해 meteric정보들을 노출 시키지 않고 모니터링이 가능하도록 구성할 예정입니다.&lt;/p&gt;</description>
      <category>인프라</category>
      <category>logback</category>
      <category>Loki</category>
      <category>Monitoring</category>
      <category>Promtail</category>
      <category>Spring</category>
      <author>RoyceWon</author>
      <guid isPermaLink="true">https://curiosity-storage.tistory.com/15</guid>
      <comments>https://curiosity-storage.tistory.com/15#entry15comment</comments>
      <pubDate>Sat, 13 Apr 2024 09:02:32 +0900</pubDate>
    </item>
    <item>
      <title>Spring boot 모니터링(Prometheus, Grafana, docker)</title>
      <link>https://curiosity-storage.tistory.com/14</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;모니터링 시스템 구축기(1)&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;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;/li&gt;
&lt;li&gt;사용자의 액세스와 활동 분석 -&amp;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;그래서 저희 셀럽잇도 운영중인 서비스에도 모니터링 시스템을 구축하고자 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring actuator&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 셀럽잇에서는 logback를 사용하여 운영중에 발생한 로그를 모으고 있습니다.&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;code&gt;Spring Boot Actuator&lt;/code&gt;를 사용하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/2.1.8.RELEASE/reference/html/production-ready.html&quot;&gt;Spring Boot Actuator&lt;/a&gt;는 운영 중인 애플리케이션을 &lt;code&gt;HTTP&lt;/code&gt;나 &lt;code&gt;JMX&lt;/code&gt;를 이용해서 모니터링하고 관리할 수 있는 기능을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;*JMX&lt;/b&gt;(&lt;b&gt;J&lt;/b&gt;ava &lt;b&gt;M&lt;/b&gt;anagement e&lt;b&gt;X&lt;/b&gt;tension): 애플리케이션, 시스템, JVM 등을 모니터링하고 관리하기 위한 도구를 제공하는 Java 기술. JMX라는 표준화된 방법을 통해 다양한 모니터링 도구 및 시스템과 쉽게 통합할 수 있다.*&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Actuator&lt;/code&gt;를 통해서 간단한 설정들로 다양한 지표들을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Dependency에 추가하여 활성화 해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;build.gradle&lt;/code&gt;에 아래 코드를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-actuator'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, &lt;code&gt;http://{server}:{port}/actuator&lt;/code&gt; 에 접속하시면 아래와 같은 화면을 볼 수 있습니다.&lt;br /&gt;(celuveat은 8080 servlet의 기본 주소가 /api여서 &lt;a href=&quot;http://localhost:8080/api/actuator&quot;&gt;http://localhost:8080/api/actuator&lt;/a&gt; 로 접속하였습니다.)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/22586a21-f559-4fee-a0ac-4c6d9b8e25a5/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;_links&lt;/code&gt; 하위에 &lt;code&gt;self&lt;/code&gt;, &lt;code&gt;health&lt;/code&gt; 등의 url들이 제공되는데요. Actuator는 이 url들을 통해 기능을 제공합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/5b7994f9-b8f0-48c8-b050-0ccdce385d84/image.png&quot; alt=&quot;&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;code&gt;application.yml&lt;/code&gt; 파일에서 아래와 같이 설정한 뒤, 다시 &lt;code&gt;actuator&lt;/code&gt; url로 이동해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;management:
  endpoints:
    web:
      exposure:
        include: &quot;*&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/afb9f102-8686-44bb-b212-8195b08f9a29/image.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/ba11f0be-f0fd-4a07-90c5-edda3644f51b/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;management:
  endpoints:
    web:
      exposure:
        include: &quot;*&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 통해 web 환경(HTTP) 에서 모든(*) endpoints들을 노출 시켰기 때문에 다양한 actuator 기능을 사용할 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;more endpoints: &lt;a href=&quot;https://docs.spring.io/spring-boot/docs/3.0.5/reference/html/actuator.html#actuator.endpoints&quot;&gt;https://docs.spring.io/spring-boot/docs/3.0.5/reference/html/actuator.html#actuator.endpoints&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중, &lt;code&gt;/metrics&lt;/code&gt; endpoints를 살펴보면 유용한 지표(metric)들을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/34aecdbe-9a92-4174-ba02-d6ddbddce2a2/image.png&quot; alt=&quot;&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;code&gt;application.ready.time&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jvm.threads.live&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jvm.gc.memory.allocated&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;logback.events&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;process.cpu.usage&lt;/code&gt;&lt;/li&gt;
&lt;li&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;예시로, &lt;code&gt;process.cpu.usage&lt;/code&gt; 를 확인하고자 한다면, endpoints에 추가하여 접근 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/api/actuator/metrics/process.cpu.usage&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/91838cd4-b4fd-4b1e-b329-76acbb1956fa/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Spring Actuator&lt;/code&gt;는 단순한 매트릭 및 헬스 체킹 뿐만 아니라&lt;br /&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/3.0.5/reference/html/actuator.html#actuator.endpoints.info&quot;&gt;실행된 git 브랜치와 커밋, AutoConfiguration 정보, Java, OS 등의 정보&lt;/a&gt;,&lt;br /&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/3.0.5/reference/html/actuator.html#actuator.loggers&quot;&gt;런타임 중 Logging 레벨 변경&lt;/a&gt; 역시 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할 점은, application에 대한 다양한 정보가 제공되기 때문에 보안적으로 심각한 문제를 야기 할 수 있습니다. 이부분을 해결하는 내용은 추후에 작성하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Spring actuator&lt;/code&gt;로 간단하게 의존성을 추가하고 약간의 설정으로 필요한 메트릭들에 대해 측정하고 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 &lt;code&gt;Spring actuator&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;Prometheus&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Spring Actuator&lt;/code&gt; 공식 문서의 &lt;a href=&quot;https://docs.spring.io/spring-boot/docs/3.0.5/reference/html/actuator.html#actuator.metrics&quot;&gt;Metrics 챕터&lt;/a&gt;를 보면 &lt;code&gt;Micrometer&lt;/code&gt;에 대한 종속성 관리 혹은 자동 구성을 제공한다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Micrometer에 대해 간단하게 짚고 넘어가겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://micrometer.io/docs/concepts&quot;&gt;Micrometer&lt;/a&gt;&lt;/b&gt;: Micrometer는 JVM 기반 애플리케이션을 위한 메트릭 측정 라이브러리입니다. 모니터링 시스템용 클라이언트에 대해 추상화된 파사드를 제공하여 벤더(vender)사에 종속되지 않고 JVM 기반 애플리케이션 코드를 계측할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 간단하게 말하면, JVM에서 메트릭을 측정할 수 있는 일종의 인터페이스라고 이해하면 좋을 것 같습니다.(``Log&lt;code&gt;의&lt;/code&gt;Slf4j`와 비슷한 역할)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Micrometer&lt;/code&gt;를 제공하는 벤더사는 다양하게 있는데요. 대표적인 것들은 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Atlas&lt;/li&gt;
&lt;li&gt;Datadog&lt;/li&gt;
&lt;li&gt;Elastic&lt;/li&gt;
&lt;li&gt;Graphite&lt;/li&gt;
&lt;li&gt;Influx&lt;/li&gt;
&lt;li&gt;JMX&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Prometheus&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중 Prometheus를 사용하여 micrometer를 수집하고 집계하고자 합니다.&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;Apache License 2.0가 사용된 무료 소프트웨어이다.&lt;/li&gt;
&lt;li&gt;PromQL을 통해 효율적으로 다양한 시계열 선택하고 집계할 수 있다.&lt;/li&gt;
&lt;li&gt;그라파나에서 시각화를 제공한다.&lt;/li&gt;
&lt;li&gt;사용자가 많아 참고할 자료가 다양한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Spring Actuator&lt;/code&gt;에서 메트릭 정보를 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 &lt;code&gt;Prometheus&lt;/code&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;code&gt;Prometheus&lt;/code&gt;가 metric을 폴링 할 수 있도록 &lt;code&gt;/actuator/prometheus&lt;/code&gt; 엔드포인트를 제공해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 build.gradle에 &lt;code&gt;prometheus&lt;/code&gt; 의존성을 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;build.gradle&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'io.micrometer:micrometer-registry-prometheus'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, &lt;code&gt;application.yml&lt;/code&gt;에서 &lt;code&gt;exposure&lt;/code&gt; 설정을 변경합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;application.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;management:
  endpoints:
    web:
      exposure:
        include: &quot;prometheus&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 적용이 되었는지 &lt;code&gt;/actuator/prometheus&lt;/code&gt; 로 접속해 확인해 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/5287d4b3-7e96-4fb8-a250-4ab4771142ff/image.png&quot; alt=&quot;&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;url과 태그로 제공되는 방식과 달리, &lt;code&gt;executor_pool_max_threads{name=&quot;applicationTaskExecutor&quot;,} 2.147483647&lt;/code&gt; 와 같은 문법으로 제공되는데, 이는 PromQL(Prometheus Query)으로 &lt;code&gt;Prometheus&lt;/code&gt;의 메트릭 질의 언어입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus을 통해 meterics를 polling하는데 필요한 준비는 끝났습니다!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, celuveat에서는 docker를 사용하여 &lt;code&gt;Spring application&lt;/code&gt;을 운영중에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;prometheus&lt;/code&gt; 역시 docker를 통해 관리하도록 하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# docker-compose-monitoring.yml

version: '3'

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - &quot;9090:9090&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로, &lt;code&gt;promtheuse&lt;/code&gt;는 9090 포트를 사용하기에 9090 포트를 바인딩 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;volumes는 추후 설명하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker compose -f docker-compose-monitoring.yml up -d&lt;/code&gt;를 통해 실행한 뒤, 9090포트에 접근하면 대시보드를 확인 하실 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/bfbd9801-7389-4ba9-b28e-31a933e06332/image.png&quot; alt=&quot;&quot; /&gt;&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;code&gt;Status&lt;/code&gt; -&amp;gt; &lt;code&gt;Targets&lt;/code&gt;를 통해 prometheus가 polling 중인 타겟을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/338924c1-94f9-4307-8678-a83f84c8ea3c/image.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/6bdbc3f5-5b33-49e2-8fb6-37ecda6e343e/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 아무런 설정을 하지 않았기에 &lt;code&gt;self&lt;/code&gt;로 metrics를 &lt;code&gt;Scrape&lt;/code&gt; 하는 것을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 타겟 설정은 &lt;code&gt;prometheus.yml&lt;/code&gt;에서 수정할 수 있습니다. 현재 저는 도커 컨테이너 내에서 동작중이다 보니, 외부에 &lt;code&gt;prometheus.yml&lt;/code&gt;파일을 작성 한 뒤, 볼륨 마운트를 통해 설정하도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;prometheus.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# prometheus.yml

scrape_configs:
  - job_name: 'celuveatdev'
    metrics_path: '/api/actuator/prometheus'
    scrape_interval: 5s
    static_configs:
      - targets: ['localhost:8080']&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;metrics_path&lt;/code&gt; : metric 경로 지정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;static_configs: - targets&lt;/code&gt;에는 host를 지정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;scrape_interval&lt;/code&gt;: 수집 주기를 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 &lt;code&gt;docker-compose-monitoring.yml&lt;/code&gt; 을 수정하여 볼륨 마운트 설정을 합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# docker-compose-monitoring.yml

version: '3'

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - &quot;9090:9090&quot;
    volumes:
       - ./prometheus.yml:/etc/prometheus/prometheus.yml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``./prometheus.yml&lt;code&gt;는 host에서의 경로,&lt;/code&gt;/etc/prometheus/prometheus.yml`는 docker container내부의 경로입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 다시 docker를 실행하면 원하는 타겟이 설정된 것을 확인 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/43dc3f80-a6ba-40b7-b555-64da1e6dc71e/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시로 들었던 PrmoQL인 &lt;code&gt;executor_pool_max_threads{name=&quot;applicationTaskExecutor&quot;,}&lt;/code&gt;을 메인 페이지에서 실행하면&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/a586fe9e-cc3f-43bd-82bf-78fa18b7a7f4/image.png&quot; alt=&quot;&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;더 다양한 PromQL을 통해 다양한 지표들을 Table 뿐만 아니라 &lt;code&gt;Graph&lt;/code&gt;로도 확인 하실 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Grafana&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Prometheus&lt;/code&gt;를 통해 주기적으로 메트릭을 수집 할 수 있지만, 확인하기엔 어려움이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 PromQL을 통해 원하는 지표를 질의해야 하고, 시각화된 자료 또한 그래프 하나가 전부이기 때문에 불편함이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;Prometheus&lt;/code&gt;에서 수집한 메트릭을 유용하게 시각화 할 수 있는 시각화 도구로서 &lt;code&gt;Grafana&lt;/code&gt;를 사용하려 합니다.&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;code&gt;Grafana&lt;/code&gt; 역시 도커로 구성하고자 합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# docker-compose-monitoring.yml

version: '3'

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - &quot;9090:9090&quot;
    volumes:
       - ./prometheus.yml:/etc/prometheus/prometheus.yml

# 추가
    grafana:
    image: grafana/grafana:latest
    container_name: grafana
    user: &quot;$UID:$GID&quot;
    ports:
      - &quot;3000:3000&quot;
    volumes:
      - ./grafana-data:/var/lib/grafana
    depends_on:
      - prometheus&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;grafana는 기본적으로 3000포트를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, prometheus를 datasource로 추가할 예정이다 보니, depends_on 옵션으로 prometheus 컨테이너가 선행되도록 설정하였습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;volumes를 꼭 설정하셔야 합니다!&lt;/b&gt; 도커 컨테이너가 내려가도 Grafana에서 설정한 대시보드나 데이터 소스가 사라지지 않도록 해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, grafana image내 &lt;code&gt;/var/lib/grafana&lt;/code&gt;의 쓰기 작업을 위해 &lt;code&gt;user: &quot;$UID:$GID&quot;&lt;/code&gt;를 설정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저도 알고 싶지 않았습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker compose -f docker-compose-monitoring.yml up -d&lt;/code&gt; 를 통해 실행 한 뒤,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3000포트로 접속하면 다음과 같은 화면이 나옵니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/6dfa0c3c-d4a2-42eb-8624-429403c0dfa1/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 계정과 비밀번호는 모두 &quot;admin&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;로그인 후 Administration -&amp;gt; Data soureces에 들어와서 prometheus를 등록합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/4992f6b5-e4e0-4d46-81f4-79c193bd1fb7/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 prometheus를 접속했던 url을 설정해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/97382e1f-f602-4893-9124-55ad44791198/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Data sources 에서 추가된 걸 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;a href=&quot;https://grafana.com/grafana/dashboards&quot;&gt;Grafana Labs&lt;/a&gt;에 가서 다양한 Dashboards를 탐색하고 선택하여 적용하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Spring boot&lt;/code&gt;로 검색한 뒤, &lt;code&gt;Spring boot 2.1 System Monitor&lt;/code&gt;를 선택합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/ae725ba6-f1f5-4862-aff6-14481c82eb60/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 대쉬보드의 ID를 복사 한 뒤,&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/3702c882-d12d-4da6-b931-389016b5f85e/image.png&quot; alt=&quot;&quot; /&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;그 후 복사한 ID를 입력하면,&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/e7ad24d4-a3da-40f5-88e7-b2d9efc87e64/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 2.1 System Monitor 가 불러와지는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/053a7e1a-a60a-449a-a80c-4438d303b0d2/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataSource도 등록하여 추가하면 아래와 같이 대쉬보드를 확인 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/e8023479-4923-426a-8581-df3d36979571/image.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/cebcd28b-8539-4cbe-b2df-a6bb938deb48/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 메트릭을 확인하다 보면 &lt;code&gt;Jetty Statistics&lt;/code&gt;를 확인할 수 있는데요.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/262d6c19-c0ad-4514-b40b-c905984ef783/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring boot 에서 사용하고 있는 tomcat was 메트릭으로 변경이 필요해 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 &lt;code&gt;application&lt;/code&gt;으로 돌아와서 &lt;code&gt;application.yml&lt;/code&gt;에 아래 옵션을 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# application.yml
server:
  tomcat:
    mbeanregistry:
      enabled: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Dashboard settings에서 수정할 수 있도록 설정을 합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/3538f8c9-4856-4915-a231-082f1b285aa4/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Query의 이름을 &lt;code&gt;tomcat_threads_config_max_threads&lt;/code&gt;로 변경합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/195c3f93-8dc3-4f4e-9004-85b507d31c05/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/b2118596-82e0-4cac-899b-0dc536fb7d1d/image.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/99a0a8d1-d12e-45e6-9378-1fcdd699618a/image.png&quot; alt=&quot;&quot; /&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;&lt;code&gt;Sping actuator&lt;/code&gt;에서 제공하는 metrics를 &lt;code&gt;Micrometer&lt;/code&gt; 를 구현한 prometheus에게 제공하고, prometheus는 주기적으로 지표들을 모아 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 prometheus에 저장되어 있는 데이터를 시각화 툴인 &lt;code&gt;Grafana&lt;/code&gt;를 사용하여 대시보드를 구성하여 모니터링 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 아래와 같은 구성이 갖추어 집니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/6587d618-293b-442a-baf2-e225d0b3b2a8/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;</description>
      <category>인프라</category>
      <category>Grafana</category>
      <category>Monitoring</category>
      <category>Prometheus</category>
      <category>Spring</category>
      <author>RoyceWon</author>
      <guid isPermaLink="true">https://curiosity-storage.tistory.com/14</guid>
      <comments>https://curiosity-storage.tistory.com/14#entry14comment</comments>
      <pubDate>Sat, 13 Apr 2024 09:00:13 +0900</pubDate>
    </item>
    <item>
      <title>Intellij를 통한 Java 원격 디버깅(Remote Debugging)</title>
      <link>https://curiosity-storage.tistory.com/13</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;디버깅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서 예상치 못한 오류가 발생하면, 디버깅 모드를 활용하여 해당 오류를 해결합니다.&lt;br /&gt;플로우를 순차적으로 따라가면서 각각의 상태와 상황들을 쉽게 파악할 수 있기 때문이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 개발자와 협업을 하기 위해선, 작성한 어플리케이션을 배포하여야 합니다.&lt;br /&gt;배포된 어플리케이션에서 발생한 오류는 어떻게 파악하면 좋을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론, 로그를 꼼꼼하게 기록한다면 파악하기 쉬울 것 같습니다. 하지만 한계는 있죠.&lt;br /&gt;배포되어있는 어플리케이션을 대상으로 원하는 지점에서 디버깅을 한다면, 좀 더 쉽게 파악할 수 있을 것 같습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Debugger, Debuggee&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Intellij&lt;/code&gt;나 &lt;code&gt;Ecplise&lt;/code&gt;와 같은 IDE(Integrated development environment, 통합 개발 환경)에서 제공해주는 디버깅 기능을 사용해본 경험이 있을겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Debugging 모드로 실행하는 것은 어떤 의미인지 간단하게만 짚고 넘어갑시다.&lt;br /&gt;&lt;code&gt;Intellij&lt;/code&gt; 같은 &lt;code&gt;IDE&lt;/code&gt;에서 디버깅 모드로 실행을 하면,&lt;br /&gt;해당 실행 어플리케이션을 Debugee(디버그 대상)으로 실행하고, Debugger(디버거)로서의 역할을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;JPDA(Java Platform Debugger Architecture)&lt;/code&gt; 라는 구조에서 동작하게 된다고 하고,&lt;br /&gt;그리고 이 &lt;code&gt;JPDA&lt;/code&gt;는 &lt;code&gt;JVM TI&lt;/code&gt;, &lt;code&gt;JDI&lt;/code&gt;라는 두개의 인터페이스와 &lt;code&gt;JDWP&lt;/code&gt;라는 프로토콜로 구성되어 있다고 합니다.&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;JWM TI(&lt;b&gt;JVM&lt;/b&gt; &lt;b&gt;T&lt;/b&gt;ools &lt;b&gt;I&lt;/b&gt;nterface): 디버깅이 진행중인 어플리케이션을 실행하는 VM&lt;/li&gt;
&lt;li&gt;JDI(&lt;b&gt;J&lt;/b&gt;ava &lt;b&gt;D&lt;/b&gt;ebug &lt;b&gt;I&lt;/b&gt;nterface): 애플리케이션이 디버깅되는 동안 &lt;code&gt;IntelliJ IDEA&lt;/code&gt;가 애플리케이션과 상호 작용할 수 있도록 하는 Java API. (Ex. 코드를 단계별로 진행, breakpoint 생성, 변수를 검사 등의 작업은 JDI 호출로 변환)&lt;/li&gt;
&lt;li&gt;JDWP(&lt;b&gt;J&lt;/b&gt;ava &lt;b&gt;D&lt;/b&gt;ebugging &lt;b&gt;W&lt;/b&gt;ire &lt;b&gt;P&lt;/b&gt;rotocol): JDWP는 디버깅 중인 프로세스(디버거)와 디버거(이 경우 IntelliJ IDEA) 간에 전송되는 정보 및 요청의 형식의 프로토콜.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 내용은 크게 중요하지 않습니다.&lt;br /&gt;잘 알지도 못하고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짚고 넘어갈 점은, &lt;code&gt;Java Application&lt;/code&gt;을 디버깅 모드로 실행하면 &lt;code&gt;[Debuggee]&lt;/code&gt;와 &lt;code&gt;[Debugger]&lt;/code&gt;가 따로 존재하고, 특정 JVM 기준이나 구조를 구현한 기능들을 사용한다는 점 입니다.&lt;br /&gt;그리고, 이 &lt;code&gt;[Debuggee]&lt;/code&gt;와 &lt;code&gt;[Debugger]&lt;/code&gt; 사이에 &lt;code&gt;JDWP&lt;/code&gt;라는 프로토콜을 통해 통신을 한다는 점 입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 구조&lt;/p&gt;
&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/843ce355-406a-470e-bf72-86f17e3ed580/image.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;출처: &lt;a href=&quot;https://docs.oracle.com/javase/8/docs/technotes/guides/jpda/architecture.html&quot;&gt;https://docs.oracle.com/javase/8/docs/technotes/guides/jpda/architecture.html&lt;/a&gt;&lt;/p&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;Remote Debugging&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 서버에서 실행중인 어플리케이션을 &lt;code&gt;Debuggee&lt;/code&gt;, 이를 디버깅 하는 &lt;code&gt;Debugger&lt;/code&gt;로 &lt;code&gt;intellij&lt;/code&gt;를 사용하면 원격으로 디버깅이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방식은 간단합니다.&lt;br /&gt;Server에 배포하는 Java Application을 실행할 때 다음과 같은 매개변수를 설정합니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8081 -jar application.jar&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;&lt;code&gt;agentlib:jdwp=transport=dt_socket&lt;/code&gt; : JVM에 JDWP를 에이전트로 등록하여 디버거가 JVM에 소켓 방식으로 연결할 수 있도록 설정합니다. 실행중인 두 어플리케이션을 연결할 때, 소켓이 표준으로 사용됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;server:y&lt;/code&gt;: 서버 역할을 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;suspend=n&lt;/code&gt;: 디버거를 기다리지 않고, 즉시 프로그램을 실행합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;address=*:8081&lt;/code&gt;: 8081 포트에 디버거가 연결될 수 있도록 Listening 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 옵션을 추가하여 Java 기반의 &lt;code&gt;Spring Application&lt;/code&gt;을 실행합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/60a205e8-910e-4b58-b0b9-1110c368f941/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;Debugee&lt;/code&gt;로 실행된 Server에 &lt;code&gt;Debugger&lt;/code&gt;를 연결해봅시다.&lt;br /&gt;&lt;code&gt;Intellij&lt;/code&gt;에서 다음과 같은 실행 환경을 추가합니다.&lt;/p&gt;
&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/2d26dde7-ef2d-4d98-90a1-fae2cb908baf/image.png&quot; alt=&quot;&quot; /&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Attach to remote JVM&lt;/code&gt;을 선택합니다.&lt;/p&gt;
&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/b2e21a2b-652e-4389-b433-827e8be198a9/image.png&quot; alt=&quot;&quot; /&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포된 서버의 endpoint를 추가하고, 서버를 실행할 때 지정한 Listening 포트를 설정합니다.&lt;/p&gt;
&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/bf643f28-1fc3-4e84-acef-91fba3466ec5/image.png&quot; alt=&quot;&quot; /&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 Breakpoint를 지정하고 디버깅을 실행하면, 해당 서버에서 발생한 요청에 대한 디버깅이 가능해집니다.&lt;/p&gt;
&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/54ca4f12-3815-4388-bc47-f838876d8522/image.png&quot; alt=&quot;&quot; /&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주의점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디버깅 모드를 사용중인 경우, 해당 프로세스를 붙잡고 있습니다.&lt;br /&gt;실제 사용자들이 사용하는 프로덕션 코드에서 개방해두면, 불필요한 병목 현상을 초래할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 보안적인 이슈가 존재합니다. 어떤 인증 Key 없이, Debugee의 url과 개방된 포트만 알고 있다면 실행중인 어플리케이션에 쉽게 접근하고 다양한 조작을 할 수 있기 때문입니다.&lt;br /&gt;프론트와 협력을 하는 상황에서 쉽게 재연하고 발생한 오류를 파악할 때 유용하게 사용될 것 같습니다.&lt;/p&gt;</description>
      <category>Java</category>
      <category>Debugging</category>
      <category>IntelliJ</category>
      <category>Java</category>
      <author>RoyceWon</author>
      <guid isPermaLink="true">https://curiosity-storage.tistory.com/13</guid>
      <comments>https://curiosity-storage.tistory.com/13#entry13comment</comments>
      <pubDate>Sat, 13 Apr 2024 01:30:14 +0900</pubDate>
    </item>
    <item>
      <title>Java의 BufferedReader, 알고 쓰자</title>
      <link>https://curiosity-storage.tistory.com/12</link>
      <description>&lt;h1&gt;Buffered Reader와 Scanner&lt;/h1&gt;
&lt;p&gt;Java를 처음 시작하면서 입력을 받을 땐 항상 &lt;code&gt;Scanner&lt;/code&gt;를 사용했다.&lt;/p&gt;
&lt;p&gt;만약 정수를 하나 입력받는 다고 하면 다음과 같이 작성할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Scanner sc = new Scanner(System.in);
int i = sc.nextInt();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; &lt;code&gt;Scanner&lt;/code&gt; 클래스가 제공하는 여러 메서드들을 통해 정수 입력, 줄 단위 입력, 문자열 입력 등등 원하는 동작을 수행할 수 있다.&lt;/p&gt;
&lt;h3&gt;의문점&lt;/h3&gt;
&lt;p&gt;사실, &lt;code&gt;Scanner&lt;/code&gt;만 사용했다면 궁금해하지도 않았을 의문점이 있다.&lt;br&gt;Java로 알고리즘 문제를 풀 때, 평소처럼 Scanner를 통해 입력을 받으면 시간초과가 나는 Case를 자주 격어보았다. 메인 로직은 모두 동일하고, 입출력의 방식만 달랐을 뿐인데.. 졸지에 틀린 풀이가 되었다는 것이다.&lt;/p&gt;
&lt;p&gt;이런 문제는 자주 발견되었고, 이를 해결하기 위해 &lt;code&gt;BufferedReader&lt;/code&gt;, &lt;code&gt;BufferedWriter&lt;/code&gt;를 사용해야하는 것을 어렵지 않게 찾아볼 수 있다.&lt;/p&gt;
&lt;p&gt;알고리즘을 중점에 두고 공부를 하다보니 어떻게 다른지 공부해보진 않았다. 매번 모르고 단순 사용하는 것이 마음에 걸려 찾아보았다.&lt;/p&gt;
&lt;h2&gt;버퍼(Buffer)&lt;/h2&gt;
&lt;p&gt;우선 &lt;code&gt;Buffer&lt;/code&gt;에 대해서 알고 넘어가야 할 것이다.&lt;/p&gt;
&lt;p&gt;간단하게 말하자면... 어떤 데이터가 전송 될 때, 일시적으로 저장되는 메모리 영역이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;In computer science, a &lt;strong&gt;data buffer&lt;/strong&gt; (or just &lt;strong&gt;buffer&lt;/strong&gt;) is a region of a memory used to temporarily store data while it is being moved from one place to another&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Data_buffer&quot;&gt;https://en.wikipedia.org/wiki/Data_buffer&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;버퍼를 지하철 플랫폼에 비교해볼 수 있다.&lt;br&gt;이동하고 싶은 많은 사람들이 가변적으로 들어와 플랫폼에 대기하고,&lt;br&gt;지하철은 이 사람들을 순식간에 태워 원하는 곳으로 한번에 이동한는 점이다.&lt;/p&gt;
&lt;p&gt;중요한 건, 매번 사람들이 들어올때마다 이동하는 것이 아니라 어느정도 사람이 모이고, 지하철이 와야 이동이 가능하다는 점이다.&lt;/p&gt;
&lt;p&gt;버퍼가 존재하면, 입출력을 OS가 직접 처리하며 발생되는 비효율적인 업무가 줄어든다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Without buffer I/O means each read or write request is handled directly by the underlying OS. This can make a program much less efficient, since each such request often triggers disk access, network activity, or some other operation that is relatively expensive.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/questions/22436289/when-to-use-buffer-and-what-for&quot;&gt;https://stackoverflow.com/questions/22436289/when-to-use-buffer-and-what-for&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이러한 버퍼를 써서 얻을 수 있는 장점은 2개정도 있는데,&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;문자를 묶어서 한 번에 전달하므로 전송시간이 적게 걸려 성능이 향상된다&lt;/li&gt;
&lt;li&gt;사용자가 문자를 잘못 입력했을 경우 수정할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이런 특징이 있기에 입력 작업에서 버퍼를 사용하는 것이 대체적으로 유리하다는 것을 짐작할 수 있다.&lt;/p&gt;
&lt;h2&gt;BufferedReader&lt;/h2&gt;
&lt;p&gt;BufferedReader로 정수 하나를 입력받는 다고 하면,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int userInputNumber = Integer.parseInt(br.readLine());&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;와 같은 작업이 수행된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BufferedReader&lt;/code&gt;말고 &lt;code&gt;InputStreamReader&lt;/code&gt;라는 게 존재한다.&lt;/p&gt;
&lt;h3&gt;Stream&lt;/h3&gt;
&lt;p&gt;설명을 위해 간단하고 짚고 넘어가보자. 여기서 스트림은 파일을 읽거나 쓸 때, 데이터가 전송되는 통로라고 생각하면 좋을 것 같다.&lt;/p&gt;
&lt;p&gt;Java에선 &lt;code&gt;노드 스트림&lt;/code&gt;과 &lt;code&gt;보조스트림&lt;/code&gt;이 있다고 이해해보자.&lt;br&gt;&lt;code&gt;노드 스트림&lt;/code&gt;은 스트림의 주축을,&lt;br&gt;&lt;code&gt;보조 스트림&lt;/code&gt;은 이 노드 스트림을 통해 들어오거나 나가는 데이터를 처리하는 것을 도와주는 보조의 역할을 하는 스트림이다.&lt;/p&gt;
&lt;p&gt;짚고 넘어가야하는 건 &lt;code&gt;BufferedReader&lt;/code&gt;는 보조 스트림이라는 점이다.&lt;/p&gt;
&lt;p&gt;스트림은 데이터 타입 ( &lt;code&gt;Char&lt;/code&gt; 또는 &lt;code&gt;Byte&lt;/code&gt; ), 방향 ( &lt;code&gt;Input&lt;/code&gt; 또는 &lt;code&gt;Output&lt;/code&gt; )에 따라 결정된다. 그리고 FileReader, BufferedReader, InputStreamReader는 &lt;strong&gt;Char&lt;/strong&gt;로 반환된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;주로 사용하는 &lt;code&gt;System.in&lt;/code&gt;은 시스템에서 설정된 입력 스트림을 따르겠다는 뜻. Stream, Redaer, File입력이 될 수 도 있다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;다시 BufferedReader&lt;/h3&gt;
&lt;p&gt;감싸지는 구조를 살펴보면 &lt;/p&gt;
&lt;p&gt;BufferedReader {&lt;br&gt;    InputStreamReader {&lt;br&gt;        InputStream 혹은 System.in&lt;br&gt;    }&lt;br&gt;}&lt;/p&gt;
&lt;p&gt;이런 구조이다. &lt;/p&gt;
&lt;p&gt;정리하면, Byte타입으로 전송되는 데이터를 Char형으로 변환하고, 버퍼링을 적용하여 Char 데이터를 반환해주는 것이다.&lt;/p&gt;
&lt;h2&gt;Scanner와의 차이&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Scanner&lt;/code&gt;가 느린 이유는, 입력을 읽는 과정에서 정규 표현식을 적용하고, 입력값 분할, 파싱 과정을 스스로 제공해주기 때문이다.&lt;br&gt;실제로, &lt;code&gt;Scanner&lt;/code&gt;에서 제공하는 &lt;code&gt;.nextInt()&lt;/code&gt;, &lt;code&gt;.nextDouble()&lt;/code&gt; 메서드는 잘못 입력하면 입력 단계에서부터 예외가 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BufferedReader&lt;/code&gt;는 모든 입력을 Char형으로, 버퍼를 사용하여 받는다.&lt;br&gt;하나의 글자에 대해 전달이 이루어지는 것이 아닌, 전체 입력(혹은 버퍼 단위)에 대해서만 전달되기 때문에 속도 부분에서 매우 유리할 수 밖에 없다.&lt;/p&gt;
&lt;p&gt;그래서 속도는 빠르지만, 사용자가 사용하기 편하게 조작하려면 별도의 메서드를 호출해야한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images%2Fkkimbj18%2Fpost%2Fe62c2948-4866-4d3b-aa94-f2d1c1ab1452%2Fimage.png&quot; alt=&quot;img&quot;&gt; &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://t1.daumcdn.net/cfile/tistory/998191335BBB7A6030&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;Scanner와 BufferedReader의 수행시간 차이&lt;br&gt;출처 : &lt;a href=&quot;https://algospot.com/forum/read/2496/&quot;&gt;https://algospot.com/forum/read/2496/&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;참고 글:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://velog.io/@kkimbj18/%EB%B2%84%ED%8D%BC%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-Feat.-BufferedReader-vs-Scanner-cache&quot;&gt;https://velog.io/@kkimbj18/%EB%B2%84%ED%8D%BC%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-Feat.-BufferedReader-vs-Scanner-cache&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://onlyfor-me-blog.tistory.com/368&quot;&gt;https://onlyfor-me-blog.tistory.com/368&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/javase/7/docs/api/java/util/Scanner.html&quot;&gt;https://docs.oracle.com/javase/7/docs/api/java/util/Scanner.html&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;</description>
      <category>Java</category>
      <author>RoyceWon</author>
      <guid isPermaLink="true">https://curiosity-storage.tistory.com/12</guid>
      <comments>https://curiosity-storage.tistory.com/12#entry12comment</comments>
      <pubDate>Fri, 3 Nov 2023 00:37:38 +0900</pubDate>
    </item>
    <item>
      <title>S3 버킷 정책 설정하여 안전하게 사용하기(CloudFront, S3, ACM)</title>
      <link>https://curiosity-storage.tistory.com/11</link>
      <description>&lt;p&gt;개발중인 셀럽잇 서비스에선 셀럽들이 다녀온 다양한 음식점에 대한 정보들을 제공합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;a href=&quot;https://www.celuveat.com&quot;&gt;https://www.celuveat.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;1035&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dU6Qr4/btsyww6CEKi/9KoZYBRk6bqKg62yAjbUiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dU6Qr4/btsyww6CEKi/9KoZYBRk6bqKg62yAjbUiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dU6Qr4/btsyww6CEKi/9KoZYBRk6bqKg62yAjbUiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdU6Qr4%2Fbtsyww6CEKi%2F9KoZYBRk6bqKg62yAjbUiK%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;1660&quot; height=&quot;1035&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;1035&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;보시는 것과 같이 음식점과 관련한 다양한 사진 파일들도 제공하고 있습니다.&lt;/p&gt;
&lt;p&gt;현재, 어플리케이션을 운영중인 서버에 해당 대략 600개의 이미지 파일들을 저장하여 관리하고 있는데요.&lt;br&gt;더 다양한 음식점과 해당 음식점과 관련한 사진들을 지속적으로 추가할 예정이며,&lt;br&gt;사용자들의 사진 리뷰 기능이 추가될 계획에 따라 더 많은 이미지 파일들이 추가 될 수 있습니다.&lt;/p&gt;
&lt;p&gt;이러한 상황에서 현재 방식(운영서버에서의 이미지 파일 저장 및 관리)은 이미지 파일이 지속적으로 적재되면서 &lt;strong&gt;운영서버의 디스크가 부족&lt;/strong&gt;해질 수 있으며,&lt;br&gt;수많은 &lt;strong&gt;이미지를 운영서버 내에서 관리&lt;/strong&gt;해야 한다는 문제점이 있습니다.&lt;br&gt;또, 이미지 요청마다 비교적 성능이 저조한 디스크로의 접근이 자주 발생하여 동일한 서버에서 운영중인 어플리케이션에도 영향을 끼칠 수 있습니다.&lt;/p&gt;
&lt;h2&gt;S3&lt;/h2&gt;
&lt;p&gt;위와 같은 문제를 해결하기 위해 AWS에서 제공하는 &lt;strong&gt;S3&lt;/strong&gt;(&lt;strong&gt;S&lt;/strong&gt;imple &lt;strong&gt;S&lt;/strong&gt;torage &lt;strong&gt;S&lt;/strong&gt;ervice)를 사용하여 운영서버에서 이미지 파일로 인한 리소스 낭비와 관리 포인트를 분리하고자 합니다.&lt;/p&gt;
&lt;p&gt;S3는 인터넷 스토리지 서비스로, 정적 파일들과 스크립트, 이미지, 음원, 바이너리 패키지등을 저장하는 관리하는 용도로 사용됩니다.&lt;/p&gt;
&lt;p&gt;용량은 가용적으로 늘어나 무제한으로 사용 할 수 있으며, 직접적인 스케일링 작업이 필요하지 않습니다.&lt;br&gt;이러한 파일들의 업로드 혹은 삭제와 같은 작업을 &lt;code&gt;http/https&lt;/code&gt;프로토콜을 통해 관리할 수 있습니다.&lt;br&gt;또한, ec2의 스토리지(ebs)를 사용하여 데이터를 저장하는 것보다 비용이 월등히 저렴합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;em&gt;EBS 볼륨 pricing&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/3f136ce0-4f5b-44b1-bd23-b4d96429ea22/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;S3 볼륨 pricing&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/5eb23371-20b4-41e3-89ba-9cf9101d4e3d/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;S3 버킷 생성&lt;/h3&gt;
&lt;p&gt;S3에서는 스토리지의 최상위 디렉토리를 &lt;strong&gt;버킷(Bucket)&lt;/strong&gt;이라고 합니다.&lt;br&gt;S3에 저장되는 데이터의 최소 단위를 &lt;strong&gt;객체(Object)&lt;/strong&gt;라고 합니다.&lt;/p&gt;
&lt;p&gt;이미지 저장을 위해 AWS에서 버킷을 생성하겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;먼저 버킷의 이름을 지정합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/df4c5969-80c0-4b47-b84e-d1a498fb8b23/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;ACLs(Access Control Lists)은 비활성화 해둡니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;빠른 접근을 위해 public access를 모두 허용합니다.&lt;br&gt;‼️&lt;strong&gt;(보안적으로 매우 취약한 설정입니다. 우선 이미지 업로드를 살펴본 후, 보안과 관련된 부분에 대해 설명하며 해결할 예정이니 따라하지 않으셔도 됩니다.)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/1da90fe4-52ac-4d7b-a48b-47316372525c/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;생성된 버킷에 접속합니다&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/a1067fba-3f79-464e-8ccd-3409686afaf4/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이미지 파일을 하나 업로드 합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/9d8cdb1f-7766-43ba-ad67-0661cb78864e/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;객체 URL을 선택하여 해당 객체에 접근합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/387b816f-35da-45b8-be15-ccfc52b85289/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;그럼 아래와 같이 Access Denied가 발생하는데요.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/42788cf5-1b13-4a5f-9d83-b485adff0c00/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;버킷 정책을 설정해주지 않아서 발생하는 현상입니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;버킷 설정으로 들어가 정책을 설정해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/e38e23a4-119a-41b4-a874-100ce0e6bb54/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/8a2bbee5-59c8-4562-a2d7-7baccc412fb8/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;정책 생성기에서 아래와 같이 설정하여 생성합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/b0cf1f98-36e8-406e-986b-b72fc154bd4e/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;ul&gt;
&lt;li&gt;[1] &lt;strong&gt;Select Type of Policy&lt;/strong&gt;: &lt;code&gt;S3 Buckey Policy 선택&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[2] &lt;strong&gt;Principal&lt;/strong&gt;: &lt;code&gt;*&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[3] &lt;strong&gt;Actions&lt;/strong&gt;: &lt;code&gt;GetObject 선택&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[4] &lt;strong&gt;Amazon Resource Name (ARN)&lt;/strong&gt;: &lt;code&gt;arn:aws:s3:::{버킷이름}/*&lt;/code&gt; (arn은 bucket properties에서 확인 할 수 있습니다)&lt;/li&gt;
&lt;li&gt;[5] 정책 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이후 생성된 정책을 복사하여 적용합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/7c623105-7719-4fbb-86f1-b40691404786/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;다시 객체 URL로 접속하면 업로드한 이미지에 접근 할 수 있습니다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/b5f6227a-b927-4f99-9243-8b8aea3e2340/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;보안 취약점&lt;/h2&gt;
&lt;p&gt;위 방식으로 이미지를 업로드하고, 해당 url을 통해 객체에 접근 할 수 있게 되었습니다.&lt;/p&gt;
&lt;p&gt;하지만 현재 S3는 보안적으로 굉장히 취약한 상태입니다.&lt;/p&gt;
&lt;p&gt;우선, 데이터들이 저장되어있는 S3 버킷의 주소가 노출되어 있어 악성 공격에 굉장히 취약한 상태입니다.&lt;br&gt;또, 모든 액세스 요청에 대해 허용되어 있기 때문에 신뢰할 수 없는 클라이언트의 접근 역시 가능합니다.&lt;/p&gt;
&lt;p&gt;실제로 S3와 관련하여 다양한 보안 이슈들이 존재한다고 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.theregister.com/2022/12/20/mcgraw_hills_s3_buckets_exposed/&quot;&gt;잘못된 S3보안 정책으로 10만명 이상의 학생들의 정보 유출&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.darkreading.com/application-security/cloud-misconfig-exposes-3tb-sensitive-airport-data-amazon-s3-bucket&quot;&gt;3TB의 항공사 직원의 개인 신상 정보(PII)와 비행기, 연료 라인, GPS 좌표 등의 민감한 회사 데이터 노출&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;출처: &lt;a href=&quot;https://github.com/nagwww/s3-leaks&quot;&gt;https://github.com/nagwww/s3-leaks&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;현재의 S3 보안 정책에 대해 개선해야할 필요가 있습니다.&lt;/p&gt;
&lt;p&gt;AWS에서는 CloudFront라는 CDN 서비스를 통해 버킷을 숨기고(OAI) 액세스를 강하게 제한하면서 S3버킷을 &lt;a href=&quot;https://aws.amazon.com/ko/blogs/korea/amazon-s3-amazon-cloudfront-a-match-made-in-the-cloud/&quot;&gt;사용하도록 권장&lt;/a&gt;하고 있습니다.&lt;/p&gt;
&lt;p&gt;그럼 간단하게 CDN에 대해 알아본 뒤, 좀 더 안전한 보안 정책을 수립해보겠습니다.&lt;/p&gt;
&lt;h2&gt;CloudFront&lt;/h2&gt;
&lt;p&gt;우선 CDN에 대해 간단하게 살펴보고 가겠습니다.&lt;/p&gt;
&lt;h3&gt;CDN(Content Delivery Network)&lt;/h3&gt;
&lt;p&gt;CDN은 사용자에게 빠르고 안전하게, 지리적 제약 없이 정적 콘텐츠를 제공하는할 수 있는 콘텐츠 전송 기술입니다.&lt;/p&gt;
&lt;p&gt;CDN은 아래 사진과 같이 CDN 네트워크(캐시서버)를 구성하여 사용자의 요청에 대한 응답의 물리적인 거리를 줄여 콘텐츠 로딩에 소요되는 시간을 최소화합니다. 근접한 사용자의 요청에 원본 서버가 아닌 캐시 서버가 콘텐츠를 전달합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;CDN없이 Origin 서버에서 서빙하는 경우&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://d2908q01vomqb2.cloudfront.net/5b384ce32d8cdef02bc3a139d4cac0a22bb029e8/2018/06/20/2-1-1024x543.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;CDN을 통해 캐시 서버에서 서빙하는 경우&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://d2908q01vomqb2.cloudfront.net/5b384ce32d8cdef02bc3a139d4cac0a22bb029e8/2018/06/20/3-1024x559.jpg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이러한 CDN기술은 다음과 같은 장점이 있습니다.&lt;/p&gt;
&lt;ol&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&gt;특히, 응답 속도가 느린 이미지를 자주 제공하는 셀럽잇에서는 CDN을 통해 웹페이지 로딩 속도를 크게 개선 할 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;em&gt;CloudFront를 사용하면 지리적 제한, 서명된 URL, 서명된 쿠키 등 액세스 제한을 추가로 설정하여 기준이 서로 다른 콘텐츠에 대한 액세스 제한을 강화할 수 있습니다.&lt;/em&gt; - AWS CloudFront&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;그리고, CloudFront는 이런 CDN을 제공하는 AWS 서비스입니다.&lt;/p&gt;
&lt;p&gt;CloudFront를 사용하여 S3버킷의 직접적인 접근을 막고 CloudFront를 통해서만 액세스가 가능하도록 구성한다면, 보안적인 취약점을 해결 할 수 있습니다.&lt;br&gt;AWS에서 제공하는 웹 방화벽인 WAF와 DDoS 보호 서비스인 AWS Shield 역시 CloudFront에 적용 가능하여 보안을 강화 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://d2908q01vomqb2.cloudfront.net/5b384ce32d8cdef02bc3a139d4cac0a22bb029e8/2018/06/27/4-v-2.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;CloudFront 적용&lt;/h3&gt;
&lt;p&gt;우선, 취약점 투성이인 S3의 정책을 강화하겠습니다.&lt;/p&gt;
&lt;p&gt;열어두었던 Public access를 모두 막도록 수정하고, Bucket Policy도 제거합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/7bd2ffce-fef2-4c71-b302-c9146b0ff57d/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/6fd28c2f-2a35-4cfc-b38a-5b65751f926d/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이후 CloudFront를 생성하겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;S3버킷을 설정 합니다&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/761c124c-aace-41f3-921c-b104cfa77812/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이후, Origin access control settings를 선택하여 CloudFront를 통해서만 bucket에 접근하도록 합니다. 제어 설정도 추가합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/2fb44228-ea38-45b5-bad0-c3147bed5bc8/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/3a5d8872-5b30-4168-80cc-2cfd461da9d5/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;다음 cache 동작에서 http 요청 모두 -&amp;gt; https를 사용하도록 수정합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/bc8331c0-1ee5-41b5-a4cc-2f6f4b17316e/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이번 적용에서는 WAF는 설정하지 않았습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/01f15cfc-6e73-4367-a494-02f43814d8a9/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;위 구성으로 CloudFront를 생성합니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;생성이 완료되면 연결된 버킷과 관련하여 버킷 정책이 생성되는데, 이를 복사한 뒤 버킷 정책에 추가합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/28dccf23-d010-40f3-9f39-7e0165e84d76/image.jpeg&quot; alt=&quot;&quot;&gt;)&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/bec1a5da-da14-421a-9c7d-e57266410f72/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;이제 S3객체 url로 직접 access는 불가능 하고, &lt;code&gt;{arn}.cloudfront.net/{객체이름}&lt;/code&gt;으로만 접근이 가능한 것을 확인 할 수 있습니다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/c3b91a22-cf61-47b6-9ea6-07d50488dd21/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;CloudFront 도메인 적용&lt;/h2&gt;
&lt;p&gt;이제 CloudFront를 통해서만 S3버킷에 접근이 가능하도록 구성하였습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;img src=&quot;https://techblog.woowahan.com/wp-content/uploads/2021/11/OAI%EA%B0%9C%EB%85%90%EB%8F%84.png&quot; alt=&quot;OAI&quot;&gt;&lt;/p&gt;
&lt;p&gt;출처: &lt;a href=&quot;https://techblog.woowahan.com/6217/&quot;&gt;https://techblog.woowahan.com/6217/&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;하지만 CloudFront의 서버주소가 노출되어 있습니다. 구매한 도메인을 사용해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;셀럽잇은 gabia에서 도메인을 구매하여 관리하고 있습니다.&lt;/p&gt;
&lt;p&gt;현재 CloudFront의 도메인을 CNAME으로 gabia에 등록하여 노출되지 않도록 수정해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/b9ba4b89-f69c-4b7a-848e-21cfb85a355b/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;전파가 된 이후, &lt;code&gt;image.celuveat.com/celuveat-logo.png&lt;/code&gt;에 접속해 보면 아래와 같이 403이 나옵니다...&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/b5b44872-6ef1-49b8-8b66-99d9f6108815/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;이는 CloudFront에선 허가되지 않은 CNAME 도메인에서의 접속을 차단하고 있기 때문입니다.&lt;/p&gt;
&lt;p&gt;AWS의 Ceritication Manager를 통해 인증을 받아 해결 할 수 있습니다. (ACM에 대한 설명은 생략하겠습니다.)&lt;/p&gt;
&lt;h3&gt;인증서 요청&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;CloudFront의 설정 수정에 들어간 뒤, SSL 인증서 요청을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/0faaf59e-1f7f-46e1-82cf-4bef0b677e05/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;public 인증서 요청을 선택 한 뒤,&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/53c9a93b-0c1a-42af-ba91-8a5841d84365/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;지정하고자 했던 도메인을 입력합니다. 이후 인증 방식을 선택 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/ee23c1c8-b2ee-481b-bc08-81fb94202869/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;DNS인증과 Email 인증 방식이 있습니다.&lt;/p&gt;
&lt;p&gt;AWS의 root 계정이 아니라면, email 인증 방식에는 어려움이 있기 때문에 DNS방식을 선택하고 ssl 인증서를 생성합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/e06e6fe0-96a7-4569-b290-f1140a5c09a6/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;그럼 아래와 같이 &lt;code&gt;CNAME name&lt;/code&gt;과 &lt;code&gt;CNAME value&lt;/code&gt;가 생성됩니다.&lt;/p&gt;
&lt;p&gt;이 CNAME과 value를 DNS에 등록하여 &lt;code&gt;image.celuveat.com&lt;/code&gt;의 도메인 관리자임을 인증받을 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;만약 name이 &lt;code&gt;abcedasbfasens23_1212n.image.celuveat.com&lt;/code&gt; 이고&lt;/p&gt;
&lt;p&gt;value가 &lt;code&gt;sjbndgaou232asnwbjoasubaasn.acm-validations.aws.&lt;/code&gt; 이라면,&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/45cc33b4-66db-412e-9219-f71ed290f0e3/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;호스트에 &lt;code&gt;abcedasbfasens23_1212n.image&lt;/code&gt;,&lt;/p&gt;
&lt;p&gt;값/위치에는 &lt;code&gt;sjbndgaou232asnwbjoasubaasn.acm-validations.aws.&lt;/code&gt; 을 넣어주면 됩니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;대략 몇분의 시간이 지난 뒤, Certificate status가 발행으로 변경됩니다! (저는 4분정도 걸렸습니다.)&lt;/p&gt;
&lt;p&gt;이제 발급받은 ssl을 CloudFront에 적용하여 도메인으로 사용하도록 합시다!&lt;/p&gt;
&lt;p&gt;CloudFront 편집에서 아래와 같이 &lt;code&gt;CNAME&lt;/code&gt;을 입력하고, 인증받은 ssl을 선택합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/9d6bff29-ae91-463d-bbd9-ad966e905d40/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;CloudFront 배포가 완료되면, &lt;code&gt;https://image.celuveat.com/celuveat-logo.png&lt;/code&gt;로 접근 가능합니다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/f4dad325-f7c5-4c98-84bb-38fa7c68c9d1/image.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;참고&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/ko/blogs/korea/amazon-s3-amazon-cloudfront-a-match-made-in-the-cloud/&quot;&gt;https://aws.amazon.com/ko/blogs/korea/amazon-s3-amazon-cloudfront-a-match-made-in-the-cloud/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=vgYfAndrpPU&quot;&gt;https://www.youtube.com/watch?v=vgYfAndrpPU&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sihyung92.oopy.io/devops/gabia/1&quot;&gt;https://sihyung92.oopy.io/devops/gabia/1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.pium.life/aws-s3-apply/&quot;&gt;https://blog.pium.life/aws-s3-apply/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;</description>
      <category>인프라/AWS</category>
      <category>aws</category>
      <category>Cloudfront</category>
      <category>S3</category>
      <author>RoyceWon</author>
      <guid isPermaLink="true">https://curiosity-storage.tistory.com/11</guid>
      <comments>https://curiosity-storage.tistory.com/11#entry11comment</comments>
      <pubDate>Tue, 17 Oct 2023 00:01:26 +0900</pubDate>
    </item>
    <item>
      <title>Spring Interceptor와 Filter</title>
      <link>https://curiosity-storage.tistory.com/9</link>
      <description>&lt;p&gt;Spring을 활용하여 웹 어플리케이션을 개발하면, 다양한 Http 요청에 대해 처리하는 기능들을 개발 할 것이다.&lt;/p&gt;
&lt;p&gt;그리고, 필연적으로 공통적으로 처리해야 할 기능과 중복된 코드들이 많이 생긴다.&lt;/p&gt;
&lt;p&gt;이러한 웹 요청에 대한 공통 관심사를 처리하기 위해 &lt;code&gt;Filter&lt;/code&gt;와 &lt;code&gt;Interceptor&lt;/code&gt;를 활용할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;SpringBoot를 활용한 Spring Web 요청 flow&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/c6242777-e770-46f8-b6b1-349c8f5d66ad/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;출처 : &lt;a href=&quot;https://gowoonsori.com/spring/architecture/&quot;&gt;https://gowoonsori.com/spring/architecture/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h1&gt;Filter&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;em&gt;&lt;code&gt;Filter.class&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A filter is an object that performs filtering tasks on either the request to a resource (a servlet or static content), or on the response from a resource or both.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;필터는 리소스에 대한 요청과 응답에 대해 필터링 작업을 수행하는 객체이다.&lt;/em&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;필터는 톰캣 같은 미들웨어 컨테이너 영역에서 동작이 수행된다. &lt;code&gt;Dispatcher Servlet&lt;/code&gt;에 요청이 전달되기 전/후에 URL 패턴에 맞는 모든 요청에 대해 부가 작업을 처리할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Filter&lt;/code&gt;는 &lt;code&gt;chaining&lt;/code&gt;을 형성하여 정의한 메서드를 수행한다. 요청 자체를 처리하려는 경우 해당 &lt;code&gt;Filter&lt;/code&gt;가 나머지 체인을 호출하지 않을 수 있다.&lt;/p&gt;
&lt;p&gt;또, &lt;code&gt;Servlet&lt;/code&gt;을 사용해서 요청과 응답을 수정할 수도 있다.&lt;/p&gt;
&lt;h3&gt;Filter 사용 용례&lt;/h3&gt;
&lt;p&gt;from. &lt;em&gt;&lt;code&gt;Filter.class&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Authentication Filters&lt;/code&gt;: 사용자 인증이나 권한 검사&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Logging and Auditing Filters&lt;/code&gt; : 요청과 응답에 대한 로깅&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Image conversion Filters&lt;/code&gt; : 이미지 형식 변환 (.jpeg -&amp;gt; png)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Data compression Filters&lt;/code&gt; : 데이터를 압축하여 반환&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Encryption Filters&lt;/code&gt; : 암호화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Tokenizing Filters&lt;/code&gt; : 토큰 필터&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Filters that trigger resource access events&lt;/code&gt; : 이벤트 발생 트리거 필터&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XSL/T filters&lt;/code&gt; : XML 응답 변환에 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Mime-type chain Filter&lt;/code&gt; : 미디어 타입에 따른 필터&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;code&gt;javax.servlet.Filter&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package javax.servlet;

import java.io.IOException;

public interface Filter {

    public default void init(FilterConfig filterConfig) throws ServletException {}

    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException;

    public default void destroy() {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Filter&lt;/code&gt; 인터페이스를 구현하여 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;ex.)구현 예시&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class FirstFilter implements Filter {

    @Override
    public void init(final FilterConfig filterConfig) throws ServletException {
        System.out.println();
        System.out.println(&amp;quot;============================================&amp;quot;);
        System.out.println(&amp;quot;============= FirstFilter init =============&amp;quot;);
        System.out.println(&amp;quot;============================================&amp;quot;);
        System.out.println();
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
            throws IOException, ServletException {
        System.out.println(&amp;quot;FirstFilter start&amp;quot;);
        chain.doFilter(request, response); //다음 필터로 이동
        System.out.println(&amp;quot;FirstFilter end&amp;quot;);
    }

    @Override
    public void destroy() {
        System.out.println();
        System.out.println(&amp;quot;============================================&amp;quot;);
        System.out.println(&amp;quot;============ FirstFilter destroy ===========&amp;quot;);
        System.out.println(&amp;quot;============================================&amp;quot;);
        System.out.println();
        Filter.super.destroy();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;init()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Servlet Container&lt;/code&gt;는 필터를 인스턴스화 할 때,  &lt;code&gt;init()&lt;/code&gt;를 한 번 호출한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;doFilter()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Request&lt;/code&gt;, &lt;code&gt;Response&lt;/code&gt;가 해당 &lt;code&gt;Filter&lt;/code&gt;를 지날 때 수행되는 로직&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FilterChain.doFilter&lt;/code&gt;를 기준으로 요청/응답이 나뉜다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FilterChain&lt;/code&gt; 을 통해 &lt;code&gt;DispatcherServlet&lt;/code&gt;에 요청이 전달 될 때 까지 정의한 &lt;code&gt;Filter&lt;/code&gt;내 메서드들이 수행된다&lt;/p&gt;
&lt;p&gt;단일 HTTP 요청을 처리하는 레이어로서, 여러개의 &lt;code&gt;Filter&lt;/code&gt;들이 연결되어 있고, 연쇄적으로 동작한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/roycewon/post/72b73a0d-bd30-43ab-afee-9472eb4548a1/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;   기본적으론, 알파벳 순서로 Filter의 순서가 등록된다.&lt;br&gt;   정의한 Filter가 여러개 존재하는 경우, &lt;code&gt;@Order&lt;/code&gt;를 통해 순서를 지정할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;chain.doFilter()&lt;/code&gt;를 실행하지 않으면 filterChain을 수행하지 않고 해당 &lt;code&gt;Filter&lt;/code&gt;에서 &lt;code&gt;filterChain&lt;/code&gt;이 종료된다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;destroy()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Filter&lt;/code&gt;가 소멸할 때 실행된다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;실행 결과&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[SecondFilter init()]&lt;/code&gt; ,&lt;/p&gt;
&lt;p&gt; &lt;code&gt;[FirstFilter init()]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;request  -&amp;gt; &lt;code&gt;FirstFilter.doFilter()&lt;/code&gt; -&amp;gt; &lt;code&gt;SecondFilter.doFilter()&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Filter 등록&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;FilterRegistrationBean&lt;/code&gt;에 구현한 &lt;code&gt;Filter&lt;/code&gt;를 등록 할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {


      @Bean
    public FilterRegistrationBean&amp;lt;FirstFilter&amp;gt; filterFilterRegistrationBean() {
        FilterRegistrationBean&amp;lt;FirstFilter&amp;gt; registrationBean = new FilterRegistrationBean&amp;lt;&amp;gt;();
        registrationBean.setFilter(new FirstFilter());
        registrationBean.addUrlPatterns(&amp;quot;/&amp;quot;); //url 패턴

        return registrationBean;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@WebFilter&lt;/code&gt;를 사용하여 등록할 수 있다. 이 경우에는 부트스트랩 클래스에 &lt;code&gt;@ServletComponentScan&lt;/code&gt; 도 같이 등록하여야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Slf4j
@WebFilter(urlPatterns = {&amp;quot;/test/*&amp;quot;, &amp;quot;/test2/*&amp;quot;})
public class firstFilter implements Filter {
    // ...
}

@ServletComponentScan //추가
@SpringBootApplication
public class HelloApplication {
    public static void main(String[] args) {
        SpringApplication.run(HelloApplication.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;추가로, &lt;code&gt;DelegatingFilterProxy&lt;/code&gt;를 통해 구현한 filter 클래스를 빈으로 등록하여 사용할 수 있다고 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Springboot&lt;/code&gt;를 통해 내장 톰캣을 사용하고 있다면, 위 과정을 거치지 않고 일반적인 방식으로 빈을 등록 할 수 있다.&lt;/p&gt;
&lt;h1&gt;Interceptor&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;Filter&lt;/code&gt;와 비슷하게 Http 요청  전/후에 URL 패턴에 맞는 요청에 대해 부가 작업을 처리할 수 있다. 차이점이 있다면 실행 시점이다. &lt;code&gt;DispatcherServlet&lt;/code&gt;의 &lt;code&gt;HandlerMapping&lt;/code&gt;과정을 거친 이후 해당 &lt;code&gt;Handler&lt;/code&gt;가 실행되기 이전에 해당 요청을 가로채서(intercept) 실행된다. 또, &lt;code&gt;Spring context&lt;/code&gt;에 등록되기 때문에 등록되어 있는 &lt;code&gt;Bean&lt;/code&gt;을 주입 받고 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HandlerInterceptor&lt;/code&gt;를 구현하여 요청 전/후에 대한 공통적인 로직을 수행할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;org.springframework.web.servlet.HandlerInterceptor&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package org.springframework.web.servlet;

public interface HandlerInterceptor {

    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        return true;
    }

    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable ModelAndView modelAndView) throws Exception {
    }

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable Exception ex) throws Exception {
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;preHandle()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Mapping된 &lt;code&gt;Handler&lt;/code&gt;가 수행되기 이전에 실행된다.&lt;/p&gt;
&lt;p&gt;반환값이 true가 아닌 경우, &lt;code&gt;handler&lt;/code&gt;를 수행하지 않고 작업이 중단된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;postHandle()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;handler&lt;/code&gt;가 정상 동작을 한 뒤, View가 생성되기 이전에 실행된다.&lt;/p&gt;
&lt;p&gt;인자로 받은 &lt;code&gt;ModelAndView&lt;/code&gt;를 통해 View에 전달되는 데이터를 조작할 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;afterCompletion&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;모든 작업이 완료된 후에 실행 된다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;ex) 구현 예시&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class FirstInterceptor implements HandlerInterceptor {


    private final Logger log = LoggerFactory.getLogger(this.getClass());

    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler)
            throws Exception {
        final HandlerMethod handlerMethod = (HandlerMethod) handler; // handler 정보

        log.warn(&amp;quot;Request URI : {}&amp;quot;, request.getRequestURI());
        log.warn(&amp;quot;Handler method name : {}&amp;quot;, handlerMethod.getMethod().getName());

        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler,
                           final ModelAndView modelAndView) throws Exception {
        log.warn(&amp;quot;Response view name : {}&amp;quot;, modelAndView.getViewName());

        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Interceptor 등록&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new FirstInterceptor())
                .addPathPatterns(&amp;quot;/**&amp;quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Filter vs Interceptor&lt;/h1&gt;
&lt;p&gt;두 기능 모두 요청 전/후 에서 공통적으로 특정 로직을 수행 할 수 있다. 정해진 용례는 없지만 앞서 살펴본 각각의 특징들을 이해하고 공통 관심사들을 적절하게 &lt;code&gt;Filter&lt;/code&gt;, 혹은 &lt;code&gt;Interceptor&lt;/code&gt;에 구현하면 좋을 것 같다.&lt;/p&gt;</description>
      <category>Java</category>
      <category>Java</category>
      <category>Spring</category>
      <category>web</category>
      <author>RoyceWon</author>
      <guid isPermaLink="true">https://curiosity-storage.tistory.com/9</guid>
      <comments>https://curiosity-storage.tistory.com/9#entry9comment</comments>
      <pubDate>Mon, 16 Oct 2023 23:43:57 +0900</pubDate>
    </item>
    <item>
      <title>다양한 HTTP Header, 언제 어떻게 사용해야 하는가</title>
      <link>https://curiosity-storage.tistory.com/8</link>
      <description>&lt;h1&gt;HTTP Headers&lt;/h1&gt;
&lt;h3&gt;&lt;strong&gt;Header&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;HTTP header는 HTTP의 요청과 응답에 필요한 부가적인 정보들을 담고있는 필드이다.&lt;/p&gt;
&lt;p&gt;Message나 Body의 데이터에 대한 의미를 변경하거나 조정하는데 사용될 수 있다.&lt;/p&gt;
&lt;p&gt;헤더는 대소문자를 구별하지 않으며, 줄의 처음에서 시작하여 바로 다음에 &lt;code&gt;:&lt;/code&gt;과 헤더에 해당하는 값이 따라온다.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Header의 종류&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Header는 사용되는 목적과 진영에 따라 다양한 방법으로 분류할 수 있다.&lt;/p&gt;
&lt;p&gt;RFC에서 정의한 명확한 구분 기준은 없지만, 편의상 분류할 수 있는데 다음과 같이 4개의 헤더로 자주 분류한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;General header&lt;/strong&gt; : 요청과 응답 모두에 적용되지만 바디에서 최종적으로 전송되는 데이터와는 관련이 없는 헤더&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Request header&lt;/strong&gt; : 요청하는 리소스 또는 클라이언트 자체에 대한 정보가 포함되는 헤더&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Response header&lt;/strong&gt; : 서버의 이름, 버전, 위치 등과 같은 서버에 대한 자체정보 또는 응답에 대한 추가 정보가 포함되는 헤더&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Representation(Entity) header&lt;/strong&gt; : MIME 유형 또는 적용된 인코딩/압축과 같은 리소스 본문에 대한 정보가 포함되는 헤더&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;General Header&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Date&lt;/strong&gt;&lt;br&gt;HTTP 메시지를 생성한 일시 (RFC 1123에서 규정)&lt;br&gt;&lt;code&gt;Date: Tue, 2 May 2023 09:05:12 GMT&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Connection&lt;/strong&gt;&lt;br&gt;클라이언트와 서버 간 연결에 대한 옵션 설정&lt;/p&gt;
&lt;p&gt;  &lt;code&gt;Connection: close
  Connection: Keep-Alive&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Content-Type&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;  HTTP 메세지에 담긴 데이터의 형식을 명시&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cache-Control&lt;/strong&gt;&lt;br&gt;쿠키/캐시 관련된 설정&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  HTTP/1.1 200 OK
  Date: Fri, 12 Mar 2021 03:52:32 GMT
  Server: WSGIServer/0.2 CPython/3.8.0
  Content-Type: application/json
  Cache-Control: max-age=15

  {&amp;quot;firstName&amp;quot;: &amp;quot;\uad11\ubbfc&amp;quot;, &amp;quot;lastName&amp;quot;: &amp;quot;\uae40&amp;quot;}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Transfer-Encoding&lt;/strong&gt;&lt;br&gt;전송되는 메세지의 인코딩 정보 설정&lt;/p&gt;
&lt;p&gt;  &lt;code&gt;Transfer-Encoding: chunked&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;  &lt;code&gt;Transfer-Encoding: compress
  Transfer-Encoding: gzip&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trailer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;  청크(chunk) 인코딩된 메세지에 대한 지시어.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  HTTP/1.1 200 OK
  Content-Type: text/plain
  Transfer-Encoding: chunked
  Trailer: Expires

  7\r\n
  Mozilla\r\n
  9\r\n
  Developer\r\n
  7\r\n
  Network\r\n
  0\r\n
  Expires: Tue, 2 May 2023 09:05:12 GMT\r\n
  \r\n&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;Request Header&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Host&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;  요청하는 호스트와 포트 번호(HTTP/1.1 이후부턴 필수 항목, IP가 아닌 도메인 정보가 담겨야 한다)&lt;/p&gt;
&lt;p&gt;  &lt;code&gt;Host: www.example.com:80&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;User-agent&lt;/strong&gt;&lt;br&gt;요청 측의 브라우저 혹은 OS와 관련된 정보를 포함&lt;/p&gt;
&lt;p&gt;  &lt;code&gt;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Authorization&lt;/strong&gt;&lt;br&gt;인증 토큰을 보낼 때 사용하는 헤더&lt;br&gt;&lt;code&gt;Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;  &lt;code&gt;Authorization: &amp;lt;schem&amp;gt; &amp;lt;credentials&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cookie&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;  이전에 서버로 부터 받은 쿠키를 담는 헤더&lt;/p&gt;
&lt;p&gt;  &lt;code&gt;Cookie: session_id=12345; user_id=67890&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Referer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;  현재 요청 직전에 머물고 있던 웹 링크 주소를 담는 헤더&lt;/p&gt;
&lt;p&gt;  &lt;code&gt;Referer: https://www.example.com/previous-page.html&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Origin&lt;/strong&gt;&lt;br&gt;요청의 출처를 담는 헤더&lt;br&gt;URI 정보는 포함하지 않고 서버 이름만 포함된다.&lt;br&gt;&lt;code&gt;Origin: https://developer.mozilla.org&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;  &lt;strong&gt;*CORS&lt;/strong&gt; 에 사용되는 header*&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accept&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;  요청 측에서 기대하는 데이터 타입을 명시&lt;br&gt;  &lt;code&gt;ex. Accept : application/json&lt;/code&gt; -&amp;gt; 서버의 응답 결과가 &lt;code&gt;application/json&lt;/code&gt; 인 경우만 받겠다는 의미&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Accept-Charset&lt;/code&gt;, &lt;code&gt;Accept-Encoding&lt;/code&gt;, &lt;code&gt;Accept-Language&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/ *;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;q는 우선순위(가중치)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;Response header&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Server&lt;/strong&gt;&lt;br&gt;요청을 처리한 웹서버 정보&lt;br&gt;&lt;code&gt;Server: nginx&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;  &lt;code&gt;Server: cloudflare&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Location&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;  생성된 리소스 주소를 포함하여 응답&lt;/p&gt;
&lt;p&gt;  &lt;code&gt;Location: /product/1&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Content-Location -&amp;gt; 컨텐츠 협상이 끝난 뒤의 경로 (&lt;code&gt;/product.html&lt;/code&gt;,  &lt;code&gt;/product.xml&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ETag&lt;/strong&gt;&lt;br&gt;리소스의 특정 버전의 식별값을 담는 헤더&lt;br&gt;웹 캐시 유효성 검증 -&amp;gt; 컨텐츠가 변경되지 않은 경우 서버는 전체 응답을 다시 전송할 필요가 없다.&lt;br&gt;&lt;code&gt;ETag: W/&amp;quot;6345748b-1f101&amp;quot;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WWW-Authenticate&lt;/strong&gt;&lt;br&gt;인증을 요구하는 401상태 응답과 함께 사용&lt;br&gt;서버에서 각각 다른 인증값을 사용하는 다양한 영역들이 있는데, 이를 표기하는 헤더&lt;br&gt;&lt;code&gt;WWW-Authenticate: Basic realm=&amp;quot;Corporate Financials&amp;quot;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Allow&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;  &lt;code&gt;405 Method Not Allowed&lt;/code&gt; 상태 응답과 함께 사용&lt;br&gt;  요청된 리소스에 대해 허용된 HTTP 메서드를 표기&lt;br&gt;  &lt;code&gt;Allow: GET, HEAD, POST&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Access-Control-Allow-Origin&lt;/strong&gt;&lt;br&gt;요청에 대한 자원을 공유할 수 있는 출처(Origin)에 대해 표기&lt;br&gt;&lt;code&gt;Access-Control-Allow-Origin: https://developer.mozilla.org
Access-Control-Allow-Origin: *&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vary&lt;/strong&gt;&lt;br&gt;서버에서 응답을 생성할 때 고려된 요청 헤더를 표시하는데 사용&lt;/p&gt;
&lt;p&gt;  &lt;code&gt;Vary: Accept-Language, Accept-Encoding, Origin&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retry-After&lt;/strong&gt;&lt;br&gt;재요청을 하기 전까지 기다려야 하는 시간&lt;/p&gt;
&lt;p&gt;  &lt;code&gt;Retry-After: Wed, 21 Oct 2015 07:28:00 GMTRetry-After: 120&lt;/code&gt; (120 seconds)&lt;/p&gt;
&lt;p&gt;  3가지 case에 사용됨. (리다이렉트, 429 status(Too many request), 503 status(Service Unavailalbe))&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;Entity header&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Content-Type&lt;/strong&gt;&lt;br&gt;데이터의 형식&lt;/p&gt;
&lt;p&gt;  &lt;code&gt;Content-Type: text/html; charset=utf-8&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Content-Encoding&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;  데이터 인코딩 방식&lt;/p&gt;
&lt;p&gt;  &lt;code&gt;Content-Encoding: gzip&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Content-Language&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;  데이터의 자연 언어&lt;/p&gt;
&lt;p&gt;  &lt;code&gt;Content-Languauge: ko&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Content-Length&lt;/strong&gt;&lt;br&gt;데이터의 길이(10진수)&lt;br&gt;&lt;code&gt;Content-Length: 5&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Content-Security-Policy&lt;/strong&gt;&lt;br&gt;컨텐츠 보안 정책 설정&lt;br&gt;컨텐츠를 불러올 소스를 명시&lt;br&gt;&lt;code&gt;Content-Security-Policy: default-src &amp;#39;self&amp;#39; example.com *.example.com; image-src: *&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;  =&amp;gt; 컨텐츠 불러올 소스 명시(&lt;code&gt;example.com&lt;/code&gt;, &lt;code&gt;example.com&lt;/code&gt;의 모든 서브 도메인, origin 으로 부터 오는 모든 컨텐츠 허용. + 이미지(컨텐츠)는 모든 소스 허용)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;Header에 표기하는 정보를 Body에 담으면 안되나?&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;여러 자원에서 공통으로 일관되게 사용되는 정보 (ex 인증)&lt;/li&gt;
&lt;li&gt;보안과 관련된 정보&lt;/li&gt;
&lt;li&gt;비즈니스 로직에 직접적으로 활용되지 않는 정보 (메타데이터)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;⇒ Header에서 사용하도록 하자&lt;/p&gt;</description>
      <category>CS</category>
      <category>HTTP</category>
      <category>HTTP hedaer</category>
      <category>Network</category>
      <author>RoyceWon</author>
      <guid isPermaLink="true">https://curiosity-storage.tistory.com/8</guid>
      <comments>https://curiosity-storage.tistory.com/8#entry8comment</comments>
      <pubDate>Fri, 12 May 2023 08:55:07 +0900</pubDate>
    </item>
  </channel>
</rss>