<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>공유/도움/개발</title>
    <link>https://keepbang.tistory.com/</link>
    <description>작은것부터 천천히</description>
    <language>ko</language>
    <pubDate>Sat, 22 Aug 2026 18:29:08 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>keepbang</managingEditor>
    <image>
      <title>공유/도움/개발</title>
      <url>https://tistory1.daumcdn.net/tistory/4670041/attach/b39d8970f0564ddf91976ae20f8497e3</url>
      <link>https://keepbang.tistory.com</link>
    </image>
    <item>
      <title>kotlin jdsl에서 ElementCollection 컬럼 조건 추가하기</title>
      <link>https://keepbang.tistory.com/entry/kotlin-jdsl%EC%97%90%EC%84%9C-ElementCollection-%EC%BB%AC%EB%9F%BC-%EC%A1%B0%EA%B1%B4-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;코프링으로 개발중에&amp;nbsp; ElementCollection으로 된 컬럼을 조건으로 조회하는 기능을 개발하던중 여러 여러가지 안되는 상황이 있어서 기록하고자 글을 남긴다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결방법은 kotlin-jdsl github 이슈에서 찾았다!!&lt;/p&gt;
&lt;figure id=&quot;og_1734666639460&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;exists subquery 문의 &amp;middot; Issue #774 &amp;middot; line/kotlin-jdsl&quot; data-og-description=&quot;안녕하세요. exists subquery에 대해 문의드립니다. Entity 구조가 아래와 같을 때, @Entity class Notification( @Id val id: Long, @Enumerated(EnumType.STRING) @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name ...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/line/kotlin-jdsl/issues/774&quot; data-og-url=&quot;https://github.com/line/kotlin-jdsl/issues/774&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zj22m/hyXOeF1tvG/RfCIgDK8AY3iKgUhK34ypk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/n1epJ/hyXOfZfrcw/wbf18sc1kg4V3ZHYc22Tkk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/line/kotlin-jdsl/issues/774&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/line/kotlin-jdsl/issues/774&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zj22m/hyXOeF1tvG/RfCIgDK8AY3iKgUhK34ypk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/n1epJ/hyXOfZfrcw/wbf18sc1kg4V3ZHYc22Tkk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;exists subquery 문의 &amp;middot; Issue #774 &amp;middot; line/kotlin-jdsl&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요. exists subquery에 대해 문의드립니다. Entity 구조가 아래와 같을 때, @Entity class Notification( @Id val id: Long, @Enumerated(EnumType.STRING) @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;엔티티 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 엔티티 내부에 enum list가 들어가야하는 상황이 있어서 어떻게 넣을까 생각하다가 ElementCollection을 사용하기로했다.&lt;/p&gt;
&lt;pre id=&quot;code_1734666468180&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Entity(
	enumTypes: Set&amp;lt;EnumType&amp;gt;,
    // 이하 생략
) {

....
 
    @ElementCollection(targetClass = EnumType::class)
    @CollectionTable(name = &quot;enum_types&quot;, joinColumns = [JoinColumn(name = &quot;entity_id&quot;)])
    @Column(name = &quot;enum_type&quot;, nullable = false)
    @Enumerated(EnumType.STRING)
    var _EnumTypes : MutableSet&amp;lt;EnumType&amp;gt; = enumTypes.toMutableSet()
        protected set
....

}&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 목록이 담긴 컬럼을 만들었다. 이렇게하면 테이블이 2개가 생기게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;repository의 jdsl쿼리는 아래처럼 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1734667087800&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override fun findByEntity(query: FindQuery): Page&amp;lt;Entity&amp;gt; {
        return queryFactory.pageQuery(query.getPageable()) {
            selectFrom(Entity::class)
            where(
                and(
                inFilter(query.enumType, Entity::enumTypes)
                )
            )
        }
    }
    
 inline fun &amp;lt;reified T, R&amp;gt; SpringDataPageableQueryDsl&amp;lt;T&amp;gt;.inFilter(
    value: R?,
    property: KProperty1&amp;lt;T, R?&amp;gt;,
): PredicateSpec? {
    return value?.let { col(property).`in`(it) }
}&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;inFilter라는 확장함수를 작성해서 만들어봤다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;발생한 에러&lt;/h2&gt;
&lt;pre id=&quot;code_1734666719200&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;kotlin&quot;&gt;&lt;code&gt;org.springframework.dao.InvalidDataAccessResourceUsageException: Multi valued paths are only allowed for the member of operator&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대충 접근할수없는 경로라서 에러가 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명이거는 다른사람도 발생했을것이라 생각하고 kotlin-jdsl의 이슈를 찾아보니 역시나 있었다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변경된 코드&lt;/h3&gt;
&lt;pre id=&quot;code_1734667246869&quot; class=&quot;ruby&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;return queryFactory.pageQuery(query.getPageable()) {
    select(entity(Entity::class))
    from(entity(Entity::class))
    join(Entity::enumTypes)
    where(
        and(
            query.enumType?.let { 
                entity(EnumType::class).`in`(it)
            }
        )
    )
}&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;join을 사용해서 해당 필드에 조인이 걸리게 작성하고 조건절에 Enum class를 넣어서 단수로 조건절을 적용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저부분을 확장함수로 변경해서 사용하기 편하게 만드는건 나중에 따로 해봐야할것같다...&lt;/p&gt;</description>
      <category>Backend/kotlin</category>
      <category>ElementCollection</category>
      <category>Kotlin</category>
      <category>kotlin-jdsl</category>
      <author>keepbang</author>
      <guid isPermaLink="true">https://keepbang.tistory.com/52</guid>
      <comments>https://keepbang.tistory.com/entry/kotlin-jdsl%EC%97%90%EC%84%9C-ElementCollection-%EC%BB%AC%EB%9F%BC-%EC%A1%B0%EA%B1%B4-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0#entry52comment</comments>
      <pubDate>Fri, 20 Dec 2024 13:02:28 +0900</pubDate>
    </item>
    <item>
      <title>Spring JPA Custom Id Generator 만들기</title>
      <link>https://keepbang.tistory.com/entry/Spring-JPA-Custom-Id-Generator-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
      <description>&lt;div style=&quot;padding: 10px 20px 0px 20px; border-radius: 10px; margin: 1em 1em; border: 1px solid #ccd0d7; background-color: #f5f5f5; box-shadow: 1px 2px 3px 1px rgba(0,0,0,.1);&quot;&gt;
&lt;p style=&quot;border-bottom: 2px solid #ccd0d7;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;▤ 목차&lt;/b&gt;&lt;/p&gt;
&lt;script src=&quot;./images/jquery.toc.min.js&quot;&gt;&lt;/script&gt;
&lt;ul id=&quot;toc&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;decimal&quot;&gt;&lt;/ul&gt;
&lt;script type=&quot;text/javascript&quot;&gt; $(function(){  $(&quot;#toc&quot;).toc({content: &quot;.tt_article_useless_p_margin&quot;, headings: &quot;h2, h3&quot;}); });&lt;/script&gt;
&lt;/div&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;JPA를 사용할 때 보통 id는 시퀀스 id를 자주 사용합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 요구사항으로 &lt;code&gt;&quot;앞에 특정 문자를 포함한 아이디 체계&quot;&lt;/code&gt;를 사용해야 한다고 하면 어떻게 해야할까 고민해보다가 내용을 찾고 정리해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IdGeneratorType Annotation&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색을 해보면 &lt;code&gt;GenericGenerator&lt;/code&gt; 애너테이션을 사용해서 &lt;code&gt;Generator&lt;/code&gt; 구현체와 함께 사용하는 예제들을 많지만 해당 애너테이션은 &lt;code&gt;deprecated&lt;/code&gt; 되어있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;614&quot; data-origin-height=&quot;240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdUq0w/btsKw2zMUr6/GCDNct3xIWUtQZS9ewBGyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdUq0w/btsKw2zMUr6/GCDNct3xIWUtQZS9ewBGyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdUq0w/btsKw2zMUr6/GCDNct3xIWUtQZS9ewBGyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdUq0w%2FbtsKw2zMUr6%2FGCDNct3xIWUtQZS9ewBGyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;156&quot; data-origin-width=&quot;614&quot; data-origin-height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 방식으로는 &lt;code&gt;IdGeneratorType&lt;/code&gt;으로 애너테이션을 만들어서 사용하는 방법이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bq2Ldl/btsKwZJPFPb/ESLYXDgjSmWKVATsu6y6Gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bq2Ldl/btsKwZJPFPb/ESLYXDgjSmWKVATsu6y6Gk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bq2Ldl/btsKwZJPFPb/ESLYXDgjSmWKVATsu6y6Gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbq2Ldl%2FbtsKwZJPFPb%2FESLYXDgjSmWKVATsu6y6Gk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;226&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부에는 &lt;code&gt;Generator&lt;/code&gt;를 상속 받는 클래스를 받고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 애너테이션으로 &lt;code&gt;prefix&lt;/code&gt;가 붙는 형식의 id를 만들어보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@IdGeneratorType(PrefixIdGenerator::class)
@Target(AnnotationTarget.FIELD, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class PrefixId(
    val name: String,
    val prefix: String = &quot;&quot;
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법은 간단합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애너테이션을 만들고 &lt;code&gt;IdGeneratorType&lt;/code&gt;에 &lt;code&gt;Generator&lt;/code&gt; 구현체를 넣으면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 추가적으로 &lt;code&gt;Generator&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;Generator 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;Generator&lt;/code&gt;를 구현해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Generator&lt;/code&gt;는 &lt;code&gt;IdentifierGenerator&lt;/code&gt; 를 상속 받아 &lt;code&gt;generate&lt;/code&gt; 함수를 &lt;code&gt;override&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;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;class PrefixIdGenerator(config: PrefixId) : IdentifierGenerator {

    // 1)
    private val prefix: String = config.prefix

    override fun generate(session: SharedSessionContractImplementor, `obj`: Any): Any {
        
        // 2)
        val identifierPropertyName = session.getEntityPersister(obj.javaClass.name, obj).identifierPropertyName

        // 3)
        val query = java.lang.String.format(
            &quot;select %s from %s order by %s desc limit 1&quot;,
            identifierPropertyName,
            obj.javaClass.getSimpleName(),
            identifierPropertyName
        )

        val ids = session.createQuery(query, String::class.java)
            .stream()

	// 4)
        val max: Long = ids.findFirst()
            .map { o -&amp;gt; o.toString().replace(prefix, &quot;&quot;).toLong() }
            .getOrElse {0L}

	// 5)
        return prefix + (max + 1)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프로퍼티로 받은 값을 가져와서 사용 할 수 있습니다.&lt;/li&gt;
&lt;li&gt;해당 &lt;code&gt;generator&lt;/code&gt;를 사용하는 entity의 아이디 이름을 가져옵니다.&lt;/li&gt;
&lt;li&gt;마지막 id값을 가져오기 위한 쿼리문을 생성하고 실행시킵니다.&lt;/li&gt;
&lt;li&gt;가져온 아이디값의 prefix제거하고 뒤에 숫자만 가져옵니다.&lt;/li&gt;
&lt;li&gt;prefix를 추가해서 마지막 id에 +1을 한 값으로 반환해서 아이디가 적용됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 예제는 &lt;code&gt;auto_increment&lt;/code&gt;로 되어있는 id 앞에 &lt;code&gt;prefix&lt;/code&gt;를 넣는 방식으로 구현해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB를 조회해서 아이디를 만드는 방식이 아닌 유일 아이디 생성기같은걸 만들어서 사용해도 될 것 같습니다.&lt;/p&gt;</description>
      <category>Backend/Spring</category>
      <category>custom id</category>
      <category>customidgenerator</category>
      <category>entity</category>
      <category>IDGenerator</category>
      <category>idgeneratortype</category>
      <category>jpa</category>
      <category>prifix id</category>
      <author>keepbang</author>
      <guid isPermaLink="true">https://keepbang.tistory.com/51</guid>
      <comments>https://keepbang.tistory.com/entry/Spring-JPA-Custom-Id-Generator-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry51comment</comments>
      <pubDate>Wed, 6 Nov 2024 14:14:09 +0900</pubDate>
    </item>
    <item>
      <title>Spring boot 3.x OAuth2(Google, Kakao) 로그인/회원가입 구현 2 - API 개발</title>
      <link>https://keepbang.tistory.com/entry/Spring-boot-3x-OAuth2Google-Kakao-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84-2-API-%EA%B0%9C%EB%B0%9C</link>
      <description>&lt;div style=&quot;padding: 10px 20px 0px 20px; border-radius: 10px; margin: 1em 1em; border: 1px solid #ccd0d7; background-color: #f5f5f5; box-shadow: 1px 2px 3px 1px rgba(0,0,0,.1);&quot;&gt;
&lt;p style=&quot;border-bottom: 2px solid #ccd0d7;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;▤ 목차&lt;/b&gt;&lt;/p&gt;
&lt;script src=&quot;./images/jquery.toc.min.js&quot;&gt;&lt;/script&gt;
&lt;ul id=&quot;toc&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;decimal&quot;&gt;&lt;/ul&gt;
&lt;script type=&quot;text/javascript&quot;&gt; $(function(){  $(&quot;#toc&quot;).toc({content: &quot;.tt_article_useless_p_margin&quot;, headings: &quot;h2, h3&quot;}); });&lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이전글&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://keepbang.tistory.com/entry/Spring-boot-3x-flutter-OAuth2Google-Kakao-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84-1-flutter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2024.07.31 - [Backend/Spring] - Spring boot 3.x OAuth2(Google, Kakao) 로그인/회원가입 구현 - Flutter&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1727868110226&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Spring boot 3.x OAuth2(Google, Kakao) 로그인/회원가입 구현 - Flutter&quot; data-og-description=&quot;▤ 목차 &amp;zwj;♂️ Flutter 개발자가 아니라 코드 작성부분은 다소 생략하였습니다.ㅠㅠ&amp;nbsp;&amp;nbsp;Flutter로 하는 OAuth2 로그인 방법Spring security로 OAuth2로그인을 구현 하려고 하면 보통 web으로 로그인 하는게&quot; data-og-host=&quot;keepbang.tistory.com&quot; data-og-source-url=&quot;https://keepbang.tistory.com/entry/Spring-boot-3x-flutter-OAuth2Google-Kakao-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84-1-flutter&quot; data-og-url=&quot;https://keepbang.tistory.com/entry/Spring-boot-3x-flutter-OAuth2Google-Kakao-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84-1-flutter&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bhfRGA/hyXaGQ14kq/2vhcvLc6hELPeHPG20Fi80/img.png?width=800&amp;amp;height=753&amp;amp;face=0_0_800_753,https://scrap.kakaocdn.net/dn/CFV83/hyXaAwvQev/yC1futGmzg72y0jsNAk7gk/img.png?width=800&amp;amp;height=753&amp;amp;face=0_0_800_753,https://scrap.kakaocdn.net/dn/bUMsUB/hyXaBa6w6r/o8jz3VSIIFUkDxfM1e0XpK/img.jpg?width=780&amp;amp;height=784&amp;amp;face=0_0_780_784&quot;&gt;&lt;a href=&quot;https://keepbang.tistory.com/entry/Spring-boot-3x-flutter-OAuth2Google-Kakao-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84-1-flutter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://keepbang.tistory.com/entry/Spring-boot-3x-flutter-OAuth2Google-Kakao-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84-1-flutter&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bhfRGA/hyXaGQ14kq/2vhcvLc6hELPeHPG20Fi80/img.png?width=800&amp;amp;height=753&amp;amp;face=0_0_800_753,https://scrap.kakaocdn.net/dn/CFV83/hyXaAwvQev/yC1futGmzg72y0jsNAk7gk/img.png?width=800&amp;amp;height=753&amp;amp;face=0_0_800_753,https://scrap.kakaocdn.net/dn/bUMsUB/hyXaBa6w6r/o8jz3VSIIFUkDxfM1e0XpK/img.jpg?width=780&amp;amp;height=784&amp;amp;face=0_0_780_784');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring boot 3.x OAuth2(Google, Kakao) 로그인/회원가입 구현 - Flutter&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;▤ 목차 &amp;zwj;♂️ Flutter 개발자가 아니라 코드 작성부분은 다소 생략하였습니다.ㅠㅠ&amp;nbsp;&amp;nbsp;Flutter로 하는 OAuth2 로그인 방법Spring security로 OAuth2로그인을 구현 하려고 하면 보통 web으로 로그인 하는게&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;keepbang.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그인/회원가입 API&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Flutter에서 호출하는 &lt;code&gt;로그인 / 회원가입 API&lt;/code&gt;를 살펴보겠습니다.&lt;br /&gt;이전글을 보셨다면 아시겠지만 사용한 API는 &lt;b&gt;한 개&lt;/b&gt;입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API의 프로세스는 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Flutter에서 provider에 따라 로그인을 진행하고 accessToken을 spring 서버로 전달&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;서버에서 accessToken으로 사용자 정보 요청&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;여기에서 만약 accessToken이 변형되었다면 사용자 정보를 못가져오고 실패하게 됨&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;사용자 정보가 성공적으로 조회되면 해당 사용자 데이터가 DB에 저장되어있는지 확인&lt;/span&gt;&lt;br /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;DB에 없을 경우 사용자 생성&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;토큰 발급 후 응답 보냄&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;위와 같은 로직으로 구성 되어있습니다. 그러면 하나하나 살펴 보겠습니다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Controller&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1727864228509&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

private final OAuthClient oAuthClient;

private final TokenCreate tokenCreate;

@PostMapping(&quot;/login/{provider}&quot;)
  public ResponseEntity&amp;lt;TokenResponse&amp;gt; login(
      @PathVariable(&quot;provider&quot;) SocialType type,
      @RequestBody TokenRequest request
  ) {

    OAuth2UserInfo oAuth2UserInfo = oAuthClient.getOAuth2UserInfo(type, request.code());

    TokenResponse response = tokenCreate.getOAuthAccessTokenByUserInfo(type,
        oAuth2UserInfo);
        
    return ResponseEntity.ok().body(response);
}

...&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;API의 전체적인 구조 입니다. &lt;code&gt;TokenRequest&lt;/code&gt;에는 accessToken이 들어가 있고 &lt;code&gt;oAuthClient.getAuth2UserInfo&lt;/code&gt; 메소드로 소셜 타입별 사용자 정보를 가져오게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;OAuthClient&lt;/code&gt;는 소셜 로그인에서 다른 서버와의 연동을 위한 컴포넌트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;TokenCreate&lt;/code&gt;는 토큰을 생성하는 기능이 담긴 컴포넌트입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;accessToken으로 사용자 정보 요청&lt;/h2&gt;
&lt;pre id=&quot;code_1727864540968&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class OAuthClient {
  private final WebClient webClient;

  @Value(&quot;${spring.security.oauth2.client.provider.google.user-info-uri}&quot;)
  private String googleUserUrl;

  @Value(&quot;${spring.security.oauth2.client.provider.kakao.user-info-uri}&quot;)
  private String KAKAO_USER_URL;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;OAuthClient&lt;/code&gt;에서 사용자 정보를 가져오는건 &lt;code&gt;WebClient&lt;/code&gt;를 사용했고 각 provider별 document에 있는 사용자 조회 API를 사용해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;kakao 사용자 정보 조회&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;google은 spring boot에 기본값으로 들어가 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1498&quot; data-origin-height=&quot;664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bua2Ni/btsJSrAwb3O/r85Om2XL3KkK9ycU7uHMpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bua2Ni/btsJSrAwb3O/r85Om2XL3KkK9ycU7uHMpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bua2Ni/btsJSrAwb3O/r85Om2XL3KkK9ycU7uHMpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbua2Ni%2FbtsJSrAwb3O%2Fr85Om2XL3KkK9ycU7uHMpk%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;596&quot; height=&quot;264&quot; data-origin-width=&quot;1498&quot; data-origin-height=&quot;664&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1727865946118&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
  private JsonNode getUserInfo(String url, String token) {
    Mono&amp;lt;JsonNode&amp;gt; response = webClient.get()
        .uri(url)
        .header(&quot;Authorization&quot;, &quot;Bearer &quot; + token)
        .retrieve()
        .bodyToMono(JsonNode.class);

    return response.block();
  }
  ...&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;token에 accessToken을 넣어서 요청하면 유효할 경우 사용자 정보를 가져올 수 있습니다. 가져온 사용자정보는 각 API 응답에따라 파싱해서 사용하면 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DB 데이터 확인 &amp;amp; 토큰 발급&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;tokenCreate.getOAuthAccessTokenByUserInfo&lt;/code&gt; 메소드를 사용하면 토큰을 발급 할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1727867640419&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

// 사용자 조회
Optional&amp;lt;User&amp;gt; oUser = userManager.findByEmailAndSocialType(userInfo.getEmail(), type);
User user = oUser.orElseGet(() -&amp;gt; addUser(type, userInfo));

// refreshToken 초기화
String refreshToken = this.createRefreshToken();
this.updateRefreshToken(user,refreshToken);

// 토큰 발급
return new TokenResponse(
    this.createAccessToken(user),
    refreshToken,
    user.getUserKey()
);

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;accessToken으로 가져온 사용자 정보에 email을 기준으로 사용자 존재 유무를 판단합니다. 일단 이부분은 전화번호로도 바꿀수 있을 것 같아요. 그 후 사용자가 존재하지 않으면 새로 DB에 저장을 합니다.&lt;/li&gt;
&lt;li&gt;refreshToken도 초기화를 해주고 기존에 존재하던 refreshToken을 업데이트 합니다.&lt;br /&gt;(로그아웃 후 다시 로그인을 하면 refreshToken 유효 기간이 늘어납니다)&lt;/li&gt;
&lt;li&gt;그 후 해당 사용자로 토큰을 만들고 응답으로 내보냅니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 로그인 및 회원가입 기능을 개발해 봤는데 로그인 방식이나 회원가입 방식이 다양해서 그 방법을 고민하느라 시간이 좀 더 걸린거같네요ㅠㅠ...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식이 맞는지는 모르겠지만 진행하면서 만약 취약점같은게 보이면 계속 적용해봐야할것 같습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/Spring</category>
      <category>Flutter</category>
      <category>oauth2</category>
      <category>oauth2 로그인</category>
      <category>spring boot</category>
      <category>spring boot3</category>
      <category>네이티브앱</category>
      <category>회원가입</category>
      <author>keepbang</author>
      <guid isPermaLink="true">https://keepbang.tistory.com/50</guid>
      <comments>https://keepbang.tistory.com/entry/Spring-boot-3x-OAuth2Google-Kakao-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84-2-API-%EA%B0%9C%EB%B0%9C#entry50comment</comments>
      <pubDate>Wed, 2 Oct 2024 20:25:16 +0900</pubDate>
    </item>
    <item>
      <title>Spring boot 3.x OAuth2(Google, Kakao) 로그인/회원가입 구현 1 - Flutter</title>
      <link>https://keepbang.tistory.com/entry/Spring-boot-3x-flutter-OAuth2Google-Kakao-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84-1-flutter</link>
      <description>&lt;div style=&quot;padding: 10px 20px 0px 20px; border-radius: 10px; margin: 1em 1em; border: 1px solid #ccd0d7; background-color: #f5f5f5; box-shadow: 1px 2px 3px 1px rgba(0,0,0,.1);&quot;&gt;
&lt;p style=&quot;border-bottom: 2px solid #ccd0d7;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;▤ 목차&lt;/b&gt;&lt;/p&gt;
&lt;script src=&quot;./images/jquery.toc.min.js&quot;&gt;&lt;/script&gt;
&lt;ul id=&quot;toc&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;decimal&quot;&gt;&lt;/ul&gt;
&lt;script type=&quot;text/javascript&quot;&gt; $(function(){  $(&quot;#toc&quot;).toc({content: &quot;.tt_article_useless_p_margin&quot;, headings: &quot;h2, h3&quot;}); });&lt;/script&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt; &amp;zwj;♂️ &lt;span style=&quot;color: #ee2323;&quot;&gt;Flutter 개발자가 아니라 코드 작성부분은 다소 생략하였습니다.ㅠㅠ&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Flutter로 하는 OAuth2 로그인 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring security로 OAuth2로그인을 구현 하려고 하면 보통 web으로 로그인 하는게 많이 보입니다. 하지만 &lt;code&gt;kakao developer&lt;/code&gt; 문서를 보면 네이티브 앱에서는 리다이렉트 방식을 사용 할 수 없다고 나옵니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bj2tE3/btsIQHRA1Pf/wNQsfKCSgfyZVsocxPP8wK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bj2tE3/btsIQHRA1Pf/wNQsfKCSgfyZVsocxPP8wK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bj2tE3/btsIQHRA1Pf/wNQsfKCSgfyZVsocxPP8wK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbj2tE3%2FbtsIQHRA1Pf%2FwNQsfKCSgfyZVsocxPP8wK%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;683&quot; height=&quot;181&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 공식 문서를 참고하여 &lt;code&gt;프론트(Flutter)&lt;/code&gt;와 &lt;code&gt;백엔드(Spring)&lt;/code&gt;의 역할을 나눠 개발하기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Google&lt;/code&gt;과 &lt;code&gt;Kakao&lt;/code&gt; 로그인 개발을 하기 위해 아래 두 문서를 참고했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[참고]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1722409309876&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Kakao Developers&quot; data-og-description=&quot;카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&quot; data-og-host=&quot;developers.kakao.com&quot; data-og-source-url=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter&quot; data-og-url=&quot;https://developers.kakao.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/IUFCL/hyWG0Jfcno/7v9NVF2RGtVZh4GakzXJmk/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/QWzGm/hyWKxekPq2/rEtCoc9umKPPauVOQothTk/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000,https://scrap.kakaocdn.net/dn/w2y0g/hyWG0bpEb3/YfAAWzIHsoyixkimV3Dsmk/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000&quot;&gt;&lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/IUFCL/hyWG0Jfcno/7v9NVF2RGtVZh4GakzXJmk/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/QWzGm/hyWKxekPq2/rEtCoc9umKPPauVOQothTk/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000,https://scrap.kakaocdn.net/dn/w2y0g/hyWG0bpEb3/YfAAWzIHsoyixkimV3Dsmk/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Kakao Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.kakao.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1722409366228&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;google_sign_in | Flutter package&quot; data-og-description=&quot;Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account.&quot; data-og-host=&quot;pub.dev&quot; data-og-source-url=&quot;https://pub.dev/packages/google_sign_in&quot; data-og-url=&quot;https://pub.dev/packages/google_sign_in&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/e3fjy/hyWGP8MTDP/XAKsXYNjbGvXmQirfpL4ck/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/EFxSg/hyWGTQTgEe/lHq5WhGjMeS73AjVST6fn0/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640&quot;&gt;&lt;a href=&quot;https://pub.dev/packages/google_sign_in&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://pub.dev/packages/google_sign_in&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/e3fjy/hyWGP8MTDP/XAKsXYNjbGvXmQirfpL4ck/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/EFxSg/hyWGTQTgEe/lHq5WhGjMeS73AjVST6fn0/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;google_sign_in | Flutter package&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;pub.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프론트 역할
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;access token&lt;/code&gt; 발급 후 api 호출&lt;/li&gt;
&lt;li&gt;&lt;code&gt;access token&lt;/code&gt; 만료 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;백엔드 역할
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;access token&lt;/code&gt;으로 사용자 정보 조회 후 로그인 / 회원가입 처리&lt;/li&gt;
&lt;li&gt;token 발급&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 내용을 토대로 시퀀스 다이어 그램을 그려봤습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1416&quot; data-origin-height=&quot;1334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddQrv0/btsIThKlAWa/nNbxHzLPSOAJ8E53KL9l2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddQrv0/btsIThKlAWa/nNbxHzLPSOAJ8E53KL9l2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddQrv0/btsIThKlAWa/nNbxHzLPSOAJ8E53KL9l2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddQrv0%2FbtsIThKlAWa%2FnNbxHzLPSOAJ8E53KL9l2K%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;585&quot; height=&quot;551&quot; data-origin-width=&quot;1416&quot; data-origin-height=&quot;1334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시퀀스 다이어그램 설명&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;[flutter]&lt;/code&gt; OAuth 로그인(Kakao, Google sdk 사용)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[spring]&lt;/code&gt; 로그인 후 받은 access_token으로 application(spring server)에 token 발급 api 요청&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;code&gt;[spring]&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;access_token으로 google 및 kakao 사용자 조회 api 호출&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;code&gt;[spring]&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;조회된 사용자로 회원 검증 및 임시 회원가입 및 토큰 발급&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[flutter]&lt;/code&gt; access_token 만료 처리&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Flutter 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;pubspec.yaml&lt;/code&gt; 파일에 sdk 파일을 추가합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722410901790&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies:
...
  kakao_flutter_sdk_user: ^1.2.1
  google_sign_in: ^5.0.7
  
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 소셜 로그인을 위한 &lt;code&gt;IOS/Android&lt;/code&gt; 세팅을 해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 아래 블로그 글을 참고하여 세팅해줬습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1722411120880&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Flutter] Kakao Login ① - 준비&quot; data-og-description=&quot;소셜 로그인 구현 세번째는 카카오 로그인 입니다. 세팅방법은 소셜 로그인중에서는 제일 간단하지 싶습니다. Kakao Developers 카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 &quot; data-og-host=&quot;dalgoodori.tistory.com&quot; data-og-source-url=&quot;https://dalgoodori.tistory.com/45&quot; data-og-url=&quot;https://dalgoodori.tistory.com/45&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bN03uk/hyWKKShOGY/unkeo637DmqMM70CEgC2ik/img.png?width=800&amp;amp;height=306&amp;amp;face=0_0_800_306,https://scrap.kakaocdn.net/dn/edLV3Y/hyWGYknnd6/3DW987XhlirJe2l1K0RZyk/img.png?width=800&amp;amp;height=306&amp;amp;face=0_0_800_306,https://scrap.kakaocdn.net/dn/Bolut/hyWGQNoRJt/uLXnfkKPzib33IDrebBmI0/img.png?width=2476&amp;amp;height=1496&amp;amp;face=0_0_2476_1496&quot;&gt;&lt;a href=&quot;https://dalgoodori.tistory.com/45&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dalgoodori.tistory.com/45&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bN03uk/hyWKKShOGY/unkeo637DmqMM70CEgC2ik/img.png?width=800&amp;amp;height=306&amp;amp;face=0_0_800_306,https://scrap.kakaocdn.net/dn/edLV3Y/hyWGYknnd6/3DW987XhlirJe2l1K0RZyk/img.png?width=800&amp;amp;height=306&amp;amp;face=0_0_800_306,https://scrap.kakaocdn.net/dn/Bolut/hyWGQNoRJt/uLXnfkKPzib33IDrebBmI0/img.png?width=2476&amp;amp;height=1496&amp;amp;face=0_0_2476_1496');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Flutter] Kakao Login ① - 준비&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;소셜 로그인 구현 세번째는 카카오 로그인 입니다. 세팅방법은 소셜 로그인중에서는 제일 간단하지 싶습니다. Kakao Developers 카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dalgoodori.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1722411134018&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Flutter] Google Login ① - 준비&quot; data-og-description=&quot;소셜 로그인 구현 두번째는 구글 로그인 입니다. 파이어베이스를 사용하면 훨씬 간단하지만 파이어베이스 없이 구현해보겠습니다. 먼저 프로젝트를 만들기 위해 Google Cloud Platform 에서 계정의 &quot; data-og-host=&quot;dalgoodori.tistory.com&quot; data-og-source-url=&quot;https://dalgoodori.tistory.com/43&quot; data-og-url=&quot;https://dalgoodori.tistory.com/43&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bXt08p/hyWKImDaAG/o983nhyIKvLgmjgAjguhx0/img.jpg?width=650&amp;amp;height=354&amp;amp;face=0_0_650_354,https://scrap.kakaocdn.net/dn/nsMj9/hyWGYLrdVe/fiDSfF4jMbkBZ1WJMMK0V1/img.jpg?width=650&amp;amp;height=354&amp;amp;face=0_0_650_354,https://scrap.kakaocdn.net/dn/ulWKH/hyWKI1eEXd/phLAWQbl101g0fikWMxsHK/img.jpg?width=662&amp;amp;height=903&amp;amp;face=0_0_662_903&quot;&gt;&lt;a href=&quot;https://dalgoodori.tistory.com/43&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dalgoodori.tistory.com/43&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bXt08p/hyWKImDaAG/o983nhyIKvLgmjgAjguhx0/img.jpg?width=650&amp;amp;height=354&amp;amp;face=0_0_650_354,https://scrap.kakaocdn.net/dn/nsMj9/hyWGYLrdVe/fiDSfF4jMbkBZ1WJMMK0V1/img.jpg?width=650&amp;amp;height=354&amp;amp;face=0_0_650_354,https://scrap.kakaocdn.net/dn/ulWKH/hyWKI1eEXd/phLAWQbl101g0fikWMxsHK/img.jpg?width=662&amp;amp;height=903&amp;amp;face=0_0_662_903');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Flutter] Google Login ① - 준비&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;소셜 로그인 구현 두번째는 구글 로그인 입니다. 파이어베이스를 사용하면 훨씬 간단하지만 파이어베이스 없이 구현해보겠습니다. 먼저 프로젝트를 만들기 위해 Google Cloud Platform 에서 계정의&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dalgoodori.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Flutter 로그인 코드 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;카카오 developer 문서&lt;/a&gt;나 &lt;a href=&quot;https://pub.dev/documentation/google_sign_in/latest/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;구글 SDK api 문서&lt;/a&gt;를 참고하여 코드를 작성해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722414618905&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;Future&amp;lt;void&amp;gt; googleLogin() async {
    var googleLoginHelper = new GoogleLoginHelper();

    googleLoginHelper.login()
    .then((accessToken) {
      log('accessToken: $accessToken');

      if (accessToken == null) {
        EasyLoading.showError('로그인/회원가입에 실패했습니다.',
            duration: const Duration(seconds: 3),
            maskType: EasyLoadingMaskType.black,
            dismissOnTap: false);
        return;
      }

      autoLogin(accessToken, LoginPlatform.GOOGLE)
          .then((value) =&amp;gt; afterLogin(value))
          .then((value) =&amp;gt; googleLoginHelper.logout(accessToken));

    });
  }

Future&amp;lt;void&amp;gt; kakaoLogin() async {
    var kakaoLoginHelper = new KakaoLoginHelper();

    kakaoLoginHelper.login().then((accessToken) {
      log('accessToken: $accessToken');

      if (accessToken == null) {
        EasyLoading.showError('로그인/회원가입에 실패했습니다.',
            duration: const Duration(seconds: 3),
            maskType: EasyLoadingMaskType.black,
            dismissOnTap: false);
        return;
      }

      autoLogin(accessToken, LoginPlatform.KAKAO)
          .then((value) =&amp;gt; afterLogin(value))
          .then((value) =&amp;gt; kakaoLoginHelper.logout());
    });
}
  
Future&amp;lt;AuthModel&amp;gt; autoLogin(String token, LoginPlatform platform) {
    return repo.login(code: token, platform: platform);
}

// repo ...

  Future&amp;lt;AuthModel&amp;gt; login({required String code, required LoginPlatform platform}) async {
    try {
      Response response = await apiClient.request(
        '/auth/login/${platform.name}',
        options: Options(method: 'POST'),
        data: {'code': code},
      );
      return AuthModel.fromJson(response.data['data']);
    } on DioException catch (ex) {
      log(ex.response.toString());
      String errorMessage = json.decode(ex.response.toString())[&quot;errorMessage&quot;];
      throw Exception(errorMessage);
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;accessToken&lt;/code&gt;이 탈취 당할 경우를 대비해서 로그인이 성공했을 때 logout을 통해 &lt;code&gt;accessToken&lt;/code&gt;을 만료시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 &lt;code&gt;Controller&lt;/code&gt; 쪽 코드를 작성해주고 kakao, google 각각 &lt;code&gt;helper 클래스&lt;/code&gt;를 생성하여 로그인을 할 수 있도록 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722415050787&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class KakaoLoginHelper {

  Future&amp;lt;String&amp;gt; getKakaoKeyHash() async {
    var key = await KakaoSdk.origin;
    return key;
  }

  Future&amp;lt;String?&amp;gt; login() async {
    var key = await getKakaoKeyHash();
    log('kakaologinHelper + $key');
    if (await isKakaoTalkInstalled()) {
      try {
        OAuthToken token = await UserApi.instance.loginWithKakaoTalk();
        log('카카오톡으로 로그인 성공 : ${token.toString()}');
        return token.accessToken;//토큰 정보
      } catch (error) {
        print('카카오톡으로 로그인 실패 $error');

        // 사용자가 카카오톡 설치 후 디바이스 권한 요청 화면에서 로그인을 취소한 경우,
        // 의도적인 로그인 취소로 보고 카카오계정으로 로그인 시도 없이 로그인 취소로 처리 (예: 뒤로 가기)
        if (error is PlatformException &amp;amp;&amp;amp; error.code == 'CANCELED') {
          return null;
        }
        // 카카오톡에 연결된 카카오계정이 없는 경우, 카카오계정으로 로그인
        try {
          OAuthToken token = await UserApi.instance.loginWithKakaoAccount();

          log('카카오계정으로 로그인 성공 : ${token.toString()}');
          return token.accessToken; //토큰 정보
        } catch (error) {
          print('카카오계정으로 로그인 실패 $error');
        }
      }
    } else {
      try {
        OAuthToken token = await UserApi.instance.loginWithKakaoAccount();
        log('카카오계정으로 로그인 성공 : ${token.toString()}');
        return token.accessToken; //토큰 정보

      } catch (error) {
        print('카카오계정으로 로그인 실패 $error');
      }
    }
    return null;
  }

  Future&amp;lt;void&amp;gt; logout() async {
    try {
      UserApi.instance.logout();
    } catch (error) {
      print('카카오 로그인 실패 $error');
    }
  }

}

...

class GoogleLoginHelper {

  final GoogleSignIn googleSignIn = GoogleSignIn();

  Future&amp;lt;String?&amp;gt; login() async {
    try {
      final GoogleSignInAccount? googleSignInAccount = await googleSignIn
          .signIn();

      final GoogleSignInAuthentication googleSignInAuthentication = await googleSignInAccount!
          .authentication;

      print(googleSignInAuthentication.accessToken);

      return googleSignInAuthentication.accessToken;
    } catch (error) {
      print(error);
    }
  }

  Future&amp;lt;void&amp;gt; logout(String? accessToken) async {
    await revokeToken(accessToken!);

    await googleSignIn.signOut();
    print('User signed out');
  }

  Future&amp;lt;void&amp;gt; revokeToken(String token) async {
    final response = await http.post(
      Uri.parse('https://oauth2.googleapis.com/revoke'),
      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
      body: 'token=$token',
    );

    if (response.statusCode == 200) {
      print('Token revoked successfully');
    } else {
      print('Failed to revoke token');
    }
  }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flutter는 여기까지 마무리하고 login 함수에서 호출하고 있는 API를 다음 블로그 글에서 만들어 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음글&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://keepbang.tistory.com/entry/Spring-boot-3x-OAuth2Google-Kakao-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84-2-API-%EA%B0%9C%EB%B0%9C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2024.10.02 - [Backend/Spring] - Spring boot 3.x OAuth2(Google, Kakao) 로그인/회원가입 구현 2 - API 개발&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1727868356728&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Spring boot 3.x OAuth2(Google, Kakao) 로그인/회원가입 구현 2 - API 개발&quot; data-og-description=&quot;▤ 목차&amp;nbsp;이전글2024.07.31 - [Backend/Spring] - Spring boot 3.x OAuth2(Google, Kakao) 로그인/회원가입 구현 - Flutter&amp;nbsp;Spring boot 3.x OAuth2(Google, Kakao) 로그인/회원가입 구현 - Flutter▤ 목차 &amp;zwj;♂️ Flutter 개발자가 &quot; data-og-host=&quot;keepbang.tistory.com&quot; data-og-source-url=&quot;https://keepbang.tistory.com/entry/Spring-boot-3x-OAuth2Google-Kakao-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84-2-API-%EA%B0%9C%EB%B0%9C&quot; data-og-url=&quot;https://keepbang.tistory.com/entry/Spring-boot-3x-OAuth2Google-Kakao-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84-2-API-%EA%B0%9C%EB%B0%9C&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bCzMg6/hyXaxNkkev/F20pvhqcyXRDODObtDEvQ1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bsrohW/hyXaxNkkdX/fDi9h3BvQpanAAK5FDTyOK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bLEzii/hyXaJNLsa2/kMNSiNmGBk3eBSbzuhs9K1/img.jpg?width=780&amp;amp;height=784&amp;amp;face=0_0_780_784&quot;&gt;&lt;a href=&quot;https://keepbang.tistory.com/entry/Spring-boot-3x-OAuth2Google-Kakao-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84-2-API-%EA%B0%9C%EB%B0%9C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://keepbang.tistory.com/entry/Spring-boot-3x-OAuth2Google-Kakao-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84-2-API-%EA%B0%9C%EB%B0%9C&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bCzMg6/hyXaxNkkev/F20pvhqcyXRDODObtDEvQ1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bsrohW/hyXaxNkkdX/fDi9h3BvQpanAAK5FDTyOK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bLEzii/hyXaJNLsa2/kMNSiNmGBk3eBSbzuhs9K1/img.jpg?width=780&amp;amp;height=784&amp;amp;face=0_0_780_784');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring boot 3.x OAuth2(Google, Kakao) 로그인/회원가입 구현 2 - API 개발&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;▤ 목차&amp;nbsp;이전글2024.07.31 - [Backend/Spring] - Spring boot 3.x OAuth2(Google, Kakao) 로그인/회원가입 구현 - Flutter&amp;nbsp;Spring boot 3.x OAuth2(Google, Kakao) 로그인/회원가입 구현 - Flutter▤ 목차 &amp;zwj;♂️ Flutter 개발자가&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;keepbang.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/Spring</category>
      <category>Flutter</category>
      <category>oauth2</category>
      <category>oauth2 로그인</category>
      <category>spring boot</category>
      <category>spring boot3</category>
      <author>keepbang</author>
      <guid isPermaLink="true">https://keepbang.tistory.com/49</guid>
      <comments>https://keepbang.tistory.com/entry/Spring-boot-3x-flutter-OAuth2Google-Kakao-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84-1-flutter#entry49comment</comments>
      <pubDate>Wed, 31 Jul 2024 15:23:19 +0900</pubDate>
    </item>
    <item>
      <title>Java 불변 객체(immutable object)</title>
      <link>https://keepbang.tistory.com/entry/Java-%EB%B6%88%EB%B3%80-%EA%B0%9D%EC%B2%B4immutable-object</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;불변 객체?&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;객체의 상태가 변경되지 않는 객체&lt;/b&gt;. 변경이 불가능한 객체를 말한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Java&lt;/code&gt;에서는 &lt;code&gt;final 키워드&lt;/code&gt;를 사용하여 불변 객체를 만들 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;불변 객체의 사용 이유&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체가 변경되어도 항상 새로운 객체를 반환하기 때문에 여러 쓰레드가 동시에 한 객체에 접근해도 안전하다.&lt;/li&gt;
&lt;li&gt;디버깅이나 예측 가능성이 높아지고 메서드 실행시 사이드 이펙트를 예방할 수 있다.&lt;/li&gt;
&lt;li&gt;변경되지 말아야할 필드값이 존재할 경우 사용가능(JPA)&lt;/li&gt;
&lt;li&gt;함수형 프로그래밍으로 사용 될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;코드로 알아보는 불변 객체&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;Person.class&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1715844101616&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Person {
    public String name;
    public Age age;

    public Person(String name, Age age) {
        this.name = name;
        this.age = age;
    }

    public Age getAge() {
        return age;
    }

    @Override
    public String toString() {
        return &quot;Person{&quot; +
                &quot;name='&quot; + name + '\'' +
                &quot;, age=&quot; + age +
                '}';
    }
}&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;i&gt;&lt;b&gt;Age.class&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1715844117476&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Age {
    private int age;

    public Age(int age) {
        this.age = age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return &quot;Age{&quot; +
                &quot;age=&quot; + age +
                '}';
    }
}&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;i&gt;&lt;b&gt;Main.class&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1715844137456&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {

    public static void main(String[] args) {
        Age age = new Age(20);
        Person kim = new Person(&quot;kim&quot;, age);
        Person lee = new Person(&quot;lee&quot;, age);
        System.out.println(&quot;kim = &quot; + kim);
        System.out.println(&quot;lee = &quot; + lee);

        lee.getAge().setAge(21);
        System.out.println(&quot;kim = &quot; + kim);
        System.out.println(&quot;lee = &quot; + lee);

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Person클래스&lt;/b&gt;는 불변객체가 아니기때문에 중간에 한 객체의 나이를 변경했을때 다른 객체의 나이도 같이 변경이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;kim&lt;/code&gt;과 &lt;code&gt;lee&lt;/code&gt;는 같은 객체(&lt;code&gt;age&lt;/code&gt;)를 생성자를 통해 받았다. 자바에서는 전달받은 변수의 값을 복사(&lt;code&gt;call by value&lt;/code&gt;)해서 가져오는데 만약 객체일 경우 &lt;b&gt;그 객체의 참조값을 복사하여 가져온다&lt;/b&gt;. 그러므로 &lt;code&gt;age&lt;/code&gt;의 값인 &lt;code&gt;20&lt;/code&gt;이 들어가는게 아닌 &lt;code&gt;age&lt;/code&gt; 인스턴스 변수의 참조값이 들어가게 되고 그 참조값을 참조하게 된다. 그렇기 때문에 해당 참조값이 기르키는 곳의 값을 두 객체가 서로 공유하기 때문에 때문에 &lt;code&gt;21&lt;/code&gt;로 변경했을 때 두 객체 모두 변경된 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dh6Uxq/btsHqZ6wduE/kJboDvOuzD25e2uP0ubWlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dh6Uxq/btsHqZ6wduE/kJboDvOuzD25e2uP0ubWlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dh6Uxq/btsHqZ6wduE/kJboDvOuzD25e2uP0ubWlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdh6Uxq%2FbtsHqZ6wduE%2FkJboDvOuzD25e2uP0ubWlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;774&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 변경의 전파를 막고 사이드 이펙트가 발생하지 않게 하려면 어떻게 해야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나이를 입력할때 &lt;b&gt;서로 다른 객체를 새로 생성&lt;/b&gt;해서 추가하면된다.&lt;/p&gt;
&lt;pre id=&quot;code_1715844581114&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Person kim = new Person(&quot;kim&quot;, new Age(20));
Person lee = new Person(&quot;lee&quot;, new Age(20));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 두 객체를 같은 값을 가지지만 서로 다른 참조값을 가지고 있으므로 사이드 이펙트가 발생하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 개발을 하다보면 모두가 이런식으로 개발을 하는게 아니다. 그렇기 때문에 &lt;b&gt;컴파일 단계에서부터 오류를 발생&lt;/b&gt;시켜 사이드 이펙트를 막아야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bb7YxZ/btsHrPbisTF/qImvqYMkVcsdk43Qjkdp80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bb7YxZ/btsHrPbisTF/qImvqYMkVcsdk43Qjkdp80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bb7YxZ/btsHrPbisTF/qImvqYMkVcsdk43Qjkdp80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbb7YxZ%2FbtsHrPbisTF%2FqImvqYMkVcsdk43Qjkdp80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;872&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 필드값을 &lt;code&gt;final&lt;/code&gt;로 선언하여 &lt;code&gt;Age&lt;/code&gt; 클래스를 불변객체로 만들어주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 필드값 수정이 불가능하게되고 컴파일단계에서 &lt;code&gt;setter&lt;/code&gt;를 사용할 수 없게된다. 값의 변경은 새로운 객체를 반환해 줌으로써 반환된 객체로 변경을 시도 할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1715844979658&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ImmutableAge {
    private final int age;

    public ImmutableAge(int age) {
        this.age = age;
    }

    // 새로운 객체를 반환함.
    public ImmutableAge withAge(int newAge) {
        return new ImmutableAge(newAge);
    }

    @Override
    public String toString() {
        return &quot;Age{&quot; +
                &quot;age=&quot; + age +
                '}';
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1715845461580&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) {
        ImmutableAge immutableAge = new ImmutableAge(20);
        Person kim = new Person(&quot;kim&quot;, immutableAge);
        Person lee = new Person(&quot;lee&quot;, immutableAge);
        System.out.println(&quot;kim = &quot; + kim);
        System.out.println(&quot;lee = &quot; + lee);

        lee.setAge(21);
        System.out.println(&quot;kim = &quot; + kim);
        System.out.println(&quot;lee = &quot; + lee);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSYgXR/btsHq5r8yzv/zMePEkqXcqeFDcAx9okfc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSYgXR/btsHq5r8yzv/zMePEkqXcqeFDcAx9okfc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSYgXR/btsHq5r8yzv/zMePEkqXcqeFDcAx9okfc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSYgXR%2FbtsHq5r8yzv%2FzMePEkqXcqeFDcAx9okfc1%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;567&quot; height=&quot;200&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불변 객체를 사용해서 명확한 의미와 기능의 전달을 할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Person&lt;/code&gt; 클래스에 &lt;code&gt;set&lt;/code&gt; 메서드를 사용하지 않고 &lt;code&gt;Person&lt;/code&gt; 객체를 반환하는 메소드를 사용 할 수도 있다. 이럴 경우 반환된 객체를 대입하는 로직이 필요하다.&lt;/p&gt;
&lt;pre id=&quot;code_1715845964798&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Person {

...

    public Person withAge(int newAge) {
        return new Person(name, new ImmutableAge(newAge));
    }
    
...
}

...

lee = lee.withAge(21);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 불변 객체의 새로운 객체를 반환하는 형태는 함수형 프로그래밍에서 자주 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 사용자목록의 나이를 특정값으로 변경하는 로직이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1715846279547&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// given
Person person1 = new Person(&quot;person1&quot;, new ImmutableAge(20));
Person person2 = new Person(&quot;person2&quot;, new ImmutableAge(20));
Person person3 = new Person(&quot;person3&quot;, new ImmutableAge(20));

List&amp;lt;Person&amp;gt; selectedList = List.of(person1, person2, person3);

// when
List&amp;lt;Person&amp;gt; changedList = selectedList.stream()
        .map(person -&amp;gt; person.withAge(21))
        .toList();

// then
System.out.println(&quot;변경 전========&quot;);
selectedList.forEach(System.out::println);

System.out.println(&quot;변경 후========&quot;);
changedList.forEach(System.out::println);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 객체를 반환하지 않는다면 &lt;code&gt;map&lt;/code&gt;에서 새로운 객체를 생성하는 로직을 작성해야 하지만 객체를 반환하기때문에 &lt;code&gt;람다식&lt;/code&gt;으로 작성 할 수 있게되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;불변 설계가 필요한 경우&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 클래스를 불변으로 만든다면 안정적인 코딩이 가능하지만 코드가 복잡해 질수도있고 여러 사람들과 작업을 진행 할 대 제약사항이 많이 발생할 수 있다. 그렇기 때문에 불변 설계가 필요한지 고민해보고 필요한곳에 사용하는것이 중요하다.&lt;/p&gt;</description>
      <category>Backend/JAVA</category>
      <category>immutable</category>
      <category>Immutable Object</category>
      <category>java</category>
      <category>불변</category>
      <category>불변객체</category>
      <category>함수형프로그래밍</category>
      <author>keepbang</author>
      <guid isPermaLink="true">https://keepbang.tistory.com/46</guid>
      <comments>https://keepbang.tistory.com/entry/Java-%EB%B6%88%EB%B3%80-%EA%B0%9D%EC%B2%B4immutable-object#entry46comment</comments>
      <pubDate>Thu, 16 May 2024 17:22:17 +0900</pubDate>
    </item>
    <item>
      <title>이커머스 서비스에서 동시성 문제 처리해보기</title>
      <link>https://keepbang.tistory.com/entry/%EC%9D%B4%EC%BB%A4%EB%A8%B8%EC%8A%A4-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90%EC%84%9C-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%EC%B2%98%EB%A6%AC%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
      <description>&lt;h1&gt;&lt;b&gt;동시성이란?&lt;/b&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;경쟁 상태(Race Condition) : 두 개 이상의 스레드가 동시에 공유 자원에 접근하고 수정을 시도할 때 발생한다. 예상치 못한 결과가 발생(DB 일관성이 깨짐)하거나 잘못된 데이터를 조회할 수 있다.&lt;/li&gt;
&lt;li&gt;교착 상태(Deadlock) : 두 개 이상의 스레드가 서로 다른 자원을 기다리면서 대기하고 있는 상황을 말한다. 서로 다른 스레드가 소유한 자원을 기다리며 멈추어 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;이커머스 서비스에서 발생 할 수 있는 동시성 문제&lt;/b&gt;&lt;/h1&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;재고 갱신시 동시성 문제 : 여러 사용자가 동시에 상품을 구매할 경우, 재고 갱신 과정에서 동시성 문제가 발생할 수 있다. 10명의 사용자가 동시에 같은 상품을 구매할때 상품 재고가 5개밖에 없다면 5명의 사용자는 구매가 안되거나 환불 처리가 되야한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;포인트 사용시 동시성 문제 : 하나의 사용자 계정으로 여러 상품을 주문할 경우 포인트 차감 과정에서 동시성 문제가 발생할 수 있다. 잔고가 부족하면 주문이 실패해야한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;장바구니 추가 동시성 문제 : 만약 중복된 요청이 들어올 경우 들어온 순서대로 요청이 처리 되어야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;동시성 문제 해결 방법&lt;/b&gt;&lt;/h1&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;b&gt;Lock 사용&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Mysql을 사용할 경우 update을 할 때 베타락이 걸리게 된다. 베타락이 걸리게 되면 다른 트랜젝션에서 읽기/쓰기 작업 모두 불가능 함으로 update문으로 동시성 문제를 처리할 수 있다. 대신, 읽기와 쓰기 모두 락이 걸리기때문에 작업이 오래걸릴경우 다른 기능에서 해당 데이터를 읽을때 timeout이 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;낙관적 락&lt;/b&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;데이터에 락을 거는것이 아닌 읽는 시점과 수정 시점의 version을 확인해서 동시성문제를 해결하는 방식으로 성능적으로 좋다고할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비관적 락&lt;/b&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;데이터를 락을 걸어서 다른 트랜젝션에서 접근이 불가능하기 때문에 만약 해당 데이터가 여러 기능에서 사용하고 있다면 요청에 대한 응답시간이 길어질 수 있다. -&amp;gt; 응답시간이 길어지면 해당 서비스를 사용하는 고객의 이탈이 발생하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분산 락&lt;/b&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의 Redisson을 통해 락을 획득하고 반납하면서 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;DB가 아닌 redis(또는 기타 다른 도구)를 사용함으로써 DB에 부하가 적어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&lt;br /&gt;&lt;b&gt;재고 차감 테스트 진행&lt;/b&gt;&lt;/h1&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;b&gt;테스트 시나리오&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상품 100개 추가&lt;/li&gt;
&lt;li&gt;101명이 동일한 상품 구매&lt;/li&gt;
&lt;li&gt;재고는 0이 되고 실패한 주문이 한개가 존재해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;void inventory_concurrency_test() throws Exception {
    // given
    long productId = 1L;
    int numberOfThreads = 101;
    ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);
    CountDownLatch latch = new CountDownLatch(numberOfThreads);

    // when - 주문
    ConcurrentLinkedQueue&amp;lt;Integer&amp;gt; failedOrder = new ConcurrentLinkedQueue&amp;lt;&amp;gt;();
    for (int i = 0; i &amp;lt; numberOfThreads; i++) {
      int finalI = i;
      executorService.execute(() -&amp;gt; {
        try {
          OrderItemRequest request = new OrderItemRequest(
              1L,
              1L,
              1
          );

          inventoryService.orderItemDeduction(request);

        } catch (Exception e) {
          failedOrder.add(finalI);
        }
        latch.countDown();
      });
    }

    latch.await();
    // then
    ProductResponse productResponse = getProductResponse(productId);
    assertThat(productResponse.stock()).isZero();
    assertThat(failedOrder).hasSize(1);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;[결과]&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img_2.png&quot; data-origin-width=&quot;621&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dn9vtf/btsHgCXc0Rv/inh4eLp6lc9Iwb4XKQLoj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dn9vtf/btsHgCXc0Rv/inh4eLp6lc9Iwb4XKQLoj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dn9vtf/btsHgCXc0Rv/inh4eLp6lc9Iwb4XKQLoj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdn9vtf%2FbtsHgCXc0Rv%2Finh4eLp6lc9Iwb4XKQLoj1%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;621&quot; height=&quot;261&quot; data-filename=&quot;img_2.png&quot; data-origin-width=&quot;621&quot; data-origin-height=&quot;261&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상품 개수가 남아있으면 안되는데 87개가 남아있다.&lt;/li&gt;
&lt;/ul&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;b&gt;낙관적락 적용&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;락을 적용할 entity에 version을 추가하고 사용하는 메서드에 Lock 어노테이션을 적용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// entity
//....
@Version
private Long version;
//....

// JpaRepository
@Lock(LockModeType.OPTIMISTIC)
Optional&amp;lt;Inventory&amp;gt; findByProductId(Long productId);
&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;i&gt;&lt;b&gt;[결과]&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img_3.png&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZxbvU/btsHcIkd7t6/9luJwk9ctYNdo6GksqykAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZxbvU/btsHcIkd7t6/9luJwk9ctYNdo6GksqykAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZxbvU/btsHcIkd7t6/9luJwk9ctYNdo6GksqykAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZxbvU%2FbtsHcIkd7t6%2F9luJwk9ctYNdo6GksqykAk%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;626&quot; height=&quot;212&quot; data-filename=&quot;img_3.png&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;212&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재고가 하나도 감소하지 않았다.(정상 동작하지 않음...)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;비관적락 사용&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;version을 사용하지 않고 Lock 어노테이션만 적용해서 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// JpaRepository
@Lock(LockModeType.PESSIMISTIC_READ)
Optional&amp;lt;Inventory&amp;gt; findByProductId(Long productId);&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;i&gt;&lt;b&gt;[결과]&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img_4.png&quot; data-origin-width=&quot;598&quot; data-origin-height=&quot;139&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dXSUPy/btsHe56RPtS/PanZgyT7KbfGSp7zl9UH0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dXSUPy/btsHe56RPtS/PanZgyT7KbfGSp7zl9UH0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dXSUPy/btsHe56RPtS/PanZgyT7KbfGSp7zl9UH0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdXSUPy%2FbtsHe56RPtS%2FPanZgyT7KbfGSp7zl9UH0K%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;598&quot; height=&quot;139&quot; data-filename=&quot;img_4.png&quot; data-origin-width=&quot;598&quot; data-origin-height=&quot;139&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;분산락 사용&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비관적락을 사용하게 되면 데이터에 대기상태가 발생되어 DB 부하가 발생하게되는데 이런 부분을 DB가 아닌 redis등을 사용해서 부하를 분산 할 수 있다.&lt;/li&gt;
&lt;li&gt;java에서는 redisson을 사용해서 분산락을 사용 할 수 있다.&lt;/li&gt;
&lt;li&gt;키를 사용해서 락을 획득하고 비즈니스로직을 수행한 다음 락을 반환하는 프로세스를 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// dependency 추가
// https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter
implementation 'org.redisson:redisson-spring-boot-starter:3.29.0'

// spring aop를 사용하여 DistributedLock 어노테이션을 사용하는 메소드에 Lock을 건다.
// 파라미터로 lockName을 받아서 lock key로 사용한다.
@Around(&quot;@annotation(com.hhplus.commerce.app.common.redis.DistributedLock)&quot;)
public Object lock(final ProceedingJoinPoint joinPoint) throws Throwable {
  MethodSignature signature = (MethodSignature) joinPoint.getSignature();
  Method method = signature.getMethod();
  DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);

  String key = REDISSON_LOCK_PREFIX + CustomSpringELParser.getDynamicValue(signature.getParameterNames(), joinPoint.getArgs(), distributedLock.key());
  RLock rLock = redissonClient.getLock(key);  // (1)

  try {
    boolean available = rLock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), distributedLock.timeUnit());  // (2)
    if (!available) {
      return false;
    }

    return aopForTransaction.proceed(joinPoint);  // (3)
  } catch (InterruptedException e) {
    throw new InterruptedException();
  } finally {
    try {
      rLock.unlock();   // (4)
    } catch (IllegalMonitorStateException e) {
      log.info(&quot;Redisson Lock Already UnLock {} {}&quot;, method.getName(), key
      );
    }
  }
}

// DistributedLock 사용 부분
@DistributedLock(key = &quot;#lockName&quot;)
public void orderItemDeduction(String lockName, OrderItemRequest request) {
  // 비즈니스 로직
}
&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;i&gt;&lt;b&gt;[결과]&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1356&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OkR4b/btsHfYflVqr/BkXhigSrk16lHswKLtXyRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OkR4b/btsHfYflVqr/BkXhigSrk16lHswKLtXyRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OkR4b/btsHfYflVqr/BkXhigSrk16lHswKLtXyRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOkR4b%2FbtsHfYflVqr%2FBkXhigSrk16lHswKLtXyRk%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;1356&quot; height=&quot;322&quot; data-origin-width=&quot;1356&quot; data-origin-height=&quot;322&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;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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/Spring</category>
      <category>Database</category>
      <category>db락</category>
      <category>MySQL</category>
      <category>Redisson</category>
      <category>낙관적락</category>
      <category>동시성</category>
      <category>락</category>
      <category>분산락</category>
      <category>비관적락</category>
      <category>테스트</category>
      <author>keepbang</author>
      <guid isPermaLink="true">https://keepbang.tistory.com/45</guid>
      <comments>https://keepbang.tistory.com/entry/%EC%9D%B4%EC%BB%A4%EB%A8%B8%EC%8A%A4-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90%EC%84%9C-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%EC%B2%98%EB%A6%AC%ED%95%B4%EB%B3%B4%EA%B8%B0#entry45comment</comments>
      <pubDate>Wed, 8 May 2024 11:10:47 +0900</pubDate>
    </item>
    <item>
      <title>Gradle + Nexus Repository library 사용 / Nexus 개인정보 암호화</title>
      <link>https://keepbang.tistory.com/entry/Gradle-Nexus-Repository-library-%EC%82%AC%EC%9A%A9-Nexus-%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4-%EC%95%94%ED%98%B8%ED%99%94</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring으로 개발을 하다보면 공통된 기능들을 따로 모아서 여러 프로젝트에 사용한다. 그럴때 많이 사용하는게 Nexus이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 아래 두가지에 대해 알아볼려고 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Nexus 개인정보 암호화 하는 방법&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Nexus에 올린 library를 가져와서 사용하는 방법&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개발환경&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;macOS(M2 Pro)&lt;br /&gt;IntelliJ IDEA&lt;br /&gt;gradle 8.1.1&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;전체 프로젝트에 적용이 된다.&lt;/li&gt;
&lt;li&gt;패스워드나 아이디, url이 변경될때마다 값을 바꿔줘야한다.&lt;/li&gt;
&lt;li&gt;프로젝트를 처음 받는 사람은 설정하기 까다롭다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그냥 이런 방법도 있구나~~하는 마음으로 봐주시길 바랍니다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1.&amp;nbsp; Nexus 개인정보 암호화 하는 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nexus library를 사용하기 위해서 &lt;code&gt;build.gradle&lt;/code&gt;에 nexus정보를 입력해서 사용게 되는데 private한 정보가 누출될 가능성이 있으므로 암호화하여 관리하는 방안이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gradle에서 nexus library를 가져오기 위해서 아래 코드블럭처럼 &lt;code&gt;build.gradle&lt;/code&gt;을 작성해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1710381356821&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;buildscript {

	...

    repositories {
        gradlePluginPortal()
        maven {
            url &quot;https://plugins.gradle.org/m2/&quot;
        }
        maven {
            url &quot;$nexusBaseUrl/maven-releases/&quot;
            allowInsecureProtocol = true // nexus url이 http일 경우 추가
            credentials {
                username = &quot;$nexusId&quot;
                password = &quot;$nexusPassword&quot;
            }
        }
    }
    
    dependencies {
    	classpath 'com.keepbang:common-library:1.0.0'
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;nexusBaseUrl&lt;/code&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;code&gt;nexusId&lt;/code&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;code&gt;nexusPassword&lt;/code&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;는 &lt;b&gt;global&lt;/b&gt;로 관리하여 코드상에 노출되는 개인정보는 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;global&lt;/b&gt;로 관리되는 정보의 위치는 &lt;code&gt;macOS&lt;/code&gt; 기준 &lt;code&gt;~/.gradle&lt;/code&gt;에 &lt;code&gt;init.gradle&lt;/code&gt; 파일을 생성해서 값을 입력해두면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1710383903616&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;init.gradle 파일

allprojects {
	project.ext.set('nexusBaseUrl', 'https://keepbang.nexus.co.kr/repository')
	project.ext.set('nexusId', 'nexusId')
	project.ext.set('nexusPassword', 'nexusPassword')
}&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;이렇게 하면 모든 gradle file에서 위에 입력한 nexus 정보들을 가져가 쓸 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 위와 같이 사용하면 프로젝트 코드에 직접적인 노출은 피할 수 있지만 저기에 적혀있는 개인 정보도 가리고 싶어졌다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;gradle 자격증명 플러그인으로 Nexus 정보 암호화하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어를 사용하여 gradle 프로젝트를 생성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1710384935121&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;% mkdir gradle-credentials-plugin-tutorial
% cd gradle-credentials-plugin-tutorial
% gradle init&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;build.gradle&lt;/code&gt;, &lt;code&gt;setting.gradle&lt;/code&gt;에 자격증명 플러그인을 추가해야&amp;nbsp;&lt;code&gt;addCredentials&lt;/code&gt;명령어를 사용 할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1710387974136&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id 'nu.studer.credentials' version '3.0'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;gradle addCredentials&lt;/code&gt; 명령어를 사용해서 &lt;code&gt;~/.gradle&lt;/code&gt; 디렉토리에 &lt;code&gt;gradle.encrypted.properties&lt;/code&gt; 파일로 입력한 내용을 암호화해서 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;암호화된 파일의 값을 사용하기 위해서 일단 &lt;code&gt;init.gradle&lt;/code&gt; 파일도 아래처럼 변경해 둔다.&lt;/p&gt;
&lt;pre id=&quot;code_1710388113074&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gradle addCredentials --key nexusUrl --value 'https://keepbang.nexus.co.kr/repository'
gradle addCredentials --key nexusId --value 'nexusId'
gradle addCredentials --key nexusPassword --value 'nexusPassword'

[gradle.encrypted.properties]
nexusUrl=ZlSkq1CQaJUSUofILEU/dr0l0qL2u2qWB3QVno9dHgulM3v3rVBxG5cWtU7fnAVW
nexusId=MBz7bWcyIuz29IxmqXmfnA\=\=
nexusPassword=8IYzQf9R016gbkzuzVCcFg\=\=

[init.gradle]
allprojects {
	project.ext.set('nexusBaseUrl', gradle.ext.nexusUrl)
	project.ext.set('nexusId', gradle.ext.nexusId)
	project.ext.set('nexusPassword', gradle.ext.nexusPassword)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2.&amp;nbsp;&lt;/b&gt;&lt;b&gt;Nexus에 올린 library를 가져와서 사용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 이미 library를 가져와서 사용하는 방법을 적었지만 암호화된 정보를 사용하기 위해선 따로 설정을 추가해줘야한다.&lt;/p&gt;
&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;settings.gradle&lt;/code&gt; 파일에 암호화된 키값을 가져와서 gradle 변수로 등록만 해주면된다.&lt;/p&gt;
&lt;pre id=&quot;code_1710391651498&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[setting.gradle]

...

plugins {
    id 'nu.studer.credentials' version '3.0'
}
ext {
    def nexusUrl = credentials.forKey('nexusUrl')
    def nexusId = credentials.forKey('nexusId')
    def nexusPassword = credentials.forKey('nexusPassword')
    gradle.ext.nexusUrl = nexusUrl
    gradle.ext.nexusId = nexusId
    gradle.ext.nexusPassword = nexusPassword
}

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자격증명 플러그인&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1710392561548&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - etiennestuder/gradle-credentials-plugin: Gradle plugin to store and access encrypted credentials for use in Gradle buil&quot; data-og-description=&quot;Gradle plugin to store and access encrypted credentials for use in Gradle builds. - etiennestuder/gradle-credentials-plugin&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/etiennestuder/gradle-credentials-plugin&quot; data-og-url=&quot;https://github.com/etiennestuder/gradle-credentials-plugin&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eUfdA/hyVxsNY9gQ/ma4ChqEAPqKljBjZvtefk1/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_140_1061_233&quot;&gt;&lt;a href=&quot;https://github.com/etiennestuder/gradle-credentials-plugin&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/etiennestuder/gradle-credentials-plugin&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eUfdA/hyVxsNY9gQ/ma4ChqEAPqKljBjZvtefk1/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_140_1061_233');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - etiennestuder/gradle-credentials-plugin: Gradle plugin to store and access encrypted credentials for use in Gradle buil&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Gradle plugin to store and access encrypted credentials for use in Gradle builds. - etiennestuder/gradle-credentials-plugin&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;gradle install&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1710392553455&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Gradle | Installation&quot; data-og-description=&quot;Install the Gradle build tool on Linux, macOS or Windows, either manually or using a package manager like SDKMAN! or Homebrew.&quot; data-og-host=&quot;gradle.org&quot; data-og-source-url=&quot;https://gradle.org/install/&quot; data-og-url=&quot;https://gradle.org/install/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bPdlCC/hyVxq3J3Io/g2q4cPQQKfa0LnB58MG8R1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/jpEnN/hyVAGKBJ4j/P1cD731YkNSZKaMoGTt1o1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://gradle.org/install/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gradle.org/install/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bPdlCC/hyVxq3J3Io/g2q4cPQQKfa0LnB58MG8R1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/jpEnN/hyVAGKBJ4j/P1cD731YkNSZKaMoGTt1o1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Gradle | Installation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Install the Gradle build tool on Linux, macOS or Windows, either manually or using a package manager like SDKMAN! or Homebrew.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gradle.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ETC</category>
      <category>credential</category>
      <category>global ext</category>
      <category>gradle</category>
      <category>NEXUS</category>
      <category>Repository</category>
      <category>암호화</category>
      <author>keepbang</author>
      <guid isPermaLink="true">https://keepbang.tistory.com/44</guid>
      <comments>https://keepbang.tistory.com/entry/Gradle-Nexus-Repository-library-%EC%82%AC%EC%9A%A9-Nexus-%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4-%EC%95%94%ED%98%B8%ED%99%94#entry44comment</comments>
      <pubDate>Thu, 14 Mar 2024 13:58:08 +0900</pubDate>
    </item>
    <item>
      <title>github issue label 모든 repository에 한번에 적용하기</title>
      <link>https://keepbang.tistory.com/entry/github-issue-label-%EB%AA%A8%EB%93%A0-repository%EC%97%90-%ED%95%9C%EB%B2%88%EC%97%90-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 ci 작업을 진행하던 중 issue label을 적용시켜야 하는 상황이 생겨서 label을 적용하는 스크립트와 내용을 공유하고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github 조직에서 관리자 권한은 issue label 을 한 번에 적용시킬 수 있을 거라 생각해서 찾아보니 처음 repository를 생성할 때 적용시키는 것만 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;Set the labels that will be included when a new repository is created in this organization.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2606&quot; data-origin-height=&quot;1762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVj4M6/btszUAkTasY/QrfJU6VoB7HSYAVkDEcwV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVj4M6/btszUAkTasY/QrfJU6VoB7HSYAVkDEcwV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVj4M6/btszUAkTasY/QrfJU6VoB7HSYAVkDEcwV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVj4M6%2FbtszUAkTasY%2FQrfJU6VoB7HSYAVkDEcwV0%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;2606&quot; height=&quot;1762&quot; data-origin-width=&quot;2606&quot; data-origin-height=&quot;1762&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 더 찾아보니 cmd로 api를 날려서 label을 적용 할 수 있는 &lt;code&gt;github-label-sync&lt;/code&gt; 라는걸 찾았다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한건 3가지 정도가 필요하고 위에서 말한 &lt;code&gt;github-label-sync&lt;/code&gt; 는 단일 repository만 적용 가능하길래 모든 repository에 적용할 수 있도록 쉘 스크립트를 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;code&gt;github-label-sync&lt;/code&gt; 는 npx로 구동되기 때문에 npx가 설치되어 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. public 은 상관없지만 private repository를 가져오려면 github token이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. label 정보가 담겨있는 json 파일이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 말한 3가지가 준비되면 내가 적용하고자 하는 repository에 모두 적용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. label 정보가 담겨있는 json 파일을 작성하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[
  {
    &quot;name&quot;: &quot;resolved&quot;,
    &quot;color&quot;: &quot;008672&quot;,
    &quot;description&quot;: &quot;이슈 해결&quot;
  },
  {
    &quot;name&quot;: &quot;bug&quot;,
    &quot;color&quot;: &quot;d73a4a&quot;,
    &quot;description&quot;: &quot;버그 수정&quot;
  },
  {
    &quot;name&quot;: &quot;enhancement&quot;,
    &quot;color&quot;: &quot;a2eeef&quot;, 
    &quot;description&quot;: &quot;기능 추가 및 개선&quot;
  },
  {
    &quot;name&quot;: &quot;test&quot;,
    &quot;color&quot;: &quot;cfd3d7&quot;, 
    &quot;description&quot;: &quot;테스트 및 테스트 코드 작성&quot;
  },
  {
    &quot;name&quot;: &quot;release&quot;,
    &quot;color&quot;: &quot;e4e669&quot;,
    &quot;description&quot;: &quot;Released Product&quot;
  },
  {
    &quot;name&quot;: &quot;staging&quot;,
    &quot;color&quot;: &quot;5319e7&quot;,
    &quot;description&quot;: &quot;staging server&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;위와 같이 적용하고자 하는 label 이름과 설명, 색상등을 입력하고 저장해 둔다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. Repository list 받아오기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;50개가 넘어가는 repository를 다 한 번씩 적용하기에는 무리인 듯싶어서 repository list를 받아서 처리하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 api를 사용하면 repository 목록을 받아올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;roboconf&quot;&gt;&lt;code&gt;curl -u username:{github_token} https://api.github.com/orgs/{organization_name}/repos\?per_page\=500 | jq '.[].name' | sort &amp;gt;&amp;gt; repositories_name.txt&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;해당 organization에 권한이 있는 repository들을 불러와서 파일로 생성해 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api 뒤에 per_page를 넣지 않으면 default 값인 30으로 들어가서 30개까지만 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어로 실행하면 큰따옴표가 포함된 텍스트파일이 만들어지는데 해당 파일을 열고 큰따옴표들을 없애준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 스크립트 실행하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 만들어진 파일 2개를 한 경로에 두고 아래 스크립트를 만들어서 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash

for repo in `cat ./repositories_name.txt`
do
    npx github-label-sync --access-token {github_token} --labels ./labels.json {organization_name}/$repo
    echo &quot;$repo label create complated!&quot;
done&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;label이 적용하기까지 좀 시간이 걸려서(1초 정도?) 기다리다 보면 다 적용이 된 걸 볼 수 있다.&lt;/p&gt;</description>
      <category>Git</category>
      <category>github</category>
      <category>issue</category>
      <category>Label</category>
      <category>한번에 적용하기</category>
      <author>keepbang</author>
      <guid isPermaLink="true">https://keepbang.tistory.com/43</guid>
      <comments>https://keepbang.tistory.com/entry/github-issue-label-%EB%AA%A8%EB%93%A0-repository%EC%97%90-%ED%95%9C%EB%B2%88%EC%97%90-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0#entry43comment</comments>
      <pubDate>Mon, 6 Nov 2023 16:23:26 +0900</pubDate>
    </item>
    <item>
      <title>Spring boot 3 JUnit test java.lang.NoSuchMethodError 'parseQualifiedMethodName' 이슈 해결</title>
      <link>https://keepbang.tistory.com/entry/Spring-boot-3-JUnit-test-javalangNoSuchMethodError-parseQualifiedMethodName-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;junit test&lt;/code&gt;를 진행할 때 &lt;code&gt;given/when/then&lt;/code&gt; 형태로 테스트 작성을 많이 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 입력 데이터를 다양하게 받고 싶을 때 아래와 같이 사용하곤한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;  private static Stream&amp;lt;Arguments&amp;gt; testParameter() {
    return Stream.of(
        Arguments.arguments(&quot;test&quot;, 1, new TestObject(1)),
        Arguments.arguments(&quot;test&quot;, 2, new TestObject(2))
    );
  }

  @ParameterizedTest
  @MethodSource(&quot;testParameter&quot;)
  void test(String arg1, Integer arg2, TestObject testObject) {
   ...
  }&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h1&gt;&lt;b&gt;&lt;br /&gt;이슈 사항&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&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;code&gt;Spring boot 3&lt;/code&gt; 로 변경하고 &lt;code&gt;ParameterizedTest&lt;/code&gt;와 &lt;code&gt;MethodSource&lt;/code&gt;를 사용하여 위와같이 썻는데 에러가 나왔다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;'java.lang.String[] org.junit.platform.commons.util.ReflectionUtils.parseQualifiedMethodName(java.lang.String)'
java.lang.NoSuchMethodError: 'java.lang.String[] org.junit.platform.commons.util.ReflectionUtils.parseQualifiedMethodName(java.lang.String)'
Suppressed: org.junit.platform.commons.PreconditionViolationException: Configuration error: You must configure at least one set of arguments for this @ParameterizedTest
        at org.junit.platform.commons.util.Preconditions.condition(Preconditions.java:299)
        at org.junit.jupiter.params.ParameterizedTestExtension.lambda$provideTestTemplateInvocationContexts$5(ParameterizedTestExtension.java:98)
        at java.base/java.util.stream.AbstractPipeline.close(AbstractPipeline.java:323)
        ....&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러내용으로 볼 때 &lt;code&gt;parseQualifiedMethodName&lt;/code&gt; 이라는 &lt;code&gt;method&lt;/code&gt;가 없어서 에러가나오는거같다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 에러에 출력된 메소드로 들어가보면 해당 메소드가 빨간색으로 표시된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MethodArgumentsProvider.java&lt;/code&gt; -&amp;gt; &lt;code&gt;getFactoryMethodBySimpleOrQualifiedName&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1612&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpauL4/btss92p2uZG/m4ZlGQW7lDlPHTcsZd6l10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpauL4/btss92p2uZG/m4ZlGQW7lDlPHTcsZd6l10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpauL4/btss92p2uZG/m4ZlGQW7lDlPHTcsZd6l10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdpauL4%2Fbtss92p2uZG%2Fm4ZlGQW7lDlPHTcsZd6l10%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;1612&quot; height=&quot;466&quot; data-origin-width=&quot;1612&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h1&gt;&lt;b&gt;&lt;br /&gt;해결&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 해당 메소드는 &lt;code&gt;MethodSource&lt;/code&gt;에 아무런 인자를 안넣거나 &lt;code&gt;Argument&lt;/code&gt; 메소드를 넣을 때만 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 메소드 위쪽 메소드를 보면 그건 사용 가능한것으로 보여서 그 메소드를 실행하는 곳을 가보니 클래스를 지정해서 사용하면 되는것으로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메소드의 실행 과정을보면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;getFactoryMethod&lt;/code&gt; -&amp;gt; &lt;code&gt;looksLikeAFullyQualifiedMethodName&lt;/code&gt; -&amp;gt; &lt;code&gt;getFactoryMethodByFullyQualifiedName&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;code&gt;getFactoryMethod&lt;/code&gt; 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;code&gt;looksLikeAFullyQualifiedMethodName&lt;/code&gt; 에서 메소드 이름 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 메소드 이름에 &lt;code&gt;#&lt;/code&gt;이 포함되어있으면 &lt;code&gt;true return&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. &lt;code&gt;getFactoryMethodByFullyQualifiedName&lt;/code&gt; 메소드 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1804&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2wOKA/btstaA719Hy/kTaKbWyYKtyOvYZAqIerb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2wOKA/btstaA719Hy/kTaKbWyYKtyOvYZAqIerb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2wOKA/btstaA719Hy/kTaKbWyYKtyOvYZAqIerb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2wOKA%2FbtstaA719Hy%2FkTaKbWyYKtyOvYZAqIerb0%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;1804&quot; height=&quot;484&quot; data-origin-width=&quot;1804&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q3hjl/btstk5LZPuX/xkS573wufkQ6oRd6dnef41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q3hjl/btstk5LZPuX/xkS573wufkQ6oRd6dnef41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q3hjl/btstk5LZPuX/xkS573wufkQ6oRd6dnef41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq3hjl%2Fbtstk5LZPuX%2FxkS573wufkQ6oRd6dnef41%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;1130&quot; height=&quot;228&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1468&quot; data-origin-height=&quot;236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crekQZ/btstaz2ond8/uyQvVzLn5z55gvGITbOYEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crekQZ/btstaz2ond8/uyQvVzLn5z55gvGITbOYEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crekQZ/btstaz2ond8/uyQvVzLn5z55gvGITbOYEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrekQZ%2Fbtstaz2ond8%2FuyQvVzLn5z55gvGITbOYEK%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;1468&quot; height=&quot;236&quot; data-origin-width=&quot;1468&quot; data-origin-height=&quot;236&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;getFactoryMethodByFullyQualifiedName&lt;/code&gt; 이 메소드를 탈려면 &lt;code&gt;MethodSource&lt;/code&gt;에 &lt;code&gt;class path&lt;/code&gt;까지 입력해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;  private static Stream&amp;lt;Arguments&amp;gt; testParameter() {
    return Stream.of(
        Arguments.arguments(&quot;test&quot;, 1, new TestObject(1)),
        Arguments.arguments(&quot;test&quot;, 2, new TestObject(2))
    );
  }

  @ParameterizedTest
  @MethodSource(&quot;com.demo.user.dto.UserDtoTest#testParameter&quot;)
  void test(String arg1, Integer arg2, TestObject testObject) {
   ...
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용방법은 &lt;code&gt;MethodSource(&quot;classPackagePath.className#ParameterMethodName&quot;)&lt;/code&gt; 이렇게 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h1&gt;&lt;b&gt;&lt;br /&gt;원인&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 버전문제인듯하다!&lt;/p&gt;
&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;gradle&lt;/code&gt; 버전이 업데이트 되어있고 &lt;code&gt;gradle&lt;/code&gt;에서도 이슈로 등록되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1693989884403&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;8.1 RC 1: Regression in JUnit5 parameterized tests &amp;middot; Issue #24429 &amp;middot; gradle/gradle&quot; data-og-description=&quot;Current Behavior Tests with @ParameterizedTest and @MethodSource no longer works, throwing java.lang.NoSuchMethodError: 'java.lang.String[] org.junit.platform.commons.util.ReflectionUtils.parseQual...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/gradle/gradle/issues/24429&quot; data-og-url=&quot;https://github.com/gradle/gradle/issues/24429&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eg6cJr/hyTSrhaJ3F/dZ6jM9eHgvwpG7UwSinp7K/img.png?width=1200&amp;amp;height=600&amp;amp;face=75_67_1081_526&quot;&gt;&lt;a href=&quot;https://github.com/gradle/gradle/issues/24429&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/gradle/gradle/issues/24429&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eg6cJr/hyTSrhaJ3F/dZ6jM9eHgvwpG7UwSinp7K/img.png?width=1200&amp;amp;height=600&amp;amp;face=75_67_1081_526');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;8.1 RC 1: Regression in JUnit5 parameterized tests &amp;middot; Issue #24429 &amp;middot; gradle/gradle&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Current Behavior Tests with @ParameterizedTest and @MethodSource no longer works, throwing java.lang.NoSuchMethodError: 'java.lang.String[] org.junit.platform.commons.util.ReflectionUtils.parseQual...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에 &lt;code&gt;gradle&lt;/code&gt;버전을 바꿔봤는데 또 에러가남... 일단 해결 방법을 알았으니 위에 방법대로 해야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 프로젝트만들때에는 처음부터 gradle버전을 최신버전으로 해야할듯싶다.&lt;/p&gt;</description>
      <category>Error  resolution</category>
      <category>error</category>
      <category>gradle</category>
      <category>MethodSource</category>
      <category>NoSuchMethodError</category>
      <category>ParameterizedTest</category>
      <category>parseQualifiedMethodName</category>
      <category>spring boot 3</category>
      <author>keepbang</author>
      <guid isPermaLink="true">https://keepbang.tistory.com/42</guid>
      <comments>https://keepbang.tistory.com/entry/Spring-boot-3-JUnit-test-javalangNoSuchMethodError-parseQualifiedMethodName-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0#entry42comment</comments>
      <pubDate>Wed, 6 Sep 2023 17:48:02 +0900</pubDate>
    </item>
    <item>
      <title>[가상면접 사례로 배우는 대규모 시스템 설계] 처리율 제한 장치</title>
      <link>https://keepbang.tistory.com/entry/%EA%B0%80%EC%83%81%EB%A9%B4%EC%A0%91-%EC%82%AC%EB%A1%80%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%B2%98%EB%A6%AC%EC%9C%A8-%EC%A0%9C%ED%95%9C-%EC%9E%A5%EC%B9%98</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;가상면접 사례로 배우는 대규모 시스템 설계 책에서 4장 처리율 제한 장치에 대한 내용을 정리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;처리율 제한 장치&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리율 제한 장치는 서버에서 특정 임계치를 정해놓고 해당 임계치를 넘는 요청이 클라이언트나 서비스에서 들어오면 해당 요청을 중단시키는 역할을한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;처리율 제한 장치를 사용하는 이유는 무엇일까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dos공격에 의한 자원 고갈을 방지&lt;/li&gt;
&lt;li&gt;비용 절감 효과.&lt;/li&gt;
&lt;li&gt;서버 과부하를 막음.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하는 서버나 시스템 규모에 따라서 처리율 제한 장치의 알고리즘이나 설계가 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 아래에 요구사항에 따라 처리율 제한 장치를 어떻게 사용할 지 정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;사용하는 시스템에 따른 처리율 제한 장치의 요구사항을 정리한다.&lt;/b&gt;&lt;b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;시스템 규모나 환경, 제어 규칙, 제한 장치의 위치, 제한된 요청의 알림 여부등의 내용들이 포함 되어 있어야 한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;처리율 제한 장치를 어디에 둘것인가?&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;클라이언트는 요청이 쉽게 위변조가 가능하기도 하고 모든 클라이언트의 요청을 통제하는 것도 어려울 수 있어서 서버측에 구현하는 것이 올바르다고 생각한다.&lt;br /&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;서버에서 구현하는 방법으로는 클라이언트와 서버 사이에 미들웨어를 만들어서 구현하는 방법과 API서버에 구현하는 방법이 있다.&lt;br /&gt;&lt;br /&gt;마이크로서비스에서는 API 게이트웨이라고 불리는 컴포넌트에 구혀노디는데 API 게이트웨이는 처리율 제한, ssl종단, 사용자 인증, ip허용 목록 관리등을 지원하는 완전 위탁관리형 서비스이다.&lt;br /&gt;&lt;br /&gt;만약 서버측에서 모든것을 구현한다고하면 알고리즘 선택은 다양하게 할 수 있다. 하지만 3자가 제공하는 게이트웨이를 사용한다면 알고리즘 선택이나 처리방법이 제한 될 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;처리율 제한 알고리즘 선택&lt;br /&gt;&lt;/b&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;토큰 버킷&lt;/li&gt;
&lt;li&gt;누출 버킷&lt;/li&gt;
&lt;li&gt;고정 윈도 카운터&lt;/li&gt;
&lt;li&gt;이동 윈도 로그&lt;/li&gt;
&lt;li&gt;이동 윈도 카운터&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;br /&gt;처리율 제한 알고리즘&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;br /&gt;토큰 버킷 알고리즘&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지정된 용량을 갖는 버킷을 지정해 놓고 사전 설정된 양의 토큰이 주기적으로 추가된다.&lt;/li&gt;
&lt;li&gt;설정된 양을 초과해서 채워지지는 않는다.&lt;/li&gt;
&lt;li&gt;요청이 하나 처리될때마다 토큰이 하나씩 사용되며 토큰이 없다면 해당 요청은 버려진다.&lt;/li&gt;
&lt;li&gt;이 토큰 버킷 알고리즘은 버킷 크기와 토큰 공급률이라는 두개의 인자가 존재한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;버킷 크기&lt;/b&gt; : 버킷에 담을 수 있는 토큰의 최대 개수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;토큰 공급률&lt;/b&gt; : 초당 버킷에 공급되는 토큰 수&lt;/li&gt;
&lt;li&gt;통상적으로는 API엔드포인트마다 별도의 버킷을 설정함&lt;/li&gt;
&lt;li&gt;장점&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점&lt;br /&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버킷 크기와 토큰 공급률을 적절하게 튜닝하는 것이 까다로운 일이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;463&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8i9zE/btrVUj4XaSa/0JYyKLNu5ekkjUGZ8lHXq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8i9zE/btrVUj4XaSa/0JYyKLNu5ekkjUGZ8lHXq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8i9zE/btrVUj4XaSa/0JYyKLNu5ekkjUGZ8lHXq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8i9zE%2FbtrVUj4XaSa%2F0JYyKLNu5ekkjUGZ8lHXq0%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;422&quot; height=&quot;463&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;463&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;누출 버킷 알고리즘&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;누출 버킷은 토큰 버킷과 비슷하지만 처리율(처리되는 사이즈)이 고정되어 있다는 점이 다르다.&lt;/li&gt;
&lt;li&gt;보통 FIFO큐로 구현된다.&lt;/li&gt;
&lt;li&gt;요청이 도착하면 큐에 쌓아두고 지정된 시간마다 큐에 있는 요청을 꺼내서 처리한다.&lt;/li&gt;
&lt;li&gt;만약 큐가 가득 차 있으면 새로운 요청은 버려진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;버킷 크기&lt;/b&gt; : 큐 사이즈와 같은 값. 처리될 항목들이 보관됨.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;처리율&lt;/b&gt; : 지정된 시간당 몇개의 항목을 처리할 지 지정하는 값, 초 단위초 표현&lt;/li&gt;
&lt;li&gt;장점&lt;br /&gt;&lt;br /&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점&lt;br /&gt;&lt;br /&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;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;569&quot; data-origin-height=&quot;205&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjcAb7/btrVXBXJrxt/QYNDysnOGpnA186pOiaL3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjcAb7/btrVXBXJrxt/QYNDysnOGpnA186pOiaL3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjcAb7/btrVXBXJrxt/QYNDysnOGpnA186pOiaL3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjcAb7%2FbtrVXBXJrxt%2FQYNDysnOGpnA186pOiaL3K%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;569&quot; height=&quot;205&quot; data-origin-width=&quot;569&quot; data-origin-height=&quot;205&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;고정 윈도 카운터 알고리즘&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;타임라인을 고정된 윈도로 나누고 임계치를 둬서 임계치 만큼 요청을 처리하는 알고리즘이다.&lt;/li&gt;
&lt;li&gt;카운터를 사용하는데 요청이 들어올때마다 카운터를 1씩 증가시키고 카운터가 임계치를 넘으면 새로운 요청은 지정된 시간이 될때가지 버려지게 된다.&lt;/li&gt;
&lt;li&gt;지정된 시간이 되면 카운터를 초기화 한다.&lt;/li&gt;
&lt;li&gt;장점&lt;br /&gt;&lt;br /&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;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;698&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yAIac/btrVUxvpGl8/QXBezKD2Xc5p5kEECrSmPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yAIac/btrVUxvpGl8/QXBezKD2Xc5p5kEECrSmPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yAIac/btrVUxvpGl8/QXBezKD2Xc5p5kEECrSmPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyAIac%2FbtrVUxvpGl8%2FQXBezKD2Xc5p5kEECrSmPk%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;513&quot; height=&quot;698&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;698&quot;/&gt;&lt;/span&gt;&lt;/figure&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;br /&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;윈도 경계 부분에서 많은 트래픽이 몰리게 되면 기대했던 시스템의 처리 한도보다 더 많은 양의 요청을 처리하게 된다.&lt;br /&gt;&lt;br /&gt;ex) 1분당 최대 5개의 요청을 처리하는 시스템에서 10시 1분 30초부터 2분까지 5개의 요청이 처리되었고 10시 2분부터 2분 30초까지 5개의 요청이 들어왔다.&lt;br /&gt;윈도 위치를 변경해서 10시 1분 30초부터 1분을 살펴보면 10시 2분 30초까지 10개의 요청이 처리된다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nI6gc/btrVT5e1q9B/rPq4kViTEpUPyprcpKllV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nI6gc/btrVT5e1q9B/rPq4kViTEpUPyprcpKllV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nI6gc/btrVT5e1q9B/rPq4kViTEpUPyprcpKllV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnI6gc%2FbtrVT5e1q9B%2FrPq4kViTEpUPyprcpKllV0%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;433&quot; height=&quot;208&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;208&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이동 윈도 로깅 알고리즘&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에 고정 윈도 카운터 알고리즘의 단점을 해결한 방식이다.&lt;/li&gt;
&lt;li&gt;요청의 타임스템프를 추적하고 특정 시간마다 새로운 윈도를 시작한다. 타임스템프는 보통 레디스의 정렬 집합같은 캐시에 보관한다.&lt;/li&gt;
&lt;li&gt;새 요청이 들어오면 만료된 타임스템프는 제거한다. 만료된 타임스템프는 현재 윈도가 시작된 시간보다 전에 들어온 타임스템프를 말한다.&lt;/li&gt;
&lt;li&gt;새로운 요청의 타임스템프를 로그에 추가한다.&lt;/li&gt;
&lt;li&gt;로그의 크기가 허용치보다 작거나 같으면 시스템에 전달한다, 그렇지 않으면 처리를 거부한다.&lt;/li&gt;
&lt;li&gt;장점&lt;br /&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어느 순간의 윈도(시간 간격)를 봐도 처리 되는 요청의 수는 동일하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점&lt;br /&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;거부된 요청의 타임스템프도 보관되기 때문에 메모리 측면에서 효율적이지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;393&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdD3Xb/btrVYjK8d1z/bautppMFWMkAPc3JtJmLN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdD3Xb/btrVYjK8d1z/bautppMFWMkAPc3JtJmLN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdD3Xb/btrVYjK8d1z/bautppMFWMkAPc3JtJmLN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdD3Xb%2FbtrVYjK8d1z%2FbautppMFWMkAPc3JtJmLN0%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;335&quot; height=&quot;285&quot; data-origin-width=&quot;393&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이동 윈도 카운터 알고리즘&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에서 설명한 두 윈도 알고리즘을 결합한 알고리즘으로 특정 시간 간격에 처리율을 계산해서 요청을 처리한다.&lt;/li&gt;
&lt;li&gt;&lt;u&gt;&lt;b&gt;현재 1분간 요청 수 + 직전 1분간의 요청 수 * 이동 윈도와 직전 1분이 겹치는 비율&lt;/b&gt;&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;장점&lt;br /&gt;&lt;br /&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점&lt;br /&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직전 시간대에 도착한 요청이 균등하게 분포되어 있다고 가정한 상태에서 추정치를 계산하기 때문에 다소 느슨하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wZr58/btrV1asnJbS/QIlO2ePr2Sl4svUm7SR4m1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wZr58/btrV1asnJbS/QIlO2ePr2Sl4svUm7SR4m1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wZr58/btrV1asnJbS/QIlO2ePr2Sl4svUm7SR4m1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwZr58%2FbtrV1asnJbS%2FQIlO2ePr2Sl4svUm7SR4m1%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;439&quot; height=&quot;276&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;276&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;생각해 볼 수 있는 이슈들&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;처리율 제한 장치에서 발생하는 카운터 정보는 어디에 저장되는가?&lt;br /&gt;&lt;/b&gt;&lt;br /&gt;데이터 베이스는 디스크 접근 때문에 느려서 사용하기가 어렵고 메모리상 접근하는 캐시가 바람직하다고 생각한다.&lt;br /&gt;대표적으로 &lt;code&gt;redis&lt;/code&gt;가 있는데 처리율 제한 장치를 구현할 때 자주 사용되는 메모리 기반 저장장치이고 &lt;code&gt;INCR&lt;/code&gt;과 &lt;code&gt;EXPIRE&lt;/code&gt;라는 두가지 명령어를 지원한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;code&gt;INCR&lt;/code&gt; : 메모리에 저장된 카운터값을 1 증가시킨다.&lt;br /&gt;&lt;code&gt;EXPIRE&lt;/code&gt; : 카운터에 타임아웃값을 설정한다. 설정된 시간이 지나면 카운터는 자동으로 삭제된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;처리가 제한된 요청들은 어떻게 처리되는가?&lt;br /&gt;&lt;/b&gt;&lt;br /&gt;사용자가 429응답(&lt;code&gt;too many requests&lt;/code&gt;)을 보낼 수 있고, 나중에 처리하기 위해 큐에 저장 할 수도 있다.&lt;br /&gt;&lt;br /&gt;HTTP 응답 헤더에 정보를 추가해서 클라이언트에 보낼 수 있다.&lt;br /&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;X-Ratelimit-Remaining: 윈도 내에 남은 처리 가능 요청의 수&lt;/li&gt;
&lt;li&gt;X-Ratelimit-Limit: 매 윈도마다 클라이언트가 전송할 수 있는 요청의 수&lt;/li&gt;
&lt;li&gt;X-Ratelimit-Retry-After: 한도 제한에 걸리지 않으려면 몇 초 뒤에 요청을 다시 보내야 하는지 알림.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분산환경에서의 구현&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;경쟁 조건(race condition)&lt;br /&gt;&lt;br /&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;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9okaS/btrV1Uis4ZT/FG69H4BGF9WWd9C8zhGRHK/img.png&quot; data-image-src=&quot;https://blog.kakaocdn.net/dn/9okaS/btrV1Uis4ZT/FG69H4BGF9WWd9C8zhGRHK/img.png&quot; data-origin-width=&quot;486&quot; data-origin-height=&quot;265&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;동기화(synchronization)&lt;br /&gt;&lt;br /&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;Redis와 같은 중앙 집중형 데이터 저장소를 써서 해결할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;모니터링을 통한 확인&lt;br /&gt;&lt;/b&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&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;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;</description>
      <category>ETC</category>
      <category>가상면접</category>
      <category>대규모시스템설계</category>
      <category>처리율제한장치</category>
      <author>keepbang</author>
      <guid isPermaLink="true">https://keepbang.tistory.com/40</guid>
      <comments>https://keepbang.tistory.com/entry/%EA%B0%80%EC%83%81%EB%A9%B4%EC%A0%91-%EC%82%AC%EB%A1%80%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%B2%98%EB%A6%AC%EC%9C%A8-%EC%A0%9C%ED%95%9C-%EC%9E%A5%EC%B9%98#entry40comment</comments>
      <pubDate>Thu, 12 Jan 2023 10:35:31 +0900</pubDate>
    </item>
  </channel>
</rss>